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

Add [python].resolves_to_constraints_file #16420

Merged

Conversation

Eric-Arellano
Copy link
Contributor

@Eric-Arellano Eric-Arellano commented Aug 5, 2022

Constraints files are extremely useful for pinning problematic transitive deps.

We abused constraints files early on as a poor-man's lockfile via [python].requirement_constraints. While we want to deprecate that in favor of [python].resolves, it is useful to allow constraint files for resolves.

Specifically, each resolve should be allowed its own constraints file. For example, if resolve A uses Django 2, it may need to pin different problematic transitive deps of Django than resolve B using Django 3.

--

This PR is the first out of a project to allow specifying config for lockfiles on a per-resolve basis, rather than globally for all resolves: https://docs.google.com/document/d/1HAvpSNvNAHreFfvTAXavZGka-A3WWvPuH0sMjGUCo48/edit#.

--

This requires us now tracking constraints_file_hash in the lockfile metadata, so that when the user changes a constraints file, we know to regenerate the lock.

Note that this means we have a V3 of lockfile metadata. It is okay to keep using V2 -- the next time people run generate-lockfiles, they will be bumped.

[ci skip-rust]
[ci skip-build-wheels]

# 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]
@jsirois
Copy link
Contributor

jsirois commented Aug 5, 2022

... pip would complain.

Exactly! Let Pip do so and do not nanny tools if you don't need to. But that's neither here nor there, I'm just continually pointing out poor argument noise when I hear it. I think its fine to start with just constraints files as a whole. Synthesizing them from other configuration sources later and letting the proper nanny (Pip) do the nannying, is always an option.

Copy link
Contributor

@jsirois jsirois left a comment

Choose a reason for hiding this comment

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

AFAICT, including constraints but not invalidating the lock based on constraints changing is a bug.

[ci skip-rust]

[ci skip-build-wheels]
Comment on lines +278 to +280
"""Configuration from `[python]` that impacts how the resolve is created."""

constraints_file: tuple[Digest, FileEntry] | None
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will soon also have --only-binary and --no-binary. Possibly [python-repos] options, pending Slack discussion

Comment on lines -225 to +226
if isinstance(self, cls):
# Note that we do exact version matches so that authors can subclass earlier versions.
if type(self) is cls:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @chrisjrn for the fix

