From 1d751a13f1cd61a15998740151df02be2565bb19 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sat, 18 Nov 2017 19:02:39 -0500 Subject: [PATCH 1/2] Properly split and parse requirements lines - Split out markers and local paths - Fixes #870 and #1083 and #858 - Also splits markers properly using pip's approach - The requirements parser we use really needs a lot of help --- pipenv/utils.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/pipenv/utils.py b/pipenv/utils.py index 8d4632a9..919f8241 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -277,15 +277,33 @@ def get_requirement(dep): remote URIs, and package names, and that we pass only valid requirement strings to the requirements parser. Performs necessary modifications to requirements object if the user input was a local relative path. + + :param str dep: A requirement line + :returns: :class:`requirements.Requirement` object """ path = None + # Split out markers if they are present - similar to how pip does it + # See pip.req.req_install.InstallRequirement.from_line + if not any(dep.startswith(uri_prefix) for uri_prefix in SCHEME_LIST): + marker_sep = ';' + else: + marker_sep = '; ' + if marker_sep in dep: + dep, markers = dep.split(marker_sep, 1) + markers = markers.strip() + if not markers: + markers = None + else: + markers = None + # Strip extras from the requirement so we can make a properly parseable req + dep, extras = pip.req.req_install._strip_extras(dep) # Only operate on local, existing, non-URI formatted paths if (is_file(dep) and isinstance(dep, six.string_types) and not any(dep.startswith(uri_prefix) for uri_prefix in SCHEME_LIST)): dep_path = Path(dep) # Only parse if it is a file or an installable dir if dep_path.is_file() or (dep_path.is_dir() and pip.utils.is_installable_dir(dep)): - if dep_path.is_absolute(): + if dep_path.is_absolute() or dep_path.as_posix() == '.': path = dep else: path = get_converted_relative_path(dep) @@ -296,6 +314,11 @@ def get_requirement(dep): if req.local_file and req.uri and not req.path and path: req.path = path req.uri = None + if markers: + req.markers = markers + if extras: + # Bizarrely this is also what pip does... + req.extras = [r for r in requirements.parse('fakepkg{0}'.format(extras))][0].extras return req @@ -611,28 +634,26 @@ def convert_deps_from_pip(dep): hashable_path = req.uri if req.uri else req.path req.name = hashlib.sha256(hashable_path.encode('utf-8')).hexdigest() req.name = req.name[len(req.name) - 7:] - # {path: uri} TOML (spec 4 I guess...) if req.uri: dependency[req.name] = {'file': hashable_path} else: dependency[req.name] = {'path': hashable_path} + if req.extras: + dependency[req.name].update(extras) + # Add --editable if applicable if req.editable: dependency[req.name].update({'editable': True}) # VCS Installs. Extra check for unparsed git over SSH - if req.vcs or is_vcs(req.path): + elif req.vcs or is_vcs(req.path): if req.name is None: raise ValueError('pipenv requires an #egg fragment for version controlled ' 'dependencies. Please install remote dependency ' 'in the form {0}#egg=.'.format(req.uri)) - # Extras: e.g. #egg=requests[security] - if req.extras: - dependency[req.name] = extras - # Set up this requirement as a proper VCS requirement if it was not if not req.vcs and req.path.startswith(VCS_LIST): req.vcs = [vcs for vcs in VCS_LIST if req.path.startswith(vcs)][0] @@ -654,6 +675,10 @@ def convert_deps_from_pip(dep): if req.revision: dependency[req.name].update({'ref': req.revision}) + # Extras: e.g. #egg=requests[security] + if req.extras: + dependency[req.name].update({'extras': req.extras}) + elif req.extras or req.specs: specs = None @@ -679,7 +704,6 @@ def convert_deps_from_pip(dep): for key in dependency.copy(): if not hasattr(dependency[key], 'keys'): del dependency[key] - return dependency From 35d5457823cca234c588fd3b92fcbbc75b12d79b Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Sun, 19 Nov 2017 20:49:49 -0500 Subject: [PATCH 2/2] Add tests for local package extras - Will be even more complete with #1098 --- tests/test_pipenv.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index 280b1df0..453dc66d 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -334,6 +334,39 @@ class TestPipenv: assert 'urllib3' in p.lockfile['default'] assert 'pysocks' in p.lockfile['default'] + @pytest.mark.extras + @pytest.mark.install + @pytest.mark.local + def test_local_extras_install(self): + with PipenvInstance() as p: + setup_py = os.path.join(p.path, 'setup.py') + with open(setup_py, 'w') as fh: + contents = """ +from setuptools import setup, find_packages + +setup( + name='test_pipenv', + version='0.1', + description='Pipenv Test Package', + author='Pipenv Test', + author_email='test@pipenv.package', + license='PIPENV', + packages=find_packages(), + install_requires=['tablib'], + extras_require={'dev': ['flake8', 'pylint']}, + zip_safe=False +) + """.strip() + fh.write(contents) + c = p.pipenv('install .[dev]') + assert c.return_code == 0 + key = [k for k in p.pipfile['packages'].keys()][0] + dep = p.pipfile['packages'][key] + assert dep['path'] == '.' + assert dep['extras'] == ['dev'] + assert key in p.lockfile['default'] + assert 'dev' in p.lockfile['default'][key]['extras'] + @pytest.mark.vcs @pytest.mark.install def test_basic_vcs_install(self):