Skip to content

Commit

Permalink
Assorted local editable file file fixes (#5886)
Browse files Browse the repository at this point in the history
* Do not consider something a local editable file unless the -e flag is passed.
* Add editable to Pipfile when specifie
* Ensure lockfile matches Pipfile for editable specification
* Handle more edge cases of parsing setup.py using ast in different python versions.
* Can assume if the name ends with an installable file extension its not a named requirement.
* Simplify logic
* Add news fragment.
  • Loading branch information
matteius committed Aug 29, 2023
1 parent 47ead91 commit 8aa204e
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 44 deletions.
2 changes: 2 additions & 0 deletions news/5885.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Do not treat named requirements as file installs just becacuse a match path exists; better handling of editable keyword for local file installs.
Handle additional edge cases in the setup.py ast parser logic for trying to determine local install package name.
2 changes: 2 additions & 0 deletions pipenv/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,8 @@ def generate_package_pipfile_entry(self, package, pip_line, category=None):
entry["extras"] = list(extras)
if path_specifier:
entry["file"] = unquote(str(path_specifier))
if pip_line.startswith("-e"):
entry["editable"] = True
elif vcs_specifier:
for vcs in VCS_LIST:
if vcs in package.link.scheme:
Expand Down
2 changes: 2 additions & 0 deletions pipenv/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"pyproject.toml",
)

INSTALLABLE_EXTENSIONS = (".whl", ".zip", ".tar", ".tar.gz", ".tgz")


def is_type_checking():
try:
Expand Down
85 changes: 43 additions & 42 deletions pipenv/utils/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)

from .constants import (
INSTALLABLE_EXTENSIONS,
RELEVANT_PROJECT_FILES,
REMOTE_SCHEMES,
SCHEME_LIST,
Expand Down Expand Up @@ -363,16 +364,8 @@ def is_pinned_requirement(ireq):


def is_editable_path(path):
not_editable = [".whl", ".zip", ".tar", ".tar.gz", ".tgz"]
if os.path.isfile(path):
return False
if os.path.isdir(path):
return True
if os.path.splitext(path)[1] in not_editable:
return False
for ext in not_editable:
if path.endswith(ext):
return False
return False


Expand Down Expand Up @@ -524,33 +517,42 @@ def parse_setup_file(content):
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "setup":
for keyword in node.keywords:
if keyword.arg == "name":
if isinstance(keyword.value, ast.Str):
return keyword.value.s
elif sys.version_info < (3, 9) and isinstance(
keyword.value, ast.Subscript
):
if (
isinstance(keyword.value.value, ast.Name)
and keyword.value.value.id == "about"
if isinstance(node, ast.Call):
if (
getattr(node.func, "id", "") == "setup"
or isinstance(node.func, ast.Attribute)
and node.func.attr == "setup"
):
for keyword in node.keywords:
if keyword.arg == "name":
if 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
elif sys.version_info < (3, 9) and isinstance(
keyword.value, ast.Subscript
):
if isinstance(
keyword.value.slice, ast.Index
) and isinstance(keyword.value.slice.value, ast.Str):
return keyword.value.slice.value.s
return keyword.value.s
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"
if (
isinstance(keyword.value.value, ast.Name)
and keyword.value.value.id == "about"
):
if isinstance(
keyword.value.slice, ast.Index
) and isinstance(keyword.value.slice.value, ast.Str):
return keyword.value.slice.value.s
return keyword.value.s
elif sys.version_info >= (3, 9) and isinstance(
keyword.value, ast.Subscript
):
return keyword.value.slice.s
# 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
Expand Down Expand Up @@ -734,7 +736,9 @@ def determine_package_name(package: InstallRequirement):
".zip"
):
req_name = find_package_name_from_zipfile(package.link.file_path)
elif package.link.file_path.endswith(".tar.gz"):
elif package.link.file_path.endswith(
".tar.gz"
) or package.link.file_path.endswith(".tar.bz2"):
req_name = find_package_name_from_tarball(package.link.file_path)
else:
req_name = find_package_name_from_directory(package.link.file_path)
Expand Down Expand Up @@ -888,20 +892,15 @@ def expansive_install_req_from_line(
for logging purposes in case of an error.
"""
name = name.strip("'")
editable = False
if name.startswith("-e "):
# Editable requirement
editable = True
if name.startswith("-e "): # Editable requirements
name = name.split("-e ")[1]
return install_req_from_editable(name, line_source)
if has_name_with_extras(name):
name = name.split(" @ ", 1)[1]

if expand_env:
name = expand_env_variables(name)

if editable or os.path.isdir(name):
return install_req_from_editable(name, line_source)

vcs_part = name
if "@ " in name: # Check for new style vcs lines
vcs_part = name.split("@ ", 1)[1]
Expand All @@ -919,7 +918,9 @@ def expansive_install_req_from_line(
constraint=constraint,
user_supplied=user_supplied,
)
if urlparse(name).scheme in ("http", "https", "file") or os.path.isfile(name):
if urlparse(name).scheme in ("http", "https", "file") or any(
name.endswith(s) for s in INSTALLABLE_EXTENSIONS
):
parts = parse_req_from_line(name, line_source)
else:
# It's a requirement
Expand Down
6 changes: 4 additions & 2 deletions pipenv/utils/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,14 @@ def format_requirement_for_lockfile(
entry["extras"] = sorted(req.extras)
if isinstance(pipfile_entry, dict) and pipfile_entry.get("file"):
entry["file"] = pipfile_entry["file"]
entry["editable"] = True
if pipfile_entry.get("editable"):
entry["editable"] = pipfile_entry.get("editable")
entry.pop("version", None)
entry.pop("index", None)
elif isinstance(pipfile_entry, dict) and pipfile_entry.get("path"):
entry["path"] = pipfile_entry["path"]
entry["editable"] = True
if pipfile_entry.get("editable"):
entry["editable"] = pipfile_entry.get("editable")
entry.pop("version", None)
entry.pop("index", None)
return name, entry
Expand Down

0 comments on commit 8aa204e

Please sign in to comment.