Repository: pypa/pipenv

Issue:
Issue installing "git+..." packages
I'm currently unable to install anything from a github repository. 

`pip install "git+https://github.com/lewiswolf/graphs.git#egg=graphs"`

Results in:

```
Installing git+https://github.com/lewiswolf/graphs.git#egg=graphs...
Resolving git+https://github.com/lewiswolf/graphs.git#egg=graphs...
Traceback (most recent call last):
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/bin/pipenv", line 8, in <module>
    sys.exit(cli())
             ^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/cli/options.py", line 58, in main
    return super().main(*args, **kwargs, windows_expand_args=False)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/decorators.py", line 84, in new_func
    return ctx.invoke(f, obj, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/vendor/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/cli/command.py", line 209, in install
    do_install(
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/routines/install.py", line 255, in do_install
    added, cat, normalized_name = project.add_package_to_pipfile(
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/project.py", line 1167, in add_package_to_pipfile
    name, normalized_name, entry = self.generate_package_pipfile_entry(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/project.py", line 1147, in generate_package_pipfile_entry
    entry["ref"] = vcs_parts[1].split("#", 1)[0].strip()
```

And both `pip install "git+https://github.com/lewiswolf/graphs.git@master#egg=graphs"` and `pip install "git+https://github.com/lewiswolf/graphs.git"` results in:

```
Installing git+https://github.com/lewiswolf/graphs.git...
Resolving git+https://github.com/lewiswolf/graphs.git...
⠋ Installing...INFO:pipenv.patched.pip._internal.vcs.git:Cloning https://github.com/lewiswolf/graphs.git to /var/folders/6w/mgc4qy2n69b8cc1nszttrrsr0000gn/T/tmpycprpo8n
INFO:pip.subprocessor:Running command git clone --filter=blob:none https://github.com/lewiswolf/graphs.git /var/folders/6w/mgc4qy2n69b8cc1nszttrrsr0000gn/T/tmpycprpo8n
INFO:pip.subprocessor:Cloning into '/var/folders/6w/mgc4qy2n69b8cc1nszttrrsr0000gn/T/tmpycprpo8n'...
⠼ Installing None...INFO:pipenv.patched.pip._internal.vcs.git:Resolved https://github.com/lewiswolf/graphs.git to commit 855109828a26129f513965f6b32f682744613e03
⠴ Installing None...Error: Traceback (most recent call last):
  File 
"/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/routines/inst
all.py", line 255, in do_install
    added, cat, normalized_name = project.add_package_to_pipfile(
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File 
"/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/project.py", 
line 1167, in add_package_to_pipfile
    name, normalized_name, entry = self.generate_package_pipfile_entry(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File 
"/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/project.py", 
line 1114, in generate_package_pipfile_entry
    req_name = determine_package_name(package)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File 
"/usr/local/Cellar/pipenv/2023.8.26/libexec/lib/python3.11/site-packages/pipenv/utils/depende
ncies.py", line 744, in determine_package_name
    raise ValueError(f"Could not determine package name from {package}")
ValueError: Could not determine package name from git+https://github.com/lewiswolf/graphs.git

✘ Failed adding package to Pipfile
```

I've also tried this with other git repositories and had the same results.


Discussion:
Its kind of two things, we are requiring you supply a ref to the branch so in your case `git+https://github.com/lewiswolf/graphs.git
@master`  and because your setup.py is more complicated it breaks our ast parser for getting the package name (since the egg is not a requirement).  Since your example has the name as a var, we should extend our ast parser logic to consider this, but also you should consider migrating away from complicated setup.py.

Resolving patch:
diff --git a/pipenv/utils/dependencies.py b/pipenv/utils/dependencies.py
--- a/pipenv/utils/dependencies.py
+++ b/pipenv/utils/dependencies.py
@@ -514,9 +514,23 @@ def parse_pkginfo_file(content: str):
 
 
 def parse_setup_file(content):
+    # A dictionary to store variable names and their values
+    variables = {}
     try:
         tree = ast.parse(content)
         for node in ast.walk(tree):
+            # Extract variable assignments and store them
+            if isinstance(node, ast.Assign):
+                for target in node.targets:
+                    if isinstance(target, ast.Name):
+                        if isinstance(node.value, ast.Str):  # for Python versions < 3.8
+                            variables[target.id] = node.value.s
+                        elif isinstance(node.value, ast.Constant) and isinstance(
+                            node.value.value, str
+                        ):
+                            variables[target.id] = node.value.value
+
+            # Check function calls to extract the 'name' attribute from the setup function
             if isinstance(node, ast.Call):
                 if (
                     getattr(node.func, "id", "") == "setup"
@@ -525,12 +539,20 @@ def parse_setup_file(content):
                 ):
                     for keyword in node.keywords:
                         if keyword.arg == "name":
-                            if isinstance(keyword.value, ast.Str):
+                            # If it's a variable, retrieve its value
+                            if (
+                                isinstance(keyword.value, ast.Name)
+                                and keyword.value.id in variables
+                            ):
+                                return variables[keyword.value.id]
+                            # Otherwise, check if it's directly provided
+                            elif isinstance(keyword.value, ast.Str):
                                 return keyword.value.s
                             elif isinstance(keyword.value, ast.Constant) and isinstance(
                                 keyword.value.value, str
                             ):
                                 return keyword.value.value
+                            # Additional handling for Python versions and specific ways of defining the name
                             elif sys.version_info < (3, 9) and isinstance(
                                 keyword.value, ast.Subscript
                             ):
@@ -546,14 +568,12 @@ def parse_setup_file(content):
                             elif sys.version_info >= (3, 9) and isinstance(
                                 keyword.value, ast.Subscript
                             ):
-                                # If the name is a lookup in a dictionary, only handle the case where it's a static lookup
                                 if (
                                     isinstance(keyword.value.value, ast.Name)
                                     and isinstance(keyword.value.slice, ast.Str)
                                     and keyword.value.value.id == "about"
                                 ):
                                     return keyword.value.slice.s
-
     except ValueError:
         pass  # We will not exec unsafe code to determine the name pre-resolver
 


Test patch: