up
This commit is contained in:
@@ -1,20 +1,11 @@
|
||||
# mypy: disable_error_code=call-overload
|
||||
# pyright: reportCallIssue=false, reportArgumentType=false
|
||||
# Can't disable on the exact line because distutils doesn't exists on Python 3.12
|
||||
# and type-checkers aren't aware of distutils_hack,
|
||||
# causing distutils.command.bdist.bdist.format_commands to be Any.
|
||||
|
||||
import sys
|
||||
|
||||
from distutils.command.bdist import bdist
|
||||
import sys
|
||||
|
||||
if 'egg' not in bdist.format_commands:
|
||||
try:
|
||||
# format_commands is a dict in vendored distutils
|
||||
# It used to be a list in older (stdlib) distutils
|
||||
# We support both for backwards compatibility
|
||||
bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file")
|
||||
except TypeError:
|
||||
# For backward compatibility with older distutils (stdlib)
|
||||
bdist.format_command['egg'] = ('bdist_egg', "Python .egg file")
|
||||
bdist.format_commands.append('egg')
|
||||
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,131 +0,0 @@
|
||||
"""Helper code used to generate ``requires.txt`` files in the egg-info directory.
|
||||
|
||||
The ``requires.txt`` file has an specific format:
|
||||
- Environment markers need to be part of the section headers and
|
||||
should not be part of the requirement spec itself.
|
||||
|
||||
See https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#requires-txt
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from itertools import filterfalse
|
||||
from typing import TypeVar
|
||||
|
||||
from jaraco.text import yield_lines
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
from .. import _reqs
|
||||
from .._reqs import _StrOrIter
|
||||
|
||||
# dict can work as an ordered set
|
||||
_T = TypeVar("_T")
|
||||
_Ordered = dict[_T, None]
|
||||
|
||||
|
||||
def _prepare(
|
||||
install_requires: _StrOrIter, extras_require: Mapping[str, _StrOrIter]
|
||||
) -> tuple[list[str], dict[str, list[str]]]:
|
||||
"""Given values for ``install_requires`` and ``extras_require``
|
||||
create modified versions in a way that can be written in ``requires.txt``
|
||||
"""
|
||||
extras = _convert_extras_requirements(extras_require)
|
||||
return _move_install_requirements_markers(install_requires, extras)
|
||||
|
||||
|
||||
def _convert_extras_requirements(
|
||||
extras_require: Mapping[str, _StrOrIter],
|
||||
) -> defaultdict[str, _Ordered[Requirement]]:
|
||||
"""
|
||||
Convert requirements in `extras_require` of the form
|
||||
`"extra": ["barbazquux; {marker}"]` to
|
||||
`"extra:{marker}": ["barbazquux"]`.
|
||||
"""
|
||||
output = defaultdict[str, _Ordered[Requirement]](dict)
|
||||
for section, v in extras_require.items():
|
||||
# Do not strip empty sections.
|
||||
output[section]
|
||||
for r in _reqs.parse(v):
|
||||
output[section + _suffix_for(r)].setdefault(r)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _move_install_requirements_markers(
|
||||
install_requires: _StrOrIter, extras_require: Mapping[str, _Ordered[Requirement]]
|
||||
) -> tuple[list[str], dict[str, list[str]]]:
|
||||
"""
|
||||
The ``requires.txt`` file has an specific format:
|
||||
- Environment markers need to be part of the section headers and
|
||||
should not be part of the requirement spec itself.
|
||||
|
||||
Move requirements in ``install_requires`` that are using environment
|
||||
markers ``extras_require``.
|
||||
"""
|
||||
|
||||
# divide the install_requires into two sets, simple ones still
|
||||
# handled by install_requires and more complex ones handled by extras_require.
|
||||
|
||||
inst_reqs = list(_reqs.parse(install_requires))
|
||||
simple_reqs = filter(_no_marker, inst_reqs)
|
||||
complex_reqs = filterfalse(_no_marker, inst_reqs)
|
||||
simple_install_requires = list(map(str, simple_reqs))
|
||||
|
||||
for r in complex_reqs:
|
||||
extras_require[':' + str(r.marker)].setdefault(r)
|
||||
|
||||
expanded_extras = dict(
|
||||
# list(dict.fromkeys(...)) ensures a list of unique strings
|
||||
(k, list(dict.fromkeys(str(r) for r in map(_clean_req, v))))
|
||||
for k, v in extras_require.items()
|
||||
)
|
||||
|
||||
return simple_install_requires, expanded_extras
|
||||
|
||||
|
||||
def _suffix_for(req):
|
||||
"""Return the 'extras_require' suffix for a given requirement."""
|
||||
return ':' + str(req.marker) if req.marker else ''
|
||||
|
||||
|
||||
def _clean_req(req):
|
||||
"""Given a Requirement, remove environment markers and return it"""
|
||||
r = Requirement(str(req)) # create a copy before modifying
|
||||
r.marker = None
|
||||
return r
|
||||
|
||||
|
||||
def _no_marker(req):
|
||||
return not req.marker
|
||||
|
||||
|
||||
def _write_requirements(stream, reqs):
|
||||
lines = yield_lines(reqs or ())
|
||||
|
||||
def append_cr(line):
|
||||
return line + '\n'
|
||||
|
||||
lines = map(append_cr, lines)
|
||||
stream.writelines(lines)
|
||||
|
||||
|
||||
def write_requirements(cmd, basename, filename):
|
||||
dist = cmd.distribution
|
||||
data = io.StringIO()
|
||||
install_requires, extras_require = _prepare(
|
||||
dist.install_requires or (), dist.extras_require or {}
|
||||
)
|
||||
_write_requirements(data, install_requires)
|
||||
for extra in sorted(extras_require):
|
||||
data.write('\n[{extra}]\n'.format(**vars()))
|
||||
_write_requirements(data, extras_require[extra])
|
||||
cmd.write_or_delete_file("requirements", filename, data.getvalue())
|
||||
|
||||
|
||||
def write_setup_requirements(cmd, basename, filename):
|
||||
data = io.StringIO()
|
||||
_write_requirements(data, cmd.distribution.setup_requires)
|
||||
cmd.write_or_delete_file("setup-requirements", filename, data.getvalue())
|
||||
@@ -1,7 +1,7 @@
|
||||
from setuptools.command.setopt import config_file, edit_config, option_base
|
||||
|
||||
from distutils.errors import DistutilsOptionError
|
||||
|
||||
from setuptools.command.setopt import edit_config, option_base, config_file
|
||||
|
||||
|
||||
def shquote(arg):
|
||||
"""Quote an argument for later parsing by shlex.split()"""
|
||||
@@ -30,14 +30,15 @@ class alias(option_base):
|
||||
self.args = None
|
||||
self.remove = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
option_base.finalize_options(self)
|
||||
if self.remove and len(self.args) != 1:
|
||||
raise DistutilsOptionError(
|
||||
"Must specify exactly one argument (the alias name) when using --remove"
|
||||
"Must specify exactly one argument (the alias name) when "
|
||||
"using --remove"
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
aliases = self.distribution.get_option_dict('aliases')
|
||||
|
||||
if not self.args:
|
||||
@@ -48,14 +49,14 @@ class alias(option_base):
|
||||
return
|
||||
|
||||
elif len(self.args) == 1:
|
||||
(alias,) = self.args
|
||||
alias, = self.args
|
||||
if self.remove:
|
||||
command = None
|
||||
elif alias in aliases:
|
||||
print("setup.py alias", format_alias(alias, aliases))
|
||||
return
|
||||
else:
|
||||
print(f"No alias definition found for {alias!r}")
|
||||
print("No alias definition found for %r" % alias)
|
||||
return
|
||||
else:
|
||||
alias = self.args[0]
|
||||
@@ -73,5 +74,5 @@ def format_alias(name, aliases):
|
||||
elif source == config_file('local'):
|
||||
source = ''
|
||||
else:
|
||||
source = f'--filename={source!r}'
|
||||
source = '--filename=%r' % source
|
||||
return source + name + ' ' + command
|
||||
|
||||
@@ -2,30 +2,21 @@
|
||||
|
||||
Build .egg distributions"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import marshal
|
||||
from distutils.dir_util import remove_tree, mkpath
|
||||
from distutils import log
|
||||
from types import CodeType
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
from sysconfig import get_path, get_python_version
|
||||
from types import CodeType
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
import marshal
|
||||
|
||||
from setuptools import Command
|
||||
from pkg_resources import get_build_platform, Distribution
|
||||
from setuptools.extension import Library
|
||||
from setuptools import Command
|
||||
from .._path import ensure_directory
|
||||
|
||||
from .._path import StrPathT, ensure_directory
|
||||
|
||||
from distutils import log
|
||||
from distutils.dir_util import mkpath, remove_tree
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
# Same as zipfile._ZipFileMode from typeshed
|
||||
_ZipFileMode: TypeAlias = Literal["r", "w", "x", "a"]
|
||||
from sysconfig import get_path, get_python_version
|
||||
|
||||
|
||||
def _get_purelib():
|
||||
@@ -50,9 +41,8 @@ def sorted_walk(dir):
|
||||
yield base, dirs, files
|
||||
|
||||
|
||||
def write_stub(resource, pyfile) -> None:
|
||||
_stub_template = textwrap.dedent(
|
||||
"""
|
||||
def write_stub(resource, pyfile):
|
||||
_stub_template = textwrap.dedent("""
|
||||
def __bootstrap__():
|
||||
global __bootstrap__, __loader__, __file__
|
||||
import sys, pkg_resources, importlib.util
|
||||
@@ -62,46 +52,44 @@ def write_stub(resource, pyfile) -> None:
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
__bootstrap__()
|
||||
"""
|
||||
).lstrip()
|
||||
with open(pyfile, 'w', encoding="utf-8") as f:
|
||||
""").lstrip()
|
||||
with open(pyfile, 'w') as f:
|
||||
f.write(_stub_template % resource)
|
||||
|
||||
|
||||
class bdist_egg(Command):
|
||||
description = 'create an "egg" distribution'
|
||||
description = "create an \"egg\" distribution"
|
||||
|
||||
user_options = [
|
||||
('bdist-dir=', 'b', "temporary directory for creating the distribution"),
|
||||
(
|
||||
'plat-name=',
|
||||
'p',
|
||||
"platform name to embed in generated filenames "
|
||||
"(by default uses `pkg_resources.get_build_platform()`)",
|
||||
),
|
||||
('exclude-source-files', None, "remove all .py files from the generated egg"),
|
||||
(
|
||||
'keep-temp',
|
||||
'k',
|
||||
"keep the pseudo-installation tree around after "
|
||||
"creating the distribution archive",
|
||||
),
|
||||
('dist-dir=', 'd', "directory to put final built distributions in"),
|
||||
('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
|
||||
('bdist-dir=', 'b',
|
||||
"temporary directory for creating the distribution"),
|
||||
('plat-name=', 'p', "platform name to embed in generated filenames "
|
||||
"(default: %s)" % get_build_platform()),
|
||||
('exclude-source-files', None,
|
||||
"remove all .py files from the generated egg"),
|
||||
('keep-temp', 'k',
|
||||
"keep the pseudo-installation tree around after " +
|
||||
"creating the distribution archive"),
|
||||
('dist-dir=', 'd',
|
||||
"directory to put final built distributions in"),
|
||||
('skip-build', None,
|
||||
"skip rebuilding everything (for testing/debugging)"),
|
||||
]
|
||||
|
||||
boolean_options = ['keep-temp', 'skip-build', 'exclude-source-files']
|
||||
boolean_options = [
|
||||
'keep-temp', 'skip-build', 'exclude-source-files'
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.bdist_dir = None
|
||||
self.plat_name = None
|
||||
self.keep_temp = False
|
||||
self.keep_temp = 0
|
||||
self.dist_dir = None
|
||||
self.skip_build = False
|
||||
self.skip_build = 0
|
||||
self.egg_output = None
|
||||
self.exclude_source_files = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
|
||||
self.egg_info = ei_cmd.egg_info
|
||||
|
||||
@@ -110,22 +98,22 @@ class bdist_egg(Command):
|
||||
self.bdist_dir = os.path.join(bdist_base, 'egg')
|
||||
|
||||
if self.plat_name is None:
|
||||
from pkg_resources import get_build_platform
|
||||
|
||||
self.plat_name = get_build_platform()
|
||||
|
||||
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
|
||||
|
||||
if self.egg_output is None:
|
||||
|
||||
# Compute filename of the output egg
|
||||
basename = ei_cmd._get_egg_basename(
|
||||
py_version=get_python_version(),
|
||||
platform=self.distribution.has_ext_modules() and self.plat_name,
|
||||
)
|
||||
basename = Distribution(
|
||||
None, None, ei_cmd.egg_name, ei_cmd.egg_version,
|
||||
get_python_version(),
|
||||
self.distribution.has_ext_modules() and self.plat_name
|
||||
).egg_name()
|
||||
|
||||
self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
|
||||
|
||||
def do_install_data(self) -> None:
|
||||
def do_install_data(self):
|
||||
# Hack for packages that install data to install's --install-lib
|
||||
self.get_finalized_command('install').install_lib = self.bdist_dir
|
||||
|
||||
@@ -140,13 +128,13 @@ class bdist_egg(Command):
|
||||
if normalized == site_packages or normalized.startswith(
|
||||
site_packages + os.sep
|
||||
):
|
||||
item = realpath[len(site_packages) + 1 :], item[1]
|
||||
item = realpath[len(site_packages) + 1:], item[1]
|
||||
# XXX else: raise ???
|
||||
self.distribution.data_files.append(item)
|
||||
|
||||
try:
|
||||
log.info("installing package data to %s", self.bdist_dir)
|
||||
self.call_command('install_data', force=False, root=None)
|
||||
self.call_command('install_data', force=0, root=None)
|
||||
finally:
|
||||
self.distribution.data_files = old
|
||||
|
||||
@@ -174,15 +162,16 @@ class bdist_egg(Command):
|
||||
instcmd.root = None
|
||||
if self.distribution.has_c_libraries() and not self.skip_build:
|
||||
self.run_command('build_clib')
|
||||
cmd = self.call_command('install_lib', warn_dir=False)
|
||||
cmd = self.call_command('install_lib', warn_dir=0)
|
||||
instcmd.root = old_root
|
||||
|
||||
all_outputs, ext_outputs = self.get_ext_outputs()
|
||||
self.stubs = []
|
||||
to_compile = []
|
||||
for p, ext_name in enumerate(ext_outputs):
|
||||
filename, _ext = os.path.splitext(ext_name)
|
||||
pyfile = os.path.join(self.bdist_dir, strip_module(filename) + '.py')
|
||||
for (p, ext_name) in enumerate(ext_outputs):
|
||||
filename, ext = os.path.splitext(ext_name)
|
||||
pyfile = os.path.join(self.bdist_dir, strip_module(filename) +
|
||||
'.py')
|
||||
self.stubs.append(pyfile)
|
||||
log.info("creating stub loader for %s", ext_name)
|
||||
if not self.dry_run:
|
||||
@@ -202,7 +191,8 @@ class bdist_egg(Command):
|
||||
if self.distribution.scripts:
|
||||
script_dir = os.path.join(egg_info, 'scripts')
|
||||
log.info("installing scripts to %s", script_dir)
|
||||
self.call_command('install_scripts', install_dir=script_dir, no_ep=True)
|
||||
self.call_command('install_scripts', install_dir=script_dir,
|
||||
no_ep=1)
|
||||
|
||||
self.copy_metadata_to(egg_info)
|
||||
native_libs = os.path.join(egg_info, "native_libs.txt")
|
||||
@@ -210,15 +200,18 @@ class bdist_egg(Command):
|
||||
log.info("writing %s", native_libs)
|
||||
if not self.dry_run:
|
||||
ensure_directory(native_libs)
|
||||
with open(native_libs, 'wt', encoding="utf-8") as libs_file:
|
||||
libs_file.write('\n'.join(all_outputs))
|
||||
libs_file.write('\n')
|
||||
libs_file = open(native_libs, 'wt')
|
||||
libs_file.write('\n'.join(all_outputs))
|
||||
libs_file.write('\n')
|
||||
libs_file.close()
|
||||
elif os.path.isfile(native_libs):
|
||||
log.info("removing %s", native_libs)
|
||||
if not self.dry_run:
|
||||
os.unlink(native_libs)
|
||||
|
||||
write_safety_flag(os.path.join(archive_root, 'EGG-INFO'), self.zip_safe())
|
||||
write_safety_flag(
|
||||
os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
|
||||
)
|
||||
|
||||
if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):
|
||||
log.warn(
|
||||
@@ -230,22 +223,14 @@ class bdist_egg(Command):
|
||||
self.zap_pyfiles()
|
||||
|
||||
# Make the archive
|
||||
make_zipfile(
|
||||
self.egg_output,
|
||||
archive_root,
|
||||
verbose=self.verbose,
|
||||
dry_run=self.dry_run,
|
||||
mode=self.gen_header(),
|
||||
)
|
||||
make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
|
||||
dry_run=self.dry_run, mode=self.gen_header())
|
||||
if not self.keep_temp:
|
||||
remove_tree(self.bdist_dir, dry_run=self.dry_run)
|
||||
|
||||
# Add to 'Distribution.dist_files' so that the "upload" command works
|
||||
getattr(self.distribution, 'dist_files', []).append((
|
||||
'bdist_egg',
|
||||
get_python_version(),
|
||||
self.egg_output,
|
||||
))
|
||||
getattr(self.distribution, 'dist_files', []).append(
|
||||
('bdist_egg', get_python_version(), self.egg_output))
|
||||
|
||||
def zap_pyfiles(self):
|
||||
log.info("Removing .py files from temporary directory")
|
||||
@@ -262,8 +247,11 @@ class bdist_egg(Command):
|
||||
|
||||
pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
|
||||
m = re.match(pattern, name)
|
||||
path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc')
|
||||
log.info(f"Renaming file from [{path_old}] to [{path_new}]")
|
||||
path_new = os.path.join(
|
||||
base, os.pardir, m.group('name') + '.pyc')
|
||||
log.info(
|
||||
"Renaming file from [%s] to [%s]"
|
||||
% (path_old, path_new))
|
||||
try:
|
||||
os.remove(path_new)
|
||||
except OSError:
|
||||
@@ -277,10 +265,10 @@ class bdist_egg(Command):
|
||||
log.warn("zip_safe flag not set; analyzing archive contents...")
|
||||
return analyze_egg(self.bdist_dir, self.stubs)
|
||||
|
||||
def gen_header(self) -> Literal["w"]:
|
||||
def gen_header(self):
|
||||
return 'w'
|
||||
|
||||
def copy_metadata_to(self, target_dir) -> None:
|
||||
def copy_metadata_to(self, target_dir):
|
||||
"Copy metadata (egg info) to the target_dir"
|
||||
# normalize the path (so that a forward-slash in egg_info will
|
||||
# match using startswith below)
|
||||
@@ -288,7 +276,7 @@ class bdist_egg(Command):
|
||||
prefix = os.path.join(norm_egg_info, '')
|
||||
for path in self.ei_cmd.filelist.files:
|
||||
if path.startswith(prefix):
|
||||
target = os.path.join(target_dir, path[len(prefix) :])
|
||||
target = os.path.join(target_dir, path[len(prefix):])
|
||||
ensure_directory(target)
|
||||
self.copy_file(path, target)
|
||||
|
||||
@@ -300,13 +288,12 @@ class bdist_egg(Command):
|
||||
|
||||
paths = {self.bdist_dir: ''}
|
||||
for base, dirs, files in sorted_walk(self.bdist_dir):
|
||||
all_outputs.extend(
|
||||
paths[base] + filename
|
||||
for filename in files
|
||||
if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS
|
||||
)
|
||||
for filename in files:
|
||||
if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
|
||||
all_outputs.append(paths[base] + filename)
|
||||
for filename in dirs:
|
||||
paths[os.path.join(base, filename)] = paths[base] + filename + '/'
|
||||
paths[os.path.join(base, filename)] = (paths[base] +
|
||||
filename + '/')
|
||||
|
||||
if self.distribution.has_ext_modules():
|
||||
build_cmd = self.get_finalized_command('build_ext')
|
||||
@@ -322,7 +309,7 @@ class bdist_egg(Command):
|
||||
return all_outputs, ext_outputs
|
||||
|
||||
|
||||
NATIVE_EXTENSIONS: dict[str, None] = dict.fromkeys('.dll .so .dylib .pyd'.split())
|
||||
NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
|
||||
|
||||
|
||||
def walk_egg(egg_dir):
|
||||
@@ -332,7 +319,8 @@ def walk_egg(egg_dir):
|
||||
if 'EGG-INFO' in dirs:
|
||||
dirs.remove('EGG-INFO')
|
||||
yield base, dirs, files
|
||||
yield from walker
|
||||
for bdf in walker:
|
||||
yield bdf
|
||||
|
||||
|
||||
def analyze_egg(egg_dir, stubs):
|
||||
@@ -353,7 +341,7 @@ def analyze_egg(egg_dir, stubs):
|
||||
return safe
|
||||
|
||||
|
||||
def write_safety_flag(egg_dir, safe) -> None:
|
||||
def write_safety_flag(egg_dir, safe):
|
||||
# Write or remove zip safety flag file(s)
|
||||
for flag, fn in safety_flags.items():
|
||||
fn = os.path.join(egg_dir, fn)
|
||||
@@ -361,8 +349,9 @@ def write_safety_flag(egg_dir, safe) -> None:
|
||||
if safe is None or bool(safe) != flag:
|
||||
os.unlink(fn)
|
||||
elif safe is not None and bool(safe) == flag:
|
||||
with open(fn, 'wt', encoding="utf-8") as f:
|
||||
f.write('\n')
|
||||
f = open(fn, 'wt')
|
||||
f.write('\n')
|
||||
f.close()
|
||||
|
||||
|
||||
safety_flags = {
|
||||
@@ -377,9 +366,12 @@ def scan_module(egg_dir, base, name, stubs):
|
||||
filename = os.path.join(base, name)
|
||||
if filename[:-1] in stubs:
|
||||
return True # Extension module
|
||||
pkg = base[len(egg_dir) + 1 :].replace(os.sep, '.')
|
||||
pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
|
||||
module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
|
||||
skip = 16 # skip magic & reserved? & date & file size
|
||||
if sys.version_info < (3, 7):
|
||||
skip = 12 # skip magic & date & file size
|
||||
else:
|
||||
skip = 16 # skip magic & reserved? & date & file size
|
||||
f = open(filename, 'rb')
|
||||
f.read(skip)
|
||||
code = marshal.load(f)
|
||||
@@ -392,18 +384,9 @@ def scan_module(egg_dir, base, name, stubs):
|
||||
safe = False
|
||||
if 'inspect' in symbols:
|
||||
for bad in [
|
||||
'getsource',
|
||||
'getabsfile',
|
||||
'getfile',
|
||||
'getsourcefile',
|
||||
'getsourcelines',
|
||||
'findsource',
|
||||
'getcomments',
|
||||
'getframeinfo',
|
||||
'getinnerframes',
|
||||
'getouterframes',
|
||||
'stack',
|
||||
'trace',
|
||||
'getsource', 'getabsfile', 'getsourcefile', 'getfile'
|
||||
'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
|
||||
'getinnerframes', 'getouterframes', 'stack', 'trace'
|
||||
]:
|
||||
if bad in symbols:
|
||||
log.warn("%s: module MAY be using inspect.%s", module, bad)
|
||||
@@ -413,40 +396,35 @@ def scan_module(egg_dir, base, name, stubs):
|
||||
|
||||
def iter_symbols(code):
|
||||
"""Yield names and strings used by `code` and its nested code objects"""
|
||||
yield from code.co_names
|
||||
for name in code.co_names:
|
||||
yield name
|
||||
for const in code.co_consts:
|
||||
if isinstance(const, str):
|
||||
yield const
|
||||
elif isinstance(const, CodeType):
|
||||
yield from iter_symbols(const)
|
||||
for name in iter_symbols(const):
|
||||
yield name
|
||||
|
||||
|
||||
def can_scan() -> bool:
|
||||
def can_scan():
|
||||
if not sys.platform.startswith('java') and sys.platform != 'cli':
|
||||
# CPython, PyPy, etc.
|
||||
return True
|
||||
log.warn("Unable to analyze compiled code on this platform.")
|
||||
log.warn(
|
||||
"Please ask the author to include a 'zip_safe'"
|
||||
" setting (either True or False) in the package's setup.py"
|
||||
)
|
||||
return False
|
||||
log.warn("Please ask the author to include a 'zip_safe'"
|
||||
" setting (either True or False) in the package's setup.py")
|
||||
|
||||
|
||||
# Attribute names of options for commands that might need to be convinced to
|
||||
# install to the egg build directory
|
||||
|
||||
INSTALL_DIRECTORY_ATTRS = ['install_lib', 'install_dir', 'install_data', 'install_base']
|
||||
INSTALL_DIRECTORY_ATTRS = [
|
||||
'install_lib', 'install_dir', 'install_data', 'install_base'
|
||||
]
|
||||
|
||||
|
||||
def make_zipfile(
|
||||
zip_filename: StrPathT,
|
||||
base_dir,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
compress=True,
|
||||
mode: _ZipFileMode = 'w',
|
||||
) -> StrPathT:
|
||||
def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
|
||||
mode='w'):
|
||||
"""Create a zip file from all the files under 'base_dir'. The output
|
||||
zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"
|
||||
Python module (if available) or the InfoZIP "zip" utility (if installed
|
||||
@@ -455,14 +433,14 @@ def make_zipfile(
|
||||
"""
|
||||
import zipfile
|
||||
|
||||
mkpath(os.path.dirname(zip_filename), dry_run=dry_run) # type: ignore[arg-type] # python/mypy#18075
|
||||
mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
|
||||
log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
|
||||
|
||||
def visit(z, dirname, names):
|
||||
for name in names:
|
||||
path = os.path.normpath(os.path.join(dirname, name))
|
||||
if os.path.isfile(path):
|
||||
p = path[len(base_dir) + 1 :]
|
||||
p = path[len(base_dir) + 1:]
|
||||
if not dry_run:
|
||||
z.write(path, p)
|
||||
log.debug("adding '%s'", p)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..dist import Distribution
|
||||
from ..warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
import distutils.command.bdist_rpm as orig
|
||||
import warnings
|
||||
|
||||
from setuptools import SetuptoolsDeprecationWarning
|
||||
|
||||
|
||||
class bdist_rpm(orig.bdist_rpm):
|
||||
@@ -13,17 +13,11 @@ class bdist_rpm(orig.bdist_rpm):
|
||||
disable eggs in RPM distributions.
|
||||
"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
def run(self) -> None:
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"Deprecated command",
|
||||
"""
|
||||
bdist_rpm is deprecated and will be removed in a future version.
|
||||
Use bdist_wheel (wheel packages) instead.
|
||||
""",
|
||||
see_url="https://github.com/pypa/setuptools/issues/1988",
|
||||
due_date=(2023, 10, 30), # Deprecation introduced in 22 Oct 2021.
|
||||
def run(self):
|
||||
warnings.warn(
|
||||
"bdist_rpm is deprecated and will be removed in a future "
|
||||
"version. Use bdist_wheel (wheel packages) instead.",
|
||||
SetuptoolsDeprecationWarning,
|
||||
)
|
||||
|
||||
# ensure distro name is up-to-date
|
||||
@@ -33,10 +27,14 @@ class bdist_rpm(orig.bdist_rpm):
|
||||
|
||||
def _make_spec_file(self):
|
||||
spec = orig.bdist_rpm._make_spec_file(self)
|
||||
return [
|
||||
spec = [
|
||||
line.replace(
|
||||
"setup.py install ",
|
||||
"setup.py install --single-version-externally-managed ",
|
||||
).replace("%setup", "%setup -n %{name}-%{unmangled_version}")
|
||||
"setup.py install --single-version-externally-managed "
|
||||
).replace(
|
||||
"%setup",
|
||||
"%setup -n %{name}-%{unmangled_version}"
|
||||
)
|
||||
for line in spec
|
||||
]
|
||||
return spec
|
||||
|
||||
@@ -1,604 +0,0 @@
|
||||
"""
|
||||
Create a wheel (.whl) distribution.
|
||||
|
||||
A wheel is a built archive format.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import sysconfig
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
from email.generator import BytesGenerator
|
||||
from glob import iglob
|
||||
from typing import Literal, cast
|
||||
from zipfile import ZIP_DEFLATED, ZIP_STORED
|
||||
|
||||
from packaging import tags, version as _packaging_version
|
||||
from wheel.wheelfile import WheelFile
|
||||
|
||||
from .. import Command, __version__, _shutil
|
||||
from .._core_metadata import _safe_license_file
|
||||
from .._normalization import safer_name
|
||||
from ..warnings import SetuptoolsDeprecationWarning
|
||||
from .egg_info import egg_info as egg_info_cls
|
||||
|
||||
from distutils import log
|
||||
|
||||
|
||||
def safe_version(version: str) -> str:
|
||||
"""
|
||||
Convert an arbitrary string to a standard version string
|
||||
"""
|
||||
try:
|
||||
# normalize the version
|
||||
return str(_packaging_version.Version(version))
|
||||
except _packaging_version.InvalidVersion:
|
||||
version = version.replace(" ", ".")
|
||||
return re.sub("[^A-Za-z0-9.]+", "-", version)
|
||||
|
||||
|
||||
setuptools_major_version = int(__version__.split(".")[0])
|
||||
|
||||
PY_LIMITED_API_PATTERN = r"cp3\d"
|
||||
|
||||
|
||||
def _is_32bit_interpreter() -> bool:
|
||||
return struct.calcsize("P") == 4
|
||||
|
||||
|
||||
def python_tag() -> str:
|
||||
return f"py{sys.version_info.major}"
|
||||
|
||||
|
||||
def get_platform(archive_root: str | None) -> str:
|
||||
"""Return our platform name 'win32', 'linux_x86_64'"""
|
||||
result = sysconfig.get_platform()
|
||||
if result.startswith("macosx") and archive_root is not None: # pragma: no cover
|
||||
from wheel.macosx_libfile import calculate_macosx_platform_tag
|
||||
|
||||
result = calculate_macosx_platform_tag(archive_root, result)
|
||||
elif _is_32bit_interpreter():
|
||||
if result == "linux-x86_64":
|
||||
# pip pull request #3497
|
||||
result = "linux-i686"
|
||||
elif result == "linux-aarch64":
|
||||
# packaging pull request #234
|
||||
# TODO armv8l, packaging pull request #690 => this did not land
|
||||
# in pip/packaging yet
|
||||
result = "linux-armv7l"
|
||||
|
||||
return result.replace("-", "_")
|
||||
|
||||
|
||||
def get_flag(
|
||||
var: str, fallback: bool, expected: bool = True, warn: bool = True
|
||||
) -> bool:
|
||||
"""Use a fallback value for determining SOABI flags if the needed config
|
||||
var is unset or unavailable."""
|
||||
val = sysconfig.get_config_var(var)
|
||||
if val is None:
|
||||
if warn:
|
||||
warnings.warn(
|
||||
f"Config variable '{var}' is unset, Python ABI tag may be incorrect",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return fallback
|
||||
return val == expected
|
||||
|
||||
|
||||
def get_abi_tag() -> str | None:
|
||||
"""Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
|
||||
soabi: str = sysconfig.get_config_var("SOABI")
|
||||
impl = tags.interpreter_name()
|
||||
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
|
||||
d = ""
|
||||
u = ""
|
||||
if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):
|
||||
d = "d"
|
||||
|
||||
abi = f"{impl}{tags.interpreter_version()}{d}{u}"
|
||||
elif soabi and impl == "cp" and soabi.startswith("cpython"):
|
||||
# non-Windows
|
||||
abi = "cp" + soabi.split("-")[1]
|
||||
elif soabi and impl == "cp" and soabi.startswith("cp"):
|
||||
# Windows
|
||||
abi = soabi.split("-")[0]
|
||||
if hasattr(sys, "gettotalrefcount"):
|
||||
# using debug build; append "d" flag
|
||||
abi += "d"
|
||||
elif soabi and impl == "pp":
|
||||
# we want something like pypy36-pp73
|
||||
abi = "-".join(soabi.split("-")[:2])
|
||||
abi = abi.replace(".", "_").replace("-", "_")
|
||||
elif soabi and impl == "graalpy":
|
||||
abi = "-".join(soabi.split("-")[:3])
|
||||
abi = abi.replace(".", "_").replace("-", "_")
|
||||
elif soabi:
|
||||
abi = soabi.replace(".", "_").replace("-", "_")
|
||||
else:
|
||||
abi = None
|
||||
|
||||
return abi
|
||||
|
||||
|
||||
def safer_version(version: str) -> str:
|
||||
return safe_version(version).replace("-", "_")
|
||||
|
||||
|
||||
class bdist_wheel(Command):
|
||||
description = "create a wheel distribution"
|
||||
|
||||
supported_compressions = {
|
||||
"stored": ZIP_STORED,
|
||||
"deflated": ZIP_DEFLATED,
|
||||
}
|
||||
|
||||
user_options = [
|
||||
("bdist-dir=", "b", "temporary directory for creating the distribution"),
|
||||
(
|
||||
"plat-name=",
|
||||
"p",
|
||||
"platform name to embed in generated filenames "
|
||||
f"[default: {get_platform(None)}]",
|
||||
),
|
||||
(
|
||||
"keep-temp",
|
||||
"k",
|
||||
"keep the pseudo-installation tree around after "
|
||||
"creating the distribution archive",
|
||||
),
|
||||
("dist-dir=", "d", "directory to put final built distributions in"),
|
||||
("skip-build", None, "skip rebuilding everything (for testing/debugging)"),
|
||||
(
|
||||
"relative",
|
||||
None,
|
||||
"build the archive using relative paths [default: false]",
|
||||
),
|
||||
(
|
||||
"owner=",
|
||||
"u",
|
||||
"Owner name used when creating a tar file [default: current user]",
|
||||
),
|
||||
(
|
||||
"group=",
|
||||
"g",
|
||||
"Group name used when creating a tar file [default: current group]",
|
||||
),
|
||||
("universal", None, "*DEPRECATED* make a universal wheel [default: false]"),
|
||||
(
|
||||
"compression=",
|
||||
None,
|
||||
f"zipfile compression (one of: {', '.join(supported_compressions)}) [default: 'deflated']",
|
||||
),
|
||||
(
|
||||
"python-tag=",
|
||||
None,
|
||||
f"Python implementation compatibility tag [default: '{python_tag()}']",
|
||||
),
|
||||
(
|
||||
"build-number=",
|
||||
None,
|
||||
"Build number for this particular version. "
|
||||
"As specified in PEP-0427, this must start with a digit. "
|
||||
"[default: None]",
|
||||
),
|
||||
(
|
||||
"py-limited-api=",
|
||||
None,
|
||||
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag [default: false]",
|
||||
),
|
||||
(
|
||||
"dist-info-dir=",
|
||||
None,
|
||||
"directory where a pre-generated dist-info can be found (e.g. as a "
|
||||
"result of calling the PEP517 'prepare_metadata_for_build_wheel' "
|
||||
"method)",
|
||||
),
|
||||
]
|
||||
|
||||
boolean_options = ["keep-temp", "skip-build", "relative", "universal"]
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
self.bdist_dir: str | None = None
|
||||
self.data_dir = ""
|
||||
self.plat_name: str | None = None
|
||||
self.plat_tag: str | None = None
|
||||
self.format = "zip"
|
||||
self.keep_temp = False
|
||||
self.dist_dir: str | None = None
|
||||
self.dist_info_dir = None
|
||||
self.egginfo_dir: str | None = None
|
||||
self.root_is_pure: bool | None = None
|
||||
self.skip_build = False
|
||||
self.relative = False
|
||||
self.owner = None
|
||||
self.group = None
|
||||
self.universal = False
|
||||
self.compression: str | int = "deflated"
|
||||
self.python_tag = python_tag()
|
||||
self.build_number: str | None = None
|
||||
self.py_limited_api: str | Literal[False] = False
|
||||
self.plat_name_supplied = False
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
if not self.bdist_dir:
|
||||
bdist_base = self.get_finalized_command("bdist").bdist_base
|
||||
self.bdist_dir = os.path.join(bdist_base, "wheel")
|
||||
|
||||
if self.dist_info_dir is None:
|
||||
egg_info = cast(egg_info_cls, self.distribution.get_command_obj("egg_info"))
|
||||
egg_info.ensure_finalized() # needed for correct `wheel_dist_name`
|
||||
|
||||
self.data_dir = self.wheel_dist_name + ".data"
|
||||
self.plat_name_supplied = bool(self.plat_name)
|
||||
|
||||
need_options = ("dist_dir", "plat_name", "skip_build")
|
||||
|
||||
self.set_undefined_options("bdist", *zip(need_options, need_options))
|
||||
|
||||
self.root_is_pure = not (
|
||||
self.distribution.has_ext_modules() or self.distribution.has_c_libraries()
|
||||
)
|
||||
|
||||
self._validate_py_limited_api()
|
||||
|
||||
# Support legacy [wheel] section for setting universal
|
||||
wheel = self.distribution.get_option_dict("wheel")
|
||||
if "universal" in wheel: # pragma: no cover
|
||||
# please don't define this in your global configs
|
||||
log.warn("The [wheel] section is deprecated. Use [bdist_wheel] instead.")
|
||||
val = wheel["universal"][1].strip()
|
||||
if val.lower() in ("1", "true", "yes"):
|
||||
self.universal = True
|
||||
|
||||
if self.universal:
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"bdist_wheel.universal is deprecated",
|
||||
"""
|
||||
With Python 2.7 end-of-life, support for building universal wheels
|
||||
(i.e., wheels that support both Python 2 and Python 3)
|
||||
is being obviated.
|
||||
Please discontinue using this option, or if you still need it,
|
||||
file an issue with pypa/setuptools describing your use case.
|
||||
""",
|
||||
due_date=(2025, 8, 30), # Introduced in 2024-08-30
|
||||
)
|
||||
|
||||
if self.build_number is not None and not self.build_number[:1].isdigit():
|
||||
raise ValueError("Build tag (build-number) must start with a digit.")
|
||||
|
||||
def _validate_py_limited_api(self) -> None:
|
||||
if not self.py_limited_api:
|
||||
return
|
||||
|
||||
if not re.match(PY_LIMITED_API_PATTERN, self.py_limited_api):
|
||||
raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'")
|
||||
|
||||
if sysconfig.get_config_var("Py_GIL_DISABLED"):
|
||||
raise ValueError(
|
||||
f"`py_limited_api={self.py_limited_api!r}` not supported. "
|
||||
"`Py_LIMITED_API` is currently incompatible with "
|
||||
"`Py_GIL_DISABLED`."
|
||||
"See https://github.com/python/cpython/issues/111506."
|
||||
)
|
||||
|
||||
@property
|
||||
def wheel_dist_name(self) -> str:
|
||||
"""Return distribution full name with - replaced with _"""
|
||||
components = [
|
||||
safer_name(self.distribution.get_name()),
|
||||
safer_version(self.distribution.get_version()),
|
||||
]
|
||||
if self.build_number:
|
||||
components.append(self.build_number)
|
||||
return "-".join(components)
|
||||
|
||||
def get_tag(self) -> tuple[str, str, str]:
|
||||
# bdist sets self.plat_name if unset, we should only use it for purepy
|
||||
# wheels if the user supplied it.
|
||||
if self.plat_name_supplied and self.plat_name:
|
||||
plat_name = self.plat_name
|
||||
elif self.root_is_pure:
|
||||
plat_name = "any"
|
||||
else:
|
||||
# macosx contains system version in platform name so need special handle
|
||||
if self.plat_name and not self.plat_name.startswith("macosx"):
|
||||
plat_name = self.plat_name
|
||||
else:
|
||||
# on macosx always limit the platform name to comply with any
|
||||
# c-extension modules in bdist_dir, since the user can specify
|
||||
# a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake
|
||||
|
||||
# on other platforms, and on macosx if there are no c-extension
|
||||
# modules, use the default platform name.
|
||||
plat_name = get_platform(self.bdist_dir)
|
||||
|
||||
if _is_32bit_interpreter():
|
||||
if plat_name in ("linux-x86_64", "linux_x86_64"):
|
||||
plat_name = "linux_i686"
|
||||
if plat_name in ("linux-aarch64", "linux_aarch64"):
|
||||
# TODO armv8l, packaging pull request #690 => this did not land
|
||||
# in pip/packaging yet
|
||||
plat_name = "linux_armv7l"
|
||||
|
||||
plat_name = (
|
||||
plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")
|
||||
)
|
||||
|
||||
if self.root_is_pure:
|
||||
if self.universal:
|
||||
impl = "py2.py3"
|
||||
else:
|
||||
impl = self.python_tag
|
||||
tag = (impl, "none", plat_name)
|
||||
else:
|
||||
impl_name = tags.interpreter_name()
|
||||
impl_ver = tags.interpreter_version()
|
||||
impl = impl_name + impl_ver
|
||||
# We don't work on CPython 3.1, 3.0.
|
||||
if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):
|
||||
impl = self.py_limited_api
|
||||
abi_tag = "abi3"
|
||||
else:
|
||||
abi_tag = str(get_abi_tag()).lower()
|
||||
tag = (impl, abi_tag, plat_name)
|
||||
# issue gh-374: allow overriding plat_name
|
||||
supported_tags = [
|
||||
(t.interpreter, t.abi, plat_name) for t in tags.sys_tags()
|
||||
]
|
||||
assert tag in supported_tags, (
|
||||
f"would build wheel with unsupported tag {tag}"
|
||||
)
|
||||
return tag
|
||||
|
||||
def run(self):
|
||||
build_scripts = self.reinitialize_command("build_scripts")
|
||||
build_scripts.executable = "python"
|
||||
build_scripts.force = True
|
||||
|
||||
build_ext = self.reinitialize_command("build_ext")
|
||||
build_ext.inplace = False
|
||||
|
||||
if not self.skip_build:
|
||||
self.run_command("build")
|
||||
|
||||
install = self.reinitialize_command("install", reinit_subcommands=True)
|
||||
install.root = self.bdist_dir
|
||||
install.compile = False
|
||||
install.skip_build = self.skip_build
|
||||
install.warn_dir = False
|
||||
|
||||
# A wheel without setuptools scripts is more cross-platform.
|
||||
# Use the (undocumented) `no_ep` option to setuptools'
|
||||
# install_scripts command to avoid creating entry point scripts.
|
||||
install_scripts = self.reinitialize_command("install_scripts")
|
||||
install_scripts.no_ep = True
|
||||
|
||||
# Use a custom scheme for the archive, because we have to decide
|
||||
# at installation time which scheme to use.
|
||||
for key in ("headers", "scripts", "data", "purelib", "platlib"):
|
||||
setattr(install, "install_" + key, os.path.join(self.data_dir, key))
|
||||
|
||||
basedir_observed = ""
|
||||
|
||||
if os.name == "nt":
|
||||
# win32 barfs if any of these are ''; could be '.'?
|
||||
# (distutils.command.install:change_roots bug)
|
||||
basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))
|
||||
self.install_libbase = self.install_lib = basedir_observed
|
||||
|
||||
setattr(
|
||||
install,
|
||||
"install_purelib" if self.root_is_pure else "install_platlib",
|
||||
basedir_observed,
|
||||
)
|
||||
|
||||
log.info(f"installing to {self.bdist_dir}")
|
||||
|
||||
self.run_command("install")
|
||||
|
||||
impl_tag, abi_tag, plat_tag = self.get_tag()
|
||||
archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"
|
||||
if not self.relative:
|
||||
archive_root = self.bdist_dir
|
||||
else:
|
||||
archive_root = os.path.join(
|
||||
self.bdist_dir, self._ensure_relative(install.install_base)
|
||||
)
|
||||
|
||||
self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))
|
||||
distinfo_dirname = (
|
||||
f"{safer_name(self.distribution.get_name())}-"
|
||||
f"{safer_version(self.distribution.get_version())}.dist-info"
|
||||
)
|
||||
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
|
||||
if self.dist_info_dir:
|
||||
# Use the given dist-info directly.
|
||||
log.debug(f"reusing {self.dist_info_dir}")
|
||||
shutil.copytree(self.dist_info_dir, distinfo_dir)
|
||||
# Egg info is still generated, so remove it now to avoid it getting
|
||||
# copied into the wheel.
|
||||
_shutil.rmtree(self.egginfo_dir)
|
||||
else:
|
||||
# Convert the generated egg-info into dist-info.
|
||||
self.egg2dist(self.egginfo_dir, distinfo_dir)
|
||||
|
||||
self.write_wheelfile(distinfo_dir)
|
||||
|
||||
# Make the archive
|
||||
if not os.path.exists(self.dist_dir):
|
||||
os.makedirs(self.dist_dir)
|
||||
|
||||
wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")
|
||||
with WheelFile(wheel_path, "w", self._zip_compression()) as wf:
|
||||
wf.write_files(archive_root)
|
||||
|
||||
# Add to 'Distribution.dist_files' so that the "upload" command works
|
||||
getattr(self.distribution, "dist_files", []).append((
|
||||
"bdist_wheel",
|
||||
f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
wheel_path,
|
||||
))
|
||||
|
||||
if not self.keep_temp:
|
||||
log.info(f"removing {self.bdist_dir}")
|
||||
if not self.dry_run:
|
||||
_shutil.rmtree(self.bdist_dir)
|
||||
|
||||
def write_wheelfile(
|
||||
self, wheelfile_base: str, generator: str = f"setuptools ({__version__})"
|
||||
) -> None:
|
||||
from email.message import Message
|
||||
|
||||
msg = Message()
|
||||
msg["Wheel-Version"] = "1.0" # of the spec
|
||||
msg["Generator"] = generator
|
||||
msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()
|
||||
if self.build_number is not None:
|
||||
msg["Build"] = self.build_number
|
||||
|
||||
# Doesn't work for bdist_wininst
|
||||
impl_tag, abi_tag, plat_tag = self.get_tag()
|
||||
for impl in impl_tag.split("."):
|
||||
for abi in abi_tag.split("."):
|
||||
for plat in plat_tag.split("."):
|
||||
msg["Tag"] = "-".join((impl, abi, plat))
|
||||
|
||||
wheelfile_path = os.path.join(wheelfile_base, "WHEEL")
|
||||
log.info(f"creating {wheelfile_path}")
|
||||
with open(wheelfile_path, "wb") as f:
|
||||
BytesGenerator(f, maxheaderlen=0).flatten(msg)
|
||||
|
||||
def _ensure_relative(self, path: str) -> str:
|
||||
# copied from dir_util, deleted
|
||||
drive, path = os.path.splitdrive(path)
|
||||
if path[0:1] == os.sep:
|
||||
path = drive + path[1:]
|
||||
return path
|
||||
|
||||
@property
|
||||
def license_paths(self) -> Iterable[str]:
|
||||
if setuptools_major_version >= 57:
|
||||
# Setuptools has resolved any patterns to actual file names
|
||||
return self.distribution.metadata.license_files or ()
|
||||
|
||||
files = set[str]()
|
||||
metadata = self.distribution.get_option_dict("metadata")
|
||||
if setuptools_major_version >= 42:
|
||||
# Setuptools recognizes the license_files option but does not do globbing
|
||||
patterns = cast(Sequence[str], self.distribution.metadata.license_files)
|
||||
else:
|
||||
# Prior to those, wheel is entirely responsible for handling license files
|
||||
if "license_files" in metadata:
|
||||
patterns = metadata["license_files"][1].split()
|
||||
else:
|
||||
patterns = ()
|
||||
|
||||
if "license_file" in metadata:
|
||||
warnings.warn(
|
||||
'The "license_file" option is deprecated. Use "license_files" instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
files.add(metadata["license_file"][1])
|
||||
|
||||
if not files and not patterns and not isinstance(patterns, list):
|
||||
patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
|
||||
|
||||
for pattern in patterns:
|
||||
for path in iglob(pattern):
|
||||
if path.endswith("~"):
|
||||
log.debug(
|
||||
f'ignoring license file "{path}" as it looks like a backup'
|
||||
)
|
||||
continue
|
||||
|
||||
if path not in files and os.path.isfile(path):
|
||||
log.info(
|
||||
f'adding license file "{path}" (matched pattern "{pattern}")'
|
||||
)
|
||||
files.add(path)
|
||||
|
||||
return files
|
||||
|
||||
def egg2dist(self, egginfo_path: str, distinfo_path: str) -> None:
|
||||
"""Convert an .egg-info directory into a .dist-info directory"""
|
||||
|
||||
def adios(p: str) -> None:
|
||||
"""Appropriately delete directory, file or link."""
|
||||
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
|
||||
_shutil.rmtree(p)
|
||||
elif os.path.exists(p):
|
||||
os.unlink(p)
|
||||
|
||||
adios(distinfo_path)
|
||||
|
||||
if not os.path.exists(egginfo_path):
|
||||
# There is no egg-info. This is probably because the egg-info
|
||||
# file/directory is not named matching the distribution name used
|
||||
# to name the archive file. Check for this case and report
|
||||
# accordingly.
|
||||
import glob
|
||||
|
||||
pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")
|
||||
possible = glob.glob(pat)
|
||||
err = f"Egg metadata expected at {egginfo_path} but not found"
|
||||
if possible:
|
||||
alt = os.path.basename(possible[0])
|
||||
err += f" ({alt} found - possible misnamed archive file?)"
|
||||
|
||||
raise ValueError(err)
|
||||
|
||||
# .egg-info is a directory
|
||||
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
|
||||
|
||||
# ignore common egg metadata that is useless to wheel
|
||||
shutil.copytree(
|
||||
egginfo_path,
|
||||
distinfo_path,
|
||||
ignore=lambda x, y: {
|
||||
"PKG-INFO",
|
||||
"requires.txt",
|
||||
"SOURCES.txt",
|
||||
"not-zip-safe",
|
||||
},
|
||||
)
|
||||
|
||||
# delete dependency_links if it is only whitespace
|
||||
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
|
||||
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
|
||||
dependency_links = dependency_links_file.read().strip()
|
||||
if not dependency_links:
|
||||
adios(dependency_links_path)
|
||||
|
||||
metadata_path = os.path.join(distinfo_path, "METADATA")
|
||||
shutil.copy(pkginfo_path, metadata_path)
|
||||
|
||||
licenses_folder_path = os.path.join(distinfo_path, "licenses")
|
||||
for license_path in self.license_paths:
|
||||
safe_path = _safe_license_file(license_path)
|
||||
dist_info_license_path = os.path.join(licenses_folder_path, safe_path)
|
||||
os.makedirs(os.path.dirname(dist_info_license_path), exist_ok=True)
|
||||
shutil.copy(license_path, dist_info_license_path)
|
||||
|
||||
adios(egginfo_path)
|
||||
|
||||
def _zip_compression(self) -> int:
|
||||
if (
|
||||
isinstance(self.compression, int)
|
||||
and self.compression in self.supported_compressions.values()
|
||||
):
|
||||
return self.compression
|
||||
|
||||
compression = self.supported_compressions.get(str(self.compression))
|
||||
if compression is not None:
|
||||
return compression
|
||||
|
||||
raise ValueError(f"Unsupported compression: {self.compression!r}")
|
||||
@@ -1,20 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from ..dist import Distribution
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, List, Dict
|
||||
from distutils.command.build import build as _build
|
||||
|
||||
from setuptools import SetuptoolsDeprecationWarning
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Protocol
|
||||
elif TYPE_CHECKING:
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
from abc import ABC as Protocol
|
||||
|
||||
|
||||
_ORIGINAL_SUBCOMMANDS = {"build_py", "build_clib", "build_ext", "build_scripts"}
|
||||
|
||||
|
||||
class build(_build):
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
# copy to avoid sharing the object with parent class
|
||||
sub_commands = _build.sub_commands[:]
|
||||
|
||||
def get_sub_commands(self):
|
||||
subcommands = {cmd[0] for cmd in _build.sub_commands}
|
||||
if subcommands - _ORIGINAL_SUBCOMMANDS:
|
||||
msg = """
|
||||
It seems that you are using `distutils.command.build` to add
|
||||
new subcommands. Using `distutils` directly is considered deprecated,
|
||||
please use `setuptools.command.build`.
|
||||
"""
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
||||
self.sub_commands = _build.sub_commands
|
||||
return super().get_sub_commands()
|
||||
|
||||
|
||||
class SubCommand(Protocol):
|
||||
"""In order to support editable installations (see :pep:`660`) all
|
||||
@@ -31,8 +48,8 @@ class SubCommand(Protocol):
|
||||
Subcommands **SHOULD** take advantage of ``editable_mode=True`` to adequate
|
||||
its behaviour or perform optimisations.
|
||||
|
||||
For example, if a subcommand doesn't need to generate an extra file and
|
||||
all it does is to copy a source file into the build directory,
|
||||
For example, if a subcommand don't need to generate any extra file and
|
||||
everything it does is to copy a source file into the build directory,
|
||||
``run()`` **SHOULD** simply "early return".
|
||||
|
||||
Similarly, if the subcommand creates files that would be placed alongside
|
||||
@@ -85,19 +102,16 @@ class SubCommand(Protocol):
|
||||
...
|
||||
"""
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
def initialize_options(self):
|
||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
||||
...
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
||||
...
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
||||
...
|
||||
|
||||
def get_source_files(self) -> list[str]:
|
||||
def get_source_files(self) -> List[str]:
|
||||
"""
|
||||
Return a list of all files that are used by the command to create the expected
|
||||
outputs.
|
||||
@@ -107,9 +121,8 @@ class SubCommand(Protocol):
|
||||
with all the files necessary to build the distribution.
|
||||
All files should be strings relative to the project root directory.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_outputs(self) -> list[str]:
|
||||
def get_outputs(self) -> List[str]:
|
||||
"""
|
||||
Return a list of files intended for distribution as they would have been
|
||||
produced by the build.
|
||||
@@ -121,9 +134,8 @@ class SubCommand(Protocol):
|
||||
in ``get_output_mapping()`` plus files that are generated during the build
|
||||
and don't correspond to any source file already present in the project.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_output_mapping(self) -> dict[str, str]:
|
||||
def get_output_mapping(self) -> Dict[str, str]:
|
||||
"""
|
||||
Return a mapping between destination files as they would be produced by the
|
||||
build (dict keys) into the respective existing (source) files (dict values).
|
||||
@@ -132,4 +144,3 @@ class SubCommand(Protocol):
|
||||
Destination files should be strings in the form of
|
||||
``"{build_lib}/destination/file/path"``.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from ..dist import Distribution
|
||||
from ..modified import newer_pairwise_group
|
||||
|
||||
import distutils.command.build_clib as orig
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsSetupError
|
||||
from distutils import log
|
||||
from setuptools.dep_util import newer_pairwise_group
|
||||
|
||||
|
||||
class build_clib(orig.build_clib):
|
||||
@@ -22,18 +20,15 @@ class build_clib(orig.build_clib):
|
||||
the compiler.
|
||||
"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
def build_libraries(self, libraries) -> None:
|
||||
for lib_name, build_info in libraries:
|
||||
def build_libraries(self, libraries):
|
||||
for (lib_name, build_info) in libraries:
|
||||
sources = build_info.get('sources')
|
||||
if sources is None or not isinstance(sources, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"in 'libraries' option (library '%s'), "
|
||||
"'sources' must be present and must be "
|
||||
"a list of source filenames"
|
||||
)
|
||||
sources = sorted(list(sources))
|
||||
"a list of source filenames" % lib_name)
|
||||
sources = list(sources)
|
||||
|
||||
log.info("building '%s' library", lib_name)
|
||||
|
||||
@@ -43,10 +38,9 @@ class build_clib(orig.build_clib):
|
||||
obj_deps = build_info.get('obj_deps', dict())
|
||||
if not isinstance(obj_deps, dict):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"in 'libraries' option (library '%s'), "
|
||||
"'obj_deps' must be a dictionary of "
|
||||
"type 'source: list'"
|
||||
)
|
||||
"type 'source: list'" % lib_name)
|
||||
dependencies = []
|
||||
|
||||
# Get the global dependencies that are specified by the '' key.
|
||||
@@ -54,10 +48,9 @@ class build_clib(orig.build_clib):
|
||||
global_deps = obj_deps.get('', list())
|
||||
if not isinstance(global_deps, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"in 'libraries' option (library '%s'), "
|
||||
"'obj_deps' must be a dictionary of "
|
||||
"type 'source: list'"
|
||||
)
|
||||
"type 'source: list'" % lib_name)
|
||||
|
||||
# Build the list to be used by newer_pairwise_group
|
||||
# each source will be auto-added to its dependencies.
|
||||
@@ -67,10 +60,9 @@ class build_clib(orig.build_clib):
|
||||
extra_deps = obj_deps.get(source, list())
|
||||
if not isinstance(extra_deps, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"in 'libraries' option (library '%s'), "
|
||||
"'obj_deps' must be a dictionary of "
|
||||
"type 'source: list'"
|
||||
)
|
||||
"type 'source: list'" % lib_name)
|
||||
src_deps.extend(extra_deps)
|
||||
dependencies.append(src_deps)
|
||||
|
||||
@@ -79,7 +71,10 @@ class build_clib(orig.build_clib):
|
||||
output_dir=self.build_temp,
|
||||
)
|
||||
|
||||
if newer_pairwise_group(dependencies, expected_objects) != ([], []):
|
||||
if (
|
||||
newer_pairwise_group(dependencies, expected_objects)
|
||||
!= ([], [])
|
||||
):
|
||||
# First, compile the source code to object files in the library
|
||||
# directory. (This should probably change to putting object
|
||||
# files in a temporary build directory.)
|
||||
@@ -92,12 +87,15 @@ class build_clib(orig.build_clib):
|
||||
macros=macros,
|
||||
include_dirs=include_dirs,
|
||||
extra_postargs=cflags,
|
||||
debug=self.debug,
|
||||
debug=self.debug
|
||||
)
|
||||
|
||||
# Now "link" the object files together into a static library.
|
||||
# (On Unix at least, this isn't really linking -- it just
|
||||
# builds an archive. Whatever.)
|
||||
self.compiler.create_static_lib(
|
||||
expected_objects, lib_name, output_dir=self.build_clib, debug=self.debug
|
||||
expected_objects,
|
||||
lib_name,
|
||||
output_dir=self.build_clib,
|
||||
debug=self.debug
|
||||
)
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
import itertools
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from importlib.util import cache_from_source as _compiled_file_name
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, Iterator, List, Tuple
|
||||
|
||||
from distutils.command.build_ext import build_ext as _du_build_ext
|
||||
from distutils.ccompiler import new_compiler
|
||||
from distutils.sysconfig import customize_compiler, get_config_var
|
||||
from distutils import log
|
||||
|
||||
from setuptools.dist import Distribution
|
||||
from setuptools.errors import BaseError
|
||||
from setuptools.extension import Extension, Library
|
||||
|
||||
from distutils import log
|
||||
from distutils.ccompiler import new_compiler
|
||||
from distutils.sysconfig import customize_compiler, get_config_var
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Cython not installed on CI tests, causing _build_ext to be `Any`
|
||||
from distutils.command.build_ext import build_ext as _build_ext
|
||||
else:
|
||||
try:
|
||||
# Attempt to use Cython for building extensions, if available
|
||||
from Cython.Distutils.build_ext import build_ext as _build_ext
|
||||
|
||||
# Additionally, assert that the compiler module will load
|
||||
# also. Ref #1229.
|
||||
__import__('Cython.Compiler.Main')
|
||||
except ImportError:
|
||||
from distutils.command.build_ext import build_ext as _build_ext
|
||||
try:
|
||||
# Attempt to use Cython for building extensions, if available
|
||||
from Cython.Distutils.build_ext import build_ext as _build_ext
|
||||
# Additionally, assert that the compiler module will load
|
||||
# also. Ref #1229.
|
||||
__import__('Cython.Compiler.Main')
|
||||
except ImportError:
|
||||
_build_ext = _du_build_ext
|
||||
|
||||
# make sure _config_vars is initialized
|
||||
get_config_var("LDSHARED")
|
||||
# Not publicly exposed in typeshed distutils stubs, but this is done on purpose
|
||||
# See https://github.com/pypa/setuptools/pull/4228#issuecomment-1959856400
|
||||
from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa: E402
|
||||
from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa
|
||||
|
||||
|
||||
def _customize_compiler_for_shlib(compiler):
|
||||
@@ -47,8 +36,7 @@ def _customize_compiler_for_shlib(compiler):
|
||||
try:
|
||||
# XXX Help! I don't have any idea whether these are right...
|
||||
_CONFIG_VARS['LDSHARED'] = (
|
||||
"gcc -Wl,-x -dynamiclib -undefined dynamic_lookup"
|
||||
)
|
||||
"gcc -Wl,-x -dynamiclib -undefined dynamic_lookup")
|
||||
_CONFIG_VARS['CCSHARED'] = " -dynamiclib"
|
||||
_CONFIG_VARS['SO'] = ".dylib"
|
||||
customize_compiler(compiler)
|
||||
@@ -67,8 +55,7 @@ if sys.platform == "darwin":
|
||||
use_stubs = True
|
||||
elif os.name != 'nt':
|
||||
try:
|
||||
import dl # type: ignore[import-not-found] # https://github.com/python/mypy/issues/13002
|
||||
|
||||
import dl
|
||||
use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW')
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -85,23 +72,21 @@ def get_abi3_suffix():
|
||||
return suffix
|
||||
elif suffix == '.pyd': # Windows
|
||||
return suffix
|
||||
return None
|
||||
|
||||
|
||||
class build_ext(_build_ext):
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
editable_mode = False
|
||||
inplace = False
|
||||
editable_mode: bool = False
|
||||
inplace: bool = False
|
||||
|
||||
def run(self):
|
||||
"""Build extensions in build directory, then copy if --inplace"""
|
||||
old_inplace, self.inplace = self.inplace, False
|
||||
old_inplace, self.inplace = self.inplace, 0
|
||||
_build_ext.run(self)
|
||||
self.inplace = old_inplace
|
||||
if old_inplace:
|
||||
self.copy_extensions_to_source()
|
||||
|
||||
def _get_inplace_equivalent(self, build_py, ext: Extension) -> tuple[str, str]:
|
||||
def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]:
|
||||
fullname = self.get_ext_fullname(ext.name)
|
||||
filename = self.get_ext_filename(fullname)
|
||||
modpath = fullname.split('.')
|
||||
@@ -111,7 +96,7 @@ class build_ext(_build_ext):
|
||||
regular_file = os.path.join(self.build_lib, filename)
|
||||
return (inplace_file, regular_file)
|
||||
|
||||
def copy_extensions_to_source(self) -> None:
|
||||
def copy_extensions_to_source(self):
|
||||
build_py = self.get_finalized_command('build_py')
|
||||
for ext in self.extensions:
|
||||
inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)
|
||||
@@ -133,7 +118,7 @@ class build_ext(_build_ext):
|
||||
_, _, name = ext.name.rpartition(".")
|
||||
return f"{os.path.join(dir_, name)}.py"
|
||||
|
||||
def _get_output_mapping(self) -> Iterator[tuple[str, str]]:
|
||||
def _get_output_mapping(self) -> Iterator[Tuple[str, str]]:
|
||||
if not self.inplace:
|
||||
return
|
||||
|
||||
@@ -158,25 +143,21 @@ class build_ext(_build_ext):
|
||||
output_cache = _compiled_file_name(regular_stub, optimization=opt)
|
||||
yield (output_cache, inplace_cache)
|
||||
|
||||
def get_ext_filename(self, fullname: str) -> str:
|
||||
def get_ext_filename(self, fullname):
|
||||
so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX')
|
||||
if so_ext:
|
||||
filename = os.path.join(*fullname.split('.')) + so_ext
|
||||
else:
|
||||
filename = _build_ext.get_ext_filename(self, fullname)
|
||||
ext_suffix = get_config_var('EXT_SUFFIX')
|
||||
if not isinstance(ext_suffix, str):
|
||||
raise OSError(
|
||||
"Configuration variable EXT_SUFFIX not found for this platform "
|
||||
"and environment variable SETUPTOOLS_EXT_SUFFIX is missing"
|
||||
)
|
||||
so_ext = ext_suffix
|
||||
so_ext = get_config_var('EXT_SUFFIX')
|
||||
|
||||
if fullname in self.ext_map:
|
||||
ext = self.ext_map[fullname]
|
||||
abi3_suffix = get_abi3_suffix()
|
||||
if ext.py_limited_api and abi3_suffix: # Use abi3
|
||||
filename = filename[: -len(so_ext)] + abi3_suffix
|
||||
use_abi3 = getattr(ext, 'py_limited_api') and get_abi3_suffix()
|
||||
if use_abi3:
|
||||
filename = filename[:-len(so_ext)]
|
||||
so_ext = get_abi3_suffix()
|
||||
filename = filename + so_ext
|
||||
if isinstance(ext, Library):
|
||||
fn, ext = os.path.splitext(filename)
|
||||
return self.shlib_compiler.library_filename(fn, libtype)
|
||||
@@ -192,11 +173,12 @@ class build_ext(_build_ext):
|
||||
self.ext_map = {}
|
||||
self.editable_mode = False
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
_build_ext.finalize_options(self)
|
||||
self.extensions = self.extensions or []
|
||||
self.check_extensions_list(self.extensions)
|
||||
self.shlibs = [ext for ext in self.extensions if isinstance(ext, Library)]
|
||||
self.shlibs = [ext for ext in self.extensions
|
||||
if isinstance(ext, Library)]
|
||||
if self.shlibs:
|
||||
self.setup_shlib_compiler()
|
||||
for ext in self.extensions:
|
||||
@@ -233,7 +215,7 @@ class build_ext(_build_ext):
|
||||
compiler.set_include_dirs(self.include_dirs)
|
||||
if self.define is not None:
|
||||
# 'define' option is a list of (name,value) tuples
|
||||
for name, value in self.define:
|
||||
for (name, value) in self.define:
|
||||
compiler.define_macro(name, value)
|
||||
if self.undef is not None:
|
||||
for macro in self.undef:
|
||||
@@ -248,14 +230,14 @@ class build_ext(_build_ext):
|
||||
compiler.set_link_objects(self.link_objects)
|
||||
|
||||
# hack so distutils' build_extension() builds a library instead
|
||||
compiler.link_shared_object = link_shared_object.__get__(compiler) # type: ignore[method-assign]
|
||||
compiler.link_shared_object = link_shared_object.__get__(compiler)
|
||||
|
||||
def get_export_symbols(self, ext):
|
||||
if isinstance(ext, Library):
|
||||
return ext.export_symbols
|
||||
return _build_ext.get_export_symbols(self, ext)
|
||||
|
||||
def build_extension(self, ext) -> None:
|
||||
def build_extension(self, ext):
|
||||
ext._convert_pyx_sources_to_lang()
|
||||
_compiler = self.compiler
|
||||
try:
|
||||
@@ -277,53 +259,12 @@ class build_ext(_build_ext):
|
||||
pkg = '.'.join(ext._full_name.split('.')[:-1] + [''])
|
||||
return any(pkg + libname in libnames for libname in ext.libraries)
|
||||
|
||||
def get_source_files(self) -> list[str]:
|
||||
return [*_build_ext.get_source_files(self), *self._get_internal_depends()]
|
||||
|
||||
def _get_internal_depends(self) -> Iterator[str]:
|
||||
"""Yield ``ext.depends`` that are contained by the project directory"""
|
||||
project_root = Path(self.distribution.src_root or os.curdir).resolve()
|
||||
depends = (dep for ext in self.extensions for dep in ext.depends)
|
||||
|
||||
def skip(orig_path: str, reason: str) -> None:
|
||||
log.info(
|
||||
"dependency %s won't be automatically "
|
||||
"included in the manifest: the path %s",
|
||||
orig_path,
|
||||
reason,
|
||||
)
|
||||
|
||||
for dep in depends:
|
||||
path = Path(dep)
|
||||
|
||||
if path.is_absolute():
|
||||
skip(dep, "must be relative")
|
||||
continue
|
||||
|
||||
if ".." in path.parts:
|
||||
skip(dep, "can't have `..` segments")
|
||||
continue
|
||||
|
||||
try:
|
||||
resolved = (project_root / path).resolve(strict=True)
|
||||
except OSError:
|
||||
skip(dep, "doesn't exist")
|
||||
continue
|
||||
|
||||
try:
|
||||
resolved.relative_to(project_root)
|
||||
except ValueError:
|
||||
skip(dep, "must be inside the project root")
|
||||
continue
|
||||
|
||||
yield path.as_posix()
|
||||
|
||||
def get_outputs(self) -> list[str]:
|
||||
def get_outputs(self) -> List[str]:
|
||||
if self.inplace:
|
||||
return list(self.get_output_mapping().keys())
|
||||
return sorted(_build_ext.get_outputs(self) + self.__get_stubs_outputs())
|
||||
|
||||
def get_output_mapping(self) -> dict[str, str]:
|
||||
def get_output_mapping(self) -> Dict[str, str]:
|
||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
||||
mapping = self._get_output_mapping()
|
||||
return dict(sorted(mapping, key=lambda x: x[0]))
|
||||
@@ -345,7 +286,7 @@ class build_ext(_build_ext):
|
||||
if self.get_finalized_command('build_py').optimize:
|
||||
yield '.pyo'
|
||||
|
||||
def write_stub(self, output_dir, ext, compile=False) -> None:
|
||||
def write_stub(self, output_dir, ext, compile=False):
|
||||
stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py'
|
||||
self._write_stub_file(stub_file, ext, compile)
|
||||
|
||||
@@ -354,13 +295,16 @@ class build_ext(_build_ext):
|
||||
if compile and os.path.exists(stub_file):
|
||||
raise BaseError(stub_file + " already exists! Please delete.")
|
||||
if not self.dry_run:
|
||||
with open(stub_file, 'w', encoding="utf-8") as f:
|
||||
content = '\n'.join([
|
||||
f = open(stub_file, 'w')
|
||||
f.write(
|
||||
'\n'.join([
|
||||
"def __bootstrap__():",
|
||||
" global __bootstrap__, __file__, __loader__",
|
||||
" import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
|
||||
" import sys, os, pkg_resources, importlib.util" +
|
||||
if_dl(", dl"),
|
||||
" __file__ = pkg_resources.resource_filename"
|
||||
f"(__name__,{os.path.basename(ext._file_name)!r})",
|
||||
"(__name__,%r)"
|
||||
% os.path.basename(ext._file_name),
|
||||
" del __bootstrap__",
|
||||
" if '__loader__' in globals():",
|
||||
" del __loader__",
|
||||
@@ -377,24 +321,22 @@ class build_ext(_build_ext):
|
||||
if_dl(" sys.setdlopenflags(old_flags)"),
|
||||
" os.chdir(old_dir)",
|
||||
"__bootstrap__()",
|
||||
"", # terminal \n
|
||||
"" # terminal \n
|
||||
])
|
||||
f.write(content)
|
||||
)
|
||||
f.close()
|
||||
if compile:
|
||||
self._compile_and_remove_stub(stub_file)
|
||||
|
||||
def _compile_and_remove_stub(self, stub_file: str):
|
||||
from distutils.util import byte_compile
|
||||
|
||||
byte_compile([stub_file], optimize=0, force=True, dry_run=self.dry_run)
|
||||
byte_compile([stub_file], optimize=0,
|
||||
force=True, dry_run=self.dry_run)
|
||||
optimize = self.get_finalized_command('install_lib').optimize
|
||||
if optimize > 0:
|
||||
byte_compile(
|
||||
[stub_file],
|
||||
optimize=optimize,
|
||||
force=True,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
byte_compile([stub_file], optimize=optimize,
|
||||
force=True, dry_run=self.dry_run)
|
||||
if os.path.exists(stub_file) and not self.dry_run:
|
||||
os.unlink(stub_file)
|
||||
|
||||
@@ -403,55 +345,25 @@ if use_stubs or os.name == 'nt':
|
||||
# Build shared libraries
|
||||
#
|
||||
def link_shared_object(
|
||||
self,
|
||||
objects,
|
||||
output_libname,
|
||||
output_dir=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
runtime_library_dirs=None,
|
||||
export_symbols=None,
|
||||
debug: bool = False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
build_temp=None,
|
||||
target_lang=None,
|
||||
) -> None:
|
||||
self, objects, output_libname, output_dir=None, libraries=None,
|
||||
library_dirs=None, runtime_library_dirs=None, export_symbols=None,
|
||||
debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,
|
||||
target_lang=None):
|
||||
self.link(
|
||||
self.SHARED_LIBRARY,
|
||||
objects,
|
||||
output_libname,
|
||||
output_dir,
|
||||
libraries,
|
||||
library_dirs,
|
||||
runtime_library_dirs,
|
||||
export_symbols,
|
||||
debug,
|
||||
extra_preargs,
|
||||
extra_postargs,
|
||||
build_temp,
|
||||
target_lang,
|
||||
self.SHARED_LIBRARY, objects, output_libname,
|
||||
output_dir, libraries, library_dirs, runtime_library_dirs,
|
||||
export_symbols, debug, extra_preargs, extra_postargs,
|
||||
build_temp, target_lang
|
||||
)
|
||||
|
||||
else:
|
||||
# Build static libraries everywhere else
|
||||
libtype = 'static'
|
||||
|
||||
def link_shared_object(
|
||||
self,
|
||||
objects,
|
||||
output_libname,
|
||||
output_dir=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
runtime_library_dirs=None,
|
||||
export_symbols=None,
|
||||
debug: bool = False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
build_temp=None,
|
||||
target_lang=None,
|
||||
) -> None:
|
||||
self, objects, output_libname, output_dir=None, libraries=None,
|
||||
library_dirs=None, runtime_library_dirs=None, export_symbols=None,
|
||||
debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,
|
||||
target_lang=None):
|
||||
# XXX we need to either disallow these attrs on Library instances,
|
||||
# or warn/abort here if set, or something...
|
||||
# libraries=None, library_dirs=None, runtime_library_dirs=None,
|
||||
@@ -460,10 +372,12 @@ else:
|
||||
|
||||
assert output_dir is None # distutils build_ext doesn't pass this
|
||||
output_dir, filename = os.path.split(output_libname)
|
||||
basename, _ext = os.path.splitext(filename)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if self.library_filename("x").startswith('lib'):
|
||||
# strip 'lib' prefix; this is kludgy if some platform uses
|
||||
# a different prefix
|
||||
basename = basename[3:]
|
||||
|
||||
self.create_static_lib(objects, basename, output_dir, debug, target_lang)
|
||||
self.create_static_lib(
|
||||
objects, basename, output_dir, debug, target_lang
|
||||
)
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
import stat
|
||||
import textwrap
|
||||
from collections.abc import Iterable, Iterator
|
||||
from functools import partial
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
|
||||
from more_itertools import unique_everseen
|
||||
|
||||
from .._path import StrPath, StrPathT
|
||||
from ..dist import Distribution
|
||||
from ..warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
import distutils.command.build_py as orig
|
||||
import distutils.errors
|
||||
from distutils.util import convert_path
|
||||
import distutils.command.build_py as orig
|
||||
import os
|
||||
import fnmatch
|
||||
import textwrap
|
||||
import io
|
||||
import distutils.errors
|
||||
import itertools
|
||||
import stat
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
|
||||
|
||||
_IMPLICIT_DATA_FILES = ('*.pyi', 'py.typed')
|
||||
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
|
||||
from setuptools.extern.more_itertools import unique_everseen
|
||||
|
||||
|
||||
def make_writable(target) -> None:
|
||||
def make_writable(target):
|
||||
os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE)
|
||||
|
||||
|
||||
@@ -36,10 +30,8 @@ class build_py(orig.build_py):
|
||||
Also, this version of the 'build_py' command allows you to specify both
|
||||
'py_modules' and 'packages' in the same setup operation.
|
||||
"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
editable_mode: bool = False
|
||||
existing_egg_info_dir: StrPath | None = None #: Private API, internal use only.
|
||||
existing_egg_info_dir: Optional[str] = None #: Private API, internal use only.
|
||||
|
||||
def finalize_options(self):
|
||||
orig.build_py.finalize_options(self)
|
||||
@@ -47,25 +39,18 @@ class build_py(orig.build_py):
|
||||
self.exclude_package_data = self.distribution.exclude_package_data or {}
|
||||
if 'data_files' in self.__dict__:
|
||||
del self.__dict__['data_files']
|
||||
self.__updated_files = []
|
||||
|
||||
def copy_file( # type: ignore[override] # No overload, no bytes support
|
||||
self,
|
||||
infile: StrPath,
|
||||
outfile: StrPathT,
|
||||
preserve_mode: bool = True,
|
||||
preserve_times: bool = True,
|
||||
link: str | None = None,
|
||||
level: object = 1,
|
||||
) -> tuple[StrPathT | str, bool]:
|
||||
def copy_file(self, infile, outfile, preserve_mode=1, preserve_times=1,
|
||||
link=None, level=1):
|
||||
# Overwrite base class to allow using links
|
||||
if link:
|
||||
infile = str(Path(infile).resolve())
|
||||
outfile = str(Path(outfile).resolve()) # type: ignore[assignment] # Re-assigning a str when outfile is StrPath is ok
|
||||
return super().copy_file( # pyright: ignore[reportReturnType] # pypa/distutils#309
|
||||
infile, outfile, preserve_mode, preserve_times, link, level
|
||||
)
|
||||
outfile = str(Path(outfile).resolve())
|
||||
return super().copy_file(infile, outfile, preserve_mode, preserve_times,
|
||||
link, level)
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
"""Build modules, packages, and copy data files to build directory"""
|
||||
if not (self.py_modules or self.packages) or self.editable_mode:
|
||||
return
|
||||
@@ -79,15 +64,21 @@ class build_py(orig.build_py):
|
||||
|
||||
# Only compile actual .py files, using our base class' idea of what our
|
||||
# output files are.
|
||||
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=False))
|
||||
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def __getattr__(self, attr):
|
||||
"lazily compute data files"
|
||||
if attr == 'data_files':
|
||||
self.data_files = self._get_data_files()
|
||||
return self.data_files
|
||||
return orig.build_py.__getattr__(self, attr)
|
||||
|
||||
def build_module(self, module, module_file, package):
|
||||
outfile, copied = orig.build_py.build_module(self, module, module_file, package)
|
||||
if copied:
|
||||
self.__updated_files.append(outfile)
|
||||
return outfile, copied
|
||||
|
||||
def _get_data_files(self):
|
||||
"""Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
|
||||
self.analyze_manifest()
|
||||
@@ -123,7 +114,6 @@ class build_py(orig.build_py):
|
||||
self.package_data,
|
||||
package,
|
||||
src_dir,
|
||||
extra_patterns=_IMPLICIT_DATA_FILES,
|
||||
)
|
||||
globs_expanded = map(partial(glob, recursive=True), patterns)
|
||||
# flatten the expanded globs into an iterable of matches
|
||||
@@ -135,13 +125,13 @@ class build_py(orig.build_py):
|
||||
)
|
||||
return self.exclude_data_files(package, src_dir, files)
|
||||
|
||||
def get_outputs(self, include_bytecode: bool = True) -> list[str]: # type: ignore[override] # Using a real boolean instead of 0|1
|
||||
def get_outputs(self, include_bytecode=1) -> List[str]:
|
||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
||||
if self.editable_mode:
|
||||
return list(self.get_output_mapping().keys())
|
||||
return super().get_outputs(include_bytecode)
|
||||
|
||||
def get_output_mapping(self) -> dict[str, str]:
|
||||
def get_output_mapping(self) -> Dict[str, str]:
|
||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
||||
mapping = itertools.chain(
|
||||
self._get_package_data_output_mapping(),
|
||||
@@ -149,14 +139,14 @@ class build_py(orig.build_py):
|
||||
)
|
||||
return dict(sorted(mapping, key=lambda x: x[0]))
|
||||
|
||||
def _get_module_mapping(self) -> Iterator[tuple[str, str]]:
|
||||
def _get_module_mapping(self) -> Iterator[Tuple[str, str]]:
|
||||
"""Iterate over all modules producing (dest, src) pairs."""
|
||||
for package, module, module_file in self.find_all_modules():
|
||||
for (package, module, module_file) in self.find_all_modules():
|
||||
package = package.split('.')
|
||||
filename = self.get_module_outfile(self.build_lib, package, module)
|
||||
yield (filename, module_file)
|
||||
|
||||
def _get_package_data_output_mapping(self) -> Iterator[tuple[str, str]]:
|
||||
def _get_package_data_output_mapping(self) -> Iterator[Tuple[str, str]]:
|
||||
"""Iterate over package data producing (dest, src) pairs."""
|
||||
for package, src_dir, build_dir, filenames in self.data_files:
|
||||
for filename in filenames:
|
||||
@@ -164,24 +154,24 @@ class build_py(orig.build_py):
|
||||
srcfile = os.path.join(src_dir, filename)
|
||||
yield (target, srcfile)
|
||||
|
||||
def build_package_data(self) -> None:
|
||||
def build_package_data(self):
|
||||
"""Copy data files into build directory"""
|
||||
for target, srcfile in self._get_package_data_output_mapping():
|
||||
self.mkpath(os.path.dirname(target))
|
||||
_outf, _copied = self.copy_file(srcfile, target)
|
||||
make_writable(target)
|
||||
|
||||
def analyze_manifest(self) -> None:
|
||||
self.manifest_files: dict[str, list[str]] = {}
|
||||
def analyze_manifest(self):
|
||||
self.manifest_files = mf = {}
|
||||
if not self.distribution.include_package_data:
|
||||
return
|
||||
src_dirs: dict[str, str] = {}
|
||||
src_dirs = {}
|
||||
for package in self.packages or ():
|
||||
# Locate package source directory
|
||||
src_dirs[assert_relative(self.get_package_dir(package))] = package
|
||||
|
||||
if (
|
||||
self.existing_egg_info_dir
|
||||
getattr(self, 'existing_egg_info_dir', None)
|
||||
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
|
||||
):
|
||||
egg_info_dir = self.existing_egg_info_dir
|
||||
@@ -210,11 +200,9 @@ class build_py(orig.build_py):
|
||||
importable = check.importable_subpackage(src_dirs[d], f)
|
||||
if importable:
|
||||
check.warn(importable)
|
||||
self.manifest_files.setdefault(src_dirs[d], []).append(path)
|
||||
mf.setdefault(src_dirs[d], []).append(path)
|
||||
|
||||
def _filter_build_files(
|
||||
self, files: Iterable[str], egg_info: StrPath
|
||||
) -> Iterator[str]:
|
||||
def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
|
||||
"""
|
||||
``build_meta`` may try to create egg_info outside of the project directory,
|
||||
and this can be problematic for certain plugins (reported in issue #3500).
|
||||
@@ -233,7 +221,7 @@ class build_py(orig.build_py):
|
||||
if not os.path.isabs(file) or all(d not in norm_path for d in norm_dirs):
|
||||
yield file
|
||||
|
||||
def get_data_files(self) -> None:
|
||||
def get_data_files(self):
|
||||
pass # Lazily compute data files in _get_data_files() function.
|
||||
|
||||
def check_package(self, package, package_dir):
|
||||
@@ -255,14 +243,14 @@ class build_py(orig.build_py):
|
||||
else:
|
||||
return init_py
|
||||
|
||||
with open(init_py, 'rb') as f:
|
||||
with io.open(init_py, 'rb') as f:
|
||||
contents = f.read()
|
||||
if b'declare_namespace' not in contents:
|
||||
raise distutils.errors.DistutilsError(
|
||||
f"Namespace package problem: {package} is a namespace package, but "
|
||||
"Namespace package problem: %s is a namespace package, but "
|
||||
"its\n__init__.py does not call declare_namespace()! Please "
|
||||
'fix it.\n(See the setuptools manual under '
|
||||
'"Namespace Packages" for details.)\n"'
|
||||
'"Namespace Packages" for details.)\n"' % (package,)
|
||||
)
|
||||
return init_py
|
||||
|
||||
@@ -295,7 +283,7 @@ class build_py(orig.build_py):
|
||||
return list(unique_everseen(keepers))
|
||||
|
||||
@staticmethod
|
||||
def _get_platform_patterns(spec, package, src_dir, extra_patterns=()):
|
||||
def _get_platform_patterns(spec, package, src_dir):
|
||||
"""
|
||||
yield platform-specific path patterns (suitable for glob
|
||||
or fn_match) from a glob-based spec (such as
|
||||
@@ -303,7 +291,6 @@ class build_py(orig.build_py):
|
||||
matching package in src_dir.
|
||||
"""
|
||||
raw_patterns = itertools.chain(
|
||||
extra_patterns,
|
||||
spec.get('', []),
|
||||
spec.get(package, []),
|
||||
)
|
||||
@@ -338,54 +325,34 @@ def assert_relative(path):
|
||||
class _IncludePackageDataAbuse:
|
||||
"""Inform users that package or module is included as 'data file'"""
|
||||
|
||||
class _Warning(SetuptoolsDeprecationWarning):
|
||||
_SUMMARY = """
|
||||
Package {importable!r} is absent from the `packages` configuration.
|
||||
"""
|
||||
MESSAGE = """\
|
||||
Installing {importable!r} as data is deprecated, please list it in `packages`.
|
||||
!!\n\n
|
||||
############################
|
||||
# Package would be ignored #
|
||||
############################
|
||||
Python recognizes {importable!r} as an importable package,
|
||||
but it is not listed in the `packages` configuration of setuptools.
|
||||
|
||||
_DETAILS = """
|
||||
############################
|
||||
# Package would be ignored #
|
||||
############################
|
||||
Python recognizes {importable!r} as an importable package[^1],
|
||||
but it is absent from setuptools' `packages` configuration.
|
||||
{importable!r} has been automatically added to the distribution only
|
||||
because it may contain data files, but this behavior is likely to change
|
||||
in future versions of setuptools (and therefore is considered deprecated).
|
||||
|
||||
This leads to an ambiguous overall configuration. If you want to distribute this
|
||||
package, please make sure that {importable!r} is explicitly added
|
||||
to the `packages` configuration field.
|
||||
Please make sure that {importable!r} is included as a package by using
|
||||
the `packages` configuration field or the proper discovery methods
|
||||
(for example by using `find_namespace_packages(...)`/`find_namespace:`
|
||||
instead of `find_packages(...)`/`find:`).
|
||||
|
||||
Alternatively, you can also rely on setuptools' discovery methods
|
||||
(for example by using `find_namespace_packages(...)`/`find_namespace:`
|
||||
instead of `find_packages(...)`/`find:`).
|
||||
|
||||
You can read more about "package discovery" on setuptools documentation page:
|
||||
|
||||
- https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
|
||||
|
||||
If you don't want {importable!r} to be distributed and are
|
||||
already explicitly excluding {importable!r} via
|
||||
`find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
|
||||
you can try to use `exclude_package_data`, or `include-package-data=False` in
|
||||
combination with a more fine grained `package-data` configuration.
|
||||
|
||||
You can read more about "package data files" on setuptools documentation page:
|
||||
|
||||
- https://setuptools.pypa.io/en/latest/userguide/datafiles.html
|
||||
|
||||
|
||||
[^1]: For Python, any directory (with suitable naming) can be imported,
|
||||
even if it does not contain any `.py` files.
|
||||
On the other hand, currently there is no concept of package data
|
||||
directory, all directories are treated like packages.
|
||||
"""
|
||||
# _DUE_DATE: still not defined as this is particularly controversial.
|
||||
# Warning initially introduced in May 2022. See issue #3340 for discussion.
|
||||
You can read more about "package discovery" and "data files" on setuptools
|
||||
documentation page.
|
||||
\n\n!!
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._already_warned = set()
|
||||
|
||||
def is_module(self, file):
|
||||
return file.endswith(".py") and file[: -len(".py")].isidentifier()
|
||||
return file.endswith(".py") and file[:-len(".py")].isidentifier()
|
||||
|
||||
def importable_subpackage(self, parent, file):
|
||||
pkg = Path(file).parent
|
||||
@@ -396,5 +363,6 @@ class _IncludePackageDataAbuse:
|
||||
|
||||
def warn(self, importable):
|
||||
if importable not in self._already_warned:
|
||||
self._Warning.emit(importable=importable)
|
||||
msg = textwrap.dedent(self.MESSAGE).format(importable=importable)
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
|
||||
self._already_warned.add(importable)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
import setuptools
|
||||
from setuptools import _normalization, _path, namespaces
|
||||
from setuptools.command.easy_install import easy_install
|
||||
|
||||
from ..unicode_utils import _read_utf8_with_fallback
|
||||
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsOptionError
|
||||
from distutils.util import convert_path
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsError, DistutilsOptionError
|
||||
import os
|
||||
import glob
|
||||
import io
|
||||
|
||||
import pkg_resources
|
||||
from setuptools.command.easy_install import easy_install
|
||||
from setuptools import namespaces
|
||||
import setuptools
|
||||
|
||||
|
||||
class develop(namespaces.DevelopInstaller, easy_install):
|
||||
@@ -42,10 +41,12 @@ class develop(namespaces.DevelopInstaller, easy_install):
|
||||
self.setup_path = None
|
||||
self.always_copy_from = '.' # always copy eggs installed in curdir
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
import pkg_resources
|
||||
|
||||
def finalize_options(self):
|
||||
ei = self.get_finalized_command("egg_info")
|
||||
if ei.broken_egg_info:
|
||||
template = "Please rename %r to %r before using 'develop'"
|
||||
args = ei.egg_info, ei.broken_egg_info
|
||||
raise DistutilsError(template % args)
|
||||
self.args = [ei.egg_name]
|
||||
|
||||
easy_install.finalize_options(self)
|
||||
@@ -54,16 +55,16 @@ class develop(namespaces.DevelopInstaller, easy_install):
|
||||
# pick up setup-dir .egg files only: no .egg-info
|
||||
self.package_index.scan(glob.glob('*.egg'))
|
||||
|
||||
egg_link_fn = (
|
||||
_normalization.filename_component_broken(ei.egg_name) + '.egg-link'
|
||||
)
|
||||
egg_link_fn = ei.egg_name + '.egg-link'
|
||||
self.egg_link = os.path.join(self.install_dir, egg_link_fn)
|
||||
self.egg_base = ei.egg_base
|
||||
if self.egg_path is None:
|
||||
self.egg_path = os.path.abspath(ei.egg_base)
|
||||
|
||||
target = _path.normpath(self.egg_base)
|
||||
egg_path = _path.normpath(os.path.join(self.install_dir, self.egg_path))
|
||||
target = pkg_resources.normalize_path(self.egg_base)
|
||||
egg_path = pkg_resources.normalize_path(
|
||||
os.path.join(self.install_dir, self.egg_path)
|
||||
)
|
||||
if egg_path != target:
|
||||
raise DistutilsOptionError(
|
||||
"--egg-path must be a relative path from the install"
|
||||
@@ -93,22 +94,23 @@ class develop(namespaces.DevelopInstaller, easy_install):
|
||||
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
|
||||
if path_to_setup != os.curdir:
|
||||
path_to_setup = '../' * (path_to_setup.count('/') + 1)
|
||||
resolved = _path.normpath(os.path.join(install_dir, egg_path, path_to_setup))
|
||||
curdir = _path.normpath(os.curdir)
|
||||
if resolved != curdir:
|
||||
resolved = pkg_resources.normalize_path(
|
||||
os.path.join(install_dir, egg_path, path_to_setup)
|
||||
)
|
||||
if resolved != pkg_resources.normalize_path(os.curdir):
|
||||
raise DistutilsOptionError(
|
||||
"Can't get a consistent path to setup script from"
|
||||
" installation directory",
|
||||
resolved,
|
||||
curdir,
|
||||
pkg_resources.normalize_path(os.curdir),
|
||||
)
|
||||
return path_to_setup
|
||||
|
||||
def install_for_development(self) -> None:
|
||||
def install_for_development(self):
|
||||
self.run_command('egg_info')
|
||||
|
||||
# Build extensions in-place
|
||||
self.reinitialize_command('build_ext', inplace=True)
|
||||
self.reinitialize_command('build_ext', inplace=1)
|
||||
self.run_command('build_ext')
|
||||
|
||||
if setuptools.bootstrap_install_from:
|
||||
@@ -120,21 +122,18 @@ class develop(namespaces.DevelopInstaller, easy_install):
|
||||
# create an .egg-link in the installation dir, pointing to our egg
|
||||
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
|
||||
if not self.dry_run:
|
||||
with open(self.egg_link, "w", encoding="utf-8") as f:
|
||||
with open(self.egg_link, "w") as f:
|
||||
f.write(self.egg_path + "\n" + self.setup_path)
|
||||
# postprocess the installed distro, fixing up .pth, installing scripts,
|
||||
# and handling requirements
|
||||
self.process_distribution(None, self.dist, not self.no_deps)
|
||||
|
||||
def uninstall_link(self) -> None:
|
||||
def uninstall_link(self):
|
||||
if os.path.exists(self.egg_link):
|
||||
log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
|
||||
|
||||
contents = [
|
||||
line.rstrip()
|
||||
for line in _read_utf8_with_fallback(self.egg_link).splitlines()
|
||||
]
|
||||
|
||||
egg_link_file = open(self.egg_link)
|
||||
contents = [line.rstrip() for line in egg_link_file]
|
||||
egg_link_file.close()
|
||||
if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
|
||||
log.warn("Link points to %s: uninstall aborted", contents)
|
||||
return
|
||||
@@ -160,11 +159,10 @@ class develop(namespaces.DevelopInstaller, easy_install):
|
||||
for script_name in self.distribution.scripts or []:
|
||||
script_path = os.path.abspath(convert_path(script_name))
|
||||
script_name = os.path.basename(script_path)
|
||||
script_text = _read_utf8_with_fallback(script_path)
|
||||
with io.open(script_path) as strm:
|
||||
script_text = strm.read()
|
||||
self.install_script(dist, script_name, script_text, script_path)
|
||||
|
||||
return None
|
||||
|
||||
def install_wrapper_scripts(self, dist):
|
||||
dist = VersionlessRequirement(dist)
|
||||
return easy_install.install_wrapper_scripts(self, dist)
|
||||
@@ -185,10 +183,10 @@ class VersionlessRequirement:
|
||||
'foo'
|
||||
"""
|
||||
|
||||
def __init__(self, dist) -> None:
|
||||
def __init__(self, dist):
|
||||
self.__dist = dist
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__dist, name)
|
||||
|
||||
def as_requirement(self):
|
||||
|
||||
@@ -4,34 +4,30 @@ As defined in the wheel specification
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from inspect import cleandoc
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from .. import _normalization
|
||||
from .._shutil import rmdir as _rm
|
||||
from .egg_info import egg_info as egg_info_cls
|
||||
|
||||
from distutils import log
|
||||
from distutils.core import Command
|
||||
from distutils import log
|
||||
from setuptools.extern import packaging
|
||||
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
|
||||
|
||||
|
||||
class dist_info(Command):
|
||||
"""
|
||||
This command is private and reserved for internal use of setuptools,
|
||||
users should rely on ``setuptools.build_meta`` APIs.
|
||||
"""
|
||||
|
||||
description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create .dist-info directory"
|
||||
description = 'create a .dist-info directory'
|
||||
|
||||
user_options = [
|
||||
(
|
||||
'output-dir=',
|
||||
'o',
|
||||
"directory inside of which the .dist-info will be"
|
||||
"created [default: top of the source tree]",
|
||||
),
|
||||
('egg-base=', 'e', "directory containing .egg-info directories"
|
||||
" (default: top of the source tree)"
|
||||
" DEPRECATED: use --output-dir."),
|
||||
('output-dir=', 'o', "directory inside of which the .dist-info will be"
|
||||
"created (default: top of the source tree)"),
|
||||
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),
|
||||
('tag-build=', 'b', "Specify explicit tag to add to version number"),
|
||||
('no-date', 'D', "Don't include date stamp [default]"),
|
||||
@@ -42,6 +38,7 @@ class dist_info(Command):
|
||||
negative_opt = {'no-date': 'tag-date'}
|
||||
|
||||
def initialize_options(self):
|
||||
self.egg_base = None
|
||||
self.output_dir = None
|
||||
self.name = None
|
||||
self.dist_info_dir = None
|
||||
@@ -49,12 +46,17 @@ class dist_info(Command):
|
||||
self.tag_build = None
|
||||
self.keep_egg_info = False
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
if self.egg_base:
|
||||
msg = "--egg-base is deprecated for dist_info command. Use --output-dir."
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
||||
self.output_dir = self.egg_base or self.output_dir
|
||||
|
||||
dist = self.distribution
|
||||
project_dir = dist.src_root or os.curdir
|
||||
self.output_dir = Path(self.output_dir or project_dir)
|
||||
|
||||
egg_info = cast(egg_info_cls, self.reinitialize_command("egg_info"))
|
||||
egg_info = self.reinitialize_command("egg_info")
|
||||
egg_info.egg_base = str(self.output_dir)
|
||||
|
||||
if self.tag_date:
|
||||
@@ -70,8 +72,8 @@ class dist_info(Command):
|
||||
egg_info.finalize_options()
|
||||
self.egg_info = egg_info
|
||||
|
||||
name = _normalization.safer_name(dist.get_name())
|
||||
version = _normalization.safer_best_effort_version(dist.get_version())
|
||||
name = _safe(dist.get_name())
|
||||
version = _version(dist.get_version())
|
||||
self.name = f"{name}-{version}"
|
||||
self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info")
|
||||
|
||||
@@ -80,7 +82,7 @@ class dist_info(Command):
|
||||
if requires_bkp:
|
||||
bkp_name = f"{dir_path}.__bkp__"
|
||||
_rm(bkp_name, ignore_errors=True)
|
||||
shutil.copytree(dir_path, bkp_name, dirs_exist_ok=True, symlinks=True)
|
||||
_copy(dir_path, bkp_name, dirs_exist_ok=True, symlinks=True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
@@ -89,15 +91,52 @@ class dist_info(Command):
|
||||
else:
|
||||
yield
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.egg_info.run()
|
||||
egg_info_dir = self.egg_info.egg_info
|
||||
assert os.path.isdir(egg_info_dir), ".egg-info dir should have been created"
|
||||
|
||||
log.info(f"creating '{os.path.abspath(self.dist_info_dir)}'")
|
||||
log.info("creating '{}'".format(os.path.abspath(self.dist_info_dir)))
|
||||
bdist_wheel = self.get_finalized_command('bdist_wheel')
|
||||
|
||||
# TODO: if bdist_wheel if merged into setuptools, just add "keep_egg_info" there
|
||||
with self._maybe_bkp_dir(egg_info_dir, self.keep_egg_info):
|
||||
bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)
|
||||
|
||||
|
||||
def _safe(component: str) -> str:
|
||||
"""Escape a component used to form a wheel name according to PEP 491"""
|
||||
return re.sub(r"[^\w\d.]+", "_", component)
|
||||
|
||||
|
||||
def _version(version: str) -> str:
|
||||
"""Convert an arbitrary string to a version string."""
|
||||
v = version.replace(' ', '.')
|
||||
try:
|
||||
return str(packaging.version.Version(v)).replace("-", "_")
|
||||
except packaging.version.InvalidVersion:
|
||||
msg = f"""Invalid version: {version!r}.
|
||||
!!\n\n
|
||||
###################
|
||||
# Invalid version #
|
||||
###################
|
||||
{version!r} is not valid according to PEP 440.\n
|
||||
Please make sure specify a valid version for your package.
|
||||
Also note that future releases of setuptools may halt the build process
|
||||
if an invalid version is given.
|
||||
\n\n!!
|
||||
"""
|
||||
warnings.warn(cleandoc(msg))
|
||||
return _safe(v).strip("_")
|
||||
|
||||
|
||||
def _rm(dir_name, **opts):
|
||||
if os.path.isdir(dir_name):
|
||||
shutil.rmtree(dir_name, **opts)
|
||||
|
||||
|
||||
def _copy(src, dst, **opts):
|
||||
if sys.version_info < (3, 8):
|
||||
opts.pop("dirs_exist_ok", None)
|
||||
shutil.copytree(src, dst, **opts)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,42 +10,49 @@ Create a wheel that, when installed, will make the source package 'editable'
|
||||
*auxiliary build directory* or ``auxiliary_dir``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
from inspect import cleandoc
|
||||
from itertools import chain, starmap
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Protocol, TypeVar, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .. import Command, _normalization, _path, _shutil, errors, namespaces
|
||||
from .._path import StrPath
|
||||
from ..compat import py312
|
||||
from ..discovery import find_package_path
|
||||
from ..dist import Distribution
|
||||
from ..warnings import InformationOnly, SetuptoolsDeprecationWarning, SetuptoolsWarning
|
||||
from .build import build as build_cls
|
||||
from .build_py import build_py as build_py_cls
|
||||
from .dist_info import dist_info as dist_info_cls
|
||||
from .egg_info import egg_info as egg_info_cls
|
||||
from .install import install as install_cls
|
||||
from .install_scripts import install_scripts as install_scripts_cls
|
||||
from setuptools import Command, SetuptoolsDeprecationWarning, errors, namespaces
|
||||
from setuptools.command.build_py import build_py as build_py_cls
|
||||
from setuptools.discovery import find_package_path
|
||||
from setuptools.dist import Distribution
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from wheel.wheelfile import WheelFile # noqa
|
||||
|
||||
from .._vendor.wheel.wheelfile import WheelFile
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Protocol
|
||||
elif TYPE_CHECKING:
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
from abc import ABC as Protocol
|
||||
|
||||
_P = TypeVar("_P", bound=StrPath)
|
||||
_Path = Union[str, Path]
|
||||
_P = TypeVar("_P", bound=_Path)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -62,7 +69,7 @@ class _EditableMode(Enum):
|
||||
COMPAT = "compat" # TODO: Remove `compat` after Dec/2022.
|
||||
|
||||
@classmethod
|
||||
def convert(cls, mode: str | None) -> _EditableMode:
|
||||
def convert(cls, mode: Optional[str]) -> "_EditableMode":
|
||||
if not mode:
|
||||
return _EditableMode.LENIENT # default
|
||||
|
||||
@@ -71,21 +78,16 @@ class _EditableMode(Enum):
|
||||
raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")
|
||||
|
||||
if _mode == "COMPAT":
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"Compat editable installs",
|
||||
"""
|
||||
The 'compat' editable mode is transitional and will be removed
|
||||
in future versions of `setuptools`.
|
||||
Please adapt your code accordingly to use either the 'strict' or the
|
||||
'lenient' modes.
|
||||
""",
|
||||
see_docs="userguide/development_mode.html",
|
||||
# TODO: define due_date
|
||||
# There is a series of shortcomings with the available editable install
|
||||
# methods, and they are very controversial. This is something that still
|
||||
# needs work.
|
||||
# Moreover, `pip` is still hiding this warning, so users are not aware.
|
||||
)
|
||||
msg = """
|
||||
The 'compat' editable mode is transitional and will be removed
|
||||
in future versions of `setuptools`.
|
||||
Please adapt your code accordingly to use either the 'strict' or the
|
||||
'lenient' modes.
|
||||
|
||||
For more information, please check:
|
||||
https://setuptools.pypa.io/en/latest/userguide/development_mode.html
|
||||
"""
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
||||
|
||||
return _EditableMode[_mode]
|
||||
|
||||
@@ -102,11 +104,10 @@ Options like `package-data`, `include/exclude-package-data` or
|
||||
|
||||
class editable_wheel(Command):
|
||||
"""Build 'editable' wheel for development.
|
||||
This command is private and reserved for internal use of setuptools,
|
||||
users should rely on ``setuptools.build_meta`` APIs.
|
||||
(This command is reserved for internal use of setuptools).
|
||||
"""
|
||||
|
||||
description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"
|
||||
description = "create a PEP 660 'editable' wheel"
|
||||
|
||||
user_options = [
|
||||
("dist-dir=", "d", "directory to put final built distributions in"),
|
||||
@@ -120,13 +121,13 @@ class editable_wheel(Command):
|
||||
self.project_dir = None
|
||||
self.mode = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
dist = self.distribution
|
||||
self.project_dir = dist.src_root or os.curdir
|
||||
self.package_dir = dist.package_dir or {}
|
||||
self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
try:
|
||||
self.dist_dir.mkdir(exist_ok=True)
|
||||
self._ensure_dist_info()
|
||||
@@ -137,15 +138,24 @@ class editable_wheel(Command):
|
||||
bdist_wheel.write_wheelfile(self.dist_info_dir)
|
||||
|
||||
self._create_wheel_file(bdist_wheel)
|
||||
except Exception:
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
project = self.distribution.name or self.distribution.get_name()
|
||||
_DebuggingTips.emit(project=project)
|
||||
raise
|
||||
msg = """
|
||||
Support for editable installs via PEP 660 was recently introduced
|
||||
in `setuptools`. If you are seeing this error, please report to:
|
||||
|
||||
https://github.com/pypa/setuptools/issues
|
||||
|
||||
Meanwhile you can try the legacy behavior by setting an
|
||||
environment variable and trying to install again:
|
||||
|
||||
SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
|
||||
"""
|
||||
raise errors.InternalError(cleandoc(msg)) from ex
|
||||
|
||||
def _ensure_dist_info(self):
|
||||
if self.dist_info_dir is None:
|
||||
dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info"))
|
||||
dist_info = self.reinitialize_command("dist_info")
|
||||
dist_info.output_dir = self.dist_dir
|
||||
dist_info.ensure_finalized()
|
||||
dist_info.run()
|
||||
@@ -164,13 +174,13 @@ class editable_wheel(Command):
|
||||
installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)
|
||||
installer.install_namespaces()
|
||||
|
||||
def _find_egg_info_dir(self) -> str | None:
|
||||
def _find_egg_info_dir(self) -> Optional[str]:
|
||||
parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()
|
||||
candidates = map(str, parent_dir.glob("*.egg-info"))
|
||||
return next(candidates, None)
|
||||
|
||||
def _configure_build(
|
||||
self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath
|
||||
self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
||||
):
|
||||
"""Configure commands to behave in the following ways:
|
||||
|
||||
@@ -192,18 +202,12 @@ class editable_wheel(Command):
|
||||
scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))
|
||||
|
||||
# egg-info may be generated again to create a manifest (used for package data)
|
||||
egg_info = cast(
|
||||
egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True)
|
||||
)
|
||||
egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True)
|
||||
egg_info.egg_base = str(tmp_dir)
|
||||
egg_info.ignore_egg_info_in_manifest = True
|
||||
|
||||
build = cast(
|
||||
build_cls, dist.reinitialize_command("build", reinit_subcommands=True)
|
||||
)
|
||||
install = cast(
|
||||
install_cls, dist.reinitialize_command("install", reinit_subcommands=True)
|
||||
)
|
||||
build = dist.reinitialize_command("build", reinit_subcommands=True)
|
||||
install = dist.reinitialize_command("install", reinit_subcommands=True)
|
||||
|
||||
build.build_platlib = build.build_purelib = build.build_lib = build_lib
|
||||
install.install_purelib = install.install_platlib = install.install_lib = wheel
|
||||
@@ -211,14 +215,12 @@ class editable_wheel(Command):
|
||||
install.install_headers = headers
|
||||
install.install_data = data
|
||||
|
||||
install_scripts = cast(
|
||||
install_scripts_cls, dist.get_command_obj("install_scripts")
|
||||
)
|
||||
install_scripts = dist.get_command_obj("install_scripts")
|
||||
install_scripts.no_ep = True
|
||||
|
||||
build.build_temp = str(tmp_dir)
|
||||
|
||||
build_py = cast(build_py_cls, dist.get_command_obj("build_py"))
|
||||
build_py = dist.get_command_obj("build_py")
|
||||
build_py.compile = False
|
||||
build_py.existing_egg_info_dir = self._find_egg_info_dir()
|
||||
|
||||
@@ -238,9 +240,9 @@ class editable_wheel(Command):
|
||||
elif hasattr(cmd, "inplace"):
|
||||
cmd.inplace = True # backward compatibility with distutils
|
||||
|
||||
def _collect_build_outputs(self) -> tuple[list[str], dict[str, str]]:
|
||||
files: list[str] = []
|
||||
mapping: dict[str, str] = {}
|
||||
def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:
|
||||
files: List[str] = []
|
||||
mapping: Dict[str, str] = {}
|
||||
build = self.get_finalized_command("build")
|
||||
|
||||
for cmd_name in build.get_sub_commands():
|
||||
@@ -253,12 +255,8 @@ class editable_wheel(Command):
|
||||
return files, mapping
|
||||
|
||||
def _run_build_commands(
|
||||
self,
|
||||
dist_name: str,
|
||||
unpacked_wheel: StrPath,
|
||||
build_lib: StrPath,
|
||||
tmp_dir: StrPath,
|
||||
) -> tuple[list[str], dict[str, str]]:
|
||||
self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
|
||||
self._run_build_subcommands()
|
||||
files, mapping = self._collect_build_outputs()
|
||||
@@ -267,7 +265,7 @@ class editable_wheel(Command):
|
||||
self._run_install("data")
|
||||
return files, mapping
|
||||
|
||||
def _run_build_subcommands(self) -> None:
|
||||
def _run_build_subcommands(self):
|
||||
"""
|
||||
Issue #3501 indicates that some plugins/customizations might rely on:
|
||||
|
||||
@@ -278,13 +276,13 @@ class editable_wheel(Command):
|
||||
This method implements a temporary workaround to support the ecosystem
|
||||
while the implementations catch up.
|
||||
"""
|
||||
# TODO: Once plugins/customizations had the chance to catch up, replace
|
||||
# TODO: Once plugins/customisations had the chance to catch up, replace
|
||||
# `self._run_build_subcommands()` with `self.run_command("build")`.
|
||||
# Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
|
||||
build = self.get_finalized_command("build")
|
||||
build: Command = self.get_finalized_command("build")
|
||||
for name in build.get_sub_commands():
|
||||
cmd = self.get_finalized_command(name)
|
||||
if name == "build_py" and type(cmd) is not build_py_cls:
|
||||
if name == "build_py" and type(cmd) != build_py_cls:
|
||||
self._safely_run(name)
|
||||
else:
|
||||
self.run_command(name)
|
||||
@@ -293,29 +291,21 @@ class editable_wheel(Command):
|
||||
try:
|
||||
return self.run_command(cmd_name)
|
||||
except Exception:
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"Customization incompatible with editable install",
|
||||
f"""
|
||||
{traceback.format_exc()}
|
||||
msg = f"""{traceback.format_exc()}\n
|
||||
If you are seeing this warning it is very likely that a setuptools
|
||||
plugin or customization overrides the `{cmd_name}` command, without
|
||||
taking into consideration how editable installs run build steps
|
||||
starting from v64.0.0.
|
||||
|
||||
If you are seeing this warning it is very likely that a setuptools
|
||||
plugin or customization overrides the `{cmd_name}` command, without
|
||||
taking into consideration how editable installs run build steps
|
||||
starting from setuptools v64.0.0.
|
||||
Plugin authors and developers relying on custom build steps are encouraged
|
||||
to update their `{cmd_name}` implementation considering the information in
|
||||
https://setuptools.pypa.io/en/latest/userguide/extension.html
|
||||
about editable installs.
|
||||
|
||||
Plugin authors and developers relying on custom build steps are
|
||||
encouraged to update their `{cmd_name}` implementation considering the
|
||||
information about editable installs in
|
||||
https://setuptools.pypa.io/en/latest/userguide/extension.html.
|
||||
|
||||
For the time being `setuptools` will silence this error and ignore
|
||||
the faulty command, but this behavior will change in future versions.
|
||||
""",
|
||||
# TODO: define due_date
|
||||
# There is a series of shortcomings with the available editable install
|
||||
# methods, and they are very controversial. This is something that still
|
||||
# needs work.
|
||||
)
|
||||
For the time being `setuptools` will silence this error and ignore
|
||||
the faulty command, but this behaviour will change in future versions.\n
|
||||
"""
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
|
||||
|
||||
def _create_wheel_file(self, bdist_wheel):
|
||||
from wheel.wheelfile import WheelFile
|
||||
@@ -336,7 +326,7 @@ class editable_wheel(Command):
|
||||
with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:
|
||||
unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)
|
||||
shutil.copytree(self.dist_info_dir, unpacked_dist_info)
|
||||
self._install_namespaces(unpacked, dist_name)
|
||||
self._install_namespaces(unpacked, dist_info.name)
|
||||
files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
|
||||
strategy = self._select_strategy(dist_name, tag, lib)
|
||||
with strategy, WheelFile(wheel_path, "w") as wheel_obj:
|
||||
@@ -355,8 +345,8 @@ class editable_wheel(Command):
|
||||
self,
|
||||
name: str,
|
||||
tag: str,
|
||||
build_lib: StrPath,
|
||||
) -> EditableStrategy:
|
||||
build_lib: _Path,
|
||||
) -> "EditableStrategy":
|
||||
"""Decides which strategy to use to implement an editable installation."""
|
||||
build_name = f"__editable__.{name}-{tag}"
|
||||
project_dir = Path(self.project_dir)
|
||||
@@ -379,30 +369,28 @@ class editable_wheel(Command):
|
||||
|
||||
|
||||
class EditableStrategy(Protocol):
|
||||
def __call__(
|
||||
self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]
|
||||
) -> object: ...
|
||||
def __enter__(self) -> Self: ...
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: type[BaseException] | None,
|
||||
_exc_value: BaseException | None,
|
||||
_traceback: TracebackType | None,
|
||||
) -> object: ...
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
...
|
||||
|
||||
def __enter__(self):
|
||||
...
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
...
|
||||
|
||||
|
||||
class _StaticPth:
|
||||
def __init__(self, dist: Distribution, name: str, path_entries: list[Path]) -> None:
|
||||
def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
|
||||
self.dist = dist
|
||||
self.name = name
|
||||
self.path_entries = path_entries
|
||||
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]):
|
||||
entries = "\n".join(str(p.resolve()) for p in self.path_entries)
|
||||
contents = _encode_pth(f"{entries}\n")
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
|
||||
contents = bytes(f"{entries}\n", "utf-8")
|
||||
wheel.writestr(f"__editable__.{self.name}.pth", contents)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
def __enter__(self):
|
||||
msg = f"""
|
||||
Editable install will be performed using .pth file to extend `sys.path` with:
|
||||
{list(map(os.fspath, self.path_entries))!r}
|
||||
@@ -410,13 +398,8 @@ class _StaticPth:
|
||||
_logger.warning(msg + _LENIENT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
pass
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
...
|
||||
|
||||
|
||||
class _LinkTree(_StaticPth):
|
||||
@@ -429,24 +412,22 @@ class _LinkTree(_StaticPth):
|
||||
By collocating ``auxiliary_dir`` and the original source code, limitations
|
||||
with hardlinks should be avoided.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dist: Distribution,
|
||||
self, dist: Distribution,
|
||||
name: str,
|
||||
auxiliary_dir: StrPath,
|
||||
build_lib: StrPath,
|
||||
) -> None:
|
||||
auxiliary_dir: _Path,
|
||||
build_lib: _Path,
|
||||
):
|
||||
self.auxiliary_dir = Path(auxiliary_dir)
|
||||
self.build_lib = Path(build_lib).resolve()
|
||||
self._file = dist.get_command_obj("build_py").copy_file
|
||||
super().__init__(dist, name, [self.auxiliary_dir])
|
||||
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]):
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
self._create_links(files, mapping)
|
||||
super().__call__(wheel, files, mapping)
|
||||
|
||||
def _normalize_output(self, file: str) -> str | None:
|
||||
def _normalize_output(self, file: str) -> Optional[str]:
|
||||
# Files relative to build_lib will be normalized to None
|
||||
with suppress(ValueError):
|
||||
path = Path(file).resolve().relative_to(self.build_lib)
|
||||
@@ -459,12 +440,14 @@ class _LinkTree(_StaticPth):
|
||||
dest.parent.mkdir(parents=True)
|
||||
self._file(src_file, dest, link=link)
|
||||
|
||||
def _create_links(self, outputs, output_mapping: Mapping[str, str]):
|
||||
def _create_links(self, outputs, output_mapping):
|
||||
self.auxiliary_dir.mkdir(parents=True, exist_ok=True)
|
||||
link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"
|
||||
normalised = ((self._normalize_output(k), v) for k, v in output_mapping.items())
|
||||
# remove files that are not relative to build_lib
|
||||
mappings = {k: v for k, v in normalised if k is not None}
|
||||
mappings = {
|
||||
self._normalize_output(k): v
|
||||
for k, v in output_mapping.items()
|
||||
}
|
||||
mappings.pop(None, None) # remove files that are not relative to build_lib
|
||||
|
||||
for output in outputs:
|
||||
relative = self._normalize_output(output)
|
||||
@@ -474,17 +457,12 @@ class _LinkTree(_StaticPth):
|
||||
for relative, src in mappings.items():
|
||||
self._create_file(relative, src, link=link_type)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
def __enter__(self):
|
||||
msg = "Strict editable install will be performed using a link tree.\n"
|
||||
_logger.warning(msg + _STRICT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
msg = f"""\n
|
||||
Strict editable installation performed using the auxiliary directory:
|
||||
{self.auxiliary_dir}
|
||||
@@ -492,91 +470,44 @@ class _LinkTree(_StaticPth):
|
||||
Please be careful to not remove this directory, otherwise you might not be able
|
||||
to import/use your package.
|
||||
"""
|
||||
InformationOnly.emit("Editable installation.", msg)
|
||||
warnings.warn(msg, InformationOnly)
|
||||
|
||||
|
||||
class _TopLevelFinder:
|
||||
def __init__(self, dist: Distribution, name: str) -> None:
|
||||
def __init__(self, dist: Distribution, name: str):
|
||||
self.dist = dist
|
||||
self.name = name
|
||||
|
||||
def template_vars(self) -> tuple[str, str, dict[str, str], dict[str, list[str]]]:
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
src_root = self.dist.src_root or os.curdir
|
||||
top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
|
||||
package_dir = self.dist.package_dir or {}
|
||||
roots = _find_package_roots(top_level, package_dir, src_root)
|
||||
|
||||
namespaces_ = dict(
|
||||
chain(
|
||||
_find_namespaces(self.dist.packages or [], roots),
|
||||
((ns, []) for ns in _find_virtual_namespaces(roots)),
|
||||
)
|
||||
)
|
||||
|
||||
legacy_namespaces = {
|
||||
pkg: find_package_path(pkg, roots, self.dist.src_root or "")
|
||||
for pkg in self.dist.namespace_packages or []
|
||||
}
|
||||
|
||||
mapping = {**roots, **legacy_namespaces}
|
||||
# ^-- We need to explicitly add the legacy_namespaces to the mapping to be
|
||||
# able to import their modules even if another package sharing the same
|
||||
# namespace is installed in a conventional (non-editable) way.
|
||||
namespaces_: Dict[str, List[str]] = dict(chain(
|
||||
_find_namespaces(self.dist.packages or [], roots),
|
||||
((ns, []) for ns in _find_virtual_namespaces(roots)),
|
||||
))
|
||||
|
||||
name = f"__editable__.{self.name}.finder"
|
||||
finder = _normalization.safe_identifier(name)
|
||||
return finder, name, mapping, namespaces_
|
||||
finder = _make_identifier(name)
|
||||
content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
|
||||
wheel.writestr(f"{finder}.py", content)
|
||||
|
||||
def get_implementation(self) -> Iterator[tuple[str, bytes]]:
|
||||
finder, name, mapping, namespaces_ = self.template_vars()
|
||||
content = bytes(f"import {finder}; {finder}.install()", "utf-8")
|
||||
wheel.writestr(f"__editable__.{self.name}.pth", content)
|
||||
|
||||
content = bytes(_finder_template(name, mapping, namespaces_), "utf-8")
|
||||
yield (f"{finder}.py", content)
|
||||
|
||||
content = _encode_pth(f"import {finder}; {finder}.install()")
|
||||
yield (f"__editable__.{self.name}.pth", content)
|
||||
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]):
|
||||
for file, content in self.get_implementation():
|
||||
wheel.writestr(file, content)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
def __enter__(self):
|
||||
msg = "Editable install will be performed using a meta path finder.\n"
|
||||
_logger.warning(msg + _LENIENT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
msg = """\n
|
||||
Please be careful with folders in your working directory with the same
|
||||
name as your package as they may take precedence during imports.
|
||||
"""
|
||||
InformationOnly.emit("Editable installation.", msg)
|
||||
|
||||
|
||||
def _encode_pth(content: str) -> bytes:
|
||||
"""
|
||||
Prior to Python 3.13 (see https://github.com/python/cpython/issues/77102),
|
||||
.pth files are always read with 'locale' encoding, the recommendation
|
||||
from the cpython core developers is to write them as ``open(path, "w")``
|
||||
and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).
|
||||
This function tries to simulate this behavior without having to create an
|
||||
actual file, in a way that supports a range of active Python versions.
|
||||
(There seems to be some variety in the way different version of Python handle
|
||||
``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``
|
||||
or ``locale.getencoding()``).
|
||||
"""
|
||||
with io.BytesIO() as buffer:
|
||||
wrapper = io.TextIOWrapper(buffer, encoding=py312.PTH_ENCODING)
|
||||
# TODO: Python 3.13 replace the whole function with `bytes(content, "utf-8")`
|
||||
wrapper.write(content)
|
||||
wrapper.flush()
|
||||
buffer.seek(0)
|
||||
return buffer.read()
|
||||
warnings.warn(msg, InformationOnly)
|
||||
|
||||
|
||||
def _can_symlink_files(base_dir: Path) -> bool:
|
||||
@@ -600,7 +531,7 @@ def _can_symlink_files(base_dir: Path) -> bool:
|
||||
|
||||
|
||||
def _simple_layout(
|
||||
packages: Iterable[str], package_dir: dict[str, str], project_dir: StrPath
|
||||
packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path
|
||||
) -> bool:
|
||||
"""Return ``True`` if:
|
||||
- all packages are contained by the same parent directory, **and**
|
||||
@@ -630,12 +561,15 @@ def _simple_layout(
|
||||
>>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")
|
||||
False
|
||||
"""
|
||||
layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}
|
||||
layout = {
|
||||
pkg: find_package_path(pkg, package_dir, project_dir)
|
||||
for pkg in packages
|
||||
}
|
||||
if not layout:
|
||||
return set(package_dir) in ({}, {""})
|
||||
parent = os.path.commonpath(starmap(_parent_path, layout.items()))
|
||||
parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
|
||||
return all(
|
||||
_path.same_path(Path(parent, *key.split('.')), value)
|
||||
_normalize_path(Path(parent, *key.split('.'))) == _normalize_path(value)
|
||||
for key, value in layout.items()
|
||||
)
|
||||
|
||||
@@ -650,7 +584,7 @@ def _parent_path(pkg, pkg_path):
|
||||
>>> _parent_path("b", "src/c")
|
||||
'src/c'
|
||||
"""
|
||||
parent = pkg_path[: -len(pkg)] if pkg_path.endswith(pkg) else pkg_path
|
||||
parent = pkg_path[:-len(pkg)] if pkg_path.endswith(pkg) else pkg_path
|
||||
return parent.rstrip("/" + os.sep)
|
||||
|
||||
|
||||
@@ -682,9 +616,9 @@ def _find_top_level_modules(dist: Distribution) -> Iterator[str]:
|
||||
def _find_package_roots(
|
||||
packages: Iterable[str],
|
||||
package_dir: Mapping[str, str],
|
||||
src_root: StrPath,
|
||||
) -> dict[str, str]:
|
||||
pkg_roots: dict[str, str] = {
|
||||
src_root: _Path,
|
||||
) -> Dict[str, str]:
|
||||
pkg_roots: Dict[str, str] = {
|
||||
pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))
|
||||
for pkg in sorted(packages)
|
||||
}
|
||||
@@ -692,7 +626,7 @@ def _find_package_roots(
|
||||
return _remove_nested(pkg_roots)
|
||||
|
||||
|
||||
def _absolute_root(path: StrPath) -> str:
|
||||
def _absolute_root(path: _Path) -> str:
|
||||
"""Works for packages and top-level modules"""
|
||||
path_ = Path(path)
|
||||
parent = path_.parent
|
||||
@@ -703,7 +637,7 @@ def _absolute_root(path: StrPath) -> str:
|
||||
return str(parent.resolve() / path_.name)
|
||||
|
||||
|
||||
def _find_virtual_namespaces(pkg_roots: dict[str, str]) -> Iterator[str]:
|
||||
def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
|
||||
"""By carefully designing ``package_dir``, it is possible to implement the logical
|
||||
structure of PEP 420 in a package without the corresponding directories.
|
||||
|
||||
@@ -728,15 +662,15 @@ def _find_virtual_namespaces(pkg_roots: dict[str, str]) -> Iterator[str]:
|
||||
|
||||
|
||||
def _find_namespaces(
|
||||
packages: list[str], pkg_roots: dict[str, str]
|
||||
) -> Iterator[tuple[str, list[str]]]:
|
||||
packages: List[str], pkg_roots: Dict[str, str]
|
||||
) -> Iterator[Tuple[str, List[str]]]:
|
||||
for pkg in packages:
|
||||
path = find_package_path(pkg, pkg_roots, "")
|
||||
if Path(path).exists() and not Path(path, "__init__.py").exists():
|
||||
yield (pkg, [path])
|
||||
|
||||
|
||||
def _remove_nested(pkg_roots: dict[str, str]) -> dict[str, str]:
|
||||
def _remove_nested(pkg_roots: Dict[str, str]) -> Dict[str, str]:
|
||||
output = dict(pkg_roots.copy())
|
||||
|
||||
for pkg, path in reversed(list(pkg_roots.items())):
|
||||
@@ -764,32 +698,52 @@ def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:
|
||||
>>> _is_nested("b.a", "path/b/a", "a", "path/a")
|
||||
False
|
||||
"""
|
||||
norm_pkg_path = _path.normpath(pkg_path)
|
||||
norm_pkg_path = _normalize_path(pkg_path)
|
||||
rest = pkg.replace(parent, "", 1).strip(".").split(".")
|
||||
return pkg.startswith(parent) and norm_pkg_path == _path.normpath(
|
||||
Path(parent_path, *rest)
|
||||
return (
|
||||
pkg.startswith(parent)
|
||||
and norm_pkg_path == _normalize_path(Path(parent_path, *rest))
|
||||
)
|
||||
|
||||
|
||||
def _normalize_path(filename: _Path) -> str:
|
||||
"""Normalize a file/dir name for comparison purposes"""
|
||||
# See pkg_resources.normalize_path
|
||||
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
|
||||
return os.path.normcase(os.path.realpath(os.path.normpath(file)))
|
||||
|
||||
|
||||
def _empty_dir(dir_: _P) -> _P:
|
||||
"""Create a directory ensured to be empty. Existing files may be removed."""
|
||||
_shutil.rmtree(dir_, ignore_errors=True)
|
||||
shutil.rmtree(dir_, ignore_errors=True)
|
||||
os.makedirs(dir_)
|
||||
return dir_
|
||||
|
||||
|
||||
def _make_identifier(name: str) -> str:
|
||||
"""Make a string safe to be used as Python identifier.
|
||||
>>> _make_identifier("12abc")
|
||||
'_12abc'
|
||||
>>> _make_identifier("__editable__.myns.pkg-78.9.3_local")
|
||||
'__editable___myns_pkg_78_9_3_local'
|
||||
"""
|
||||
safe = re.sub(r'\W|^(?=\d)', '_', name)
|
||||
assert safe.isidentifier()
|
||||
return safe
|
||||
|
||||
|
||||
class _NamespaceInstaller(namespaces.Installer):
|
||||
def __init__(self, distribution, installation_dir, editable_name, src_root) -> None:
|
||||
def __init__(self, distribution, installation_dir, editable_name, src_root):
|
||||
self.distribution = distribution
|
||||
self.src_root = src_root
|
||||
self.installation_dir = installation_dir
|
||||
self.editable_name = editable_name
|
||||
self.outputs: list[str] = []
|
||||
self.outputs = []
|
||||
self.dry_run = False
|
||||
|
||||
def _get_nspkg_file(self):
|
||||
def _get_target(self):
|
||||
"""Installation target."""
|
||||
return os.path.join(self.installation_dir, self.editable_name + self.nspkg_ext)
|
||||
return os.path.join(self.installation_dir, self.editable_name)
|
||||
|
||||
def _get_root(self):
|
||||
"""Where the modules/packages should be loaded from."""
|
||||
@@ -797,68 +751,51 @@ class _NamespaceInstaller(namespaces.Installer):
|
||||
|
||||
|
||||
_FINDER_TEMPLATE = """\
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from importlib.machinery import ModuleSpec, PathFinder
|
||||
from importlib.machinery import ModuleSpec
|
||||
from importlib.machinery import all_suffixes as module_suffixes
|
||||
from importlib.util import spec_from_file_location
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
MAPPING: dict[str, str] = {mapping!r}
|
||||
NAMESPACES: dict[str, list[str]] = {namespaces!r}
|
||||
MAPPING = {mapping!r}
|
||||
NAMESPACES = {namespaces!r}
|
||||
PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
|
||||
|
||||
|
||||
class _EditableFinder: # MetaPathFinder
|
||||
@classmethod
|
||||
def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None: # type: ignore
|
||||
# Top-level packages and modules (we know these exist in the FS)
|
||||
if fullname in MAPPING:
|
||||
pkg_path = MAPPING[fullname]
|
||||
return cls._find_spec(fullname, Path(pkg_path))
|
||||
def find_spec(cls, fullname, path=None, target=None):
|
||||
for pkg, pkg_path in reversed(list(MAPPING.items())):
|
||||
if fullname == pkg or fullname.startswith(f"{{pkg}}."):
|
||||
rest = fullname.replace(pkg, "", 1).strip(".").split(".")
|
||||
return cls._find_spec(fullname, Path(pkg_path, *rest))
|
||||
|
||||
# Handle immediate children modules (required for namespaces to work)
|
||||
# To avoid problems with case sensitivity in the file system we delegate
|
||||
# to the importlib.machinery implementation.
|
||||
parent, _, child = fullname.rpartition(".")
|
||||
if parent and parent in MAPPING:
|
||||
return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
|
||||
|
||||
# Other levels of nesting should be handled automatically by importlib
|
||||
# using the parent path.
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:
|
||||
def _find_spec(cls, fullname, candidate_path):
|
||||
init = candidate_path / "__init__.py"
|
||||
candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
|
||||
for candidate in chain([init], candidates):
|
||||
if candidate.exists():
|
||||
return spec_from_file_location(fullname, candidate)
|
||||
return None
|
||||
|
||||
|
||||
class _EditableNamespaceFinder: # PathEntryFinder
|
||||
@classmethod
|
||||
def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:
|
||||
def _path_hook(cls, path):
|
||||
if path == PATH_PLACEHOLDER:
|
||||
return cls
|
||||
raise ImportError
|
||||
|
||||
@classmethod
|
||||
def _paths(cls, fullname: str) -> list[str]:
|
||||
paths = NAMESPACES[fullname]
|
||||
if not paths and fullname in MAPPING:
|
||||
paths = [MAPPING[fullname]]
|
||||
# Always add placeholder, for 2 reasons:
|
||||
# 1. __path__ cannot be empty for the spec to be considered namespace.
|
||||
# 2. In the case of nested namespaces, we need to force
|
||||
# import machinery to query _EditableNamespaceFinder again.
|
||||
return [*paths, PATH_PLACEHOLDER]
|
||||
def _paths(cls, fullname):
|
||||
# Ensure __path__ is not empty for the spec to be considered a namespace.
|
||||
return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
|
||||
|
||||
@classmethod
|
||||
def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None: # type: ignore
|
||||
def find_spec(cls, fullname, target=None):
|
||||
if fullname in NAMESPACES:
|
||||
spec = ModuleSpec(fullname, None, is_package=True)
|
||||
spec.submodule_search_locations = cls._paths(fullname)
|
||||
@@ -866,7 +803,7 @@ class _EditableNamespaceFinder: # PathEntryFinder
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_module(cls, _fullname) -> None:
|
||||
def find_module(cls, fullname):
|
||||
return None
|
||||
|
||||
|
||||
@@ -886,7 +823,7 @@ def install():
|
||||
|
||||
|
||||
def _finder_template(
|
||||
name: str, mapping: Mapping[str, str], namespaces: dict[str, list[str]]
|
||||
name: str, mapping: Mapping[str, str], namespaces: Dict[str, List[str]]
|
||||
) -> str:
|
||||
"""Create a string containing the code for the``MetaPathFinder`` and
|
||||
``PathEntryFinder``.
|
||||
@@ -895,31 +832,13 @@ def _finder_template(
|
||||
return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)
|
||||
|
||||
|
||||
class InformationOnly(UserWarning):
|
||||
"""Currently there is no clear way of displaying messages to the users
|
||||
that use the setuptools backend directly via ``pip``.
|
||||
The only thing that might work is a warning, although it is not the
|
||||
most appropriate tool for the job...
|
||||
"""
|
||||
|
||||
|
||||
class LinksNotSupported(errors.FileError):
|
||||
"""File system does not seem to support either symlinks or hard links."""
|
||||
|
||||
|
||||
class _DebuggingTips(SetuptoolsWarning):
|
||||
_SUMMARY = "Problem in editable installation."
|
||||
_DETAILS = """
|
||||
An error happened while installing `{project}` in editable mode.
|
||||
|
||||
The following steps are recommended to help debug this problem:
|
||||
|
||||
- Try to install the project normally, without using the editable mode.
|
||||
Does the error still persist?
|
||||
(If it does, try fixing the problem before attempting the editable mode).
|
||||
- If you are using binary extensions, make sure you have all OS-level
|
||||
dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
|
||||
- Try the latest version of setuptools (maybe the error was already fixed).
|
||||
- If you (or your project dependencies) are using any setuptools extension
|
||||
or customization, make sure they support the editable mode.
|
||||
|
||||
After following the steps above, if the problem still persists and
|
||||
you think this is related to how setuptools handles editable installations,
|
||||
please submit a reproducible example
|
||||
(see https://stackoverflow.com/help/minimal-reproducible-example) to:
|
||||
|
||||
https://github.com/pypa/setuptools/issues
|
||||
"""
|
||||
_SEE_DOCS = "userguide/development_mode.html"
|
||||
|
||||
@@ -2,37 +2,38 @@
|
||||
|
||||
Create a distribution's .egg-info directory and contents"""
|
||||
|
||||
from distutils.filelist import FileList as _FileList
|
||||
from distutils.errors import DistutilsInternalError
|
||||
from distutils.util import convert_path
|
||||
from distutils import log
|
||||
import distutils.errors
|
||||
import distutils.filelist
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import io
|
||||
import warnings
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
import collections
|
||||
|
||||
import packaging
|
||||
import packaging.requirements
|
||||
import packaging.version
|
||||
from .._importlib import metadata
|
||||
from .. import _entry_points
|
||||
|
||||
import setuptools.unicode_utils as unicode_utils
|
||||
from setuptools import Command
|
||||
from setuptools.command import bdist_egg
|
||||
from setuptools.command.sdist import sdist, walk_revctrl
|
||||
from setuptools.command.sdist import sdist
|
||||
from setuptools.command.sdist import walk_revctrl
|
||||
from setuptools.command.setopt import edit_config
|
||||
from setuptools.command import bdist_egg
|
||||
from pkg_resources import (
|
||||
Requirement, safe_name, parse_version,
|
||||
safe_version, to_filename)
|
||||
import setuptools.unicode_utils as unicode_utils
|
||||
from setuptools.glob import glob
|
||||
|
||||
from .. import _entry_points, _normalization
|
||||
from .._importlib import metadata
|
||||
from ..warnings import SetuptoolsDeprecationWarning
|
||||
from . import _requirestxt
|
||||
|
||||
import distutils.errors
|
||||
import distutils.filelist
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsInternalError
|
||||
from distutils.filelist import FileList as _FileList
|
||||
from distutils.util import convert_path
|
||||
|
||||
PY_MAJOR = f'{sys.version_info.major}.{sys.version_info.minor}'
|
||||
from setuptools.extern import packaging
|
||||
from setuptools.extern.jaraco.text import yield_lines
|
||||
from setuptools import SetuptoolsDeprecationWarning
|
||||
|
||||
|
||||
def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
@@ -48,7 +49,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
chunks = glob.split(os.path.sep)
|
||||
|
||||
sep = re.escape(os.sep)
|
||||
valid_char = f'[^{sep}]'
|
||||
valid_char = '[^%s]' % (sep,)
|
||||
|
||||
for c, chunk in enumerate(chunks):
|
||||
last_chunk = c == len(chunks) - 1
|
||||
@@ -60,7 +61,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
pat += '.*'
|
||||
else:
|
||||
# Match '(name/)*'
|
||||
pat += f'(?:{valid_char}+{sep})*'
|
||||
pat += '(?:%s+%s)*' % (valid_char, sep)
|
||||
continue # Break here as the whole path component has been handled
|
||||
|
||||
# Find any special characters in the remainder
|
||||
@@ -93,7 +94,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
pat += re.escape(char)
|
||||
else:
|
||||
# Grab the insides of the [brackets]
|
||||
inner = chunk[i + 1 : inner_i]
|
||||
inner = chunk[i + 1:inner_i]
|
||||
char_class = ''
|
||||
|
||||
# Class negation
|
||||
@@ -102,7 +103,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
inner = inner[1:]
|
||||
|
||||
char_class += re.escape(inner)
|
||||
pat += f'[{char_class}]'
|
||||
pat += '[%s]' % (char_class,)
|
||||
|
||||
# Skip to the end ]
|
||||
i = inner_i
|
||||
@@ -124,11 +125,10 @@ class InfoCommon:
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return _normalization.safe_name(self.distribution.get_name())
|
||||
return safe_name(self.distribution.get_name())
|
||||
|
||||
def tagged_version(self):
|
||||
tagged = self._maybe_tag(self.distribution.get_version())
|
||||
return _normalization.safe_version(tagged)
|
||||
return safe_version(self._maybe_tag(self.distribution.get_version()))
|
||||
|
||||
def _maybe_tag(self, version):
|
||||
"""
|
||||
@@ -136,8 +136,7 @@ class InfoCommon:
|
||||
in which case the version string already contains all tags.
|
||||
"""
|
||||
return (
|
||||
version
|
||||
if self.vtags and self._already_tagged(version)
|
||||
version if self.vtags and self._already_tagged(version)
|
||||
else version + self.vtags
|
||||
)
|
||||
|
||||
@@ -149,19 +148,15 @@ class InfoCommon:
|
||||
def _safe_tags(self) -> str:
|
||||
# To implement this we can rely on `safe_version` pretending to be version 0
|
||||
# followed by tags. Then we simply discard the starting 0 (fake version number)
|
||||
try:
|
||||
return _normalization.safe_version(f"0{self.vtags}")[1:]
|
||||
except packaging.version.InvalidVersion:
|
||||
return _normalization.safe_name(self.vtags.replace(' ', '.'))
|
||||
return safe_version(f"0{self.vtags}")[1:]
|
||||
|
||||
def tags(self) -> str:
|
||||
version = ''
|
||||
if self.tag_build:
|
||||
version += self.tag_build
|
||||
if self.tag_date:
|
||||
version += time.strftime("%Y%m%d")
|
||||
version += time.strftime("-%Y%m%d")
|
||||
return version
|
||||
|
||||
vtags = property(tags)
|
||||
|
||||
|
||||
@@ -169,12 +164,8 @@ class egg_info(InfoCommon, Command):
|
||||
description = "create a distribution's .egg-info directory"
|
||||
|
||||
user_options = [
|
||||
(
|
||||
'egg-base=',
|
||||
'e',
|
||||
"directory containing .egg-info directories"
|
||||
" [default: top of the source tree]",
|
||||
),
|
||||
('egg-base=', 'e', "directory containing .egg-info directories"
|
||||
" (default: top of the source tree)"),
|
||||
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),
|
||||
('tag-build=', 'b', "Specify explicit tag to add to version number"),
|
||||
('no-date', 'D', "Don't include date stamp [default]"),
|
||||
@@ -190,48 +181,51 @@ class egg_info(InfoCommon, Command):
|
||||
self.egg_name = None
|
||||
self.egg_info = None
|
||||
self.egg_version = None
|
||||
self.broken_egg_info = False
|
||||
self.ignore_egg_info_in_manifest = False
|
||||
|
||||
####################################
|
||||
# allow the 'tag_svn_revision' to be detected and
|
||||
# set, supporting sdists built on older Setuptools.
|
||||
@property
|
||||
def tag_svn_revision(self) -> None:
|
||||
def tag_svn_revision(self):
|
||||
pass
|
||||
|
||||
@tag_svn_revision.setter
|
||||
def tag_svn_revision(self, value):
|
||||
pass
|
||||
|
||||
####################################
|
||||
|
||||
def save_version_info(self, filename) -> None:
|
||||
def save_version_info(self, filename):
|
||||
"""
|
||||
Materialize the value of date into the
|
||||
build tag. Install build keys in a deterministic order
|
||||
to avoid arbitrary reordering on subsequent builds.
|
||||
"""
|
||||
egg_info = collections.OrderedDict()
|
||||
# follow the order these keys would have been added
|
||||
# when PYTHONHASHSEED=0
|
||||
egg_info = dict(tag_build=self.tags(), tag_date=0)
|
||||
egg_info['tag_build'] = self.tags()
|
||||
egg_info['tag_date'] = 0
|
||||
edit_config(filename, dict(egg_info=egg_info))
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
# Note: we need to capture the current value returned
|
||||
# by `self.tagged_version()`, so we can later update
|
||||
# `self.distribution.metadata.version` without
|
||||
# repercussions.
|
||||
self.egg_name = self.name
|
||||
self.egg_version = self.tagged_version()
|
||||
parsed_version = packaging.version.Version(self.egg_version)
|
||||
parsed_version = parse_version(self.egg_version)
|
||||
|
||||
try:
|
||||
is_version = isinstance(parsed_version, packaging.version.Version)
|
||||
spec = "%s==%s" if is_version else "%s===%s"
|
||||
packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version))
|
||||
Requirement(spec % (self.egg_name, self.egg_version))
|
||||
except ValueError as e:
|
||||
raise distutils.errors.DistutilsOptionError(
|
||||
f"Invalid distribution name or version syntax: {self.egg_name}-{self.egg_version}"
|
||||
"Invalid distribution name or version syntax: %s-%s" %
|
||||
(self.egg_name, self.egg_version)
|
||||
) from e
|
||||
|
||||
if self.egg_base is None:
|
||||
@@ -239,20 +233,28 @@ class egg_info(InfoCommon, Command):
|
||||
self.egg_base = (dirs or {}).get('', os.curdir)
|
||||
|
||||
self.ensure_dirname('egg_base')
|
||||
self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info'
|
||||
self.egg_info = to_filename(self.egg_name) + '.egg-info'
|
||||
if self.egg_base != os.curdir:
|
||||
self.egg_info = os.path.join(self.egg_base, self.egg_info)
|
||||
if '-' in self.egg_name:
|
||||
self.check_broken_egg_info()
|
||||
|
||||
# Set package version for the benefit of dumber commands
|
||||
# (e.g. sdist, bdist_wininst, etc.)
|
||||
#
|
||||
self.distribution.metadata.version = self.egg_version
|
||||
|
||||
def _get_egg_basename(self, py_version=PY_MAJOR, platform=None):
|
||||
"""Compute filename of the output egg. Private API."""
|
||||
return _egg_basename(self.egg_name, self.egg_version, py_version, platform)
|
||||
# If we bootstrapped around the lack of a PKG-INFO, as might be the
|
||||
# case in a fresh checkout, make sure that any special tags get added
|
||||
# to the version info
|
||||
#
|
||||
pd = self.distribution._patched_dist
|
||||
if pd is not None and pd.key == self.egg_name.lower():
|
||||
pd._version = self.egg_version
|
||||
pd._parsed_version = parse_version(self.egg_version)
|
||||
self.distribution._patched_dist = None
|
||||
|
||||
def write_or_delete_file(self, what, filename, data, force: bool = False) -> None:
|
||||
def write_or_delete_file(self, what, filename, data, force=False):
|
||||
"""Write `data` to `filename` or delete if empty
|
||||
|
||||
If `data` is non-empty, this routine is the same as ``write_file()``.
|
||||
@@ -265,12 +267,14 @@ class egg_info(InfoCommon, Command):
|
||||
self.write_file(what, filename, data)
|
||||
elif os.path.exists(filename):
|
||||
if data is None and not force:
|
||||
log.warn("%s not set in setup(), but %s exists", what, filename)
|
||||
log.warn(
|
||||
"%s not set in setup(), but %s exists", what, filename
|
||||
)
|
||||
return
|
||||
else:
|
||||
self.delete_file(filename)
|
||||
|
||||
def write_file(self, what, filename, data) -> None:
|
||||
def write_file(self, what, filename, data):
|
||||
"""Write `data` to `filename` (if not a dry run) after announcing it
|
||||
|
||||
`what` is used in a log message to identify what is being written
|
||||
@@ -283,24 +287,16 @@ class egg_info(InfoCommon, Command):
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
def delete_file(self, filename) -> None:
|
||||
def delete_file(self, filename):
|
||||
"""Delete `filename` (if not a dry run) after announcing it"""
|
||||
log.info("deleting %s", filename)
|
||||
if not self.dry_run:
|
||||
os.unlink(filename)
|
||||
|
||||
def run(self) -> None:
|
||||
# Pre-load to avoid iterating over entry-points while an empty .egg-info
|
||||
# exists in sys.path. See pypa/pyproject-hooks#206
|
||||
writers = list(metadata.entry_points(group='egg_info.writers'))
|
||||
|
||||
def run(self):
|
||||
self.mkpath(self.egg_info)
|
||||
try:
|
||||
os.utime(self.egg_info, None)
|
||||
except OSError as e:
|
||||
msg = f"Cannot update time stamp of directory '{self.egg_info}'"
|
||||
raise distutils.errors.DistutilsFileError(msg) from e
|
||||
for ep in writers:
|
||||
os.utime(self.egg_info, None)
|
||||
for ep in metadata.entry_points(group='egg_info.writers'):
|
||||
writer = ep.load()
|
||||
writer(self, ep.name, os.path.join(self.egg_info, ep.name))
|
||||
|
||||
@@ -311,7 +307,7 @@ class egg_info(InfoCommon, Command):
|
||||
|
||||
self.find_sources()
|
||||
|
||||
def find_sources(self) -> None:
|
||||
def find_sources(self):
|
||||
"""Generate SOURCES.txt manifest file"""
|
||||
manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")
|
||||
mm = manifest_maker(self.distribution)
|
||||
@@ -320,17 +316,30 @@ class egg_info(InfoCommon, Command):
|
||||
mm.run()
|
||||
self.filelist = mm.filelist
|
||||
|
||||
def check_broken_egg_info(self):
|
||||
bei = self.egg_name + '.egg-info'
|
||||
if self.egg_base != os.curdir:
|
||||
bei = os.path.join(self.egg_base, bei)
|
||||
if os.path.exists(bei):
|
||||
log.warn(
|
||||
"-" * 78 + '\n'
|
||||
"Note: Your current .egg-info directory has a '-' in its name;"
|
||||
'\nthis will not work correctly with "setup.py develop".\n\n'
|
||||
'Please rename %s to %s to correct this problem.\n' + '-' * 78,
|
||||
bei, self.egg_info
|
||||
)
|
||||
self.broken_egg_info = self.egg_info
|
||||
self.egg_info = bei # make it work for now
|
||||
|
||||
|
||||
class FileList(_FileList):
|
||||
# Implementations of the various MANIFEST.in commands
|
||||
|
||||
def __init__(
|
||||
self, warn=None, debug_print=None, ignore_egg_info_dir: bool = False
|
||||
) -> None:
|
||||
def __init__(self, warn=None, debug_print=None, ignore_egg_info_dir=False):
|
||||
super().__init__(warn, debug_print)
|
||||
self.ignore_egg_info_dir = ignore_egg_info_dir
|
||||
|
||||
def process_template_line(self, line) -> None:
|
||||
def process_template_line(self, line):
|
||||
# Parse the line: split it up, make sure the right number of words
|
||||
# is there, and return the relevant words. 'action' is always
|
||||
# defined: it's the first word of the line. Which of the other
|
||||
@@ -338,34 +347,37 @@ class FileList(_FileList):
|
||||
# patterns, (dir and patterns), or (dir_pattern).
|
||||
(action, patterns, dir, dir_pattern) = self._parse_template_line(line)
|
||||
|
||||
action_map: dict[str, Callable] = {
|
||||
action_map = {
|
||||
'include': self.include,
|
||||
'exclude': self.exclude,
|
||||
'global-include': self.global_include,
|
||||
'global-exclude': self.global_exclude,
|
||||
'recursive-include': functools.partial(
|
||||
self.recursive_include,
|
||||
dir,
|
||||
self.recursive_include, dir,
|
||||
),
|
||||
'recursive-exclude': functools.partial(
|
||||
self.recursive_exclude,
|
||||
dir,
|
||||
self.recursive_exclude, dir,
|
||||
),
|
||||
'graft': self.graft,
|
||||
'prune': self.prune,
|
||||
}
|
||||
log_map = {
|
||||
'include': "warning: no files found matching '%s'",
|
||||
'exclude': ("warning: no previously-included files found matching '%s'"),
|
||||
'exclude': (
|
||||
"warning: no previously-included files found "
|
||||
"matching '%s'"
|
||||
),
|
||||
'global-include': (
|
||||
"warning: no files found matching '%s' anywhere in distribution"
|
||||
"warning: no files found matching '%s' "
|
||||
"anywhere in distribution"
|
||||
),
|
||||
'global-exclude': (
|
||||
"warning: no previously-included files matching "
|
||||
"'%s' found anywhere in distribution"
|
||||
),
|
||||
'recursive-include': (
|
||||
"warning: no files found matching '%s' under directory '%s'"
|
||||
"warning: no files found matching '%s' "
|
||||
"under directory '%s'"
|
||||
),
|
||||
'recursive-exclude': (
|
||||
"warning: no previously-included files matching "
|
||||
@@ -378,8 +390,10 @@ class FileList(_FileList):
|
||||
try:
|
||||
process_action = action_map[action]
|
||||
except KeyError:
|
||||
msg = f"Invalid MANIFEST.in: unknown action {action!r} in {line!r}"
|
||||
raise DistutilsInternalError(msg) from None
|
||||
raise DistutilsInternalError(
|
||||
"this cannot happen: invalid action '{action!s}'".
|
||||
format(action=action),
|
||||
)
|
||||
|
||||
# OK, now we know that the action is valid and we have the
|
||||
# right number of words on the line for that action -- so we
|
||||
@@ -388,12 +402,14 @@ class FileList(_FileList):
|
||||
action_is_recursive = action.startswith('recursive-')
|
||||
if action in {'graft', 'prune'}:
|
||||
patterns = [dir_pattern]
|
||||
extra_log_args = (dir,) if action_is_recursive else ()
|
||||
extra_log_args = (dir, ) if action_is_recursive else ()
|
||||
log_tmpl = log_map[action]
|
||||
|
||||
self.debug_print(
|
||||
' '.join(
|
||||
[action] + ([dir] if action_is_recursive else []) + patterns,
|
||||
[action] +
|
||||
([dir] if action_is_recursive else []) +
|
||||
patterns,
|
||||
)
|
||||
)
|
||||
for pattern in patterns:
|
||||
@@ -429,7 +445,8 @@ class FileList(_FileList):
|
||||
Include all files anywhere in 'dir/' that match the pattern.
|
||||
"""
|
||||
full_pattern = os.path.join(dir, '**', pattern)
|
||||
found = [f for f in glob(full_pattern, recursive=True) if not os.path.isdir(f)]
|
||||
found = [f for f in glob(full_pattern, recursive=True)
|
||||
if not os.path.isdir(f)]
|
||||
self.extend(found)
|
||||
return bool(found)
|
||||
|
||||
@@ -474,7 +491,7 @@ class FileList(_FileList):
|
||||
match = translate_pattern(os.path.join('**', pattern))
|
||||
return self._remove_files(match.match)
|
||||
|
||||
def append(self, item) -> None:
|
||||
def append(self, item):
|
||||
if item.endswith('\r'): # Fix older sdists built on Windows
|
||||
item = item[:-1]
|
||||
path = convert_path(item)
|
||||
@@ -482,7 +499,7 @@ class FileList(_FileList):
|
||||
if self._safe_path(path):
|
||||
self.files.append(path)
|
||||
|
||||
def extend(self, paths) -> None:
|
||||
def extend(self, paths):
|
||||
self.files.extend(filter(self._safe_path, paths))
|
||||
|
||||
def _repair(self):
|
||||
@@ -501,7 +518,7 @@ class FileList(_FileList):
|
||||
# To avoid accidental trans-codings errors, first to unicode
|
||||
u_path = unicode_utils.filesys_decode(path)
|
||||
if u_path is None:
|
||||
log.warn(f"'{path}' in unexpected encoding -- skipping")
|
||||
log.warn("'%s' in unexpected encoding -- skipping" % path)
|
||||
return False
|
||||
|
||||
# Must ensure utf-8 encodability
|
||||
@@ -526,17 +543,17 @@ class FileList(_FileList):
|
||||
class manifest_maker(sdist):
|
||||
template = "MANIFEST.in"
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
self.use_defaults = True
|
||||
self.prune = True
|
||||
self.manifest_only = True
|
||||
self.force_manifest = True
|
||||
def initialize_options(self):
|
||||
self.use_defaults = 1
|
||||
self.prune = 1
|
||||
self.manifest_only = 1
|
||||
self.force_manifest = 1
|
||||
self.ignore_egg_info_dir = False
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir)
|
||||
if not os.path.exists(self.manifest):
|
||||
self.write_manifest() # it must exist so it'll get in the list
|
||||
@@ -544,7 +561,6 @@ class manifest_maker(sdist):
|
||||
if os.path.exists(self.template):
|
||||
self.read_template()
|
||||
self.add_license_files()
|
||||
self._add_referenced_files()
|
||||
self.prune_file_list()
|
||||
self.filelist.sort()
|
||||
self.filelist.remove_duplicates()
|
||||
@@ -554,7 +570,7 @@ class manifest_maker(sdist):
|
||||
path = unicode_utils.filesys_decode(path)
|
||||
return path.replace(os.sep, '/')
|
||||
|
||||
def write_manifest(self) -> None:
|
||||
def write_manifest(self):
|
||||
"""
|
||||
Write the file list in 'self.filelist' to the manifest file
|
||||
named by 'self.manifest'.
|
||||
@@ -563,10 +579,10 @@ class manifest_maker(sdist):
|
||||
|
||||
# Now _repairs should encodability, but not unicode
|
||||
files = [self._manifest_normalize(f) for f in self.filelist.files]
|
||||
msg = f"writing manifest file '{self.manifest}'"
|
||||
msg = "writing manifest file '%s'" % self.manifest
|
||||
self.execute(write_file, (self.manifest, files), msg)
|
||||
|
||||
def warn(self, msg) -> None:
|
||||
def warn(self, msg):
|
||||
if not self._should_suppress_warning(msg):
|
||||
sdist.warn(self, msg)
|
||||
|
||||
@@ -577,7 +593,7 @@ class manifest_maker(sdist):
|
||||
"""
|
||||
return re.match(r"standard file .*not found", msg)
|
||||
|
||||
def add_defaults(self) -> None:
|
||||
def add_defaults(self):
|
||||
sdist.add_defaults(self)
|
||||
self.filelist.append(self.template)
|
||||
self.filelist.append(self.manifest)
|
||||
@@ -595,19 +611,21 @@ class manifest_maker(sdist):
|
||||
ei_cmd = self.get_finalized_command('egg_info')
|
||||
self.filelist.graft(ei_cmd.egg_info)
|
||||
|
||||
def add_license_files(self) -> None:
|
||||
def add_license_files(self):
|
||||
license_files = self.distribution.metadata.license_files or []
|
||||
for lf in license_files:
|
||||
log.info("adding license file '%s'", lf)
|
||||
pass
|
||||
self.filelist.extend(license_files)
|
||||
|
||||
def _add_referenced_files(self):
|
||||
"""Add files referenced by the config (e.g. `file:` directive) to filelist"""
|
||||
referenced = getattr(self.distribution, '_referenced_files', [])
|
||||
# ^-- fallback if dist comes from distutils or is a custom class
|
||||
for rf in referenced:
|
||||
log.debug("adding file referenced by config '%s'", rf)
|
||||
self.filelist.extend(referenced)
|
||||
def prune_file_list(self):
|
||||
build = self.get_finalized_command('build')
|
||||
base_dir = self.distribution.get_fullname()
|
||||
self.filelist.prune(build.build_base)
|
||||
self.filelist.prune(base_dir)
|
||||
sep = re.escape(os.sep)
|
||||
self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep,
|
||||
is_regex=1)
|
||||
|
||||
def _safe_data_files(self, build_py):
|
||||
"""
|
||||
@@ -622,19 +640,16 @@ class manifest_maker(sdist):
|
||||
if hasattr(build_py, 'get_data_files_without_manifest'):
|
||||
return build_py.get_data_files_without_manifest()
|
||||
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"`build_py` command does not inherit from setuptools' `build_py`.",
|
||||
"""
|
||||
Custom 'build_py' does not implement 'get_data_files_without_manifest'.
|
||||
Please extend command classes from setuptools instead of distutils.
|
||||
""",
|
||||
see_url="https://peps.python.org/pep-0632/",
|
||||
# due_date not defined yet, old projects might still do it?
|
||||
warnings.warn(
|
||||
"Custom 'build_py' does not implement "
|
||||
"'get_data_files_without_manifest'.\nPlease extend command classes"
|
||||
" from setuptools instead of distutils.",
|
||||
SetuptoolsDeprecationWarning
|
||||
)
|
||||
return build_py.get_data_files()
|
||||
|
||||
|
||||
def write_file(filename, contents) -> None:
|
||||
def write_file(filename, contents):
|
||||
"""Create a file with the specified name and write 'contents' (a
|
||||
sequence of strings without line terminators) to it.
|
||||
"""
|
||||
@@ -647,7 +662,7 @@ def write_file(filename, contents) -> None:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def write_pkg_info(cmd, basename, filename) -> None:
|
||||
def write_pkg_info(cmd, basename, filename):
|
||||
log.info("writing %s", filename)
|
||||
if not cmd.dry_run:
|
||||
metadata = cmd.distribution.metadata
|
||||
@@ -666,33 +681,55 @@ def write_pkg_info(cmd, basename, filename) -> None:
|
||||
bdist_egg.write_safety_flag(cmd.egg_info, safe)
|
||||
|
||||
|
||||
def warn_depends_obsolete(cmd, basename, filename) -> None:
|
||||
"""
|
||||
Unused: left to avoid errors when updating (from source) from <= 67.8.
|
||||
Old installations have a .dist-info directory with the entry-point
|
||||
``depends.txt = setuptools.command.egg_info:warn_depends_obsolete``.
|
||||
This may trigger errors when running the first egg_info in build_meta.
|
||||
TODO: Remove this function in a version sufficiently > 68.
|
||||
"""
|
||||
def warn_depends_obsolete(cmd, basename, filename):
|
||||
if os.path.exists(filename):
|
||||
log.warn(
|
||||
"WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
|
||||
"Use the install_requires/extras_require setup() args instead."
|
||||
)
|
||||
|
||||
|
||||
# Export API used in entry_points
|
||||
write_requirements = _requirestxt.write_requirements
|
||||
write_setup_requirements = _requirestxt.write_setup_requirements
|
||||
def _write_requirements(stream, reqs):
|
||||
lines = yield_lines(reqs or ())
|
||||
|
||||
def append_cr(line):
|
||||
return line + '\n'
|
||||
lines = map(append_cr, lines)
|
||||
stream.writelines(lines)
|
||||
|
||||
|
||||
def write_toplevel_names(cmd, basename, filename) -> None:
|
||||
pkgs = dict.fromkeys([
|
||||
k.split('.', 1)[0] for k in cmd.distribution.iter_distribution_names()
|
||||
])
|
||||
def write_requirements(cmd, basename, filename):
|
||||
dist = cmd.distribution
|
||||
data = io.StringIO()
|
||||
_write_requirements(data, dist.install_requires)
|
||||
extras_require = dist.extras_require or {}
|
||||
for extra in sorted(extras_require):
|
||||
data.write('\n[{extra}]\n'.format(**vars()))
|
||||
_write_requirements(data, extras_require[extra])
|
||||
cmd.write_or_delete_file("requirements", filename, data.getvalue())
|
||||
|
||||
|
||||
def write_setup_requirements(cmd, basename, filename):
|
||||
data = io.StringIO()
|
||||
_write_requirements(data, cmd.distribution.setup_requires)
|
||||
cmd.write_or_delete_file("setup-requirements", filename, data.getvalue())
|
||||
|
||||
|
||||
def write_toplevel_names(cmd, basename, filename):
|
||||
pkgs = dict.fromkeys(
|
||||
[
|
||||
k.split('.', 1)[0]
|
||||
for k in cmd.distribution.iter_distribution_names()
|
||||
]
|
||||
)
|
||||
cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n')
|
||||
|
||||
|
||||
def overwrite_arg(cmd, basename, filename) -> None:
|
||||
def overwrite_arg(cmd, basename, filename):
|
||||
write_arg(cmd, basename, filename, True)
|
||||
|
||||
|
||||
def write_arg(cmd, basename, filename, force: bool = False) -> None:
|
||||
def write_arg(cmd, basename, filename, force=False):
|
||||
argname = os.path.splitext(basename)[0]
|
||||
value = getattr(cmd.distribution, argname, None)
|
||||
if value is not None:
|
||||
@@ -700,20 +737,26 @@ def write_arg(cmd, basename, filename, force: bool = False) -> None:
|
||||
cmd.write_or_delete_file(argname, filename, value, force)
|
||||
|
||||
|
||||
def write_entries(cmd, basename, filename) -> None:
|
||||
def write_entries(cmd, basename, filename):
|
||||
eps = _entry_points.load(cmd.distribution.entry_points)
|
||||
defn = _entry_points.render(eps)
|
||||
cmd.write_or_delete_file('entry points', filename, defn, True)
|
||||
|
||||
|
||||
def _egg_basename(egg_name, egg_version, py_version=None, platform=None):
|
||||
"""Compute filename of the output egg. Private API."""
|
||||
name = _normalization.filename_component(egg_name)
|
||||
version = _normalization.filename_component(egg_version)
|
||||
egg = f"{name}-{version}-py{py_version or PY_MAJOR}"
|
||||
if platform:
|
||||
egg += f"-{platform}"
|
||||
return egg
|
||||
def get_pkg_info_revision():
|
||||
"""
|
||||
Get a -r### off of PKG-INFO Version in case this is an sdist of
|
||||
a subversion revision.
|
||||
"""
|
||||
warnings.warn(
|
||||
"get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)
|
||||
if os.path.exists('PKG-INFO'):
|
||||
with io.open('PKG-INFO') as f:
|
||||
for line in f:
|
||||
match = re.match(r"Version:.*-r(\d+)\s*$", line)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return 0
|
||||
|
||||
|
||||
class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
|
||||
|
||||
@@ -1,89 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
from distutils.errors import DistutilsArgError
|
||||
import inspect
|
||||
import glob
|
||||
import warnings
|
||||
import platform
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
||||
import distutils.command.install as orig
|
||||
|
||||
import setuptools
|
||||
|
||||
from ..dist import Distribution
|
||||
from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
|
||||
from .bdist_egg import bdist_egg as bdist_egg_cls
|
||||
|
||||
import distutils.command.install as orig
|
||||
from distutils.errors import DistutilsArgError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This is only used for a type-cast, don't import at runtime or it'll cause deprecation warnings
|
||||
from .easy_install import easy_install as easy_install_cls
|
||||
else:
|
||||
easy_install_cls = None
|
||||
|
||||
|
||||
def __getattr__(name: str): # pragma: no cover
|
||||
if name == "_install":
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"`setuptools.command._install` was an internal implementation detail "
|
||||
+ "that was left in for numpy<1.9 support.",
|
||||
due_date=(2025, 5, 2), # Originally added on 2024-11-01
|
||||
)
|
||||
return orig.install
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
# Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for
|
||||
# now. See https://github.com/pypa/setuptools/issues/199/
|
||||
_install = orig.install
|
||||
|
||||
|
||||
class install(orig.install):
|
||||
"""Use easy_install to install the package, w/dependencies"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
user_options = orig.install.user_options + [
|
||||
('old-and-unmanageable', None, "Try not to use this!"),
|
||||
(
|
||||
'single-version-externally-managed',
|
||||
None,
|
||||
"used by system package builders to create 'flat' eggs",
|
||||
),
|
||||
('single-version-externally-managed', None,
|
||||
"used by system package builders to create 'flat' eggs"),
|
||||
]
|
||||
boolean_options = orig.install.boolean_options + [
|
||||
'old-and-unmanageable',
|
||||
'single-version-externally-managed',
|
||||
'old-and-unmanageable', 'single-version-externally-managed',
|
||||
]
|
||||
# Type the same as distutils.command.install.install.sub_commands
|
||||
# Must keep the second tuple item potentially None due to invariance
|
||||
new_commands: ClassVar[list[tuple[str, Callable[[Any], bool] | None]]] = [
|
||||
new_commands = [
|
||||
('install_egg_info', lambda self: True),
|
||||
('install_scripts', lambda self: True),
|
||||
]
|
||||
_nc = dict(new_commands)
|
||||
|
||||
def initialize_options(self):
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"setup.py install is deprecated.",
|
||||
"""
|
||||
Please avoid running ``setup.py`` directly.
|
||||
Instead, use pypa/build, pypa/installer or other
|
||||
standards-based tools.
|
||||
""",
|
||||
see_url="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html",
|
||||
# TODO: Document how to bootstrap setuptools without install
|
||||
# (e.g. by unzipping the wheel file)
|
||||
# and then add a due_date to this warning.
|
||||
|
||||
warnings.warn(
|
||||
"setup.py install is deprecated. "
|
||||
"Use build and pip and other standards-based tools.",
|
||||
setuptools.SetuptoolsDeprecationWarning,
|
||||
)
|
||||
|
||||
super().initialize_options()
|
||||
orig.install.initialize_options(self)
|
||||
self.old_and_unmanageable = None
|
||||
self.single_version_externally_managed = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
super().finalize_options()
|
||||
def finalize_options(self):
|
||||
orig.install.finalize_options(self)
|
||||
if self.root:
|
||||
self.single_version_externally_managed = True
|
||||
elif self.single_version_externally_managed:
|
||||
if not self.root and not self.record:
|
||||
raise DistutilsArgError(
|
||||
"You must specify --record or --root when building system packages"
|
||||
"You must specify --record or --root when building system"
|
||||
" packages"
|
||||
)
|
||||
|
||||
def handle_extra_path(self):
|
||||
@@ -95,21 +61,18 @@ class install(orig.install):
|
||||
# command without --root or --single-version-externally-managed
|
||||
self.path_file = None
|
||||
self.extra_dirs = ''
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
# Explicit request for old-style install? Just do it
|
||||
if self.old_and_unmanageable or self.single_version_externally_managed:
|
||||
return super().run()
|
||||
return orig.install.run(self)
|
||||
|
||||
if not self._called_from_setup(inspect.currentframe()):
|
||||
# Run in backward-compatibility mode to support bdist_* commands.
|
||||
super().run()
|
||||
orig.install.run(self)
|
||||
else:
|
||||
self.do_egg_install()
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _called_from_setup(run_frame):
|
||||
"""
|
||||
@@ -123,15 +86,15 @@ class install(orig.install):
|
||||
"""
|
||||
if run_frame is None:
|
||||
msg = "Call stack not available. bdist_* commands may fail."
|
||||
SetuptoolsWarning.emit(msg)
|
||||
warnings.warn(msg)
|
||||
if platform.python_implementation() == 'IronPython':
|
||||
msg = "For best results, pass -X:Frames to enable call stack."
|
||||
SetuptoolsWarning.emit(msg)
|
||||
warnings.warn(msg)
|
||||
return True
|
||||
|
||||
frames = inspect.getouterframes(run_frame)
|
||||
for frame in frames[2:4]:
|
||||
(caller,) = frame[:1]
|
||||
caller, = frame[:1]
|
||||
info = inspect.getframeinfo(caller)
|
||||
caller_module = caller.f_globals.get('__name__', '')
|
||||
|
||||
@@ -139,24 +102,17 @@ class install(orig.install):
|
||||
# Starting from v61.0.0 setuptools overwrites dist.run_command
|
||||
continue
|
||||
|
||||
return caller_module == 'distutils.dist' and info.function == 'run_commands'
|
||||
return (
|
||||
caller_module == 'distutils.dist'
|
||||
and info.function == 'run_commands'
|
||||
)
|
||||
|
||||
return False
|
||||
def do_egg_install(self):
|
||||
|
||||
def do_egg_install(self) -> None:
|
||||
easy_install = self.distribution.get_command_class('easy_install')
|
||||
|
||||
cmd = cast(
|
||||
# We'd want to cast easy_install as type[easy_install_cls] but a bug in
|
||||
# mypy makes it think easy_install() returns a Command on Python 3.12+
|
||||
# https://github.com/python/mypy/issues/18088
|
||||
easy_install_cls,
|
||||
easy_install( # type: ignore[call-arg]
|
||||
self.distribution,
|
||||
args="x",
|
||||
root=self.root,
|
||||
record=self.record,
|
||||
),
|
||||
cmd = easy_install(
|
||||
self.distribution, args="x", root=self.root, record=self.record,
|
||||
)
|
||||
cmd.ensure_finalized() # finalize before bdist_egg munges install cmd
|
||||
cmd.always_copy_from = '.' # make sure local-dir eggs get installed
|
||||
@@ -165,8 +121,7 @@ class install(orig.install):
|
||||
cmd.package_index.scan(glob.glob('*.egg'))
|
||||
|
||||
self.run_command('bdist_egg')
|
||||
bdist_egg = cast(bdist_egg_cls, self.distribution.get_command_obj('bdist_egg'))
|
||||
args = [bdist_egg.egg_output]
|
||||
args = [self.distribution.get_command_obj('bdist_egg').egg_output]
|
||||
|
||||
if setuptools.bootstrap_install_from:
|
||||
# Bootstrap self-installation of setuptools
|
||||
@@ -178,6 +133,7 @@ class install(orig.install):
|
||||
|
||||
|
||||
# XXX Python 3.1 doesn't see _nc if this is inside the class
|
||||
install.sub_commands = [
|
||||
cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc
|
||||
] + install.new_commands
|
||||
install.sub_commands = (
|
||||
[cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc] +
|
||||
install.new_commands
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from distutils import log, dir_util
|
||||
import os
|
||||
|
||||
from setuptools import Command, namespaces
|
||||
from setuptools import Command
|
||||
from setuptools import namespaces
|
||||
from setuptools.archive_util import unpack_archive
|
||||
|
||||
from .._path import ensure_directory
|
||||
|
||||
from distutils import dir_util, log
|
||||
import pkg_resources
|
||||
|
||||
|
||||
class install_egg_info(namespaces.Installer, Command):
|
||||
@@ -20,15 +20,18 @@ class install_egg_info(namespaces.Installer, Command):
|
||||
def initialize_options(self):
|
||||
self.install_dir = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))
|
||||
def finalize_options(self):
|
||||
self.set_undefined_options('install_lib',
|
||||
('install_dir', 'install_dir'))
|
||||
ei_cmd = self.get_finalized_command("egg_info")
|
||||
basename = f"{ei_cmd._get_egg_basename()}.egg-info"
|
||||
basename = pkg_resources.Distribution(
|
||||
None, None, ei_cmd.egg_name, ei_cmd.egg_version
|
||||
).egg_name() + '.egg-info'
|
||||
self.source = ei_cmd.egg_info
|
||||
self.target = os.path.join(self.install_dir, basename)
|
||||
self.outputs: list[str] = []
|
||||
self.outputs = []
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.run_command('egg_info')
|
||||
if os.path.isdir(self.target) and not os.path.islink(self.target):
|
||||
dir_util.remove_tree(self.target, dry_run=self.dry_run)
|
||||
@@ -36,13 +39,15 @@ class install_egg_info(namespaces.Installer, Command):
|
||||
self.execute(os.unlink, (self.target,), "Removing " + self.target)
|
||||
if not self.dry_run:
|
||||
ensure_directory(self.target)
|
||||
self.execute(self.copytree, (), f"Copying {self.source} to {self.target}")
|
||||
self.execute(
|
||||
self.copytree, (), "Copying %s to %s" % (self.source, self.target)
|
||||
)
|
||||
self.install_namespaces()
|
||||
|
||||
def get_outputs(self):
|
||||
return self.outputs
|
||||
|
||||
def copytree(self) -> None:
|
||||
def copytree(self):
|
||||
# Copy the .egg-info tree to site-packages
|
||||
def skimmer(src, dst):
|
||||
# filter out source-control directories; note that 'src' is always
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from itertools import product, starmap
|
||||
|
||||
from .._path import StrPath
|
||||
from ..dist import Distribution
|
||||
|
||||
import distutils.command.install_lib as orig
|
||||
|
||||
|
||||
class install_lib(orig.install_lib):
|
||||
"""Don't add compiled flags to filenames of non-Python files"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.build()
|
||||
outfiles = self.install()
|
||||
if outfiles is not None:
|
||||
@@ -52,7 +44,7 @@ class install_lib(orig.install_lib):
|
||||
"""
|
||||
while pkg_name:
|
||||
yield pkg_name
|
||||
pkg_name, _sep, _child = pkg_name.rpartition('.')
|
||||
pkg_name, sep, child = pkg_name.rpartition('.')
|
||||
|
||||
def _get_SVEM_NSPs(self):
|
||||
"""
|
||||
@@ -85,25 +77,18 @@ class install_lib(orig.install_lib):
|
||||
if not hasattr(sys, 'implementation'):
|
||||
return
|
||||
|
||||
base = os.path.join('__pycache__', '__init__.' + sys.implementation.cache_tag)
|
||||
base = os.path.join(
|
||||
'__pycache__', '__init__.' + sys.implementation.cache_tag)
|
||||
yield base + '.pyc'
|
||||
yield base + '.pyo'
|
||||
yield base + '.opt-1.pyc'
|
||||
yield base + '.opt-2.pyc'
|
||||
|
||||
def copy_tree(
|
||||
self,
|
||||
infile: StrPath,
|
||||
outfile: str,
|
||||
# override: Using actual booleans
|
||||
preserve_mode: bool = True, # type: ignore[override]
|
||||
preserve_times: bool = True, # type: ignore[override]
|
||||
preserve_symlinks: bool = False, # type: ignore[override]
|
||||
level: object = 1,
|
||||
) -> list[str]:
|
||||
assert preserve_mode
|
||||
assert preserve_times
|
||||
assert not preserve_symlinks
|
||||
self, infile, outfile,
|
||||
preserve_mode=1, preserve_times=1, preserve_symlinks=0, level=1
|
||||
):
|
||||
assert preserve_mode and preserve_times and not preserve_symlinks
|
||||
exclude = self.get_exclusions()
|
||||
|
||||
if not exclude:
|
||||
@@ -112,14 +97,14 @@ class install_lib(orig.install_lib):
|
||||
# Exclude namespace package __init__.py* files from the output
|
||||
|
||||
from setuptools.archive_util import unpack_directory
|
||||
|
||||
from distutils import log
|
||||
|
||||
outfiles: list[str] = []
|
||||
outfiles = []
|
||||
|
||||
def pf(src: str, dst: str):
|
||||
def pf(src, dst):
|
||||
if dst in exclude:
|
||||
log.warn("Skipping installation of %s (namespace package)", dst)
|
||||
log.warn("Skipping installation of %s (namespace package)",
|
||||
dst)
|
||||
return False
|
||||
|
||||
log.info("copying %s -> %s", src, os.path.dirname(dst))
|
||||
|
||||
@@ -1,51 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from distutils import log
|
||||
import distutils.command.install_scripts as orig
|
||||
from distutils.errors import DistutilsModuleError
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pkg_resources import Distribution, PathMetadata
|
||||
from .._path import ensure_directory
|
||||
from ..dist import Distribution
|
||||
|
||||
import distutils.command.install_scripts as orig
|
||||
from distutils import log
|
||||
|
||||
|
||||
class install_scripts(orig.install_scripts):
|
||||
"""Do normal script install, plus any egg_info wrapper scripts"""
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
def initialize_options(self):
|
||||
orig.install_scripts.initialize_options(self)
|
||||
self.no_ep = False
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
import setuptools.command.easy_install as ei
|
||||
|
||||
self.run_command("egg_info")
|
||||
if self.distribution.scripts:
|
||||
orig.install_scripts.run(self) # run first to set up self.outfiles
|
||||
else:
|
||||
self.outfiles: list[str] = []
|
||||
self.outfiles = []
|
||||
if self.no_ep:
|
||||
# don't install entry point scripts into .egg file!
|
||||
return
|
||||
self._install_ep_scripts()
|
||||
|
||||
def _install_ep_scripts(self):
|
||||
# Delay import side-effects
|
||||
from pkg_resources import Distribution, PathMetadata
|
||||
|
||||
from . import easy_install as ei
|
||||
|
||||
ei_cmd = self.get_finalized_command("egg_info")
|
||||
dist = Distribution(
|
||||
ei_cmd.egg_base,
|
||||
PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
|
||||
ei_cmd.egg_name,
|
||||
ei_cmd.egg_version,
|
||||
ei_cmd.egg_base, PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
|
||||
ei_cmd.egg_name, ei_cmd.egg_version,
|
||||
)
|
||||
bs_cmd = self.get_finalized_command('build_scripts')
|
||||
exec_param = getattr(bs_cmd, 'executable', None)
|
||||
try:
|
||||
bw_cmd = self.get_finalized_command("bdist_wininst")
|
||||
is_wininst = getattr(bw_cmd, '_is_running', False)
|
||||
except (ImportError, DistutilsModuleError):
|
||||
is_wininst = False
|
||||
writer = ei.ScriptWriter
|
||||
if is_wininst:
|
||||
exec_param = "python.exe"
|
||||
writer = ei.WindowsScriptWriter
|
||||
if exec_param == sys.executable:
|
||||
# In case the path to the Python executable contains a space, wrap
|
||||
# it so it's not split up.
|
||||
@@ -56,7 +53,7 @@ class install_scripts(orig.install_scripts):
|
||||
for args in writer.get_args(dist, cmd.as_header()):
|
||||
self.write_script(*args)
|
||||
|
||||
def write_script(self, script_name, contents, mode: str = "t", *ignored) -> None:
|
||||
def write_script(self, script_name, contents, mode="t", *ignored):
|
||||
"""Write an executable file to the scripts directory"""
|
||||
from setuptools.command.easy_install import chmod, current_umask
|
||||
|
||||
@@ -64,10 +61,10 @@ class install_scripts(orig.install_scripts):
|
||||
target = os.path.join(self.install_dir, script_name)
|
||||
self.outfiles.append(target)
|
||||
|
||||
encoding = None if "b" in mode else "utf-8"
|
||||
mask = current_umask()
|
||||
if not self.dry_run:
|
||||
ensure_directory(target)
|
||||
with open(target, "w" + mode, encoding=encoding) as f:
|
||||
f.write(contents)
|
||||
f = open(target, "w" + mode)
|
||||
f.write(contents)
|
||||
f.close()
|
||||
chmod(target, 0o777 - mask)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import ClassVar
|
||||
|
||||
from .. import Command, _shutil
|
||||
|
||||
from distutils.util import convert_path
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsOptionError
|
||||
from distutils.util import convert_path
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from setuptools import Command
|
||||
|
||||
|
||||
class rotate(Command):
|
||||
@@ -20,14 +17,14 @@ class rotate(Command):
|
||||
('keep=', 'k', "number of matching distributions to keep"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = []
|
||||
boolean_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
self.match = None
|
||||
self.dist_dir = None
|
||||
self.keep = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
if self.match is None:
|
||||
raise DistutilsOptionError(
|
||||
"Must specify one or more (comma-separated) match patterns "
|
||||
@@ -40,10 +37,12 @@ class rotate(Command):
|
||||
except ValueError as e:
|
||||
raise DistutilsOptionError("--keep must be an integer") from e
|
||||
if isinstance(self.match, str):
|
||||
self.match = [convert_path(p.strip()) for p in self.match.split(',')]
|
||||
self.match = [
|
||||
convert_path(p.strip()) for p in self.match.split(',')
|
||||
]
|
||||
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.run_command("egg_info")
|
||||
from glob import glob
|
||||
|
||||
@@ -55,11 +54,11 @@ class rotate(Command):
|
||||
files.reverse()
|
||||
|
||||
log.info("%d file(s) matching %s", len(files), pattern)
|
||||
files = files[self.keep :]
|
||||
for t, f in files:
|
||||
files = files[self.keep:]
|
||||
for (t, f) in files:
|
||||
log.info("Deleting %s", f)
|
||||
if not self.dry_run:
|
||||
if os.path.isdir(f):
|
||||
_shutil.rmtree(f)
|
||||
shutil.rmtree(f)
|
||||
else:
|
||||
os.unlink(f)
|
||||
|
||||
@@ -6,11 +6,12 @@ class saveopts(option_base):
|
||||
|
||||
description = "save supplied options to setup.cfg or other config file"
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
dist = self.distribution
|
||||
settings: dict[str, dict[str, str]] = {}
|
||||
settings = {}
|
||||
|
||||
for cmd in dist.command_options:
|
||||
|
||||
if cmd == 'saveopts':
|
||||
continue # don't save our own options!
|
||||
|
||||
|
||||
@@ -1,61 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from distutils import log
|
||||
import distutils.command.sdist as orig
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import io
|
||||
import contextlib
|
||||
from itertools import chain
|
||||
from typing import ClassVar
|
||||
|
||||
from .py36compat import sdist_add_defaults
|
||||
|
||||
from .._importlib import metadata
|
||||
from ..dist import Distribution
|
||||
from .build import _ORIGINAL_SUBCOMMANDS
|
||||
|
||||
import distutils.command.sdist as orig
|
||||
from distutils import log
|
||||
|
||||
_default_revctrl = list
|
||||
|
||||
|
||||
def walk_revctrl(dirname=''):
|
||||
"""Find all files under revision control"""
|
||||
for ep in metadata.entry_points(group='setuptools.file_finders'):
|
||||
yield from ep.load()(dirname)
|
||||
for item in ep.load()(dirname):
|
||||
yield item
|
||||
|
||||
|
||||
class sdist(orig.sdist):
|
||||
class sdist(sdist_add_defaults, orig.sdist):
|
||||
"""Smart sdist that finds anything supported by revision control"""
|
||||
|
||||
user_options = [
|
||||
('formats=', None, "formats for source distribution (comma-separated list)"),
|
||||
(
|
||||
'keep-temp',
|
||||
'k',
|
||||
"keep the distribution tree around after creating " + "archive file(s)",
|
||||
),
|
||||
(
|
||||
'dist-dir=',
|
||||
'd',
|
||||
"directory to put the source distribution archive(s) in [default: dist]",
|
||||
),
|
||||
(
|
||||
'owner=',
|
||||
'u',
|
||||
"Owner name used when creating a tar file [default: current user]",
|
||||
),
|
||||
(
|
||||
'group=',
|
||||
'g',
|
||||
"Group name used when creating a tar file [default: current group]",
|
||||
),
|
||||
('formats=', None,
|
||||
"formats for source distribution (comma-separated list)"),
|
||||
('keep-temp', 'k',
|
||||
"keep the distribution tree around after creating " +
|
||||
"archive file(s)"),
|
||||
('dist-dir=', 'd',
|
||||
"directory to put the source distribution archive(s) in "
|
||||
"[default: dist]"),
|
||||
('owner=', 'u',
|
||||
"Owner name used when creating a tar file [default: current user]"),
|
||||
('group=', 'g',
|
||||
"Group name used when creating a tar file [default: current group]"),
|
||||
]
|
||||
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
negative_opt: ClassVar[dict[str, str]] = {}
|
||||
negative_opt = {}
|
||||
|
||||
README_EXTENSIONS = ['', '.rst', '.txt', '.md']
|
||||
READMES = tuple(f'README{ext}' for ext in README_EXTENSIONS)
|
||||
READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS)
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
self.run_command('egg_info')
|
||||
ei_cmd = self.get_finalized_command('egg_info')
|
||||
self.filelist = ei_cmd.filelist
|
||||
@@ -74,10 +63,18 @@ class sdist(orig.sdist):
|
||||
if data not in dist_files:
|
||||
dist_files.append(data)
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
def initialize_options(self):
|
||||
orig.sdist.initialize_options(self)
|
||||
|
||||
def make_distribution(self) -> None:
|
||||
self._default_to_gztar()
|
||||
|
||||
def _default_to_gztar(self):
|
||||
# only needed on Python prior to 3.6.
|
||||
if sys.version_info >= (3, 6, 0, 'beta', 1):
|
||||
return
|
||||
self.formats = ['gztar']
|
||||
|
||||
def make_distribution(self):
|
||||
"""
|
||||
Workaround for #516
|
||||
"""
|
||||
@@ -103,9 +100,9 @@ class sdist(orig.sdist):
|
||||
yield
|
||||
finally:
|
||||
if orig_val is not NoValue:
|
||||
os.link = orig_val
|
||||
setattr(os, 'link', orig_val)
|
||||
|
||||
def add_defaults(self) -> None:
|
||||
def add_defaults(self):
|
||||
super().add_defaults()
|
||||
self._add_defaults_build_sub_commands()
|
||||
|
||||
@@ -158,22 +155,17 @@ class sdist(orig.sdist):
|
||||
except TypeError:
|
||||
log.warn("data_files contains unexpected objects")
|
||||
|
||||
def prune_file_list(self) -> None:
|
||||
super().prune_file_list()
|
||||
# Prevent accidental inclusion of test-related cache dirs at the project root
|
||||
sep = re.escape(os.sep)
|
||||
self.filelist.exclude_pattern(r"^(\.tox|\.nox|\.venv)" + sep, is_regex=True)
|
||||
|
||||
def check_readme(self) -> None:
|
||||
def check_readme(self):
|
||||
for f in self.READMES:
|
||||
if os.path.exists(f):
|
||||
return
|
||||
else:
|
||||
self.warn(
|
||||
"standard file not found: should have one of " + ', '.join(self.READMES)
|
||||
"standard file not found: should have one of " +
|
||||
', '.join(self.READMES)
|
||||
)
|
||||
|
||||
def make_release_tree(self, base_dir, files) -> None:
|
||||
def make_release_tree(self, base_dir, files):
|
||||
orig.sdist.make_release_tree(self, base_dir, files)
|
||||
|
||||
# Save any egg_info command line options used to create this sdist
|
||||
@@ -191,9 +183,10 @@ class sdist(orig.sdist):
|
||||
if not os.path.isfile(self.manifest):
|
||||
return False
|
||||
|
||||
with open(self.manifest, 'rb') as fp:
|
||||
with io.open(self.manifest, 'rb') as fp:
|
||||
first_line = fp.readline()
|
||||
return first_line != b'# file GENERATED by distutils, do NOT edit\n'
|
||||
return (first_line !=
|
||||
'# file GENERATED by distutils, do NOT edit\n'.encode())
|
||||
|
||||
def read_manifest(self):
|
||||
"""Read the manifest file (named by 'self.manifest') and use it to
|
||||
@@ -202,12 +195,12 @@ class sdist(orig.sdist):
|
||||
"""
|
||||
log.info("reading manifest file '%s'", self.manifest)
|
||||
manifest = open(self.manifest, 'rb')
|
||||
for bytes_line in manifest:
|
||||
for line in manifest:
|
||||
# The manifest must contain UTF-8. See #303.
|
||||
try:
|
||||
line = bytes_line.decode('UTF-8')
|
||||
line = line.decode('UTF-8')
|
||||
except UnicodeDecodeError:
|
||||
log.warn(f"{line!r} not UTF-8 decodable -- skipping")
|
||||
log.warn("%r not UTF-8 decodable -- skipping" % line)
|
||||
continue
|
||||
# ignore comments and blank lines
|
||||
line = line.strip()
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import configparser
|
||||
import os
|
||||
|
||||
from .. import Command
|
||||
from ..unicode_utils import _cfg_read_utf8_with_fallback
|
||||
|
||||
import distutils
|
||||
from distutils.util import convert_path
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsOptionError
|
||||
from distutils.util import convert_path
|
||||
import distutils
|
||||
import os
|
||||
import configparser
|
||||
|
||||
from setuptools import Command
|
||||
|
||||
__all__ = ['config_file', 'edit_config', 'option_base', 'setopt']
|
||||
|
||||
@@ -20,11 +18,15 @@ def config_file(kind="local"):
|
||||
if kind == 'local':
|
||||
return 'setup.cfg'
|
||||
if kind == 'global':
|
||||
return os.path.join(os.path.dirname(distutils.__file__), 'distutils.cfg')
|
||||
return os.path.join(
|
||||
os.path.dirname(distutils.__file__), 'distutils.cfg'
|
||||
)
|
||||
if kind == 'user':
|
||||
dot = os.name == 'posix' and '.' or ''
|
||||
return os.path.expanduser(convert_path(f"~/{dot}pydistutils.cfg"))
|
||||
raise ValueError("config_file() type must be 'local', 'global', or 'user'", kind)
|
||||
return os.path.expanduser(convert_path("~/%spydistutils.cfg" % dot))
|
||||
raise ValueError(
|
||||
"config_file() type must be 'local', 'global', or 'user'", kind
|
||||
)
|
||||
|
||||
|
||||
def edit_config(filename, settings, dry_run=False):
|
||||
@@ -37,9 +39,8 @@ def edit_config(filename, settings, dry_run=False):
|
||||
"""
|
||||
log.debug("Reading configuration from %s", filename)
|
||||
opts = configparser.RawConfigParser()
|
||||
opts.optionxform = lambda optionstr: optionstr # type: ignore[method-assign] # overriding method
|
||||
_cfg_read_utf8_with_fallback(opts, filename)
|
||||
|
||||
opts.optionxform = lambda x: x
|
||||
opts.read([filename])
|
||||
for section, options in settings.items():
|
||||
if options is None:
|
||||
log.info("Deleting section [%s] from %s", section, filename)
|
||||
@@ -50,22 +51,25 @@ def edit_config(filename, settings, dry_run=False):
|
||||
opts.add_section(section)
|
||||
for option, value in options.items():
|
||||
if value is None:
|
||||
log.debug("Deleting %s.%s from %s", section, option, filename)
|
||||
log.debug(
|
||||
"Deleting %s.%s from %s",
|
||||
section, option, filename
|
||||
)
|
||||
opts.remove_option(section, option)
|
||||
if not opts.options(section):
|
||||
log.info(
|
||||
"Deleting empty [%s] section from %s", section, filename
|
||||
)
|
||||
log.info("Deleting empty [%s] section from %s",
|
||||
section, filename)
|
||||
opts.remove_section(section)
|
||||
else:
|
||||
log.debug(
|
||||
"Setting %s.%s to %r in %s", section, option, value, filename
|
||||
"Setting %s.%s to %r in %s",
|
||||
section, option, value, filename
|
||||
)
|
||||
opts.set(section, option, value)
|
||||
|
||||
log.info("Writing %s", filename)
|
||||
if not dry_run:
|
||||
with open(filename, 'w', encoding="utf-8") as f:
|
||||
with open(filename, 'w') as f:
|
||||
opts.write(f)
|
||||
|
||||
|
||||
@@ -73,14 +77,16 @@ class option_base(Command):
|
||||
"""Abstract base class for commands that mess with config files"""
|
||||
|
||||
user_options = [
|
||||
('global-config', 'g', "save options to the site-wide distutils.cfg file"),
|
||||
('user-config', 'u', "save options to the current user's pydistutils.cfg file"),
|
||||
('filename=', 'f', "configuration file to use (default=setup.cfg)"),
|
||||
('global-config', 'g',
|
||||
"save options to the site-wide distutils.cfg file"),
|
||||
('user-config', 'u',
|
||||
"save options to the current user's pydistutils.cfg file"),
|
||||
('filename=', 'f',
|
||||
"configuration file to use (default=setup.cfg)"),
|
||||
]
|
||||
|
||||
boolean_options = [
|
||||
'global-config',
|
||||
'user-config',
|
||||
'global-config', 'user-config',
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
@@ -100,9 +106,10 @@ class option_base(Command):
|
||||
filenames.append(config_file('local'))
|
||||
if len(filenames) > 1:
|
||||
raise DistutilsOptionError(
|
||||
"Must specify only one configuration file option", filenames
|
||||
"Must specify only one configuration file option",
|
||||
filenames
|
||||
)
|
||||
(self.filename,) = filenames
|
||||
self.filename, = filenames
|
||||
|
||||
|
||||
class setopt(option_base):
|
||||
@@ -126,16 +133,17 @@ class setopt(option_base):
|
||||
self.set_value = None
|
||||
self.remove = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
def finalize_options(self):
|
||||
option_base.finalize_options(self)
|
||||
if self.command is None or self.option is None:
|
||||
raise DistutilsOptionError("Must specify --command *and* --option")
|
||||
if self.set_value is None and not self.remove:
|
||||
raise DistutilsOptionError("Must specify --set-value or --remove")
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
edit_config(
|
||||
self.filename,
|
||||
{self.command: {self.option.replace('-', '_'): self.set_value}},
|
||||
self.dry_run,
|
||||
self.filename, {
|
||||
self.command: {self.option.replace('-', '_'): self.set_value}
|
||||
},
|
||||
self.dry_run
|
||||
)
|
||||
|
||||
@@ -1,29 +1,82 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import operator
|
||||
import sys
|
||||
import contextlib
|
||||
import itertools
|
||||
import unittest
|
||||
from distutils.errors import DistutilsError, DistutilsOptionError
|
||||
from distutils import log
|
||||
from unittest import TestLoader
|
||||
|
||||
from pkg_resources import (
|
||||
resource_listdir,
|
||||
resource_exists,
|
||||
normalize_path,
|
||||
working_set,
|
||||
evaluate_marker,
|
||||
add_activation_listener,
|
||||
require,
|
||||
)
|
||||
from .._importlib import metadata
|
||||
from setuptools import Command
|
||||
from setuptools.warnings import SetuptoolsDeprecationWarning
|
||||
from setuptools.extern.more_itertools import unique_everseen
|
||||
from setuptools.extern.jaraco.functools import pass_none
|
||||
|
||||
|
||||
# Would restrict to Literal["test"], but mypy doesn't support it: https://github.com/python/mypy/issues/8203
|
||||
def __getattr__(name: str) -> type[_test]:
|
||||
if name == 'test':
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"The test command is disabled and references to it are deprecated.",
|
||||
"Please remove any references to `setuptools.command.test` in all "
|
||||
"supported versions of the affected package.",
|
||||
due_date=(2024, 11, 15),
|
||||
stacklevel=2,
|
||||
)
|
||||
return _test
|
||||
raise AttributeError(name)
|
||||
class ScanningLoader(TestLoader):
|
||||
def __init__(self):
|
||||
TestLoader.__init__(self)
|
||||
self._visited = set()
|
||||
|
||||
def loadTestsFromModule(self, module, pattern=None):
|
||||
"""Return a suite of all tests cases contained in the given module
|
||||
|
||||
If the module is a package, load tests from all the modules in it.
|
||||
If the module has an ``additional_tests`` function, call it and add
|
||||
the return value to the tests.
|
||||
"""
|
||||
if module in self._visited:
|
||||
return None
|
||||
self._visited.add(module)
|
||||
|
||||
tests = []
|
||||
tests.append(TestLoader.loadTestsFromModule(self, module))
|
||||
|
||||
if hasattr(module, "additional_tests"):
|
||||
tests.append(module.additional_tests())
|
||||
|
||||
if hasattr(module, '__path__'):
|
||||
for file in resource_listdir(module.__name__, ''):
|
||||
if file.endswith('.py') and file != '__init__.py':
|
||||
submodule = module.__name__ + '.' + file[:-3]
|
||||
else:
|
||||
if resource_exists(module.__name__, file + '/__init__.py'):
|
||||
submodule = module.__name__ + '.' + file
|
||||
else:
|
||||
continue
|
||||
tests.append(self.loadTestsFromName(submodule))
|
||||
|
||||
if len(tests) != 1:
|
||||
return self.suiteClass(tests)
|
||||
else:
|
||||
return tests[0] # don't create a nested suite for only one return
|
||||
|
||||
|
||||
class _test(Command):
|
||||
"""
|
||||
Stub to warn when test command is referenced or used.
|
||||
"""
|
||||
# adapted from jaraco.classes.properties:NonDataProperty
|
||||
class NonDataProperty:
|
||||
def __init__(self, fget):
|
||||
self.fget = fget
|
||||
|
||||
description = "stub for old test command (do not use)"
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
return self
|
||||
return self.fget(obj)
|
||||
|
||||
|
||||
class test(Command):
|
||||
"""Command to run unit tests after in-place build"""
|
||||
|
||||
description = "run unit tests after in-place build (deprecated)"
|
||||
|
||||
user_options = [
|
||||
('test-module=', 'm', "Run 'test_suite' in specified module"),
|
||||
@@ -36,10 +89,163 @@ class _test(Command):
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
self.test_suite = None
|
||||
self.test_module = None
|
||||
self.test_loader = None
|
||||
self.test_runner = None
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
if self.test_suite and self.test_module:
|
||||
msg = "You may specify a module or a suite, but not both"
|
||||
raise DistutilsOptionError(msg)
|
||||
|
||||
if self.test_suite is None:
|
||||
if self.test_module is None:
|
||||
self.test_suite = self.distribution.test_suite
|
||||
else:
|
||||
self.test_suite = self.test_module + ".test_suite"
|
||||
|
||||
if self.test_loader is None:
|
||||
self.test_loader = getattr(self.distribution, 'test_loader', None)
|
||||
if self.test_loader is None:
|
||||
self.test_loader = "setuptools.command.test:ScanningLoader"
|
||||
if self.test_runner is None:
|
||||
self.test_runner = getattr(self.distribution, 'test_runner', None)
|
||||
|
||||
@NonDataProperty
|
||||
def test_args(self):
|
||||
return list(self._test_args())
|
||||
|
||||
def _test_args(self):
|
||||
if not self.test_suite:
|
||||
yield 'discover'
|
||||
if self.verbose:
|
||||
yield '--verbose'
|
||||
if self.test_suite:
|
||||
yield self.test_suite
|
||||
|
||||
def with_project_on_sys_path(self, func):
|
||||
"""
|
||||
Backward compatibility for project_on_sys_path context.
|
||||
"""
|
||||
with self.project_on_sys_path():
|
||||
func()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def project_on_sys_path(self, include_dists=[]):
|
||||
self.run_command('egg_info')
|
||||
|
||||
# Build extensions in-place
|
||||
self.reinitialize_command('build_ext', inplace=1)
|
||||
self.run_command('build_ext')
|
||||
|
||||
ei_cmd = self.get_finalized_command("egg_info")
|
||||
|
||||
old_path = sys.path[:]
|
||||
old_modules = sys.modules.copy()
|
||||
|
||||
try:
|
||||
project_path = normalize_path(ei_cmd.egg_base)
|
||||
sys.path.insert(0, project_path)
|
||||
working_set.__init__()
|
||||
add_activation_listener(lambda dist: dist.activate())
|
||||
require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version))
|
||||
with self.paths_on_pythonpath([project_path]):
|
||||
yield
|
||||
finally:
|
||||
sys.path[:] = old_path
|
||||
sys.modules.clear()
|
||||
sys.modules.update(old_modules)
|
||||
working_set.__init__()
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def paths_on_pythonpath(paths):
|
||||
"""
|
||||
Add the indicated paths to the head of the PYTHONPATH environment
|
||||
variable so that subprocesses will also see the packages at
|
||||
these paths.
|
||||
|
||||
Do this in a context that restores the value on exit.
|
||||
"""
|
||||
nothing = object()
|
||||
orig_pythonpath = os.environ.get('PYTHONPATH', nothing)
|
||||
current_pythonpath = os.environ.get('PYTHONPATH', '')
|
||||
try:
|
||||
prefix = os.pathsep.join(unique_everseen(paths))
|
||||
to_join = filter(None, [prefix, current_pythonpath])
|
||||
new_path = os.pathsep.join(to_join)
|
||||
if new_path:
|
||||
os.environ['PYTHONPATH'] = new_path
|
||||
yield
|
||||
finally:
|
||||
if orig_pythonpath is nothing:
|
||||
os.environ.pop('PYTHONPATH', None)
|
||||
else:
|
||||
os.environ['PYTHONPATH'] = orig_pythonpath
|
||||
|
||||
@staticmethod
|
||||
def install_dists(dist):
|
||||
"""
|
||||
Install the requirements indicated by self.distribution and
|
||||
return an iterable of the dists that were built.
|
||||
"""
|
||||
ir_d = dist.fetch_build_eggs(dist.install_requires)
|
||||
tr_d = dist.fetch_build_eggs(dist.tests_require or [])
|
||||
er_d = dist.fetch_build_eggs(
|
||||
v
|
||||
for k, v in dist.extras_require.items()
|
||||
if k.startswith(':') and evaluate_marker(k[1:])
|
||||
)
|
||||
return itertools.chain(ir_d, tr_d, er_d)
|
||||
|
||||
def run(self):
|
||||
raise RuntimeError("Support for the test command was removed in Setuptools 72")
|
||||
self.announce(
|
||||
"WARNING: Testing via this command is deprecated and will be "
|
||||
"removed in a future version. Users looking for a generic test "
|
||||
"entry point independent of test runner are encouraged to use "
|
||||
"tox.",
|
||||
log.WARN,
|
||||
)
|
||||
|
||||
installed_dists = self.install_dists(self.distribution)
|
||||
|
||||
cmd = ' '.join(self._argv)
|
||||
if self.dry_run:
|
||||
self.announce('skipping "%s" (dry run)' % cmd)
|
||||
return
|
||||
|
||||
self.announce('running "%s"' % cmd)
|
||||
|
||||
paths = map(operator.attrgetter('location'), installed_dists)
|
||||
with self.paths_on_pythonpath(paths):
|
||||
with self.project_on_sys_path():
|
||||
self.run_tests()
|
||||
|
||||
def run_tests(self):
|
||||
test = unittest.main(
|
||||
None,
|
||||
None,
|
||||
self._argv,
|
||||
testLoader=self._resolve_as_ep(self.test_loader),
|
||||
testRunner=self._resolve_as_ep(self.test_runner),
|
||||
exit=False,
|
||||
)
|
||||
if not test.result.wasSuccessful():
|
||||
msg = 'Test failed: %s' % test.result
|
||||
self.announce(msg, log.ERROR)
|
||||
raise DistutilsError(msg)
|
||||
|
||||
@property
|
||||
def _argv(self):
|
||||
return ['unittest'] + self.test_args
|
||||
|
||||
@staticmethod
|
||||
@pass_none
|
||||
def _resolve_as_ep(val):
|
||||
"""
|
||||
Load the indicated attribute value, called, as a as if it were
|
||||
specified as an entry point.
|
||||
"""
|
||||
return metadata.EntryPoint(value=val, name=None, group=None).load()()
|
||||
|
||||
Reference in New Issue
Block a user