diff --git a/newsfragments/4649.removal.rst b/newsfragments/4649.removal.rst new file mode 100644 index 0000000000..f53388b230 --- /dev/null +++ b/newsfragments/4649.removal.rst @@ -0,0 +1 @@ +Merge with pypa/distutils@7283751. Removed the register and upload commands and the config module that backs them (pypa/distutils#294). Removed the borland compiler. Replaced vendored dependencies with natural dependencies. Cygwin C compiler now gets compilers from sysconfig (pypa/distutils#296). \ No newline at end of file diff --git a/setuptools/_distutils/_collections.py b/setuptools/_distutils/_collections.py deleted file mode 100644 index 863030b3cf..0000000000 --- a/setuptools/_distutils/_collections.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import collections -import itertools - - -# from jaraco.collections 3.5.1 -class DictStack(list, collections.abc.Mapping): - """ - A stack of dictionaries that behaves as a view on those dictionaries, - giving preference to the last. - - >>> stack = DictStack([dict(a=1, c=2), dict(b=2, a=2)]) - >>> stack['a'] - 2 - >>> stack['b'] - 2 - >>> stack['c'] - 2 - >>> len(stack) - 3 - >>> stack.push(dict(a=3)) - >>> stack['a'] - 3 - >>> set(stack.keys()) == set(['a', 'b', 'c']) - True - >>> set(stack.items()) == set([('a', 3), ('b', 2), ('c', 2)]) - True - >>> dict(**stack) == dict(stack) == dict(a=3, c=2, b=2) - True - >>> d = stack.pop() - >>> stack['a'] - 2 - >>> d = stack.pop() - >>> stack['a'] - 1 - >>> stack.get('b', None) - >>> 'c' in stack - True - """ - - def __iter__(self): - dicts = list.__iter__(self) - return iter(set(itertools.chain.from_iterable(c.keys() for c in dicts))) - - def __getitem__(self, key): - for scope in reversed(tuple(list.__iter__(self))): - if key in scope: - return scope[key] - raise KeyError(key) - - push = list.append - - def __contains__(self, other): - return collections.abc.Mapping.__contains__(self, other) - - def __len__(self): - return len(list(iter(self))) diff --git a/setuptools/_distutils/_functools.py b/setuptools/_distutils/_functools.py deleted file mode 100644 index e03365eafa..0000000000 --- a/setuptools/_distutils/_functools.py +++ /dev/null @@ -1,73 +0,0 @@ -import collections.abc -import functools - - -# from jaraco.functools 3.5 -def pass_none(func): - """ - Wrap func so it's not called if its first param is None - - >>> print_text = pass_none(print) - >>> print_text('text') - text - >>> print_text(None) - """ - - @functools.wraps(func) - def wrapper(param, *args, **kwargs): - if param is not None: - return func(param, *args, **kwargs) - - return wrapper - - -# from jaraco.functools 4.0 -@functools.singledispatch -def _splat_inner(args, func): - """Splat args to func.""" - return func(*args) - - -@_splat_inner.register -def _(args: collections.abc.Mapping, func): - """Splat kargs to func as kwargs.""" - return func(**args) - - -def splat(func): - """ - Wrap func to expect its parameters to be passed positionally in a tuple. - - Has a similar effect to that of ``itertools.starmap`` over - simple ``map``. - - >>> import itertools, operator - >>> pairs = [(-1, 1), (0, 2)] - >>> _ = tuple(itertools.starmap(print, pairs)) - -1 1 - 0 2 - >>> _ = tuple(map(splat(print), pairs)) - -1 1 - 0 2 - - The approach generalizes to other iterators that don't have a "star" - equivalent, such as a "starfilter". - - >>> list(filter(splat(operator.add), pairs)) - [(0, 2)] - - Splat also accepts a mapping argument. - - >>> def is_nice(msg, code): - ... return "smile" in msg or code == 0 - >>> msgs = [ - ... dict(msg='smile!', code=20), - ... dict(msg='error :(', code=1), - ... dict(msg='unknown', code=0), - ... ] - >>> for msg in filter(splat(is_nice), msgs): - ... print(msg) - {'msg': 'smile!', 'code': 20} - {'msg': 'unknown', 'code': 0} - """ - return functools.wraps(func)(functools.partial(_splat_inner, func=func)) diff --git a/setuptools/_distutils/_itertools.py b/setuptools/_distutils/_itertools.py deleted file mode 100644 index 85b2951186..0000000000 --- a/setuptools/_distutils/_itertools.py +++ /dev/null @@ -1,52 +0,0 @@ -# from more_itertools 10.2 -def always_iterable(obj, base_type=(str, bytes)): - """If *obj* is iterable, return an iterator over its items:: - - >>> obj = (1, 2, 3) - >>> list(always_iterable(obj)) - [1, 2, 3] - - If *obj* is not iterable, return a one-item iterable containing *obj*:: - - >>> obj = 1 - >>> list(always_iterable(obj)) - [1] - - If *obj* is ``None``, return an empty iterable: - - >>> obj = None - >>> list(always_iterable(None)) - [] - - By default, binary and text strings are not considered iterable:: - - >>> obj = 'foo' - >>> list(always_iterable(obj)) - ['foo'] - - If *base_type* is set, objects for which ``isinstance(obj, base_type)`` - returns ``True`` won't be considered iterable. - - >>> obj = {'a': 1} - >>> list(always_iterable(obj)) # Iterate over the dict's keys - ['a'] - >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit - [{'a': 1}] - - Set *base_type* to ``None`` to avoid any special handling and treat objects - Python considers iterable as iterable: - - >>> obj = 'foo' - >>> list(always_iterable(obj, base_type=None)) - ['f', 'o', 'o'] - """ - if obj is None: - return iter(()) - - if (base_type is not None) and isinstance(obj, base_type): - return iter((obj,)) - - try: - return iter(obj) - except TypeError: - return iter((obj,)) diff --git a/setuptools/_distutils/_modified.py b/setuptools/_distutils/_modified.py index b7bdaa2943..7cdca9398f 100644 --- a/setuptools/_distutils/_modified.py +++ b/setuptools/_distutils/_modified.py @@ -3,7 +3,8 @@ import functools import os.path -from ._functools import splat +from jaraco.functools import splat + from .compat.py39 import zip_strict from .errors import DistutilsFileError diff --git a/setuptools/_distutils/_msvccompiler.py b/setuptools/_distutils/_msvccompiler.py index bf10ae2365..97b067c686 100644 --- a/setuptools/_distutils/_msvccompiler.py +++ b/setuptools/_distutils/_msvccompiler.py @@ -159,7 +159,7 @@ def _get_vc_env(plat_spec): stderr=subprocess.STDOUT, ).decode('utf-16le', errors='replace') except subprocess.CalledProcessError as exc: - log.error(exc.output) # noqa: RUF100, TRY400 + log.error(exc.output) raise DistutilsPlatformError(f"Error executing {exc.cmd}") env = { diff --git a/setuptools/_distutils/archive_util.py b/setuptools/_distutils/archive_util.py index cc4699b1a3..5bb6df763d 100644 --- a/setuptools/_distutils/archive_util.py +++ b/setuptools/_distutils/archive_util.py @@ -4,8 +4,6 @@ that sort of thing).""" import os -import sys -from warnings import warn try: import zipfile @@ -67,8 +65,7 @@ def make_tarball( """Create a (possibly compressed) tar file from all the files under 'base_dir'. - 'compress' must be "gzip" (the default), "bzip2", "xz", "compress", or - None. ("compress" will be deprecated in Python 3.2) + 'compress' must be "gzip" (the default), "bzip2", "xz", or None. 'owner' and 'group' can be used to define an owner and a group for the archive that is being built. If not provided, the current owner and group @@ -84,20 +81,17 @@ def make_tarball( 'bzip2': 'bz2', 'xz': 'xz', None: '', - 'compress': '', } - compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz', 'compress': '.Z'} + compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz'} # flags for compression program, each element of list will be an argument if compress is not None and compress not in compress_ext.keys(): raise ValueError( - "bad value for 'compress': must be None, 'gzip', 'bzip2', " - "'xz' or 'compress'" + "bad value for 'compress': must be None, 'gzip', 'bzip2', 'xz'" ) archive_name = base_name + '.tar' - if compress != 'compress': - archive_name += compress_ext.get(compress, '') + archive_name += compress_ext.get(compress, '') mkpath(os.path.dirname(archive_name), dry_run=dry_run) @@ -125,18 +119,6 @@ def _set_uid_gid(tarinfo): finally: tar.close() - # compression using `compress` - if compress == 'compress': - warn("'compress' is deprecated.", DeprecationWarning) - # the option varies depending on the platform - compressed_name = archive_name + compress_ext[compress] - if sys.platform == 'win32': - cmd = [compress, archive_name, compressed_name] - else: - cmd = [compress, '-f', archive_name] - spawn(cmd, dry_run=dry_run) - return compressed_name - return archive_name diff --git a/setuptools/_distutils/bcppcompiler.py b/setuptools/_distutils/bcppcompiler.py deleted file mode 100644 index 9157b43328..0000000000 --- a/setuptools/_distutils/bcppcompiler.py +++ /dev/null @@ -1,396 +0,0 @@ -"""distutils.bcppcompiler - -Contains BorlandCCompiler, an implementation of the abstract CCompiler class -for the Borland C++ compiler. -""" - -# This implementation by Lyle Johnson, based on the original msvccompiler.py -# module and using the directions originally published by Gordon Williams. - -# XXX looks like there's a LOT of overlap between these two classes: -# someone should sit down and factor out the common code as -# WindowsCCompiler! --GPW - -import os -import warnings - -from ._log import log -from ._modified import newer -from .ccompiler import CCompiler, gen_preprocess_options -from .errors import ( - CompileError, - DistutilsExecError, - LibError, - LinkError, - UnknownFileError, -) -from .file_util import write_file - -warnings.warn( - "bcppcompiler is deprecated and slated to be removed " - "in the future. Please discontinue use or file an issue " - "with pypa/distutils describing your use case.", - DeprecationWarning, -) - - -class BCPPCompiler(CCompiler): - """Concrete class that implements an interface to the Borland C/C++ - compiler, as defined by the CCompiler abstract class. - """ - - compiler_type = 'bcpp' - - # Just set this so CCompiler's constructor doesn't barf. We currently - # don't use the 'set_executables()' bureaucracy provided by CCompiler, - # as it really isn't necessary for this sort of single-compiler class. - # Would be nice to have a consistent interface with UnixCCompiler, - # though, so it's worth thinking about. - executables = {} - - # Private class data (need to distinguish C from C++ source for compiler) - _c_extensions = ['.c'] - _cpp_extensions = ['.cc', '.cpp', '.cxx'] - - # Needed for the filename generation methods provided by the - # base class, CCompiler. - src_extensions = _c_extensions + _cpp_extensions - obj_extension = '.obj' - static_lib_extension = '.lib' - shared_lib_extension = '.dll' - static_lib_format = shared_lib_format = '%s%s' - exe_extension = '.exe' - - def __init__(self, verbose=False, dry_run=False, force=False): - super().__init__(verbose, dry_run, force) - - # These executables are assumed to all be in the path. - # Borland doesn't seem to use any special registry settings to - # indicate their installation locations. - - self.cc = "bcc32.exe" - self.linker = "ilink32.exe" - self.lib = "tlib.exe" - - self.preprocess_options = None - self.compile_options = ['/tWM', '/O2', '/q', '/g0'] - self.compile_options_debug = ['/tWM', '/Od', '/q', '/g0'] - - self.ldflags_shared = ['/Tpd', '/Gn', '/q', '/x'] - self.ldflags_shared_debug = ['/Tpd', '/Gn', '/q', '/x'] - self.ldflags_static = [] - self.ldflags_exe = ['/Gn', '/q', '/x'] - self.ldflags_exe_debug = ['/Gn', '/q', '/x', '/r'] - - # -- Worker methods ------------------------------------------------ - - def compile( - self, - sources, - output_dir=None, - macros=None, - include_dirs=None, - debug=False, - extra_preargs=None, - extra_postargs=None, - depends=None, - ): - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( - output_dir, macros, include_dirs, sources, depends, extra_postargs - ) - compile_opts = extra_preargs or [] - compile_opts.append('-c') - if debug: - compile_opts.extend(self.compile_options_debug) - else: - compile_opts.extend(self.compile_options) - - for obj in objects: - try: - src, ext = build[obj] - except KeyError: - continue - # XXX why do the normpath here? - src = os.path.normpath(src) - obj = os.path.normpath(obj) - # XXX _setup_compile() did a mkpath() too but before the normpath. - # Is it possible to skip the normpath? - self.mkpath(os.path.dirname(obj)) - - if ext == '.res': - # This is already a binary file -- skip it. - continue # the 'for' loop - if ext == '.rc': - # This needs to be compiled to a .res file -- do it now. - try: - self.spawn(["brcc32", "-fo", obj, src]) - except DistutilsExecError as msg: - raise CompileError(msg) - continue # the 'for' loop - - # The next two are both for the real compiler. - if ext in self._c_extensions: - input_opt = "" - elif ext in self._cpp_extensions: - input_opt = "-P" - else: - # Unknown file type -- no extra options. The compiler - # will probably fail, but let it just in case this is a - # file the compiler recognizes even if we don't. - input_opt = "" - - output_opt = "-o" + obj - - # Compiler command line syntax is: "bcc32 [options] file(s)". - # Note that the source file names must appear at the end of - # the command line. - try: - self.spawn( - [self.cc] - + compile_opts - + pp_opts - + [input_opt, output_opt] - + extra_postargs - + [src] - ) - except DistutilsExecError as msg: - raise CompileError(msg) - - return objects - - # compile () - - def create_static_lib( - self, objects, output_libname, output_dir=None, debug=False, target_lang=None - ): - (objects, output_dir) = self._fix_object_args(objects, output_dir) - output_filename = self.library_filename(output_libname, output_dir=output_dir) - - if self._need_link(objects, output_filename): - lib_args = [output_filename, '/u'] + objects - if debug: - pass # XXX what goes here? - try: - self.spawn([self.lib] + lib_args) - except DistutilsExecError as msg: - raise LibError(msg) - else: - log.debug("skipping %s (up-to-date)", output_filename) - - # create_static_lib () - - def link( # noqa: C901 - self, - target_desc, - objects, - output_filename, - output_dir=None, - libraries=None, - library_dirs=None, - runtime_library_dirs=None, - export_symbols=None, - debug=False, - extra_preargs=None, - extra_postargs=None, - build_temp=None, - target_lang=None, - ): - # XXX this ignores 'build_temp'! should follow the lead of - # msvccompiler.py - - (objects, output_dir) = self._fix_object_args(objects, output_dir) - (libraries, library_dirs, runtime_library_dirs) = self._fix_lib_args( - libraries, library_dirs, runtime_library_dirs - ) - - if runtime_library_dirs: - log.warning( - "I don't know what to do with 'runtime_library_dirs': %s", - str(runtime_library_dirs), - ) - - if output_dir is not None: - output_filename = os.path.join(output_dir, output_filename) - - if self._need_link(objects, output_filename): - # Figure out linker args based on type of target. - if target_desc == CCompiler.EXECUTABLE: - startup_obj = 'c0w32' - if debug: - ld_args = self.ldflags_exe_debug[:] - else: - ld_args = self.ldflags_exe[:] - else: - startup_obj = 'c0d32' - if debug: - ld_args = self.ldflags_shared_debug[:] - else: - ld_args = self.ldflags_shared[:] - - # Create a temporary exports file for use by the linker - if export_symbols is None: - def_file = '' - else: - head, tail = os.path.split(output_filename) - modname, ext = os.path.splitext(tail) - temp_dir = os.path.dirname(objects[0]) # preserve tree structure - def_file = os.path.join(temp_dir, f'{modname}.def') - contents = ['EXPORTS'] - contents.extend(f' {sym}=_{sym}' for sym in export_symbols) - self.execute(write_file, (def_file, contents), f"writing {def_file}") - - # Borland C++ has problems with '/' in paths - objects2 = map(os.path.normpath, objects) - # split objects in .obj and .res files - # Borland C++ needs them at different positions in the command line - objects = [startup_obj] - resources = [] - for file in objects2: - (base, ext) = os.path.splitext(os.path.normcase(file)) - if ext == '.res': - resources.append(file) - else: - objects.append(file) - - for ell in library_dirs: - ld_args.append(f"/L{os.path.normpath(ell)}") - ld_args.append("/L.") # we sometimes use relative paths - - # list of object files - ld_args.extend(objects) - - # XXX the command-line syntax for Borland C++ is a bit wonky; - # certain filenames are jammed together in one big string, but - # comma-delimited. This doesn't mesh too well with the - # Unix-centric attitude (with a DOS/Windows quoting hack) of - # 'spawn()', so constructing the argument list is a bit - # awkward. Note that doing the obvious thing and jamming all - # the filenames and commas into one argument would be wrong, - # because 'spawn()' would quote any filenames with spaces in - # them. Arghghh!. Apparently it works fine as coded... - - # name of dll/exe file - ld_args.extend([',', output_filename]) - # no map file and start libraries - ld_args.append(',,') - - for lib in libraries: - # see if we find it and if there is a bcpp specific lib - # (xxx_bcpp.lib) - libfile = self.find_library_file(library_dirs, lib, debug) - if libfile is None: - ld_args.append(lib) - # probably a BCPP internal library -- don't warn - else: - # full name which prefers bcpp_xxx.lib over xxx.lib - ld_args.append(libfile) - - # some default libraries - ld_args.extend(('import32', 'cw32mt')) - - # def file for export symbols - ld_args.extend([',', def_file]) - # add resource files - ld_args.append(',') - ld_args.extend(resources) - - if extra_preargs: - ld_args[:0] = extra_preargs - if extra_postargs: - ld_args.extend(extra_postargs) - - self.mkpath(os.path.dirname(output_filename)) - try: - self.spawn([self.linker] + ld_args) - except DistutilsExecError as msg: - raise LinkError(msg) - - else: - log.debug("skipping %s (up-to-date)", output_filename) - - # link () - - # -- Miscellaneous methods ----------------------------------------- - - def find_library_file(self, dirs, lib, debug=False): - # List of effective library names to try, in order of preference: - # xxx_bcpp.lib is better than xxx.lib - # and xxx_d.lib is better than xxx.lib if debug is set - # - # The "_bcpp" suffix is to handle a Python installation for people - # with multiple compilers (primarily Distutils hackers, I suspect - # ;-). The idea is they'd have one static library for each - # compiler they care about, since (almost?) every Windows compiler - # seems to have a different format for static libraries. - if debug: - dlib = lib + "_d" - try_names = (dlib + "_bcpp", lib + "_bcpp", dlib, lib) - else: - try_names = (lib + "_bcpp", lib) - - for dir in dirs: - for name in try_names: - libfile = os.path.join(dir, self.library_filename(name)) - if os.path.exists(libfile): - return libfile - else: - # Oops, didn't find it in *any* of 'dirs' - return None - - # overwrite the one from CCompiler to support rc and res-files - def object_filenames(self, source_filenames, strip_dir=False, output_dir=''): - if output_dir is None: - output_dir = '' - obj_names = [] - for src_name in source_filenames: - # use normcase to make sure '.rc' is really '.rc' and not '.RC' - (base, ext) = os.path.splitext(os.path.normcase(src_name)) - if ext not in (self.src_extensions + ['.rc', '.res']): - raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") - if strip_dir: - base = os.path.basename(base) - if ext == '.res': - # these can go unchanged - obj_names.append(os.path.join(output_dir, base + ext)) - elif ext == '.rc': - # these need to be compiled to .res-files - obj_names.append(os.path.join(output_dir, base + '.res')) - else: - obj_names.append(os.path.join(output_dir, base + self.obj_extension)) - return obj_names - - # object_filenames () - - def preprocess( - self, - source, - output_file=None, - macros=None, - include_dirs=None, - extra_preargs=None, - extra_postargs=None, - ): - (_, macros, include_dirs) = self._fix_compile_args(None, macros, include_dirs) - pp_opts = gen_preprocess_options(macros, include_dirs) - pp_args = ['cpp32.exe'] + pp_opts - if output_file is not None: - pp_args.append('-o' + output_file) - if extra_preargs: - pp_args[:0] = extra_preargs - if extra_postargs: - pp_args.extend(extra_postargs) - pp_args.append(source) - - # We need to preprocess: either we're being forced to, or the - # source file is newer than the target (or the target doesn't - # exist). - if self.force or output_file is None or newer(source, output_file): - if output_file: - self.mkpath(os.path.dirname(output_file)) - try: - self.spawn(pp_args) - except DistutilsExecError as msg: - print(msg) - raise CompileError(msg) - - # preprocess() diff --git a/setuptools/_distutils/ccompiler.py b/setuptools/_distutils/ccompiler.py index bc4743bcbf..5e73e56d02 100644 --- a/setuptools/_distutils/ccompiler.py +++ b/setuptools/_distutils/ccompiler.py @@ -9,7 +9,8 @@ import types import warnings -from ._itertools import always_iterable +from more_itertools import always_iterable + from ._log import log from ._modified import newer_group from .dir_util import mkpath diff --git a/setuptools/_distutils/command/__init__.py b/setuptools/_distutils/command/__init__.py index 1e8fbe60c2..0f8a1692ba 100644 --- a/setuptools/_distutils/command/__init__.py +++ b/setuptools/_distutils/command/__init__.py @@ -16,10 +16,8 @@ 'install_scripts', 'install_data', 'sdist', - 'register', 'bdist', 'bdist_dumb', 'bdist_rpm', 'check', - 'upload', ] diff --git a/setuptools/_distutils/command/install.py b/setuptools/_distutils/command/install.py index b83e061e02..ceb453e041 100644 --- a/setuptools/_distutils/command/install.py +++ b/setuptools/_distutils/command/install.py @@ -10,7 +10,8 @@ from distutils._log import log from site import USER_BASE, USER_SITE -from .. import _collections +import jaraco.collections + from ..core import Command from ..debug import DEBUG from ..errors import DistutilsOptionError, DistutilsPlatformError @@ -428,7 +429,7 @@ def finalize_options(self): # noqa: C901 local_vars['userbase'] = self.install_userbase local_vars['usersite'] = self.install_usersite - self.config_vars = _collections.DictStack([ + self.config_vars = jaraco.collections.DictStack([ fw.vars(), compat_vars, sysconfig.get_config_vars(), diff --git a/setuptools/_distutils/command/register.py b/setuptools/_distutils/command/register.py deleted file mode 100644 index c1acd27b54..0000000000 --- a/setuptools/_distutils/command/register.py +++ /dev/null @@ -1,322 +0,0 @@ -"""distutils.command.register - -Implements the Distutils 'register' command (register with the repository). -""" - -# created 2002/10/21, Richard Jones - -import getpass -import io -import logging -import urllib.parse -import urllib.request -from distutils._log import log -from warnings import warn - -from .._itertools import always_iterable -from ..core import PyPIRCCommand - - -class register(PyPIRCCommand): - description = "register the distribution with the Python package index" - user_options = PyPIRCCommand.user_options + [ - ('list-classifiers', None, 'list the valid Trove classifiers'), - ( - 'strict', - None, - 'Will stop the registering if the meta-data are not fully compliant', - ), - ] - boolean_options = PyPIRCCommand.boolean_options + [ - 'verify', - 'list-classifiers', - 'strict', - ] - - sub_commands = [('check', lambda self: True)] - - def initialize_options(self): - PyPIRCCommand.initialize_options(self) - self.list_classifiers = False - self.strict = False - - def finalize_options(self): - PyPIRCCommand.finalize_options(self) - # setting options for the `check` subcommand - check_options = { - 'strict': ('register', self.strict), - 'restructuredtext': ('register', 1), - } - self.distribution.command_options['check'] = check_options - - def run(self): - self.finalize_options() - self._set_config() - - # Run sub commands - for cmd_name in self.get_sub_commands(): - self.run_command(cmd_name) - - if self.dry_run: - self.verify_metadata() - elif self.list_classifiers: - self.classifiers() - else: - self.send_metadata() - - def check_metadata(self): - """Deprecated API.""" - warn( - "distutils.command.register.check_metadata is deprecated; " - "use the check command instead", - DeprecationWarning, - ) - check = self.distribution.get_command_obj('check') - check.ensure_finalized() - check.strict = self.strict - check.restructuredtext = True - check.run() - - def _set_config(self): - """Reads the configuration file and set attributes.""" - config = self._read_pypirc() - if config != {}: - self.username = config['username'] - self.password = config['password'] - self.repository = config['repository'] - self.realm = config['realm'] - self.has_config = True - else: - if self.repository not in ('pypi', self.DEFAULT_REPOSITORY): - raise ValueError(f'{self.repository} not found in .pypirc') - if self.repository == 'pypi': - self.repository = self.DEFAULT_REPOSITORY - self.has_config = False - - def classifiers(self): - """Fetch the list of classifiers from the server.""" - url = self.repository + '?:action=list_classifiers' - response = urllib.request.urlopen(url) - log.info(self._read_pypi_response(response)) - - def verify_metadata(self): - """Send the metadata to the package index server to be checked.""" - # send the info to the server and report the result - (code, result) = self.post_to_server(self.build_post_data('verify')) - log.info('Server response (%s): %s', code, result) - - def send_metadata(self): # noqa: C901 - """Send the metadata to the package index server. - - Well, do the following: - 1. figure who the user is, and then - 2. send the data as a Basic auth'ed POST. - - First we try to read the username/password from $HOME/.pypirc, - which is a ConfigParser-formatted file with a section - [distutils] containing username and password entries (both - in clear text). Eg: - - [distutils] - index-servers = - pypi - - [pypi] - username: fred - password: sekrit - - Otherwise, to figure who the user is, we offer the user three - choices: - - 1. use existing login, - 2. register as a new user, or - 3. set the password to a random string and email the user. - - """ - # see if we can short-cut and get the username/password from the - # config - if self.has_config: - choice = '1' - username = self.username - password = self.password - else: - choice = 'x' - username = password = '' - - # get the user's login info - choices = '1 2 3 4'.split() - while choice not in choices: - self.announce( - """\ -We need to know who you are, so please choose either: - 1. use your existing login, - 2. register as a new user, - 3. have the server generate a new password for you (and email it to you), or - 4. quit -Your selection [default 1]: """, - logging.INFO, - ) - choice = input() - if not choice: - choice = '1' - elif choice not in choices: - print('Please choose one of the four options!') - - if choice == '1': - # get the username and password - while not username: - username = input('Username: ') - while not password: - password = getpass.getpass('Password: ') - - # set up the authentication - auth = urllib.request.HTTPPasswordMgr() - host = urllib.parse.urlparse(self.repository)[1] - auth.add_password(self.realm, host, username, password) - # send the info to the server and report the result - code, result = self.post_to_server(self.build_post_data('submit'), auth) - self.announce(f'Server response ({code}): {result}', logging.INFO) - - # possibly save the login - if code == 200: - if self.has_config: - # sharing the password in the distribution instance - # so the upload command can reuse it - self.distribution.password = password - else: - self.announce( - ( - 'I can store your PyPI login so future ' - 'submissions will be faster.' - ), - logging.INFO, - ) - self.announce( - f'(the login will be stored in {self._get_rc_file()})', - logging.INFO, - ) - choice = 'X' - while choice.lower() not in 'yn': - choice = input('Save your login (y/N)?') - if not choice: - choice = 'n' - if choice.lower() == 'y': - self._store_pypirc(username, password) - - elif choice == '2': - data = {':action': 'user'} - data['name'] = data['password'] = data['email'] = '' - data['confirm'] = None - while not data['name']: - data['name'] = input('Username: ') - while data['password'] != data['confirm']: - while not data['password']: - data['password'] = getpass.getpass('Password: ') - while not data['confirm']: - data['confirm'] = getpass.getpass(' Confirm: ') - if data['password'] != data['confirm']: - data['password'] = '' - data['confirm'] = None - print("Password and confirm don't match!") - while not data['email']: - data['email'] = input(' EMail: ') - code, result = self.post_to_server(data) - if code != 200: - log.info('Server response (%s): %s', code, result) - else: - log.info('You will receive an email shortly.') - log.info('Follow the instructions in it to complete registration.') - elif choice == '3': - data = {':action': 'password_reset'} - data['email'] = '' - while not data['email']: - data['email'] = input('Your email address: ') - code, result = self.post_to_server(data) - log.info('Server response (%s): %s', code, result) - - def build_post_data(self, action): - # figure the data to send - the metadata plus some additional - # information used by the package server - meta = self.distribution.metadata - data = { - ':action': action, - 'metadata_version': '1.0', - 'name': meta.get_name(), - 'version': meta.get_version(), - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), - } - if data['provides'] or data['requires'] or data['obsoletes']: - data['metadata_version'] = '1.1' - return data - - def post_to_server(self, data, auth=None): # noqa: C901 - """Post a query to the server, and return a string response.""" - if 'name' in data: - self.announce( - 'Registering {} to {}'.format(data['name'], self.repository), - logging.INFO, - ) - # Build up the MIME payload for the urllib2 POST data - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = '\n--' + boundary - end_boundary = sep_boundary + '--' - body = io.StringIO() - for key, values in data.items(): - for value in map(str, make_iterable(values)): - body.write(sep_boundary) - body.write(f'\nContent-Disposition: form-data; name="{key}"') - body.write("\n\n") - body.write(value) - if value and value[-1] == '\r': - body.write('\n') # write an extra newline (lurve Macs) - body.write(end_boundary) - body.write("\n") - body = body.getvalue().encode("utf-8") - - # build the Request - headers = { - 'Content-type': f'multipart/form-data; boundary={boundary}; charset=utf-8', - 'Content-length': str(len(body)), - } - req = urllib.request.Request(self.repository, body, headers) - - # handle HTTP and include the Basic Auth handler - opener = urllib.request.build_opener( - urllib.request.HTTPBasicAuthHandler(password_mgr=auth) - ) - data = '' - try: - result = opener.open(req) - except urllib.error.HTTPError as e: - if self.show_response: - data = e.fp.read() - result = e.code, e.msg - except urllib.error.URLError as e: - result = 500, str(e) - else: - if self.show_response: - data = self._read_pypi_response(result) - result = 200, 'OK' - if self.show_response: - msg = '\n'.join(('-' * 75, data, '-' * 75)) - self.announce(msg, logging.INFO) - return result - - -def make_iterable(values): - if values is None: - return [None] - return always_iterable(values) diff --git a/setuptools/_distutils/command/sdist.py b/setuptools/_distutils/command/sdist.py index eda6afe811..d723a1c9fb 100644 --- a/setuptools/_distutils/command/sdist.py +++ b/setuptools/_distutils/command/sdist.py @@ -8,7 +8,6 @@ from distutils._log import log from glob import glob from itertools import filterfalse -from warnings import warn from ..core import Command from ..errors import DistutilsOptionError, DistutilsTemplateError @@ -177,17 +176,6 @@ def run(self): # or zipfile, or whatever. self.make_distribution() - def check_metadata(self): - """Deprecated API.""" - warn( - "distutils.command.sdist.check_metadata is deprecated, \ - use the check command instead", - PendingDeprecationWarning, - ) - check = self.distribution.get_command_obj('check') - check.ensure_finalized() - check.run() - def get_file_list(self): """Figure out the list of files to include in the source distribution, and put it in 'self.filelist'. This might involve diff --git a/setuptools/_distutils/command/upload.py b/setuptools/_distutils/command/upload.py deleted file mode 100644 index a2461e089f..0000000000 --- a/setuptools/_distutils/command/upload.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -distutils.command.upload - -Implements the Distutils 'upload' subcommand (upload package to a package -index). -""" - -import hashlib -import io -import logging -import os -from base64 import standard_b64encode -from urllib.parse import urlparse -from urllib.request import HTTPError, Request, urlopen - -from .._itertools import always_iterable -from ..core import PyPIRCCommand -from ..errors import DistutilsError, DistutilsOptionError -from ..spawn import spawn - -# PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256) -# https://bugs.python.org/issue40698 -_FILE_CONTENT_DIGESTS = { - "md5_digest": getattr(hashlib, "md5", None), - "sha256_digest": getattr(hashlib, "sha256", None), - "blake2_256_digest": getattr(hashlib, "blake2b", None), -} - - -class upload(PyPIRCCommand): - description = "upload binary package to PyPI" - - user_options = PyPIRCCommand.user_options + [ - ('sign', 's', 'sign files to upload using gpg'), - ('identity=', 'i', 'GPG identity used to sign files'), - ] - - boolean_options = PyPIRCCommand.boolean_options + ['sign'] - - def initialize_options(self): - PyPIRCCommand.initialize_options(self) - self.username = '' - self.password = '' - self.show_response = False - self.sign = False - self.identity = None - - def finalize_options(self): - PyPIRCCommand.finalize_options(self) - if self.identity and not self.sign: - raise DistutilsOptionError("Must use --sign for --identity to have meaning") - config = self._read_pypirc() - if config != {}: - self.username = config['username'] - self.password = config['password'] - self.repository = config['repository'] - self.realm = config['realm'] - - # getting the password from the distribution - # if previously set by the register command - if not self.password and self.distribution.password: - self.password = self.distribution.password - - def run(self): - if not self.distribution.dist_files: - msg = ( - "Must create and upload files in one command " - "(e.g. setup.py sdist upload)" - ) - raise DistutilsOptionError(msg) - for command, pyversion, filename in self.distribution.dist_files: - self.upload_file(command, pyversion, filename) - - def upload_file(self, command, pyversion, filename): # noqa: C901 - # Makes sure the repository URL is compliant - schema, netloc, url, params, query, fragments = urlparse(self.repository) - if params or query or fragments: - raise AssertionError(f"Incompatible url {self.repository}") - - if schema not in ('http', 'https'): - raise AssertionError("unsupported schema " + schema) - - # Sign if requested - if self.sign: - gpg_args = ["gpg", "--detach-sign", "-a", filename] - if self.identity: - gpg_args[2:2] = ["--local-user", self.identity] - spawn(gpg_args, dry_run=self.dry_run) - - # Fill in the data - send all the meta-data in case we need to - # register a new release - f = open(filename, 'rb') - try: - content = f.read() - finally: - f.close() - - meta = self.distribution.metadata - data = { - # action - ':action': 'file_upload', - 'protocol_version': '1', - # identify release - 'name': meta.get_name(), - 'version': meta.get_version(), - # file content - 'content': (os.path.basename(filename), content), - 'filetype': command, - 'pyversion': pyversion, - # additional meta-data - 'metadata_version': '1.0', - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), - } - - data['comment'] = '' - - # file content digests - for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items(): - if digest_cons is None: - continue - try: - data[digest_name] = digest_cons(content).hexdigest() - except ValueError: - # hash digest not available or blocked by security policy - pass - - if self.sign: - with open(filename + ".asc", "rb") as f: - data['gpg_signature'] = (os.path.basename(filename) + ".asc", f.read()) - - # set up the authentication - user_pass = (self.username + ":" + self.password).encode('ascii') - # The exact encoding of the authentication string is debated. - # Anyway PyPI only accepts ascii for both username or password. - auth = "Basic " + standard_b64encode(user_pass).decode('ascii') - - # Build up the MIME payload for the POST data - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = b'\r\n--' + boundary.encode('ascii') - end_boundary = sep_boundary + b'--\r\n' - body = io.BytesIO() - for key, values in data.items(): - title = f'\r\nContent-Disposition: form-data; name="{key}"' - for value in make_iterable(values): - if type(value) is tuple: - title += f'; filename="{value[0]}"' - value = value[1] - else: - value = str(value).encode('utf-8') - body.write(sep_boundary) - body.write(title.encode('utf-8')) - body.write(b"\r\n\r\n") - body.write(value) - body.write(end_boundary) - body = body.getvalue() - - msg = f"Submitting {filename} to {self.repository}" - self.announce(msg, logging.INFO) - - # build the Request - headers = { - 'Content-type': f'multipart/form-data; boundary={boundary}', - 'Content-length': str(len(body)), - 'Authorization': auth, - } - - request = Request(self.repository, data=body, headers=headers) - # send the data - try: - result = urlopen(request) - status = result.getcode() - reason = result.msg - except HTTPError as e: - status = e.code - reason = e.msg - except OSError as e: - self.announce(str(e), logging.ERROR) - raise - - if status == 200: - self.announce(f'Server response ({status}): {reason}', logging.INFO) - if self.show_response: - text = self._read_pypi_response(result) - msg = '\n'.join(('-' * 75, text, '-' * 75)) - self.announce(msg, logging.INFO) - else: - msg = f'Upload failed ({status}): {reason}' - self.announce(msg, logging.ERROR) - raise DistutilsError(msg) - - -def make_iterable(values): - if values is None: - return [None] - return always_iterable(values, base_type=(bytes, str, tuple)) diff --git a/setuptools/_distutils/config.py b/setuptools/_distutils/config.py deleted file mode 100644 index ebd2e11da3..0000000000 --- a/setuptools/_distutils/config.py +++ /dev/null @@ -1,151 +0,0 @@ -"""distutils.pypirc - -Provides the PyPIRCCommand class, the base class for the command classes -that uses .pypirc in the distutils.command package. -""" - -import email.message -import os -from configparser import RawConfigParser - -from .cmd import Command - -DEFAULT_PYPIRC = """\ -[distutils] -index-servers = - pypi - -[pypi] -username:%s -password:%s -""" - - -class PyPIRCCommand(Command): - """Base command that knows how to handle the .pypirc file""" - - DEFAULT_REPOSITORY = 'https://upload.pypi.org/legacy/' - DEFAULT_REALM = 'pypi' - repository = None - realm = None - - user_options = [ - ('repository=', 'r', f"url of repository [default: {DEFAULT_REPOSITORY}]"), - ('show-response', None, 'display full response text from server'), - ] - - boolean_options = ['show-response'] - - def _get_rc_file(self): - """Returns rc file path.""" - return os.path.join(os.path.expanduser('~'), '.pypirc') - - def _store_pypirc(self, username, password): - """Creates a default .pypirc file.""" - rc = self._get_rc_file() - raw = os.open(rc, os.O_CREAT | os.O_WRONLY, 0o600) - with os.fdopen(raw, 'w', encoding='utf-8') as f: - f.write(DEFAULT_PYPIRC % (username, password)) - - def _read_pypirc(self): # noqa: C901 - """Reads the .pypirc file.""" - rc = self._get_rc_file() - if os.path.exists(rc): - self.announce(f'Using PyPI login from {rc}') - repository = self.repository or self.DEFAULT_REPOSITORY - - config = RawConfigParser() - config.read(rc, encoding='utf-8') - sections = config.sections() - if 'distutils' in sections: - # let's get the list of servers - index_servers = config.get('distutils', 'index-servers') - _servers = [ - server.strip() - for server in index_servers.split('\n') - if server.strip() != '' - ] - if _servers == []: - # nothing set, let's try to get the default pypi - if 'pypi' in sections: - _servers = ['pypi'] - else: - # the file is not properly defined, returning - # an empty dict - return {} - for server in _servers: - current = {'server': server} - current['username'] = config.get(server, 'username') - - # optional params - for key, default in ( - ('repository', self.DEFAULT_REPOSITORY), - ('realm', self.DEFAULT_REALM), - ('password', None), - ): - if config.has_option(server, key): - current[key] = config.get(server, key) - else: - current[key] = default - - # work around people having "repository" for the "pypi" - # section of their config set to the HTTP (rather than - # HTTPS) URL - if server == 'pypi' and repository in ( - self.DEFAULT_REPOSITORY, - 'pypi', - ): - current['repository'] = self.DEFAULT_REPOSITORY - return current - - if ( - current['server'] == repository - or current['repository'] == repository - ): - return current - elif 'server-login' in sections: - # old format - server = 'server-login' - if config.has_option(server, 'repository'): - repository = config.get(server, 'repository') - else: - repository = self.DEFAULT_REPOSITORY - return { - 'username': config.get(server, 'username'), - 'password': config.get(server, 'password'), - 'repository': repository, - 'server': server, - 'realm': self.DEFAULT_REALM, - } - - return {} - - def _read_pypi_response(self, response): - """Read and decode a PyPI HTTP response.""" - content_type = response.getheader('content-type', 'text/plain') - return response.read().decode(_extract_encoding(content_type)) - - def initialize_options(self): - """Initialize options.""" - self.repository = None - self.realm = None - self.show_response = False - - def finalize_options(self): - """Finalizes options.""" - if self.repository is None: - self.repository = self.DEFAULT_REPOSITORY - if self.realm is None: - self.realm = self.DEFAULT_REALM - - -def _extract_encoding(content_type): - """ - >>> _extract_encoding('text/plain') - 'ascii' - >>> _extract_encoding('text/html; charset="utf8"') - 'utf8' - """ - msg = email.message.EmailMessage() - msg['content-type'] = content_type - return msg['content-type'].params.get('charset', 'ascii') diff --git a/setuptools/_distutils/core.py b/setuptools/_distutils/core.py index 82113c47c1..bc06091abb 100644 --- a/setuptools/_distutils/core.py +++ b/setuptools/_distutils/core.py @@ -11,7 +11,6 @@ import tokenize from .cmd import Command -from .config import PyPIRCCommand from .debug import DEBUG # Mainly import these so setup scripts can "from distutils.core import" them. @@ -24,7 +23,7 @@ ) from .extension import Extension -__all__ = ['Distribution', 'Command', 'PyPIRCCommand', 'Extension', 'setup'] +__all__ = ['Distribution', 'Command', 'Extension', 'setup'] # This is a barebones help message generated displayed when the user # runs the setup script with no arguments at all. More useful help diff --git a/setuptools/_distutils/cygwinccompiler.py b/setuptools/_distutils/cygwinccompiler.py index 18b1b3557b..3c67524e6d 100644 --- a/setuptools/_distutils/cygwinccompiler.py +++ b/setuptools/_distutils/cygwinccompiler.py @@ -21,6 +21,7 @@ DistutilsPlatformError, ) from .file_util import write_file +from .sysconfig import get_config_vars from .unixccompiler import UnixCCompiler from .version import LooseVersion, suppress_known_deprecation @@ -61,8 +62,12 @@ def __init__(self, verbose=False, dry_run=False, force=False): "Compiling may fail because of undefined preprocessor macros." ) - self.cc = os.environ.get('CC', 'gcc') - self.cxx = os.environ.get('CXX', 'g++') + self.cc, self.cxx = get_config_vars('CC', 'CXX') + + # Override 'CC' and 'CXX' environment variables for + # building using MINGW compiler for MSVC python. + self.cc = os.environ.get('CC', self.cc or 'gcc') + self.cxx = os.environ.get('CXX', self.cxx or 'g++') self.linker_dll = self.cc self.linker_dll_cxx = self.cxx diff --git a/setuptools/_distutils/dir_util.py b/setuptools/_distutils/dir_util.py index 724afeff6f..3b22839d27 100644 --- a/setuptools/_distutils/dir_util.py +++ b/setuptools/_distutils/dir_util.py @@ -2,85 +2,83 @@ Utility functions for manipulating directories and directory trees.""" -import errno +import functools +import itertools import os +import pathlib +from . import file_util from ._log import log from .errors import DistutilsFileError, DistutilsInternalError -# cache for by mkpath() -- in addition to cheapening redundant calls, -# eliminates redundant "creating /foo/bar/baz" messages in dry-run mode -_path_created = set() + +class SkipRepeatAbsolutePaths(set): + """ + Cache for mkpath. + + In addition to cheapening redundant calls, eliminates redundant + "creating /foo/bar/baz" messages in dry-run mode. + """ + + def __init__(self): + SkipRepeatAbsolutePaths.instance = self + + @classmethod + def clear(cls): + super(cls, cls.instance).clear() + + def wrap(self, func): + @functools.wraps(func) + def wrapper(path, *args, **kwargs): + if path.absolute() in self: + return + self.add(path.absolute()) + return func(path, *args, **kwargs) + + return wrapper -def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 +# Python 3.8 compatibility +wrapper = SkipRepeatAbsolutePaths().wrap + + +@functools.singledispatch +@wrapper +def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False): """Create a directory and any missing ancestor directories. If the directory already exists (or if 'name' is the empty string, which means the current directory, which of course exists), then do nothing. Raise DistutilsFileError if unable to create some directory along the way (eg. some sub-path exists, but is a file rather than a directory). - If 'verbose' is true, print a one-line summary of each mkdir to stdout. + If 'verbose' is true, log the directory created. Return the list of directories actually created. - - os.makedirs is not used because: - - a) It's new to Python 1.5.2, and - b) it blows up if the directory already exists (in which case it should - silently succeed). """ + if verbose and not name.is_dir(): + log.info("creating %s", name) - global _path_created - - # Detect a common bug -- name is None - if not isinstance(name, str): - raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})") - - # XXX what's the better way to handle verbosity? print as we create - # each directory in the path (the current behaviour), or only announce - # the creation of the whole path? (quite easy to do the latter since - # we're not using a recursive algorithm) - - name = os.path.normpath(name) - created_dirs = [] - if os.path.isdir(name) or name == '': - return created_dirs - if os.path.abspath(name) in _path_created: - return created_dirs - - (head, tail) = os.path.split(name) - tails = [tail] # stack of lone dirs to create + ancestry = itertools.chain((name,), name.parents) + missing = (path for path in ancestry if not path.is_dir()) - while head and tail and not os.path.isdir(head): - (head, tail) = os.path.split(head) - tails.insert(0, tail) # push next higher dir onto stack + try: + dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True) + except OSError as exc: + raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}") - # now 'head' contains the deepest directory that already exists - # (that is, the child of 'head' in 'name' is the highest directory - # that does *not* exist) - for d in tails: - # print "head = %s, d = %s: " % (head, d), - head = os.path.join(head, d) - abs_head = os.path.abspath(head) + return list(map(str, missing)) - if abs_head in _path_created: - continue - if verbose >= 1: - log.info("creating %s", head) +@mkpath.register +def _(name: str, *args, **kwargs): + return mkpath(pathlib.Path(name), *args, **kwargs) - if not dry_run: - try: - os.mkdir(head, mode) - except OSError as exc: - if not (exc.errno == errno.EEXIST and os.path.isdir(head)): - raise DistutilsFileError( - f"could not create '{head}': {exc.args[-1]}" - ) - created_dirs.append(head) - _path_created.add(abs_head) - return created_dirs +@mkpath.register +def _(name: None, *args, **kwargs): + """ + Detect a common bug -- name is None. + """ + raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})") def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False): @@ -101,7 +99,7 @@ def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False): mkpath(dir, mode, verbose=verbose, dry_run=dry_run) -def copy_tree( # noqa: C901 +def copy_tree( src, dst, preserve_mode=True, @@ -130,8 +128,6 @@ def copy_tree( # noqa: C901 (the default), the destination of the symlink will be copied. 'update' and 'verbose' are the same as for 'copy_file'. """ - from distutils.file_util import copy_file - if not dry_run and not os.path.isdir(src): raise DistutilsFileError(f"cannot copy tree '{src}': not a directory") try: @@ -145,50 +141,69 @@ def copy_tree( # noqa: C901 if not dry_run: mkpath(dst, verbose=verbose) - outputs = [] - - for n in names: - src_name = os.path.join(src, n) - dst_name = os.path.join(dst, n) - - if n.startswith('.nfs'): - # skip NFS rename files - continue - - if preserve_symlinks and os.path.islink(src_name): - link_dest = os.readlink(src_name) - if verbose >= 1: - log.info("linking %s -> %s", dst_name, link_dest) - if not dry_run: - os.symlink(link_dest, dst_name) - outputs.append(dst_name) - - elif os.path.isdir(src_name): - outputs.extend( - copy_tree( - src_name, - dst_name, - preserve_mode, - preserve_times, - preserve_symlinks, - update, - verbose=verbose, - dry_run=dry_run, - ) - ) - else: - copy_file( - src_name, - dst_name, - preserve_mode, - preserve_times, - update, - verbose=verbose, - dry_run=dry_run, - ) - outputs.append(dst_name) + copy_one = functools.partial( + _copy_one, + src=src, + dst=dst, + preserve_symlinks=preserve_symlinks, + verbose=verbose, + dry_run=dry_run, + preserve_mode=preserve_mode, + preserve_times=preserve_times, + update=update, + ) + return list(itertools.chain.from_iterable(map(copy_one, names))) + + +def _copy_one( + name, + *, + src, + dst, + preserve_symlinks, + verbose, + dry_run, + preserve_mode, + preserve_times, + update, +): + src_name = os.path.join(src, name) + dst_name = os.path.join(dst, name) - return outputs + if name.startswith('.nfs'): + # skip NFS rename files + return + + if preserve_symlinks and os.path.islink(src_name): + link_dest = os.readlink(src_name) + if verbose >= 1: + log.info("linking %s -> %s", dst_name, link_dest) + if not dry_run: + os.symlink(link_dest, dst_name) + yield dst_name + + elif os.path.isdir(src_name): + yield from copy_tree( + src_name, + dst_name, + preserve_mode, + preserve_times, + preserve_symlinks, + update, + verbose=verbose, + dry_run=dry_run, + ) + else: + file_util.copy_file( + src_name, + dst_name, + preserve_mode, + preserve_times, + update, + verbose=verbose, + dry_run=dry_run, + ) + yield dst_name def _build_cmdtuple(path, cmdtuples): @@ -208,8 +223,6 @@ def remove_tree(directory, verbose=True, dry_run=False): Any errors are ignored (apart from being reported to stdout if 'verbose' is true). """ - global _path_created - if verbose >= 1: log.info("removing '%s' (and everything under it)", directory) if dry_run: @@ -219,10 +232,8 @@ def remove_tree(directory, verbose=True, dry_run=False): for cmd in cmdtuples: try: cmd[0](cmd[1]) - # remove dir from cache if it's already there - abspath = os.path.abspath(cmd[1]) - if abspath in _path_created: - _path_created.remove(abspath) + # Clear the cache + SkipRepeatAbsolutePaths.clear() except OSError as exc: log.warning("error removing %s: %s", directory, exc) diff --git a/setuptools/_distutils/errors.py b/setuptools/_distutils/errors.py index 626254c321..3196a4f097 100644 --- a/setuptools/_distutils/errors.py +++ b/setuptools/_distutils/errors.py @@ -1,12 +1,9 @@ -"""distutils.errors +""" +Exceptions used by the Distutils modules. -Provides exceptions used by the Distutils modules. Note that Distutils -modules may raise standard exceptions; in particular, SystemExit is -usually raised for errors that are obviously the end-user's fault -(eg. bad command-line arguments). - -This module is safe to use in "from ... import *" mode; it only exports -symbols whose names start with "Distutils" and end with "Error".""" +Distutils modules may raise these or standard exceptions, +including :exc:`SystemExit`. +""" class DistutilsError(Exception): diff --git a/setuptools/_distutils/sysconfig.py b/setuptools/_distutils/sysconfig.py index 28a7c571dc..da1eecbe7e 100644 --- a/setuptools/_distutils/sysconfig.py +++ b/setuptools/_distutils/sysconfig.py @@ -16,7 +16,8 @@ import sys import sysconfig -from ._functools import pass_none +from jaraco.functools import pass_none + from .compat import py39 from .errors import DistutilsPlatformError from .util import is_mingw diff --git a/setuptools/_distutils/tests/test_archive_util.py b/setuptools/_distutils/tests/test_archive_util.py index 389eba16e8..3e4ed75a76 100644 --- a/setuptools/_distutils/tests/test_archive_util.py +++ b/setuptools/_distutils/tests/test_archive_util.py @@ -6,7 +6,6 @@ import pathlib import sys import tarfile -import warnings from distutils import archive_util from distutils.archive_util import ( ARCHIVE_FORMATS, @@ -23,7 +22,6 @@ import pytest from test.support import patch -from .compat.py38 import check_warnings from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id @@ -190,37 +188,6 @@ def test_tarfile_vs_tar(self): tarball = base_name + '.tar' assert os.path.exists(tarball) - @pytest.mark.skipif("not shutil.which('compress')") - def test_compress_deprecated(self): - tmpdir = self._create_files() - base_name = os.path.join(self.mkdtemp(), 'archive') - - # using compress and testing the DeprecationWarning - old_dir = os.getcwd() - os.chdir(tmpdir) - try: - with check_warnings() as w: - warnings.simplefilter("always") - make_tarball(base_name, 'dist', compress='compress') - finally: - os.chdir(old_dir) - tarball = base_name + '.tar.Z' - assert os.path.exists(tarball) - assert len(w.warnings) == 1 - - # same test with dry_run - os.remove(tarball) - old_dir = os.getcwd() - os.chdir(tmpdir) - try: - with check_warnings() as w: - warnings.simplefilter("always") - make_tarball(base_name, 'dist', compress='compress', dry_run=True) - finally: - os.chdir(old_dir) - assert not os.path.exists(tarball) - assert len(w.warnings) == 1 - @pytest.mark.usefixtures('needs_zlib') def test_make_zipfile(self): zipfile = pytest.importorskip('zipfile') diff --git a/setuptools/_distutils/tests/test_config.py b/setuptools/_distutils/tests/test_config.py deleted file mode 100644 index be5ae0a687..0000000000 --- a/setuptools/_distutils/tests/test_config.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Tests for distutils.pypirc.pypirc.""" - -import os -from distutils.tests import support - -import pytest - -PYPIRC = """\ -[distutils] - -index-servers = - server1 - server2 - server3 - -[server1] -username:me -password:secret - -[server2] -username:meagain -password: secret -realm:acme -repository:http://another.pypi/ - -[server3] -username:cbiggles -password:yh^%#rest-of-my-password -""" - -PYPIRC_OLD = """\ -[server-login] -username:tarek -password:secret -""" - -WANTED = """\ -[distutils] -index-servers = - pypi - -[pypi] -username:tarek -password:xxx -""" - - -@support.combine_markers -@pytest.mark.usefixtures('pypirc') -class BasePyPIRCCommandTestCase(support.TempdirManager): - pass - - -class PyPIRCCommandTestCase(BasePyPIRCCommandTestCase): - def test_server_registration(self): - # This test makes sure PyPIRCCommand knows how to: - # 1. handle several sections in .pypirc - # 2. handle the old format - - # new format - self.write_file(self.rc, PYPIRC) - cmd = self._cmd(self.dist) - config = cmd._read_pypirc() - - config = list(sorted(config.items())) - waited = [ - ('password', 'secret'), - ('realm', 'pypi'), - ('repository', 'https://upload.pypi.org/legacy/'), - ('server', 'server1'), - ('username', 'me'), - ] - assert config == waited - - # old format - self.write_file(self.rc, PYPIRC_OLD) - config = cmd._read_pypirc() - config = list(sorted(config.items())) - waited = [ - ('password', 'secret'), - ('realm', 'pypi'), - ('repository', 'https://upload.pypi.org/legacy/'), - ('server', 'server-login'), - ('username', 'tarek'), - ] - assert config == waited - - def test_server_empty_registration(self): - cmd = self._cmd(self.dist) - rc = cmd._get_rc_file() - assert not os.path.exists(rc) - cmd._store_pypirc('tarek', 'xxx') - assert os.path.exists(rc) - f = open(rc) - try: - content = f.read() - assert content == WANTED - finally: - f.close() - - def test_config_interpolation(self): - # using the % character in .pypirc should not raise an error (#20120) - self.write_file(self.rc, PYPIRC) - cmd = self._cmd(self.dist) - cmd.repository = 'server3' - config = cmd._read_pypirc() - - config = list(sorted(config.items())) - waited = [ - ('password', 'yh^%#rest-of-my-password'), - ('realm', 'pypi'), - ('repository', 'https://upload.pypi.org/legacy/'), - ('server', 'server3'), - ('username', 'cbiggles'), - ] - assert config == waited diff --git a/setuptools/_distutils/tests/test_dir_util.py b/setuptools/_distutils/tests/test_dir_util.py index c00ffbbd81..12e643ab74 100644 --- a/setuptools/_distutils/tests/test_dir_util.py +++ b/setuptools/_distutils/tests/test_dir_util.py @@ -34,7 +34,7 @@ def test_mkpath_remove_tree_verbosity(self, caplog): remove_tree(self.root_target, verbose=False) mkpath(self.target, verbose=True) - wanted = [f'creating {self.root_target}', f'creating {self.target}'] + wanted = [f'creating {self.target}'] assert caplog.messages == wanted caplog.clear() diff --git a/setuptools/_distutils/tests/test_msvccompiler.py b/setuptools/_distutils/tests/test_msvccompiler.py index 71129cae27..ceb15d3a63 100644 --- a/setuptools/_distutils/tests/test_msvccompiler.py +++ b/setuptools/_distutils/tests/test_msvccompiler.py @@ -2,11 +2,13 @@ import os import sys +import sysconfig import threading import unittest.mock as mock from distutils import _msvccompiler from distutils.errors import DistutilsPlatformError from distutils.tests import support +from distutils.util import get_platform import pytest @@ -28,6 +30,30 @@ def _find_vcvarsall(plat_spec): 'wont find this version', ) + @pytest.mark.skipif( + not sysconfig.get_platform().startswith("win"), + reason="Only run test for non-mingw Windows platforms", + ) + @pytest.mark.parametrize( + "plat_name, expected", + [ + ("win-arm64", "win-arm64"), + ("win-amd64", "win-amd64"), + (None, get_platform()), + ], + ) + def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected): + """ + Ensure a specified target platform is passed to _get_vcvars_spec. + """ + compiler = _msvccompiler.MSVCCompiler() + + def _get_vcvars_spec(host_platform, platform): + assert platform == expected + + monkeypatch.setattr(_msvccompiler, '_get_vcvars_spec', _get_vcvars_spec) + compiler.initialize(plat_name) + @needs_winreg def test_get_vc_env_unicode(self): test_var = 'ṰḖṤṪ┅ṼẨṜ' diff --git a/setuptools/_distutils/tests/test_register.py b/setuptools/_distutils/tests/test_register.py deleted file mode 100644 index 14dfb832c7..0000000000 --- a/setuptools/_distutils/tests/test_register.py +++ /dev/null @@ -1,314 +0,0 @@ -"""Tests for distutils.command.register.""" - -import getpass -import os -import pathlib -import urllib -from distutils.command import register as register_module -from distutils.command.register import register -from distutils.errors import DistutilsSetupError -from distutils.tests.test_config import BasePyPIRCCommandTestCase - -import pytest - -try: - import docutils -except ImportError: - docutils = None - -PYPIRC_NOPASSWORD = """\ -[distutils] - -index-servers = - server1 - -[server1] -username:me -""" - -WANTED_PYPIRC = """\ -[distutils] -index-servers = - pypi - -[pypi] -username:tarek -password:password -""" - - -class Inputs: - """Fakes user inputs.""" - - def __init__(self, *answers): - self.answers = answers - self.index = 0 - - def __call__(self, prompt=''): - try: - return self.answers[self.index] - finally: - self.index += 1 - - -class FakeOpener: - """Fakes a PyPI server""" - - def __init__(self): - self.reqs = [] - - def __call__(self, *args): - return self - - def open(self, req, data=None, timeout=None): - self.reqs.append(req) - return self - - def read(self): - return b'xxx' - - def getheader(self, name, default=None): - return { - 'content-type': 'text/plain; charset=utf-8', - }.get(name.lower(), default) - - -@pytest.fixture(autouse=True) -def autopass(monkeypatch): - monkeypatch.setattr(getpass, 'getpass', lambda prompt: 'password') - - -@pytest.fixture(autouse=True) -def fake_opener(monkeypatch, request): - opener = FakeOpener() - monkeypatch.setattr(urllib.request, 'build_opener', opener) - monkeypatch.setattr(urllib.request, '_opener', None) - request.instance.conn = opener - - -class TestRegister(BasePyPIRCCommandTestCase): - def _get_cmd(self, metadata=None): - if metadata is None: - metadata = { - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'long_description': 'xxx', - } - pkg_info, dist = self.create_dist(**metadata) - return register(dist) - - def test_create_pypirc(self): - # this test makes sure a .pypirc file - # is created when requested. - - # let's create a register instance - cmd = self._get_cmd() - - # we shouldn't have a .pypirc file yet - assert not os.path.exists(self.rc) - - # patching input and getpass.getpass - # so register gets happy - # - # Here's what we are faking : - # use your existing login (choice 1.) - # Username : 'tarek' - # Password : 'password' - # Save your login (y/N)? : 'y' - inputs = Inputs('1', 'tarek', 'y') - register_module.input = inputs.__call__ - # let's run the command - try: - cmd.run() - finally: - del register_module.input - - # A new .pypirc file should contain WANTED_PYPIRC - assert pathlib.Path(self.rc).read_text(encoding='utf-8') == WANTED_PYPIRC - - # now let's make sure the .pypirc file generated - # really works : we shouldn't be asked anything - # if we run the command again - def _no_way(prompt=''): - raise AssertionError(prompt) - - register_module.input = _no_way - - cmd.show_response = True - cmd.run() - - # let's see what the server received : we should - # have 2 similar requests - assert len(self.conn.reqs) == 2 - req1 = dict(self.conn.reqs[0].headers) - req2 = dict(self.conn.reqs[1].headers) - - assert req1['Content-length'] == '1358' - assert req2['Content-length'] == '1358' - assert b'xxx' in self.conn.reqs[1].data - - def test_password_not_in_file(self): - self.write_file(self.rc, PYPIRC_NOPASSWORD) - cmd = self._get_cmd() - cmd._set_config() - cmd.finalize_options() - cmd.send_metadata() - - # dist.password should be set - # therefore used afterwards by other commands - assert cmd.distribution.password == 'password' - - def test_registering(self): - # this test runs choice 2 - cmd = self._get_cmd() - inputs = Inputs('2', 'tarek', 'tarek@ziade.org') - register_module.input = inputs.__call__ - try: - # let's run the command - cmd.run() - finally: - del register_module.input - - # we should have send a request - assert len(self.conn.reqs) == 1 - req = self.conn.reqs[0] - headers = dict(req.headers) - assert headers['Content-length'] == '608' - assert b'tarek' in req.data - - def test_password_reset(self): - # this test runs choice 3 - cmd = self._get_cmd() - inputs = Inputs('3', 'tarek@ziade.org') - register_module.input = inputs.__call__ - try: - # let's run the command - cmd.run() - finally: - del register_module.input - - # we should have send a request - assert len(self.conn.reqs) == 1 - req = self.conn.reqs[0] - headers = dict(req.headers) - assert headers['Content-length'] == '290' - assert b'tarek' in req.data - - def test_strict(self): - # testing the strict option - # when on, the register command stops if - # the metadata is incomplete or if - # long_description is not reSt compliant - - pytest.importorskip('docutils') - - # empty metadata - cmd = self._get_cmd({}) - cmd.ensure_finalized() - cmd.strict = True - with pytest.raises(DistutilsSetupError): - cmd.run() - - # metadata are OK but long_description is broken - metadata = { - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'éxéxé', - 'name': 'xxx', - 'version': 'xxx', - 'long_description': 'title\n==\n\ntext', - } - - cmd = self._get_cmd(metadata) - cmd.ensure_finalized() - cmd.strict = True - with pytest.raises(DistutilsSetupError): - cmd.run() - - # now something that works - metadata['long_description'] = 'title\n=====\n\ntext' - cmd = self._get_cmd(metadata) - cmd.ensure_finalized() - cmd.strict = True - inputs = Inputs('1', 'tarek', 'y') - register_module.input = inputs.__call__ - # let's run the command - try: - cmd.run() - finally: - del register_module.input - - # strict is not by default - cmd = self._get_cmd() - cmd.ensure_finalized() - inputs = Inputs('1', 'tarek', 'y') - register_module.input = inputs.__call__ - # let's run the command - try: - cmd.run() - finally: - del register_module.input - - # and finally a Unicode test (bug #12114) - metadata = { - 'url': 'xxx', - 'author': '\u00c9ric', - 'author_email': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'description': 'Something about esszet \u00df', - 'long_description': 'More things about esszet \u00df', - } - - cmd = self._get_cmd(metadata) - cmd.ensure_finalized() - cmd.strict = True - inputs = Inputs('1', 'tarek', 'y') - register_module.input = inputs.__call__ - # let's run the command - try: - cmd.run() - finally: - del register_module.input - - def test_register_invalid_long_description(self, monkeypatch): - pytest.importorskip('docutils') - description = ':funkie:`str`' # mimic Sphinx-specific markup - metadata = { - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'long_description': description, - } - cmd = self._get_cmd(metadata) - cmd.ensure_finalized() - cmd.strict = True - inputs = Inputs('2', 'tarek', 'tarek@ziade.org') - monkeypatch.setattr(register_module, 'input', inputs, raising=False) - - with pytest.raises(DistutilsSetupError): - cmd.run() - - def test_list_classifiers(self, caplog): - cmd = self._get_cmd() - cmd.list_classifiers = True - cmd.run() - assert caplog.messages == ['running check', 'xxx'] - - def test_show_response(self, caplog): - # test that the --show-response option return a well formatted response - cmd = self._get_cmd() - inputs = Inputs('1', 'tarek', 'y') - register_module.input = inputs.__call__ - cmd.show_response = True - try: - cmd.run() - finally: - del register_module.input - - assert caplog.messages[3] == 75 * '-' + '\nxxx\n' + 75 * '-' diff --git a/setuptools/_distutils/tests/test_sdist.py b/setuptools/_distutils/tests/test_sdist.py index 7daaaa6323..5aca43e34f 100644 --- a/setuptools/_distutils/tests/test_sdist.py +++ b/setuptools/_distutils/tests/test_sdist.py @@ -4,14 +4,12 @@ import pathlib import shutil # noqa: F401 import tarfile -import warnings import zipfile from distutils.archive_util import ARCHIVE_FORMATS from distutils.command.sdist import sdist, show_formats from distutils.core import Distribution from distutils.errors import DistutilsOptionError from distutils.filelist import FileList -from distutils.tests.test_config import BasePyPIRCCommandTestCase from os.path import join from textwrap import dedent @@ -20,7 +18,7 @@ import pytest from more_itertools import ilen -from .compat.py38 import check_warnings +from . import support from .unix_compat import grp, pwd, require_uid_0, require_unix_id SETUP_PY = """ @@ -47,8 +45,9 @@ @pytest.fixture(autouse=True) -def project_dir(request, pypirc): +def project_dir(request, distutils_managed_tempdir): self = request.instance + self.tmp_dir = self.mkdtemp() jaraco.path.build( { 'somecode': { @@ -68,7 +67,7 @@ def clean_lines(filepath): yield from filter(None, map(str.strip, f)) -class TestSDist(BasePyPIRCCommandTestCase): +class TestSDist(support.TempdirManager): def get_cmd(self, metadata=None): """Returns a cmd""" if metadata is None: @@ -275,14 +274,6 @@ def test_metadata_check_option(self, caplog): cmd.run() assert len(self.warnings(caplog.messages, 'warning: check: ')) == 0 - def test_check_metadata_deprecated(self): - # makes sure make_metadata is deprecated - dist, cmd = self.get_cmd() - with check_warnings() as w: - warnings.simplefilter("always") - cmd.check_metadata() - assert len(w.warnings) == 1 - def test_show_formats(self, capsys): show_formats() diff --git a/setuptools/_distutils/tests/test_upload.py b/setuptools/_distutils/tests/test_upload.py deleted file mode 100644 index 56df209c73..0000000000 --- a/setuptools/_distutils/tests/test_upload.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Tests for distutils.command.upload.""" - -import os -import unittest.mock as mock -from distutils.command import upload as upload_mod -from distutils.command.upload import upload -from distutils.core import Distribution -from distutils.errors import DistutilsError -from distutils.tests.test_config import PYPIRC, BasePyPIRCCommandTestCase -from urllib.request import HTTPError - -import pytest - -PYPIRC_LONG_PASSWORD = """\ -[distutils] - -index-servers = - server1 - server2 - -[server1] -username:me -password:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - -[server2] -username:meagain -password: secret -realm:acme -repository:http://another.pypi/ -""" - - -PYPIRC_NOPASSWORD = """\ -[distutils] - -index-servers = - server1 - -[server1] -username:me -""" - - -class FakeOpen: - def __init__(self, url, msg=None, code=None): - self.url = url - if not isinstance(url, str): - self.req = url - else: - self.req = None - self.msg = msg or 'OK' - self.code = code or 200 - - def getheader(self, name, default=None): - return { - 'content-type': 'text/plain; charset=utf-8', - }.get(name.lower(), default) - - def read(self): - return b'xyzzy' - - def getcode(self): - return self.code - - -@pytest.fixture(autouse=True) -def urlopen(request, monkeypatch): - self = request.instance - monkeypatch.setattr(upload_mod, 'urlopen', self._urlopen) - self.next_msg = self.next_code = None - - -class TestUpload(BasePyPIRCCommandTestCase): - def _urlopen(self, url): - self.last_open = FakeOpen(url, msg=self.next_msg, code=self.next_code) - return self.last_open - - def test_finalize_options(self): - # new format - self.write_file(self.rc, PYPIRC) - dist = Distribution() - cmd = upload(dist) - cmd.finalize_options() - for attr, waited in ( - ('username', 'me'), - ('password', 'secret'), - ('realm', 'pypi'), - ('repository', 'https://upload.pypi.org/legacy/'), - ): - assert getattr(cmd, attr) == waited - - def test_saved_password(self): - # file with no password - self.write_file(self.rc, PYPIRC_NOPASSWORD) - - # make sure it passes - dist = Distribution() - cmd = upload(dist) - cmd.finalize_options() - assert cmd.password is None - - # make sure we get it as well, if another command - # initialized it at the dist level - dist.password = 'xxx' - cmd = upload(dist) - cmd.finalize_options() - assert cmd.password == 'xxx' - - def test_upload(self, caplog): - tmp = self.mkdtemp() - path = os.path.join(tmp, 'xxx') - self.write_file(path) - command, pyversion, filename = 'xxx', '2.6', path - dist_files = [(command, pyversion, filename)] - self.write_file(self.rc, PYPIRC_LONG_PASSWORD) - - # lets run it - pkg_dir, dist = self.create_dist(dist_files=dist_files) - cmd = upload(dist) - cmd.show_response = True - cmd.ensure_finalized() - cmd.run() - - # what did we send ? - headers = dict(self.last_open.req.headers) - assert int(headers['Content-length']) >= 2162 - content_type = headers['Content-type'] - assert content_type.startswith('multipart/form-data') - assert self.last_open.req.get_method() == 'POST' - expected_url = 'https://upload.pypi.org/legacy/' - assert self.last_open.req.get_full_url() == expected_url - data = self.last_open.req.data - assert b'xxx' in data - assert b'protocol_version' in data - assert b'sha256_digest' in data - assert ( - b'cd2eb0837c9b4c962c22d2ff8b5441b7b45805887f051d39bf133b583baf' - b'6860' in data - ) - if b'md5_digest' in data: - assert b'f561aaf6ef0bf14d4208bb46a4ccb3ad' in data - if b'blake2_256_digest' in data: - assert ( - b'b6f289a27d4fe90da63c503bfe0a9b761a8f76bb86148565065f040be' - b'6d1c3044cf7ded78ef800509bccb4b648e507d88dc6383d67642aadcc' - b'ce443f1534330a' in data - ) - - # The PyPI response body was echoed - results = caplog.messages - assert results[-1] == 75 * '-' + '\nxyzzy\n' + 75 * '-' - - # bpo-32304: archives whose last byte was b'\r' were corrupted due to - # normalization intended for Mac OS 9. - def test_upload_correct_cr(self): - # content that ends with \r should not be modified. - tmp = self.mkdtemp() - path = os.path.join(tmp, 'xxx') - self.write_file(path, content='yy\r') - command, pyversion, filename = 'xxx', '2.6', path - dist_files = [(command, pyversion, filename)] - self.write_file(self.rc, PYPIRC_LONG_PASSWORD) - - # other fields that ended with \r used to be modified, now are - # preserved. - pkg_dir, dist = self.create_dist( - dist_files=dist_files, description='long description\r' - ) - cmd = upload(dist) - cmd.show_response = True - cmd.ensure_finalized() - cmd.run() - - headers = dict(self.last_open.req.headers) - assert int(headers['Content-length']) >= 2172 - assert b'long description\r' in self.last_open.req.data - - def test_upload_fails(self, caplog): - self.next_msg = "Not Found" - self.next_code = 404 - with pytest.raises(DistutilsError): - self.test_upload(caplog) - - @pytest.mark.parametrize( - 'exception,expected,raised_exception', - [ - (OSError('oserror'), 'oserror', OSError), - pytest.param( - HTTPError('url', 400, 'httperror', {}, None), - 'Upload failed (400): httperror', - DistutilsError, - id="HTTP 400", - ), - ], - ) - def test_wrong_exception_order(self, exception, expected, raised_exception, caplog): - tmp = self.mkdtemp() - path = os.path.join(tmp, 'xxx') - self.write_file(path) - dist_files = [('xxx', '2.6', path)] # command, pyversion, filename - self.write_file(self.rc, PYPIRC_LONG_PASSWORD) - - pkg_dir, dist = self.create_dist(dist_files=dist_files) - - with mock.patch( - 'distutils.command.upload.urlopen', - new=mock.Mock(side_effect=exception), - ): - with pytest.raises(raised_exception): - cmd = upload(dist) - cmd.ensure_finalized() - cmd.run() - results = caplog.messages - assert expected in results[-1] - caplog.clear() diff --git a/setuptools/_distutils/util.py b/setuptools/_distutils/util.py index 4cc6bd283c..609c1a50cd 100644 --- a/setuptools/_distutils/util.py +++ b/setuptools/_distutils/util.py @@ -17,7 +17,8 @@ import sysconfig import tempfile -from ._functools import pass_none +from jaraco.functools import pass_none + from ._log import log from ._modified import newer from .errors import DistutilsByteCompileError, DistutilsPlatformError