From abb07fcd7a9d549cf81786d9241c8ca6d69b7067 Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Sun, 25 Apr 2021 01:45:38 +0200 Subject: [PATCH 1/7] add PEP 440 support and tests --- piptools/utils.py | 9 ++++++++- tests/test_cli_compile.py | 6 +++--- tests/test_utils.py | 27 +++++++++++++++++++++++++++ tests/test_writer.py | 6 +++--- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index b4841890a..3ac0a2576 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -110,7 +110,14 @@ def format_requirement( if ireq.editable: line = f"-e {ireq.link.url}" elif is_url_requirement(ireq): - line = ireq.link.url + if not ireq.name: + line = ireq.link.url + elif ireq.link.url.startswith("file:./"): + # file:./ is a hack to use a relative path to a package + # Direct reference does not work for this, so only the URL is used + line = ireq.link.url + else: + line = f"{ireq.name.lower()} @ {ireq.link.url}" else: line = str(ireq.req).lower() diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index f52281657..3bdb87533 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -45,19 +45,19 @@ def test_command_line_overrides_pip_conf(pip_with_index_conf, runner): pytest.param("small-fake-a==0.1", "small-fake-a==0.1", id="regular"), pytest.param( "pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip", - "https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip", + "pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip", id="zip URL", ), pytest.param( "pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3", - "git+https://github.com/jazzband/pip-tools@7d86c8d3", + "pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3", id="scm URL", ), pytest.param( "pip-tools @ https://files.pythonhosted.org/packages/06/96/" "89872db07ae70770fba97205b0737c17ef013d0d1c790" "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl", - "https://files.pythonhosted.org/packages/06/96/" + "pip-tools @ https://files.pythonhosted.org/packages/06/96/" "89872db07ae70770fba97205b0737c17ef013d0d1c790" "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl", id="wheel URL", diff --git a/tests/test_utils.py b/tests/test_utils.py index e143a46d4..07b22f29a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -32,6 +32,33 @@ def test_format_requirement_url(from_line): assert format_requirement(ireq) == "https://example.com/example.zip" +def test_format_requirement_url_with_direct_link(from_line): + ireq = from_line("example @ https://example.com/example.zip") + assert format_requirement(ireq) == "example @ https://example.com/example.zip" + + +def test_format_requirement_url_with_direct_link_is_lower_case(from_line): + ireq = from_line("https://example.com/example.zip#egg=Example") + assert ireq.name == "Example" + assert ( + format_requirement(ireq) + == "example @ https://example.com/example.zip#egg=Example" + ) + + +def test_format_requirement_url_with_egg(from_line): + ireq = from_line("https://example.com/example.zip#egg=example") + assert ( + format_requirement(ireq) + == "example @ https://example.com/example.zip#egg=example" + ) + + +def test_format_requirement_url_relative_path(from_line): + ireq = from_line("file:./vendor/package.zip") + assert format_requirement(ireq) == "file:./vendor/package.zip" + + def test_format_requirement_editable_vcs(from_editable): ireq = from_editable("git+git://fake.org/x/y.git#egg=y") assert format_requirement(ireq) == "-e git+git://fake.org/x/y.git#egg=y" diff --git a/tests/test_writer.py b/tests/test_writer.py index 7a3d82d2f..1c4008358 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -148,7 +148,7 @@ def test_iter_lines__hash_missing(capsys, writer, from_line): expected_lines = ( MESSAGE_UNHASHED_PACKAGE, - "file:///example/#egg=example", + "example @ file:///example/#egg=example", "test==1.2 \\\n --hash=FAKEHASH", ) assert tuple(lines) == expected_lines @@ -173,8 +173,8 @@ def test_iter_lines__no_warn_if_only_unhashable_packages(writer, from_line): lines = writer._iter_lines(ireqs, hashes=hashes) expected_lines = ( - "file:///unhashable-pkg1/#egg=unhashable-pkg1", - "file:///unhashable-pkg2/#egg=unhashable-pkg2", + "unhashable-pkg1 @ file:///unhashable-pkg1/#egg=unhashable-pkg1", + "unhashable-pkg2 @ file:///unhashable-pkg2/#egg=unhashable-pkg2", ) assert tuple(lines) == expected_lines From 210fa12a44bded0b276a8dc9d32ae4d1c7f78ee0 Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Sun, 25 Apr 2021 02:50:04 +0200 Subject: [PATCH 2/7] add github url dependency in fake_with_deps --- tests/test_data/packages/fake_with_deps/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_data/packages/fake_with_deps/setup.py b/tests/test_data/packages/fake_with_deps/setup.py index 44a9e8959..709b59097 100644 --- a/tests/test_data/packages/fake_with_deps/setup.py +++ b/tests/test_data/packages/fake_with_deps/setup.py @@ -18,5 +18,6 @@ "SQLAlchemy!=0.9.5,<2.0.0,>=0.7.8,>=1.0.0", "python-memcached>=1.57,<2.0", "xmltodict<=0.11,>=0.4.6", + "requests @ git+git://github.com/psf/requests@v2.25.1", ], ) From 49993576b678160785b52e63a24cab531efb446e Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Sun, 25 Apr 2021 09:00:00 +0200 Subject: [PATCH 3/7] no direct reference if egg in URL --- piptools/utils.py | 2 ++ tests/test_utils.py | 68 +++++++++++++++++++++++++------------------- tests/test_writer.py | 6 ++-- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index 3ac0a2576..4bb79b6d8 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -116,6 +116,8 @@ def format_requirement( # file:./ is a hack to use a relative path to a package # Direct reference does not work for this, so only the URL is used line = ireq.link.url + elif "#" in ireq.link.url and "egg=" in ireq.link.url.rsplit("#", 1)[1]: + line = ireq.link.url else: line = f"{ireq.name.lower()} @ {ireq.link.url}" else: diff --git a/tests/test_utils.py b/tests/test_utils.py index 07b22f29a..7efe79285 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -27,36 +27,44 @@ def test_format_requirement(from_line): assert format_requirement(ireq) == "test==1.2" -def test_format_requirement_url(from_line): - ireq = from_line("https://example.com/example.zip") - assert format_requirement(ireq) == "https://example.com/example.zip" - - -def test_format_requirement_url_with_direct_link(from_line): - ireq = from_line("example @ https://example.com/example.zip") - assert format_requirement(ireq) == "example @ https://example.com/example.zip" - - -def test_format_requirement_url_with_direct_link_is_lower_case(from_line): - ireq = from_line("https://example.com/example.zip#egg=Example") - assert ireq.name == "Example" - assert ( - format_requirement(ireq) - == "example @ https://example.com/example.zip#egg=Example" - ) - - -def test_format_requirement_url_with_egg(from_line): - ireq = from_line("https://example.com/example.zip#egg=example") - assert ( - format_requirement(ireq) - == "example @ https://example.com/example.zip#egg=example" - ) - - -def test_format_requirement_url_relative_path(from_line): - ireq = from_line("file:./vendor/package.zip") - assert format_requirement(ireq) == "file:./vendor/package.zip" +@pytest.mark.parametrize( + ("line", "expected"), + ( + pytest.param( + "https://example.com/example.zip", + "https://example.com/example.zip", + id="simple url", + ), + pytest.param( + "example @ https://example.com/example.zip", + "example @ https://example.com/example.zip", + id="direct reference", + ), + pytest.param( + "Example @ https://example.com/example.zip", + "example @ https://example.com/example.zip", + id="direct reference lower case", + ), + pytest.param( + "https://example.com/example.zip#egg=example", + "https://example.com/example.zip#egg=example", + id="url with egg", + ), + pytest.param( + "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", + "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", + id="egg as query", + ), + pytest.param( + "file:./vendor/package.zip#egg=example", + "file:./vendor/package.zip#egg=example", + id="relative path", + ), + ), +) +def test_format_requirement_url(from_line, line, expected): + ireq = from_line(line) + assert format_requirement(ireq) == expected def test_format_requirement_editable_vcs(from_editable): diff --git a/tests/test_writer.py b/tests/test_writer.py index 1c4008358..7a3d82d2f 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -148,7 +148,7 @@ def test_iter_lines__hash_missing(capsys, writer, from_line): expected_lines = ( MESSAGE_UNHASHED_PACKAGE, - "example @ file:///example/#egg=example", + "file:///example/#egg=example", "test==1.2 \\\n --hash=FAKEHASH", ) assert tuple(lines) == expected_lines @@ -173,8 +173,8 @@ def test_iter_lines__no_warn_if_only_unhashable_packages(writer, from_line): lines = writer._iter_lines(ireqs, hashes=hashes) expected_lines = ( - "unhashable-pkg1 @ file:///unhashable-pkg1/#egg=unhashable-pkg1", - "unhashable-pkg2 @ file:///unhashable-pkg2/#egg=unhashable-pkg2", + "file:///unhashable-pkg1/#egg=unhashable-pkg1", + "file:///unhashable-pkg2/#egg=unhashable-pkg2", ) assert tuple(lines) == expected_lines From 7f8685fe8c84e9ccdd2ccb1022e75a4dc0ef8c4c Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Mon, 21 Jun 2021 23:19:30 +0200 Subject: [PATCH 4/7] update logic for file: and update tests --- piptools/utils.py | 8 +++++--- tests/test_utils.py | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index ac882a1f5..abdf2088d 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -115,14 +115,16 @@ def format_requirement( if ireq.editable: line = f"-e {ireq.link.url}" elif is_url_requirement(ireq): + # if the requirement has no name then it's just a URL if not ireq.name: line = ireq.link.url - elif ireq.link.url.startswith("file:./"): - # file:./ is a hack to use a relative path to a package - # Direct reference does not work for this, so only the URL is used + # if it starts with "file:", PEP508 does not support direct reference + elif ireq.link.url.startswith("file:"): line = ireq.link.url + # if egg is after # then it's not a direct reference elif "#" in ireq.link.url and "egg=" in ireq.link.url.rsplit("#", 1)[1]: line = ireq.link.url + # otherwise, it's a direct reference else: line = f"{ireq.name.lower()} @ {ireq.link.url}" else: diff --git a/tests/test_utils.py b/tests/test_utils.py index 7f56526bc..32d9fd8bc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -48,23 +48,33 @@ def test_format_requirement(from_line): pytest.param( "Example @ https://example.com/example.zip", "example @ https://example.com/example.zip", - id="direct reference lower case", + id="direct reference lowered case", ), pytest.param( + "exemple @ https://example.com/example.zip#egg=example", "https://example.com/example.zip#egg=example", - "https://example.com/example.zip#egg=example", - id="url with egg", + id="url with egg after #", ), pytest.param( "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", - id="egg as query", + id="url without egg after #", + ), + pytest.param( + "file:./vendor/package.zip", + "file:./vendor/package.zip", + id="relative path", ), pytest.param( - "file:./vendor/package.zip#egg=example", - "file:./vendor/package.zip#egg=example", + "file:vendor/package.zip", + "file:vendor/package.zip", id="relative path", ), + pytest.param( + "file:///vendor/package.zip", + "file:///vendor/package.zip", + id="full path", + ), ), ) def test_format_requirement_url(from_line, line, expected): From d45422d37ac23db067a4ce2e68f03029485dbe43 Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Wed, 23 Jun 2021 00:59:00 +0200 Subject: [PATCH 5/7] update logic to parse URL in ireq and add more tests --- piptools/utils.py | 15 +++++++-------- tests/test_utils.py | 28 ++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index abdf2088d..b3ff4f2f6 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -15,6 +15,7 @@ TypeVar, Union, ) +from urllib.parse import urlparse import click from click.utils import LazyFile @@ -118,15 +119,13 @@ def format_requirement( # if the requirement has no name then it's just a URL if not ireq.name: line = ireq.link.url - # if it starts with "file:", PEP508 does not support direct reference - elif ireq.link.url.startswith("file:"): - line = ireq.link.url - # if egg is after # then it's not a direct reference - elif "#" in ireq.link.url and "egg=" in ireq.link.url.rsplit("#", 1)[1]: - line = ireq.link.url - # otherwise, it's a direct reference + # otherwise parse the URL else: - line = f"{ireq.name.lower()} @ {ireq.link.url}" + parsed_url = urlparse(ireq.link.url) + if "egg=" in parsed_url.fragment: + line = ireq.link.url + else: + line = f"{ireq.name.lower()} @ {ireq.link.url}" else: line = str(ireq.req).lower() diff --git a/tests/test_utils.py b/tests/test_utils.py index 32d9fd8bc..49a6f3c8c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -51,14 +51,19 @@ def test_format_requirement(from_line): id="direct reference lowered case", ), pytest.param( - "exemple @ https://example.com/example.zip#egg=example", + "example @ https://example.com/example.zip#egg=example", "https://example.com/example.zip#egg=example", - id="url with egg after #", + id="url with egg in fragment", + ), + pytest.param( + "example @ https://example.com/example.zip#subdirectory=test&egg=example", + "https://example.com/example.zip#subdirectory=test&egg=example", + id="url with subdirectory and egg in fragment", ), pytest.param( "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", "example @ https://example.com/example.zip?egg=test#subdirectory=project_a", - id="url without egg after #", + id="url with egg in query", ), pytest.param( "file:./vendor/package.zip", @@ -70,10 +75,25 @@ def test_format_requirement(from_line): "file:vendor/package.zip", id="relative path", ), + pytest.param( + "file:vendor/package.zip#egg=example", + "file:vendor/package.zip#egg=example", + id="relative path with egg", + ), pytest.param( "file:///vendor/package.zip", "file:///vendor/package.zip", - id="full path", + id="full path without direct reference", + ), + pytest.param( + "package @ file:///vendor/package.zip", + "package @ file:///vendor/package.zip", + id="full path with direct reference", + ), + pytest.param( + "package @ file:///vendor/package.zip#egg=example", + "file:///vendor/package.zip#egg=example", + id="full path with direct reference and egg", ), ), ) From 54ab1ea82036d4cc8427457db92ea8e0ff572bd8 Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:45:25 +0200 Subject: [PATCH 6/7] add more tests in test_cli_compile.py --- tests/test_cli_compile.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index 720038b8a..d2a7bf10a 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -1,6 +1,8 @@ import os +import shutil import subprocess import sys +from pathlib import Path from textwrap import dedent from unittest import mock @@ -532,6 +534,20 @@ def test_locally_available_editable_package_is_not_archived_in_cache_dir( "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl", "\nclick==", ), + ( + "pytest-django @ git+git://github.com/pytest-dev/pytest-django" + "@21492afc88a19d4ca01cd0ac392a5325b14f95c7" + "#egg=pytest-django", + "git+git://github.com/pytest-dev/pytest-django" + "@21492afc88a19d4ca01cd0ac392a5325b14f95c7#egg=pytest-django", + ), + ( + "git+git://github.com/open-telemetry/opentelemetry-python.git" + "@v1.3.0#subdirectory=opentelemetry-api" + "&egg=opentelemetry-api", + "git+git://github.com/open-telemetry/opentelemetry-python.git" + "@v1.3.0#subdirectory=opentelemetry-api", + ), ), ) @pytest.mark.parametrize("generate_hashes", ((True,), (False,))) @@ -596,6 +612,40 @@ def test_local_url_package( assert dependency in out.stderr +@pytest.mark.parametrize( + ("line", "dependency", "rewritten_line"), + ( + pytest.param( + os.path.join( + MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl" + ), + "\nfile:small_fake_with_deps-0.1-py2.py3-none-any.whl", + "file:small_fake_with_deps-0.1-py2.py3-none-any.whl", + id="Relative URL", + ), + pytest.param( + os.path.join( + MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl" + ), + "\nsmall-fake-with-deps" + " @ file://{absolute_path}/small_fake_with_deps-0.1-py2.py3-none-any.whl", + "small-fake-with-deps" + " @ file://{absolute_path}/small_fake_with_deps-0.1-py2.py3-none-any.whl", + id="Relative URL", + ), + ), +) +def test_relative_url_package(pip_conf, runner, line, dependency, rewritten_line): + dependency = dependency.format(absolute_path=Path(".").absolute()) + rewritten_line = rewritten_line.format(absolute_path=Path(".").absolute()) + shutil.copy(line, ".") + with open("requirements.in", "w") as req_in: + req_in.write(dependency) + out = runner.invoke(cli, ["-n", "--rebuild"]) + assert out.exit_code == 0 + assert rewritten_line in out.stderr + + def test_input_file_without_extension(pip_conf, runner): """ piptools can compile a file without an extension, From fe1021400f9baf95cf201dac92af6cce8fb94f08 Mon Sep 17 00:00:00 2001 From: Florent Jeannot <12172017+FlorentJeannot@users.noreply.github.com> Date: Wed, 7 Jul 2021 07:11:51 +0200 Subject: [PATCH 7/7] tweak logic in format_requirement --- piptools/utils.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index b3ff4f2f6..3ae79b807 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -15,7 +15,6 @@ TypeVar, Union, ) -from urllib.parse import urlparse import click from click.utils import LazyFile @@ -116,16 +115,14 @@ def format_requirement( if ireq.editable: line = f"-e {ireq.link.url}" elif is_url_requirement(ireq): - # if the requirement has no name then it's just a URL - if not ireq.name: - line = ireq.link.url - # otherwise parse the URL + if ireq.name: + line = ( + ireq.link.url + if ireq.link.egg_fragment + else f"{ireq.name.lower()} @ {ireq.link.url}" + ) else: - parsed_url = urlparse(ireq.link.url) - if "egg=" in parsed_url.fragment: - line = ireq.link.url - else: - line = f"{ireq.name.lower()} @ {ireq.link.url}" + line = ireq.link.url else: line = str(ireq.req).lower()