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

PEP 440 Direct Reference support #1392

Merged
merged 17 commits into from Jul 17, 2021

Conversation

FlorentJeannot
Copy link
Contributor

@FlorentJeannot FlorentJeannot commented Apr 25, 2021

This PR adds PEP 440 Direct References support to fix #1391.

pip can support direct references, so the egg can be omitted inside the URL (see the code from pip).

In my projects which have git repositories specified as dependencies, without a direct reference or an egg, pip was not able to determine the package name, which was causing an issue during pip-sync (inside the merge, comparing two identical requirements from two different files).

Also, inside setup.py, install_requires must be written with a direct reference (pip fails to install the dependencies if it is not using a direct reference). pip-tools was ignoring the direct reference, so it was not generating the requirements.txt correctly. See my example:

# setup.py
install_requires=["package @ git+ssh://url@version"]

# requirements.txt (from pip-tools)
git+ssh://url@version

Changelog-friendly one-liner: PEP 440 Direct References support

Contributor checklist
  • Provided the tests for the changes.
  • Gave a clear one-line description in the PR (that the maintainers can add to CHANGELOG.md on release).
  • Assign the PR to an existing or new milestone for the target version (following Semantic Versioning).

@FlorentJeannot FlorentJeannot changed the title add PEP 440 support and tests add PEP 440 Direct Reference support Apr 25, 2021
@FlorentJeannot FlorentJeannot changed the title add PEP 440 Direct Reference support PEP 440 Direct Reference support Apr 25, 2021
@FlorentJeannot
Copy link
Contributor Author

FlorentJeannot commented Apr 25, 2021

FYI I installed pip-tools in my project using my fork and I generated and compared the requirements.txt and dev-requirements.txt files with the new ones:

  • there was no regression
  • the direct references were generated when it was only needed
  • one sub-dependency generated a URL with an egg, without direct reference (which is fine: it keeps the original behavior of the tool)

