diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..bcef31d9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,16 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* + + # local + */compat/* +disable_warnings = + couldnt-parse + +[report] +show_missing = True +exclude_also = + # jaraco/skeleton#97 + @overload + if TYPE_CHECKING: diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..304196f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space +max_line_length = 88 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rst] +indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e244014d..1d5ab14e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,39 +1,99 @@ name: tests -on: [push, pull_request] +on: + merge_group: + push: + branches-ignore: + # temporary GH branches relating to merge queues (jaraco/skeleton#93) + - gh-readonly-queue/** + tags: + # required if branches-ignore is supplied (jaraco/skeleton#103) + - '**' + pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + group: >- + ${{ github.workflow }}- + ${{ github.ref_type }}- + ${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true +permissions: + contents: read + +env: + # Environment variable to support color support (jaraco/skeleton#66) + FORCE_COLOR: 1 + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_PYTHON_VERSION_WARNING: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_*,FORCE_COLOR + jobs: test: strategy: + # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - # Build on pre-releases until stable, then stable releases. - # actions/setup-python#213 - - ~3.7.0-0 - - ~3.10.0-0 - - ~3.11.0-0 + - "3.8" + - "3.12" platform: - ubuntu-latest - macos-latest - windows-latest + include: + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest + - python: "3.11" + platform: ubuntu-latest + - python: pypy3.10 + platform: ubuntu-latest runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + allow-prereleases: true - name: Install tox - run: | - python -m pip install tox - - name: Run tests + run: python -m pip install tox + - name: Run run: tox + collateral: + strategy: + fail-fast: false + matrix: + job: + - diffcov + - docs + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.python == '3.12' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: python -m pip install tox + - name: Eval ${{ matrix.job }} + run: tox -e ${{ matrix.job }} + test_cygwin: + # disabled due to lack of Rust support pypa/setuptools#3921 + if: ${{ false }} strategy: matrix: python: @@ -42,9 +102,9 @@ jobs: - windows-latest runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v1 + uses: cygwin/cygwin-install-action@v2 with: platform: x86_64 packages: >- @@ -55,6 +115,7 @@ jobs: gcc-core, gcc-g++, ncompress + git - name: Run tests shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} run: tox @@ -71,16 +132,16 @@ jobs: env: SETUPTOOLS_USE_DISTUTILS: local steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install tox run: | python -m pip install tox - name: Check out pypa/setuptools - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: pypa/setuptools ref: main @@ -97,21 +158,40 @@ jobs: env: VIRTUALENV_NO_SETUPTOOLS: null + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + - collateral + # disabled due to disabled job + # - test_cygwin + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + release: - needs: test + permissions: + contents: write + needs: + - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: 3.11-dev - name: Install tox - run: | - python -m pip install tox - - name: Release + run: python -m pip install tox + - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..5a4a7e91 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 + hooks: + - id: ruff + - id: ruff-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..85dfea9d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs + +# required boilerplate readthedocs/readthedocs.org#10401 +build: + os: ubuntu-lts-latest + tools: + python: latest + # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 + jobs: + post_checkout: + - git fetch --unshallow || true diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1bb5a443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/NEWS.rst b/NEWS.rst new file mode 100644 index 00000000..e69de29b diff --git a/README.rst b/README.rst index 588898bb..aa3b65f1 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,27 @@ -Python Module Distribution Utilities extracted from the Python Standard Library +.. image:: https://img.shields.io/pypi/v/distutils.svg + :target: https://pypi.org/project/distutils + +.. image:: https://img.shields.io/pypi/pyversions/distutils.svg + +.. image:: https://github.com/pypa/distutils/actions/workflows/main.yml/badge.svg + :target: https://github.com/pypa/distutils/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff -Synchronizing -============= +.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest +.. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton + +Python Module Distribution Utilities extracted from the Python Standard Library -This project is no longer kept in sync with the code still in stdlib, which is deprecated and scheduled for removal. +This package is unsupported except as integrated into and exposed by Setuptools. -To Setuptools -------------- +Integration +----------- Simply merge the changes directly into setuptools' repo. diff --git a/conftest.py b/conftest.py index 3c0521c4..4a3bbd34 100644 --- a/conftest.py +++ b/conftest.py @@ -11,11 +11,9 @@ if platform.system() != 'Windows': - collect_ignore.extend( - [ - 'distutils/msvc9compiler.py', - ] - ) + collect_ignore.extend([ + 'distutils/msvc9compiler.py', + ]) @pytest.fixture @@ -58,7 +56,7 @@ def _save_cwd(): @pytest.fixture def distutils_managed_tempdir(request): - from distutils.tests import py38compat as os_helper + from distutils.tests.compat import py38 as os_helper self = request.instance self.tempdirs = [] @@ -152,3 +150,10 @@ def temp_home(tmp_path, monkeypatch): def fake_home(fs, monkeypatch): home = fs.create_dir('/fakehome') return _set_home(monkeypatch, pathlib.Path(home.path)) + + +@pytest.fixture +def disable_macos_customization(monkeypatch): + from distutils import sysconfig + + monkeypatch.setattr(sysconfig, '_customize_macos', lambda: None) diff --git a/distutils/_collections.py b/distutils/_collections.py index 02556614..d11a8346 100644 --- a/distutils/_collections.py +++ b/distutils/_collections.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import collections import functools import itertools import operator +from collections.abc import Mapping +from typing import Any # from jaraco.collections 3.5.1 @@ -58,7 +62,7 @@ def __len__(self): return len(list(iter(self))) -# from jaraco.collections 3.7 +# from jaraco.collections 5.0.1 class RangeMap(dict): """ A dictionary-like object that uses the keys as bounds for a range. @@ -70,7 +74,7 @@ class RangeMap(dict): One may supply keyword parameters to be passed to the sort function used to sort keys (i.e. key, reverse) as sort_params. - Let's create a map that maps 1-3 -> 'a', 4-6 -> 'b' + Create a map that maps 1-3 -> 'a', 4-6 -> 'b' >>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy >>> r[1], r[2], r[3], r[4], r[5], r[6] @@ -82,7 +86,7 @@ class RangeMap(dict): >>> r[4.5] 'b' - But you'll notice that the way rangemap is defined, it must be open-ended + Notice that the way rangemap is defined, it must be open-ended on one side. >>> r[0] @@ -140,7 +144,12 @@ class RangeMap(dict): """ - def __init__(self, source, sort_params={}, key_match_comparator=operator.le): + def __init__( + self, + source, + sort_params: Mapping[str, Any] = {}, + key_match_comparator=operator.le, + ): dict.__init__(self, source) self.sort_params = sort_params self.match = key_match_comparator @@ -185,7 +194,7 @@ def bounds(self): return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) # some special values for the RangeMap - undefined_value = type(str('RangeValueUndefined'), (), {})() + undefined_value = type('RangeValueUndefined', (), {})() class Item(int): "RangeMap Item" diff --git a/distutils/_functools.py b/distutils/_functools.py index e7053bac..e03365ea 100644 --- a/distutils/_functools.py +++ b/distutils/_functools.py @@ -1,3 +1,4 @@ +import collections.abc import functools @@ -18,3 +19,55 @@ def wrapper(param, *args, **kwargs): 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 new file mode 100644 index 00000000..85b29511 --- /dev/null +++ b/distutils/_itertools.py @@ -0,0 +1,52 @@ +# 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 new file mode 100644 index 00000000..07b2ead0 --- /dev/null +++ b/distutils/_modified.py @@ -0,0 +1,72 @@ +"""Timestamp comparison of files and groups of files.""" + +import functools +import os.path + +from ._functools import splat +from .compat.py39 import zip_strict +from .errors import DistutilsFileError + + +def _newer(source, target): + return not os.path.exists(target) or ( + os.path.getmtime(source) > os.path.getmtime(target) + ) + + +def newer(source, target): + """ + Is source modified more recently than target. + + Returns True if 'source' is modified more recently than + 'target' or if 'target' does not exist. + + Raises DistutilsFileError if 'source' does not exist. + """ + if not os.path.exists(source): + raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) + + return _newer(source, target) + + +def newer_pairwise(sources, targets, newer=newer): + """ + Filter filenames where sources are newer than targets. + + Walk two filename iterables in parallel, testing if each source is newer + than its corresponding target. Returns a pair of lists (sources, + targets) where source is newer than target, according to the semantics + of 'newer()'. + """ + newer_pairs = filter(splat(newer), zip_strict(sources, targets)) + return tuple(map(list, zip(*newer_pairs))) or ([], []) + + +def newer_group(sources, target, missing='error'): + """ + Is target out-of-date with respect to any file in sources. + + Return True if 'target' is out-of-date with respect to any file + listed in 'sources'. In other words, if 'target' exists and is newer + than every file in 'sources', return False; otherwise return True. + ``missing`` controls how to handle a missing source file: + + - error (default): allow the ``stat()`` call to fail. + - ignore: silently disregard any missing source files. + - newer: treat missing source files as "target out of date". This + mode is handy in "dry-run" mode: it will pretend to carry out + commands that wouldn't work because inputs are missing, but + that doesn't matter because dry-run won't run the commands. + """ + + def missing_as_newer(source): + return missing == 'newer' and not os.path.exists(source) + + ignored = os.path.exists if missing == 'ignore' else None + return any( + missing_as_newer(source) or _newer(source, target) + for source in filter(ignored, sources) + ) + + +newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 4844f18e..a2159fef 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -253,7 +253,7 @@ def initialize(self, plat_name=None): vc_env = _get_vc_env(plat_spec) if not vc_env: raise DistutilsPlatformError( - "Unable to find a compatible " "Visual Studio installation." + "Unable to find a compatible Visual Studio installation." ) self._configure(vc_env) @@ -339,7 +339,6 @@ def compile( # noqa: C901 extra_postargs=None, depends=None, ): - if not self.initialized: self.initialize() compile_info = self._setup_compile( @@ -413,8 +412,7 @@ def compile( # noqa: C901 args = [self.cc] + compile_opts + pp_opts if add_cpp_opts: args.append('/EHsc') - args.append(input_opt) - args.append("/Fo" + obj) + args.extend((input_opt, "/Fo" + obj)) args.extend(extra_postargs) try: @@ -427,7 +425,6 @@ def compile( # noqa: C901 def create_static_lib( self, objects, output_libname, output_dir=None, debug=0, target_lang=None ): - if not self.initialized: self.initialize() objects, output_dir = self._fix_object_args(objects, output_dir) @@ -461,7 +458,6 @@ def link( build_temp=None, target_lang=None, ): - if not self.initialized: self.initialize() objects, output_dir = self._fix_object_args(objects, output_dir) diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index b37cb6ab..8600c2b1 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -11,13 +11,12 @@ # 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 .compilers.C.base import Compiler, gen_preprocess_options -from .dep_util import newer from .errors import ( CompileError, DistutilsExecError, @@ -63,7 +62,6 @@ class BCPPCompiler(Compiler): exe_extension = '.exe' def __init__(self, verbose=0, dry_run=0, force=0): - super().__init__(verbose, dry_run, force) # These executables are assumed to all be in the path. @@ -97,7 +95,6 @@ def compile( # noqa: C901 extra_postargs=None, depends=None, ): - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( output_dir, macros, include_dirs, sources, depends, extra_postargs ) @@ -166,7 +163,6 @@ def compile( # noqa: C901 def create_static_lib( self, objects, output_libname, output_dir=None, debug=0, target_lang=None ): - (objects, output_dir) = self._fix_object_args(objects, output_dir) output_filename = self.library_filename(output_libname, output_dir=output_dir) @@ -199,7 +195,6 @@ def link( # noqa: C901 build_temp=None, target_lang=None, ): - # XXX this ignores 'build_temp'! should follow the lead of # msvccompiler.py @@ -218,7 +213,6 @@ def link( # noqa: C901 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 == Compiler.EXECUTABLE: startup_obj = 'c0w32' @@ -243,7 +237,7 @@ def link( # noqa: C901 def_file = os.path.join(temp_dir, '%s.def' % modname) contents = ['EXPORTS'] for sym in export_symbols or []: - contents.append(' {}=_{}'.format(sym, sym)) + contents.append(f' {sym}=_{sym}') self.execute(write_file, (def_file, contents), "writing %s" % def_file) # Borland C++ has problems with '/' in paths @@ -293,8 +287,7 @@ def link( # noqa: C901 ld_args.append(libfile) # some default libraries - ld_args.append('import32') - ld_args.append('cw32mt') + ld_args.extend(('import32', 'cw32mt')) # def file for export symbols ld_args.extend([',', def_file]) @@ -354,9 +347,7 @@ def object_filenames(self, source_filenames, strip_dir=0, output_dir=''): # 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( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: base = os.path.basename(base) if ext == '.res': @@ -380,7 +371,6 @@ def preprocess( 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 diff --git a/distutils/cmd.py b/distutils/cmd.py index 83eca0c8..02dbf165 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -9,7 +9,7 @@ import re import sys -from . import archive_util, dep_util, dir_util, file_util, util +from . import _modified, archive_util, dir_util, file_util, util from ._log import log from .errors import DistutilsOptionError @@ -160,12 +160,12 @@ def dump_options(self, header=None, indent=""): header = "command options for '%s':" % self.get_command_name() self.announce(indent + header, level=logging.INFO) indent = indent + " " - for (option, _, _) in self.user_options: + for option, _, _ in self.user_options: option = option.translate(longopt_xlate) if option[-1] == "=": option = option[:-1] value = getattr(self, option) - self.announce(indent + "{} = {}".format(option, value), level=logging.INFO) + self.announce(indent + f"{option} = {value}", level=logging.INFO) def run(self): """A command's raison d'etre: carry out the action it exists to @@ -213,9 +213,7 @@ def _ensure_stringlike(self, option, what, default=None): setattr(self, option, default) return default elif not isinstance(val, str): - raise DistutilsOptionError( - "'{}' must be a {} (got `{}`)".format(option, what, val) - ) + raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)") return val def ensure_string(self, option, default=None): @@ -242,7 +240,7 @@ def ensure_string_list(self, option): ok = False if not ok: raise DistutilsOptionError( - "'{}' must be a list of strings (got {!r})".format(option, val) + f"'{option}' must be a list of strings (got {val!r})" ) def _ensure_tested_string(self, option, tester, what, error_fmt, default=None): @@ -291,7 +289,7 @@ def set_undefined_options(self, src_cmd, *option_pairs): # Option_pairs: list of (src_option, dst_option) tuples src_cmd_obj = self.distribution.get_command_obj(src_cmd) src_cmd_obj.ensure_finalized() - for (src_option, dst_option) in option_pairs: + for src_option, dst_option in option_pairs: if getattr(self, dst_option) is None: setattr(self, dst_option, getattr(src_cmd_obj, src_option)) @@ -325,7 +323,7 @@ def get_sub_commands(self): run for the current distribution. Return a list of command names. """ commands = [] - for (cmd_name, method) in self.sub_commands: + for cmd_name, method in self.sub_commands: if method is None or method(self): commands.append(cmd_name) return commands @@ -428,7 +426,7 @@ def make_file( # If 'outfile' must be regenerated (either because it doesn't # exist, is out-of-date, or the 'force' flag is true) then # perform the action that presumably regenerates it - if self.force or dep_util.newer_group(infiles, outfile): + if self.force or _modified.newer_group(infiles, outfile): self.execute(func, args, exec_msg, level) # Otherwise, print the "skip" message else: diff --git a/distutils/command/_framework_compat.py b/distutils/command/_framework_compat.py index cf80c981..00d34bc7 100644 --- a/distutils/command/_framework_compat.py +++ b/distutils/command/_framework_compat.py @@ -2,7 +2,6 @@ Backward compatibility for homebrew builds on macOS. """ - import functools import os import subprocess @@ -10,7 +9,7 @@ import sysconfig -@functools.lru_cache() +@functools.lru_cache def enabled(): """ Only enabled for Python 3.9 framework homebrew builds @@ -38,7 +37,7 @@ def enabled(): ) -@functools.lru_cache() +@functools.lru_cache def vars(): if not enabled(): return {} diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py index 3301082b..ade98445 100644 --- a/distutils/command/bdist.py +++ b/distutils/command/bdist.py @@ -33,7 +33,6 @@ def append(self, item): class bdist(Command): - description = "create a built (binary) distribution" user_options = [ @@ -48,18 +47,18 @@ class bdist(Command): ( 'dist-dir=', 'd', - "directory to put final built distributions in " "[default: dist]", + "directory to put final built distributions in [default: dist]", ), ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), ( 'owner=', 'u', - "Owner name used when creating a tar file" " [default: current user]", + "Owner name used when creating a tar file [default: current user]", ), ( 'group=', 'g', - "Group name used when creating a tar file" " [default: current group]", + "Group name used when creating a tar file [default: current group]", ), ] @@ -77,17 +76,15 @@ class bdist(Command): default_format = {'posix': 'gztar', 'nt': 'zip'} # Define commands in preferred order for the --help-formats option - format_commands = ListCompat( - { - 'rpm': ('bdist_rpm', "RPM distribution"), - 'gztar': ('bdist_dumb', "gzip'ed tar file"), - 'bztar': ('bdist_dumb', "bzip2'ed tar file"), - 'xztar': ('bdist_dumb', "xz'ed tar file"), - 'ztar': ('bdist_dumb', "compressed tar file"), - 'tar': ('bdist_dumb', "tar file"), - 'zip': ('bdist_dumb', "ZIP file"), - } - ) + format_commands = ListCompat({ + 'rpm': ('bdist_rpm', "RPM distribution"), + 'gztar': ('bdist_dumb', "gzip'ed tar file"), + 'bztar': ('bdist_dumb', "bzip2'ed tar file"), + 'xztar': ('bdist_dumb', "xz'ed tar file"), + 'ztar': ('bdist_dumb', "compressed tar file"), + 'tar': ('bdist_dumb', "tar file"), + 'zip': ('bdist_dumb', "ZIP file"), + }) # for compatibility until consumers only reference format_commands format_command = format_commands diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py index 3edfd659..06502d20 100644 --- a/distutils/command/bdist_dumb.py +++ b/distutils/command/bdist_dumb.py @@ -15,7 +15,6 @@ class bdist_dumb(Command): - description = "create a \"dumb\" built distribution" user_options = [ @@ -29,7 +28,7 @@ class bdist_dumb(Command): ( 'format=', 'f', - "archive format to create (tar, gztar, bztar, xztar, " "ztar, zip)", + "archive format to create (tar, gztar, bztar, xztar, ztar, zip)", ), ( 'keep-temp', @@ -42,17 +41,17 @@ class bdist_dumb(Command): ( 'relative', None, - "build the archive using relative paths " "(default: false)", + "build the archive using relative paths (default: false)", ), ( 'owner=', 'u', - "Owner name used when creating a tar file" " [default: current user]", + "Owner name used when creating a tar file [default: current user]", ), ( 'group=', 'g', - "Group name used when creating a tar file" " [default: current group]", + "Group name used when creating a tar file [default: current group]", ), ] @@ -106,9 +105,7 @@ def run(self): # And make an archive relative to the root of the # pseudo-installation tree. - archive_basename = "{}.{}".format( - self.distribution.get_fullname(), self.plat_name - ) + archive_basename = f"{self.distribution.get_fullname()}.{self.plat_name}" pseudoinstall_root = os.path.join(self.dist_dir, archive_basename) if not self.relative: @@ -119,8 +116,7 @@ def run(self): ): raise DistutilsPlatformError( "can't make a dumb built distribution where " - "base and platbase are different (%s, %s)" - % (repr(install.install_base), repr(install.install_platbase)) + f"base and platbase are different ({repr(install.install_base)}, {repr(install.install_platbase)})" ) else: archive_root = os.path.join( diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 24ed019f..649968a5 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -21,7 +21,6 @@ class bdist_rpm(Command): - description = "create an RPM distribution" user_options = [ @@ -35,7 +34,7 @@ class bdist_rpm(Command): ( 'dist-dir=', 'd', - "directory to put final RPM files in " "(and .spec files if --spec-only)", + "directory to put final RPM files in (and .spec files if --spec-only)", ), ( 'python=', @@ -76,7 +75,7 @@ class bdist_rpm(Command): ( 'packager=', None, - "RPM packager (eg. \"Jane Doe \") " "[default: vendor]", + "RPM packager (eg. \"Jane Doe \") [default: vendor]", ), ('doc-files=', None, "list of documentation files (space or comma-separated)"), ('changelog=', None, "RPM changelog"), @@ -215,7 +214,7 @@ def finalize_options(self): if os.name != 'posix': raise DistutilsPlatformError( - "don't know how to create RPM " "distributions on platform %s" % os.name + "don't know how to create RPM distributions on platform %s" % os.name ) if self.binary_only and self.source_only: raise DistutilsOptionError( @@ -233,8 +232,7 @@ def finalize_package_data(self): self.ensure_string('group', "Development/Libraries") self.ensure_string( 'vendor', - "%s <%s>" - % (self.distribution.get_contact(), self.distribution.get_contact_email()), + f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>", ) self.ensure_string('packager') self.ensure_string_list('doc_files') @@ -353,11 +351,7 @@ def run(self): # noqa: C901 nvr_string = "%{name}-%{version}-%{release}" src_rpm = nvr_string + ".src.rpm" non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm" - q_cmd = r"rpm -q --qf '{} {}\n' --specfile '{}'".format( - src_rpm, - non_src_rpm, - spec_path, - ) + q_cmd = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'" out = os.popen(q_cmd) try: @@ -402,9 +396,11 @@ def run(self): # noqa: C901 if os.path.exists(rpm): self.move_file(rpm, self.dist_dir) filename = os.path.join(self.dist_dir, os.path.basename(rpm)) - self.distribution.dist_files.append( - ('bdist_rpm', pyversion, filename) - ) + self.distribution.dist_files.append(( + 'bdist_rpm', + pyversion, + filename, + )) def _dist_path(self, path): return os.path.join(self.dist_dir, os.path.basename(path)) @@ -429,14 +425,14 @@ def _make_spec_file(self): # noqa: C901 # Generate a potential replacement value for __os_install_post (whilst # normalizing the whitespace to simplify the test for whether the # invocation of brp-python-bytecompile passes in __python): - vendor_hook = '\n'.join( - [' %s \\' % line.strip() for line in vendor_hook.splitlines()] - ) + vendor_hook = '\n'.join([ + ' %s \\' % line.strip() for line in vendor_hook.splitlines() + ]) problem = "brp-python-bytecompile \\\n" fixed = "brp-python-bytecompile %{__python} \\\n" fixed_hook = vendor_hook.replace(problem, fixed) if fixed_hook != vendor_hook: - spec_file.append('# Workaround for http://bugs.python.org/issue14443') + spec_file.append('# Workaround for https://bugs.python.org/issue14443') spec_file.append('%define __os_install_post ' + fixed_hook + '\n') # put locale summaries into spec file @@ -446,13 +442,11 @@ def _make_spec_file(self): # noqa: C901 # spec_file.append('Summary(%s): %s' % (locale, # self.summaries[locale])) - spec_file.extend( - [ - 'Name: %{name}', - 'Version: %{version}', - 'Release: %{release}', - ] - ) + spec_file.extend([ + 'Name: %{name}', + 'Version: %{version}', + 'Release: %{release}', + ]) # XXX yuck! this filename is available from the "sdist" command, # but only after it has run: and we create the spec file before @@ -462,14 +456,12 @@ def _make_spec_file(self): # noqa: C901 else: spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz') - spec_file.extend( - [ - 'License: ' + (self.distribution.get_license() or "UNKNOWN"), - 'Group: ' + self.group, - 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', - 'Prefix: %{_prefix}', - ] - ) + spec_file.extend([ + 'License: ' + (self.distribution.get_license() or "UNKNOWN"), + 'Group: ' + self.group, + 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', + 'Prefix: %{_prefix}', + ]) if not self.force_arch: # noarch if no extension modules @@ -490,7 +482,7 @@ def _make_spec_file(self): # noqa: C901 if isinstance(val, list): spec_file.append('{}: {}'.format(field, ' '.join(val))) elif val is not None: - spec_file.append('{}: {}'.format(field, val)) + spec_file.append(f'{field}: {val}') if self.distribution.get_url(): spec_file.append('Url: ' + self.distribution.get_url()) @@ -507,13 +499,11 @@ def _make_spec_file(self): # noqa: C901 if self.no_autoreq: spec_file.append('AutoReq: 0') - spec_file.extend( - [ - '', - '%description', - self.distribution.get_long_description() or "", - ] - ) + spec_file.extend([ + '', + '%description', + self.distribution.get_long_description() or "", + ]) # put locale descriptions into spec file # XXX again, suppressed because config file syntax doesn't @@ -527,7 +517,7 @@ def _make_spec_file(self): # noqa: C901 # rpm scripts # figure out default build script - def_setup_call = "{} {}".format(self.python, os.path.basename(sys.argv[0])) + def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}" def_build = "%s build" % def_setup_call if self.use_rpm_opt_flags: def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build @@ -554,17 +544,15 @@ def _make_spec_file(self): # noqa: C901 ('postun', 'post_uninstall', None), ] - for (rpm_opt, attr, default) in script_options: + for rpm_opt, attr, default in script_options: # Insert contents of file referred to, if no file is referred to # use 'default' as contents of script val = getattr(self, attr) if val or default: - spec_file.extend( - [ - '', - '%' + rpm_opt, - ] - ) + spec_file.extend([ + '', + '%' + rpm_opt, + ]) if val: with open(val) as f: spec_file.extend(f.read().split('\n')) @@ -572,24 +560,20 @@ def _make_spec_file(self): # noqa: C901 spec_file.append(default) # files section - spec_file.extend( - [ - '', - '%files -f INSTALLED_FILES', - '%defattr(-,root,root)', - ] - ) + spec_file.extend([ + '', + '%files -f INSTALLED_FILES', + '%defattr(-,root,root)', + ]) if self.doc_files: spec_file.append('%doc ' + ' '.join(self.doc_files)) if self.changelog: - spec_file.extend( - [ - '', - '%changelog', - ] - ) + spec_file.extend([ + '', + '%changelog', + ]) spec_file.extend(self.changelog) return spec_file diff --git a/distutils/command/build.py b/distutils/command/build.py index a4c48ec7..d18ed503 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -17,7 +17,6 @@ def show_compilers(): class build(Command): - description = "build everything needed to install" user_options = [ @@ -80,7 +79,7 @@ def finalize_options(self): # noqa: C901 "using './configure --help' on your platform)" ) - plat_specifier = ".{}-{}".format(self.plat_name, sys.implementation.cache_tag) + plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}" # Make it so Python 2.x and Python 2.x with --with-pydebug don't # share the same build directories. Doing so confuses the build diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py index 9ed814f3..360575d0 100644 --- a/distutils/command/build_clib.py +++ b/distutils/command/build_clib.py @@ -29,7 +29,6 @@ def show_compilers(): class build_clib(Command): - description = "build C/C++ libraries used by Python extensions" user_options = [ @@ -104,7 +103,7 @@ def run(self): self.compiler.set_include_dirs(self.include_dirs) if self.define is not None: # 'define' option is a list of (name,value) tuples - for (name, value) in self.define: + for name, value in self.define: self.compiler.define_macro(name, value) if self.undef is not None: for macro in self.undef: @@ -156,14 +155,14 @@ def get_library_names(self): return None lib_names = [] - for (lib_name, build_info) in self.libraries: + for lib_name, _build_info in self.libraries: lib_names.append(lib_name) return lib_names def get_source_files(self): self.check_library_list(self.libraries) filenames = [] - for (lib_name, build_info) in self.libraries: + for lib_name, build_info in self.libraries: sources = build_info.get('sources') if sources is None or not isinstance(sources, (list, tuple)): raise DistutilsSetupError( @@ -176,7 +175,7 @@ def get_source_files(self): return filenames def build_libraries(self, libraries): - for (lib_name, build_info) in libraries: + for lib_name, build_info in libraries: sources = build_info.get('sources') if sources is None or not isinstance(sources, (list, tuple)): raise DistutilsSetupError( diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index c05e0eb7..06d949af 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -11,8 +11,8 @@ from distutils._log import log from site import USER_BASE +from .._modified import newer_group from ..core import Command -from ..dep_util import newer_group from ..errors import ( CCompilerError, CompileError, @@ -24,7 +24,6 @@ from ..extension import Extension from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version from ..util import get_platform -from . import py37compat # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). @@ -38,7 +37,6 @@ def show_compilers(): class build_ext(Command): - description = "build C/C++ extensions (compile/link to build directory)" # XXX thoughts on how to deal with complex command-line options like @@ -130,6 +128,31 @@ def initialize_options(self): self.user = None self.parallel = None + @staticmethod + def _python_lib_dir(sysconfig): + """ + Resolve Python's library directory for building extensions + that rely on a shared Python library. + + See python/cpython#44264 and python/cpython#48686 + """ + if not sysconfig.get_config_var('Py_ENABLE_SHARED'): + return + + if sysconfig.python_build: + yield '.' + return + + if sys.platform == 'zos': + # On z/OS, a user is not required to install Python to + # a predetermined path, but can use Python portably + installed_dir = sysconfig.get_config_var('base') + lib_dir = sysconfig.get_config_var('platlibdir') + yield os.path.join(installed_dir, lib_dir) + else: + # building third party extensions + yield sysconfig.get_config_var('LIBDIR') + def finalize_options(self): # noqa: C901 from distutils import sysconfig @@ -231,16 +254,7 @@ def finalize_options(self): # noqa: C901 # building python standard extensions self.library_dirs.append('.') - # For building extensions with a shared Python library, - # Python's library directory must be appended to library_dirs - # See Issues: #1600860, #4366 - if sysconfig.get_config_var('Py_ENABLE_SHARED'): - if not sysconfig.python_build: - # building third party extensions - self.library_dirs.append(sysconfig.get_config_var('LIBDIR')) - else: - # building python standard extensions - self.library_dirs.append('.') + self.library_dirs.extend(self._python_lib_dir(sysconfig)) # The argument parsing will result in self.define being a string, but # it has to be a list of 2-tuples. All the preprocessor symbols @@ -327,7 +341,7 @@ def run(self): # noqa: C901 self.compiler.set_include_dirs(self.include_dirs) if self.define is not None: # 'define' option is a list of (name,value) tuples - for (name, value) in self.define: + for name, value in self.define: self.compiler.define_macro(name, value) if self.undef is not None: for macro in self.undef: @@ -412,9 +426,7 @@ def check_extensions_list(self, extensions): # noqa: C901 # Medium-easy stuff: same syntax/semantics, different names. ext.runtime_library_dirs = build_info.get('rpath') if 'def_file' in build_info: - log.warning( - "'def_file' element of build info dict " "no longer supported" - ) + log.warning("'def_file' element of build info dict no longer supported") # Non-trivial stuff: 'macros' split into 'define_macros' # and 'undef_macros'. @@ -499,7 +511,7 @@ def _filter_build_errors(self, ext): except (CCompilerError, DistutilsError, CompileError) as e: if not ext.optional: raise - self.warn('building extension "{}" failed: {}'.format(ext.name, e)) + self.warn(f'building extension "{ext.name}" failed: {e}') def build_extension(self, ext): sources = ext.sources @@ -720,7 +732,7 @@ def get_export_symbols(self, ext): name = ext.name.split('.')[-1] try: # Unicode module name support as defined in PEP-489 - # https://www.python.org/dev/peps/pep-0489/#export-hook-name + # https://peps.python.org/pep-0489/#export-hook-name name.encode('ascii') except UnicodeEncodeError: suffix = 'U_' + name.encode('punycode').replace(b'-', b'_').decode('ascii') @@ -785,4 +797,4 @@ def get_libraries(self, ext): # noqa: C901 ldversion = get_config_var('LDVERSION') return ext.libraries + ['python' + ldversion] - return ext.libraries + py37compat.pythonlib() + return ext.libraries diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py index 798f67b5..56e6fa2e 100644 --- a/distutils/command/build_py.py +++ b/distutils/command/build_py.py @@ -14,7 +14,6 @@ class build_py(Command): - description = "\"build\" pure Python modules (copy to build directory)" user_options = [ @@ -130,14 +129,14 @@ def find_data_files(self, package, src_dir): os.path.join(glob.escape(src_dir), convert_path(pattern)) ) # Files that match more than one pattern are only added once - files.extend( - [fn for fn in filelist if fn not in files and os.path.isfile(fn)] - ) + files.extend([ + fn for fn in filelist if fn not in files and os.path.isfile(fn) + ]) return files def build_package_data(self): """Copy data files into build directory""" - for package, src_dir, build_dir, filenames in self.data_files: + for _package, src_dir, build_dir, filenames in self.data_files: for filename in filenames: target = os.path.join(build_dir, filename) self.mkpath(os.path.dirname(target)) @@ -310,7 +309,7 @@ def get_module_outfile(self, build_dir, package, module): def get_outputs(self, include_bytecode=1): modules = self.find_all_modules() outputs = [] - for (package, module, module_file) in modules: + for package, module, _module_file in modules: package = package.split('.') filename = self.get_module_outfile(self.build_lib, package, module) outputs.append(filename) @@ -352,7 +351,7 @@ def build_module(self, module, module_file, package): def build_modules(self): modules = self.find_modules() - for (package, module, module_file) in modules: + for package, module, module_file in modules: # Now "build" the module -- ie. copy the source file to # self.build_lib (the build directory for Python source). # (Actually, it gets copied to the directory for this package @@ -375,7 +374,7 @@ def build_packages(self): # Now loop over the modules we found, "building" each one (just # copy it to self.build_lib). - for (package_, module, module_file) in modules: + for package_, module, module_file in modules: assert package == package_ self.build_module(module, module_file, package) diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index fe351d11..5f3902a0 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -9,8 +9,8 @@ from distutils._log import log from stat import ST_MODE +from .._modified import newer from ..core import Command -from ..dep_util import newer from ..util import convert_path shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$') @@ -23,7 +23,6 @@ class build_scripts(Command): - description = "\"build\" scripts (copy and fixup #! line)" user_options = [ @@ -111,8 +110,7 @@ def _copy_script(self, script, outfiles, updated_files): # noqa: C901 else: executable = os.path.join( sysconfig.get_config_var("BINDIR"), - "python%s%s" - % ( + "python{}{}".format( sysconfig.get_config_var("VERSION"), sysconfig.get_config_var("EXE"), ), @@ -158,9 +156,7 @@ def _validate_shebang(shebang, encoding): try: shebang.encode('utf-8') except UnicodeEncodeError: - raise ValueError( - "The shebang ({!r}) is not encodable " "to utf-8".format(shebang) - ) + raise ValueError(f"The shebang ({shebang!r}) is not encodable to utf-8") # If the script is encoded to a custom encoding (use a # #coding:xxx cookie), the shebang has to be encodable to @@ -169,6 +165,6 @@ def _validate_shebang(shebang, encoding): shebang.encode(encoding) except UnicodeEncodeError: raise ValueError( - "The shebang ({!r}) is not encodable " - "to the script encoding ({})".format(shebang, encoding) + f"The shebang ({shebang!r}) is not encodable " + f"to the script encoding ({encoding})" ) diff --git a/distutils/command/check.py b/distutils/command/check.py index 3d084a45..28599e10 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -2,6 +2,7 @@ Implements the Distutils 'check' command. """ + import contextlib from ..core import Command @@ -32,7 +33,7 @@ def __init__( def system_message(self, level, message, *children, **kwargs): self.messages.append((level, message, children, kwargs)) return docutils.nodes.system_message( - message, level=level, type=self.levels[level], *children, **kwargs + message, *children, level=level, type=self.levels[level], **kwargs ) @@ -115,7 +116,7 @@ def check_restructuredtext(self): if line is None: warning = warning[1] else: - warning = '{} (line {})'.format(warning[1], line) + warning = f'{warning[1]} (line {line})' self.warn(warning) def _check_rst_data(self, data): @@ -144,8 +145,11 @@ def _check_rst_data(self, data): try: parser.parse(data, document) except AttributeError as e: - reporter.messages.append( - (-1, 'Could not finish the parsing: %s.' % e, '', {}) - ) + reporter.messages.append(( + -1, + 'Could not finish the parsing: %s.' % e, + '', + {}, + )) return reporter.messages diff --git a/distutils/command/clean.py b/distutils/command/clean.py index ad29e489..4167a83f 100644 --- a/distutils/command/clean.py +++ b/distutils/command/clean.py @@ -12,7 +12,6 @@ class clean(Command): - description = "clean up temporary files from 'build' command" user_options = [ ('build-base=', 'b', "base build directory (default: 'build.build-base')"), diff --git a/distutils/command/config.py b/distutils/command/config.py index 247df95d..d4b2b0a3 100644 --- a/distutils/command/config.py +++ b/distutils/command/config.py @@ -9,8 +9,12 @@ this header file lives". """ +from __future__ import annotations + import os +import pathlib import re +from collections.abc import Sequence from distutils._log import log from ..core import Command @@ -21,7 +25,6 @@ class config(Command): - description = "prepare to build" user_options = [ @@ -103,7 +106,7 @@ def _check_compiler(self): def _gen_temp_sourcefile(self, body, headers, lang): filename = "_configtest" + LANG_EXT[lang] - with open(filename, "w") as file: + with open(filename, "w", encoding='utf-8') as file: if headers: for header in headers: file.write("#include <%s>\n" % header) @@ -200,15 +203,8 @@ def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang=" if isinstance(pattern, str): pattern = re.compile(pattern) - with open(out) as file: - match = False - while True: - line = file.readline() - if line == '': - break - if pattern.search(line): - match = True - break + with open(out, encoding='utf-8') as file: + match = any(pattern.search(line) for line in file) self._clean() return match @@ -332,7 +328,7 @@ def check_lib( library_dirs=None, headers=None, include_dirs=None, - other_libraries=[], + other_libraries: Sequence[str] = [], ): """Determine if 'library' is available to be linked against, without actually checking that any particular symbols are provided @@ -347,7 +343,7 @@ def check_lib( "int main (void) { }", headers, include_dirs, - [library] + other_libraries, + [library] + list(other_libraries), library_dirs, ) @@ -370,8 +366,4 @@ def dump_file(filename, head=None): log.info('%s', filename) else: log.info(head) - file = open(filename) - try: - log.info(file.read()) - finally: - file.close() + log.info(pathlib.Path(filename).read_text(encoding='utf-8')) diff --git a/distutils/command/install.py b/distutils/command/install.py index 110e442b..8e920be4 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -177,7 +177,6 @@ def _pypy_hack(name): class install(Command): - description = "install everything from build directory" user_options = [ @@ -243,9 +242,11 @@ class install(Command): boolean_options = ['compile', 'force', 'skip-build'] if HAS_USER_SITE: - user_options.append( - ('user', None, "install in user site-package '%s'" % USER_SITE) - ) + user_options.append(( + 'user', + None, + "install in user site-package '%s'" % USER_SITE, + )) boolean_options.append('user') negative_opt = {'no-compile': 'compile'} @@ -430,9 +431,12 @@ def finalize_options(self): # noqa: C901 local_vars['userbase'] = self.install_userbase local_vars['usersite'] = self.install_usersite - self.config_vars = _collections.DictStack( - [fw.vars(), compat_vars, sysconfig.get_config_vars(), local_vars] - ) + self.config_vars = _collections.DictStack([ + fw.vars(), + compat_vars, + sysconfig.get_config_vars(), + local_vars, + ]) self.expand_basedirs() @@ -606,7 +610,7 @@ def _expand_attrs(self, attrs): for attr in attrs: val = getattr(self, attr) if val is not None: - if os.name == 'posix' or os.name == 'nt': + if os.name in ('posix', 'nt'): val = os.path.expanduser(val) val = subst_vars(val, self.config_vars) setattr(self, attr, val) @@ -618,16 +622,14 @@ def expand_basedirs(self): def expand_dirs(self): """Calls `os.path.expanduser` on install dirs.""" - self._expand_attrs( - [ - 'install_purelib', - 'install_platlib', - 'install_lib', - 'install_headers', - 'install_scripts', - 'install_data', - ] - ) + self._expand_attrs([ + 'install_purelib', + 'install_platlib', + 'install_lib', + 'install_headers', + 'install_scripts', + 'install_data', + ]) def convert_paths(self, *names): """Call `convert_path` over `names`.""" @@ -681,7 +683,7 @@ def create_home_path(self): if not self.user: return home = convert_path(os.path.expanduser("~")) - for name, path in self.config_vars.items(): + for _name, path in self.config_vars.items(): if str(path).startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) @@ -699,7 +701,7 @@ def run(self): # internally, and not to sys.path, so we don't check the platform # matches what we are running. if self.warn_dir and build_plat != get_platform(): - raise DistutilsPlatformError("Can't install when " "cross-compiling") + raise DistutilsPlatformError("Can't install when cross-compiling") # Run all sub-commands (at least those that need to be run) for cmd_name in self.get_sub_commands(): diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py index 11410594..b63a1af2 100644 --- a/distutils/command/install_data.py +++ b/distutils/command/install_data.py @@ -12,7 +12,6 @@ class install_data(Command): - description = "install data files" user_options = [ @@ -53,7 +52,7 @@ def run(self): if self.warn_dir: self.warn( "setup script did not provide a directory for " - "'%s' -- installing right in '%s'" % (f, self.install_dir) + f"'{f}' -- installing right in '{self.install_dir}'" ) (out, _) = self.copy_file(f, self.install_dir) self.outfiles.append(out) diff --git a/distutils/command/install_headers.py b/distutils/command/install_headers.py index 1cdee823..085272c1 100644 --- a/distutils/command/install_headers.py +++ b/distutils/command/install_headers.py @@ -8,7 +8,6 @@ # XXX force is never used class install_headers(Command): - description = "install C/C++ header files" user_options = [ diff --git a/distutils/command/install_lib.py b/distutils/command/install_lib.py index bcb96308..b1f346f0 100644 --- a/distutils/command/install_lib.py +++ b/distutils/command/install_lib.py @@ -15,7 +15,6 @@ class install_lib(Command): - description = "install all Python modules (extensions and pure Python)" # The byte-compilation options are a tad confusing. Here are the diff --git a/distutils/command/install_scripts.py b/distutils/command/install_scripts.py index 977a6829..e66b13a1 100644 --- a/distutils/command/install_scripts.py +++ b/distutils/command/install_scripts.py @@ -13,7 +13,6 @@ class install_scripts(Command): - description = "install scripts (Python or otherwise)" user_options = [ diff --git a/distutils/command/py37compat.py b/distutils/command/py37compat.py deleted file mode 100644 index aa0c0a7f..00000000 --- a/distutils/command/py37compat.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys - - -def _pythonlib_compat(): - """ - On Python 3.7 and earlier, distutils would include the Python - library. See pypa/distutils#9. - """ - from distutils import sysconfig - - if not sysconfig.get_config_var('Py_ENABLED_SHARED'): - return - - yield 'python{}.{}{}'.format( - sys.hexversion >> 24, - (sys.hexversion >> 16) & 0xFF, - sysconfig.get_config_var('ABIFLAGS'), - ) - - -def compose(f1, f2): - return lambda *args, **kwargs: f1(f2(*args, **kwargs)) - - -pythonlib = ( - compose(list, _pythonlib_compat) - if sys.version_info < (3, 8) - and sys.platform != 'darwin' - and sys.platform[:3] != 'aix' - else list -) diff --git a/distutils/command/register.py b/distutils/command/register.py index 2506dedc..ee6c54da 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -13,11 +13,11 @@ from distutils._log import log from warnings import warn +from .._itertools import always_iterable from ..core import PyPIRCCommand class register(PyPIRCCommand): - description = "register the distribution with the Python package index" user_options = PyPIRCCommand.user_options + [ ('list-classifiers', None, 'list the valid Trove classifiers'), @@ -78,7 +78,7 @@ def check_metadata(self): check.run() def _set_config(self): - '''Reads the configuration file and set attributes.''' + """Reads the configuration file and set attributes.""" config = self._read_pypirc() if config != {}: self.username = config['username'] @@ -94,19 +94,19 @@ def _set_config(self): self.has_config = False def classifiers(self): - '''Fetch the list of classifiers from the server.''' + """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 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. + """Send the metadata to the package index server. Well, do the following: 1. figure who the user is, and then @@ -132,7 +132,7 @@ def send_metadata(self): # noqa: C901 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: @@ -147,13 +147,13 @@ def send_metadata(self): # noqa: C901 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]: ''', +Your selection [default 1]: """, logging.INFO, ) choice = input() @@ -175,7 +175,7 @@ def send_metadata(self): # noqa: C901 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('Server response ({}): {}'.format(code, result), logging.INFO) + self.announce(f'Server response ({code}): {result}', logging.INFO) # possibly save the login if code == 200: @@ -263,7 +263,7 @@ def build_post_data(self, action): return data def post_to_server(self, data, auth=None): # noqa: C901 - '''Post a query to the server, and return a string response.''' + """Post a query to the server, and return a string response.""" if 'name' in data: self.announce( 'Registering {} to {}'.format(data['name'], self.repository), @@ -274,12 +274,8 @@ def post_to_server(self, data, auth=None): # noqa: C901 sep_boundary = '\n--' + boundary end_boundary = sep_boundary + '--' body = io.StringIO() - for key, value in data.items(): - # handle multiple entries for the same name - if type(value) not in (type([]), type(())): - value = [value] - for value in value: - value = str(value) + for key, values in data.items(): + for value in map(str, make_iterable(values)): body.write(sep_boundary) body.write('\nContent-Disposition: form-data; name="%s"' % key) body.write("\n\n") @@ -319,3 +315,9 @@ def post_to_server(self, data, auth=None): # noqa: C901 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/sdist.py b/distutils/command/sdist.py index 2cb4e55c..387d27c9 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -7,6 +7,7 @@ from distutils import archive_util, dir_util, file_util from distutils._log import log from glob import glob +from itertools import filterfalse from warnings import warn from ..core import Command @@ -31,7 +32,6 @@ def show_formats(): class sdist(Command): - description = "create a source distribution (tarball, zip file, etc.)" def checking_metadata(self): @@ -61,7 +61,7 @@ def checking_metadata(self): ( 'manifest-only', 'o', - "just regenerate the manifest and then stop " "(implies --force-manifest)", + "just regenerate the manifest and then stop (implies --force-manifest)", ), ( 'force-manifest', @@ -78,7 +78,7 @@ def checking_metadata(self): ( 'dist-dir=', 'd', - "directory to put the source distribution archive(s) in " "[default: dist]", + "directory to put the source distribution archive(s) in [default: dist]", ), ( 'metadata-check', @@ -233,7 +233,7 @@ def add_defaults(self): """Add all the default files to self.filelist: - README or README.txt - setup.py - - test/test*.py + - tests/test*.py and test/test*.py - all pure Python modules mentioned in setup script - all files pointed by package_data (build_py) - all files defined in data_files. @@ -291,7 +291,7 @@ def _add_defaults_standards(self): self.warn("standard file '%s' not found" % fn) def _add_defaults_optional(self): - optional = ['test/test*.py', 'setup.cfg'] + optional = ['tests/test*.py', 'test/test*.py', 'setup.cfg'] for pattern in optional: files = filter(os.path.isfile, glob(pattern)) self.filelist.extend(files) @@ -308,7 +308,7 @@ def _add_defaults_python(self): # getting package_data files # (computed in build_py.data_files by build_py.finalize_options) - for pkg, src_dir, build_dir, filenames in build_py.data_files: + for _pkg, src_dir, _build_dir, filenames in build_py.data_files: for filename in filenames: self.filelist.append(os.path.join(src_dir, filename)) @@ -428,11 +428,8 @@ def _manifest_is_not_generated(self): if not os.path.isfile(self.manifest): return False - fp = open(self.manifest) - try: - first_line = fp.readline() - finally: - fp.close() + with open(self.manifest, encoding='utf-8') as fp: + first_line = next(fp) return first_line != '# file GENERATED by distutils, do NOT edit\n' def read_manifest(self): @@ -441,13 +438,11 @@ def read_manifest(self): distribution. """ log.info("reading manifest file '%s'", self.manifest) - with open(self.manifest) as manifest: - for line in manifest: + with open(self.manifest, encoding='utf-8') as lines: + self.filelist.extend( # ignore comments and blank lines - line = line.strip() - if line.startswith('#') or not line: - continue - self.filelist.append(line) + filter(None, filterfalse(is_comment, map(str.strip, lines))) + ) def make_release_tree(self, base_dir, files): """Create the directory tree that will become the source @@ -527,3 +522,7 @@ def get_archive_files(self): was run, or None if the command hasn't run yet. """ return self.archive_files + + +def is_comment(line): + return line.startswith('#') diff --git a/distutils/command/upload.py b/distutils/command/upload.py index 3520b3d1..cf541f8a 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -13,6 +13,7 @@ from urllib.parse import urlparse from urllib.request import HTTPError, Request, urlopen +from .._itertools import always_iterable from ..core import PyPIRCCommand from ..errors import DistutilsError, DistutilsOptionError from ..spawn import spawn @@ -27,7 +28,6 @@ class upload(PyPIRCCommand): - description = "upload binary package to PyPI" user_options = PyPIRCCommand.user_options + [ @@ -152,12 +152,9 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 sep_boundary = b'\r\n--' + boundary.encode('ascii') end_boundary = sep_boundary + b'--\r\n' body = io.BytesIO() - for key, value in data.items(): + for key, values in data.items(): title = '\r\nContent-Disposition: form-data; name="%s"' % key - # handle multiple entries for the same name - if not isinstance(value, list): - value = [value] - for value in value: + for value in make_iterable(values): if type(value) is tuple: title += '; filename="%s"' % value[0] value = value[1] @@ -170,7 +167,7 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 body.write(end_boundary) body = body.getvalue() - msg = "Submitting {} to {}".format(filename, self.repository) + msg = f"Submitting {filename} to {self.repository}" self.announce(msg, logging.INFO) # build the Request @@ -194,14 +191,18 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 raise if status == 200: - self.announce( - 'Server response ({}): {}'.format(status, reason), logging.INFO - ) + 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 = 'Upload failed ({}): {}'.format(status, reason) + 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/compat/__init__.py b/distutils/compat/__init__.py new file mode 100644 index 00000000..e12534a3 --- /dev/null +++ b/distutils/compat/__init__.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from .py38 import removeprefix + + +def consolidate_linker_args(args: list[str]) -> list[str] | str: + """ + Ensure the return value is a string for backward compatibility. + + Retain until at least 2025-04-31. See pypa/distutils#246 + """ + + if not all(arg.startswith('-Wl,') for arg in args): + return args + return '-Wl,' + ','.join(removeprefix(arg, '-Wl,') for arg in args) diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py new file mode 100644 index 00000000..2d442111 --- /dev/null +++ b/distutils/compat/py38.py @@ -0,0 +1,33 @@ +import sys + +if sys.version_info < (3, 9): + + def removesuffix(self, suffix): + # suffix='' should not call self[:-0]. + if suffix and self.endswith(suffix): + return self[: -len(suffix)] + else: + return self[:] + + def removeprefix(self, prefix): + if self.startswith(prefix): + return self[len(prefix) :] + else: + return self[:] +else: + + def removesuffix(self, suffix): + return self.removesuffix(suffix) + + def removeprefix(self, prefix): + return self.removeprefix(prefix) + + +def aix_platform(osname, version, release): + try: + import _aix_support # type: ignore + + return _aix_support.aix_platform() + except ImportError: + pass + return f"{osname}-{version}.{release}" diff --git a/distutils/compat/py39.py b/distutils/compat/py39.py new file mode 100644 index 00000000..1b436d76 --- /dev/null +++ b/distutils/compat/py39.py @@ -0,0 +1,66 @@ +import functools +import itertools +import platform +import sys + + +def add_ext_suffix_39(vars): + """ + Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130 + """ + import _imp + + ext_suffix = _imp.extension_suffixes()[0] + vars.update( + EXT_SUFFIX=ext_suffix, + # sysconfig sets SO to match EXT_SUFFIX, so maintain + # that expectation. + # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673 + SO=ext_suffix, + ) + + +needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows' +add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None + + +# from more_itertools +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format(*details) + + super().__init__(msg) + + +# from more_itertools +def _zip_equal_generator(iterables): + _marker = object() + for combo in itertools.zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +# from more_itertools +def _zip_equal(*iterables): + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_size: + raise UnequalIterablesError(details=(first_size, i, size)) + # All sizes are equal, we can use the built-in zip. + return zip(*iterables) + # If any one of the iterables didn't have a length, start reading + # them until one runs out. + except TypeError: + return _zip_equal_generator(iterables) + + +zip_strict = ( + _zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True) +) diff --git a/distutils/compilers/C/Unix.py b/distutils/compilers/C/Unix.py index 82bad4b0..5d46b56b 100644 --- a/distutils/compilers/C/Unix.py +++ b/distutils/compilers/C/Unix.py @@ -13,6 +13,8 @@ * link shared library handled by 'cc -shared' """ +from __future__ import annotations + import itertools import os import re @@ -22,7 +24,8 @@ from ... import sysconfig from ..._log import log from ..._macos_compat import compiler_fixup -from ...dep_util import newer +from ..._modified import newer +from ...compat import consolidate_linker_args from ...errors import CompileError, DistutilsExecError, LibError, LinkError from . import base from .base import gen_lib_options, gen_preprocess_options @@ -104,7 +107,6 @@ def _linker_params(linker_cmd, compiler_cmd): class Compiler(base.Compiler): - compiler_type = 'unix' # These are used by CCompiler in two places: the constructor sets @@ -283,10 +285,9 @@ def _is_gcc(self): compiler = os.path.basename(shlex.split(cc_var)[0]) return "gcc" in compiler or "g++" in compiler - def runtime_library_dir_option(self, dir): + def runtime_library_dir_option(self, dir: str) -> str | list[str]: # XXX Hackish, at the very least. See Python bug #445902: - # http://sourceforge.net/tracker/index.php - # ?func=detail&aid=445902&group_id=5470&atid=105470 + # https://bugs.python.org/issue445902 # Linkers on different platforms need different options to # specify that directories need to be added to the list of # directories searched for dependencies when a dynamic library @@ -313,13 +314,14 @@ def runtime_library_dir_option(self, dir): "-L" + dir, ] - # For all compilers, `-Wl` is the presumed way to - # pass a compiler option to the linker and `-R` is - # the way to pass an RPATH. + # For all compilers, `-Wl` is the presumed way to pass a + # compiler option to the linker if sysconfig.get_config_var("GNULD") == "yes": - # GNU ld needs an extra option to get a RUNPATH - # instead of just an RPATH. - return "-Wl,--enable-new-dtags,-R" + dir + return consolidate_linker_args([ + # Force RUNPATH instead of RPATH + "-Wl,--enable-new-dtags", + "-Wl,-rpath," + dir, + ]) else: return "-Wl,-R" + dir @@ -391,10 +393,7 @@ def find_library_file(self, dirs, lib, debug=0): roots = map(self._library_root, dirs) - searched = ( - os.path.join(root, lib_name) - for root, lib_name in itertools.product(roots, lib_names) - ) + searched = itertools.starmap(os.path.join, itertools.product(roots, lib_names)) found = filter(os.path.exists, searched) diff --git a/distutils/compilers/C/base.py b/distutils/compilers/C/base.py index 896dcf09..eb1b5506 100644 --- a/distutils/compilers/C/base.py +++ b/distutils/compilers/C/base.py @@ -5,9 +5,11 @@ import os import re import sys +import warnings +from ..._itertools import always_iterable from ..._log import log -from ...dep_util import newer_group +from ..._modified import newer_group from ...dir_util import mkpath from ...errors import ( CompileError, @@ -166,8 +168,7 @@ class (via the 'executables' class attribute), but most will have: for key in kwargs: if key not in self.executables: raise ValueError( - "unknown executable '%s' for class %s" - % (key, self.__class__.__name__) + f"unknown executable '{key}' for class {self.__class__.__name__}" ) self.set_executable(key, kwargs[key]) @@ -380,14 +381,14 @@ def _fix_compile_args(self, output_dir, macros, include_dirs): raise TypeError("'output_dir' must be a string or None") if macros is None: - macros = self.macros + macros = list(self.macros) elif isinstance(macros, list): macros = macros + (self.macros or []) else: raise TypeError("'macros' (if supplied) must be a list of tuples") if include_dirs is None: - include_dirs = self.include_dirs + include_dirs = list(self.include_dirs) elif isinstance(include_dirs, (list, tuple)): include_dirs = list(include_dirs) + (self.include_dirs or []) else: @@ -439,14 +440,14 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): fixed versions of all arguments. """ if libraries is None: - libraries = self.libraries + libraries = list(self.libraries) elif isinstance(libraries, (list, tuple)): libraries = list(libraries) + (self.libraries or []) else: raise TypeError("'libraries' (if supplied) must be a list of strings") if library_dirs is None: - library_dirs = self.library_dirs + library_dirs = list(self.library_dirs) elif isinstance(library_dirs, (list, tuple)): library_dirs = list(library_dirs) + (self.library_dirs or []) else: @@ -456,14 +457,14 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): library_dirs += self.__class__.library_dirs if runtime_library_dirs is None: - runtime_library_dirs = self.runtime_library_dirs + runtime_library_dirs = list(self.runtime_library_dirs) elif isinstance(runtime_library_dirs, (list, tuple)): runtime_library_dirs = list(runtime_library_dirs) + ( self.runtime_library_dirs or [] ) else: raise TypeError( - "'runtime_library_dirs' (if supplied) " "must be a list of strings" + "'runtime_library_dirs' (if supplied) must be a list of strings" ) return (libraries, library_dirs, runtime_library_dirs) @@ -823,9 +824,19 @@ def has_function( # noqa: C901 libraries=None, library_dirs=None, ): - """Return a boolean indicating whether funcname is supported on - the current platform. The optional arguments can be used to - augment the compilation environment. + """Return a boolean indicating whether funcname is provided as + a symbol on the current platform. The optional arguments can + be used to augment the compilation environment. + + The libraries argument is a list of flags to be passed to the + linker to make additional symbol definitions available for + linking. + + The includes and include_dirs arguments are deprecated. + Usually, supplying include files with function declarations + will cause function detection to fail even in cases where the + symbol is available for linking. + """ # this can't be included at module scope because it tries to # import math which might not be available at that point - maybe @@ -834,17 +845,37 @@ def has_function( # noqa: C901 if includes is None: includes = [] + else: + warnings.warn("includes is deprecated", DeprecationWarning) if include_dirs is None: include_dirs = [] + else: + warnings.warn("include_dirs is deprecated", DeprecationWarning) if libraries is None: libraries = [] if library_dirs is None: library_dirs = [] fd, fname = tempfile.mkstemp(".c", funcname, text=True) - f = os.fdopen(fd, "w") - try: + with os.fdopen(fd, "w", encoding='utf-8') as f: for incl in includes: f.write("""#include "%s"\n""" % incl) + if not includes: + # Use "char func(void);" as the prototype to follow + # what autoconf does. This prototype does not match + # any well-known function the compiler might recognize + # as a builtin, so this ends up as a true link test. + # Without a fake prototype, the test would need to + # know the exact argument types, and the has_function + # interface does not provide that level of information. + f.write( + """\ +#ifdef __cplusplus +extern "C" +#endif +char %s(void); +""" + % funcname + ) f.write( """\ int main (int argc, char **argv) { @@ -854,8 +885,7 @@ def has_function( # noqa: C901 """ % funcname ) - finally: - f.close() + try: objects = self.compile([fname], include_dirs=include_dirs) except CompileError: @@ -870,7 +900,9 @@ def has_function( # noqa: C901 except (LinkError, TypeError): return False else: - os.remove(os.path.join(self.output_dir or '', "a.out")) + os.remove( + self.executable_filename("a.out", output_dir=self.output_dir or '') + ) finally: for fn in objects: os.remove(fn) @@ -937,9 +969,7 @@ def _make_out_path(self, output_dir, strip_dir, src_name): try: new_ext = self.out_extensions[ext] except LookupError: - raise UnknownFileError( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: base = os.path.basename(base) return os.path.join(output_dir, base + new_ext) @@ -969,7 +999,11 @@ def executable_filename(self, basename, strip_dir=0, output_dir=''): return os.path.join(output_dir, basename + (self.exe_extension or '')) def library_filename( - self, libname, lib_type='static', strip_dir=0, output_dir='' # or 'shared' + self, + libname, + lib_type='static', + strip_dir=0, + output_dir='', # or 'shared' ): assert output_dir is not None expected = '"static", "shared", "dylib", "xcode_stub"' @@ -1021,6 +1055,7 @@ def mkpath(self, name, mode=0o777): # on a cygwin built python we can use gcc like an ordinary UNIXish # compiler ('cygwin.*', 'unix'), + ('zos', 'zos'), # OS name mappings ('posix', 'unix'), ('nt', 'msvc'), @@ -1068,6 +1103,7 @@ def get_default_compiler(osname=None, platform=None): "Mingw32 port of GNU C Compiler for Win32", ), 'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"), + 'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'), } @@ -1124,8 +1160,8 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0): ) except KeyError: raise DistutilsModuleError( - "can't compile C/C++ code: unable to find class '%s' " - "in module '%s'" % (class_name, module_name) + f"can't compile C/C++ code: unable to find class '{class_name}' " + f"in module '{module_name}'" ) # XXX The None is necessary to preserve backwards compatibility @@ -1172,7 +1208,7 @@ def gen_preprocess_options(macros, include_dirs): # XXX *don't* need to be clever about quoting the # macro value here, because we're going to avoid the # shell at all costs when we spawn the command! - pp_opts.append("-D%s=%s" % macro) + pp_opts.append("-D{}={}".format(*macro)) for dir in include_dirs: pp_opts.append("-I%s" % dir) @@ -1192,11 +1228,7 @@ def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries): lib_opts.append(compiler.library_dir_option(dir)) for dir in runtime_library_dirs: - opt = compiler.runtime_library_dir_option(dir) - if isinstance(opt, list): - lib_opts = lib_opts + opt - else: - lib_opts.append(opt) + lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir))) # XXX it's important that we *not* remove redundant library mentions! # sometimes you really do have to say "-lfoo -lbar -lfoo" in order to @@ -1212,7 +1244,7 @@ def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries): lib_opts.append(lib_file) else: compiler.warn( - "no library file corresponding to " "'%s' found (skipping)" % lib + "no library file corresponding to '%s' found (skipping)" % lib ) else: lib_opts.append(compiler.library_option(lib)) diff --git a/distutils/compilers/C/cygwin.py b/distutils/compilers/C/cygwin.py index 87613adb..ba9cd2ad 100644 --- a/distutils/compilers/C/cygwin.py +++ b/distutils/compilers/C/cygwin.py @@ -8,6 +8,7 @@ import copy import os +import pathlib import re import shlex import sys @@ -42,7 +43,7 @@ # VS2013 / MSVC 12.0 1800: ['msvcr120'], # VS2015 / MSVC 14.0 - 1900: ['ucrt', 'vcruntime140'], + 1900: ['vcruntime140'], 2000: RangeMap.undefined_value, }, ) @@ -83,13 +84,10 @@ class Compiler(Unix.Compiler): exe_extension = ".exe" def __init__(self, verbose=0, dry_run=0, force=0): - super().__init__(verbose, dry_run, force) status, details = check_config_h() - self.debug_print( - "Python's GCC status: {} (details: {})".format(status, details) - ) + self.debug_print(f"Python's GCC status: {status} (details: {details})") if status is not CONFIG_H_OK: self.warn( "Python's pyconfig.h doesn't seem to support your compiler. " @@ -108,7 +106,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mcygwin -mdll -O -Wall' % self.cc, compiler_cxx='%s -mcygwin -O -Wall' % self.cxx, linker_exe='%s -mcygwin' % self.cc, - linker_so=('{} -mcygwin {}'.format(self.linker_dll, shared_option)), + linker_so=(f'{self.linker_dll} -mcygwin {shared_option}'), ) # Include the appropriate MSVC runtime library if Python was built @@ -117,7 +115,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): @property def gcc_version(self): - # Older numpy dependend on this existing to check for ancient + # Older numpy depended on this existing to check for ancient # gcc versions. This doesn't make much sense with clang etc so # just hardcode to something recent. # https://github.com/numpy/numpy/pull/20333 @@ -132,7 +130,7 @@ def gcc_version(self): def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): """Compiles the source by spawning GCC and windres if needed.""" - if ext == '.rc' or ext == '.res': + if ext in ('.rc', '.res'): # gcc needs '.res' and '.rc' compiled to object files !!! try: self.spawn(["windres", "-i", src, "-o", obj]) @@ -267,7 +265,6 @@ class MinGW32Compiler(Compiler): compiler_type = 'mingw32' def __init__(self, verbose=0, dry_run=0, force=0): - super().__init__(verbose, dry_run, force) shared_option = "-shared" @@ -280,7 +277,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mdll -O -Wall' % self.cc, compiler_cxx='%s -O -Wall' % self.cxx, linker_exe='%s' % self.cc, - linker_so='{} {}'.format(self.linker_dll, shared_option), + linker_so=f'{self.linker_dll} {shared_option}', ) def runtime_library_dir_option(self, dir): @@ -331,20 +328,21 @@ def check_config_h(): # let's see if __GNUC__ is mentioned in python.h fn = sysconfig.get_config_h_filename() try: - config_h = open(fn) - try: - if "__GNUC__" in config_h.read(): - return CONFIG_H_OK, "'%s' mentions '__GNUC__'" % fn - else: - return CONFIG_H_NOTOK, "'%s' does not mention '__GNUC__'" % fn - finally: - config_h.close() + config_h = pathlib.Path(fn).read_text(encoding='utf-8') + substring = '__GNUC__' + if substring in config_h: + code = CONFIG_H_OK + mention_inflected = 'mentions' + else: + code = CONFIG_H_NOTOK + mention_inflected = 'does not mention' + return code, f"{fn!r} {mention_inflected} {substring!r}" except OSError as exc: - return (CONFIG_H_UNCERTAIN, "couldn't read '{}': {}".format(fn, exc.strerror)) + return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}") def is_cygwincc(cc): - '''Try to determine if the compiler that would be used is from cygwin.''' + """Try to determine if the compiler that would be used is from cygwin.""" out_string = check_output(shlex.split(cc) + ['-dumpmachine']) return out_string.strip().endswith(b'cygwin') diff --git a/distutils/config.py b/distutils/config.py index 9a4044ad..83f96a9e 100644 --- a/distutils/config.py +++ b/distutils/config.py @@ -3,6 +3,8 @@ 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 @@ -41,7 +43,8 @@ def _get_rc_file(self): def _store_pypirc(self, username, password): """Creates a default .pypirc file.""" rc = self._get_rc_file() - with os.fdopen(os.open(rc, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: + 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 @@ -52,7 +55,7 @@ def _read_pypirc(self): # noqa: C901 repository = self.repository or self.DEFAULT_REPOSITORY config = RawConfigParser() - config.read(rc) + config.read(rc, encoding='utf-8') sections = config.sections() if 'distutils' in sections: # let's get the list of servers @@ -119,11 +122,8 @@ def _read_pypirc(self): # noqa: C901 def _read_pypi_response(self, response): """Read and decode a PyPI HTTP response.""" - import cgi - content_type = response.getheader('content-type', 'text/plain') - encoding = cgi.parse_header(content_type)[1].get('charset', 'ascii') - return response.read().decode(encoding) + return response.read().decode(_extract_encoding(content_type)) def initialize_options(self): """Initialize options.""" @@ -137,3 +137,15 @@ def finalize_options(self): 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 8132689a..309ce696 100644 --- a/distutils/core.py +++ b/distutils/core.py @@ -131,7 +131,7 @@ class found in 'cmdclass' is used in place of the default, which is # our Distribution (see below). klass = attrs.get('distclass') if klass: - del attrs['distclass'] + attrs.pop('distclass') else: klass = Distribution @@ -202,10 +202,10 @@ def run_commands(dist): raise SystemExit("interrupted") except OSError as exc: if DEBUG: - sys.stderr.write("error: {}\n".format(exc)) + sys.stderr.write(f"error: {exc}\n") raise else: - raise SystemExit("error: {}".format(exc)) + raise SystemExit(f"error: {exc}") except (DistutilsError, CCompilerError) as msg: if DEBUG: @@ -248,7 +248,7 @@ def run_setup(script_name, script_args=None, stop_after="run"): used to drive the Distutils. """ if stop_after not in ('init', 'config', 'commandline', 'run'): - raise ValueError("invalid value for 'stop_after': {!r}".format(stop_after)) + raise ValueError(f"invalid value for 'stop_after': {stop_after!r}") global _setup_stop_after, _setup_distribution _setup_stop_after = stop_after diff --git a/distutils/dep_util.py b/distutils/dep_util.py index bee05ffa..09a8a2e1 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -1,97 +1,14 @@ -"""distutils.dep_util +import warnings -Utility functions for simple, timestamp-based dependency of files -and groups of files; also, function based entirely on such -timestamp dependency analysis.""" +from . import _modified -import os -from .errors import DistutilsFileError - - -def newer(source, target): - """Return true if 'source' exists and is more recently modified than - 'target', or if 'source' exists and 'target' doesn't. Return false if - both exist and 'target' is the same age or younger than 'source'. - Raise DistutilsFileError if 'source' does not exist. - """ - if not os.path.exists(source): - raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) - if not os.path.exists(target): - return 1 - - from stat import ST_MTIME - - mtime1 = os.stat(source)[ST_MTIME] - mtime2 = os.stat(target)[ST_MTIME] - - return mtime1 > mtime2 - - -# newer () - - -def newer_pairwise(sources, targets): - """Walk two filename lists in parallel, testing if each source is newer - than its corresponding target. Return a pair of lists (sources, - targets) where source is newer than target, according to the semantics - of 'newer()'. - """ - if len(sources) != len(targets): - raise ValueError("'sources' and 'targets' must be same length") - - # build a pair of lists (sources, targets) where source is newer - n_sources = [] - n_targets = [] - for i in range(len(sources)): - if newer(sources[i], targets[i]): - n_sources.append(sources[i]) - n_targets.append(targets[i]) - - return (n_sources, n_targets) - - -# newer_pairwise () - - -def newer_group(sources, target, missing='error'): - """Return true if 'target' is out-of-date with respect to any file - listed in 'sources'. In other words, if 'target' exists and is newer - than every file in 'sources', return false; otherwise return true. - 'missing' controls what we do when a source file is missing; the - default ("error") is to blow up with an OSError from inside 'stat()'; - if it is "ignore", we silently drop any missing source files; if it is - "newer", any missing source files make us assume that 'target' is - out-of-date (this is handy in "dry-run" mode: it'll make you pretend to - carry out commands that wouldn't work because inputs are missing, but - that doesn't matter because you're not actually going to run the - commands). - """ - # If the target doesn't even exist, then it's definitely out-of-date. - if not os.path.exists(target): - return 1 - - # Otherwise we have to find out the hard way: if *any* source file - # is more recent than 'target', then 'target' is out-of-date and - # we can immediately return true. If we fall through to the end - # of the loop, then 'target' is up-to-date and we return false. - from stat import ST_MTIME - - target_mtime = os.stat(target)[ST_MTIME] - for source in sources: - if not os.path.exists(source): - if missing == 'error': # blow up when we stat() the file - pass - elif missing == 'ignore': # missing source dropped from - continue # target's dependency list - elif missing == 'newer': # missing source means target is - return 1 # out-of-date - - source_mtime = os.stat(source)[ST_MTIME] - if source_mtime > target_mtime: - return 1 - else: - return 0 - - -# newer_group () +def __getattr__(name): + if name not in ['newer', 'newer_group', 'newer_pairwise']: + raise AttributeError(name) + warnings.warn( + "dep_util is Deprecated. Use functions from setuptools instead.", + DeprecationWarning, + stacklevel=2, + ) + return getattr(_modified, name) diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 8fa44dc9..370c6ffd 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -34,9 +34,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 # Detect a common bug -- name is None if not isinstance(name, str): - raise DistutilsInternalError( - "mkpath: 'name' must be a string (got {!r})".format(name) - ) + 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 @@ -77,7 +75,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 except OSError as exc: if not (exc.errno == errno.EEXIST and os.path.isdir(head)): raise DistutilsFileError( - "could not create '{}': {}".format(head, exc.args[-1]) + f"could not create '{head}': {exc.args[-1]}" ) created_dirs.append(head) @@ -96,9 +94,7 @@ def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0): 'dry_run' flags are as for 'mkpath()'. """ # First get the list of directories to create - need_dir = set() - for file in files: - need_dir.add(os.path.join(base_dir, os.path.dirname(file))) + need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files) # Now create them for dir in sorted(need_dir): @@ -144,9 +140,7 @@ def copy_tree( # noqa: C901 if dry_run: names = [] else: - raise DistutilsFileError( - "error listing files in '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}") if not dry_run: mkpath(dst, verbose=verbose) @@ -228,7 +222,7 @@ def remove_tree(directory, verbose=1, dry_run=0): # remove dir from cache if it's already there abspath = os.path.abspath(cmd[1]) if abspath in _path_created: - del _path_created[abspath] + _path_created.pop(abspath) except OSError as exc: log.warning("error removing %s: %s", directory, exc) diff --git a/distutils/dist.py b/distutils/dist.py index 8179e96b..668ce7eb 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -10,6 +10,7 @@ import pathlib import re import sys +from collections.abc import Iterable from email import message_from_file try: @@ -237,9 +238,9 @@ def __init__(self, attrs=None): # noqa: C901 options = attrs.get('options') if options is not None: del attrs['options'] - for (command, cmd_options) in options.items(): + for command, cmd_options in options.items(): opt_dict = self.get_option_dict(command) - for (opt, val) in cmd_options.items(): + for opt, val in cmd_options.items(): opt_dict[opt] = ("setup script", val) if 'licence' in attrs: @@ -253,7 +254,7 @@ def __init__(self, attrs=None): # noqa: C901 # Now work on the rest of the attributes. Any attribute that's # not already defined is invalid! - for (key, val) in attrs.items(): + for key, val in attrs.items(): if hasattr(self.metadata, "set_" + key): getattr(self.metadata, "set_" + key)(val) elif hasattr(self.metadata, key): @@ -395,7 +396,7 @@ def parse_config_files(self, filenames=None): # noqa: C901 for filename in filenames: if DEBUG: self.announce(" reading %s" % filename) - parser.read(filename) + parser.read(filename, encoding='utf-8') for section in parser.sections(): options = parser.options(section) opt_dict = self.get_option_dict(section) @@ -414,7 +415,7 @@ def parse_config_files(self, filenames=None): # noqa: C901 # to set Distribution options. if 'global' in self.command_options: - for (opt, (src, val)) in self.command_options['global'].items(): + for opt, (_src, val) in self.command_options['global'].items(): alias = self.negative_opt.get(opt) try: if alias: @@ -585,16 +586,15 @@ def _parse_command_opts(self, parser, args): # noqa: C901 cmd_class.help_options, list ): help_option_found = 0 - for (help_option, short, desc, func) in cmd_class.help_options: + for help_option, _short, _desc, func in cmd_class.help_options: if hasattr(opts, parser.get_attr_name(help_option)): help_option_found = 1 if callable(func): func() else: raise DistutilsClassError( - "invalid help function %r for help option '%s': " + f"invalid help function {func!r} for help option '{help_option}': " "must be a callable object (function, etc.)" - % (func, help_option) ) if help_option_found: @@ -603,7 +603,7 @@ def _parse_command_opts(self, parser, args): # noqa: C901 # Put the options from the command-line into their official # holding pen, the 'command_options' dictionary. opt_dict = self.get_option_dict(command) - for (name, value) in vars(opts).items(): + for name, value in vars(opts).items(): opt_dict[name] = ("command line", value) return args @@ -621,7 +621,9 @@ def finalize_options(self): value = [elm.strip() for elm in value.split(',')] setattr(self.metadata, attr, value) - def _show_help(self, parser, global_options=1, display_options=1, commands=[]): + def _show_help( + self, parser, global_options=1, display_options=1, commands: Iterable = () + ): """Show help for the setup script command-line in the form of several lists of command-line options. 'parser' should be a FancyGetopt instance; do not expect it to be returned in the @@ -645,7 +647,7 @@ def _show_help(self, parser, global_options=1, display_options=1, commands=[]): options = self.global_options parser.set_option_table(options) parser.print_help(self.common_usage + "\nGlobal options:") - print('') + print() if display_options: parser.set_option_table(self.display_options) @@ -653,7 +655,7 @@ def _show_help(self, parser, global_options=1, display_options=1, commands=[]): "Information display options (just display " + "information, ignore any commands)" ) - print('') + print() for command in self.commands: if isinstance(command, type) and issubclass(command, Command): @@ -667,7 +669,7 @@ def _show_help(self, parser, global_options=1, display_options=1, commands=[]): else: parser.set_option_table(klass.user_options) parser.print_help("Options for '%s' command:" % klass.__name__) - print('') + print() print(gen_usage(self.script_name)) @@ -684,7 +686,7 @@ def handle_display_options(self, option_order): # we ignore "foo bar"). if self.help_commands: self.print_commands() - print('') + print() print(gen_usage(self.script_name)) return 1 @@ -696,11 +698,11 @@ def handle_display_options(self, option_order): for option in self.display_options: is_display_option[option[0]] = 1 - for (opt, val) in option_order: + for opt, val in option_order: if val and is_display_option.get(opt): opt = translate_longopt(opt) value = getattr(self.metadata, "get_" + opt)() - if opt in ['keywords', 'platforms']: + if opt in ('keywords', 'platforms'): print(','.join(value)) elif opt in ('classifiers', 'provides', 'requires', 'obsoletes'): print('\n'.join(value)) @@ -821,7 +823,7 @@ def get_command_class(self, command): return klass for pkgname in self.get_command_packages(): - module_name = "{}.{}".format(pkgname, command) + module_name = f"{pkgname}.{command}" klass_name = command try: @@ -834,8 +836,7 @@ def get_command_class(self, command): klass = getattr(module, klass_name) except AttributeError: raise DistutilsModuleError( - "invalid command '%s' (no class '%s' in module '%s')" - % (command, klass_name, module_name) + f"invalid command '{command}' (no class '{klass_name}' in module '{module_name}')" ) self.cmdclass[command] = klass @@ -887,9 +888,9 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 if DEBUG: self.announce(" setting options for '%s' command:" % command_name) - for (option, (source, value)) in option_dict.items(): + for option, (source, value) in option_dict.items(): if DEBUG: - self.announce(" {} = {} (from {})".format(option, value, source)) + self.announce(f" {option} = {value} (from {source})") try: bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] except AttributeError: @@ -909,8 +910,7 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 setattr(command_obj, option, value) else: raise DistutilsOptionError( - "error in %s: command '%s' has no such option '%s'" - % (source, command_name, option) + f"error in {source}: command '{command_name}' has no such option '{option}'" ) except ValueError as msg: raise DistutilsOptionError(msg) @@ -1178,7 +1178,7 @@ def maybe_write(header, val): def _write_list(self, file, name, values): values = values or [] for value in values: - file.write('{}: {}\n'.format(name, value)) + file.write(f'{name}: {value}\n') # -- Metadata query methods ---------------------------------------- @@ -1189,7 +1189,7 @@ def get_version(self): return self.version or "0.0.0" def get_fullname(self): - return "{}-{}".format(self.get_name(), self.get_version()) + return f"{self.get_name()}-{self.get_version()}" def get_author(self): return self.author diff --git a/distutils/extension.py b/distutils/extension.py index 36bed336..94e71635 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -102,7 +102,7 @@ def __init__( depends=None, language=None, optional=None, - **kw # To catch unknown keywords + **kw, # To catch unknown keywords ): if not isinstance(name, str): raise AssertionError("'name' must be a string") @@ -134,12 +134,7 @@ def __init__( warnings.warn(msg) def __repr__(self): - return '<{}.{}({!r}) at {:#x}>'.format( - self.__class__.__module__, - self.__class__.__qualname__, - self.name, - id(self), - ) + return f'<{self.__class__.__module__}.{self.__class__.__qualname__}({self.name!r}) at {id(self):#x}>' def read_setup_file(filename): # noqa: C901 diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 8ebdea3e..e905aede 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -12,6 +12,7 @@ import re import string import sys +from typing import Any, Sequence from .errors import DistutilsArgError, DistutilsGetoptError @@ -23,7 +24,7 @@ longopt_re = re.compile(r'^%s$' % longopt_pat) # For recognizing "negative alias" options, eg. "quiet=!verbose" -neg_alias_re = re.compile("^({})=!({})$".format(longopt_pat, longopt_pat)) +neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$") # This is used to translate long options to legitimate Python identifiers # (for use as attributes of some object). @@ -114,16 +115,14 @@ def get_attr_name(self, long_option): def _check_alias_dict(self, aliases, what): assert isinstance(aliases, dict) - for (alias, opt) in aliases.items(): + for alias, opt in aliases.items(): if alias not in self.option_index: raise DistutilsGetoptError( - ("invalid %s '%s': " "option '%s' not defined") - % (what, alias, alias) + f"invalid {what} '{alias}': " f"option '{alias}' not defined" ) if opt not in self.option_index: raise DistutilsGetoptError( - ("invalid %s '%s': " "aliased option '%s' not defined") - % (what, alias, opt) + f"invalid {what} '{alias}': " f"aliased option '{opt}' not defined" ) def set_aliases(self, alias): @@ -158,13 +157,12 @@ def _grok_option_table(self): # noqa: C901 else: # the option table is part of the code, so simply # assert that it is correct - raise ValueError("invalid option tuple: {!r}".format(option)) + raise ValueError(f"invalid option tuple: {option!r}") # Type- and value-check the option names if not isinstance(long, str) or len(long) < 2: raise DistutilsGetoptError( - ("invalid long option '%s': " "must be a string of length >= 2") - % long + ("invalid long option '%s': must be a string of length >= 2") % long ) if not ((short is None) or (isinstance(short, str) and len(short) == 1)): @@ -188,8 +186,8 @@ def _grok_option_table(self): # noqa: C901 if alias_to is not None: if self.takes_arg[alias_to]: raise DistutilsGetoptError( - "invalid negative alias '%s': " - "aliased option '%s' takes a value" % (long, alias_to) + f"invalid negative alias '{long}': " + f"aliased option '{alias_to}' takes a value" ) self.long_opts[-1] = long # XXX redundant?! @@ -201,9 +199,9 @@ def _grok_option_table(self): # noqa: C901 if alias_to is not None: if self.takes_arg[long] != self.takes_arg[alias_to]: raise DistutilsGetoptError( - "invalid alias '%s': inconsistent with " - "aliased option '%s' (one of them takes a value, " - "the other doesn't" % (long, alias_to) + f"invalid alias '{long}': inconsistent with " + f"aliased option '{alias_to}' (one of them takes a value, " + "the other doesn't" ) # Now enforce some bondage on the long option name, so we can @@ -360,7 +358,7 @@ def generate_help(self, header=None): # noqa: C901 # Case 2: we have a short option, so we have to include it # just after the long option else: - opt_names = "{} (-{})".format(long, short) + opt_names = f"{long} (-{short})" if text: lines.append(" --%-*s %s" % (max_opt, opt_names, text[0])) else: @@ -451,7 +449,7 @@ class OptionDummy: """Dummy class just used as a place to hold command-line option values as instance attributes.""" - def __init__(self, options=[]): + def __init__(self, options: Sequence[Any] = []): """Create a new OptionDummy instance. The attributes listed in 'options' will be initialized to None.""" for opt in options: diff --git a/distutils/file_util.py b/distutils/file_util.py index 8b37b23d..960def9c 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -27,30 +27,24 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fsrc = open(src, 'rb') except OSError as e: - raise DistutilsFileError("could not open '{}': {}".format(src, e.strerror)) + raise DistutilsFileError(f"could not open '{src}': {e.strerror}") if os.path.exists(dst): try: os.unlink(dst) except OSError as e: - raise DistutilsFileError( - "could not delete '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}") try: fdst = open(dst, 'wb') except OSError as e: - raise DistutilsFileError( - "could not create '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not create '{dst}': {e.strerror}") while True: try: buf = fsrc.read(buffer_size) except OSError as e: - raise DistutilsFileError( - "could not read from '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"could not read from '{src}': {e.strerror}") if not buf: break @@ -58,9 +52,7 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fdst.write(buf) except OSError as e: - raise DistutilsFileError( - "could not write to '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}") finally: if fdst: fdst.close() @@ -109,7 +101,7 @@ def copy_file( # noqa: C901 # changing it (ie. it's not already a hard/soft link to src OR # (not update) and (src newer than dst). - from distutils.dep_util import newer + from distutils._modified import newer from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME if not os.path.isfile(src): @@ -177,7 +169,6 @@ def copy_file( # noqa: C901 # XXX I suspect this is Unix-specific -- need porting help! def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 - """Move a file 'src' to 'dst'. If 'dst' is a directory, the file will be moved into it with the same name; otherwise, 'src' is just renamed to 'dst'. Return the new full name of the file. @@ -201,12 +192,12 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 dst = os.path.join(dst, basename(src)) elif exists(dst): raise DistutilsFileError( - "can't move '{}': destination '{}' already exists".format(src, dst) + f"can't move '{src}': destination '{dst}' already exists" ) if not isdir(dirname(dst)): raise DistutilsFileError( - "can't move '{}': destination '{}' not a valid path".format(src, dst) + f"can't move '{src}': destination '{dst}' not a valid path" ) copy_it = False @@ -217,9 +208,7 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 if num == errno.EXDEV: copy_it = True else: - raise DistutilsFileError( - "couldn't move '{}' to '{}': {}".format(src, dst, msg) - ) + raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}") if copy_it: copy_file(src, dst, verbose=verbose) @@ -232,8 +221,8 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 except OSError: pass raise DistutilsFileError( - "couldn't move '%s' to '%s' by copy/delete: " - "delete '%s' failed: %s" % (src, dst, src, msg) + f"couldn't move '{src}' to '{dst}' by copy/delete: " + f"delete '{src}' failed: {msg}" ) return dst @@ -242,9 +231,5 @@ def write_file(filename, contents): """Create a file with the specified name and write 'contents' (a sequence of strings without line terminators) to it. """ - f = open(filename, "w") - try: - for line in contents: - f.write(line + "\n") - finally: - f.close() + with open(filename, 'w', encoding='utf-8') as f: + f.writelines(line + '\n' for line in contents) diff --git a/distutils/filelist.py b/distutils/filelist.py index 56b8d375..71ffb2ab 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -162,9 +162,7 @@ def process_template_line(self, line): # noqa: C901 self.debug_print("recursive-include {} {}".format(dir, ' '.join(patterns))) for pattern in patterns: if not self.include_pattern(pattern, prefix=dir): - msg = ( - "warning: no files found matching '%s' " "under directory '%s'" - ) + msg = "warning: no files found matching '%s' under directory '%s'" log.warning(msg, pattern, dir) elif action == 'recursive-exclude': @@ -189,7 +187,7 @@ def process_template_line(self, line): # noqa: C901 self.debug_print("prune " + dir_pattern) if not self.exclude_pattern(None, prefix=dir_pattern): log.warning( - ("no previously-included directories found " "matching '%s'"), + ("no previously-included directories found matching '%s'"), dir_pattern, ) else: @@ -363,9 +361,9 @@ def translate_pattern(pattern, anchor=1, prefix=None, is_regex=0): if os.sep == '\\': sep = r'\\' pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)] - pattern_re = r'{}\A{}{}.*{}{}'.format(start, prefix_re, sep, pattern_re, end) + pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}' else: # no prefix -- respect anchor flag if anchor: - pattern_re = r'{}\A{}'.format(start, pattern_re[len(start) :]) + pattern_re = rf'{start}\A{pattern_re[len(start) :]}' return re.compile(pattern_re) diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index 6c291325..1e2f7104 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -174,7 +174,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = Reg.get_value(base, r"{}\{}".format(p, key)) + d = Reg.get_value(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -280,7 +280,7 @@ def query_vcvarsall(version, arch="x86"): raise DistutilsPlatformError("Unable to find vcvarsall.bat") log.debug("Calling 'vcvarsall.bat %s' (version=%s)", arch, version) popen = subprocess.Popen( - '"{}" {} & set'.format(vcvarsall, arch), + f'"{vcvarsall}" {arch} & set', stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) @@ -369,9 +369,7 @@ def initialize(self, plat_name=None): # noqa: C901 # sanity check for platforms to prevent obscure errors later. ok_plats = 'win32', 'win-amd64' if plat_name not in ok_plats: - raise DistutilsPlatformError( - "--plat-name must be one of {}".format(ok_plats) - ) + raise DistutilsPlatformError(f"--plat-name must be one of {ok_plats}") if ( "DISTUTILS_USE_SDK" in os.environ @@ -390,7 +388,7 @@ def initialize(self, plat_name=None): # noqa: C901 # to cross compile, you use 'x86_amd64'. # On AMD64, 'vcvars32.bat amd64' is a native build env; to cross # compile use 'x86' (ie, it runs the x86 compiler directly) - if plat_name == get_platform() or plat_name == 'win32': + if plat_name in (get_platform(), 'win32'): # native build or cross-compile to win32 plat_spec = PLAT_TO_VCVARS[plat_name] else: @@ -498,7 +496,6 @@ def compile( # noqa: C901 extra_postargs=None, depends=None, ): - if not self.initialized: self.initialize() compile_info = self._setup_compile( @@ -564,9 +561,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: @@ -585,7 +580,6 @@ def compile( # noqa: C901 def create_static_lib( self, objects, output_libname, output_dir=None, debug=0, target_lang=None ): - if not self.initialized: self.initialize() (objects, output_dir) = self._fix_object_args(objects, output_dir) @@ -618,7 +612,6 @@ def link( # noqa: C901 build_temp=None, target_lang=None, ): - if not self.initialized: self.initialize() (objects, output_dir) = self._fix_object_args(objects, output_dir) @@ -689,7 +682,7 @@ def link( # noqa: C901 mfinfo = self.manifest_get_embed_info(target_desc, ld_args) if mfinfo is not None: mffilename, mfid = mfinfo - out_arg = '-outputresource:{};{}'.format(output_filename, mfid) + out_arg = f'-outputresource:{output_filename};{mfid}' try: self.spawn(['mt.exe', '-nologo', '-manifest', mffilename, out_arg]) except DistutilsExecError as msg: @@ -700,8 +693,8 @@ def link( # noqa: C901 def manifest_setup_ldargs(self, output_filename, build_temp, ld_args): # If we need a manifest at all, an embedded manifest is recommended. # See MSDN article titled - # "How to: Embed a Manifest Inside a C/C++ Application" - # (currently at http://msdn2.microsoft.com/en-us/library/ms235591(VS.80).aspx) + # "Understanding manifest generation for C/C++ programs" + # (currently at https://learn.microsoft.com/en-us/cpp/build/understanding-manifest-generation-for-c-cpp-programs) # Ask the linker to generate the manifest in the temp dir, so # we can check it, and possibly embed it, later. temp_manifest = os.path.join( @@ -712,7 +705,7 @@ def manifest_setup_ldargs(self, output_filename, build_temp, ld_args): def manifest_get_embed_info(self, target_desc, ld_args): # If a manifest should be embedded, return a tuple of # (manifest_filename, resource_id). Returns None if no manifest - # should be embedded. See http://bugs.python.org/issue7833 for why + # should be embedded. See https://bugs.python.org/issue7833 for why # we want to avoid any manifest for extension modules if we can) for arg in ld_args: if arg.startswith("/MANIFESTFILE:"): diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index 8f0beb6d..b4e1f979 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -160,7 +160,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = read_values(base, r"{}\{}".format(p, key)) + d = read_values(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -390,7 +390,6 @@ def compile( # noqa: C901 extra_postargs=None, depends=None, ): - if not self.initialized: self.initialize() compile_info = self._setup_compile( @@ -456,9 +455,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: @@ -477,7 +474,6 @@ def compile( # noqa: C901 def create_static_lib( self, objects, output_libname, output_dir=None, debug=0, target_lang=None ): - if not self.initialized: self.initialize() (objects, output_dir) = self._fix_object_args(objects, output_dir) @@ -510,7 +506,6 @@ def link( # noqa: C901 build_temp=None, target_lang=None, ): - if not self.initialized: self.initialize() (objects, output_dir) = self._fix_object_args(objects, output_dir) @@ -641,14 +636,11 @@ def get_msvc_paths(self, path, platform='x86'): path = path + " dirs" if self.__version >= 7: - key = r"{}\{:0.1f}\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories".format( - self.__root, - self.__version, - ) + key = rf"{self.__root}\{self.__version:0.1f}\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories" else: key = ( - r"%s\6.0\Build System\Components\Platforms" - r"\Win32 (%s)\Directories" % (self.__root, platform) + rf"{self.__root}\6.0\Build System\Components\Platforms" + rf"\Win32 ({platform})\Directories" ) for base in HKEYS: diff --git a/distutils/py38compat.py b/distutils/py38compat.py deleted file mode 100644 index 59224e71..00000000 --- a/distutils/py38compat.py +++ /dev/null @@ -1,8 +0,0 @@ -def aix_platform(osname, version, release): - try: - import _aix_support - - return _aix_support.aix_platform() - except ImportError: - pass - return "{}-{}.{}".format(osname, version, release) diff --git a/distutils/py39compat.py b/distutils/py39compat.py deleted file mode 100644 index 61a90fdd..00000000 --- a/distutils/py39compat.py +++ /dev/null @@ -1,22 +0,0 @@ -import platform -import sys - - -def add_ext_suffix_39(vars): - """ - Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130 - """ - import _imp - - ext_suffix = _imp.extension_suffixes()[0] - vars.update( - EXT_SUFFIX=ext_suffix, - # sysconfig sets SO to match EXT_SUFFIX, so maintain - # that expectation. - # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673 - SO=ext_suffix, - ) - - -needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows' -add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None diff --git a/distutils/spawn.py b/distutils/spawn.py index f44c609e..234d5cd1 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -6,16 +6,42 @@ executable name. """ +from __future__ import annotations + import os +import platform import subprocess import sys +from typing import Mapping from ._log import log from .debug import DEBUG from .errors import DistutilsExecError -def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 +def _debug(cmd): + """ + Render a subprocess command differently depending on DEBUG. + """ + return cmd if DEBUG else cmd[0] + + +def _inject_macos_ver(env: Mapping[str:str] | None) -> Mapping[str:str] | None: + if platform.system() != 'Darwin': + return env + + from distutils.util import MACOSX_VERSION_VAR, get_macosx_target_ver + + target_ver = get_macosx_target_ver() + update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {} + return {**_resolve(env), **update} + + +def _resolve(env: Mapping[str:str] | None) -> Mapping[str:str]: + return os.environ if env is None else env + + +def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None): """Run another program, specified as a command list 'cmd', in a new process. 'cmd' is just the argument list for the new process, ie. @@ -31,10 +57,6 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 Raise DistutilsExecError if running the program fails in any way; just return on success. """ - # cmd is documented as a list, but just in case some code passes a tuple - # in, protect our %-formatting code against horrible death - cmd = list(cmd) - log.info(subprocess.list2cmdline(cmd)) if dry_run: return @@ -44,32 +66,16 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 if executable is not None: cmd[0] = executable - env = env if env is not None else dict(os.environ) - - if sys.platform == 'darwin': - from distutils.util import MACOSX_VERSION_VAR, get_macosx_target_ver - - macosx_target_ver = get_macosx_target_ver() - if macosx_target_ver: - env[MACOSX_VERSION_VAR] = macosx_target_ver - try: - proc = subprocess.Popen(cmd, env=env) - proc.wait() - exitcode = proc.returncode + subprocess.check_call(cmd, env=_inject_macos_ver(env)) except OSError as exc: - if not DEBUG: - cmd = cmd[0] raise DistutilsExecError( - "command {!r} failed: {}".format(cmd, exc.args[-1]) + f"command {_debug(cmd)!r} failed: {exc.args[-1]}" ) from exc - - if exitcode: - if not DEBUG: - cmd = cmd[0] + except subprocess.CalledProcessError as err: raise DistutilsExecError( - "command {!r} failed with exit code {}".format(cmd, exitcode) - ) + f"command {_debug(cmd)!r} failed with exit code {err.returncode}" + ) from err def find_executable(executable, path=None): @@ -87,14 +93,13 @@ def find_executable(executable, path=None): if path is None: path = os.environ.get('PATH', None) + # bpo-35755: Don't fall through if PATH is the empty string if path is None: try: path = os.confstr("CS_PATH") except (AttributeError, ValueError): # os.confstr() or CS_PATH is not available path = os.defpath - # bpo-35755: Don't use os.defpath if the PATH environment variable is - # set to an empty string # PATH='' doesn't match, whereas PATH=':' looks in the current directory if not path: diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 2583d3ec..4ed51c1f 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -9,14 +9,15 @@ Email: """ +import functools import os import pathlib import re import sys import sysconfig -from . import py39compat from ._functools import pass_none +from .compat import py39 from .errors import DistutilsPlatformError IS_PYPY = '__pypy__' in sys.builtin_module_names @@ -130,12 +131,20 @@ def get_python_inc(plat_specific=0, prefix=None): return getter(resolved_prefix, prefix, plat_specific) +@pass_none +def _extant(path): + """ + Replace path with None if it doesn't exist. + """ + return path if os.path.exists(path) else None + + def _get_python_inc_posix(prefix, spec_prefix, plat_specific): if IS_PYPY and sys.version_info < (3, 8): return os.path.join(prefix, 'include') return ( _get_python_inc_posix_python(plat_specific) - or _get_python_inc_from_config(plat_specific, spec_prefix) + or _extant(_get_python_inc_from_config(plat_specific, spec_prefix)) or _get_python_inc_posix_prefix(prefix) ) @@ -187,12 +196,11 @@ def _get_python_inc_posix_prefix(prefix): def _get_python_inc_nt(prefix, spec_prefix, plat_specific): if python_build: - # Include both the include and PC dir to ensure we can find - # pyconfig.h + # Include both include dirs to ensure we can find pyconfig.h return ( os.path.join(prefix, "include") + os.path.pathsep - + os.path.join(prefix, "PC") + + os.path.dirname(sysconfig.get_config_h_filename()) ) return os.path.join(prefix, "include") @@ -259,6 +267,24 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): ) +@functools.lru_cache +def _customize_macos(): + """ + Perform first-time customization of compiler-related + config vars on macOS. Use after a compiler is known + to be needed. This customization exists primarily to support Pythons + from binary installers. The kind and paths to build tools on + the user system may vary significantly from the system + that Python itself was built on. Also the user OS + version and build tools may not support the same set + of CPU architectures for universal builds. + """ + + sys.platform == "darwin" and __import__('_osx_support').customize_compiler( + get_config_vars() + ) + + def customize_compiler(compiler): # noqa: C901 """Do any platform-specific customization of a CCompiler instance. @@ -266,22 +292,7 @@ def customize_compiler(compiler): # noqa: C901 varies across Unices and is stored in Python's Makefile. """ if compiler.compiler_type == "unix": - if sys.platform == "darwin": - # Perform first-time customization of compiler-related - # config vars on OS X now that we know we need a compiler. - # This is primarily to support Pythons from binary - # installers. The kind and paths to build tools on - # the user system may vary significantly from the system - # that Python itself was built on. Also the user OS - # version and build tools may not support the same set - # of CPU architectures for universal builds. - global _config_vars - # Use get_config_var() to ensure _config_vars is initialized. - if not get_config_var('CUSTOMIZED_OSX_COMPILER'): - import _osx_support - - _osx_support.customize_compiler(_config_vars) - _config_vars['CUSTOMIZED_OSX_COMPILER'] = 'True' + _customize_macos() ( cc, @@ -353,14 +364,7 @@ def customize_compiler(compiler): # noqa: C901 def get_config_h_filename(): """Return full pathname of installed pyconfig.h file.""" - if python_build: - if os.name == "nt": - inc_dir = os.path.join(_sys_home or project_base, "PC") - else: - inc_dir = _sys_home or project_base - return os.path.join(inc_dir, 'pyconfig.h') - else: - return sysconfig.get_config_h_filename() + return sysconfig.get_config_h_filename() def get_makefile_filename(): @@ -474,7 +478,6 @@ def parse_makefile(fn, g=None): # noqa: C901 del notdone[name] if name.startswith('PY_') and name[3:] in renamed_variables: - name = name[3:] if name not in done: done[name] = value @@ -535,7 +538,7 @@ def get_config_vars(*args): global _config_vars if _config_vars is None: _config_vars = sysconfig.get_config_vars().copy() - py39compat.add_ext_suffix(_config_vars) + py39.add_ext_suffix(_config_vars) return [_config_vars.get(name) for name in args] if args else _config_vars diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 27e73393..20dfe8f1 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -6,3 +6,36 @@ distutils.command.tests package, since command identification is done by import rather than matching pre-defined names. """ + +from typing import Sequence + + +def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no cover + """Check if the compiler components used to build the interpreter exist. + + Check for the existence of the compiler executables whose names are listed + in 'cmd_names' or all the compiler executables when 'cmd_names' is empty + and return the first missing executable or None when none is found + missing. + + """ + from distutils import ccompiler, errors, spawn, sysconfig + + compiler = ccompiler.new_compiler() + sysconfig.customize_compiler(compiler) + if compiler.compiler_type == "msvc": + # MSVC has no executables, so check whether initialization succeeds + try: + compiler.initialize() + except errors.DistutilsPlatformError: + return "msvc" + for name in compiler.executables: + if cmd_names and name not in cmd_names: + continue + cmd = getattr(compiler, name) + if cmd_names: + assert cmd is not None, "the '%s' executable is not configured" % name + elif not cmd: + continue + if spawn.find_executable(cmd[0]) is None: + return cmd[0] diff --git a/distutils/tests/compat/__init__.py b/distutils/tests/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/distutils/tests/py38compat.py b/distutils/tests/compat/py38.py similarity index 100% rename from distutils/tests/py38compat.py rename to distutils/tests/compat/py38.py diff --git a/distutils/tests/py37compat.py b/distutils/tests/py37compat.py deleted file mode 100644 index 76d3551c..00000000 --- a/distutils/tests/py37compat.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import platform -import sys - - -def subprocess_args_compat(*args): - return list(map(os.fspath, args)) - - -def subprocess_args_passthrough(*args): - return list(args) - - -subprocess_args = ( - subprocess_args_compat - if platform.system() == "Windows" and sys.version_info < (3, 8) - else subprocess_args_passthrough -) diff --git a/distutils/tests/support.py b/distutils/tests/support.py index 4a2e8236..9cd2b8a9 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -1,4 +1,5 @@ """Support code for distutils test cases.""" + import itertools import os import pathlib @@ -32,7 +33,7 @@ def write_file(self, path, content='xxx'): path can be a string or a sequence. """ - pathlib.Path(*always_iterable(path)).write_text(content) + pathlib.Path(*always_iterable(path)).write_text(content, encoding='utf-8') def create_dist(self, pkg_name='foo', **kw): """Will generate a test environment. diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 1b4e1e30..02af2aa0 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -1,4 +1,5 @@ """Tests for distutils.archive_util.""" + import functools import operator import os @@ -22,7 +23,7 @@ import path import pytest -from .py38compat import check_warnings +from .compat.py38 import check_warnings from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id @@ -288,7 +289,7 @@ def _breaks(*args, **kw): pass assert os.getcwd() == current_dir finally: - del ARCHIVE_FORMATS['xxx'] + ARCHIVE_FORMATS.pop('xxx') def test_make_archive_tar(self): base_dir = self._create_files() diff --git a/distutils/tests/test_bdist.py b/distutils/tests/test_bdist.py index af330a06..18048077 100644 --- a/distutils/tests/test_bdist.py +++ b/distutils/tests/test_bdist.py @@ -1,4 +1,5 @@ """Tests for distutils.command.bdist.""" + from distutils.command.bdist import bdist from distutils.tests import support diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index 78d0b050..78928fea 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -28,7 +28,6 @@ class TestBuildDumb( ): @pytest.mark.usefixtures('needs_zlib') def test_simple_built(self): - # let's create a simple package tmp_dir = self.mkdtemp() pkg_dir = os.path.join(tmp_dir, 'foo') @@ -38,16 +37,14 @@ def test_simple_built(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) @@ -63,7 +60,7 @@ def test_simple_built(self): # see what we have dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) - base = "{}.{}.zip".format(dist.get_fullname(), cmd.plat_name) + base = f"{dist.get_fullname()}.{cmd.plat_name}.zip" assert dist_created == [base] @@ -75,7 +72,7 @@ def test_simple_built(self): fp.close() contents = sorted(filter(None, map(os.path.basename, contents))) - wanted = ['foo-0.1-py%s.%s.egg-info' % sys.version_info[:2], 'foo.py'] + wanted = ['foo-0.1-py{}.{}.egg-info'.format(*sys.version_info[:2]), 'foo.py'] if not sys.dont_write_bytecode: wanted.append('foo.%s.pyc' % sys.implementation.cache_tag) assert contents == sorted(wanted) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index e4cb9f50..a5cb42c3 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -9,7 +9,7 @@ import pytest -from .py38compat import requires_zlib +from .compat.py38 import requires_zlib SETUP_PY = """\ from distutils.core import setup @@ -56,16 +56,14 @@ def test_quiet(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) @@ -87,7 +85,7 @@ def test_quiet(self): @mac_woes @requires_zlib() - # http://bugs.python.org/issue1533164 + # https://bugs.python.org/issue1533164 @pytest.mark.skipif("not find_executable('rpm')") @pytest.mark.skipif("not find_executable('rpmbuild')") def test_no_optimize_flag(self): @@ -101,16 +99,14 @@ def test_no_optimize_flag(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index 54582fa8..25483ad7 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -1,4 +1,5 @@ """Tests for distutils.command.build.""" + import os import sys from distutils.command.build import build @@ -22,7 +23,7 @@ def test_finalize_options(self): # build_platlib is 'build/lib.platform-cache_tag[-pydebug]' # examples: # build/lib.macosx-10.3-i386-cpython39 - plat_spec = '.{}-{}'.format(cmd.plat_name, sys.implementation.cache_tag) + plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}' if hasattr(sys, 'gettotalrefcount'): assert cmd.build_platlib.endswith('-pydebug') plat_spec += '-pydebug' diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index 1323fd54..9c69b3e7 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -1,9 +1,9 @@ """Tests for distutils.command.build_clib.""" + import os from distutils.command.build_clib import build_clib from distutils.errors import DistutilsSetupError -from distutils.tests import support -from test.support import missing_compiler_executable +from distutils.tests import missing_compiler_executable, support import pytest @@ -69,7 +69,6 @@ def test_get_source_files(self): assert cmd.get_source_files() == ['a', 'b', 'c', 'd'] def test_build_libraries(self): - pkg_dir, dist = self.create_dist() cmd = build_clib(dist) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index eb2748e7..cc83e7fb 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -18,6 +18,7 @@ UnknownFileError, ) from distutils.extension import Extension +from distutils.tests import missing_compiler_executable from distutils.tests.support import ( TempdirManager, copy_xxmodule_c, @@ -26,16 +27,18 @@ from io import StringIO from test import support +import jaraco.path import path import pytest -from . import py38compat as import_helper +from .compat import py38 as import_helper @pytest.fixture() def user_site_dir(request): self = request.instance self.tmp_dir = self.mkdtemp() + self.tmp_path = path.Path(self.tmp_dir) from distutils.command import build_ext orig_user_base = site.USER_BASE @@ -46,7 +49,7 @@ def user_site_dir(request): # bpo-30132: On Windows, a .pdb file may be created in the current # working directory. Create a temporary working directory to cleanup # everything at the end of the test. - with path.Path(self.tmp_dir): + with self.tmp_path: yield site.USER_BASE = orig_user_base @@ -88,7 +91,7 @@ def build_ext(self, *args, **kwargs): return build_ext(*args, **kwargs) def test_build_ext(self): - cmd = support.missing_compiler_executable() + missing_compiler_executable() copy_xxmodule_c(self.tmp_dir) xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') xx_ext = Extension('xx', [xx_c]) @@ -157,7 +160,7 @@ def test_user_site(self): cmd = self.build_ext(dist) # making sure the user option is there - options = [name for name, short, lable in cmd.user_options] + options = [name for name, short, label in cmd.user_options] assert 'user' in options # setting a value @@ -179,7 +182,6 @@ def test_user_site(self): assert incl in cmd.include_dirs def test_optional_extension(self): - # this extension will fail, but let's ignore this failure # with the optional argument. modules = [Extension('foo', ['xxx'], optional=False)] @@ -359,7 +361,7 @@ def test_compiler_option(self): assert cmd.compiler == 'unix' def test_get_outputs(self): - cmd = support.missing_compiler_executable() + missing_compiler_executable() tmp_dir = self.mkdtemp() c_file = os.path.join(tmp_dir, 'foo.c') self.write_file(c_file, 'void PyInit_foo(void) {}\n') @@ -476,7 +478,7 @@ def test_deployment_target_too_low(self): @pytest.mark.skipif('platform.system() != "Darwin"') @pytest.mark.usefixtures('save_env') - def test_deployment_target_higher_ok(self): + def test_deployment_target_higher_ok(self): # pragma: no cover # Issue 9516: Test that an extension module can be compiled with a # deployment target higher than that of the interpreter: the ext # module may depend on some newer OS feature. @@ -488,32 +490,29 @@ def test_deployment_target_higher_ok(self): deptarget = '.'.join(str(i) for i in deptarget) self._try_compile_deployment_target('<', deptarget) - def _try_compile_deployment_target(self, operator, target): + def _try_compile_deployment_target(self, operator, target): # pragma: no cover if target is None: if os.environ.get('MACOSX_DEPLOYMENT_TARGET'): del os.environ['MACOSX_DEPLOYMENT_TARGET'] else: os.environ['MACOSX_DEPLOYMENT_TARGET'] = target - deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c') - - with open(deptarget_c, 'w') as fp: - fp.write( - textwrap.dedent( - '''\ - #include + jaraco.path.build( + { + 'deptargetmodule.c': textwrap.dedent(f"""\ + #include - int dummy; + int dummy; - #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED - #else - #error "Unexpected target" - #endif + #if TARGET {operator} MAC_OS_X_VERSION_MIN_REQUIRED + #else + #error "Unexpected target" + #endif - ''' - % operator - ) - ) + """), + }, + self.tmp_path, + ) # get the deployment target that the interpreter was built with target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') @@ -533,8 +532,8 @@ def _try_compile_deployment_target(self, operator, target): target = '%02d0000' % target deptarget_ext = Extension( 'deptarget', - [deptarget_c], - extra_compile_args=['-DTARGET={}'.format(target)], + [self.tmp_path / 'deptargetmodule.c'], + extra_compile_args=[f'-DTARGET={target}'], ) dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]}) dist.package_dir = self.tmp_dir diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 4d44bfca..8bc0e98a 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -7,6 +7,7 @@ from distutils.errors import DistutilsFileError from distutils.tests import support +import jaraco.path import pytest @@ -14,16 +15,13 @@ class TestBuildPy(support.TempdirManager): def test_package_data(self): sources = self.mkdtemp() - f = open(os.path.join(sources, "__init__.py"), "w") - try: - f.write("# Pretend this is a package.") - finally: - f.close() - f = open(os.path.join(sources, "README.txt"), "w") - try: - f.write("Info about this package") - finally: - f.close() + jaraco.path.build( + { + '__init__.py': "# Pretend this is a package.", + 'README.txt': 'Info about this package', + }, + sources, + ) destination = self.mkdtemp() @@ -60,20 +58,14 @@ def test_package_data(self): def test_empty_package_dir(self): # See bugs #1668596/#1720897 sources = self.mkdtemp() - open(os.path.join(sources, "__init__.py"), "w").close() - - testdir = os.path.join(sources, "doc") - os.mkdir(testdir) - open(os.path.join(testdir, "testfile"), "w").close() + jaraco.path.build({'__init__.py': '', 'doc': {'testfile': ''}}, sources) os.chdir(sources) - dist = Distribution( - { - "packages": ["pkg"], - "package_dir": {"pkg": ""}, - "package_data": {"pkg": ["doc/*"]}, - } - ) + dist = Distribution({ + "packages": ["pkg"], + "package_dir": {"pkg": ""}, + "package_data": {"pkg": ["doc/*"]}, + }) # script_name need not exist, it just need to be initialized dist.script_name = os.path.join(sources, "setup.py") dist.script_args = ["build"] @@ -124,17 +116,19 @@ def test_dir_in_package_data(self): """ # See bug 19286 sources = self.mkdtemp() - pkg_dir = os.path.join(sources, "pkg") - - os.mkdir(pkg_dir) - open(os.path.join(pkg_dir, "__init__.py"), "w").close() - - docdir = os.path.join(pkg_dir, "doc") - os.mkdir(docdir) - open(os.path.join(docdir, "testfile"), "w").close() - - # create the directory that could be incorrectly detected as a file - os.mkdir(os.path.join(docdir, 'otherdir')) + jaraco.path.build( + { + 'pkg': { + '__init__.py': '', + 'doc': { + 'testfile': '', + # create a directory that could be incorrectly detected as a file + 'otherdir': {}, + }, + } + }, + sources, + ) os.chdir(sources) dist = Distribution({"packages": ["pkg"], "package_data": {"pkg": ["doc/*"]}}) @@ -174,9 +168,8 @@ def test_namespace_package_does_not_warn(self, caplog): """ # Create a fake project structure with a package namespace: tmp = self.mkdtemp() + jaraco.path.build({'ns': {'pkg': {'module.py': ''}}}, tmp) os.chdir(tmp) - os.makedirs("ns/pkg") - open("ns/pkg/module.py", "w").close() # Configure the package: attrs = { diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index b01330d6..208b1f6e 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -1,11 +1,14 @@ """Tests for distutils.command.build_scripts.""" import os +import textwrap from distutils import sysconfig from distutils.command.build_scripts import build_scripts from distutils.core import Distribution from distutils.tests import support +import jaraco.path + class TestBuildScripts(support.TempdirManager): def test_default_settings(self): @@ -43,38 +46,27 @@ def get_build_scripts_cmd(self, target, scripts): ) return build_scripts(dist) - def write_sample_scripts(self, dir): - expected = [] - expected.append("script1.py") - self.write_script( - dir, - "script1.py", - ( - "#! /usr/bin/env python2.3\n" - "# bogus script w/ Python sh-bang\n" - "pass\n" - ), - ) - expected.append("script2.py") - self.write_script( - dir, - "script2.py", - ("#!/usr/bin/python\n" "# bogus script w/ Python sh-bang\n" "pass\n"), - ) - expected.append("shell.sh") - self.write_script( - dir, - "shell.sh", - ("#!/bin/sh\n" "# bogus shell script w/ sh-bang\n" "exit 0\n"), - ) - return expected - - def write_script(self, dir, name, text): - f = open(os.path.join(dir, name), "w") - try: - f.write(text) - finally: - f.close() + @staticmethod + def write_sample_scripts(dir): + spec = { + 'script1.py': textwrap.dedent(""" + #! /usr/bin/env python2.3 + # bogus script w/ Python sh-bang + pass + """).lstrip(), + 'script2.py': textwrap.dedent(""" + #!/usr/bin/python + # bogus script w/ Python sh-bang + pass + """).lstrip(), + 'shell.sh': textwrap.dedent(""" + #!/bin/sh + # bogus shell script w/ sh-bang + exit 0 + """).lstrip(), + } + jaraco.path.build(spec, dir) + return list(spec) def test_version_int(self): source = self.mkdtemp() @@ -86,7 +78,7 @@ def test_version_int(self): ) cmd.finalize_options() - # http://bugs.python.org/issue4524 + # https://bugs.python.org/issue4524 # # On linux-g++-32 with command line `./configure --enable-ipv6 # --with-suffix=3`, python is compiled okay but the build scripts diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index aa073ed6..d23b907c 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -35,7 +35,7 @@ def c_file(tmp_path): .lstrip() .replace('#headers', headers) ) - c_file.write_text(payload) + c_file.write_text(payload, encoding='utf-8') return c_file @@ -52,3 +52,40 @@ def test_set_include_dirs(c_file): # do it again, setting include dirs after any initialization compiler.set_include_dirs([python]) compiler.compile(_make_strs([c_file])) + + +def test_has_function_prototype(): + # Issue https://github.com/pypa/setuptools/issues/3648 + # Test prototype-generating behavior. + + compiler = ccompiler.new_compiler() + + # Every C implementation should have these. + assert compiler.has_function('abort') + assert compiler.has_function('exit') + with pytest.deprecated_call(match='includes is deprecated'): + # abort() is a valid expression with the prototype. + assert compiler.has_function('abort', includes=['stdlib.h']) + with pytest.deprecated_call(match='includes is deprecated'): + # But exit() is not valid with the actual prototype in scope. + assert not compiler.has_function('exit', includes=['stdlib.h']) + # And setuptools_does_not_exist is not declared or defined at all. + assert not compiler.has_function('setuptools_does_not_exist') + with pytest.deprecated_call(match='includes is deprecated'): + assert not compiler.has_function( + 'setuptools_does_not_exist', includes=['stdio.h'] + ) + + +def test_include_dirs_after_multiple_compile_calls(c_file): + """ + Calling compile multiple times should not change the include dirs + (regression test for setuptools issue #3591). + """ + compiler = ccompiler.new_compiler() + python = sysconfig.get_paths()['include'] + compiler.set_include_dirs([python]) + compiler.compile(_make_strs([c_file])) + assert compiler.include_dirs == [python] + compiler.compile(_make_strs([c_file])) + assert compiler.include_dirs == [python] diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index e3a502ef..580cb2a2 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -1,4 +1,5 @@ """Tests for distutils.command.check.""" + import os import textwrap from distutils.command.check import check @@ -151,8 +152,7 @@ def test_check_restructuredtext_with_syntax_highlight(self): pytest.importorskip('docutils') # Don't fail if there is a `code` or `code-block` directive - example_rst_docs = [] - example_rst_docs.append( + example_rst_docs = [ textwrap.dedent( """\ Here's some code: @@ -162,9 +162,7 @@ def test_check_restructuredtext_with_syntax_highlight(self): def foo(): pass """ - ) - ) - example_rst_docs.append( + ), textwrap.dedent( """\ Here's some code: @@ -174,8 +172,8 @@ def foo(): def foo(): pass """ - ) - ) + ), + ] for rest_with_code in example_rst_docs: pkg_info, dist = self.create_dist(long_description=rest_with_code) diff --git a/distutils/tests/test_clean.py b/distutils/tests/test_clean.py index e7e7020d..bdbcd4fa 100644 --- a/distutils/tests/test_clean.py +++ b/distutils/tests/test_clean.py @@ -1,4 +1,5 @@ """Tests for distutils.command.clean.""" + import os from distutils.command.clean import clean from distutils.tests import support @@ -35,7 +36,7 @@ def test_simple_run(self): cmd.run() # make sure the files where removed - for name, path in dirs: + for _name, path in dirs: assert not os.path.exists(path), '%s was not removed' % path # let's run the command again (should spit warnings but succeed) diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py index d23209b2..f366aa65 100644 --- a/distutils/tests/test_cmd.py +++ b/distutils/tests/test_cmd.py @@ -1,4 +1,5 @@ """Tests for distutils.cmd.""" + import os from distutils import debug from distutils.cmd import Command @@ -58,7 +59,6 @@ def _execute(func, args, exec_msg, level): cmd.make_file(infiles='in', outfile='out', func='func', args=()) def test_dump_options(self, cmd): - msgs = [] def _announce(msg, level): diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 99a4d777..be5ae0a6 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -1,4 +1,5 @@ """Tests for distutils.pypirc.pypirc.""" + import os from distutils.tests import support diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 9449a4d4..fc0a7885 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -1,11 +1,13 @@ """Tests for distutils.command.config.""" + import os import sys from distutils._log import log from distutils.command.config import config, dump_file -from distutils.tests import support -from test.support import missing_compiler_executable +from distutils.tests import missing_compiler_executable, support +import more_itertools +import path import pytest @@ -23,12 +25,9 @@ def _info(self, msg, *args): self._logs.append(line) def test_dump_file(self): - this_file = os.path.splitext(__file__)[0] + '.py' - f = open(this_file) - try: - numlines = len(f.readlines()) - finally: - f.close() + this_file = path.Path(__file__).with_suffix('.py') + with this_file.open(encoding='utf-8') as f: + numlines = more_itertools.ilen(f) dump_file(this_file, 'I am the header') assert len(self._logs) == numlines + 1 diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index b1fe0bc6..bad3fb7e 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -69,20 +69,20 @@ class TestCore: def test_run_setup_provides_file(self, temp_file): # Make sure the script can use __file__; if that's missing, the test # setup.py script will raise NameError. - temp_file.write_text(setup_using___file__) + temp_file.write_text(setup_using___file__, encoding='utf-8') distutils.core.run_setup(temp_file) def test_run_setup_preserves_sys_argv(self, temp_file): # Make sure run_setup does not clobber sys.argv argv_copy = sys.argv.copy() - temp_file.write_text(setup_does_nothing) + temp_file.write_text(setup_does_nothing, encoding='utf-8') distutils.core.run_setup(temp_file) assert sys.argv == argv_copy def test_run_setup_defines_subclass(self, temp_file): # Make sure the script can use __file__; if that's missing, the test # setup.py script will raise NameError. - temp_file.write_text(setup_defines_subclass) + temp_file.write_text(setup_defines_subclass, encoding='utf-8') dist = distutils.core.run_setup(temp_file) install = dist.get_command_obj('install') assert 'cmd' in install.sub_commands @@ -97,7 +97,7 @@ def test_run_setup_uses_current_dir(self, tmp_path): # Create a directory and write the setup.py file there: setup_py = tmp_path / 'setup.py' - setup_py.write_text(setup_prints_cwd) + setup_py.write_text(setup_prints_cwd, encoding='utf-8') distutils.core.run_setup(setup_py) output = sys.stdout.getvalue() @@ -106,14 +106,14 @@ def test_run_setup_uses_current_dir(self, tmp_path): assert cwd == output def test_run_setup_within_if_main(self, temp_file): - temp_file.write_text(setup_within_if_main) + temp_file.write_text(setup_within_if_main, encoding='utf-8') dist = distutils.core.run_setup(temp_file, stop_after="config") assert isinstance(dist, Distribution) assert dist.get_name() == "setup_within_if_main" def test_run_commands(self, temp_file): sys.argv = ['setup.py', 'build'] - temp_file.write_text(setup_within_if_main) + temp_file.write_text(setup_within_if_main, encoding='utf-8') dist = distutils.core.run_setup(temp_file, stop_after="commandline") assert 'build' not in dist.have_run distutils.core.run_commands(dist) @@ -123,7 +123,7 @@ def test_debug_mode(self, capsys, monkeypatch): # this covers the code called when DEBUG is set sys.argv = ['setup.py', '--name'] distutils.core.setup(name='bar') - capsys.readouterr().out == 'bar\n' + assert capsys.readouterr().out == 'bar\n' monkeypatch.setattr(distutils.core, 'DEBUG', True) distutils.core.setup(name='bar') wanted = "options (after parsing config files):\n" diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index bc4b150e..d95654f5 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -1,4 +1,5 @@ """Tests for distutils.cygwinccompiler.""" + import os import sys from distutils import sysconfig @@ -46,7 +47,6 @@ def test_runtime_library_dir_option(self): assert compiler.runtime_library_dir_option('/foo') == [] def test_check_config_h(self): - # check_config_h looks for "GCC" in sys.version first # returns CONFIG_H_OK if found sys.version = ( @@ -71,7 +71,6 @@ def test_check_config_h(self): assert check_config_h()[0] == CONFIG_H_OK def test_get_msvcr(self): - # none sys.version = ( '2.6.1 (r261:67515, Dec 6 2008, 16:42:21) ' @@ -81,25 +80,25 @@ def test_get_msvcr(self): # MSVC 7.0 sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1300 32 bits (Intel)]' + '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1300 32 bits (Intel)]' ) assert get_msvcr() == ['msvcr70'] # MSVC 7.1 sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1310 32 bits (Intel)]' + '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bits (Intel)]' ) assert get_msvcr() == ['msvcr71'] # VS2005 / MSVC 8.0 sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1400 32 bits (Intel)]' + '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1400 32 bits (Intel)]' ) assert get_msvcr() == ['msvcr80'] # VS2008 / MSVC 9.0 sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1500 32 bits (Intel)]' + '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1500 32 bits (Intel)]' ) assert get_msvcr() == ['msvcr90'] @@ -107,11 +106,11 @@ def test_get_msvcr(self): '3.10.0 (tags/v3.10.0:b494f59, Oct 4 2021, 18:46:30) ' '[MSC v.1929 32 bit (Intel)]' ) - assert get_msvcr() == ['ucrt', 'vcruntime140'] + assert get_msvcr() == ['vcruntime140'] # unknown sys.version = ( - '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.2000 32 bits (Intel)]' + '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.2000 32 bits (Intel)]' ) with pytest.raises(ValueError): get_msvcr() diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index eb92bad7..84cda619 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -1,4 +1,5 @@ """Tests for distutils.dir_util.""" + import os import stat import unittest.mock as mock @@ -12,6 +13,8 @@ ) from distutils.tests import support +import jaraco.path +import path import pytest @@ -50,7 +53,6 @@ def test_mkpath_with_custom_mode(self): assert stat.S_IMODE(os.stat(self.target2).st_mode) == 0o555 & ~umask def test_create_tree_verbosity(self, caplog): - create_tree(self.root_target, ['one', 'two', 'three'], verbose=0) assert caplog.messages == [] remove_tree(self.root_target, verbose=0) @@ -62,7 +64,6 @@ def test_create_tree_verbosity(self, caplog): remove_tree(self.root_target, verbose=0) def test_copy_tree_verbosity(self, caplog): - mkpath(self.target, verbose=0) copy_tree(self.target, self.target2, verbose=0) @@ -71,11 +72,10 @@ def test_copy_tree_verbosity(self, caplog): remove_tree(self.root_target, verbose=0) mkpath(self.target, verbose=0) - a_file = os.path.join(self.target, 'ok.txt') - with open(a_file, 'w') as f: - f.write('some content') + a_file = path.Path(self.target) / 'ok.txt' + jaraco.path.build({'ok.txt': 'some content'}, self.target) - wanted = ['copying {} -> {}'.format(a_file, self.target2)] + wanted = [f'copying {a_file} -> {self.target2}'] copy_tree(self.target, self.target2, verbose=1) assert caplog.messages == wanted @@ -85,11 +85,7 @@ def test_copy_tree_verbosity(self, caplog): def test_copy_tree_skips_nfs_temp_files(self): mkpath(self.target, verbose=0) - a_file = os.path.join(self.target, 'ok.txt') - nfs_file = os.path.join(self.target, '.nfs123abc') - for f in a_file, nfs_file: - with open(f, 'w') as fh: - fh.write('some content') + jaraco.path.build({'ok.txt': 'some content', '.nfs123abc': ''}, self.target) copy_tree(self.target, self.target2) assert os.listdir(self.target2) == ['ok.txt'] diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index d337d4af..9ed4d16d 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -1,4 +1,8 @@ """Tests for distutils.dist.""" + +import email +import email.generator +import email.policy import functools import io import os @@ -63,14 +67,12 @@ def test_command_packages_unspecified(self, clear_argv): def test_command_packages_cmdline(self, clear_argv): from distutils.tests.test_dist import test_dist - sys.argv.extend( - [ - "--command-packages", - "foo.bar,distutils.tests", - "test_dist", - "-Ssometext", - ] - ) + sys.argv.extend([ + "--command-packages", + "foo.bar,distutils.tests", + "test_dist", + "-Ssometext", + ]) d = self.create_distribution() # let's actually try to load our test command: assert d.get_command_packages() == [ @@ -92,9 +94,8 @@ def test_venv_install_options(self, tmp_path): fakepath = '/somedir' - jaraco.path.build( - { - file: f""" + jaraco.path.build({ + file: f""" [install] install-base = {fakepath} install-platbase = {fakepath} @@ -110,8 +111,7 @@ def test_venv_install_options(self, tmp_path): user = {fakepath} root = {fakepath} """, - } - ) + }) # Base case: Not in a Virtual Environment with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'): @@ -139,7 +139,7 @@ def test_venv_install_options(self, tmp_path): result_dict.keys() ) - for (key, value) in d.command_options.get('install').items(): + for key, value in d.command_options.get('install').items(): assert value == result_dict[key] # Test case: In a Virtual Environment @@ -152,14 +152,12 @@ def test_venv_install_options(self, tmp_path): def test_command_packages_configfile(self, tmp_path, clear_argv): sys.argv.append("build") file = str(tmp_path / "file") - jaraco.path.build( - { - file: """ + jaraco.path.build({ + file: """ [global] command_packages = foo.bar, splat """, - } - ) + }) d = self.create_distribution([file]) assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"] @@ -256,7 +254,7 @@ def test_find_config_files_permission_error(self, fake_home): """ Finding config files should not fail when directory is inaccessible. """ - fake_home.joinpath(pydistutils_cfg).write_text('') + fake_home.joinpath(pydistutils_cfg).write_text('', encoding='utf-8') fake_home.chmod(0o000) Distribution().find_config_files() @@ -507,3 +505,41 @@ def test_read_metadata(self): assert metadata.platforms is None assert metadata.obsoletes is None assert metadata.requires == ['foo'] + + def test_round_trip_through_email_generator(self): + """ + In pypa/setuptools#4033, it was shown that once PKG-INFO is + re-generated using ``email.generator.Generator``, some control + characters might cause problems. + """ + # Given a PKG-INFO file ... + attrs = { + "name": "package", + "version": "1.0", + "long_description": "hello\x0b\nworld\n", + } + dist = Distribution(attrs) + metadata = dist.metadata + + with io.StringIO() as buffer: + metadata.write_pkg_file(buffer) + msg = buffer.getvalue() + + # ... when it is read and re-written using stdlib's email library, + orig = email.message_from_string(msg) + policy = email.policy.EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with io.StringIO() as buffer: + email.generator.Generator(buffer, policy=policy).flatten(orig) + + buffer.seek(0) + regen = email.message_from_file(buffer) + + # ... then it should be the same as the original + # (except for the specific line break characters) + orig_desc = set(orig["Description"].splitlines()) + regen_desc = set(regen["Description"].splitlines()) + assert regen_desc == orig_desc diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 9c3b46db..527a1355 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -1,11 +1,12 @@ """Tests for distutils.extension.""" + import os import warnings from distutils.extension import Extension, read_setup_file import pytest -from .py38compat import check_warnings +from .compat.py38 import check_warnings class TestExtension: diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 27cf19a8..4c2abd24 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -1,32 +1,26 @@ """Tests for distutils.file_util.""" + import errno import os import unittest.mock as mock from distutils.errors import DistutilsFileError from distutils.file_util import copy_file, move_file -from distutils.tests import support +import jaraco.path import pytest -from .py38compat import unlink - @pytest.fixture(autouse=True) -def stuff(request, monkeypatch, distutils_managed_tempdir): +def stuff(request, tmp_path): self = request.instance - tmp_dir = self.mkdtemp() - self.source = os.path.join(tmp_dir, 'f1') - self.target = os.path.join(tmp_dir, 'f2') - self.target_dir = os.path.join(tmp_dir, 'd1') + self.source = tmp_path / 'f1' + self.target = tmp_path / 'f2' + self.target_dir = tmp_path / 'd1' -class TestFileUtil(support.TempdirManager): +class TestFileUtil: def test_move_file_verbosity(self, caplog): - f = open(self.source, 'w') - try: - f.write('some content') - finally: - f.close() + jaraco.path.build({self.source: 'some content'}) move_file(self.source, self.target, verbose=0) assert not caplog.messages @@ -35,7 +29,7 @@ def test_move_file_verbosity(self, caplog): move_file(self.target, self.source, verbose=0) move_file(self.source, self.target, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target)] + wanted = [f'moving {self.source} -> {self.target}'] assert caplog.messages == wanted # back to original state @@ -45,7 +39,7 @@ def test_move_file_verbosity(self, caplog): # now the target is a dir os.mkdir(self.target_dir) move_file(self.source, self.target_dir, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target_dir)] + wanted = [f'moving {self.source} -> {self.target_dir}'] assert caplog.messages == wanted def test_move_file_exception_unpacking_rename(self): @@ -53,8 +47,7 @@ def test_move_file_exception_unpacking_rename(self): with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( DistutilsFileError ): - with open(self.source, 'w') as fobj: - fobj.write('spam eggs') + jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=0) def test_move_file_exception_unpacking_unlink(self): @@ -64,13 +57,11 @@ def test_move_file_exception_unpacking_unlink(self): ), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises( DistutilsFileError ): - with open(self.source, 'w') as fobj: - fobj.write('spam eggs') + jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=0) def test_copy_file_hard_link(self): - with open(self.source, 'w') as f: - f.write('some content') + jaraco.path.build({self.source: 'some content'}) # Check first that copy_file() will not fall back on copying the file # instead of creating the hard link. try: @@ -78,22 +69,20 @@ def test_copy_file_hard_link(self): except OSError as e: self.skipTest('os.link: %s' % e) else: - unlink(self.target) + self.target.unlink() st = os.stat(self.source) copy_file(self.source, self.target, link='hard') st2 = os.stat(self.source) st3 = os.stat(self.target) assert os.path.samestat(st, st2), (st, st2) assert os.path.samestat(st2, st3), (st2, st3) - with open(self.source) as f: - assert f.read() == 'some content' + assert self.source.read_text(encoding='utf-8') == 'some content' def test_copy_file_hard_link_failure(self): # If hard linking fails, copy_file() falls back on copying file # (some special filesystems don't support hard linking even under # Unix, see issue #8876). - with open(self.source, 'w') as f: - f.write('some content') + jaraco.path.build({self.source: 'some content'}) st = os.stat(self.source) with mock.patch("os.link", side_effect=OSError(0, "linking unsupported")): copy_file(self.source, self.target, link='hard') @@ -102,5 +91,4 @@ def test_copy_file_hard_link_failure(self): assert os.path.samestat(st, st2), (st, st2) assert not os.path.samestat(st2, st3), (st2, st3) for fn in (self.source, self.target): - with open(fn) as f: - assert f.read() == 'some content' + assert fn.read_text(encoding='utf-8') == 'some content' diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index 62e56162..ec7e5cf3 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -1,4 +1,5 @@ """Tests for distutils.filelist.""" + import logging import os import re @@ -9,7 +10,7 @@ import jaraco.path import pytest -from . import py38compat as os_helper +from .compat import py38 as os_helper MANIFEST_IN = """\ include ok @@ -318,14 +319,18 @@ def test_non_local_discovery(self, tmp_path): When findall is called with another path, the full path name should be returned. """ - filename = tmp_path / 'file1.txt' - filename.write_text('') - expected = [str(filename)] + jaraco.path.build({'file1.txt': ''}, tmp_path) + expected = [str(tmp_path / 'file1.txt')] assert filelist.findall(tmp_path) == expected @os_helper.skip_unless_symlink def test_symlink_loop(self, tmp_path): - tmp_path.joinpath('link-to-parent').symlink_to('.') - tmp_path.joinpath('somefile').write_text('') + jaraco.path.build( + { + 'link-to-parent': jaraco.path.Symlink('.'), + 'somefile': '', + }, + tmp_path, + ) files = filelist.findall(tmp_path) assert len(files) == 1 diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index e87ecece..08f0f839 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -12,8 +12,7 @@ from distutils.core import Distribution from distutils.errors import DistutilsOptionError from distutils.extension import Extension -from distutils.tests import support -from test import support as test_support +from distutils.tests import missing_compiler_executable, support import pytest @@ -97,7 +96,7 @@ def _expanduser(path): cmd = install(dist) # making sure the user option is there - options = [name for name, short, lable in cmd.user_options] + options = [name for name, short, label in cmd.user_options] assert 'user' in options # setting a value @@ -194,23 +193,19 @@ def test_record(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.record) - try: - content = f.read() - finally: - f.close() + content = pathlib.Path(cmd.record).read_text(encoding='utf-8') - found = [os.path.basename(line) for line in content.splitlines()] + found = [pathlib.Path(line).name for line in content.splitlines()] expected = [ 'hello.py', 'hello.%s.pyc' % sys.implementation.cache_tag, 'sayhi', - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2], + 'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]), ] assert found == expected def test_record_extensions(self): - cmd = test_support.missing_compiler_executable() + cmd = missing_compiler_executable() if cmd is not None: pytest.skip('The %r command is not found' % cmd) install_dir = self.mkdtemp() @@ -232,12 +227,12 @@ def test_record_extensions(self): cmd.ensure_finalized() cmd.run() - content = pathlib.Path(cmd.record).read_text() + content = pathlib.Path(cmd.record).read_text(encoding='utf-8') - found = [os.path.basename(line) for line in content.splitlines()] + found = [pathlib.Path(line).name for line in content.splitlines()] expected = [ _make_ext_name('xx'), - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2], + 'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]), ] assert found == expected diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py index ea5edd63..e453d01f 100644 --- a/distutils/tests/test_install_data.py +++ b/distutils/tests/test_install_data.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_data.""" + import os from distutils.command.install_data import install_data from distutils.tests import support diff --git a/distutils/tests/test_install_headers.py b/distutils/tests/test_install_headers.py index 1d87406f..2c74f06b 100644 --- a/distutils/tests/test_install_headers.py +++ b/distutils/tests/test_install_headers.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_headers.""" + import os from distutils.command.install_headers import install_headers from distutils.tests import support diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py index 89e91341..964106fa 100644 --- a/distutils/tests/test_install_lib.py +++ b/distutils/tests/test_install_lib.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_data.""" + import importlib.util import os import sys diff --git a/distutils/tests/test_install_scripts.py b/distutils/tests/test_install_scripts.py index 94628298..5d9f13a4 100644 --- a/distutils/tests/test_install_scripts.py +++ b/distutils/tests/test_install_scripts.py @@ -5,6 +5,8 @@ from distutils.core import Distribution from distutils.tests import support +from . import test_build_scripts + class TestInstallScripts(support.TempdirManager): def test_default_settings(self): @@ -30,31 +32,8 @@ def test_default_settings(self): def test_installation(self): source = self.mkdtemp() - expected = [] - - def write_script(name, text): - expected.append(name) - f = open(os.path.join(source, name), "w") - try: - f.write(text) - finally: - f.close() - write_script( - "script1.py", - ( - "#! /usr/bin/env python2.3\n" - "# bogus script w/ Python sh-bang\n" - "pass\n" - ), - ) - write_script( - "script2.py", - ("#!/usr/bin/python\n" "# bogus script w/ Python sh-bang\n" "pass\n"), - ) - write_script( - "shell.sh", ("#!/bin/sh\n" "# bogus shell script w/ sh-bang\n" "exit 0\n") - ) + expected = test_build_scripts.TestBuildScripts.write_sample_scripts(source) target = self.mkdtemp() dist = Distribution() diff --git a/distutils/tests/test_dep_util.py b/distutils/tests/test_modified.py similarity index 59% rename from distutils/tests/test_dep_util.py rename to distutils/tests/test_modified.py index fb3c0660..2bd82346 100644 --- a/distutils/tests/test_dep_util.py +++ b/distutils/tests/test_modified.py @@ -1,6 +1,8 @@ -"""Tests for distutils.dep_util.""" +"""Tests for distutils._modified.""" + import os -from distutils.dep_util import newer, newer_group, newer_pairwise +import types +from distutils._modified import newer, newer_group, newer_pairwise, newer_pairwise_group from distutils.errors import DistutilsFileError from distutils.tests import support @@ -9,7 +11,6 @@ class TestDepUtil(support.TempdirManager): def test_newer(self): - tmpdir = self.mkdtemp() new_file = os.path.join(tmpdir, 'new') old_file = os.path.abspath(__file__) @@ -28,7 +29,7 @@ def test_newer(self): # than 'new_file'. assert not newer(old_file, new_file) - def test_newer_pairwise(self): + def _setup_1234(self): tmpdir = self.mkdtemp() sources = os.path.join(tmpdir, 'sources') targets = os.path.join(tmpdir, 'targets') @@ -41,9 +42,30 @@ def test_newer_pairwise(self): self.write_file(one) self.write_file(two) self.write_file(four) + return one, two, three, four + + def test_newer_pairwise(self): + one, two, three, four = self._setup_1234() assert newer_pairwise([one, two], [three, four]) == ([one], [three]) + def test_newer_pairwise_mismatch(self): + one, two, three, four = self._setup_1234() + + with pytest.raises(ValueError): + newer_pairwise([one], [three, four]) + + with pytest.raises(ValueError): + newer_pairwise([one, two], [three]) + + def test_newer_pairwise_empty(self): + assert newer_pairwise([], []) == ([], []) + + def test_newer_pairwise_fresh(self): + one, two, three, four = self._setup_1234() + + assert newer_pairwise([one, three], [two, four]) == ([], []) + def test_newer_group(self): tmpdir = self.mkdtemp() sources = os.path.join(tmpdir, 'sources') @@ -69,3 +91,29 @@ def test_newer_group(self): assert not newer_group([one, two, old_file], three, missing='ignore') assert newer_group([one, two, old_file], three, missing='newer') + + +@pytest.fixture +def groups_target(tmp_path): + """ + Set up some older sources, a target, and newer sources. + + Returns a simple namespace with these values. + """ + filenames = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] + paths = [tmp_path / name for name in filenames] + + for mtime, path in enumerate(paths): + path.write_text('', encoding='utf-8') + + # make sure modification times are sequential + os.utime(path, (mtime, mtime)) + + return types.SimpleNamespace(older=paths[:2], target=paths[2], newer=paths[3:]) + + +def test_newer_pairwise_group(groups_target): + older = newer_pairwise_group([groups_target.older], [groups_target.target]) + newer = newer_pairwise_group([groups_target.newer], [groups_target.target]) + assert older == ([], []) + assert newer == ([groups_target.newer], [groups_target.target]) diff --git a/distutils/tests/test_msvc9compiler.py b/distutils/tests/test_msvc9compiler.py index a648b4c3..6f6aabee 100644 --- a/distutils/tests/test_msvc9compiler.py +++ b/distutils/tests/test_msvc9compiler.py @@ -1,4 +1,5 @@ """Tests for distutils.msvc9compiler.""" + import os import sys from distutils.errors import DistutilsPlatformError @@ -160,7 +161,7 @@ def test_remove_visual_c_ref(self): f = open(manifest) try: # removing trailing spaces - content = '\n'.join([line.rstrip() for line in f.readlines()]) + content = '\n'.join([line.rstrip() for line in f]) finally: f.close() diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index cad0658f..23b6c732 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -1,4 +1,5 @@ """Tests for distutils._msvccompiler.""" + import os import sys import threading diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 1f1ca84c..d071bbe9 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -1,6 +1,8 @@ """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 @@ -124,16 +126,8 @@ def test_create_pypirc(self): finally: del register_module.input - # we should have a brand new .pypirc file - assert os.path.exists(self.rc) - - # with the content similar to WANTED_PYPIRC - f = open(self.rc) - try: - content = f.read() - assert content == WANTED_PYPIRC - finally: - f.close() + # 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 @@ -157,7 +151,6 @@ def _no_way(prompt=''): 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() diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index b898a661..a85997f1 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,5 +1,7 @@ """Tests for distutils.command.sdist.""" + import os +import pathlib import tarfile import warnings import zipfile @@ -16,8 +18,9 @@ import jaraco.path import path import pytest +from more_itertools import ilen -from .py38compat import check_warnings +from .compat.py38 import check_warnings from .unix_compat import grp, pwd, require_uid_0, require_unix_id SETUP_PY = """ @@ -60,6 +63,11 @@ def project_dir(request, pypirc): yield +def clean_lines(filepath): + with pathlib.Path(filepath).open(encoding='utf-8') as f: + yield from filter(None, map(str.strip, f)) + + class TestSDist(BasePyPIRCCommandTestCase): def get_cmd(self, metadata=None): """Returns a cmd""" @@ -161,8 +169,7 @@ def test_make_distribution(self): @pytest.mark.usefixtures('needs_zlib') def test_add_defaults(self): - - # http://bugs.python.org/issue2279 + # https://bugs.python.org/issue2279 # add_default should also include # data_files and package_data @@ -242,11 +249,7 @@ def test_add_defaults(self): assert sorted(content) == ['fake-1.0/' + x for x in expected] # checking the MANIFEST - f = open(join(self.tmp_dir, 'MANIFEST')) - try: - manifest = f.read() - finally: - f.close() + manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8') assert manifest == MANIFEST % {'sep': os.sep} @staticmethod @@ -351,15 +354,7 @@ def test_get_file_list(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert len(manifest) == 5 + assert ilen(clean_lines(cmd.manifest)) == 5 # adding a file self.write_file((self.tmp_dir, 'somecode', 'doc2.txt'), '#') @@ -371,13 +366,7 @@ def test_get_file_list(self): cmd.run() - f = open(cmd.manifest) - try: - manifest2 = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() + manifest2 = list(clean_lines(cmd.manifest)) # do we have the new file in MANIFEST ? assert len(manifest2) == 6 @@ -390,15 +379,10 @@ def test_manifest_marker(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert manifest[0] == '# file GENERATED by distutils, do NOT edit' + assert ( + next(clean_lines(cmd.manifest)) + == '# file GENERATED by distutils, do NOT edit' + ) @pytest.mark.usefixtures('needs_zlib') def test_manifest_comments(self): @@ -433,15 +417,7 @@ def test_manual_manifest(self): cmd.run() assert cmd.filelist.files == ['README.manual'] - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert manifest == ['README.manual'] + assert list(clean_lines(cmd.manifest)) == ['README.manual'] archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') archive = tarfile.open(archive_name) diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index 13307828..1f623837 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -1,4 +1,5 @@ """Tests for distutils.spawn.""" + import os import stat import sys @@ -11,7 +12,7 @@ import path import pytest -from . import py38compat as os_helper +from .compat import py38 as os_helper class TestSpawn(support.TempdirManager): @@ -44,14 +45,9 @@ def test_spawn(self): spawn([exe]) # should work without any error def test_find_executable(self, tmp_path): - program_noeext = 'program' - # Give the temporary program an ".exe" suffix for all. - # It's needed on Windows and not harmful on other platforms. - program = program_noeext + ".exe" - - program_path = tmp_path / program - program_path.write_text("") - program_path.chmod(stat.S_IXUSR) + program_path = self._make_executable(tmp_path, '.exe') + program = program_path.name + program_noeext = program_path.with_suffix('').name filename = str(program_path) tmp_dir = path.Path(tmp_path) @@ -120,6 +116,15 @@ def test_find_executable(self, tmp_path): rv = find_executable(program) assert rv == filename + @staticmethod + def _make_executable(tmp_path, ext): + # Give the temporary program a suffix regardless of platform. + # It's needed on Windows and not harmful on others. + program = tmp_path.joinpath('program').with_suffix(ext) + program.write_text("", encoding='utf-8') + program.chmod(stat.S_IXUSR) + return program + def test_spawn_missing_exe(self): with pytest.raises(DistutilsExecError) as ctx: spawn(['does-not-exist']) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 55d558e9..faa8e31c 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -1,4 +1,5 @@ """Tests for distutils.sysconfig.""" + import contextlib import distutils import os @@ -15,7 +16,10 @@ import pytest from jaraco.text import trim -from . import py37compat + +def _gen_makefile(root, contents): + jaraco.path.build({'Makefile': trim(contents)}, root) + return root / 'Makefile' @pytest.mark.usefixtures('save_env') @@ -96,8 +100,6 @@ def set_executables(self, **kw): 'CCSHARED': '--sc-ccshared', 'LDSHARED': 'sc_ldshared', 'SHLIB_SUFFIX': 'sc_shutil_suffix', - # On macOS, disable _osx_support.customize_compiler() - 'CUSTOMIZED_OSX_COMPILER': 'True', } comp = compiler() @@ -109,6 +111,7 @@ def set_executables(self, **kw): return comp @pytest.mark.skipif("get_default_compiler() != 'unix'") + @pytest.mark.usefixtures('disable_macos_customization') def test_customize_compiler(self): # Make sure that sysconfig._config_vars is initialized sysconfig.get_config_vars() @@ -129,12 +132,12 @@ def test_customize_compiler(self): assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags' assert comp.exes['compiler'] == 'env_cc --sc-cflags --env-cflags --env-cppflags' assert comp.exes['compiler_so'] == ( - 'env_cc --sc-cflags ' '--env-cflags ' '--env-cppflags --sc-ccshared' + 'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared' ) assert comp.exes['compiler_cxx'] == 'env_cxx --env-cxx-flags' assert comp.exes['linker_exe'] == 'env_cc' assert comp.exes['linker_so'] == ( - 'env_ldshared --env-ldflags --env-cflags' ' --env-cppflags' + 'env_ldshared --env-ldflags --env-cflags --env-cppflags' ) assert comp.shared_lib_extension == 'sc_shutil_suffix' @@ -166,29 +169,25 @@ def test_customize_compiler(self): assert 'ranlib' not in comp.exes def test_parse_makefile_base(self, tmp_path): - makefile = tmp_path / 'Makefile' - makefile.write_text( - trim( - """ - CONFIG_ARGS= '--arg1=optarg1' 'ENV=LIB' - VAR=$OTHER - OTHER=foo - """ - ) + makefile = _gen_makefile( + tmp_path, + """ + CONFIG_ARGS= '--arg1=optarg1' 'ENV=LIB' + VAR=$OTHER + OTHER=foo + """, ) d = sysconfig.parse_makefile(makefile) assert d == {'CONFIG_ARGS': "'--arg1=optarg1' 'ENV=LIB'", 'OTHER': 'foo'} def test_parse_makefile_literal_dollar(self, tmp_path): - makefile = tmp_path / 'Makefile' - makefile.write_text( - trim( - """ - CONFIG_ARGS= '--arg1=optarg1' 'ENV=\\$$LIB' - VAR=$OTHER - OTHER=foo - """ - ) + makefile = _gen_makefile( + tmp_path, + """ + CONFIG_ARGS= '--arg1=optarg1' 'ENV=\\$$LIB' + VAR=$OTHER + OTHER=foo + """, ) d = sysconfig.parse_makefile(makefile) assert d == {'CONFIG_ARGS': r"'--arg1=optarg1' 'ENV=\$LIB'", 'OTHER': 'foo'} @@ -203,22 +202,21 @@ def test_sysconfig_module(self): 'LDFLAGS' ) + # On macOS, binary installers support extension module building on + # various levels of the operating system with differing Xcode + # configurations, requiring customization of some of the + # compiler configuration directives to suit the environment on + # the installed machine. Some of these customizations may require + # running external programs and are thus deferred until needed by + # the first extension module build. Only + # the Distutils version of sysconfig is used for extension module + # builds, which happens earlier in the Distutils tests. This may + # cause the following tests to fail since no tests have caused + # the global version of sysconfig to call the customization yet. + # The solution for now is to simply skip this test in this case. + # The longer-term solution is to only have one version of sysconfig. @pytest.mark.skipif("sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER')") def test_sysconfig_compiler_vars(self): - # On OS X, binary installers support extension module building on - # various levels of the operating system with differing Xcode - # configurations. This requires customization of some of the - # compiler configuration directives to suit the environment on - # the installed machine. Some of these customizations may require - # running external programs and, so, are deferred until needed by - # the first extension module build. With Python 3.3, only - # the Distutils version of sysconfig is used for extension module - # builds, which happens earlier in the Distutils tests. This may - # cause the following tests to fail since no tests have caused - # the global version of sysconfig to call the customization yet. - # The solution for now is to simply skip this test in this case. - # The longer-term solution is to only have one version of sysconfig. - import sysconfig as global_sysconfig if sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER'): @@ -237,23 +235,24 @@ def test_customize_compiler_before_get_config_vars(self, tmp_path): # Issue #21923: test that a Distribution compiler # instance can be called without an explicit call to # get_config_vars(). - file = tmp_path / 'file' - file.write_text( - trim( - """ - from distutils.core import Distribution - config = Distribution().get_command_obj('config') - # try_compile may pass or it may fail if no compiler - # is found but it should not raise an exception. - rc = config.try_compile('int x;') - """ - ) + jaraco.path.build( + { + 'file': trim(""" + from distutils.core import Distribution + config = Distribution().get_command_obj('config') + # try_compile may pass or it may fail if no compiler + # is found but it should not raise an exception. + rc = config.try_compile('int x;') + """) + }, + tmp_path, ) p = subprocess.Popen( - py37compat.subprocess_args(sys.executable, file), + [sys.executable, tmp_path / 'file'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, + encoding='utf-8', ) outs, errs = p.communicate() assert 0 == p.returncode, "Subprocess failed: " + outs @@ -296,3 +295,22 @@ def test_win_build_venv_from_source_tree(self, tmp_path): cmd, env={**os.environ, "PYTHONPATH": distutils_path} ) assert out == "True" + + def test_get_python_inc_missing_config_dir(self, monkeypatch): + """ + In portable Python installations, the sysconfig will be broken, + pointing to the directories where the installation was built and + not where it currently is. In this case, ensure that the missing + directory isn't used for get_python_inc. + + See pypa/distutils#178. + """ + + def override(name): + if name == 'INCLUDEPY': + return '/does-not-exist' + return sysconfig.get_config_var(name) + + monkeypatch.setattr(sysconfig, 'get_config_var', override) + + assert os.path.exists(sysconfig.get_python_inc()) diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py index 9895deaa..c5c910a8 100644 --- a/distutils/tests/test_text_file.py +++ b/distutils/tests/test_text_file.py @@ -1,8 +1,11 @@ """Tests for distutils.text_file.""" -import os + from distutils.tests import support from distutils.text_file import TextFile +import jaraco.path +import path + TEST_DATA = """# test file line 3 \\ @@ -52,13 +55,9 @@ def test_input(count, description, file, expected_result): result = file.readlines() assert result == expected_result - tmpdir = self.mkdtemp() - filename = os.path.join(tmpdir, "test.txt") - out_file = open(filename, "w") - try: - out_file.write(TEST_DATA) - finally: - out_file.close() + tmp_path = path.Path(self.mkdtemp()) + filename = tmp_path / 'test.txt' + jaraco.path.build({filename.name: TEST_DATA}, tmp_path) in_file = TextFile( filename, strip_comments=0, skip_blanks=0, lstrip_ws=0, rstrip_ws=0 diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index c54f8515..543aa20d 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -1,8 +1,10 @@ """Tests for distutils.unixccompiler.""" + import os import sys import unittest.mock as mock from distutils import sysconfig +from distutils.compat import consolidate_linker_args from distutils.errors import DistutilsPlatformError from distutils.unixccompiler import UnixCCompiler from distutils.util import _clear_cached_macosx_ver @@ -10,7 +12,7 @@ import pytest from . import support -from .py38compat import EnvironmentVarGuard +from .compat.py38 import EnvironmentVarGuard @pytest.fixture(autouse=True) @@ -71,10 +73,7 @@ def gcv(var): def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag): env = os.environ - msg = "macOS version = (sysconfig={!r}, env={!r})".format( - syscfg_macosx_ver, - env_macosx_ver, - ) + msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})" # Save old_gcv = sysconfig.get_config_var @@ -151,7 +150,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == consolidate_linker_args([ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ]) def gcv(v): if v == 'CC': @@ -160,7 +162,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == consolidate_linker_args([ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ]) # GCC non-GNULD sys.platform = 'bar' @@ -185,7 +190,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == consolidate_linker_args([ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ]) # non-GCC GNULD sys.platform = 'bar' @@ -197,7 +205,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == consolidate_linker_args([ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ]) # non-GCC non-GNULD sys.platform = 'bar' @@ -234,6 +245,7 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): assert self.cc.linker_so[0] == 'my_cc' @pytest.mark.skipif('platform.system == "Windows"') + @pytest.mark.usefixtures('disable_macos_customization') def test_cc_overrides_ldshared_for_cxx_correctly(self): """ Ensure that setting CC env variable also changes default linker @@ -302,4 +314,4 @@ def test_has_function(self): # FileNotFoundError: [Errno 2] No such file or directory: 'a.out' self.cc.output_dir = 'scratch' os.chdir(self.mkdtemp()) - self.cc.has_function('abort', includes=['stdlib.h']) + self.cc.has_function('abort') diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index 263d92e1..0692f001 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -1,4 +1,5 @@ """Tests for distutils.command.upload.""" + import os import unittest.mock as mock from distutils.command import upload as upload_mod @@ -75,7 +76,6 @@ def _urlopen(self, url): return self.last_open def test_finalize_options(self): - # new format self.write_file(self.rc, PYPIRC) dist = Distribution() diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 8864b33f..78d8b1e3 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -1,4 +1,9 @@ """Tests for distutils.util.""" + +import email +import email.generator +import email.policy +import io import os import sys import sysconfig as stdlib_sysconfig @@ -149,9 +154,15 @@ def test_check_environ_getpwuid(self): import pwd # only set pw_dir field, other fields are not used - result = pwd.struct_passwd( - (None, None, None, None, None, '/home/distutils', None) - ) + result = pwd.struct_passwd(( + None, + None, + None, + None, + None, + '/home/distutils', + None, + )) with mock.patch.object(pwd, 'getpwuid', return_value=result): check_environ() assert os.environ['HOME'] == '/home/distutils' @@ -182,12 +193,55 @@ def test_strtobool(self): for n in no: assert not strtobool(n) - def test_rfc822_escape(self): - header = 'I am a\npoor\nlonesome\nheader\n' - res = rfc822_escape(header) - wanted = ('I am a%(8s)spoor%(8s)slonesome%(8s)s' 'header%(8s)s') % { - '8s': '\n' + 8 * ' ' - } + indent = 8 * ' ' + + @pytest.mark.parametrize( + "given,wanted", + [ + # 0x0b, 0x0c, ..., etc are also considered a line break by Python + ("hello\x0b\nworld\n", f"hello\x0b{indent}\n{indent}world\n{indent}"), + ("hello\x1eworld", f"hello\x1e{indent}world"), + ("", ""), + ( + "I am a\npoor\nlonesome\nheader\n", + f"I am a\n{indent}poor\n{indent}lonesome\n{indent}header\n{indent}", + ), + ], + ) + def test_rfc822_escape(self, given, wanted): + """ + We want to ensure a multi-line header parses correctly. + + For interoperability, the escaped value should also "round-trip" over + `email.generator.Generator.flatten` and `email.message_from_*` + (see pypa/setuptools#4033). + + The main issue is that internally `email.policy.EmailPolicy` uses + `splitlines` which will split on some control chars. If all the new lines + are not prefixed with spaces, the parser will interrupt reading + the current header and produce an incomplete value, while + incorrectly interpreting the rest of the headers as part of the payload. + """ + res = rfc822_escape(given) + + policy = email.policy.EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with io.StringIO() as buffer: + raw = f"header: {res}\nother-header: 42\n\npayload\n" + orig = email.message_from_string(raw) + email.generator.Generator(buffer, policy=policy).flatten(orig) + buffer.seek(0) + regen = email.message_from_file(buffer) + + for msg in (orig, regen): + assert msg.get_payload() == "payload\n" + assert msg["other-header"] == "42" + # Generator may replace control chars with `\n` + assert set(msg["header"].splitlines()) == set(res.splitlines()) + assert res == wanted def test_dont_write_bytecode(self): @@ -203,6 +257,6 @@ def test_dont_write_bytecode(self): def test_grok_environment_error(self): # test obsolete function to ensure backward compat (#4931) - exc = IOError("Unable to find batch file") + exc = OSError("Unable to find batch file") msg = grok_environment_error(exc) assert msg == "error: Unable to find batch file" diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index f6ceece3..1508e1cc 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -1,4 +1,5 @@ """Tests for distutils.version.""" + import distutils from distutils.version import LooseVersion, StrictVersion @@ -47,20 +48,14 @@ def test_cmp_strict(self): if wanted is ValueError: continue else: - raise AssertionError( - ("cmp(%s, %s) " "shouldn't raise ValueError") % (v1, v2) - ) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + raise AssertionError(f"cmp({v1}, {v2}) shouldn't raise ValueError") + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(v2) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' def test_cmp(self): versions = ( @@ -76,14 +71,10 @@ def test_cmp(self): for v1, v2, wanted in versions: res = LooseVersion(v1)._cmp(LooseVersion(v2)) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(v2) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' diff --git a/distutils/text_file.py b/distutils/text_file.py index 7274d4b1..0f846e3c 100644 --- a/distutils/text_file.py +++ b/distutils/text_file.py @@ -115,7 +115,7 @@ def open(self, filename): """Open a new file named 'filename'. This overrides both the 'filename' and 'file' arguments to the constructor.""" self.filename = filename - self.file = open(self.filename, errors=self.errors) + self.file = open(self.filename, errors=self.errors, encoding='utf-8') self.current_line = 0 def close(self): @@ -180,7 +180,6 @@ def readline(self): # noqa: C901 line = None if self.strip_comments and line: - # Look for the first "#" in the line. If none, never # mind. If we find one and it's the first character, or # is not preceded by "\", then it starts a comment -- @@ -221,7 +220,7 @@ def readline(self): # noqa: C901 if self.join_lines and buildup_line: # oops: end of file if line is None: - self.warn("continuation line immediately precedes " "end-of-file") + self.warn("continuation line immediately precedes end-of-file") return buildup_line if self.collapse_join: @@ -255,7 +254,7 @@ def readline(self): # noqa: C901 # blank line (whether we rstrip'ed or not)? skip to next line # if appropriate - if (line == '' or line == '\n') and self.skip_blanks: + if line in ('', '\n') and self.skip_blanks: continue if self.join_lines: diff --git a/distutils/util.py b/distutils/util.py index a03c1d45..2cdea143 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -14,7 +14,7 @@ import sysconfig from ._log import log -from .dep_util import newer +from ._modified import newer from .errors import DistutilsByteCompileError, DistutilsPlatformError from .spawn import spawn @@ -30,18 +30,11 @@ def get_host_platform(): # even with older Python versions when distutils was split out. # Now it delegates to stdlib sysconfig, but maintains compatibility. - if sys.version_info < (3, 8): - if os.name == 'nt': - if '(arm)' in sys.version.lower(): - return 'win-arm32' - if '(arm64)' in sys.version.lower(): - return 'win-arm64' - if sys.version_info < (3, 9): if os.name == "posix" and hasattr(os, 'uname'): osname, host, release, version, machine = os.uname() if osname[:3] == "aix": - from .py38compat import aix_platform + from .compat.py38 import aix_platform return aix_platform(osname, version, release) @@ -109,8 +102,8 @@ def get_macosx_target_ver(): ): my_msg = ( '$' + MACOSX_VERSION_VAR + ' mismatch: ' - 'now "%s" but "%s" during configure; ' - 'must use 10.3 or later' % (env_ver, syscfg_ver) + f'now "{env_ver}" but "{syscfg_ver}" during configure; ' + 'must use 10.3 or later' ) raise DistutilsPlatformError(my_msg) return env_ver @@ -172,7 +165,7 @@ def change_root(new_root, pathname): raise DistutilsPlatformError(f"nothing known about platform '{os.name}'") -@functools.lru_cache() +@functools.lru_cache def check_environ(): """Ensure that 'os.environ' has all the environment variables we guarantee that users can use in config files, command-line options, @@ -228,7 +221,7 @@ def _subst(match): import warnings warnings.warn( - "shell/Perl-style substitions are deprecated", + "shell/Perl-style substitutions are deprecated", DeprecationWarning, ) return repl @@ -328,7 +321,7 @@ def execute(func, args, msg=None, verbose=0, dry_run=0): print. """ if msg is None: - msg = "{}{!r}".format(func.__name__, args) + msg = f"{func.__name__}{args!r}" if msg[-2:] == ',)': # correct for singleton tuple msg = msg[0:-2] + ')' @@ -350,7 +343,7 @@ def strtobool(val): elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 else: - raise ValueError("invalid truth value {!r}".format(val)) + raise ValueError(f"invalid truth value {val!r}") def byte_compile( # noqa: C901 @@ -423,9 +416,9 @@ def byte_compile( # noqa: C901 log.info("writing byte-compilation script '%s'", script_name) if not dry_run: if script_fd is not None: - script = os.fdopen(script_fd, "w") - else: - script = open(script_name, "w") + script = os.fdopen(script_fd, "w", encoding='utf-8') + else: # pragma: no cover + script = open(script_name, "w", encoding='utf-8') with script: script.write( @@ -447,13 +440,12 @@ def byte_compile( # noqa: C901 script.write(",\n".join(map(repr, py_files)) + "]\n") script.write( - """ -byte_compile(files, optimize=%r, force=%r, - prefix=%r, base_dir=%r, - verbose=%r, dry_run=0, + f""" +byte_compile(files, optimize={optimize!r}, force={force!r}, + prefix={prefix!r}, base_dir={base_dir!r}, + verbose={verbose!r}, dry_run=0, direct=1) """ - % (optimize, force, prefix, base_dir, verbose) ) cmd = [sys.executable] @@ -487,8 +479,7 @@ def byte_compile( # noqa: C901 if prefix: if file[: len(prefix)] != prefix: raise ValueError( - "invalid prefix: filename %r doesn't start with %r" - % (file, prefix) + f"invalid prefix: filename {file!r} doesn't start with {prefix!r}" ) dfile = dfile[len(prefix) :] if base_dir: @@ -508,6 +499,12 @@ def rfc822_escape(header): """Return a version of the string escaped for inclusion in an RFC-822 header, by ensuring there are 8 spaces space after each newline. """ - lines = header.split('\n') - sep = '\n' + 8 * ' ' - return sep.join(lines) + indent = 8 * " " + lines = header.splitlines(keepends=True) + + # Emulate the behaviour of `str.split` + # (the terminal line break in `splitlines` does not result in an extra line): + ends_in_newline = lines and lines[-1].splitlines()[0] != lines[-1] + suffix = indent if ends_in_newline else "" + + return indent.join(lines) + suffix diff --git a/distutils/version.py b/distutils/version.py index 481f3c72..806d233c 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -60,7 +60,7 @@ def __init__(self, vstring=None): ) def __repr__(self): - return "{} ('{}')".format(self.__class__.__name__, str(self)) + return f"{self.__class__.__name__} ('{str(self)}')" def __eq__(self, other): c = self._cmp(other) @@ -111,7 +111,6 @@ def __ge__(self, other): class StrictVersion(Version): - """Version numbering for anal retentives and software idealists. Implements the standard interface for version number classes as described above. A version number consists of two or three @@ -169,7 +168,6 @@ def parse(self, vstring): self.prerelease = None def __str__(self): - if self.version[2] == 0: vstring = '.'.join(map(str, self.version[0:2])) else: @@ -180,42 +178,36 @@ def __str__(self): return vstring - def _cmp(self, other): # noqa: C901 + def _cmp(self, other): if isinstance(other, str): with suppress_known_deprecation(): other = StrictVersion(other) elif not isinstance(other, StrictVersion): return NotImplemented - if self.version != other.version: - # numeric versions don't match - # prerelease stuff doesn't matter - if self.version < other.version: - return -1 - else: - return 1 - - # have to compare prerelease - # case 1: neither has prerelease; they're equal - # case 2: self has prerelease, other doesn't; other is greater - # case 3: self doesn't have prerelease, other does: self is greater - # case 4: both have prerelease: must compare them! - - if not self.prerelease and not other.prerelease: - return 0 - elif self.prerelease and not other.prerelease: + if self.version == other.version: + # versions match; pre-release drives the comparison + return self._cmp_prerelease(other) + + return -1 if self.version < other.version else 1 + + def _cmp_prerelease(self, other): + """ + case 1: self has prerelease, other doesn't; other is greater + case 2: self doesn't have prerelease, other does: self is greater + case 3: both or neither have prerelease: compare them! + """ + if self.prerelease and not other.prerelease: return -1 elif not self.prerelease and other.prerelease: return 1 - elif self.prerelease and other.prerelease: - if self.prerelease == other.prerelease: - return 0 - elif self.prerelease < other.prerelease: - return -1 - else: - return 1 + + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 else: - assert False, "never get here" + return 1 # end class StrictVersion @@ -287,7 +279,6 @@ def _cmp(self, other): # noqa: C901 class LooseVersion(Version): - """Version numbering for anarchists and software realists. Implements the standard interface for version number classes as described above. A version number consists of a series of numbers, diff --git a/distutils/versionpredicate.py b/distutils/versionpredicate.py index 686b21c1..31c42016 100644 --- a/distutils/versionpredicate.py +++ b/distutils/versionpredicate.py @@ -1,5 +1,5 @@ -"""Module for parsing and testing package version predicate strings. -""" +"""Module for parsing and testing package version predicate strings.""" + import operator import re diff --git a/distutils/zosccompiler.py b/distutils/zosccompiler.py new file mode 100644 index 00000000..c7a7ca61 --- /dev/null +++ b/distutils/zosccompiler.py @@ -0,0 +1,229 @@ +"""distutils.zosccompiler + +Contains the selection of the c & c++ compilers on z/OS. There are several +different c compilers on z/OS, all of them are optional, so the correct +one needs to be chosen based on the users input. This is compatible with +the following compilers: + +IBM C/C++ For Open Enterprise Languages on z/OS 2.0 +IBM Open XL C/C++ 1.1 for z/OS +IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5 +IBM z/OS XL C/C++ +""" + +import os + +from . import sysconfig +from .errors import CompileError, DistutilsExecError +from .unixccompiler import UnixCCompiler + +_cc_args = { + 'ibm-openxl': [ + '-m64', + '-fvisibility=default', + '-fzos-le-char-mode=ascii', + '-fno-short-enums', + ], + 'ibm-xlclang': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + ], + 'ibm-xlc': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + '-qlanglvl=extc99', + ], +} + +_cxx_args = { + 'ibm-openxl': [ + '-m64', + '-fvisibility=default', + '-fzos-le-char-mode=ascii', + '-fno-short-enums', + ], + 'ibm-xlclang': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + ], + 'ibm-xlc': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + '-qlanglvl=extended0x', + ], +} + +_asm_args = { + 'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'], + 'ibm-xlclang': [], + 'ibm-xlc': [], +} + +_ld_args = { + 'ibm-openxl': [], + 'ibm-xlclang': ['-Wl,dll', '-q64'], + 'ibm-xlc': ['-Wl,dll', '-q64'], +} + + +# Python on z/OS is built with no compiler specific options in it's CFLAGS. +# But each compiler requires it's own specific options to build successfully, +# though some of the options are common between them +class zOSCCompiler(UnixCCompiler): + src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s'] + _cpp_extensions = ['.cc', '.cpp', '.cxx', '.C'] + _asm_extensions = ['.s'] + + def _get_zos_compiler_name(self): + zos_compiler_names = [ + os.path.basename(binary) + for envvar in ('CC', 'CXX', 'LDSHARED') + if (binary := os.environ.get(envvar, None)) + ] + if len(zos_compiler_names) == 0: + return 'ibm-openxl' + + zos_compilers = {} + for compiler in ( + 'ibm-clang', + 'ibm-clang64', + 'ibm-clang++', + 'ibm-clang++64', + 'clang', + 'clang++', + 'clang-14', + ): + zos_compilers[compiler] = 'ibm-openxl' + + for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'): + zos_compilers[compiler] = 'ibm-xlclang' + + for compiler in ('xlc', 'xlC', 'xlc++'): + zos_compilers[compiler] = 'ibm-xlc' + + return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl') + + def __init__(self, verbose=0, dry_run=0, force=0): + super().__init__(verbose, dry_run, force) + self.zos_compiler = self._get_zos_compiler_name() + sysconfig.customize_compiler(self) + + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): + local_args = [] + if ext in self._cpp_extensions: + compiler = self.compiler_cxx + local_args.extend(_cxx_args[self.zos_compiler]) + elif ext in self._asm_extensions: + compiler = self.compiler_so + local_args.extend(_cc_args[self.zos_compiler]) + local_args.extend(_asm_args[self.zos_compiler]) + else: + compiler = self.compiler_so + local_args.extend(_cc_args[self.zos_compiler]) + local_args.extend(cc_args) + + try: + self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs) + except DistutilsExecError as msg: + raise CompileError(msg) + + def runtime_library_dir_option(self, dir): + return '-L' + dir + + def link( + self, + target_desc, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, + ): + # For a built module to use functions from cpython, it needs to use Pythons + # side deck file. The side deck is located beside the libpython3.xx.so + ldversion = sysconfig.get_config_var('LDVERSION') + if sysconfig.python_build: + side_deck_path = os.path.join( + sysconfig.get_config_var('abs_builddir'), + f'libpython{ldversion}.x', + ) + else: + side_deck_path = os.path.join( + sysconfig.get_config_var('installed_base'), + sysconfig.get_config_var('platlibdir'), + f'libpython{ldversion}.x', + ) + + if os.path.exists(side_deck_path): + if extra_postargs: + extra_postargs.append(side_deck_path) + else: + extra_postargs = [side_deck_path] + + # Check and replace libraries included side deck files + if runtime_library_dirs: + for dir in runtime_library_dirs: + for library in libraries[:]: + library_side_deck = os.path.join(dir, f'{library}.x') + if os.path.exists(library_side_deck): + libraries.remove(library) + extra_postargs.append(library_side_deck) + break + + # Any required ld args for the given compiler + extra_postargs.extend(_ld_args[self.zos_compiler]) + + super().link( + target_desc, + objects, + output_filename, + output_dir, + libraries, + library_dirs, + runtime_library_dirs, + export_symbols, + debug, + extra_preargs, + extra_postargs, + build_temp, + target_lang, + ) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..d673f413 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,45 @@ +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] + +master_doc = "index" +html_theme = "furo" + +# Link dates and other references in the changelog +extensions += ['rst.linker'] +link_files = { + '../NEWS.rst': dict( + using=dict(GH='https://github.com'), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://peps.python.org/pep-{pep_number:0>4}/', + ), + ], + ) +} + +# Be strict about any broken references +nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True + +# for distutils, disable nitpicky +nitpicky = False diff --git a/docs/distutils/apiref.rst b/docs/distutils/apiref.rst index 83b8ef5d..beb17bc3 100644 --- a/docs/distutils/apiref.rst +++ b/docs/distutils/apiref.rst @@ -1021,7 +1021,7 @@ directories. Files in *src* that begin with :file:`.nfs` are skipped (more information on these files is available in answer D2 of the `NFS FAQ page - `_). + `_). .. versionchanged:: 3.3.1 NFS files are ignored. diff --git a/docs/distutils/configfile.rst b/docs/distutils/configfile.rst index bdd7c455..30cccd71 100644 --- a/docs/distutils/configfile.rst +++ b/docs/distutils/configfile.rst @@ -131,13 +131,6 @@ Note that the ``doc_files`` option is simply a whitespace-separated string split across multiple lines for readability. -.. seealso:: - - :ref:`inst-config-syntax` in "Installing Python Modules" - More information on the configuration files is available in the manual for - system administrators. - - .. rubric:: Footnotes .. [#] This ideal probably won't be achieved until auto-configuration is fully diff --git a/docs/distutils/examples.rst b/docs/distutils/examples.rst index 28582bab..d758a810 100644 --- a/docs/distutils/examples.rst +++ b/docs/distutils/examples.rst @@ -335,4 +335,4 @@ loads its values:: .. % \section{Putting it all together} -.. _docutils: http://docutils.sourceforge.net +.. _docutils: https://docutils.sourceforge.io diff --git a/docs/distutils/packageindex.rst b/docs/distutils/packageindex.rst index ccb9a598..27ea717a 100644 --- a/docs/distutils/packageindex.rst +++ b/docs/distutils/packageindex.rst @@ -6,11 +6,10 @@ The Python Package Index (PyPI) ******************************* -The `Python Package Index (PyPI)`_ stores metadata describing distributions -packaged with distutils and other publishing tools, as well the distribution -archives themselves. +The `Python Package Index (PyPI) `_ stores +metadata describing distributions packaged with distutils and +other publishing tools, as well the distribution archives +themselves. -References to up to date PyPI documentation can be found at -:ref:`publishing-python-packages`. - -.. _Python Package Index (PyPI): https://pypi.org +The best resource for working with PyPI is the +`Python Packaging User Guide `_. diff --git a/docs/distutils/setupscript.rst b/docs/distutils/setupscript.rst index 3c8e1ab1..71d2439f 100644 --- a/docs/distutils/setupscript.rst +++ b/docs/distutils/setupscript.rst @@ -642,7 +642,7 @@ Notes: 'long string' Multiple lines of plain text in reStructuredText format (see - http://docutils.sourceforge.net/). + https://docutils.sourceforge.io/). 'list of strings' See below. diff --git a/docs/distutils/uploading.rst b/docs/distutils/uploading.rst index 4c391cab..f5c4c619 100644 --- a/docs/distutils/uploading.rst +++ b/docs/distutils/uploading.rst @@ -4,5 +4,6 @@ Uploading Packages to the Package Index *************************************** -References to up to date PyPI documentation can be found at -:ref:`publishing-python-packages`. +See the +`Python Packaging User Guide `_ +for the best guidance on uploading packages. diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..5bdc2320 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../NEWS (links).rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..6b70ccf9 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +Welcome to |project| documentation! +=================================== + +.. sidebar-links:: + :home: + :pypi: + +.. toctree:: + :maxdepth: 1 + + history + distutils/index + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b6f97276 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True +# required to support namespace packages +# https://github.com/python/mypy/issues/14057 +explicit_package_bases = True diff --git a/pyproject.toml b/pyproject.toml index e6863cff..738546e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,60 @@ -[tool.black] -skip-string-normalization = true +[build-system] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" -[tool.pytest-enabler.black] -addopts = "--black" +[project] +name = "distutils" +authors = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "Distribution utilities formerly from standard library" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] +requires-python = ">=3.8" +dependencies = [] +dynamic = ["version"] -[tool.pytest-enabler.flake8] -addopts = "--flake8" +[project.urls] +Homepage = "https://github.com/pypa/distutils" -[tool.pytest-enabler.cov] -addopts = "--cov" +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6, != 8.1.1", + "pytest-checkdocs >= 2.4", + "pytest-cov", + "pytest-mypy", + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local + "pytest >= 7.4.3", # 186 + "jaraco.envs>=2.4", + "jaraco.path", + "jaraco.text", + "path >= 10.6", + "docutils", + "pyfakefs", + "more_itertools", +] +docs = [ + # upstream + "sphinx >= 3.5", + "jaraco.packaging >= 9.3", + "rst.linker >= 1.9", + "furo", + "sphinx-lint", + + # local +] + +[tool.setuptools_scm] + +[tool.pytest-enabler.mypy] +# disabled diff --git a/pytest.ini b/pytest.ini index 3f8cc25f..f9b1d1fc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,18 +1,28 @@ [pytest] -addopts=--doctest-modules +norecursedirs=dist build .tox .eggs +addopts= + --doctest-modules + --import-mode importlib +consider_namespace_packages=true filterwarnings= - # Suppress deprecation warning in flake8 - ignore:SelectableGroups dict interface is deprecated::flake8 - - # shopkeep/pytest-black#55 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning - ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - - # tholo/pytest-flake8#83 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning - ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning + ## upstream + + # Ensure ResourceWarnings are emitted + default::ResourceWarning + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore:'encoding' argument not specified::platform + + # pypa/build#615 + ignore:'encoding' argument not specified::build.env + + # dateutil/dateutil#1284 + ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz + + ## end upstream # acknowledge that TestDistribution isn't a test ignore:cannot collect test class 'TestDistribution' diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..70612985 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,28 @@ +[lint] +extend-select = [ + "C901", + "W", +] +ignore = [ + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] + +[format] +# Enable preview, required for quote-style = "preserve" +preview = true +# https://docs.astral.sh/ruff/settings/#format-quote-style +quote-style = "preserve" diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini index c4200483..d2bccae3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,63 @@ -[tox] -minversion = 3.25 -toxworkdir={env:TOX_WORK_DIR:.tox} - - [testenv] +description = perform primary checks (tests, style, types, coverage) deps = - # < 7.2 due to pypa/distutils#186 - pytest < 7.2 +setenv = + PYTHONWARNDEFAULTENCODING = 1 + # pypa/distutils#99 + VIRTUALENV_NO_SETUPTOOLS = 1 +commands = + pytest {posargs} +usedevelop = True +extras = + testing - pytest-flake8 - # workaround for tholo/pytest-flake8#87 - flake8 < 5 +[testenv:diffcov] +description = run tests and check that diff from main is covered +deps = + {[testenv]deps} + diff-cover +commands = + pytest {posargs} --cov-report xml + diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 - pytest-black - pytest-cov - pytest-enabler >= 1.3 +[testenv:docs] +description = build the documentation +extras = + docs + testing +changedir = docs +commands = + python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint \ + # workaround for sphinx-contrib/sphinx-lint#83 + --jobs 1 - jaraco.envs>=2.4 - jaraco.path - jaraco.text - path - docutils - pyfakefs - more_itertools +[testenv:finalize] +description = assemble changelog and tag a release +skip_install = True +deps = + towncrier + jaraco.develop >= 7.23 +pass_env = * commands = - pytest {posargs} -setenv = - PYTHONPATH = {toxinidir} - # pypa/distutils#99 - VIRTUALENV_NO_SETUPTOOLS = 1 + python -m jaraco.develop.finalize + + +[testenv:release] +description = publish the package to PyPI and GitHub skip_install = True +deps = + build + twine>=3 + jaraco.develop>=7.1 +pass_env = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + python -m twine upload dist/* + python -m jaraco.develop.create-github-release