-
-
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
Support pex_binary addresses in setup_py(entry_points) along with dep… #12414
Conversation
…endency inference. Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se> # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
@@ -384,7 +384,7 @@ 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
So, before it was inferring a dep on the python_library
and now it does it on the pex_binary
? Cool!
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.
Well, there are too many foo
, bar
, baz
and qux
in this test. But no, I don't think so. It refers to a module (that is not part of any target, so "orphan") but being a console script, it must point at a method, not just a module.
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.
Well, there are too many foo, bar, baz and qux in this test
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 :)
""" | ||
|
||
entry_points_field: Optional[PythonDistributionEntryPointsField] = None | ||
provides_field: Optional[PythonProvidesField] = None |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
To be extra defensive, you could add a def __post__init()
that raises AssertionError
if both are unspecified.
return ResolvedPythonDistributionEntryPoints() | ||
|
||
address = request.entry_points_field.address | ||
classified_entry_points = list(_classify_entry_points(field_value)) | ||
classified_entry_points = list(_classify_entry_points(all_entry_points)) |
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.
Had a bit of a struggle with MyPy here (not yet that experienced with python type hints..)
So ended up resolving to using a few intermediary variables to overcome the Optional
s. If there is a better way....
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.
One way to do this is to set all_entry_points: MyType[Foo, Bar]
at the top of this whole conditional block. Note that you don't set the actual value to anything. It's only to let MyPy know "hey I'm gonna have this variable with this type".
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.
Will try that next time. Not feeling like digging around any more in this as it is working, unless it's too painful for you eyes ;)
@@ -209,7 +239,7 @@ async def resolve_python_distribution_entry_points( | |||
# Check that we only have targets with a pex entry_point field. | |||
for target in targets: | |||
if not target.has_field(PexEntryPointField): | |||
raise InvalidFieldException( | |||
raise InvalidEntryPoint( |
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.
This got my head scratching. I'd like to be consistent, but some code raise InvalidFieldException, where as other InvalidEntryPoint... feels like InvalidEntryPoint ought to inherit from InvalidFieldException too, at least... so a catch for field exceptions would catch this.
Any way, I went with keeping the tests as is, as that is the user facing interface. But I think we could make a do-over here to make it more consistent. ;)
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.
That sounds good to have InvalidEntryPoint
subclass InvalidFieldException
, definitely. And makes sense if you want to do in a followup to keep this PR better scoped.
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.
Issue #12430
return InjectedDependencies( | ||
Addresses(module_owners) + with_binaries_addresses + all_entry_points.pex_binary_addresses |
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.
with binaries moved to the resolve entry points rule.
Address("project", relative_file_path="app.py", target_name="my_library"), | ||
], | ||
) | ||
|
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.
test case driving the change.
Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se> # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
), | ||
) |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
The with_binaries()
are now merged during resolve of the provides field, so we're left with the two resolves for each field, entry points and provides, respectively. The downside is, that when there is a conflict with a entry point from the distributions field, we can't say if it is with setup_py().with_binaries or with setup_py(entry_points) any more..
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.
That is totally reasonable, esp. becauase with_binaries() is now deprecated. Thanks for the disclaimer!
@@ -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 comment
The 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 List[str]
form for entry points is applied at the last moment when outputting for the setup kwargs.
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.
Good change!
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
adjusted name to be true to what it does
|
||
with_binaries = request.provides_field.value.binaries | ||
if with_binaries: | ||
all_entry_points = merge_entry_points( |
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.
reusing merge_entry_points ;)
@@ -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", | |||
"foo_bin":":foo-bin", |
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.
second commit was to pick this up, which it wasn't before..
that is, target address to pex_binary in entry points of setup_py..
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.
This is really great! Thanks @kaos, excellent improvements 👏
Outside of a couple small style things, my main feedback is that it'd be helpful to add a PR description. Doesn't need to be long, but explaining a little what this change is about and giving a brief overview of how you achieve it.
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 comment
The 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!
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 comment
The 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 entry_points.items()
more than necessary, and so the style guide says:
If constructing multiple collections by iterating over the same original collection, use a for loop for performance.
Here, I think you would have for ep_name, ep_val in entry_points.items()
in your outer loop. Then two inner loops over provides_field.val.items()
and entry_points_field.val.items()
.
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.
Eh.. it is not the same entry_points.items()
, the first comes from entry_points_field.val
, the other from provides_field.val
. ;)
Could however have a little helper function to reduce code duplication, and this would become super clear :)
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Good change!
for _, entry_point in entry_points_with_source: | ||
return f"{name}={entry_point}" | ||
_, entry_point = entry_points_with_source[0] | ||
return name, entry_point |
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.
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 comment
The 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.. ;)
@@ -384,7 +384,7 @@ 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 comment
The reason will be displayed to describe this comment to others. Learn more.
So, before it was inferring a dep on the python_library
and now it does it on the pex_binary
? Cool!
""" | ||
|
||
entry_points_field: Optional[PythonDistributionEntryPointsField] = None | ||
provides_field: Optional[PythonProvidesField] = None |
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.
To be extra defensive, you could add a def __post__init()
that raises AssertionError
if both are unspecified.
@@ -209,7 +239,7 @@ async def resolve_python_distribution_entry_points( | |||
# Check that we only have targets with a pex entry_point field. | |||
for target in targets: | |||
if not target.has_field(PexEntryPointField): | |||
raise InvalidFieldException( | |||
raise InvalidEntryPoint( |
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.
That sounds good to have InvalidEntryPoint
subclass InvalidFieldException
, definitely. And makes sense if you want to do in a followup to keep this PR better scoped.
return ResolvedPythonDistributionEntryPoints() | ||
|
||
address = request.entry_points_field.address | ||
classified_entry_points = list(_classify_entry_points(field_value)) | ||
classified_entry_points = list(_classify_entry_points(all_entry_points)) |
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.
One way to do this is to set all_entry_points: MyType[Foo, Bar]
at the top of this whole conditional block. Note that you don't set the actual value to anything. It's only to let MyPy know "hey I'm gonna have this variable with this type".
for category, entry_points in chain( | ||
distribution_entry_points.explicit_modules.items(), | ||
provides_entry_points.explicit_modules.items(), | ||
) |
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.
Nice!
Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com>
Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se> # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se>
# Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
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.
🤘
…19149) Fixes #12430 #12414 (comment) > This got my head scratching. I'd like to be consistent, but some code > raise InvalidFieldException, where as other InvalidEntryPoint... feels > like InvalidEntryPoint ought to inherit from InvalidFieldException too, > at least... so a catch for field exceptions would catch this. > > Any way, I went with keeping the tests as is, as that is the user > facing interface. But I think we could make a do-over here to make it > more consistent. ;)
With the new
entry_points
field onpython_distribution
that supports arbitrary entry points for thesetuptools.setup()
call, including getting the entry point method from apex_binary
targets entry point and dependency inference, this PR brings the same features to thesetup_py(entry_points)
object field.By uniting all entry point processing to the
resolve_python_distribution_entry_points
andinject_python_distribution_dependencies
rules, it is easier to treat all the different sources of entry points the same.Legacy
with_binaries()
processing in thegenerate_chroot
rule can thus be removed in favor of the updated python distribution rules mentioned above.Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]
Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]