Repository: kennethreitz/pipenv (python)

Issue:
extras_require and length of name > 5 fails.
I have a setup.py that looks like this:

```python3
from setuptools import setup, find_packages

setup(
    name='graphene_utils',
    version='1.0',
    packages=find_packages(),
    include_package_data=True,
    install_requires=[
        'graphene>=2.0.dev',
    ],
    extras_require={
        'sqlalchemy': [
            'sqlalchemy',
        ],
        'test': [
            'pylint',
        ],
    },
    entry_points='''
    [console_scripts]
    run=server:run
    ''',
)
```

When attempting `pipenv install "-e .[sqlalchemy]"` it fails with:
```bash
~/code/graphene_utils (virtualenvs/graphene_utils-Dw3J0BOS)
$ pipenv install "-e .[sqlalchemy]"
Courtesy Notice: Pipenv found itself running within a virtual environment, so it will automatically use that environment, instead of creating its own for any project.
Discovering imports from local codebase…
Installing -e .[sqlal…
⠸
Error:  An error occurred while installing -e .[sqlal!
.[sqlal should either be a path to a local project or a VCS url beginning with svn+, git+, hg+, or bzr+
```

However, renaming the extra_requires to `sqla` works fine.

Pull request description:
Properly split and parse requirements lines
- Split out markers and local paths
- Fixes #870 and #1083 and #858 and #848 
- Also splits markers properly using pip's approach
- The requirements parser we use really needs a lot of help

Patch:
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=<package-name>.'.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
 
 


Test patch:
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):