And pip-sync worked (it was not without this fix; see #1391)

@codecov
Copy link

codecov bot commented Apr 27, 2021

Codecov Report

Merging #1392 (4999357) into master (e8982fb) will increase coverage by 0.00%.
The diff coverage is 100.00%.

❗ Current head 4999357 differs from pull request most recent head dee9ddf. Consider uploading reports for the commit dee9ddf to get more accurate results
Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1392   +/-   ##
=======================================
  Coverage   99.67%   99.67%           
=======================================
  Files          33       33           
  Lines        3037     3044    +7     
  Branches      327      330    +3     
=======================================
+ Hits         3027     3034    +7     
  Misses          5        5           
  Partials        5        5           
Impacted Files Coverage Δ
tests/test_cli_compile.py 100.00% <ø> (ø)
piptools/utils.py 100.00% <100.00%> (ø)
tests/test_utils.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e8982fb...dee9ddf. Read the comment docs.

@FlorentJeannot
Copy link
Contributor Author

FlorentJeannot commented May 2, 2021

To help you understand this fix/enhancement, from the master branch, try the following tests:

Test 1

In pip-tools/tests/test_data/packages/fake_with_deps/setup.py, add in install_requires:

"requests @ git+git://github.com/psf/requests@v2.25.1"

In test_realistic_complex_sub_dependencies, add the following at the end:

print(out.stderr)
assert "requests @ git+git://github.com/psf/requests@v2.25.1" in out.stderr

This test will fail because it is missing the direct reference, inside the output there is:

git+git://github.com/psf/requests@v2.25.1

Also, note that there is no egg specified.

Test 2

In pip-tools/tests/test_resolver.py, add the following:

def test_combine_install_requirements_direct_ref(repository, from_line):
    first_dep = from_line("git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0", comes_from="-r requirements.in")
    second_dep = from_line("git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0", comes_from="-r dev-requirements.in")

    combined = combine_install_requirements(repository, [first_dep, second_dep])
    assert combined.comes_from == first_dep.comes_from  # shortest string
    assert set(combined._source_ireqs) == {first_dep, second_dep}
    assert str(combined.req.specifier) == ""

This is failing because both git URLs are missing a specifier. You can make the test pass by specifying a direct reference or an egg:

  • enums @ git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0
  • git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0#egg=enums

For example this works:

def test_combine_install_requirements_direct_ref(repository, from_line):
    first_dep = from_line("enums @ git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0", comes_from="-r requirements.in")
    second_dep = from_line("git+ssh://****@bitbucket.org/private-bucket/enums.git@4.0.0#egg=enums", comes_from="-r dev-requirements.in")

    combined = combine_install_requirements(repository, [first_dep, second_dep])
    assert combined.comes_from == first_dep.comes_from  # shortest string
    assert set(combined._source_ireqs) == {first_dep, second_dep}
    assert str(combined.req.specifier) == ""

Conclusion

  • Test 1 does not generate the requirement with the direct reference, even though it was specified in install_requires
  • Test 2 fails if a specifier (a direct reference or an egg) is missing

Because pip-tools currently does not handle direct references:

  • when requirements.in contains a direct reference and dev-requirements.in contains the same direct reference
  • then pip-compile generates these files without their direct references
  • and pip-sync requirements.txt dev-requirements.txt fails when the resolver enters combine_install_requirements

I hope this helps to make it clearer and I am looking forward to reading your feedback!

@AndydeCleyre
Copy link
Contributor

Hey, you reached out last week over at #1329, and today I was able to start looking at this. It's possible that, aside from changes to tests, all that's needed on top of my WIP PR is:

diff --git a/piptools/utils.py b/piptools/utils.py
index 4f15729..2f81af1 100644
--- a/piptools/utils.py
+++ b/piptools/utils.py
@@ -1,6 +1,7 @@
 import collections
 import itertools
 import os
+import re
 import shlex
 from contextlib import contextmanager, suppress
 from typing import (
@@ -137,6 +138,8 @@ def format_requirement(
                         ireq.local_file_path, from_dir
                     ).replace(os.path.sep, "/")
             ireq_url = f"{path_url}{fragment_string(ireq)}"
+        if not re.match(r"^#(|.*&)egg=.*", fragment_string(ireq)) and ireq.name:
+            ireq_url = f"{ireq.name} @ {ireq_url}"
         line = f"-e {ireq_url}" if ireq.editable else ireq_url
 
     if marker:

I'll leave this diff right here, and if #1329 is ever done this will help me merge.

@FlorentJeannot
Copy link
Contributor Author

@AndydeCleyre Thanks for your suggestion. Yeah it looks like that would work, the tests I've added will help you find out. :)

@atugushev atugushev added enhancement Improvements to functionality writer Related to results output writer component labels Jun 11, 2021
@luzfcb
Copy link

luzfcb commented Jun 17, 2021

@FlorentJeannot I guess that's necessary to include these four test cases to ensure this continues to work in the future. Anyway looks good to me. (A github archive zip or tar.gz with and without the archive hash value)

The output should be the same as the input:

django-debug-toolbar@https://github.com/jazzband/django-debug-toolbar/archive/6ec6bfe0ba5912d7fd0a2b98176a215847157696.zip

django-taggit@https://github.com/jazzband/django-taggit/archive/74460fdc4a8b8bf2d36fb8a59691403cde7b4528.zip#sha1=594b7dd32bec37d8bf70a6ffa8866d30e93f3c42

@FlorentJeannot
Copy link
Contributor Author

Hey @luzfcb, this PR would definitely fix your issue! :)

Concerning your suggestion, I think the existing tests are enough. Could you explain your point of view?

@atugushev Do you know when this PR will be reviewed?

Thanks!

piptools/utils.py Outdated Show resolved Hide resolved
@atugushev atugushev added the vcs Related to VCS requirements label Jun 21, 2021
piptools/utils.py Outdated Show resolved Hide resolved
tests/test_utils.py Outdated Show resolved Hide resolved
piptools/utils.py Outdated Show resolved Hide resolved
piptools/utils.py Outdated Show resolved Hide resolved
Copy link
Member

@atugushev atugushev left a comment

Choose a reason for hiding this comment

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

Thanks for addressing suggestions 👍


As a side note, this transformation feels confusing:

example @ https://example.com/example.zip#egg=example
→
https://example.com/example.zip#egg=example

I'd prefer the opposed instead:

https://example.com/example.zip#egg=example
→
example @ https://example.com/example.zip

AFAIK egg fragment appears to be discouraged for direct references. However, pip is going to refactor this within pypa/pip#1289, so it would make sense to address that later, as soon as the issue is resolved.

@FlorentJeannot
Copy link
Contributor Author

Thanks for addressing suggestions 👍

As a side note, this transformation feels confusing:

example @ https://example.com/example.zip#egg=example
→
https://example.com/example.zip#egg=example

I'd prefer the opposed instead:

https://example.com/example.zip#egg=example
→
example @ https://example.com/example.zip

AFAIK egg fragment appears to be discouraged for direct references. However, pip is going to refactor this within pypa/pip#1289, so it would make sense to address that later, as soon as the issue is resolved.

Hello @atugushev,

Thank you for your feedback and your approval for this PR.
I suggest to mention "PEP 508 support" in the CHANGELOG.md instead of PEP 440 as it seems to be more appropriate.

Regarding your side note, I agree with you and I'll happily provide a PR for this when pypa/pip#1289 will be resolved.

@atugushev
Copy link
Member

@FlorentJeannot

We should also support extras, see #1450 for details. Would you like to address it here or in a following up PR?

@FlorentJeannot
Copy link
Contributor Author

@FlorentJeannot

We should also support extras, see #1450 for details. Would you like to address it here or in a following up PR?

@atugushev

I will do it in another PR since this one has been approved.

Also I have another question. It seems that pip already converts URLs to the PEP 508 format. See https://github.com/pypa/pip/blob/7da05f33345de4c59930398aefa011ed7ccdeca3/src/pip/_vendor/packaging/requirements.py#L148
Is there a good reason to wait for pypa/pip#1289 or could we do it like them now?

@atugushev atugushev merged commit 53c9272 into jazzband:master Jul 17, 2021
@atugushev
Copy link
Member

@FlorentJeannot

Thanks for the PR! Awesome job 👍🏻

Is there a good reason to wait for pypa/pip#1289 or could we do it like them now?

Could you elaborate on how code in Requirement.__str__() related to egg fragment refactoring in pypa/pip#1289?

@FlorentJeannot
Copy link
Contributor Author

@atugushev Thanks 👍 Happy to contribute to this project 😄

So I believe Requirement.__str__() is what is used to format the requirement in pip. I think that's the equivalent to the function I modified in piptools to support the direct reference.
In their code they seem to always convert requirements to the direct reference format when they have a URL with a name.

So for example if you do this:
pip install 'git+https://github.com/encode/uvicorn.git@0.14.0#egg=uvicorn[standard]'

Even though it was specified with an egg, the output of pip freeze will be a direct reference for uvicorn (which makes me wonder if we shouldn't do like them already):

asgiref==3.4.1
click==8.0.1
h11==0.12.0
httptools==0.2.0
pep517==0.10.0
python-dotenv==0.18.0
PyYAML==5.4.1
toml==0.10.2
uvicorn @ git+https://github.com/encode/uvicorn.git@87da6cf4082cd28306bdb78d7d42552d131cc179
uvloop==0.15.3
watchgod==0.7
websockets==9.1

@atugushev
Copy link
Member

atugushev commented Jul 17, 2021

@FlorentJeannot

if we shouldn't do like them already

Ideally, we should. Feel free to shoot a PR if you think it's doable.

@FlorentJeannot FlorentJeannot deleted the pep440-direct-reference branch July 17, 2021 02:27
@FlorentJeannot
Copy link
Contributor Author

FlorentJeannot commented Jul 17, 2021

@FlorentJeannot

if we shouldn't do like them already

Ideally, we should. Feel free to shoot a PR if you think it's doable.

Will do!

Edit: @atugushev Done! #1455

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvements to functionality vcs Related to VCS requirements writer Related to results output writer component
Projects
None yet
Development

Successfully merging this pull request may close these issues.

pip-compile fails AttributeError: 'NoneType' object has no attribute 'specifier'
5 participants