Skip to content

Commit 9653305

Browse files
authored
Merge pull request #4955 from pypa/debt/develop-uses-pip
Rely on pip for setup.py develop
2 parents 607f6be + da119e7 commit 9653305

File tree

5 files changed

+41
-308
lines changed

5 files changed

+41
-308
lines changed

newsfragments/4955.removal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Develop command no longer uses easy_install, but instead defers execution to pip (which then will re-invoke Setuptools via PEP 517 to build the editable wheel). Most of the options to develop are dropped. This is the final warning before the command is dropped completely in a few months. Use-cases relying on 'setup.py develop' should pin to older Setuptools version or migrate to modern build tooling.

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ filterwarnings=
5252
# https://github.com/pypa/setuptools/issues/917
5353
ignore:setup.py install is deprecated.
5454
ignore:easy_install command is deprecated.
55+
ignore:develop command is deprecated.
5556

5657
# https://github.com/pypa/setuptools/issues/2497
5758
ignore:.* is an invalid version and will not be supported::pkg_resources

setuptools/command/develop.py

Lines changed: 39 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,55 @@
1-
import glob
2-
import os
1+
import site
2+
import subprocess
3+
import sys
34

4-
import setuptools
5-
from setuptools import _normalization, _path, namespaces
6-
from setuptools.command.easy_install import easy_install
5+
from setuptools import Command
6+
from setuptools.warnings import SetuptoolsDeprecationWarning
77

8-
from ..unicode_utils import _read_utf8_with_fallback
98

10-
from distutils import log
11-
from distutils.errors import DistutilsOptionError
12-
from distutils.util import convert_path
13-
14-
15-
class develop(namespaces.DevelopInstaller, easy_install):
9+
class develop(Command):
1610
"""Set up package for development"""
1711

18-
description = "install package in 'development mode'"
19-
20-
user_options = easy_install.user_options + [
21-
("uninstall", "u", "Uninstall this source package"),
22-
("egg-path=", None, "Set the path to be used in the .egg-link file"),
12+
user_options = [
13+
("install-dir=", "d", "install package to DIR"),
14+
('no-deps', 'N', "don't install dependencies"),
15+
('user', None, f"install in user site-package '{site.USER_SITE}'"),
16+
('prefix=', None, "installation prefix"),
17+
("index-url=", "i", "base URL of Python Package Index"),
18+
]
19+
boolean_options = [
20+
'no-deps',
21+
'user',
2322
]
2423

25-
boolean_options = easy_install.boolean_options + ['uninstall']
26-
27-
command_consumes_arguments = False # override base
24+
install_dir = None
25+
no_deps = False
26+
user = False
27+
prefix = None
28+
index_url = None
2829

2930
def run(self):
30-
if self.uninstall:
31-
self.multi_version = True
32-
self.uninstall_link()
33-
self.uninstall_namespaces()
34-
else:
35-
self.install_for_development()
36-
self.warn_deprecated_options()
31+
cmd = (
32+
[sys.executable, '-m', 'pip', 'install', '-e', '.', '--use-pep517']
33+
+ ['--target', self.install_dir] * bool(self.install_dir)
34+
+ ['--no-deps'] * self.no_deps
35+
+ ['--user'] * self.user
36+
+ ['--prefix', self.prefix] * bool(self.prefix)
37+
+ ['--index-url', self.index_url] * bool(self.prefix)
38+
)
39+
subprocess.check_call(cmd)
3740

3841
def initialize_options(self):
39-
self.uninstall = None
40-
self.egg_path = None
41-
easy_install.initialize_options(self)
42-
self.setup_path = None
43-
self.always_copy_from = '.' # always copy eggs installed in curdir
42+
DevelopDeprecationWarning.emit()
4443

4544
def finalize_options(self) -> None:
46-
import pkg_resources
47-
48-
ei = self.get_finalized_command("egg_info")
49-
self.args = [ei.egg_name]
50-
51-
easy_install.finalize_options(self)
52-
self.expand_basedirs()
53-
self.expand_dirs()
54-
# pick up setup-dir .egg files only: no .egg-info
55-
self.package_index.scan(glob.glob('*.egg'))
56-
57-
egg_link_fn = (
58-
_normalization.filename_component_broken(ei.egg_name) + '.egg-link'
59-
)
60-
self.egg_link = os.path.join(self.install_dir, egg_link_fn)
61-
self.egg_base = ei.egg_base
62-
if self.egg_path is None:
63-
self.egg_path = os.path.abspath(ei.egg_base)
64-
65-
target = _path.normpath(self.egg_base)
66-
egg_path = _path.normpath(os.path.join(self.install_dir, self.egg_path))
67-
if egg_path != target:
68-
raise DistutilsOptionError(
69-
"--egg-path must be a relative path from the install"
70-
" directory to " + target
71-
)
72-
73-
# Make a distribution for the package's source
74-
self.dist = pkg_resources.Distribution(
75-
target,
76-
pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),
77-
project_name=ei.egg_name,
78-
)
79-
80-
self.setup_path = self._resolve_setup_path(
81-
self.egg_base,
82-
self.install_dir,
83-
self.egg_path,
84-
)
85-
86-
@staticmethod
87-
def _resolve_setup_path(egg_base, install_dir, egg_path):
88-
"""
89-
Generate a path from egg_base back to '.' where the
90-
setup script resides and ensure that path points to the
91-
setup path from $install_dir/$egg_path.
92-
"""
93-
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
94-
if path_to_setup != os.curdir:
95-
path_to_setup = '../' * (path_to_setup.count('/') + 1)
96-
resolved = _path.normpath(os.path.join(install_dir, egg_path, path_to_setup))
97-
curdir = _path.normpath(os.curdir)
98-
if resolved != curdir:
99-
raise DistutilsOptionError(
100-
"Can't get a consistent path to setup script from"
101-
" installation directory",
102-
resolved,
103-
curdir,
104-
)
105-
return path_to_setup
106-
107-
def install_for_development(self) -> None:
108-
self.run_command('egg_info')
109-
110-
# Build extensions in-place
111-
self.reinitialize_command('build_ext', inplace=True)
112-
self.run_command('build_ext')
45+
pass
11346

