From 340137fa4cdd2f7945bcaa7aa0aa1a6600a8d11a Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:14:15 +0300 Subject: [PATCH 01/24] Ignore TRY400 globally While the suggestion might be valid in some cases, it doesn't fit all use cases and ends up being a nuisance. --- distutils/_msvccompiler.py | 2 +- ruff.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 03653929a8..e7652218d8 100644 --- a/distutils/_msvccompiler.py +++ b/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/ruff.toml b/ruff.toml index 664d894a86..370e9cae49 100644 --- a/ruff.toml +++ b/ruff.toml @@ -39,6 +39,7 @@ ignore = [ # local "B028", "B904", + "TRY400", ] [format] From aba9eefa8273a86346256dae5b2f565427dc1cab Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:15:58 +0300 Subject: [PATCH 02/24] Clean up ruff ignore --- ruff.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index 370e9cae49..730732b197 100644 --- a/ruff.toml +++ b/ruff.toml @@ -16,11 +16,7 @@ extend-select = [ "YTT", ] ignore = [ - # local - "PERF203", - "TRY003", - - # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", @@ -39,6 +35,8 @@ ignore = [ # local "B028", "B904", + "PERF203", + "TRY003", "TRY400", ] From 8e8f0cbcc4060cbd7668f27cfb23b5d78eabd3d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 09:51:07 -0400 Subject: [PATCH 03/24] Clean up cruft in errors docstring. --- distutils/errors.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/distutils/errors.py b/distutils/errors.py index 626254c321..3196a4f097 100644 --- a/distutils/errors.py +++ b/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): From cded66c0ef65488cc1c7b0ccc762945e53f67341 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 10:11:08 -0400 Subject: [PATCH 04/24] Rely on dependencies instead of vendoring them. --- distutils/_collections.py | 58 ---------------------------- distutils/_functools.py | 73 ----------------------------------- distutils/_itertools.py | 52 ------------------------- distutils/_modified.py | 3 +- distutils/ccompiler.py | 3 +- distutils/command/install.py | 5 ++- distutils/command/register.py | 3 +- distutils/command/upload.py | 3 +- distutils/sysconfig.py | 3 +- distutils/util.py | 3 +- pyproject.toml | 3 ++ 11 files changed, 18 insertions(+), 191 deletions(-) delete mode 100644 distutils/_collections.py delete mode 100644 distutils/_functools.py delete mode 100644 distutils/_itertools.py diff --git a/distutils/_collections.py b/distutils/_collections.py deleted file mode 100644 index 863030b3cf..0000000000 --- a/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/distutils/_functools.py b/distutils/_functools.py deleted file mode 100644 index e03365eafa..0000000000 --- a/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/distutils/_itertools.py b/distutils/_itertools.py deleted file mode 100644 index 85b2951186..0000000000 --- a/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/distutils/_modified.py b/distutils/_modified.py index b7bdaa2943..7cdca9398f 100644 --- a/distutils/_modified.py +++ b/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/distutils/ccompiler.py b/distutils/ccompiler.py index bc4743bcbf..5e73e56d02 100644 --- a/distutils/ccompiler.py +++ b/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/distutils/command/install.py b/distutils/command/install.py index b83e061e02..ceb453e041 100644 --- a/distutils/command/install.py +++ b/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/distutils/command/register.py b/distutils/command/register.py index c1acd27b54..1089daf78f 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -13,7 +13,8 @@ from distutils._log import log from warnings import warn -from .._itertools import always_iterable +from more_itertools import always_iterable + from ..core import PyPIRCCommand diff --git a/distutils/command/upload.py b/distutils/command/upload.py index a2461e089f..5717eef1fa 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -13,7 +13,8 @@ from urllib.parse import urlparse from urllib.request import HTTPError, Request, urlopen -from .._itertools import always_iterable +from more_itertools import always_iterable + from ..core import PyPIRCCommand from ..errors import DistutilsError, DistutilsOptionError from ..spawn import spawn diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 28a7c571dc..da1eecbe7e 100644 --- a/distutils/sysconfig.py +++ b/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/distutils/util.py b/distutils/util.py index 4cc6bd283c..609c1a50cd 100644 --- a/distutils/util.py +++ b/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 diff --git a/pyproject.toml b/pyproject.toml index 068902265f..9f528752ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ requires-python = ">=3.8" dependencies = [ # Setuptools must require these "packaging", + "jaraco.functools", + "more_itertools", + "jaraco.collections", ] dynamic = ["version"] From 10078fe2b139c424f034f0f817d9695c899cd9f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 10:27:16 -0400 Subject: [PATCH 05/24] Simply log the directory being created, rather than the whole ancestry. --- distutils/dir_util.py | 14 +++++--------- distutils/tests/test_dir_util.py | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 724afeff6f..dfe0613429 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -20,7 +20,7 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 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: @@ -36,12 +36,11 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 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) + + if verbose and not os.path.isdir(name): + log.info("creating %s", name) + created_dirs = [] if os.path.isdir(name) or name == '': return created_dirs @@ -66,9 +65,6 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 if abs_head in _path_created: continue - if verbose >= 1: - log.info("creating %s", head) - if not dry_run: try: os.mkdir(head, mode) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index c00ffbbd81..12e643ab74 100644 --- a/distutils/tests/test_dir_util.py +++ b/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() From 45b1295d9717b7a6498b092f2272d754cd86203a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 13:36:55 -0400 Subject: [PATCH 06/24] Updated mkpath to use pathlib.Path.mkdir. --- distutils/dir_util.py | 60 ++++++++++++------------------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index dfe0613429..a9ee842aac 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -2,8 +2,9 @@ Utility functions for manipulating directories and directory trees.""" -import errno +import itertools import os +import pathlib from ._log import log from .errors import DistutilsFileError, DistutilsInternalError @@ -13,7 +14,7 @@ _path_created = set() -def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 +def mkpath(name, 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 @@ -22,12 +23,6 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 (eg. some sub-path exists, but is a file rather than a directory). 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). """ global _path_created @@ -36,47 +31,24 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): # noqa: C901 if not isinstance(name, str): raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})") - name = os.path.normpath(name) - - if verbose and not os.path.isdir(name): - log.info("creating %s", name) - - created_dirs = [] - if os.path.isdir(name) or name == '': - return created_dirs - if os.path.abspath(name) in _path_created: - return created_dirs + name = pathlib.Path(name) - (head, tail) = os.path.split(name) - tails = [tail] # stack of lone dirs to create + if str(name.absolute()) in _path_created: + return - 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 + if verbose and not name.is_dir(): + log.info("creating %s", name) - # 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) + ancestry = itertools.chain((name,), name.parents) + missing = (path for path in ancestry if not path.is_dir()) - if abs_head in _path_created: - continue + try: + dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True) + _path_created.add(name.absolute()) + except OSError as exc: + raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}") - 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 + return list(map(str, missing)) def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False): From 284279d7eed2466ae933cc61ac1cacc5b9c44527 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 14:20:08 -0400 Subject: [PATCH 07/24] Refactored mkpath as a singledispatch function. --- distutils/dir_util.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index a9ee842aac..ab848240ea 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -2,6 +2,7 @@ Utility functions for manipulating directories and directory trees.""" +import functools import itertools import os import pathlib @@ -14,7 +15,8 @@ _path_created = set() -def mkpath(name, mode=0o777, verbose=True, dry_run=False): +@functools.singledispatch +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 @@ -27,12 +29,6 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): 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})") - - name = pathlib.Path(name) - if str(name.absolute()) in _path_created: return @@ -51,6 +47,19 @@ def mkpath(name, mode=0o777, verbose=True, dry_run=False): return list(map(str, missing)) +@mkpath.register +def _(name: str, *args, **kwargs): + return mkpath(pathlib.Path(name), *args, **kwargs) + + +@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): """Create all the empty directories under 'base_dir' needed to put 'files' there. From f2157f50101906b300108e953bd4ccff26be9e2e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 14:31:45 -0400 Subject: [PATCH 08/24] Replaced global variable with a custom cache wrapper. --- distutils/dir_util.py | 48 ++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index ab848240ea..1705cd050d 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -10,12 +10,39 @@ 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 + + +# 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. @@ -26,12 +53,6 @@ def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False): If 'verbose' is true, log the directory created. Return the list of directories actually created. """ - - global _path_created - - if str(name.absolute()) in _path_created: - return - if verbose and not name.is_dir(): log.info("creating %s", name) @@ -40,7 +61,6 @@ def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False): try: dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True) - _path_created.add(name.absolute()) except OSError as exc: raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}") @@ -185,8 +205,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: @@ -196,10 +214,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) From e2ab6012da6b0dcf895e95f219327f489db6e04c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 17:41:40 -0400 Subject: [PATCH 09/24] Extracted _copy_one function. --- distutils/dir_util.py | 112 ++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 1705cd050d..3b22839d27 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -7,6 +7,7 @@ import os import pathlib +from . import file_util from ._log import log from .errors import DistutilsFileError, DistutilsInternalError @@ -98,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, @@ -127,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: @@ -142,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) - - return outputs + 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) + + 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): From bcba955249ae22b9cadab363a300fb7a828f0be8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 17:58:01 -0400 Subject: [PATCH 10/24] "Removed support for 'compress="compress"' to archive_util.make_tarball." --- distutils/archive_util.py | 26 ++++------------------ distutils/tests/test_archive_util.py | 33 ---------------------------- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/distutils/archive_util.py b/distutils/archive_util.py index cc4699b1a3..5bb6df763d 100644 --- a/distutils/archive_util.py +++ b/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/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 389eba16e8..3e4ed75a76 100644 --- a/distutils/tests/test_archive_util.py +++ b/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') From 3d38185e82baf307ea6a1f815d906a29e63fa89e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:05:58 -0400 Subject: [PATCH 11/24] Removed deprecated 'check_warnings' from 'sdist' and 'regitser' commands. --- distutils/command/register.py | 14 -------------- distutils/command/sdist.py | 12 ------------ distutils/tests/test_sdist.py | 10 ---------- 3 files changed, 36 deletions(-) diff --git a/distutils/command/register.py b/distutils/command/register.py index 1089daf78f..9645401fd7 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -11,7 +11,6 @@ import urllib.parse import urllib.request from distutils._log import log -from warnings import warn from more_itertools import always_iterable @@ -65,19 +64,6 @@ def run(self): 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() diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index eda6afe811..d723a1c9fb 100644 --- a/distutils/command/sdist.py +++ b/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/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 7daaaa6323..c49a4bfc7a 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -4,7 +4,6 @@ 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 @@ -20,7 +19,6 @@ import pytest from more_itertools import ilen -from .compat.py38 import check_warnings from .unix_compat import grp, pwd, require_uid_0, require_unix_id SETUP_PY = """ @@ -275,14 +273,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() From 05668ed2e1c9013259a62b34247d2b318dd949f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:23:33 -0400 Subject: [PATCH 12/24] Enabled TestPyPIRCCommand to run its tests. --- distutils/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index be5ae0a687..ee6d7604de 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -51,7 +51,7 @@ class BasePyPIRCCommandTestCase(support.TempdirManager): pass -class PyPIRCCommandTestCase(BasePyPIRCCommandTestCase): +class TestPyPIRCCommand(BasePyPIRCCommandTestCase): def test_server_registration(self): # This test makes sure PyPIRCCommand knows how to: # 1. handle several sections in .pypirc From eebb1212effa7a60b5a6cded5072013be43d3801 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:34:02 -0400 Subject: [PATCH 13/24] Removed dependence of TestSdist on test_config. --- distutils/tests/test_sdist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index c49a4bfc7a..9b0930d757 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -10,7 +10,6 @@ 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 @@ -19,6 +18,7 @@ import pytest from more_itertools import ilen +from . import support from .unix_compat import grp, pwd, require_uid_0, require_unix_id SETUP_PY = """ @@ -66,7 +66,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: From dc7e30936b05f3fb4935c0db5064160b077c6216 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:35:44 -0400 Subject: [PATCH 14/24] Replaced open/read/close logic with 'read_text' and added encoding to address EncodingWarning. --- distutils/tests/test_config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index ee6d7604de..6e3f5f24dd 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -1,6 +1,7 @@ """Tests for distutils.pypirc.pypirc.""" import os +import pathlib from distutils.tests import support import pytest @@ -91,12 +92,7 @@ def test_server_empty_registration(self): 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() + assert pathlib.Path(rc).read_text(encoding='utf-8') == WANTED def test_config_interpolation(self): # using the % character in .pypirc should not raise an error (#20120) From c2c1f323ac50de697c5274aff5a027e6d2f6db53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:38:08 -0400 Subject: [PATCH 15/24] Remove collect_ignore referring to msvc9compiler. --- conftest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/conftest.py b/conftest.py index 930a595274..9667879d24 100644 --- a/conftest.py +++ b/conftest.py @@ -7,14 +7,6 @@ import path import pytest -collect_ignore = [] - - -if platform.system() != 'Windows': - collect_ignore.extend([ - 'distutils/msvc9compiler.py', - ]) - @pytest.fixture def save_env(): From ddc15558b168fbb8394df643eab427625863dad5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Sep 2024 18:44:10 -0400 Subject: [PATCH 16/24] Flagged register and upload commands and deprecated. --- distutils/command/register.py | 2 ++ distutils/command/upload.py | 2 ++ pytest.ini | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/distutils/command/register.py b/distutils/command/register.py index 9645401fd7..b3373a3c21 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -10,6 +10,7 @@ import logging import urllib.parse import urllib.request +import warnings from distutils._log import log from more_itertools import always_iterable @@ -50,6 +51,7 @@ def finalize_options(self): self.distribution.command_options['check'] = check_options def run(self): + warnings.warn("register command is deprecated. Do not use.") self.finalize_options() self._set_config() diff --git a/distutils/command/upload.py b/distutils/command/upload.py index 5717eef1fa..3428a6b613 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -9,6 +9,7 @@ import io import logging import os +import warnings from base64 import standard_b64encode from urllib.parse import urlparse from urllib.request import HTTPError, Request, urlopen @@ -63,6 +64,7 @@ def finalize_options(self): self.password = self.distribution.password def run(self): + warnings.warn("upload command is deprecated. Do not use.") if not self.distribution.dist_files: msg = ( "Must create and upload files in one command " diff --git a/pytest.ini b/pytest.ini index dd57c6ef4e..0efd9e4199 100644 --- a/pytest.ini +++ b/pytest.ini @@ -44,3 +44,7 @@ filterwarnings= # https://sourceforge.net/p/docutils/bugs/490/ ignore:'encoding' argument not specified::docutils.io ignore:UTF-8 Mode affects locale.getpreferredencoding()::docutils.io + + # suppress known deprecation + ignore:register command is deprecated + ignore:upload command is deprecated From 6ce5426cfa104012a47ee2cf94f905f86d647304 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 6 Sep 2024 11:45:39 -0400 Subject: [PATCH 17/24] Decouple sdist tests from pypirc fixture. --- distutils/tests/test_sdist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 9b0930d757..5aca43e34f 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -45,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': { From 2e6f25c51909bd646975326360ebabac7dfaf476 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 6 Sep 2024 11:50:19 -0400 Subject: [PATCH 18/24] Removed PyPI commands (register, upload) and supporting logic. --- conftest.py | 23 --- distutils/command/__init__.py | 2 - distutils/command/register.py | 311 ------------------------------ distutils/command/upload.py | 211 --------------------- distutils/config.py | 151 --------------- distutils/core.py | 3 +- distutils/tests/test_config.py | 112 ----------- distutils/tests/test_register.py | 314 ------------------------------- distutils/tests/test_upload.py | 215 --------------------- 9 files changed, 1 insertion(+), 1341 deletions(-) delete mode 100644 distutils/command/register.py delete mode 100644 distutils/command/upload.py delete mode 100644 distutils/config.py delete mode 100644 distutils/tests/test_config.py delete mode 100644 distutils/tests/test_register.py delete mode 100644 distutils/tests/test_upload.py diff --git a/conftest.py b/conftest.py index 9667879d24..98f98d41ab 100644 --- a/conftest.py +++ b/conftest.py @@ -82,29 +82,6 @@ def temp_cwd(tmp_path): yield -@pytest.fixture -def pypirc(request, save_env, distutils_managed_tempdir): - from distutils.core import Distribution, PyPIRCCommand - - self = request.instance - self.tmp_dir = self.mkdtemp() - os.environ['HOME'] = self.tmp_dir - os.environ['USERPROFILE'] = self.tmp_dir - self.rc = os.path.join(self.tmp_dir, '.pypirc') - self.dist = Distribution() - - class command(PyPIRCCommand): - def __init__(self, dist): - super().__init__(dist) - - def initialize_options(self): - pass - - finalize_options = initialize_options - - self._cmd = command - - # from pytest-dev/pytest#363 @pytest.fixture(scope="session") def monkeysession(request): diff --git a/distutils/command/__init__.py b/distutils/command/__init__.py index 1e8fbe60c2..0f8a1692ba 100644 --- a/distutils/command/__init__.py +++ b/distutils/command/__init__.py @@ -16,10 +16,8 @@ 'install_scripts', 'install_data', 'sdist', - 'register', 'bdist', 'bdist_dumb', 'bdist_rpm', 'check', - 'upload', ] diff --git a/distutils/command/register.py b/distutils/command/register.py deleted file mode 100644 index b3373a3c21..0000000000 --- a/distutils/command/register.py +++ /dev/null @@ -1,311 +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 -import warnings -from distutils._log import log - -from more_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): - warnings.warn("register command is deprecated. Do not use.") - 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 _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/distutils/command/upload.py b/distutils/command/upload.py deleted file mode 100644 index 3428a6b613..0000000000 --- a/distutils/command/upload.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -distutils.command.upload - -Implements the Distutils 'upload' subcommand (upload package to a package -index). -""" - -import hashlib -import io -import logging -import os -import warnings -from base64 import standard_b64encode -from urllib.parse import urlparse -from urllib.request import HTTPError, Request, urlopen - -from more_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): - warnings.warn("upload command is deprecated. Do not use.") - 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/distutils/config.py b/distutils/config.py deleted file mode 100644 index ebd2e11da3..0000000000 --- a/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/distutils/core.py b/distutils/core.py index 82113c47c1..bc06091abb 100644 --- a/distutils/core.py +++ b/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/distutils/tests/test_config.py b/distutils/tests/test_config.py deleted file mode 100644 index 6e3f5f24dd..0000000000 --- a/distutils/tests/test_config.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Tests for distutils.pypirc.pypirc.""" - -import os -import pathlib -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 TestPyPIRCCommand(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) - assert pathlib.Path(rc).read_text(encoding='utf-8') == WANTED - - 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/distutils/tests/test_register.py b/distutils/tests/test_register.py deleted file mode 100644 index 14dfb832c7..0000000000 --- a/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/distutils/tests/test_upload.py b/distutils/tests/test_upload.py deleted file mode 100644 index 56df209c73..0000000000 --- a/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() From bddd2591d35cf59b2a0d54156b195c334ab7dc8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 6 Sep 2024 12:49:42 -0400 Subject: [PATCH 19/24] Remove borland compiler. --- distutils/bcppcompiler.py | 396 -------------------------------------- 1 file changed, 396 deletions(-) delete mode 100644 distutils/bcppcompiler.py diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py deleted file mode 100644 index 9157b43328..0000000000 --- a/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() From 4ecf617a4b4482186570b295acd0db593fa24e10 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Thu, 11 Jul 2024 20:43:06 +0530 Subject: [PATCH 20/24] cygwinccompiler: Get the compilers from sysconfig On the CLANG64 environment of MSYS2, we should use clang instead of gcc. This patch gets the compiler from sysconfig which would be set when Python was built. Without this patch, the build fails when it's trying to check if the compiler is cygwin's one. --- distutils/cygwinccompiler.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 18b1b3557b..3c67524e6d 100644 --- a/distutils/cygwinccompiler.py +++ b/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 From a0339c1ef84032b47997fdcd503332a52ffd88bf Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 14 Sep 2024 19:39:30 -0400 Subject: [PATCH 21/24] Fix cross-platform compilation using `distutils._msvccompiler.MSVCCompiler` Actually use the `plat_name` param in `MSVCCompiler.initialize` Added tests --- distutils/_msvccompiler.py | 2 +- distutils/tests/test_msvccompiler.py | 24 ++++++++++++++++++++++++ newsfragments/298.bugfix.rst | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 newsfragments/298.bugfix.rst diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index e7652218d8..97b067c686 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -284,7 +284,7 @@ def initialize(self, plat_name=None): f"--plat-name must be one of {tuple(_vcvars_names)}" ) - plat_spec = _get_vcvars_spec(get_host_platform(), get_platform()) + plat_spec = _get_vcvars_spec(get_host_platform(), plat_name) vc_env = _get_vc_env(plat_spec) if not vc_env: diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index 71129cae27..028dcdf5cc 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/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,28 @@ 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): + compiler = _msvccompiler.MSVCCompiler() + + # makes sure that the right target platform name is used + 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/newsfragments/298.bugfix.rst b/newsfragments/298.bugfix.rst new file mode 100644 index 0000000000..eeb10466bf --- /dev/null +++ b/newsfragments/298.bugfix.rst @@ -0,0 +1 @@ +Fix cross-platform compilation using `distutils._msvccompiler.MSVCCompiler` -- by :user:`saschanaz` and :user:`Avasam` From d38d1a3e241f3c9a07dad4d1b70f5e1350b7b64e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Sep 2024 10:26:46 -0400 Subject: [PATCH 22/24] Move the comment to the docstring. --- distutils/tests/test_msvccompiler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index 028dcdf5cc..ceb15d3a63 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -43,9 +43,11 @@ def _find_vcvarsall(plat_spec): ], ) 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() - # makes sure that the right target platform name is used def _get_vcvars_spec(host_platform, platform): assert platform == expected From f15beb8b79a0180c662e832e5517ab42ddb8c946 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Sep 2024 10:27:33 -0400 Subject: [PATCH 23/24] Use double-backticks for rst compatibility. --- newsfragments/298.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/298.bugfix.rst b/newsfragments/298.bugfix.rst index eeb10466bf..a357d34f25 100644 --- a/newsfragments/298.bugfix.rst +++ b/newsfragments/298.bugfix.rst @@ -1 +1 @@ -Fix cross-platform compilation using `distutils._msvccompiler.MSVCCompiler` -- by :user:`saschanaz` and :user:`Avasam` +Fix cross-platform compilation using ``distutils._msvccompiler.MSVCCompiler`` -- by :user:`saschanaz` and :user:`Avasam` From 378984e02edae91d5f49425da8436f8dd9152b8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Sep 2024 10:48:59 -0400 Subject: [PATCH 24/24] Remove news fragments, not useful here. --- newsfragments/298.bugfix.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 newsfragments/298.bugfix.rst diff --git a/newsfragments/298.bugfix.rst b/newsfragments/298.bugfix.rst deleted file mode 100644 index a357d34f25..0000000000 --- a/newsfragments/298.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix cross-platform compilation using ``distutils._msvccompiler.MSVCCompiler`` -- by :user:`saschanaz` and :user:`Avasam`