From 6d2432f115d6993e6b15160a46e623a800f1320a Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Fri, 12 May 2017 14:07:07 +0100 Subject: [PATCH 1/4] Add a is_subdirectory and make format_requirement use it when appropriate --- piptools/utils.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/piptools/utils.py b/piptools/utils.py index 679a145f0..e212782be 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -2,6 +2,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +import os import sys from itertools import chain, groupby from collections import OrderedDict @@ -61,13 +62,30 @@ def make_install_requirement(name, version, extras, constraint=False): return InstallRequirement.from_line('{}{}=={}'.format(name, extras_string, str(version)), constraint=constraint) +def is_subdirectory(base, directory): + """ + Return True if directory is a child directory of base + """ + base = os.path.join(os.path.realpath(base), '') + directory = os.path.join(os.path.realpath(directory), '') + + return os.path.commonprefix([base, directory]) == base + + def format_requirement(ireq, marker=None): """ Generic formatter for pretty printing InstallRequirements to the terminal in a less verbose way than using its `__str__` method. """ if ireq.editable: - line = '-e {}'.format(ireq.link) + path = ireq.link.path + if ireq.link.scheme == 'file' and is_subdirectory(os.getcwd(), path): + # If the ireq.link is relative to the current directory then output a relative path + path = 'file:' + os.path.join('.', os.path.relpath(path)) + else: + path = ireq.link + + line = '-e {}'.format(path) else: line = str(ireq.req).lower() From 15b5aefc87b516599b8b03a8c7a1151c3a670dc9 Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Fri, 12 May 2017 14:07:30 +0100 Subject: [PATCH 2/4] Add tests and refactor some fixtures --- tests/conftest.py | 16 ++++++++++++++++ tests/test_cli.py | 42 ++++++++++++++++++++++++++++++------------ tests/test_sync.py | 14 ++++++-------- tests/test_utils.py | 27 ++++++++++++++++++++++++++- tests/test_writer.py | 5 +++++ 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 62de67f94..6a33ad235 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import json +import os from functools import partial from pip._vendor.packaging.version import Version @@ -106,3 +107,18 @@ def from_line(): @fixture def from_editable(): return InstallRequirement.from_editable + + +@fixture +def fake_package_dir(): + return os.path.join(os.path.split(__file__)[0], 'fixtures', 'fake_package') + + +@fixture +def small_fake_package_dir(): + return os.path.join(os.path.split(__file__)[0], 'fixtures', 'small_fake_package') + + +@fixture +def minimal_wheels_dir(): + return os.path.join(os.path.split(__file__)[0], 'fixtures', 'minimal_wheels') diff --git a/tests/test_cli.py b/tests/test_cli.py index 3dd56dd44..e76f574e4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,6 +3,7 @@ from six.moves.urllib.request import pathname2url import subprocess import sys +import shutil from click.testing import CliRunner @@ -65,8 +66,7 @@ def test_command_line_overrides_pip_conf(pip_conf): assert 'Using indexes:\n http://override.com' in out.output -def test_command_line_setuptools_read(pip_conf): - +def test_command_line_setuptools_read(): runner = CliRunner() with runner.isolated_filesystem(): package = open('setup.py', 'w') @@ -177,7 +177,7 @@ def _invoke(command): return status, output -def test_run_as_module_compile(tmpdir): +def test_run_as_module_compile(): """piptools can be run as ``python -m piptools ...``.""" status, output = _invoke([ @@ -205,24 +205,43 @@ def test_run_as_module_sync(): assert status == 0 -def test_editable_package(tmpdir): +def test_editable_package(small_fake_package_dir): """ piptools can compile an editable """ - fake_package_dir = os.path.join(os.path.split(__file__)[0], 'fixtures', 'small_fake_package') - fake_package_dir = 'file:' + pathname2url(fake_package_dir) + small_fake_package_dir = 'file:' + pathname2url(small_fake_package_dir) runner = CliRunner() with runner.isolated_filesystem(): with open('requirements.in', 'w') as req_in: - req_in.write('-e ' + fake_package_dir) # require editable fake package + req_in.write('-e ' + small_fake_package_dir) # require editable fake package out = runner.invoke(cli, ['-n']) print(out.output) assert out.exit_code == 0 - assert fake_package_dir in out.output + assert small_fake_package_dir in out.output assert 'six==1.10.0' in out.output -def test_input_file_without_extension(tmpdir): +def test_relative_editable_package(small_fake_package_dir): + # fake_package_dir = 'file:' + pathname2url(fake_package_dir) + runner = CliRunner() + with runner.isolated_filesystem() as loc: + new_package_dir = os.path.join(loc, 'small_fake_package') + # Move the small_fake_package inside the temp directory + shutil.copytree(small_fake_package_dir, new_package_dir) + relative_package_dir = os.path.relpath(new_package_dir) + relative_package_req = '-e file:' + os.path.join('.', relative_package_dir) + + with open('requirements.in', 'w') as req_in: + req_in.write('-e ' + 'small_fake_package') # require editable fake package + + out = runner.invoke(cli, ['-n']) + + print(out.output) + assert out.exit_code == 0 + assert relative_package_req in out.output + + +def test_input_file_without_extension(): """ piptools can compile a file without an extension, and add .txt as the defaut output file extension. @@ -240,11 +259,10 @@ def test_input_file_without_extension(tmpdir): assert 'six==1.10.0' in out.output -def test_upgrade_packages_option(tmpdir): +def test_upgrade_packages_option(minimal_wheels_dir): """ piptools respects --upgrade-package/-P inline list. """ - fake_package_dir = os.path.join(os.path.split(__file__)[0], 'fixtures', 'minimal_wheels') runner = CliRunner() with runner.isolated_filesystem(): with open('requirements.in', 'w') as req_in: @@ -254,7 +272,7 @@ def test_upgrade_packages_option(tmpdir): out = runner.invoke(cli, [ '-P', 'small_fake_b', - '-f', fake_package_dir, + '-f', minimal_wheels_dir, ]) print(out.output) diff --git a/tests/test_sync.py b/tests/test_sync.py index 731b15464..d1ef3652c 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -161,14 +161,13 @@ def _get_file_url(local_path): return 'file://%s' % local_path -def test_diff_with_editable(fake_dist, from_editable): +def test_diff_with_editable(fake_dist, from_editable, small_fake_package_dir): installed = [ fake_dist('small-fake-with-deps==0.0.1'), fake_dist('six==1.10.0'), ] - path_to_package = os.path.join(os.path.dirname(__file__), 'fixtures', 'small_fake_package') reqs = [ - from_editable(path_to_package), + from_editable(small_fake_package_dir), ] to_install, to_uninstall = diff(reqs, installed) @@ -179,13 +178,12 @@ def test_diff_with_editable(fake_dist, from_editable): assert len(to_install) == 1 package = list(to_install)[0] assert package.editable - assert str(package.link) == _get_file_url(path_to_package) + assert str(package.link) == _get_file_url(small_fake_package_dir) -def test_sync_with_editable(from_editable): +def test_sync_with_editable(from_editable, small_fake_package_dir): with mock.patch('piptools.sync.check_call') as check_call: - path_to_package = os.path.join(os.path.dirname(__file__), 'fixtures', 'small_fake_package') - to_install = {from_editable(path_to_package)} + to_install = {from_editable(small_fake_package_dir)} sync(to_install, set()) - check_call.assert_called_once_with(['pip', 'install', '-q', '-e', _get_file_url(path_to_package)]) + check_call.assert_called_once_with(['pip', 'install', '-q', '-e', _get_file_url(small_fake_package_dir)]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 78206c3a7..52212368e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,20 @@ +import os +import shutil + from pytest import raises from piptools.utils import ( - as_tuple, format_requirement, format_specifier, flat_map, dedup) + as_tuple, format_requirement, format_specifier, flat_map, dedup, is_subdirectory) + + +def test_is_subdirectory(): + cwd = os.getcwd() + test_dir = os.path.join(cwd, 'test') + assert is_subdirectory(cwd, test_dir) + assert is_subdirectory(os.path.join(test_dir, '..'), test_dir) + assert is_subdirectory(cwd, cwd) + + assert not is_subdirectory(test_dir, cwd) def test_format_requirement(from_line): @@ -14,6 +27,18 @@ def test_format_requirement_editable(from_editable): assert format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y' +def test_format_requirement_non_relative_editable(from_editable, small_fake_package_dir, tmpdir): + tmp_package_dir = os.path.join(str(tmpdir), 'small_fake_package') + shutil.copytree(small_fake_package_dir, tmp_package_dir) + ireq = from_editable(tmp_package_dir) + assert format_requirement(ireq) == '-e file://' + tmp_package_dir + + +def test_format_requirement_relative_editable(from_editable, small_fake_package_dir): + ireq = from_editable(small_fake_package_dir) + assert format_requirement(ireq) == '-e file:./tests/fixtures/small_fake_package' + + def test_format_specifier(from_line): ireq = from_line('foo') assert format_specifier(ireq) == '' diff --git a/tests/test_writer.py b/tests/test_writer.py index 503cdd290..7ba4c82b5 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -17,6 +17,11 @@ def writer(): format_control=FormatControl(set(), set())) +def test_format_requirements_relative_path(from_editable, writer): + ireq = from_editable('file:fixtures/fake_package#egg=fake_package') + assert writer._format_requirement(ireq, {}, primary_packages=['fake_package']) == '-e file:./fixtures/fake_package' + + def test_format_requirement_annotation_editable(from_editable, writer): # Annotations are printed as comments at a fixed column ireq = from_editable('git+git://fake.org/x/y.git#egg=y') From 87de2545e57bb684bcaf945b45e56b63e5b94c80 Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Fri, 12 May 2017 14:16:59 +0100 Subject: [PATCH 3/4] Add a changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fde6886a..aeaf7090a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Master + +Features: +- Made local, editable, and relative requirements resolve to relative paths instead of absolute ones ([#507](https://github.com/jazzband/pip-tools/pull/507). Thanks to @orf) + # 1.9.0 Features: From bc8579eb931e4eacbb447f95cbc27aabcff9b945 Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Fri, 12 May 2017 14:17:39 +0100 Subject: [PATCH 4/4] Flake8 fixes --- tests/test_sync.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index d1ef3652c..51ea53f32 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,5 +1,4 @@ from collections import Counter -import os import platform import mock