diff --git a/news/9944.bugfix.rst b/news/9944.bugfix.rst new file mode 100644 index 00000000000..58bb70b78f3 --- /dev/null +++ b/news/9944.bugfix.rst @@ -0,0 +1,2 @@ +Emit clearer error message when a project root does not contain either +``pyproject.toml``, ``setup.py`` or ``setup.cfg``. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 55c17ac8b45..c2eea37123e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -509,6 +509,19 @@ def load_pyproject_toml(self): self.unpacked_source_directory, backend, backend_path=backend_path, ) + def _check_setup_py_or_cfg_exists(self) -> bool: + """Check if the requirement actually has a setuptools build file. + + If setup.py does not exist, we also check setup.cfg in the same + directory and allow the directory if that exists. + """ + if os.path.exists(self.setup_py_path): + return True + stem, ext = os.path.splitext(self.setup_py_path) + if ext == ".py" and os.path.exists(f"{stem}.cfg"): + return True + return False + def _generate_metadata(self): # type: () -> str """Invokes metadata generator functions, with the required arguments. @@ -516,6 +529,12 @@ def _generate_metadata(self): if not self.use_pep517: assert self.unpacked_source_directory + if not self._check_setup_py_or_cfg_exists(): + raise InstallationError( + f'File "setup.py" or "setup.cfg" not found for legacy ' + f'project {self}.' + ) + return generate_metadata_legacy( build_env=self.build_env, setup_py_path=self.setup_py_path, diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 26037dbdcbb..a4ad35be61c 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -269,18 +269,14 @@ def tabulate(rows): return table, sizes -def is_installable_dir(path): - # type: (str) -> bool - """Is path is a directory containing setup.py or pyproject.toml?""" +def is_installable_dir(path: str) -> bool: + """Is path is a directory containing pyproject.toml, setup.cfg or setup.py?""" if not os.path.isdir(path): return False - setup_py = os.path.join(path, "setup.py") - if os.path.isfile(setup_py): - return True - pyproject_toml = os.path.join(path, "pyproject.toml") - if os.path.isfile(pyproject_toml): - return True - return False + return any( + os.path.isfile(os.path.join(path, signifier)) + for signifier in ("pyproject.toml", "setup.cfg", "setup.py") + ) def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):