Skip to content

Commit

Permalink
Unify Python project root detection logic
Browse files Browse the repository at this point in the history
A Python project root is now defined as containing a pyproject.toml, or
a setup.py (pre-PEP-517 legacy layout). After this patch, this logic
applies to all checks except parse_editable, where we check for setup.py
and setup.cfg instead since non-setuptools PEP 517 projects cannot be
installed as editable right now.
  • Loading branch information
uranusjr committed Jun 19, 2021
1 parent 7c3abcc commit 288bffc
Show file tree
Hide file tree
Showing 8 changed files with 42 additions and 33 deletions.
1 change: 1 addition & 0 deletions news/10080.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correctly allow PEP 517 projects to be detected without warnings in ``pip freeze``.
4 changes: 2 additions & 2 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from pip._internal.utils.filesystem import copy2_fixed
from pip._internal.utils.hashes import Hashes, MissingHashes
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import display_path, hide_url, rmtree
from pip._internal.utils.misc import display_path, hide_url, is_installable_dir, rmtree
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unpacking import unpack_file
from pip._internal.vcs import vcs
Expand Down Expand Up @@ -376,7 +376,7 @@ def _ensure_link_req_src_dir(self, req, parallel_builds):
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
if os.path.exists(os.path.join(req.source_dir, 'setup.py')):
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
"pre-existing build directory ({}). This is likely "
Expand Down
4 changes: 2 additions & 2 deletions src/pip/_internal/req/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ def _looks_like_path(name):
def _get_url_from_path(path, name):
# type: (str, str) -> Optional[str]
"""
First, it checks whether a provided path is an installable directory
(e.g. it has a setup.py). If it is, returns the path.
First, it checks whether a provided path is an installable directory. If it
is, returns the path.
If false, check if the path is an archive file (such as a .whl).
The function checks if the path is a file. If false, if the path has
Expand Down
17 changes: 12 additions & 5 deletions src/pip/_internal/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,20 @@ def tabulate(rows):


def is_installable_dir(path: str) -> bool:
"""Is path is a directory containing pyproject.toml, setup.cfg or setup.py?"""
"""Is path is a directory containing pyproject.toml or setup.py?
If pyproject.toml exists, this is a PEP 517 project. Otherwise we look for
a legacy setuptools layout by identifying setup.py. We don't check for the
setup.cfg because using it without setup.py is only available for PEP 517
projects, which are already covered by the pyproject.toml check.
"""
if not os.path.isdir(path):
return False
return any(
os.path.isfile(os.path.join(path, signifier))
for signifier in ("pyproject.toml", "setup.cfg", "setup.py")
)
if os.path.isfile(os.path.join(path, "pyproject.toml")):
return True
if os.path.isfile(os.path.join(path, "setup.py")):
return True
return False


def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_internal/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
RemoteNotValidError,
RevOptions,
VersionControl,
find_path_to_setup_from_repo_root,
find_path_to_project_root_from_repo_root,
vcs,
)

Expand Down Expand Up @@ -410,8 +410,8 @@ def get_revision(cls, location, rev=None):
def get_subdirectory(cls, location):
# type: (str) -> Optional[str]
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
# find the repo root
git_dir = cls.run_command(
Expand All @@ -423,7 +423,7 @@ def get_subdirectory(cls, location):
if not os.path.isabs(git_dir):
git_dir = os.path.join(location, git_dir)
repo_root = os.path.abspath(os.path.join(git_dir, '..'))
return find_path_to_setup_from_repo_root(location, repo_root)
return find_path_to_project_root_from_repo_root(location, repo_root)

@classmethod
def get_url_rev_and_auth(cls, url):
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_internal/vcs/mercurial.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pip._internal.vcs.versioncontrol import (
RevOptions,
VersionControl,
find_path_to_setup_from_repo_root,
find_path_to_project_root_from_repo_root,
vcs,
)

Expand Down Expand Up @@ -120,16 +120,16 @@ def is_commit_id_equal(cls, dest, name):
def get_subdirectory(cls, location):
# type: (str) -> Optional[str]
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
# find the repo root
repo_root = cls.run_command(
['root'], show_stdout=False, stdout_only=True, cwd=location
).strip()
if not os.path.isabs(repo_root):
repo_root = os.path.abspath(os.path.join(location, repo_root))
return find_path_to_setup_from_repo_root(location, repo_root)
return find_path_to_project_root_from_repo_root(location, repo_root)

@classmethod
def get_repository_root(cls, location):
Expand Down
12 changes: 6 additions & 6 deletions src/pip/_internal/vcs/subversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
HiddenText,
display_path,
is_console_interactive,
is_installable_dir,
split_auth_from_netloc,
)
from pip._internal.utils.subprocess import CommandArgs, make_command
Expand Down Expand Up @@ -111,18 +112,17 @@ def make_rev_args(username, password):
@classmethod
def get_remote_url(cls, location):
# type: (str) -> str
# In cases where the source is in a subdirectory, not alongside
# setup.py we have to look up in the location until we find a real
# setup.py
# In cases where the source is in a subdirectory, we have to look up in
# the location until we find a valid project root.
orig_location = location
while not os.path.exists(os.path.join(location, 'setup.py')):
while not is_installable_dir(location):
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem without
# finding setup.py
# finding a Python project.
logger.warning(
"Could not find setup.py for directory %s (tried all "
"Could not find Python project for directory %s (tried all "
"parent directories)",
orig_location,
)
Expand Down
21 changes: 11 additions & 10 deletions src/pip/_internal/vcs/versioncontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
display_path,
hide_url,
hide_value,
is_installable_dir,
rmtree,
)
from pip._internal.utils.subprocess import CommandArgs, call_subprocess, make_command
Expand Down Expand Up @@ -68,23 +69,23 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
return req


def find_path_to_setup_from_repo_root(location, repo_root):
def find_path_to_project_root_from_repo_root(location, repo_root):
# type: (str, str) -> Optional[str]
"""
Find the path to `setup.py` by searching up the filesystem from `location`.
Return the path to `setup.py` relative to `repo_root`.
Return None if `setup.py` is in `repo_root` or cannot be found.
Find the the Python project's root by searching up the filesystem from
`location`. Return the path to project root relative to `repo_root`.
Return None if the project root is `repo_root`, or cannot be found.
"""
# find setup.py
# find project root.
orig_location = location
while not os.path.exists(os.path.join(location, 'setup.py')):
while not is_installable_dir(location):
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem without
# finding setup.py
# finding a Python project.
logger.warning(
"Could not find setup.py for directory %s (tried all "
"Could not find a Python project for directory %s (tried all "
"parent directories)",
orig_location,
)
Expand Down Expand Up @@ -296,8 +297,8 @@ def should_add_vcs_url_prefix(cls, remote_url):
def get_subdirectory(cls, location):
# type: (str) -> Optional[str]
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
return None

Expand Down

0 comments on commit 288bffc

Please sign in to comment.