Skip to content

Always narrow markers by Python version#6076

Merged
charliermarsh merged 3 commits intomainfrom
charlie/py-fork
Aug 15, 2024
Merged

Always narrow markers by Python version#6076
charliermarsh merged 3 commits intomainfrom
charlie/py-fork

Conversation

@charliermarsh
Copy link
Member

@charliermarsh charliermarsh commented Aug 14, 2024

Summary

Using #6064 as a motivating example: at present, on main, we're not properly propagating the Requires-Python simplifications. In that case, for example, we end up solving for a branch with python_version < 3.11, and a branch >= 3.11, even though Requires-Python is >=3.11. Later, when we get to the graph, we apply version simplification based on Requires-Python, which causes us to remove the python_version < 3.11 markers entirely, leaving us with duplicate dependencies for pylint.

This PR instead tries to ensure that we always apply this narrowing to requirements and forks, so that we don't need to apply the same simplification when constructing the graph at all.

Closes #6064.

Closes #6059.

@charliermarsh charliermarsh added bug Something isn't working preview Experimental behavior labels Aug 14, 2024
let not_markers = simplify_python(markers.negate(), python_requirement);
let mut new_markers = markers.clone();
new_markers.and(fork.markers.negate());
new_markers.and(simplify_python(fork.markers.negate(), python_requirement));
Copy link
Member Author

Choose a reason for hiding this comment

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

Honestly, I can't figure out if the simplify_python here is necessary. It's definitely necessary on not_markers above... But I'm not sure about here. I don't think any tests change when I remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Logically, I think this simplification here is right and even necessary. Because while fork.markers, by construction, should already be simplified, negating that here might wind up with a marker that includes (or is completely excluded by) requires-python. So re-doing the simplification does indeed seem correct.

I think that the same simplification ought to happen to markers too right? Specifically, before passing it to fork.intersect below.

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 thought markers was already simplified because we simplify when generating PubGrubDependency.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Talking over DM, the markers here are already simplified.

@charliermarsh charliermarsh force-pushed the charlie/py-fork branch 3 times, most recently from 5f09f9b to 817ff9f Compare August 14, 2024 01:11
// Requires-Python specifier.
requires_python = requires_python.unwrap()
let requirement = if let Some(requires_python) = python_requirement.target().and_then(|target| target.as_requires_python()).filter(|_| !requirement.marker.is_true()) {
let marker = requirement.marker.clone().simplify_python_versions(
Copy link
Member

Choose a reason for hiding this comment

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

Hmm.. I'm surprised this works. It's important to note that calling simplify_python_versions creates a different marker tree than the same tree parsed from the lockfile. For example, if we restrict the range of python_version >= 3.12 given requires-python >= 3.9, we now get the marker tree [3.9,3.12)=>false,[3.12,inf)=>true. If you parse that same expression from the lockfile, you get [0,3.12)=>false,[3.12,inf)=>true, which is not equal. This can cause false negatives in a --locked operation. Current we solve this by reapplying the requires-python restriction after parsing the lockfile. However, I believe this PR now restricts markers to the narrowed python requirement, which we do not reapply, so I'm surprised we aren't seeing failing test cases. Is there something I'm missing and can we remove the extra code in Lock::from_toml now?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is... interesting. If I remove that code from from_toml, lock_conditional_dependency_extra fails (for one).

Copy link
Member Author

Choose a reason for hiding this comment

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

It may just be that this doesn't work (for that particular problem) and we just don't have a proper test case for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any other ideas on how we can solve this problem?

Copy link
Member

@ibraheemdev ibraheemdev Aug 14, 2024

Choose a reason for hiding this comment

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

I think we could fix this by removing the whole idea of a restricted range with a special "rest" range, and then only apply simplification when displaying a marker, using disjointness checks instead for requires-python filtering. I was hoping we could use restriction to our advantage but it seems that may not be possible with narrowing.

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 guess the other option is: try to write this to the lockfile.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we use environment-markers to try and enforce this narrowing when we deserialize the lockfile?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe.. how would we map those to requirements?

Copy link
Member Author

Choose a reason for hiding this comment

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

Could we instead just retain the lower-bound, like >=3.7, <3.10 instead of using <3.10? Does that cause other problems?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok @ibraheemdev -- I added a failing test for this.

@charliermarsh charliermarsh force-pushed the charlie/py-fork branch 3 times, most recently from 6542938 to a838135 Compare August 14, 2024 14:10
@charliermarsh
Copy link
Member Author

I think something like this is critical (it fixes a bunch of issues), but need some solution to the problem Ibraheem brought up. Let me try to write a test for it.

warning: `uv lock` is experimental and may change without warning
warning: `uv.sources` is experimental and may change without warning
Resolved 3 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
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 instability is due to the problem @ibraheemdev mentioned.

@charliermarsh
Copy link
Member Author

@ibraheemdev -- Maybe an alternative idea... Could we add requires-python to Dependency? And only set it when it differs from the top-level?

@charliermarsh
Copy link
Member Author

Hmm, but that might not cover fork_markers, just dependency markers.

@charliermarsh
Copy link
Member Author

I think we can safely do this if we merge #6091?

charliermarsh added a commit that referenced this pull request Aug 15, 2024
)

## Summary

This PR changes the definition of `--locked` from:

> Produces the same `Lock`

To:

> Passes `Lock::satisfies`

This is a subtle but important difference. Previous, if
`Lock::satisfies` failed, we would run a resolution, then do
`existing_lock == lock`. If the two weren't equal, and `--locked` was
specified, we'd throw an error.

The equality check is hard to get right. For example, it means that we
can't ship #6076 without changing our marker representation, since the
deserialized lockfile "loses" some of the internal marker state that
gets accumulated during resolution.

The downside of this change is that there could be scenarios in which
`uv lock --locked` fails even though the lockfile would actually work
and the exact TOML would be unchanged. But... I think it's ok if
`--locked` fails after the user modifies something?
"python_version >= '3.12' and python_version < '3.13'",
"python_version < '3.12'",
"python_version < '3.13'",
"python_version >= '3.13'",
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this remove too? since it doesn't fill in >=3.11, <3.12 required range.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't respect upper-bounds on requires-python.

Copy link
Member

@BurntSushi BurntSushi left a comment

Choose a reason for hiding this comment

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

I think I found one possible additional simplification that should be done, but otherwise I think this LGTM assuming @ibraheemdev's concern is addressed. (I think it is now with #6102 merged?)

Copy link
Member

@ibraheemdev ibraheemdev left a comment

Choose a reason for hiding this comment

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

This looks good to me now that we no longer rely on marker equality for --locked.

@charliermarsh charliermarsh merged commit fe0b873 into main Aug 15, 2024
@charliermarsh charliermarsh deleted the charlie/py-fork branch August 15, 2024 15:50
charliermarsh added a commit that referenced this pull request Aug 15, 2024
## Summary

This is no longer required since we no longer implement `Eq` on `Lock`.
It will also sometimes be "wrong" as of #6076, since we now apply
different `requires-python` filtering to different parts of the tree
during resolution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working preview Experimental behavior

Projects

None yet

4 participants