114-
if setuptools.bootstrap_install_from:
115-
self.easy_install(setuptools.bootstrap_install_from)
116-
setuptools.bootstrap_install_from = None
11747

118-
self.install_namespaces()
119-
120-
# create an .egg-link in the installation dir, pointing to our egg
121-
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
122-
if not self.dry_run:
123-
with open(self.egg_link, "w", encoding="utf-8") as f:
124-
f.write(self.egg_path + "\n" + self.setup_path)
125-
# postprocess the installed distro, fixing up .pth, installing scripts,
126-
# and handling requirements
127-
self.process_distribution(None, self.dist, not self.no_deps)
128-
129-
def uninstall_link(self) -> None:
130-
if os.path.exists(self.egg_link):
131-
log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
132-
133-
contents = [
134-
line.rstrip()
135-
for line in _read_utf8_with_fallback(self.egg_link).splitlines()
136-
]
137-
138-
if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
139-
log.warn("Link points to %s: uninstall aborted", contents)
140-
return
141-
if not self.dry_run:
142-
os.unlink(self.egg_link)
143-
if not self.dry_run:
144-
self.update_pth(self.dist) # remove any .pth link to us
145-
if self.distribution.scripts:
146-
# XXX should also check for entry point scripts!
147-
log.warn("Note: you must uninstall or replace scripts manually!")
148-
149-
def install_egg_scripts(self, dist):
150-
if dist is not self.dist:
151-
# Installing a dependency, so fall back to normal behavior
152-
return easy_install.install_egg_scripts(self, dist)
153-
154-
# create wrapper scripts in the script dir, pointing to dist.scripts
155-
156-
# new-style...
157-
self.install_wrapper_scripts(dist)
158-
159-
# ...and old-style
160-
for script_name in self.distribution.scripts or []:
161-
script_path = os.path.abspath(convert_path(script_name))
162-
script_name = os.path.basename(script_path)
163-
script_text = _read_utf8_with_fallback(script_path)
164-
self.install_script(dist, script_name, script_text, script_path)
165-
166-
return None
167-
168-
def install_wrapper_scripts(self, dist):
169-
dist = VersionlessRequirement(dist)
170-
return easy_install.install_wrapper_scripts(self, dist)
171-
172-
173-
class VersionlessRequirement:
174-
"""
175-
Adapt a pkg_resources.Distribution to simply return the project
176-
name as the 'requirement' so that scripts will work across
177-
multiple versions.
178-
179-
>>> from pkg_resources import Distribution
180-
>>> dist = Distribution(project_name='foo', version='1.0')
181-
>>> str(dist.as_requirement())
182-
'foo==1.0'
183-
>>> adapted_dist = VersionlessRequirement(dist)
184-
>>> str(adapted_dist.as_requirement())
185-
'foo'
48+
class DevelopDeprecationWarning(SetuptoolsDeprecationWarning):
49+
_SUMMARY = "develop command is deprecated."
50+
_DETAILS = """
51+
Please avoid running ``setup.py`` and ``develop``.
52+
Instead, use standards-based tools like pip or uv.
18653
"""
187-
188-
def __init__(self, dist) -> None:
189-
self.__dist = dist
190-
191-
def __getattr__(self, name: str):
192-
return getattr(self.__dist, name)
193-
194-
def as_requirement(self):
195-
return self.project_name
54+
_SEE_URL = "https://github.com/pypa/setuptools/issues/917"
55+
_DUE_DATE = 2025, 10, 31

setuptools/tests/test_develop.py

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""develop tests"""
22