Comment on lines +301 to +303
all_tool_resolve_names = tuple(
sentinel.resolve_name for sentinel in union_membership.get(GenerateToolLockfileSentinel)
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is weird. It means that our error message for unrecognized resolve names is going to include JVM tools like javac. I could not think of a good way to filter down to only Python tools.

Copy link
Contributor

Choose a reason for hiding this comment

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

Its also a bit weird in the small that you could ask for ResolvePexConfig for the resolve "junit" and get back a non-error answer at all. It definitely seems like a level of typing structure is missing from the infra here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

# 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]
pip_args_file = "__pip_args.txt"
extra_args.extend(["-r", pip_args_file])
pip_args_file_content = "\n".join(
[f"--no-binary {pkg}" for pkg in python_setup.no_binary]
Copy link
Contributor

Choose a reason for hiding this comment

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

This was pre-existing, but changing the order of the python_setup args produces a new file here and the subsequent downstream invalidations that flow when it need not, --no-binary / --only-binary is not an order-sensitive thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was pre-existing

My next PR will likely be reworking this mechanism to be resolve-specific, so I can clean that up there.

Comment on lines +301 to +303
all_tool_resolve_names = tuple(
sentinel.resolve_name for sentinel in union_membership.get(GenerateToolLockfileSentinel)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Its also a bit weird in the small that you could ask for ResolvePexConfig for the resolve "junit" and get back a non-error answer at all. It definitely seems like a level of typing structure is missing from the infra here.

@@ -306,6 +381,9 @@ def validate_metadata(
lockfile=lockfile,
user_interpreter_constraints=interpreter_constraints,
user_requirements=user_requirements,
maybe_constraints_file_path=constraints_file_path_and_hash[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Types certainly mean 0 must be the correct index, but its definitely not reader-friendly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I considered using NamedTuple, but felt confusing to introduce yet another class when we already have so many. So I was hoping the path_and_hash[0] would be clear enough it means path. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I expressed my thoughts! I'd personally break out a 2 field dataclass, but I ship-it-ed because this is all fashion and not too important to the end goal.

Copy link
Contributor

Choose a reason for hiding this comment

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

I second that this should be a dataclass.

constraints_file: tuple[Digest, FileEntry] | None

@property
def constraints_file_path_and_hash(self) -> tuple[str, str] | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better if this returned a dataclass and not a tuple whose unpacking is not self-explanatory. Reading the code that uses this was confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

But this can wait for a followup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

John agreed with you. Will do :)

@@ -306,6 +381,9 @@ def validate_metadata(
lockfile=lockfile,
user_interpreter_constraints=interpreter_constraints,
user_requirements=user_requirements,
maybe_constraints_file_path=constraints_file_path_and_hash[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

I second that this should be a dataclass.

@Eric-Arellano Eric-Arellano merged commit fda17c9 into pantsbuild:main Aug 9, 2022
@Eric-Arellano Eric-Arellano deleted the constraints-file-with-locks branch August 9, 2022 02:16
Eric-Arellano added a commit that referenced this pull request Aug 9, 2022
…bclasses (#16449)

Closes #16443, which is necessary for the per-resolve config project from https://docs.google.com/document/d/1HAvpSNvNAHreFfvTAXavZGka-A3WWvPuH0sMjGUCo48/edit# to work properly. See #16420 (comment). 

As discussed on the ticket, this is not a very elegant solution. But we don't have the rule graph introspection mechanism necessary to automagically figure out what type each tool is, and that also seems brittle.

As explained in this PR's updated docs, plugin authors don't have to technically make this change.

[ci skip-rust]
[ci skip-build-wheels]
Eric-Arellano added a commit that referenced this pull request Aug 10, 2022
Improves upon #16420. @huonw wisely suggested we avoid using `hash` for constraints files because it makes Git diffs and merge conflicts much worse. python-poetry/poetry#496 rust-lang/cargo#7070

[ci skip-rust]
[ci skip-build-wheels]
cczona pushed a commit to cczona/pants that referenced this pull request Sep 1, 2022
Constraints files are extremely useful for pinning problematic transitive deps.

We abused constraints files early on as a poor-man's lockfile via `[python].requirement_constraints`. While we want to deprecate that in favor of `[python].resolves`, it is useful to allow constraint files for resolves.

Specifically, each resolve should be allowed its own constraints file. For example, if resolve A uses Django 2, it may need to pin different problematic transitive deps of Django than resolve B using Django 3.

--

This PR is the first out of a project to allow specifying config for lockfiles on a per-resolve basis, rather than globally for all resolves: https://docs.google.com/document/d/1HAvpSNvNAHreFfvTAXavZGka-A3WWvPuH0sMjGUCo48/edit#.

--

This requires us now tracking `constraints_file_hash` in the lockfile metadata, so that when the user changes a constraints file, we know to regenerate the lock.

Note that this means we have a V3 of lockfile metadata. It is okay to keep using V2 -- the next time people run `generate-lockfiles`, they will be bumped.

[ci skip-rust]
[ci skip-build-wheels]
cczona pushed a commit to cczona/pants that referenced this pull request Sep 1, 2022
…bclasses (pantsbuild#16449)

Closes pantsbuild#16443, which is necessary for the per-resolve config project from https://docs.google.com/document/d/1HAvpSNvNAHreFfvTAXavZGka-A3WWvPuH0sMjGUCo48/edit# to work properly. See pantsbuild#16420 (comment). 

As discussed on the ticket, this is not a very elegant solution. But we don't have the rule graph introspection mechanism necessary to automagically figure out what type each tool is, and that also seems brittle.

As explained in this PR's updated docs, plugin authors don't have to technically make this change.

[ci skip-rust]
[ci skip-build-wheels]
cczona pushed a commit to cczona/pants that referenced this pull request Sep 1, 2022
…sbuild#16469)

Improves upon pantsbuild#16420. @huonw wisely suggested we avoid using `hash` for constraints files because it makes Git diffs and merge conflicts much worse. python-poetry/poetry#496 rust-lang/cargo#7070

[ci skip-rust]
[ci skip-build-wheels]
@huonw huonw mentioned this pull request Jul 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants