diff --git a/news/9547.feature.rst b/news/9547.feature.rst new file mode 100644 index 00000000000..364a8f68817 --- /dev/null +++ b/news/9547.feature.rst @@ -0,0 +1 @@ +Add support for editable installs for project with only setup.cfg files. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 810fc085988..b279bccbcd2 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -77,16 +77,19 @@ def parse_editable(editable_req): url_no_extras, extras = _strip_extras(url) if os.path.isdir(url_no_extras): - if not os.path.exists(os.path.join(url_no_extras, 'setup.py')): + setup_py = os.path.join(url_no_extras, 'setup.py') + setup_cfg = os.path.join(url_no_extras, 'setup.cfg') + if not os.path.exists(setup_py) and not os.path.exists(setup_cfg): msg = ( - 'File "setup.py" not found. Directory cannot be installed ' - 'in editable mode: {}'.format(os.path.abspath(url_no_extras)) + 'File "setup.py" or "setup.cfg" not found. Directory cannot be ' + 'installed in editable mode: {}' + .format(os.path.abspath(url_no_extras)) ) pyproject_path = make_pyproject_path(url_no_extras) if os.path.isfile(pyproject_path): msg += ( '\n(A "pyproject.toml" file was found, but editable ' - 'mode currently requires a setup.py based build.)' + 'mode currently requires a setuptools-based build.)' ) raise InstallationError(msg) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index e16deecd580..4b8e4b359f3 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -8,9 +8,11 @@ # invoking via the shim. This avoids e.g. the following manifest_maker # warning: "warning: manifest_maker: standard file '-c' not found". _SETUPTOOLS_SHIM = ( - "import sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};" - "f=getattr(tokenize, 'open', open)(__file__);" - "code=f.read().replace('\\r\\n', '\\n');" + "import io, os, sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};" + "f = getattr(tokenize, 'open', open)(__file__) " + "if os.path.exists(__file__) " + "else io.StringIO('from setuptools import setup; setup()');" + "code = f.read().replace('\\r\\n', '\\n');" "f.close();" "exec(compile(code, __file__, 'exec'))" ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 95d9b73aa44..369c15a7619 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -641,9 +641,9 @@ def test_editable_install__local_dir_no_setup_py( msg = result.stderr if deprecated_python: - assert 'File "setup.py" not found. ' in msg + assert 'File "setup.py" or "setup.cfg" not found. ' in msg else: - assert msg.startswith('ERROR: File "setup.py" not found. ') + assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ') assert 'pyproject.toml' not in msg @@ -663,9 +663,9 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( msg = result.stderr if deprecated_python: - assert 'File "setup.py" not found. ' in msg + assert 'File "setup.py" or "setup.cfg" not found. ' in msg else: - assert msg.startswith('ERROR: File "setup.py" not found. ') + assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ') assert 'A "pyproject.toml" file was found' in msg @@ -1034,15 +1034,13 @@ def test_install_package_with_prefix(script, data): result.did_create(install_path) -def test_install_editable_with_prefix(script): +def _test_install_editable_with_prefix(script, files): # make a dummy project pkga_path = script.scratch_path / 'pkga' pkga_path.mkdir() - pkga_path.joinpath("setup.py").write_text(textwrap.dedent(""" - from setuptools import setup - setup(name='pkga', - version='0.1') - """)) + + for fn, contents in files.items(): + pkga_path.joinpath(fn).write_text(textwrap.dedent(contents)) if hasattr(sys, "pypy_version_info"): site_packages = os.path.join( @@ -1087,6 +1085,28 @@ def test_install_editable_with_target(script): result.did_create(script.scratch / 'target' / 'watching_testrunner.py') +def test_install_editable_with_prefix_setup_py(script): + setup_py = """ +from setuptools import setup +setup(name='pkga', version='0.1') +""" + _test_install_editable_with_prefix(script, {"setup.py": setup_py}) + + +def test_install_editable_with_prefix_setup_cfg(script): + setup_cfg = """[metadata] +name = pkga +version = 0.1 +""" + pyproject_toml = """[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" +""" + _test_install_editable_with_prefix( + script, {"setup.cfg": setup_cfg, "pyproject.toml": pyproject_toml} + ) + + def test_install_package_conflict_prefix_and_user(script, data): """ Test installing a package using pip install --prefix --user errors out