Skip to content

Commit e7b8084

Browse files
authored
Merge pull request #4963 from pypa/debt/remove-easy-install
Remove easy_install and package_index
2 parents 5061e67 + 1ef1ee1 commit e7b8084

12 files changed

+564
-4474
lines changed

newsfragments/917.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removed easy_install and package_index modules.

setuptools/_scripts.py

+353
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import re
5+
import shlex
6+
import shutil
7+
import struct
8+
import subprocess
9+
import sys
10+
import textwrap
11+
from collections.abc import Iterable
12+
from typing import TYPE_CHECKING, TypedDict
13+
14+
import pkg_resources
15+
16+
if TYPE_CHECKING:
17+
from typing_extensions import Self
18+
19+
from .warnings import SetuptoolsWarning
20+
21+
from distutils.command.build_scripts import first_line_re
22+
from distutils.util import get_platform
23+
24+
25+
class _SplitArgs(TypedDict, total=False):
26+
comments: bool
27+
posix: bool
28+
29+
30+
class CommandSpec(list):
31+
"""
32+
A command spec for a #! header, specified as a list of arguments akin to
33+
those passed to Popen.
34+
"""
35+
36+
options: list[str] = []
37+
split_args = _SplitArgs()
38+
39+
@classmethod
40+
def best(cls):
41+
"""
42+
Choose the best CommandSpec class based on environmental conditions.
43+
"""
44+
return cls
45+
46+
@classmethod
47+
def _sys_executable(cls):
48+
_default = os.path.normpath(sys.executable)
49+
return os.environ.get('__PYVENV_LAUNCHER__', _default)
50+
51+
@classmethod
52+
def from_param(cls, param: Self | str | Iterable[str] | None) -> Self:
53+
"""
54+
Construct a CommandSpec from a parameter to build_scripts, which may
55+
be None.
56+
"""
57+
if isinstance(param, cls):
58+
return param
59+
if isinstance(param, str):
60+
return cls.from_string(param)
61+
if isinstance(param, Iterable):
62+
return cls(param)
63+
if param is None:
64+
return cls.from_environment()
65+
raise TypeError(f"Argument has an unsupported type {type(param)}")
66+
67+
@classmethod
68+
def from_environment(cls):
69+
return cls([cls._sys_executable()])
70+
71+
@classmethod
72+
def from_string(cls, string: str) -> Self:
73+
"""
74+
Construct a command spec from a simple string representing a command
75+
line parseable by shlex.split.
76+
"""
77+
items = shlex.split(string, **cls.split_args)
78+
return cls(items)
79+
80+
def install_options(self, script_text: str):
81+
self.options = shlex.split(self._extract_options(script_text))
82+
cmdline = subprocess.list2cmdline(self)
83+
if not isascii(cmdline):
84+
self.options[:0] = ['-x']
85+
86+
@staticmethod
87+
def _extract_options(orig_script):
88+
"""
89+
Extract any options from the first line of the script.
90+
"""
91+
first = (orig_script + '\n').splitlines()[0]
92+
match = _first_line_re().match(first)
93+
options = match.group(1) or '' if match else ''
94+
return options.strip()
95+
96+
def as_header(self):
97+
return self._render(self + list(self.options))
98+
99+
@staticmethod
100+
def _strip_quotes(item):
101+
_QUOTES = '"\''
102+
for q in _QUOTES:
103+
if item.startswith(q) and item.endswith(q):
104+
return item[1:-1]
105+
return item
106+
107+
@staticmethod
108+
def _render(items):
109+
cmdline = subprocess.list2cmdline(
110+
CommandSpec._strip_quotes(item.strip()) for item in items
111+
)
112+
return '#!' + cmdline + '\n'
113+
114+
115+
class WindowsCommandSpec(CommandSpec):
116+
split_args = _SplitArgs(posix=False)
117+
118+
119+
class ScriptWriter:
120+
"""
121+
Encapsulates behavior around writing entry point scripts for console and
122+
gui apps.
123+
"""
124+
125+
template = textwrap.dedent(
126+
r"""
127+
# EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
128+
import re
129+
import sys
130+
131+
# for compatibility with easy_install; see #2198
132+
__requires__ = %(spec)r
133+
134+
try:
135+
from importlib.metadata import distribution
136+
except ImportError:
137+
try:
138+
from importlib_metadata import distribution
139+
except ImportError:
140+
from pkg_resources import load_entry_point
141+
142+
143+
def importlib_load_entry_point(spec, group, name):
144+
dist_name, _, _ = spec.partition('==')
145+
matches = (
146+
entry_point
147+
for entry_point in distribution(dist_name).entry_points
148+
if entry_point.group == group and entry_point.name == name
149+
)
150+
return next(matches).load()
151+
152+
153+
globals().setdefault('load_entry_point', importlib_load_entry_point)
154+
155+
156+
if __name__ == '__main__':
157+
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
158+
sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
159+
"""
160+
).lstrip()
161+
162+
command_spec_class = CommandSpec
163+
164+
@classmethod
165+
def get_args(cls, dist, header=None):
166+
"""
167+
Yield write_script() argument tuples for a distribution's
168+
console_scripts and gui_scripts entry points.
169+
"""
170+
if header is None:
171+
header = cls.get_header()
172+
spec = str(dist.as_requirement())
173+
for type_ in 'console', 'gui':
174+
group = type_ + '_scripts'
175+
for name in dist.get_entry_map(group).keys():
176+
cls._ensure_safe_name(name)
177+
script_text = cls.template % locals()
178+
args = cls._get_script_args(type_, name, header, script_text)
179+
yield from args
180+
181+
@staticmethod
182+
def _ensure_safe_name(name):
183+
"""
184+
Prevent paths in *_scripts entry point names.
185+
"""
186+
has_path_sep = re.search(r'[\\/]', name)
187+
if has_path_sep:
188+
raise ValueError("Path separators not allowed in script names")
189+
190+
@classmethod
191+
def best(cls):
192+
"""
193+
Select the best ScriptWriter for this environment.
194+
"""
195+
if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
196+
return WindowsScriptWriter.best()
197+
else:
198+
return cls
199+
200+
@classmethod
201+
def _get_script_args(cls, type_, name, header, script_text):
202+
# Simply write the stub with no extension.
203+
yield (name, header + script_text)
204+
205+
@classmethod
206+
def get_header(
207+
cls,
208+
script_text: str = "",
209+
executable: str | CommandSpec | Iterable[str] | None = None,
210+
) -> str:
211+
"""Create a #! line, getting options (if any) from script_text"""
212+
cmd = cls.command_spec_class.best().from_param(executable)
213+
cmd.install_options(script_text)
214+
return cmd.as_header()
215+
216+
217+
class WindowsScriptWriter(ScriptWriter):
218+
command_spec_class = WindowsCommandSpec
219+
220+
@classmethod
221+
def best(cls):
222+
"""
223+
Select the best ScriptWriter suitable for Windows
224+
"""
225+
writer_lookup = dict(
226+
executable=WindowsExecutableLauncherWriter,
227+
natural=cls,
228+
)
229+
# for compatibility, use the executable launcher by default
230+
launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')
231+
return writer_lookup[launcher]
232+
233+
@classmethod
234+
def _get_script_args(cls, type_, name, header, script_text):
235+
"For Windows, add a .py extension"
236+
ext = dict(console='.pya', gui='.pyw')[type_]
237+
if ext not in os.environ['PATHEXT'].lower().split(';'):
238+
msg = (
239+
"{ext} not listed in PATHEXT; scripts will not be "
240+
"recognized as executables."
241+
).format(**locals())
242+
SetuptoolsWarning.emit(msg)
243+
old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
244+
old.remove(ext)
245+
header = cls._adjust_header(type_, header)
246+
blockers = [name + x for x in old]
247+
yield name + ext, header + script_text, 't', blockers
248+
249+
@classmethod
250+
def _adjust_header(cls, type_, orig_header):
251+
"""
252+
Make sure 'pythonw' is used for gui and 'python' is used for
253+
console (regardless of what sys.executable is).
254+
"""
255+
pattern = 'pythonw.exe'
256+
repl = 'python.exe'
257+
if type_ == 'gui':
258+
pattern, repl = repl, pattern
259+
pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
260+
new_header = pattern_ob.sub(string=orig_header, repl=repl)
261+
return new_header if cls._use_header(new_header) else orig_header
262+
263+
@staticmethod
264+
def _use_header(new_header):
265+
"""
266+
Should _adjust_header use the replaced header?
267+
268+
On non-windows systems, always use. On
269+
Windows systems, only use the replaced header if it resolves
270+
to an executable on the system.
271+
"""
272+
clean_header = new_header[2:-1].strip('"')
273+
return sys.platform != 'win32' or shutil.which(clean_header)
274+
275+
276+
class WindowsExecutableLauncherWriter(WindowsScriptWriter):
277+
@classmethod
278+
def _get_script_args(cls, type_, name, header, script_text):
279+
"""
280+
For Windows, add a .py extension and an .exe launcher
281+
"""
282+
if type_ == 'gui':
283+
launcher_type = 'gui'
284+
ext = '-script.pyw'
285+
old = ['.pyw']
286+
else:
287+
launcher_type = 'cli'
288+
ext = '-script.py'
289+
old = ['.py', '.pyc', '.pyo']
290+
hdr = cls._adjust_header(type_, header)
291+
blockers = [name + x for x in old]
292+
yield (name + ext, hdr + script_text, 't', blockers)
293+
yield (
294+
name + '.exe',
295+
get_win_launcher(launcher_type),
296+
'b', # write in binary mode
297+
)
298+
if not is_64bit():
299+
# install a manifest for the launcher to prevent Windows
300+
# from detecting it as an installer (which it will for
301+
# launchers like easy_install.exe). Consider only
302+
# adding a manifest for launchers detected as installers.
303+
# See Distribute #143 for details.
304+
m_name = name + '.exe.manifest'
305+
yield (m_name, load_launcher_manifest(name), 't')
306+
307+
308+
def get_win_launcher(type):
309+
"""
310+
Load the Windows launcher (executable) suitable for launching a script.
311+
312+
`type` should be either 'cli' or 'gui'
313+
314+
Returns the executable as a byte string.
315+
"""
316+
launcher_fn = f'{type}.exe'
317+
if is_64bit():
318+
if get_platform() == "win-arm64":
319+
launcher_fn = launcher_fn.replace(".", "-arm64.")
320+
else:
321+
launcher_fn = launcher_fn.replace(".", "-64.")
322+
else:
323+
launcher_fn = launcher_fn.replace(".", "-32.")
324+
return pkg_resources.resource_string('setuptools', launcher_fn)
325+
326+
327+
def load_launcher_manifest(name):
328+
manifest = pkg_resources.resource_string(__name__, 'launcher manifest.xml')
329+
return manifest.decode('utf-8') % vars()
330+
331+
332+
def _first_line_re():
333+
"""
334+
Return a regular expression based on first_line_re suitable for matching
335+
strings.
336+
"""
337+
if isinstance(first_line_re.pattern, str):
338+
return first_line_re
339+
340+
# first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.
341+
return re.compile(first_line_re.pattern.decode())
342+
343+
344+
def is_64bit():
345+
return struct.calcsize("P") == 8
346+
347+
348+
def isascii(s):
349+
try:
350+
s.encode('ascii')
351+
except UnicodeError:
352+
return False
353+
return True

setuptools/_shutil.py

+6
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,9 @@ def rmtree(path, ignore_errors=False, onexc=_auto_chmod):
5151
def rmdir(path, **opts):
5252
if os.path.isdir(path):
5353
rmtree(path, **opts)
54+
55+
56+
def current_umask():
57+
tmp = os.umask(0o022)
58+
os.umask(tmp)
59+
return tmp

0 commit comments

Comments
 (0)