33
import os
4-
import pathlib
54
import platform
65
import subprocess
76
import sys
87

98
import pytest
109

1110
from setuptools._path import paths_on_pythonpath
12-
from setuptools.command.develop import develop
13-
from setuptools.dist import Distribution
1411

1512
from . import contexts, namespaces
1613

@@ -51,67 +48,6 @@ def test_env(tmpdir, temp_user):
5148
yield target
5249

5350

54-
class TestDevelop:
55-
in_virtualenv = hasattr(sys, 'real_prefix')
56-
in_venv = hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
57-
58-
def test_console_scripts(self, tmpdir):
59-
"""
60-
Test that console scripts are installed and that they reference
61-
only the project by name and not the current version.
62-
"""
63-
pytest.skip(
64-
"TODO: needs a fixture to cause 'develop' "
65-
"to be invoked without mutating environment."
66-
)
67-
settings = dict(
68-
name='foo',
69-
packages=['foo'],
70-
version='0.0',
71-
entry_points={
72-
'console_scripts': [
73-
'foocmd = foo:foo',
74-
],
75-
},
76-
)
77-
dist = Distribution(settings)
78-
dist.script_name = 'setup.py'
79-
cmd = develop(dist)
80-
cmd.ensure_finalized()
81-
cmd.install_dir = tmpdir
82-
cmd.run()
83-
# assert '0.0' not in foocmd_text
84-
85-
@pytest.mark.xfail(reason="legacy behavior retained for compatibility #4167")
86-
def test_egg_link_filename(self):
87-
settings = dict(
88-
name='Foo $$$ Bar_baz-bing',
89-
)
90-
dist = Distribution(settings)
91-
cmd = develop(dist)
92-
cmd.ensure_finalized()
93-
link = pathlib.Path(cmd.egg_link)
94-
assert link.suffix == '.egg-link'
95-
assert link.stem == 'Foo_Bar_baz_bing'
96-
97-
98-
class TestResolver:
99-
"""
100-
TODO: These tests were written with a minimal understanding
101-
of what _resolve_setup_path is intending to do. Come up with
102-
more meaningful cases that look like real-world scenarios.
103-
"""
104-
105-
def test_resolve_setup_path_cwd(self):
106-
assert develop._resolve_setup_path('.', '.', '.') == '.'
107-
108-
def test_resolve_setup_path_one_dir(self):
109-
assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../'
110-
111-
def test_resolve_setup_path_one_dir_trailing_slash(self):
112-
assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../'
113-
114-
11551
class TestNamespaces:
11652
@staticmethod
11753
def install_develop(src_dir, target):

0 commit comments

Comments
 (0)