-
-
Notifications
You must be signed in to change notification settings - Fork 631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support pex_binary addresses in setup_py(entry_points) along with dep… #12414
Changes from 2 commits
a8e90cc
4ab55a1
fc73526
262b4e6
43197b1
85b02c7
0bcccc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,14 +16,11 @@ | |
from pants.backend.python.macros.python_artifact import PythonArtifact | ||
from pants.backend.python.subsystems.setuptools import Setuptools | ||
from pants.backend.python.target_types import ( | ||
PexEntryPointField, | ||
PythonDistributionEntryPointsField, | ||
PythonProvidesField, | ||
PythonRequirementsField, | ||
PythonSources, | ||
ResolvedPexEntryPoint, | ||
ResolvedPythonDistributionEntryPoints, | ||
ResolvePexEntryPointRequest, | ||
ResolvePythonDistributionEntryPointsRequest, | ||
SetupPyCommandsField, | ||
) | ||
|
@@ -563,72 +560,46 @@ async def generate_chroot(request: SetupPyChrootRequest) -> SetupPyChroot: | |
} | ||
) | ||
|
||
# Collect any `pex_binary` targets from `setup_py().with_binaries()` | ||
key_to_binary_spec = exported_target.provides.binaries | ||
binaries = await Get( | ||
Targets, UnparsedAddressInputs(key_to_binary_spec.values(), owning_address=target.address) | ||
) | ||
entry_point_requests = [] | ||
for binary in binaries: | ||
if not binary.has_field(PexEntryPointField): | ||
raise InvalidEntryPoint( | ||
"Expected addresses to `pex_binary` targets in `.with_binaries()` for the " | ||
f"`provides` field for {exported_addr}, but found {binary.address} with target " | ||
f"type {binary.alias}." | ||
) | ||
entry_point = binary[PexEntryPointField].value | ||
url = "https://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point" | ||
if not entry_point.function: | ||
raise InvalidEntryPoint( | ||
"Every `pex_binary` used in `with_binaries()` for the `provides()` field for " | ||
f"{exported_addr} must end in the format `:my_func` for the `entry_point` field, " | ||
f"but {binary.address} set it to {entry_point.spec!r}. For example, set " | ||
f"`entry_point='{entry_point.module}:main'. See {url}." | ||
) | ||
entry_point_requests.append(ResolvePexEntryPointRequest(binary[PexEntryPointField])) | ||
|
||
binary_entry_points = await MultiGet( | ||
Get(ResolvedPexEntryPoint, ResolvePexEntryPointRequest, request) | ||
for request in entry_point_requests | ||
) | ||
|
||
entry_points_from_with_binaries = { | ||
key: binary_entry_point.val.spec | ||
for key, binary_entry_point in zip(key_to_binary_spec.keys(), binary_entry_points) | ||
if binary_entry_point.val is not None | ||
} | ||
|
||
# Collect entry points from `python_distribution(entry_points=...)` | ||
entry_points_from_field = {} | ||
if exported_target.target.has_field(PythonDistributionEntryPointsField): | ||
entry_points_field = await Get( | ||
# Resolve entry points from python_distribution(entry_points=...) and from | ||
# python_distribution(provides=setup_py(entry_points=...).with_binaries(...) | ||
entry_points_field, provides_field = await MultiGet( | ||
Get( | ||
ResolvedPythonDistributionEntryPoints, | ||
ResolvePythonDistributionEntryPointsRequest( | ||
exported_target.target[PythonDistributionEntryPointsField] | ||
entry_points_field=exported_target.target.get(PythonDistributionEntryPointsField) | ||
), | ||
) | ||
), | ||
Get( | ||
ResolvedPythonDistributionEntryPoints, | ||
ResolvePythonDistributionEntryPointsRequest( | ||
provides_field=exported_target.target.get(PythonProvidesField) | ||
), | ||
), | ||
) | ||
|
||
entry_points_from_field = { | ||
category: {ep_name: ep_val.entry_point.spec for ep_name, ep_val in entry_points.items()} | ||
for category, entry_points in entry_points_field.val.items() | ||
} | ||
entry_points_from_field = { | ||
category: {ep_name: ep_val.entry_point.spec for ep_name, ep_val in entry_points.items()} | ||
for category, entry_points in entry_points_field.val.items() | ||
} | ||
|
||
# Collect any entry points from the setup_py() object. Note that this was already normalized in | ||
# `python_artifacts.py`. | ||
entry_points_from_provides = setup_kwargs.get("entry_points", {}) | ||
entry_points_from_provides = { | ||
category: {ep_name: ep_val.entry_point.spec for ep_name, ep_val in entry_points.items()} | ||
for category, entry_points in provides_field.val.items() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally, we prefer comprehensions like this. But here, it looks like you're iterating over
Here, I think you would have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh.. it is not the same Could however have a little helper function to reduce code duplication, and this would become super clear :) |
||
|
||
# Gather entry points with source description for any error messages when merging them. | ||
entry_point_sources = { | ||
f"{exported_addr}'s field `provides=setup_py().with_binaries()`": { | ||
"console_scripts": entry_points_from_with_binaries | ||
}, | ||
f"{exported_addr}'s field `entry_points`": entry_points_from_field, | ||
f"{exported_addr}'s field `provides=setup_py(..., entry_points={...})`": entry_points_from_provides, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is totally reasonable, esp. becauase with_binaries() is now deprecated. Thanks for the disclaimer! |
||
f"{exported_addr}'s field `provides=setup_py()`": entry_points_from_provides, | ||
} | ||
|
||
# Merge all collected entry points and add them to the dist's entry points. | ||
entry_points = merge_entry_points(*list(entry_point_sources.items())) | ||
if entry_points: | ||
setup_kwargs["entry_points"] = entry_points | ||
all_entry_points = merge_entry_points(*list(entry_point_sources.items())) | ||
if all_entry_points: | ||
setup_kwargs["entry_points"] = { | ||
category: [f"{name} = {entry_point}" for name, entry_point in entry_points.items()] | ||
for category, entry_points in all_entry_points.items() | ||
} | ||
|
||
# Generate the setup script. | ||
setup_py_content = SETUP_BOILERPLATE.format( | ||
|
@@ -1011,7 +982,7 @@ def has_args(call_node: ast.Call, required_arg_ids: Tuple[str, ...]) -> bool: | |
|
||
def merge_entry_points( | ||
*all_entry_points_with_descriptions_of_source: Tuple[str, Dict[str, Dict[str, str]]] | ||
) -> Dict[str, List[str]]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed signature, to stay true to our "normalized" structure for entry points. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good change! |
||
) -> Dict[str, Dict[str, str]]: | ||
"""Merge all entry points, throwing ValueError if there are any conflicts.""" | ||
merged = cast( | ||
# this gives us a two level deep defaultdict with the inner values being of list type | ||
|
@@ -1024,22 +995,22 @@ def merge_entry_points( | |
for ep_name, entry_point in entry_points.items(): | ||
merged[category][ep_name].append((description_of_source, entry_point)) | ||
|
||
def _merge_entry_point( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. adjusted name to be true to what it does |
||
def _check_entry_point_single_source( | ||
category: str, name: str, entry_points_with_source: List[Tuple[str, str]] | ||
): | ||
) -> Tuple[str, str]: | ||
if len(entry_points_with_source) > 1: | ||
raise ValueError( | ||
f"Multiple entry_points registered for {category} {name} in: " | ||
f"{', '.join(ep_source for ep_source, _ in entry_points_with_source)}" | ||
) | ||
for _, entry_point in entry_points_with_source: | ||
return f"{name}={entry_point}" | ||
_, entry_point = entry_points_with_source[0] | ||
return name, entry_point | ||
Comment on lines
-1035
to
+1009
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent change! This is much more obvious what it does. Nice :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the most rewarding part to me, tbh. Learning to write comprehensible code. Normally, it's just I that have to grok my code, and my peers are lost in the mist of what it is supposed to do in the first place, so there is no real pressure to be super clear with the intentions. But, it will be a lot easier to maintain in the long run, so well worth spending a few minutes to get there. So, that's what I'm happy about, to be able to get there with more ease than before.. ;) |
||
|
||
return { | ||
category: [ | ||
_merge_entry_point(category, name, entry_points_with_source) | ||
category: dict( | ||
_check_entry_point_single_source(category, name, entry_points_with_source) | ||
for name, entry_points_with_source in merged_entry_points.items() | ||
] | ||
) | ||
for category, merged_entry_points in merged.items() | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,15 +230,15 @@ def test_merge_entry_points() -> None: | |
}, | ||
} | ||
expect = { | ||
"console_scripts": [ | ||
"foo_tool=foo.bar.baz:Tool.main", | ||
"foo_qux=foo.baz.qux", | ||
"foo_main=foo.qux.bin:main", | ||
], | ||
"foo_plugins": [ | ||
"qux=foo.qux", | ||
"foo-bar=foo.bar:plugin", | ||
], | ||
"console_scripts": { | ||
"foo_tool": "foo.bar.baz:Tool.main", | ||
"foo_qux": "foo.baz.qux", | ||
"foo_main": "foo.qux.bin:main", | ||
}, | ||
"foo_plugins": { | ||
"qux": "foo.qux", | ||
"foo-bar": "foo.bar:plugin", | ||
}, | ||
} | ||
assert merge_entry_points(*list(sources.items())) == expect | ||
|
||
|
@@ -346,7 +346,7 @@ def test_generate_chroot(chroot_rule_runner: RuleRunner) -> None: | |
"namespace_packages": ("foo",), | ||
"package_data": {"foo": ("resources/js/code.js",)}, | ||
"install_requires": ("baz==1.1.1",), | ||
"entry_points": {"console_scripts": ["foo_main=foo.qux.bin:main"]}, | ||
"entry_points": {"console_scripts": ["foo_main = foo.qux.bin:main"]}, | ||
}, | ||
Address("src/python/foo", target_name="foo-dist"), | ||
) | ||
|
@@ -384,7 +384,8 @@ def test_generate_chroot_entry_points(chroot_rule_runner: RuleRunner) -> None: | |
name='foo', version='1.2.3', | ||
entry_points={ | ||
"console_scripts":{ | ||
"foo_qux":"foo.baz.qux", | ||
"foo_qux":"foo.baz.qux:main", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to see that we catch bugs in the test suite :P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, before it was inferring a dep on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, there are too many There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep, agreed. I personally don't like foo and friends. https://www.techyourchance.com/never-use-foo-bar-baz/ Feel free to update in another change if you'd like :) |
||
"foo_bin":":foo-bin", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. second commit was to pick this up, which it wasn't before.. |
||
}, | ||
"foo_plugins":[ | ||
"foo-bar=foo.bar:plugin", | ||
|
@@ -422,16 +423,17 @@ def test_generate_chroot_entry_points(chroot_rule_runner: RuleRunner) -> None: | |
"install_requires": tuple(), | ||
"entry_points": { | ||
"console_scripts": [ | ||
"foo_main=foo.qux.bin:main", | ||
"foo_tool=foo.bar.baz:Tool.main", | ||
"bin_tool=foo.qux.bin:main", | ||
"bin_tool2=foo.qux.bin:main", | ||
"hello=foo.bin:main", | ||
"foo_qux=foo.baz.qux", | ||
"foo_tool = foo.bar.baz:Tool.main", | ||
"bin_tool = foo.qux.bin:main", | ||
"bin_tool2 = foo.qux.bin:main", | ||
"hello = foo.bin:main", | ||
"foo_qux = foo.baz.qux:main", | ||
"foo_bin = foo.bin:main", | ||
"foo_main = foo.qux.bin:main", | ||
], | ||
"foo_plugins": [ | ||
"qux=foo.qux", | ||
"foo-bar=foo.bar:plugin", | ||
"qux = foo.qux", | ||
"foo-bar = foo.bar:plugin", | ||
], | ||
}, | ||
}, | ||
|
@@ -516,7 +518,7 @@ def test_binary_shorthand(chroot_rule_runner: RuleRunner) -> None: | |
"namespace_packages": (), | ||
"install_requires": (), | ||
"package_data": {}, | ||
"entry_points": {"console_scripts": ["foo=project.app:func"]}, | ||
"entry_points": {"console_scripts": ["foo = project.app:func"]}, | ||
}, | ||
Address("src/python/project", target_name="dist"), | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -706,7 +706,7 @@ class PythonDistributionDependencies(Dependencies): | |
supports_transitive_excludes = True | ||
|
||
|
||
class PythonProvidesField(ScalarField, ProvidesField): | ||
class PythonProvidesField(ScalarField, ProvidesField, AsyncFieldMixin): | ||
expected_type = PythonArtifact | ||
expected_type_help = "setup_py(name='my-dist', **kwargs)" | ||
value: PythonArtifact | ||
|
@@ -808,9 +808,17 @@ def pex_binary_addresses(self) -> Addresses: | |
@dataclass(frozen=True) | ||
class ResolvePythonDistributionEntryPointsRequest: | ||
"""Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address | ||
that should be resolved into a setuptools entry point.""" | ||
that should be resolved into a setuptools entry point. | ||
|
||
entry_points_field: PythonDistributionEntryPointsField | ||
If the `entry_points_field` is present, inspect the specified entry points. | ||
If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`. | ||
|
||
This is to support inspecting one or the other depending on use case, using the same | ||
logic for resolving pex_binary addresses etc. | ||
""" | ||
|
||
entry_points_field: Optional[PythonDistributionEntryPointsField] = None | ||
provides_field: Optional[PythonProvidesField] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I extended the support for fields to look at for resolving entry points, so we get the same logic applied consistently across all different ways to provide entry points. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be extra defensive, you could add a |
||
|
||
|
||
class SetupPyCommandsField(StringSequenceField): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got to clean up a lot of code, as that functionality moved to resolve entry points rule.