Skip to content
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

Merged

Conversation

kaos
Copy link
Member

@kaos kaos commented Jul 23, 2021

With the new entry_points field on python_distribution that supports arbitrary entry points for the setuptools.setup() call, including getting the entry point method from a pex_binary targets entry point and dependency inference, this PR brings the same features to the setup_py(entry_points) object field.

By uniting all entry point processing to the resolve_python_distribution_entry_points and inject_python_distribution_dependencies rules, it is easier to treat all the different sources of entry points the same.

Legacy with_binaries() processing in the generate_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]

…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",
Copy link
Member Author

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

Copy link
Contributor

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!

Copy link
Member Author

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.

Copy link
Contributor

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
Copy link
Member Author

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.

Copy link
Contributor

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))
Copy link
Member Author

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 Optionals. If there is a better way....

Copy link
Contributor

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".

Copy link
Member Author

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(
Copy link
Member Author

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. ;)

Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Member Author

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"),
],
)

Copy link
Member Author

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]
),
)
Copy link
Member Author

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,
Copy link
Member Author

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..

Copy link
Contributor

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]]:
Copy link
Member Author

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.

Copy link
Contributor

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(
Copy link
Member Author

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(
Copy link
Member Author

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",
Copy link
Member Author

@kaos kaos Jul 23, 2021

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..

Copy link
Contributor

@Eric-Arellano Eric-Arellano left a 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,
Copy link
Contributor

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!

Comment on lines 580 to 588
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()
}
Copy link
Contributor

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().

Copy link
Member Author

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]]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change!

Comment on lines -1035 to +1007
for _, entry_point in entry_points_with_source:
return f"{name}={entry_point}"
_, entry_point = entry_points_with_source[0]
return name, entry_point
Copy link
Contributor

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

Copy link
Member Author

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",
Copy link
Contributor

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
Copy link
Contributor

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(
Copy link
Contributor

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.

src/python/pants/backend/python/target_types_rules.py Outdated Show resolved Hide resolved
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))
Copy link
Contributor

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".

Comment on lines +335 to +338
for category, entry_points in chain(
distribution_entry_points.explicit_modules.items(),
provides_entry_points.explicit_modules.items(),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

kaos and others added 5 commits July 27, 2021 10:04
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]
Copy link
Contributor

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤘

@Eric-Arellano Eric-Arellano merged commit 54fb09d into pantsbuild:main Jul 27, 2021
@kaos kaos deleted the setup_py_entry_points_dep_inference branch July 27, 2021 20:54
huonw added a commit that referenced this pull request May 25, 2023
…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. ;)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants