-
-
Notifications
You must be signed in to change notification settings - Fork 677
Expand file tree
/
Copy pathpy_console_script_gen.py
More file actions
189 lines (158 loc) · 6.2 KB
/
py_console_script_gen.py
File metadata and controls
189 lines (158 loc) · 6.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# Copyright 2023 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
console_script generator from entry_points.txt contents.
For Python versions earlier than 3.11 and for earlier bazel versions than 7.0 we need to workaround the issue of
sys.path[0] breaking out of the runfiles tree see the following for more context:
* https://github.com/bazel-contrib/rules_python/issues/382
* https://github.com/bazelbuild/bazel/pull/15701
In affected bazel and Python versions we see in programs such as `flake8`, `pylint` or `pytest` errors because the
first `sys.path` element is outside the `runfiles` directory and if the `name` of the `py_binary` is the same as
the program name, then the script (e.g. `flake8`) will start failing whilst trying to import its own internals from
the bazel entrypoint script.
The mitigation strategy is to remove the first entry in the `sys.path` if it does not have `.runfiles` and it seems
to fix the behaviour of console_scripts under `bazel run`.
This would not happen if we created a console_script binary in the root of an external repository, e.g.
`@pypi_pylint//` because the path for the external repository is already in the runfiles directory.
"""
from __future__ import annotations
import argparse
import configparser
import pathlib
import re
import sys
import textwrap
_ENTRY_POINTS_TXT = "entry_points.txt"
_TEMPLATE = """\
{shebang}import sys
# See @rules_python//python/private:py_console_script_gen.py for explanation
if getattr(sys.flags, "safe_path", False):
# We are running on Python 3.11 and we don't need this workaround
pass
elif ".runfiles" not in sys.path[0]:
sys.path = sys.path[1:]
try:
from {module} import {attr}
except ImportError:
entries = "\\n".join(sys.path)
print("Printing sys.path entries for easier debugging:", file=sys.stderr)
print(f"sys.path is:\\n{{entries}}", file=sys.stderr)
raise
if __name__ == "__main__":
sys.exit({entry_point}())
"""
class EntryPointsParser(configparser.ConfigParser):
"""A class handling entry_points.txt
See https://packaging.python.org/en/latest/specifications/entry-points/
"""
optionxform = staticmethod(str)
def _guess_entry_point(guess: str, console_scripts: dict[string, string]) -> str | None:
for key, candidate in console_scripts.items():
if guess == key:
return candidate
def run(
*,
entry_points: pathlib.Path,
out: pathlib.Path,
console_script: str,
console_script_guess: str,
shebang: str,
):
"""Run the generator
Args:
entry_points: The entry_points.txt file to be parsed.
out: The output file.
console_script: The console_script entry in the entry_points.txt file.
console_script_guess: The string used for guessing the console_script if it is not provided.
shebang: The shebang to use for the entry point python file. Defaults to empty string (no shebang).
"""
config = EntryPointsParser()
config.read(entry_points)
try:
console_scripts = dict(config["console_scripts"])
except KeyError:
raise RuntimeError(
f"The package does not provide any console_scripts in its {_ENTRY_POINTS_TXT}"
)
if console_script:
try:
entry_point = console_scripts[console_script]
except KeyError:
available = ", ".join(sorted(console_scripts.keys()))
raise RuntimeError(
f"The console_script '{console_script}' was not found, only the following are available: {available}"
) from None
else:
# Get rid of the extension and the common prefix
entry_point = _guess_entry_point(
guess=console_script_guess,
console_scripts=console_scripts,
)
if not entry_point:
available = ", ".join(sorted(console_scripts.keys()))
raise RuntimeError(
f"Tried to guess that you wanted '{console_script_guess}', but could not find it. "
f"Please select one of the following console scripts: {available}"
) from None
module, _, entry_point = entry_point.rpartition(":")
attr, _, _ = entry_point.partition(".")
# TODO: handle 'extras' in entry_point generation
# See https://github.com/bazel-contrib/rules_python/issues/1383
# See https://packaging.python.org/en/latest/specifications/entry-points/
with open(out, "w") as f:
f.write(
_TEMPLATE.format(
shebang=f"{shebang}\n" if shebang else "",
module=module,
attr=attr,
entry_point=entry_point,
),
)
def main():
parser = argparse.ArgumentParser(description="console_script generator")
parser.add_argument(
"--console-script",
help="The console_script to generate the entry_point template for.",
)
parser.add_argument(
"--console-script-guess",
required=True,
help="The string used for guessing the console_script if it is not provided.",
)
parser.add_argument(
"--shebang",
help="The shebang to use for the entry point python file.",
)
parser.add_argument(
"entry_points",
metavar="ENTRY_POINTS_TXT",
type=pathlib.Path,
help="The entry_points.txt within the dist-info of a PyPI wheel",
)
parser.add_argument(
"out",
type=pathlib.Path,
metavar="OUT",
help="The output file.",
)
args = parser.parse_args()
run(
entry_points=args.entry_points,
out=args.out,
console_script=args.console_script,
console_script_guess=args.console_script_guess,
shebang=args.shebang,
)
if __name__ == "__main__":
main()