From 288bffc43c599773608745eea10bfe6b787554aa Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sat, 19 Jun 2021 15:29:04 +0800 Subject: [PATCH] Unify Python project root detection logic 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. --- news/10080.bugfix.rst | 1 + src/pip/_internal/operations/prepare.py | 4 ++-- src/pip/_internal/req/constructors.py | 4 ++-- src/pip/_internal/utils/misc.py | 17 ++++++++++++----- src/pip/_internal/vcs/git.py | 8 ++++---- src/pip/_internal/vcs/mercurial.py | 8 ++++---- src/pip/_internal/vcs/subversion.py | 12 ++++++------ src/pip/_internal/vcs/versioncontrol.py | 21 +++++++++++---------- 8 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 news/10080.bugfix.rst diff --git a/news/10080.bugfix.rst b/news/10080.bugfix.rst new file mode 100644 index 00000000000..f1aa2d6a8f3 --- /dev/null +++ b/news/10080.bugfix.rst @@ -0,0 +1 @@ +Correctly allow PEP 517 projects to be detected without warnings in ``pip freeze``. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3d074f9f629..247e63fc86c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -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 @@ -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 " diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 3f9e7dd7734..0887102eae4 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -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 diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index d88f3f46ae6..99ebea30c91 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -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): diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 364ccca6ca5..b860e350a2d 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -18,7 +18,7 @@ RemoteNotValidError, RevOptions, VersionControl, - find_path_to_setup_from_repo_root, + find_path_to_project_root_from_repo_root, vcs, ) @@ -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( @@ -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): diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index b4f887d327b..8f8b09bd239 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -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, ) @@ -120,8 +120,8 @@ 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( @@ -129,7 +129,7 @@ def get_subdirectory(cls, 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): diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 4d1237ca0ca..965e0b425a4 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -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 @@ -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, ) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index d06c81032b0..cddd78c5ec2 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -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 @@ -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, ) @@ -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