-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Re-install PEP 508 wheel dependencies when the version changes #6402
Conversation
@cjerdonek can you take a look at this? |
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 made some more superficial comments. Unfortunately, I don't feel qualified enough right now to evaluate the overall effect of the change and whether it's being implemented correctly, since this isn't something I've looked into before..
…q_string Previously, an InvalidRequirement would raise a NameError while trying to raise an InstallationError because `req` was not defined. Discovered while working on pypa#6402.
@cjerdonek thanks for the review 🙌 If you don't feel qualified to evaluate the overall effect, any suggestions for who is? |
Just some anecdotal evidence I'll add: We've been using that patch since it was posted and it's been working well for months with daily deployments across dozens of servers. |
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 comment.
tests/functional/test_install.py
Outdated
@@ -1391,6 +1391,50 @@ def test_install_pep508_with_url_in_install_requires(script): | |||
assert "Successfully installed packaging-15.3" in str(res), str(res) | |||
|
|||
|
|||
@pytest.mark.parametrize('create_dep, format', [ | |||
(create_test_package_with_setup, 'directory'), | |||
(create_basic_wheel_for_package, 'wheel'), |
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.
Adding these strings "directory" and "wheel" and then doing if-checks in the test method to toggle behavior is a sign that this test would be better as two separate tests. To share code, you can put the common code in a private helper method used by both (e.g. still passing create_dep
).
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 went back and forth on that originally. I couldn't come up with a helper that makes sense, so I went with parametrization and the conditional.
Pushed up a version where it's split into two tests though
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 couldn't come up with a helper that makes sense,
For the helper, you can basically use the body of the test function you had before more or less as is -- minus the if format
toggles at the beginning and the end. Then each new test will have the form <beginning> <helper call> <end>
, where the beginning and end portions are the if clauses that would have executed in your original test. There's a lot of repetition in what you have now in a long-ish test, so it would be helpful in understanding to be able to see the common structure being used.
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 seems a lot harder to follow than the original parametrized version:
def function_that_does_a_lot(script, create_dep, dep_v1_path):
dep_v2_path = create_dep(script, name='dep', version='2.0')
pkga_path = create_basic_wheel_for_package(
script, name='pkga', version='1.0',
depends=['dep @ ' + path_to_url(dep_v1_path)],
)
res = script.pip('install', pkga_path)
assert "Successfully installed dep-1.0" in str(res), str(res)
pkga_path.rmtree()
# Updating the URL to the dependency installs the updated dependency
pkga_path = create_basic_wheel_for_package(
script, name='pkga', version='2.0',
depends=['dep @ ' + path_to_url(dep_v2_path)],
)
res = script.pip('install', pkga_path)
assert "Successfully installed dep-2.0" in str(res), str(res)
res = script.pip('install', pkga_path)
return res
def test_install_pep508_with_url_in_install_requires_url_change_wheel(script):
dep_v1_path = create_basic_wheel_for_package(
script, name='dep', version='1.0',
)
res = function_that_does_a_lot(script, create_basic_wheel_for_package, dep_v1_path)
# pip can determine the version from a wheel's filename, so the
# dependency is not reinstalled if the URL doesn't change
assert "Requirement already satisfied: dep==2.0" in str(res), str(res)
def test_install_pep508_with_url_in_install_requires_url_change_directory(
script):
dep_v1_path = create_test_package_with_setup(
script, name='dep', version='1.0',
)
# Rename the package directory so it doesn't get overwritten when
# creating the package for dep_v2
dep_v1_path.move(dep_v1_path.folder / 'dep_v1')
dep_v1_path = dep_v1_path.folder / 'dep_v1'
res = function_that_does_a_lot(script, create_test_package_with_setup, dep_v1_path)
# pip can't determine versions from a directory name, so it will always
# reinstall the dependency
assert "Successfully installed dep-2.0" in str(res), str(res)
If you have ideas to improve this I'm open to suggestions because I don't see a version of this that's better than the parametrization.
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.
Conceptually, it seems a lot simpler to me. Any way it's written, the reader will need to develop an understanding of what that large chunk of code is doing. How about starting by describing to me in words what the function is doing? (Not line by line but the goal in terms of setting up the test.)
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.
Also, something else to consider is that if you make your create_dep
function in the second case one that returns a path dependent on the version (i.e. doing a move in both cases), then it looks like you can pass only create_dep
, which would move more code into the common function. Aside from the function definition in the second case, the body of both test functions would be just two lines.
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.
Haven't looked at most of the change/discussion here. Just noting a PEP compliance related question.
# Create an InstallRequirement for a PEP 508 URL with the same behavior | ||
# as 'pip install req.url' | ||
return install_req_from_line( | ||
req.url, comes_from=comes_from, isolated=isolated, |
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.
What about extras?
PEP 508 allows them: url_req = name wsp* extras? wsp* urlspec wsp+ quoted_marker?
pip install requests[security] @ https://files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz
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.
oooh good catch. I'll try figuring that out...
pip install requests[security] @ https://files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz
I think this actually doesn't work with pip install
:
$ pip --version
pip 19.0.3 from /Users/andy/.virtualenvs/tmp-de2709e9577734b/lib/python3.7/site-packages/pip (python 3.7)
$ pip install requests[security]@https://files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz
Requirement 'requests[security]@https://files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz' looks like a filename, but the file does not exist
Processing ./requests[security]@https:/files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz
Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory: '/Users/andy/.virtualenvs/requests[security]@https:/files.pythonhosted.org/packages/52/2c/514e4ac25da2b08ca5a464c50463682126385c4272c18193876e91f4bc38/requests-2.21.0.tar.gz'
it works as expected in setup.py
though.
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.
It looks like there's lots of weird cases around PEP 508 URLs: #5788 (comment) 😕
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 opened a pr : rouge8#1 to fix it
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.
Thanks! I’ll try to update this PR with a test for the extras behavior.
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.
@pradyunsg updated with a test for the extras behavior 🤞
What's required to get this PR in? I know that how PEP508 should be supported is not completely nailed down, but this would be a big QoL upgrade for those of us who need to use this feature today. |
I haven't had time to come back to it to deal with #6402 (comment) 😩 |
Hello! I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the |
@rouge8 alternatively to the PR (rouge8#1) here is the patch to apply after having merge master (if required and to save time here is the process I followed)
|
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.
Two comments:
-
If we delegate to
parse_req_from_line
then we're opening ourselves up to supporting everything that it supports for linked dependencies, making it harder to make changes in the future. Instead can we make a more targeted change? It looks like the fundamental difference is here - for wheel URLs we extract the version and use that as the target forRequirement
. We could do the same thing likeif req.url: link = Link(req.url) if link.is_wheel and not req.specifier: wheel = Wheel(link.filename) req.specifier = SpecifierSet("=={}".format(wheel.version))
This would give us a lot more flexibility going forward to adapt the implementation.
-
After the above, can we extract the parsing (not including the
domains_not_allowed
check) into a separate function that returns aRequirementParts
? That will align with the rest of the functions in this module. I would construct the result likeRequirementParts(req, None, None, set())
(instead of passing thelink
toRequirementParts
) to reinforce that we're not doing any special post-processing on the link.
@pradyunsg it seems like this could be rewritten to take use #7612 and actually know when the URL changes, which makes #5780 and #7678 overlap right? |
# same behavior as 'pip install req.url' | ||
parts = parse_req_from_line(req.url, None) | ||
link = Link(req.url) | ||
if link.is_wheel: |
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.
@pradyunsg @chrahunt can you take another look? |
Does the new resolver have the same issue? I kind of feel it’s the resolver’s job to detect a version change and ask for upgrade if needed. Although I’m not very worried with this change either—there is really no harm in parsing out the wheel link if we can. With that said, the implementation of constructors looks a bit weird to me with this patch. What’s the difference between |
It does, but then I ran into #8204 😩
I'm not really sure. I originally pulled this from #5780 (comment). Digging into the new resolver though, it looks like it's actually doing the same thing as this PR with |
My understanding, right now, is that the new resolver works like this PR, so "bundling" this change as part of the new resolver, would be beneficial in terms of a rollout strategy for us here (since Hyrum's Law applies). The |
As in only fixing it in the new resolver going forward? In that case let me know if there’s any logging or debugging I can do to help with the |
From #5780 (comment)
Fixes #5780