├── .coveragerc ├── .editorconfig ├── .flake8 ├── .github └── workflows │ └── main.yml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── NEWS.rst ├── README.rst ├── conftest.py ├── distutils ├── __init__.py ├── _log.py ├── _macos_compat.py ├── _modified.py ├── _msvccompiler.py ├── archive_util.py ├── ccompiler.py ├── cmd.py ├── command │ ├── __init__.py │ ├── _framework_compat.py │ ├── bdist.py │ ├── bdist_dumb.py │ ├── bdist_rpm.py │ ├── build.py │ ├── build_clib.py │ ├── build_ext.py │ ├── build_py.py │ ├── build_scripts.py │ ├── check.py │ ├── clean.py │ ├── command_template │ ├── config.py │ ├── install.py │ ├── install_data.py │ ├── install_egg_info.py │ ├── install_headers.py │ ├── install_lib.py │ ├── install_scripts.py │ └── sdist.py ├── compat │ ├── __init__.py │ ├── numpy.py │ └── py39.py ├── compilers │ └── C │ │ ├── base.py │ │ ├── cygwin.py │ │ ├── errors.py │ │ ├── msvc.py │ │ ├── py.typed │ │ ├── tests │ │ ├── test_base.py │ │ ├── test_cygwin.py │ │ ├── test_mingw.py │ │ ├── test_msvc.py │ │ └── test_unix.py │ │ ├── unix.py │ │ └── zos.py ├── core.py ├── cygwinccompiler.py ├── debug.py ├── dep_util.py ├── dir_util.py ├── dist.py ├── errors.py ├── extension.py ├── fancy_getopt.py ├── file_util.py ├── filelist.py ├── log.py ├── py.typed ├── spawn.py ├── sysconfig.py ├── tests │ ├── Setup.sample │ ├── __init__.py │ ├── compat │ │ ├── __init__.py │ │ └── py39.py │ ├── includetest.rst │ ├── support.py │ ├── test_archive_util.py │ ├── test_bdist.py │ ├── test_bdist_dumb.py │ ├── test_bdist_rpm.py │ ├── test_build.py │ ├── test_build_clib.py │ ├── test_build_ext.py │ ├── test_build_py.py │ ├── test_build_scripts.py │ ├── test_check.py │ ├── test_clean.py │ ├── test_cmd.py │ ├── test_config_cmd.py │ ├── test_core.py │ ├── test_dir_util.py │ ├── test_dist.py │ ├── test_extension.py │ ├── test_file_util.py │ ├── test_filelist.py │ ├── test_install.py │ ├── test_install_data.py │ ├── test_install_headers.py │ ├── test_install_lib.py │ ├── test_install_scripts.py │ ├── test_log.py │ ├── test_modified.py │ ├── test_sdist.py │ ├── test_spawn.py │ ├── test_sysconfig.py │ ├── test_text_file.py │ ├── test_util.py │ ├── test_version.py │ ├── test_versionpredicate.py │ ├── unix_compat.py │ ├── xxmodule-3.8.c │ └── xxmodule.c ├── text_file.py ├── unixccompiler.py ├── util.py ├── version.py ├── versionpredicate.py └── zosccompiler.py ├── docs ├── conf.py ├── distutils │ ├── _setuptools_disclaimer.rst │ ├── apiref.rst │ ├── builtdist.rst │ ├── commandref.rst │ ├── configfile.rst │ ├── examples.rst │ ├── extending.rst │ ├── index.rst │ ├── introduction.rst │ ├── packageindex.rst │ ├── setupscript.rst │ ├── sourcedist.rst │ └── uploading.rst ├── history.rst └── index.rst ├── mypy.ini ├── newsfragments ├── 4874.feature.rst └── 4902.bugfix.rst ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── towncrier.toml └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # leading `*/` for pytest-dev/pytest-cov#456 4 | */.tox/* 5 | 6 | # local 7 | */compat/* 8 | */distutils/_vendor/* 9 | disable_warnings = 10 | couldnt-parse 11 | 12 | [report] 13 | show_missing = True 14 | exclude_also = 15 | # Exclude common false positives per 16 | # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion 17 | # Ref jaraco/skeleton#97 and jaraco/skeleton#135 18 | class .*\bProtocol\): 19 | if TYPE_CHECKING: 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.py] 11 | indent_style = space 12 | max_line_length = 88 13 | 14 | [*.{yml,yaml}] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.rst] 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | 4 | # jaraco/skeleton#34 5 | max-complexity = 10 6 | 7 | extend-ignore = 8 | # Black creates whitespace before colon 9 | E203 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.9 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --unsafe-fixes] 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | install: 4 | - path: . 5 | extra_requirements: 6 | - doc 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # required boilerplate readthedocs/readthedocs.org#10401 12 | build: 13 | os: ubuntu-lts-latest 14 | tools: 15 | python: latest 16 | # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 17 | jobs: 18 | post_checkout: 19 | - git fetch --unshallow || true 20 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/distutils/603b94ec2e7876294345e5bceac276496b369641/NEWS.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/distutils.svg 2 | :target: https://pypi.org/project/distutils 3 | 4 | .. image:: https://img.shields.io/pypi/pyversions/distutils.svg 5 | 6 | .. image:: https://github.com/pypa/distutils/actions/workflows/main.yml/badge.svg 7 | :target: https://github.com/pypa/distutils/actions?query=workflow%3A%22tests%22 8 | :alt: tests 9 | 10 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 11 | :target: https://github.com/astral-sh/ruff 12 | :alt: Ruff 13 | 14 | .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest 15 | .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest 16 | 17 | .. image:: https://img.shields.io/badge/skeleton-2025-informational 18 | :target: https://blog.jaraco.com/skeleton 19 | 20 | Python Module Distribution Utilities extracted from the Python Standard Library 21 | 22 | This package is unsupported except as integrated into and exposed by Setuptools. 23 | 24 | Integration 25 | ----------- 26 | 27 | Simply merge the changes directly into setuptools' repo. 28 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pathlib 4 | import platform 5 | import sys 6 | 7 | import path 8 | import pytest 9 | 10 | 11 | @pytest.fixture 12 | def save_env(): 13 | orig = os.environ.copy() 14 | try: 15 | yield 16 | finally: 17 | for key in set(os.environ) - set(orig): 18 | del os.environ[key] 19 | for key, value in orig.items(): 20 | if os.environ.get(key) != value: 21 | os.environ[key] = value 22 | 23 | 24 | @pytest.fixture 25 | def needs_zlib(): 26 | pytest.importorskip('zlib') 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | def log_everything(): 31 | """ 32 | For tests, set the level on the logger to log everything. 33 | """ 34 | logging.getLogger('distutils').setLevel(0) 35 | 36 | 37 | @pytest.fixture(autouse=True) 38 | def capture_log_at_info(caplog): 39 | """ 40 | By default, capture logs at INFO and greater. 41 | """ 42 | caplog.set_level(logging.INFO) 43 | 44 | 45 | def _save_cwd(): 46 | return path.Path('.') 47 | 48 | 49 | @pytest.fixture 50 | def distutils_managed_tempdir(request): 51 | from distutils.tests.compat import py39 as os_helper 52 | 53 | self = request.instance 54 | self.tempdirs = [] 55 | try: 56 | with _save_cwd(): 57 | yield 58 | finally: 59 | while self.tempdirs: 60 | tmpdir = self.tempdirs.pop() 61 | os_helper.rmtree(tmpdir) 62 | 63 | 64 | @pytest.fixture 65 | def save_argv(): 66 | orig = sys.argv[:] 67 | try: 68 | yield 69 | finally: 70 | sys.argv[:] = orig 71 | 72 | 73 | @pytest.fixture 74 | def save_cwd(): 75 | with _save_cwd(): 76 | yield 77 | 78 | 79 | @pytest.fixture 80 | def temp_cwd(tmp_path): 81 | with path.Path(tmp_path): 82 | yield 83 | 84 | 85 | # from pytest-dev/pytest#363 86 | @pytest.fixture(scope="session") 87 | def monkeysession(request): 88 | from _pytest.monkeypatch import MonkeyPatch 89 | 90 | mpatch = MonkeyPatch() 91 | yield mpatch 92 | mpatch.undo() 93 | 94 | 95 | @pytest.fixture(scope="module") 96 | def suppress_path_mangle(monkeysession): 97 | """ 98 | Disable the path mangling in CCompiler. Workaround for #169. 99 | """ 100 | from distutils import ccompiler 101 | 102 | monkeysession.setattr( 103 | ccompiler.CCompiler, '_make_relative', staticmethod(lambda x: x) 104 | ) 105 | 106 | 107 | def _set_home(monkeypatch, path): 108 | var = 'USERPROFILE' if platform.system() == 'Windows' else 'HOME' 109 | monkeypatch.setenv(var, str(path)) 110 | return path 111 | 112 | 113 | @pytest.fixture 114 | def temp_home(tmp_path, monkeypatch): 115 | return _set_home(monkeypatch, tmp_path) 116 | 117 | 118 | @pytest.fixture 119 | def fake_home(fs, monkeypatch): 120 | home = fs.create_dir('/fakehome') 121 | return _set_home(monkeypatch, pathlib.Path(home.path)) 122 | 123 | 124 | @pytest.fixture 125 | def disable_macos_customization(monkeypatch): 126 | from distutils import sysconfig 127 | 128 | monkeypatch.setattr(sysconfig, '_customize_macos', lambda: None) 129 | 130 | 131 | @pytest.fixture(autouse=True, scope="session") 132 | def monkey_patch_get_default_compiler(monkeysession): 133 | """ 134 | Monkey patch distutils get_default_compiler to allow overriding the 135 | default compiler. Mainly to test mingw32 with a MSVC Python. 136 | """ 137 | from distutils import ccompiler 138 | 139 | default_compiler = os.environ.get("DISTUTILS_TEST_DEFAULT_COMPILER") 140 | 141 | if default_compiler is None: 142 | return 143 | 144 | def patched_getter(*args, **kwargs): 145 | return default_compiler 146 | 147 | monkeysession.setattr(ccompiler, 'get_default_compiler', patched_getter) 148 | -------------------------------------------------------------------------------- /distutils/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | 4 | __version__, _, _ = sys.version.partition(' ') 5 | 6 | 7 | try: 8 | # Allow Debian and pkgsrc (only) to customize system 9 | # behavior. Ref pypa/distutils#2 and pypa/distutils#16. 10 | # This hook is deprecated and no other environments 11 | # should use it. 12 | importlib.import_module('_distutils_system_mod') 13 | except ImportError: 14 | pass 15 | -------------------------------------------------------------------------------- /distutils/_log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | log = logging.getLogger() 4 | -------------------------------------------------------------------------------- /distutils/_macos_compat.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | 4 | 5 | def bypass_compiler_fixup(cmd, args): 6 | return cmd 7 | 8 | 9 | if sys.platform == 'darwin': 10 | compiler_fixup = importlib.import_module('_osx_support').compiler_fixup 11 | else: 12 | compiler_fixup = bypass_compiler_fixup 13 | -------------------------------------------------------------------------------- /distutils/_modified.py: -------------------------------------------------------------------------------- 1 | """Timestamp comparison of files and groups of files.""" 2 | 3 | from __future__ import annotations 4 | 5 | import functools 6 | import os.path 7 | from collections.abc import Callable, Iterable 8 | from typing import Literal, TypeVar 9 | 10 | from jaraco.functools import splat 11 | 12 | from .compat.py39 import zip_strict 13 | from .errors import DistutilsFileError 14 | 15 | _SourcesT = TypeVar( 16 | "_SourcesT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]" 17 | ) 18 | _TargetsT = TypeVar( 19 | "_TargetsT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]" 20 | ) 21 | 22 | 23 | def _newer(source, target): 24 | return not os.path.exists(target) or ( 25 | os.path.getmtime(source) > os.path.getmtime(target) 26 | ) 27 | 28 | 29 | def newer( 30 | source: str | bytes | os.PathLike[str] | os.PathLike[bytes], 31 | target: str | bytes | os.PathLike[str] | os.PathLike[bytes], 32 | ) -> bool: 33 | """ 34 | Is source modified more recently than target. 35 | 36 | Returns True if 'source' is modified more recently than 37 | 'target' or if 'target' does not exist. 38 | 39 | Raises DistutilsFileError if 'source' does not exist. 40 | """ 41 | if not os.path.exists(source): 42 | raise DistutilsFileError(f"file {os.path.abspath(source)!r} does not exist") 43 | 44 | return _newer(source, target) 45 | 46 | 47 | def newer_pairwise( 48 | sources: Iterable[_SourcesT], 49 | targets: Iterable[_TargetsT], 50 | newer: Callable[[_SourcesT, _TargetsT], bool] = newer, 51 | ) -> tuple[list[_SourcesT], list[_TargetsT]]: 52 | """ 53 | Filter filenames where sources are newer than targets. 54 | 55 | Walk two filename iterables in parallel, testing if each source is newer 56 | than its corresponding target. Returns a pair of lists (sources, 57 | targets) where source is newer than target, according to the semantics 58 | of 'newer()'. 59 | """ 60 | newer_pairs = filter(splat(newer), zip_strict(sources, targets)) 61 | return tuple(map(list, zip(*newer_pairs))) or ([], []) 62 | 63 | 64 | def newer_group( 65 | sources: Iterable[str | bytes | os.PathLike[str] | os.PathLike[bytes]], 66 | target: str | bytes | os.PathLike[str] | os.PathLike[bytes], 67 | missing: Literal["error", "ignore", "newer"] = "error", 68 | ) -> bool: 69 | """ 70 | Is target out-of-date with respect to any file in sources. 71 | 72 | Return True if 'target' is out-of-date with respect to any file 73 | listed in 'sources'. In other words, if 'target' exists and is newer 74 | than every file in 'sources', return False; otherwise return True. 75 | ``missing`` controls how to handle a missing source file: 76 | 77 | - error (default): allow the ``stat()`` call to fail. 78 | - ignore: silently disregard any missing source files. 79 | - newer: treat missing source files as "target out of date". This 80 | mode is handy in "dry-run" mode: it will pretend to carry out 81 | commands that wouldn't work because inputs are missing, but 82 | that doesn't matter because dry-run won't run the commands. 83 | """ 84 | 85 | def missing_as_newer(source): 86 | return missing == 'newer' and not os.path.exists(source) 87 | 88 | ignored = os.path.exists if missing == 'ignore' else None 89 | return not os.path.exists(target) or any( 90 | missing_as_newer(source) or _newer(source, target) 91 | for source in filter(ignored, sources) 92 | ) 93 | 94 | 95 | newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group) 96 | -------------------------------------------------------------------------------- /distutils/_msvccompiler.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .compilers.C import msvc 4 | 5 | __all__ = ["MSVCCompiler"] 6 | 7 | MSVCCompiler = msvc.Compiler 8 | 9 | 10 | def __getattr__(name): 11 | if name == '_get_vc_env': 12 | warnings.warn( 13 | "_get_vc_env is private; find an alternative (pypa/distutils#340)" 14 | ) 15 | return msvc._get_vc_env 16 | raise AttributeError(name) 17 | -------------------------------------------------------------------------------- /distutils/ccompiler.py: -------------------------------------------------------------------------------- 1 | from .compat.numpy import ( # noqa: F401 2 | _default_compilers, 3 | compiler_class, 4 | ) 5 | from .compilers.C import base 6 | from .compilers.C.base import ( 7 | gen_lib_options, 8 | gen_preprocess_options, 9 | get_default_compiler, 10 | new_compiler, 11 | show_compilers, 12 | ) 13 | from .compilers.C.errors import CompileError, LinkError 14 | 15 | __all__ = [ 16 | 'CompileError', 17 | 'LinkError', 18 | 'gen_lib_options', 19 | 'gen_preprocess_options', 20 | 'get_default_compiler', 21 | 'new_compiler', 22 | 'show_compilers', 23 | ] 24 | 25 | 26 | CCompiler = base.Compiler 27 | -------------------------------------------------------------------------------- /distutils/command/__init__.py: -------------------------------------------------------------------------------- 1 | """distutils.command 2 | 3 | Package containing implementation of all the standard Distutils 4 | commands.""" 5 | 6 | __all__ = [ 7 | 'build', 8 | 'build_py', 9 | 'build_ext', 10 | 'build_clib', 11 | 'build_scripts', 12 | 'clean', 13 | 'install', 14 | 'install_lib', 15 | 'install_headers', 16 | 'install_scripts', 17 | 'install_data', 18 | 'sdist', 19 | 'bdist', 20 | 'bdist_dumb', 21 | 'bdist_rpm', 22 | 'check', 23 | ] 24 | -------------------------------------------------------------------------------- /distutils/command/_framework_compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backward compatibility for homebrew builds on macOS. 3 | """ 4 | 5 | import functools 6 | import os 7 | import subprocess 8 | import sys 9 | import sysconfig 10 | 11 | 12 | @functools.lru_cache 13 | def enabled(): 14 | """ 15 | Only enabled for Python 3.9 framework homebrew builds 16 | except ensurepip and venv. 17 | """ 18 | PY39 = (3, 9) < sys.version_info < (3, 10) 19 | framework = sys.platform == 'darwin' and sys._framework 20 | homebrew = "Cellar" in sysconfig.get_config_var('projectbase') 21 | venv = sys.prefix != sys.base_prefix 22 | ensurepip = os.environ.get("ENSUREPIP_OPTIONS") 23 | return PY39 and framework and homebrew and not venv and not ensurepip 24 | 25 | 26 | schemes = dict( 27 | osx_framework_library=dict( 28 | stdlib='{installed_base}/{platlibdir}/python{py_version_short}', 29 | platstdlib='{platbase}/{platlibdir}/python{py_version_short}', 30 | purelib='{homebrew_prefix}/lib/python{py_version_short}/site-packages', 31 | platlib='{homebrew_prefix}/{platlibdir}/python{py_version_short}/site-packages', 32 | include='{installed_base}/include/python{py_version_short}{abiflags}', 33 | platinclude='{installed_platbase}/include/python{py_version_short}{abiflags}', 34 | scripts='{homebrew_prefix}/bin', 35 | data='{homebrew_prefix}', 36 | ) 37 | ) 38 | 39 | 40 | @functools.lru_cache 41 | def vars(): 42 | if not enabled(): 43 | return {} 44 | homebrew_prefix = subprocess.check_output(['brew', '--prefix'], text=True).strip() 45 | return locals() 46 | 47 | 48 | def scheme(name): 49 | """ 50 | Override the selected scheme for posix_prefix. 51 | """ 52 | if not enabled() or not name.endswith('_prefix'): 53 | return name 54 | return 'osx_framework_library' 55 | -------------------------------------------------------------------------------- /distutils/command/bdist.py: -------------------------------------------------------------------------------- 1 | """distutils.command.bdist 2 | 3 | Implements the Distutils 'bdist' command (create a built [binary] 4 | distribution).""" 5 | 6 | from __future__ import annotations 7 | 8 | import os 9 | import warnings 10 | from collections.abc import Callable 11 | from typing import TYPE_CHECKING, ClassVar 12 | 13 | from ..core import Command 14 | from ..errors import DistutilsOptionError, DistutilsPlatformError 15 | from ..util import get_platform 16 | 17 | if TYPE_CHECKING: 18 | from typing_extensions import deprecated 19 | else: 20 | 21 | def deprecated(message): 22 | return lambda fn: fn 23 | 24 | 25 | def show_formats(): 26 | """Print list of available formats (arguments to "--format" option).""" 27 | from ..fancy_getopt import FancyGetopt 28 | 29 | formats = [ 30 | ("formats=" + format, None, bdist.format_commands[format][1]) 31 | for format in bdist.format_commands 32 | ] 33 | pretty_printer = FancyGetopt(formats) 34 | pretty_printer.print_help("List of available distribution formats:") 35 | 36 | 37 | class ListCompat(dict[str, tuple[str, str]]): 38 | # adapter to allow for Setuptools compatibility in format_commands 39 | @deprecated("format_commands is now a dict. append is deprecated.") 40 | def append(self, item: object) -> None: 41 | warnings.warn( 42 | "format_commands is now a dict. append is deprecated.", 43 | DeprecationWarning, 44 | stacklevel=2, 45 | ) 46 | 47 | 48 | class bdist(Command): 49 | description = "create a built (binary) distribution" 50 | 51 | user_options = [ 52 | ('bdist-base=', 'b', "temporary directory for creating built distributions"), 53 | ( 54 | 'plat-name=', 55 | 'p', 56 | "platform name to embed in generated filenames " 57 | f"[default: {get_platform()}]", 58 | ), 59 | ('formats=', None, "formats for distribution (comma-separated list)"), 60 | ( 61 | 'dist-dir=', 62 | 'd', 63 | "directory to put final built distributions in [default: dist]", 64 | ), 65 | ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), 66 | ( 67 | 'owner=', 68 | 'u', 69 | "Owner name used when creating a tar file [default: current user]", 70 | ), 71 | ( 72 | 'group=', 73 | 'g', 74 | "Group name used when creating a tar file [default: current group]", 75 | ), 76 | ] 77 | 78 | boolean_options: ClassVar[list[str]] = ['skip-build'] 79 | 80 | help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [ 81 | ('help-formats', None, "lists available distribution formats", show_formats), 82 | ] 83 | 84 | # The following commands do not take a format option from bdist 85 | no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',) 86 | 87 | # This won't do in reality: will need to distinguish RPM-ish Linux, 88 | # Debian-ish Linux, Solaris, FreeBSD, ..., Windows, Mac OS. 89 | default_format: ClassVar[dict[str, str]] = {'posix': 'gztar', 'nt': 'zip'} 90 | 91 | # Define commands in preferred order for the --help-formats option 92 | format_commands = ListCompat({ 93 | 'rpm': ('bdist_rpm', "RPM distribution"), 94 | 'gztar': ('bdist_dumb', "gzip'ed tar file"), 95 | 'bztar': ('bdist_dumb', "bzip2'ed tar file"), 96 | 'xztar': ('bdist_dumb', "xz'ed tar file"), 97 | 'ztar': ('bdist_dumb', "compressed tar file"), 98 | 'tar': ('bdist_dumb', "tar file"), 99 | 'zip': ('bdist_dumb', "ZIP file"), 100 | }) 101 | 102 | # for compatibility until consumers only reference format_commands 103 | format_command = format_commands 104 | 105 | def initialize_options(self): 106 | self.bdist_base = None 107 | self.plat_name = None 108 | self.formats = None 109 | self.dist_dir = None 110 | self.skip_build = False 111 | self.group = None 112 | self.owner = None 113 | 114 | def finalize_options(self) -> None: 115 | # have to finalize 'plat_name' before 'bdist_base' 116 | if self.plat_name is None: 117 | if self.skip_build: 118 | self.plat_name = get_platform() 119 | else: 120 | self.plat_name = self.get_finalized_command('build').plat_name 121 | 122 | # 'bdist_base' -- parent of per-built-distribution-format 123 | # temporary directories (eg. we'll probably have 124 | # "build/bdist./dumb", "build/bdist./rpm", etc.) 125 | if self.bdist_base is None: 126 | build_base = self.get_finalized_command('build').build_base 127 | self.bdist_base = os.path.join(build_base, 'bdist.' + self.plat_name) 128 | 129 | self.ensure_string_list('formats') 130 | if self.formats is None: 131 | try: 132 | self.formats = [self.default_format[os.name]] 133 | except KeyError: 134 | raise DistutilsPlatformError( 135 | "don't know how to create built distributions " 136 | f"on platform {os.name}" 137 | ) 138 | 139 | if self.dist_dir is None: 140 | self.dist_dir = "dist" 141 | 142 | def run(self) -> None: 143 | # Figure out which sub-commands we need to run. 144 | commands = [] 145 | for format in self.formats: 146 | try: 147 | commands.append(self.format_commands[format][0]) 148 | except KeyError: 149 | raise DistutilsOptionError(f"invalid format '{format}'") 150 | 151 | # Reinitialize and run each command. 152 | for i in range(len(self.formats)): 153 | cmd_name = commands[i] 154 | sub_cmd = self.reinitialize_command(cmd_name) 155 | if cmd_name not in self.no_format_option: 156 | sub_cmd.format = self.formats[i] 157 | 158 | # passing the owner and group names for tar archiving 159 | if cmd_name == 'bdist_dumb': 160 | sub_cmd.owner = self.owner 161 | sub_cmd.group = self.group 162 | 163 | # If we're going to need to run this command again, tell it to 164 | # keep its temporary files around so subsequent runs go faster. 165 | if cmd_name in commands[i + 1 :]: 166 | sub_cmd.keep_temp = True 167 | self.run_command(cmd_name) 168 | -------------------------------------------------------------------------------- /distutils/command/bdist_dumb.py: -------------------------------------------------------------------------------- 1 | """distutils.command.bdist_dumb 2 | 3 | Implements the Distutils 'bdist_dumb' command (create a "dumb" built 4 | distribution -- i.e., just an archive to be unpacked under $prefix or 5 | $exec_prefix).""" 6 | 7 | import os 8 | from distutils._log import log 9 | from typing import ClassVar 10 | 11 | from ..core import Command 12 | from ..dir_util import ensure_relative, remove_tree 13 | from ..errors import DistutilsPlatformError 14 | from ..sysconfig import get_python_version 15 | from ..util import get_platform 16 | 17 | 18 | class bdist_dumb(Command): 19 | description = "create a \"dumb\" built distribution" 20 | 21 | user_options = [ 22 | ('bdist-dir=', 'd', "temporary directory for creating the distribution"), 23 | ( 24 | 'plat-name=', 25 | 'p', 26 | "platform name to embed in generated filenames " 27 | f"[default: {get_platform()}]", 28 | ), 29 | ( 30 | 'format=', 31 | 'f', 32 | "archive format to create (tar, gztar, bztar, xztar, ztar, zip)", 33 | ), 34 | ( 35 | 'keep-temp', 36 | 'k', 37 | "keep the pseudo-installation tree around after creating the distribution archive", 38 | ), 39 | ('dist-dir=', 'd', "directory to put final built distributions in"), 40 | ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), 41 | ( 42 | 'relative', 43 | None, 44 | "build the archive using relative paths [default: false]", 45 | ), 46 | ( 47 | 'owner=', 48 | 'u', 49 | "Owner name used when creating a tar file [default: current user]", 50 | ), 51 | ( 52 | 'group=', 53 | 'g', 54 | "Group name used when creating a tar file [default: current group]", 55 | ), 56 | ] 57 | 58 | boolean_options: ClassVar[list[str]] = ['keep-temp', 'skip-build', 'relative'] 59 | 60 | default_format = {'posix': 'gztar', 'nt': 'zip'} 61 | 62 | def initialize_options(self): 63 | self.bdist_dir = None 64 | self.plat_name = None 65 | self.format = None 66 | self.keep_temp = False 67 | self.dist_dir = None 68 | self.skip_build = None 69 | self.relative = False 70 | self.owner = None 71 | self.group = None 72 | 73 | def finalize_options(self): 74 | if self.bdist_dir is None: 75 | bdist_base = self.get_finalized_command('bdist').bdist_base 76 | self.bdist_dir = os.path.join(bdist_base, 'dumb') 77 | 78 | if self.format is None: 79 | try: 80 | self.format = self.default_format[os.name] 81 | except KeyError: 82 | raise DistutilsPlatformError( 83 | "don't know how to create dumb built distributions " 84 | f"on platform {os.name}" 85 | ) 86 | 87 | self.set_undefined_options( 88 | 'bdist', 89 | ('dist_dir', 'dist_dir'), 90 | ('plat_name', 'plat_name'), 91 | ('skip_build', 'skip_build'), 92 | ) 93 | 94 | def run(self): 95 | if not self.skip_build: 96 | self.run_command('build') 97 | 98 | install = self.reinitialize_command('install', reinit_subcommands=True) 99 | install.root = self.bdist_dir 100 | install.skip_build = self.skip_build 101 | install.warn_dir = False 102 | 103 | log.info("installing to %s", self.bdist_dir) 104 | self.run_command('install') 105 | 106 | # And make an archive relative to the root of the 107 | # pseudo-installation tree. 108 | archive_basename = f"{self.distribution.get_fullname()}.{self.plat_name}" 109 | 110 | pseudoinstall_root = os.path.join(self.dist_dir, archive_basename) 111 | if not self.relative: 112 | archive_root = self.bdist_dir 113 | else: 114 | if self.distribution.has_ext_modules() and ( 115 | install.install_base != install.install_platbase 116 | ): 117 | raise DistutilsPlatformError( 118 | "can't make a dumb built distribution where " 119 | f"base and platbase are different ({install.install_base!r}, {install.install_platbase!r})" 120 | ) 121 | else: 122 | archive_root = os.path.join( 123 | self.bdist_dir, ensure_relative(install.install_base) 124 | ) 125 | 126 | # Make the archive 127 | filename = self.make_archive( 128 | pseudoinstall_root, 129 | self.format, 130 | root_dir=archive_root, 131 | owner=self.owner, 132 | group=self.group, 133 | ) 134 | if self.distribution.has_ext_modules(): 135 | pyversion = get_python_version() 136 | else: 137 | pyversion = 'any' 138 | self.distribution.dist_files.append(('bdist_dumb', pyversion, filename)) 139 | 140 | if not self.keep_temp: 141 | remove_tree(self.bdist_dir, dry_run=self.dry_run) 142 | -------------------------------------------------------------------------------- /distutils/command/build.py: -------------------------------------------------------------------------------- 1 | """distutils.command.build 2 | 3 | Implements the Distutils 'build' command.""" 4 | 5 | from __future__ import annotations 6 | 7 | import os 8 | import sys 9 | import sysconfig 10 | from collections.abc import Callable 11 | from typing import ClassVar 12 | 13 | from ..ccompiler import show_compilers 14 | from ..core import Command 15 | from ..errors import DistutilsOptionError 16 | from ..util import get_platform 17 | 18 | 19 | class build(Command): 20 | description = "build everything needed to install" 21 | 22 | user_options = [ 23 | ('build-base=', 'b', "base directory for build library"), 24 | ('build-purelib=', None, "build directory for platform-neutral distributions"), 25 | ('build-platlib=', None, "build directory for platform-specific distributions"), 26 | ( 27 | 'build-lib=', 28 | None, 29 | "build directory for all distribution (defaults to either build-purelib or build-platlib", 30 | ), 31 | ('build-scripts=', None, "build directory for scripts"), 32 | ('build-temp=', 't', "temporary build directory"), 33 | ( 34 | 'plat-name=', 35 | 'p', 36 | f"platform name to build for, if supported [default: {get_platform()}]", 37 | ), 38 | ('compiler=', 'c', "specify the compiler type"), 39 | ('parallel=', 'j', "number of parallel build jobs"), 40 | ('debug', 'g', "compile extensions and libraries with debugging information"), 41 | ('force', 'f', "forcibly build everything (ignore file timestamps)"), 42 | ('executable=', 'e', "specify final destination interpreter path (build.py)"), 43 | ] 44 | 45 | boolean_options: ClassVar[list[str]] = ['debug', 'force'] 46 | 47 | help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [ 48 | ('help-compiler', None, "list available compilers", show_compilers), 49 | ] 50 | 51 | def initialize_options(self): 52 | self.build_base = 'build' 53 | # these are decided only after 'build_base' has its final value 54 | # (unless overridden by the user or client) 55 | self.build_purelib = None 56 | self.build_platlib = None 57 | self.build_lib = None 58 | self.build_temp = None 59 | self.build_scripts = None 60 | self.compiler = None 61 | self.plat_name = None 62 | self.debug = None 63 | self.force = False 64 | self.executable = None 65 | self.parallel = None 66 | 67 | def finalize_options(self) -> None: # noqa: C901 68 | if self.plat_name is None: 69 | self.plat_name = get_platform() 70 | else: 71 | # plat-name only supported for windows (other platforms are 72 | # supported via ./configure flags, if at all). Avoid misleading 73 | # other platforms. 74 | if os.name != 'nt': 75 | raise DistutilsOptionError( 76 | "--plat-name only supported on Windows (try " 77 | "using './configure --help' on your platform)" 78 | ) 79 | 80 | plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}" 81 | 82 | # Python 3.13+ with --disable-gil shouldn't share build directories 83 | if sysconfig.get_config_var('Py_GIL_DISABLED'): 84 | plat_specifier += 't' 85 | 86 | # Make it so Python 2.x and Python 2.x with --with-pydebug don't 87 | # share the same build directories. Doing so confuses the build 88 | # process for C modules 89 | if hasattr(sys, 'gettotalrefcount'): 90 | plat_specifier += '-pydebug' 91 | 92 | # 'build_purelib' and 'build_platlib' just default to 'lib' and 93 | # 'lib.' under the base build directory. We only use one of 94 | # them for a given distribution, though -- 95 | if self.build_purelib is None: 96 | self.build_purelib = os.path.join(self.build_base, 'lib') 97 | if self.build_platlib is None: 98 | self.build_platlib = os.path.join(self.build_base, 'lib' + plat_specifier) 99 | 100 | # 'build_lib' is the actual directory that we will use for this 101 | # particular module distribution -- if user didn't supply it, pick 102 | # one of 'build_purelib' or 'build_platlib'. 103 | if self.build_lib is None: 104 | if self.distribution.has_ext_modules(): 105 | self.build_lib = self.build_platlib 106 | else: 107 | self.build_lib = self.build_purelib 108 | 109 | # 'build_temp' -- temporary directory for compiler turds, 110 | # "build/temp." 111 | if self.build_temp is None: 112 | self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier) 113 | if self.build_scripts is None: 114 | self.build_scripts = os.path.join( 115 | self.build_base, 116 | f'scripts-{sys.version_info.major}.{sys.version_info.minor}', 117 | ) 118 | 119 | if self.executable is None and sys.executable: 120 | self.executable = os.path.normpath(sys.executable) 121 | 122 | if isinstance(self.parallel, str): 123 | try: 124 | self.parallel = int(self.parallel) 125 | except ValueError: 126 | raise DistutilsOptionError("parallel should be an integer") 127 | 128 | def run(self) -> None: 129 | # Run all relevant sub-commands. This will be some subset of: 130 | # - build_py - pure Python modules 131 | # - build_clib - standalone C libraries 132 | # - build_ext - Python extensions 133 | # - build_scripts - (Python) scripts 134 | for cmd_name in self.get_sub_commands(): 135 | self.run_command(cmd_name) 136 | 137 | # -- Predicates for the sub-command list --------------------------- 138 | 139 | def has_pure_modules(self) -> bool: 140 | return self.distribution.has_pure_modules() 141 | 142 | def has_c_libraries(self) -> bool: 143 | return self.distribution.has_c_libraries() 144 | 145 | def has_ext_modules(self) -> bool: 146 | return self.distribution.has_ext_modules() 147 | 148 | def has_scripts(self) -> bool: 149 | return self.distribution.has_scripts() 150 | 151 | sub_commands = [ 152 | ('build_py', has_pure_modules), 153 | ('build_clib', has_c_libraries), 154 | ('build_ext', has_ext_modules), 155 | ('build_scripts', has_scripts), 156 | ] 157 | -------------------------------------------------------------------------------- /distutils/command/build_scripts.py: -------------------------------------------------------------------------------- 1 | """distutils.command.build_scripts 2 | 3 | Implements the Distutils 'build_scripts' command.""" 4 | 5 | import os 6 | import re 7 | import tokenize 8 | from distutils._log import log 9 | from stat import ST_MODE 10 | from typing import ClassVar 11 | 12 | from .._modified import newer 13 | from ..core import Command 14 | from ..util import convert_path 15 | 16 | shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$') 17 | """ 18 | Pattern matching a Python interpreter indicated in first line of a script. 19 | """ 20 | 21 | # for Setuptools compatibility 22 | first_line_re = shebang_pattern 23 | 24 | 25 | class build_scripts(Command): 26 | description = "\"build\" scripts (copy and fixup #! line)" 27 | 28 | user_options: ClassVar[list[tuple[str, str, str]]] = [ 29 | ('build-dir=', 'd', "directory to \"build\" (copy) to"), 30 | ('force', 'f', "forcibly build everything (ignore file timestamps"), 31 | ('executable=', 'e', "specify final destination interpreter path"), 32 | ] 33 | 34 | boolean_options: ClassVar[list[str]] = ['force'] 35 | 36 | def initialize_options(self): 37 | self.build_dir = None 38 | self.scripts = None 39 | self.force = None 40 | self.executable = None 41 | 42 | def finalize_options(self): 43 | self.set_undefined_options( 44 | 'build', 45 | ('build_scripts', 'build_dir'), 46 | ('force', 'force'), 47 | ('executable', 'executable'), 48 | ) 49 | self.scripts = self.distribution.scripts 50 | 51 | def get_source_files(self): 52 | return self.scripts 53 | 54 | def run(self): 55 | if not self.scripts: 56 | return 57 | self.copy_scripts() 58 | 59 | def copy_scripts(self): 60 | """ 61 | Copy each script listed in ``self.scripts``. 62 | 63 | If a script is marked as a Python script (first line matches 64 | 'shebang_pattern', i.e. starts with ``#!`` and contains 65 | "python"), then adjust in the copy the first line to refer to 66 | the current Python interpreter. 67 | """ 68 | self.mkpath(self.build_dir) 69 | outfiles = [] 70 | updated_files = [] 71 | for script in self.scripts: 72 | self._copy_script(script, outfiles, updated_files) 73 | 74 | self._change_modes(outfiles) 75 | 76 | return outfiles, updated_files 77 | 78 | def _copy_script(self, script, outfiles, updated_files): 79 | shebang_match = None 80 | script = convert_path(script) 81 | outfile = os.path.join(self.build_dir, os.path.basename(script)) 82 | outfiles.append(outfile) 83 | 84 | if not self.force and not newer(script, outfile): 85 | log.debug("not copying %s (up-to-date)", script) 86 | return 87 | 88 | # Always open the file, but ignore failures in dry-run mode 89 | # in order to attempt to copy directly. 90 | try: 91 | f = tokenize.open(script) 92 | except OSError: 93 | if not self.dry_run: 94 | raise 95 | f = None 96 | else: 97 | first_line = f.readline() 98 | if not first_line: 99 | self.warn(f"{script} is an empty file (skipping)") 100 | return 101 | 102 | shebang_match = shebang_pattern.match(first_line) 103 | 104 | updated_files.append(outfile) 105 | if shebang_match: 106 | log.info("copying and adjusting %s -> %s", script, self.build_dir) 107 | if not self.dry_run: 108 | post_interp = shebang_match.group(1) or '' 109 | shebang = "#!" + self.executable + post_interp + "\n" 110 | self._validate_shebang(shebang, f.encoding) 111 | with open(outfile, "w", encoding=f.encoding) as outf: 112 | outf.write(shebang) 113 | outf.writelines(f.readlines()) 114 | if f: 115 | f.close() 116 | else: 117 | if f: 118 | f.close() 119 | self.copy_file(script, outfile) 120 | 121 | def _change_modes(self, outfiles): 122 | if os.name != 'posix': 123 | return 124 | 125 | for file in outfiles: 126 | self._change_mode(file) 127 | 128 | def _change_mode(self, file): 129 | if self.dry_run: 130 | log.info("changing mode of %s", file) 131 | return 132 | 133 | oldmode = os.stat(file)[ST_MODE] & 0o7777 134 | newmode = (oldmode | 0o555) & 0o7777 135 | if newmode != oldmode: 136 | log.info("changing mode of %s from %o to %o", file, oldmode, newmode) 137 | os.chmod(file, newmode) 138 | 139 | @staticmethod 140 | def _validate_shebang(shebang, encoding): 141 | # Python parser starts to read a script using UTF-8 until 142 | # it gets a #coding:xxx cookie. The shebang has to be the 143 | # first line of a file, the #coding:xxx cookie cannot be 144 | # written before. So the shebang has to be encodable to 145 | # UTF-8. 146 | try: 147 | shebang.encode('utf-8') 148 | except UnicodeEncodeError: 149 | raise ValueError(f"The shebang ({shebang!r}) is not encodable to utf-8") 150 | 151 | # If the script is encoded to a custom encoding (use a 152 | # #coding:xxx cookie), the shebang has to be encodable to 153 | # the script encoding too. 154 | try: 155 | shebang.encode(encoding) 156 | except UnicodeEncodeError: 157 | raise ValueError( 158 | f"The shebang ({shebang!r}) is not encodable " 159 | f"to the script encoding ({encoding})" 160 | ) 161 | -------------------------------------------------------------------------------- /distutils/command/check.py: -------------------------------------------------------------------------------- 1 | """distutils.command.check 2 | 3 | Implements the Distutils 'check' command. 4 | """ 5 | 6 | import contextlib 7 | from typing import ClassVar 8 | 9 | from ..core import Command 10 | from ..errors import DistutilsSetupError 11 | 12 | with contextlib.suppress(ImportError): 13 | import docutils.frontend 14 | import docutils.nodes 15 | import docutils.parsers.rst 16 | import docutils.utils 17 | 18 | class SilentReporter(docutils.utils.Reporter): 19 | def __init__( 20 | self, 21 | source, 22 | report_level, 23 | halt_level, 24 | stream=None, 25 | debug=False, 26 | encoding='ascii', 27 | error_handler='replace', 28 | ): 29 | self.messages = [] 30 | super().__init__( 31 | source, report_level, halt_level, stream, debug, encoding, error_handler 32 | ) 33 | 34 | def system_message(self, level, message, *children, **kwargs): 35 | self.messages.append((level, message, children, kwargs)) 36 | return docutils.nodes.system_message( 37 | message, *children, level=level, type=self.levels[level], **kwargs 38 | ) 39 | 40 | 41 | class check(Command): 42 | """This command checks the meta-data of the package.""" 43 | 44 | description = "perform some checks on the package" 45 | user_options: ClassVar[list[tuple[str, str, str]]] = [ 46 | ('metadata', 'm', 'Verify meta-data'), 47 | ( 48 | 'restructuredtext', 49 | 'r', 50 | 'Checks if long string meta-data syntax are reStructuredText-compliant', 51 | ), 52 | ('strict', 's', 'Will exit with an error if a check fails'), 53 | ] 54 | 55 | boolean_options: ClassVar[list[str]] = ['metadata', 'restructuredtext', 'strict'] 56 | 57 | def initialize_options(self): 58 | """Sets default values for options.""" 59 | self.restructuredtext = False 60 | self.metadata = 1 61 | self.strict = False 62 | self._warnings = 0 63 | 64 | def finalize_options(self): 65 | pass 66 | 67 | def warn(self, msg): 68 | """Counts the number of warnings that occurs.""" 69 | self._warnings += 1 70 | return Command.warn(self, msg) 71 | 72 | def run(self): 73 | """Runs the command.""" 74 | # perform the various tests 75 | if self.metadata: 76 | self.check_metadata() 77 | if self.restructuredtext: 78 | if 'docutils' in globals(): 79 | try: 80 | self.check_restructuredtext() 81 | except TypeError as exc: 82 | raise DistutilsSetupError(str(exc)) 83 | elif self.strict: 84 | raise DistutilsSetupError('The docutils package is needed.') 85 | 86 | # let's raise an error in strict mode, if we have at least 87 | # one warning 88 | if self.strict and self._warnings > 0: 89 | raise DistutilsSetupError('Please correct your package.') 90 | 91 | def check_metadata(self): 92 | """Ensures that all required elements of meta-data are supplied. 93 | 94 | Required fields: 95 | name, version 96 | 97 | Warns if any are missing. 98 | """ 99 | metadata = self.distribution.metadata 100 | 101 | missing = [ 102 | attr for attr in ('name', 'version') if not getattr(metadata, attr, None) 103 | ] 104 | 105 | if missing: 106 | self.warn("missing required meta-data: {}".format(', '.join(missing))) 107 | 108 | def check_restructuredtext(self): 109 | """Checks if the long string fields are reST-compliant.""" 110 | data = self.distribution.get_long_description() 111 | for warning in self._check_rst_data(data): 112 | line = warning[-1].get('line') 113 | if line is None: 114 | warning = warning[1] 115 | else: 116 | warning = f'{warning[1]} (line {line})' 117 | self.warn(warning) 118 | 119 | def _check_rst_data(self, data): 120 | """Returns warnings when the provided data doesn't compile.""" 121 | # the include and csv_table directives need this to be a path 122 | source_path = self.distribution.script_name or 'setup.py' 123 | parser = docutils.parsers.rst.Parser() 124 | settings = docutils.frontend.OptionParser( 125 | components=(docutils.parsers.rst.Parser,) 126 | ).get_default_values() 127 | settings.tab_width = 4 128 | settings.pep_references = None 129 | settings.rfc_references = None 130 | reporter = SilentReporter( 131 | source_path, 132 | settings.report_level, 133 | settings.halt_level, 134 | stream=settings.warning_stream, 135 | debug=settings.debug, 136 | encoding=settings.error_encoding, 137 | error_handler=settings.error_encoding_error_handler, 138 | ) 139 | 140 | document = docutils.nodes.document(settings, reporter, source=source_path) 141 | document.note_source(source_path, -1) 142 | try: 143 | parser.parse(data, document) 144 | except (AttributeError, TypeError) as e: 145 | reporter.messages.append(( 146 | -1, 147 | f'Could not finish the parsing: {e}.', 148 | '', 149 | {}, 150 | )) 151 | 152 | return reporter.messages 153 | -------------------------------------------------------------------------------- /distutils/command/clean.py: -------------------------------------------------------------------------------- 1 | """distutils.command.clean 2 | 3 | Implements the Distutils 'clean' command.""" 4 | 5 | # contributed by Bastian Kleineidam , added 2000-03-18 6 | 7 | import os 8 | from distutils._log import log 9 | from typing import ClassVar 10 | 11 | from ..core import Command 12 | from ..dir_util import remove_tree 13 | 14 | 15 | class clean(Command): 16 | description = "clean up temporary files from 'build' command" 17 | user_options = [ 18 | ('build-base=', 'b', "base build directory [default: 'build.build-base']"), 19 | ( 20 | 'build-lib=', 21 | None, 22 | "build directory for all modules [default: 'build.build-lib']", 23 | ), 24 | ('build-temp=', 't', "temporary build directory [default: 'build.build-temp']"), 25 | ( 26 | 'build-scripts=', 27 | None, 28 | "build directory for scripts [default: 'build.build-scripts']", 29 | ), 30 | ('bdist-base=', None, "temporary directory for built distributions"), 31 | ('all', 'a', "remove all build output, not just temporary by-products"), 32 | ] 33 | 34 | boolean_options: ClassVar[list[str]] = ['all'] 35 | 36 | def initialize_options(self): 37 | self.build_base = None 38 | self.build_lib = None 39 | self.build_temp = None 40 | self.build_scripts = None 41 | self.bdist_base = None 42 | self.all = None 43 | 44 | def finalize_options(self): 45 | self.set_undefined_options( 46 | 'build', 47 | ('build_base', 'build_base'), 48 | ('build_lib', 'build_lib'), 49 | ('build_scripts', 'build_scripts'), 50 | ('build_temp', 'build_temp'), 51 | ) 52 | self.set_undefined_options('bdist', ('bdist_base', 'bdist_base')) 53 | 54 | def run(self): 55 | # remove the build/temp. directory (unless it's already 56 | # gone) 57 | if os.path.exists(self.build_temp): 58 | remove_tree(self.build_temp, dry_run=self.dry_run) 59 | else: 60 | log.debug("'%s' does not exist -- can't clean it", self.build_temp) 61 | 62 | if self.all: 63 | # remove build directories 64 | for directory in (self.build_lib, self.bdist_base, self.build_scripts): 65 | if os.path.exists(directory): 66 | remove_tree(directory, dry_run=self.dry_run) 67 | else: 68 | log.warning("'%s' does not exist -- can't clean it", directory) 69 | 70 | # just for the heck of it, try to remove the base build directory: 71 | # we might have emptied it right now, but if not we don't care 72 | if not self.dry_run: 73 | try: 74 | os.rmdir(self.build_base) 75 | log.info("removing '%s'", self.build_base) 76 | except OSError: 77 | pass 78 | -------------------------------------------------------------------------------- /distutils/command/command_template: -------------------------------------------------------------------------------- 1 | """distutils.command.x 2 | 3 | Implements the Distutils 'x' command. 4 | """ 5 | 6 | # created 2000/mm/dd, John Doe 7 | 8 | __revision__ = "$Id$" 9 | 10 | from distutils.core import Command 11 | from typing import ClassVar 12 | 13 | 14 | class x(Command): 15 | # Brief (40-50 characters) description of the command 16 | description = "" 17 | 18 | # List of option tuples: long name, short name (None if no short 19 | # name), and help string. 20 | user_options: ClassVar[list[tuple[str, str, str]]] = [ 21 | ('', '', ""), 22 | ] 23 | 24 | def initialize_options(self): 25 | self. = None 26 | self. = None 27 | self. = None 28 | 29 | def finalize_options(self): 30 | if self.x is None: 31 | self.x = 32 | 33 | def run(self): 34 | -------------------------------------------------------------------------------- /distutils/command/install_data.py: -------------------------------------------------------------------------------- 1 | """distutils.command.install_data 2 | 3 | Implements the Distutils 'install_data' command, for installing 4 | platform-independent data files.""" 5 | 6 | # contributed by Bastian Kleineidam 7 | 8 | from __future__ import annotations 9 | 10 | import functools 11 | import os 12 | from collections.abc import Iterable 13 | from typing import ClassVar 14 | 15 | from ..core import Command 16 | from ..util import change_root, convert_path 17 | 18 | 19 | class install_data(Command): 20 | description = "install data files" 21 | 22 | user_options = [ 23 | ( 24 | 'install-dir=', 25 | 'd', 26 | "base directory for installing data files [default: installation base dir]", 27 | ), 28 | ('root=', None, "install everything relative to this alternate root directory"), 29 | ('force', 'f', "force installation (overwrite existing files)"), 30 | ] 31 | 32 | boolean_options: ClassVar[list[str]] = ['force'] 33 | 34 | def initialize_options(self): 35 | self.install_dir = None 36 | self.outfiles = [] 37 | self.root = None 38 | self.force = False 39 | self.data_files = self.distribution.data_files 40 | self.warn_dir = True 41 | 42 | def finalize_options(self) -> None: 43 | self.set_undefined_options( 44 | 'install', 45 | ('install_data', 'install_dir'), 46 | ('root', 'root'), 47 | ('force', 'force'), 48 | ) 49 | 50 | def run(self) -> None: 51 | self.mkpath(self.install_dir) 52 | for f in self.data_files: 53 | self._copy(f) 54 | 55 | @functools.singledispatchmethod 56 | def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]): 57 | # it's a tuple with path to install to and a list of files 58 | dir = convert_path(f[0]) 59 | if not os.path.isabs(dir): 60 | dir = os.path.join(self.install_dir, dir) 61 | elif self.root: 62 | dir = change_root(self.root, dir) 63 | self.mkpath(dir) 64 | 65 | if f[1] == []: 66 | # If there are no files listed, the user must be 67 | # trying to create an empty directory, so add the 68 | # directory to the list of output files. 69 | self.outfiles.append(dir) 70 | else: 71 | # Copy files, adding them to the list of output files. 72 | for data in f[1]: 73 | data = convert_path(data) 74 | (out, _) = self.copy_file(data, dir) 75 | self.outfiles.append(out) 76 | 77 | @_copy.register(str) 78 | @_copy.register(os.PathLike) 79 | def _(self, f: str | os.PathLike): 80 | # it's a simple file, so copy it 81 | f = convert_path(f) 82 | if self.warn_dir: 83 | self.warn( 84 | "setup script did not provide a directory for " 85 | f"'{f}' -- installing right in '{self.install_dir}'" 86 | ) 87 | (out, _) = self.copy_file(f, self.install_dir) 88 | self.outfiles.append(out) 89 | 90 | def get_inputs(self): 91 | return self.data_files or [] 92 | 93 | def get_outputs(self): 94 | return self.outfiles 95 | -------------------------------------------------------------------------------- /distutils/command/install_egg_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | distutils.command.install_egg_info 3 | 4 | Implements the Distutils 'install_egg_info' command, for installing 5 | a package's PKG-INFO metadata. 6 | """ 7 | 8 | import os 9 | import re 10 | import sys 11 | from typing import ClassVar 12 | 13 | from .. import dir_util 14 | from .._log import log 15 | from ..cmd import Command 16 | 17 | 18 | class install_egg_info(Command): 19 | """Install an .egg-info file for the package""" 20 | 21 | description = "Install package's PKG-INFO metadata as an .egg-info file" 22 | user_options: ClassVar[list[tuple[str, str, str]]] = [ 23 | ('install-dir=', 'd', "directory to install to"), 24 | ] 25 | 26 | def initialize_options(self): 27 | self.install_dir = None 28 | 29 | @property 30 | def basename(self): 31 | """ 32 | Allow basename to be overridden by child class. 33 | Ref pypa/distutils#2. 34 | """ 35 | name = to_filename(safe_name(self.distribution.get_name())) 36 | version = to_filename(safe_version(self.distribution.get_version())) 37 | return f"{name}-{version}-py{sys.version_info.major}.{sys.version_info.minor}.egg-info" 38 | 39 | def finalize_options(self): 40 | self.set_undefined_options('install_lib', ('install_dir', 'install_dir')) 41 | self.target = os.path.join(self.install_dir, self.basename) 42 | self.outputs = [self.target] 43 | 44 | def run(self): 45 | target = self.target 46 | if os.path.isdir(target) and not os.path.islink(target): 47 | dir_util.remove_tree(target, dry_run=self.dry_run) 48 | elif os.path.exists(target): 49 | self.execute(os.unlink, (self.target,), "Removing " + target) 50 | elif not os.path.isdir(self.install_dir): 51 | self.execute( 52 | os.makedirs, (self.install_dir,), "Creating " + self.install_dir 53 | ) 54 | log.info("Writing %s", target) 55 | if not self.dry_run: 56 | with open(target, 'w', encoding='UTF-8') as f: 57 | self.distribution.metadata.write_pkg_file(f) 58 | 59 | def get_outputs(self): 60 | return self.outputs 61 | 62 | 63 | # The following routines were originally copied from setuptools' pkg_resources 64 | # module and intended to be replaced by stdlib versions. They're now just legacy 65 | # cruft. 66 | 67 | 68 | def safe_name(name): 69 | """Convert an arbitrary string to a standard distribution name 70 | 71 | Any runs of non-alphanumeric/. characters are replaced with a single '-'. 72 | """ 73 | return re.sub('[^A-Za-z0-9.]+', '-', name) 74 | 75 | 76 | def safe_version(version): 77 | """Convert an arbitrary string to a standard version string 78 | 79 | Spaces become dots, and all other non-alphanumeric characters become 80 | dashes, with runs of multiple dashes condensed to a single dash. 81 | """ 82 | version = version.replace(' ', '.') 83 | return re.sub('[^A-Za-z0-9.]+', '-', version) 84 | 85 | 86 | def to_filename(name): 87 | """Convert a project or version name to its filename-escaped form 88 | 89 | Any '-' characters are currently replaced with '_'. 90 | """ 91 | return name.replace('-', '_') 92 | -------------------------------------------------------------------------------- /distutils/command/install_headers.py: -------------------------------------------------------------------------------- 1 | """distutils.command.install_headers 2 | 3 | Implements the Distutils 'install_headers' command, to install C/C++ header 4 | files to the Python include directory.""" 5 | 6 | from typing import ClassVar 7 | 8 | from ..core import Command 9 | 10 | 11 | # XXX force is never used 12 | class install_headers(Command): 13 | description = "install C/C++ header files" 14 | 15 | user_options: ClassVar[list[tuple[str, str, str]]] = [ 16 | ('install-dir=', 'd', "directory to install header files to"), 17 | ('force', 'f', "force installation (overwrite existing files)"), 18 | ] 19 | 20 | boolean_options: ClassVar[list[str]] = ['force'] 21 | 22 | def initialize_options(self): 23 | self.install_dir = None 24 | self.force = False 25 | self.outfiles = [] 26 | 27 | def finalize_options(self): 28 | self.set_undefined_options( 29 | 'install', ('install_headers', 'install_dir'), ('force', 'force') 30 | ) 31 | 32 | def run(self): 33 | headers = self.distribution.headers 34 | if not headers: 35 | return 36 | 37 | self.mkpath(self.install_dir) 38 | for header in headers: 39 | (out, _) = self.copy_file(header, self.install_dir) 40 | self.outfiles.append(out) 41 | 42 | def get_inputs(self): 43 | return self.distribution.headers or [] 44 | 45 | def get_outputs(self): 46 | return self.outfiles 47 | -------------------------------------------------------------------------------- /distutils/command/install_scripts.py: -------------------------------------------------------------------------------- 1 | """distutils.command.install_scripts 2 | 3 | Implements the Distutils 'install_scripts' command, for installing 4 | Python scripts.""" 5 | 6 | # contributed by Bastian Kleineidam 7 | 8 | import os 9 | from distutils._log import log 10 | from stat import ST_MODE 11 | from typing import ClassVar 12 | 13 | from ..core import Command 14 | 15 | 16 | class install_scripts(Command): 17 | description = "install scripts (Python or otherwise)" 18 | 19 | user_options = [ 20 | ('install-dir=', 'd', "directory to install scripts to"), 21 | ('build-dir=', 'b', "build directory (where to install from)"), 22 | ('force', 'f', "force installation (overwrite existing files)"), 23 | ('skip-build', None, "skip the build steps"), 24 | ] 25 | 26 | boolean_options: ClassVar[list[str]] = ['force', 'skip-build'] 27 | 28 | def initialize_options(self): 29 | self.install_dir = None 30 | self.force = False 31 | self.build_dir = None 32 | self.skip_build = None 33 | 34 | def finalize_options(self) -> None: 35 | self.set_undefined_options('build', ('build_scripts', 'build_dir')) 36 | self.set_undefined_options( 37 | 'install', 38 | ('install_scripts', 'install_dir'), 39 | ('force', 'force'), 40 | ('skip_build', 'skip_build'), 41 | ) 42 | 43 | def run(self) -> None: 44 | if not self.skip_build: 45 | self.run_command('build_scripts') 46 | self.outfiles = self.copy_tree(self.build_dir, self.install_dir) 47 | if os.name == 'posix': 48 | # Set the executable bits (owner, group, and world) on 49 | # all the scripts we just installed. 50 | for file in self.get_outputs(): 51 | if self.dry_run: 52 | log.info("changing mode of %s", file) 53 | else: 54 | mode = ((os.stat(file)[ST_MODE]) | 0o555) & 0o7777 55 | log.info("changing mode of %s to %o", file, mode) 56 | os.chmod(file, mode) 57 | 58 | def get_inputs(self): 59 | return self.distribution.scripts or [] 60 | 61 | def get_outputs(self): 62 | return self.outfiles or [] 63 | -------------------------------------------------------------------------------- /distutils/compat/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable 4 | from typing import TypeVar 5 | 6 | _IterableT = TypeVar("_IterableT", bound="Iterable[str]") 7 | 8 | 9 | def consolidate_linker_args(args: _IterableT) -> _IterableT | str: 10 | """ 11 | Ensure the return value is a string for backward compatibility. 12 | 13 | Retain until at least 2025-04-31. See pypa/distutils#246 14 | """ 15 | 16 | if not all(arg.startswith('-Wl,') for arg in args): 17 | return args 18 | return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args) 19 | -------------------------------------------------------------------------------- /distutils/compat/numpy.py: -------------------------------------------------------------------------------- 1 | # required for older numpy versions on Pythons prior to 3.12; see pypa/setuptools#4876 2 | from ..compilers.C.base import _default_compilers, compiler_class # noqa: F401 3 | -------------------------------------------------------------------------------- /distutils/compat/py39.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import itertools 3 | import platform 4 | import sys 5 | 6 | 7 | def add_ext_suffix_39(vars): 8 | """ 9 | Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130 10 | """ 11 | import _imp 12 | 13 | ext_suffix = _imp.extension_suffixes()[0] 14 | vars.update( 15 | EXT_SUFFIX=ext_suffix, 16 | # sysconfig sets SO to match EXT_SUFFIX, so maintain 17 | # that expectation. 18 | # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673 19 | SO=ext_suffix, 20 | ) 21 | 22 | 23 | needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows' 24 | add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None 25 | 26 | 27 | # from more_itertools 28 | class UnequalIterablesError(ValueError): 29 | def __init__(self, details=None): 30 | msg = 'Iterables have different lengths' 31 | if details is not None: 32 | msg += (': index 0 has length {}; index {} has length {}').format(*details) 33 | 34 | super().__init__(msg) 35 | 36 | 37 | # from more_itertools 38 | def _zip_equal_generator(iterables): 39 | _marker = object() 40 | for combo in itertools.zip_longest(*iterables, fillvalue=_marker): 41 | for val in combo: 42 | if val is _marker: 43 | raise UnequalIterablesError() 44 | yield combo 45 | 46 | 47 | # from more_itertools 48 | def _zip_equal(*iterables): 49 | # Check whether the iterables are all the same size. 50 | try: 51 | first_size = len(iterables[0]) 52 | for i, it in enumerate(iterables[1:], 1): 53 | size = len(it) 54 | if size != first_size: 55 | raise UnequalIterablesError(details=(first_size, i, size)) 56 | # All sizes are equal, we can use the built-in zip. 57 | return zip(*iterables) 58 | # If any one of the iterables didn't have a length, start reading 59 | # them until one runs out. 60 | except TypeError: 61 | return _zip_equal_generator(iterables) 62 | 63 | 64 | zip_strict = ( 65 | _zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True) 66 | ) 67 | -------------------------------------------------------------------------------- /distutils/compilers/C/errors.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | """Some compile/link operation failed.""" 3 | 4 | 5 | class PreprocessError(Error): 6 | """Failure to preprocess one or more C/C++ files.""" 7 | 8 | 9 | class CompileError(Error): 10 | """Failure to compile one or more C/C++ source files.""" 11 | 12 | 13 | class LibError(Error): 14 | """Failure to create a static library from one or more C/C++ object 15 | files.""" 16 | 17 | 18 | class LinkError(Error): 19 | """Failure to link one or more C/C++ object files into an executable 20 | or shared library file.""" 21 | 22 | 23 | class UnknownFileType(Error): 24 | """Attempt to process an unknown file type.""" 25 | -------------------------------------------------------------------------------- /distutils/compilers/C/py.typed: -------------------------------------------------------------------------------- 1 | # Ensure that checkers who see this as a separate namespace package still understand it's typed. 2 | -------------------------------------------------------------------------------- /distutils/compilers/C/tests/test_base.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sysconfig 3 | import textwrap 4 | 5 | import pytest 6 | 7 | from .. import base 8 | 9 | pytestmark = pytest.mark.usefixtures('suppress_path_mangle') 10 | 11 | 12 | @pytest.fixture 13 | def c_file(tmp_path): 14 | c_file = tmp_path / 'foo.c' 15 | gen_headers = ('Python.h',) 16 | is_windows = platform.system() == "Windows" 17 | plat_headers = ('windows.h',) * is_windows 18 | all_headers = gen_headers + plat_headers 19 | headers = '\n'.join(f'#include <{header}>\n' for header in all_headers) 20 | payload = ( 21 | textwrap.dedent( 22 | """ 23 | #headers 24 | void PyInit_foo(void) {} 25 | """ 26 | ) 27 | .lstrip() 28 | .replace('#headers', headers) 29 | ) 30 | c_file.write_text(payload, encoding='utf-8') 31 | return c_file 32 | 33 | 34 | def test_set_include_dirs(c_file): 35 | """ 36 | Extensions should build even if set_include_dirs is invoked. 37 | In particular, compiler-specific paths should not be overridden. 38 | """ 39 | compiler = base.new_compiler() 40 | python = sysconfig.get_paths()['include'] 41 | compiler.set_include_dirs([python]) 42 | compiler.compile([c_file]) 43 | 44 | # do it again, setting include dirs after any initialization 45 | compiler.set_include_dirs([python]) 46 | compiler.compile([c_file]) 47 | 48 | 49 | def test_has_function_prototype(): 50 | # Issue https://github.com/pypa/setuptools/issues/3648 51 | # Test prototype-generating behavior. 52 | 53 | compiler = base.new_compiler() 54 | 55 | # Every C implementation should have these. 56 | assert compiler.has_function('abort') 57 | assert compiler.has_function('exit') 58 | with pytest.deprecated_call(match='includes is deprecated'): 59 | # abort() is a valid expression with the prototype. 60 | assert compiler.has_function('abort', includes=['stdlib.h']) 61 | with pytest.deprecated_call(match='includes is deprecated'): 62 | # But exit() is not valid with the actual prototype in scope. 63 | assert not compiler.has_function('exit', includes=['stdlib.h']) 64 | # And setuptools_does_not_exist is not declared or defined at all. 65 | assert not compiler.has_function('setuptools_does_not_exist') 66 | with pytest.deprecated_call(match='includes is deprecated'): 67 | assert not compiler.has_function( 68 | 'setuptools_does_not_exist', includes=['stdio.h'] 69 | ) 70 | 71 | 72 | def test_include_dirs_after_multiple_compile_calls(c_file): 73 | """ 74 | Calling compile multiple times should not change the include dirs 75 | (regression test for setuptools issue #3591). 76 | """ 77 | compiler = base.new_compiler() 78 | python = sysconfig.get_paths()['include'] 79 | compiler.set_include_dirs([python]) 80 | compiler.compile([c_file]) 81 | assert compiler.include_dirs == [python] 82 | compiler.compile([c_file]) 83 | assert compiler.include_dirs == [python] 84 | -------------------------------------------------------------------------------- /distutils/compilers/C/tests/test_cygwin.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.cygwinccompiler.""" 2 | 3 | import os 4 | import sys 5 | from distutils import sysconfig 6 | from distutils.tests import support 7 | 8 | import pytest 9 | 10 | from .. import cygwin 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def stuff(request, monkeypatch, distutils_managed_tempdir): 15 | self = request.instance 16 | self.python_h = os.path.join(self.mkdtemp(), 'python.h') 17 | monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename) 18 | monkeypatch.setattr(sys, 'version', sys.version) 19 | 20 | 21 | class TestCygwinCCompiler(support.TempdirManager): 22 | def _get_config_h_filename(self): 23 | return self.python_h 24 | 25 | @pytest.mark.skipif('sys.platform != "cygwin"') 26 | @pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")') 27 | def test_find_library_file(self): 28 | from distutils.cygwinccompiler import CygwinCCompiler 29 | 30 | compiler = CygwinCCompiler() 31 | link_name = "bash" 32 | linkable_file = compiler.find_library_file(["/usr/lib"], link_name) 33 | assert linkable_file is not None 34 | assert os.path.exists(linkable_file) 35 | assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a" 36 | 37 | @pytest.mark.skipif('sys.platform != "cygwin"') 38 | def test_runtime_library_dir_option(self): 39 | from distutils.cygwinccompiler import CygwinCCompiler 40 | 41 | compiler = CygwinCCompiler() 42 | assert compiler.runtime_library_dir_option('/foo') == [] 43 | 44 | def test_check_config_h(self): 45 | # check_config_h looks for "GCC" in sys.version first 46 | # returns CONFIG_H_OK if found 47 | sys.version = ( 48 | '2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC ' 49 | '4.0.1 (Apple Computer, Inc. build 5370)]' 50 | ) 51 | 52 | assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK 53 | 54 | # then it tries to see if it can find "__GNUC__" in pyconfig.h 55 | sys.version = 'something without the *CC word' 56 | 57 | # if the file doesn't exist it returns CONFIG_H_UNCERTAIN 58 | assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_UNCERTAIN 59 | 60 | # if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK 61 | self.write_file(self.python_h, 'xxx') 62 | assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_NOTOK 63 | 64 | # and CONFIG_H_OK if __GNUC__ is found 65 | self.write_file(self.python_h, 'xxx __GNUC__ xxx') 66 | assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK 67 | 68 | def test_get_msvcr(self): 69 | assert cygwin.get_msvcr() == [] 70 | 71 | @pytest.mark.skipif('sys.platform != "cygwin"') 72 | def test_dll_libraries_not_none(self): 73 | from distutils.cygwinccompiler import CygwinCCompiler 74 | 75 | compiler = CygwinCCompiler() 76 | assert compiler.dll_libraries is not None 77 | -------------------------------------------------------------------------------- /distutils/compilers/C/tests/test_mingw.py: -------------------------------------------------------------------------------- 1 | from distutils import sysconfig 2 | from distutils.errors import DistutilsPlatformError 3 | from distutils.util import is_mingw, split_quoted 4 | 5 | import pytest 6 | 7 | from .. import cygwin, errors 8 | 9 | 10 | class TestMinGW32Compiler: 11 | @pytest.mark.skipif(not is_mingw(), reason='not on mingw') 12 | def test_compiler_type(self): 13 | compiler = cygwin.MinGW32Compiler() 14 | assert compiler.compiler_type == 'mingw32' 15 | 16 | @pytest.mark.skipif(not is_mingw(), reason='not on mingw') 17 | def test_set_executables(self, monkeypatch): 18 | monkeypatch.setenv('CC', 'cc') 19 | monkeypatch.setenv('CXX', 'c++') 20 | 21 | compiler = cygwin.MinGW32Compiler() 22 | 23 | assert compiler.compiler == split_quoted('cc -O -Wall') 24 | assert compiler.compiler_so == split_quoted('cc -shared -O -Wall') 25 | assert compiler.compiler_cxx == split_quoted('c++ -O -Wall') 26 | assert compiler.linker_exe == split_quoted('cc') 27 | assert compiler.linker_so == split_quoted('cc -shared') 28 | 29 | @pytest.mark.skipif(not is_mingw(), reason='not on mingw') 30 | def test_runtime_library_dir_option(self): 31 | compiler = cygwin.MinGW32Compiler() 32 | with pytest.raises(DistutilsPlatformError): 33 | compiler.runtime_library_dir_option('/usr/lib') 34 | 35 | @pytest.mark.skipif(not is_mingw(), reason='not on mingw') 36 | def test_cygwincc_error(self, monkeypatch): 37 | monkeypatch.setattr(cygwin, 'is_cygwincc', lambda _: True) 38 | 39 | with pytest.raises(errors.Error): 40 | cygwin.MinGW32Compiler() 41 | 42 | @pytest.mark.skipif('sys.platform == "cygwin"') 43 | def test_customize_compiler_with_msvc_python(self): 44 | # In case we have an MSVC Python build, but still want to use 45 | # MinGW32Compiler, then customize_compiler() shouldn't fail at least. 46 | # https://github.com/pypa/setuptools/issues/4456 47 | compiler = cygwin.MinGW32Compiler() 48 | sysconfig.customize_compiler(compiler) 49 | -------------------------------------------------------------------------------- /distutils/compilers/C/tests/test_msvc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import sysconfig 4 | import threading 5 | import unittest.mock as mock 6 | from distutils.errors import DistutilsPlatformError 7 | from distutils.tests import support 8 | from distutils.util import get_platform 9 | 10 | import pytest 11 | 12 | from .. import msvc 13 | 14 | needs_winreg = pytest.mark.skipif('not hasattr(msvc, "winreg")') 15 | 16 | 17 | class Testmsvccompiler(support.TempdirManager): 18 | def test_no_compiler(self, monkeypatch): 19 | # makes sure query_vcvarsall raises 20 | # a DistutilsPlatformError if the compiler 21 | # is not found 22 | def _find_vcvarsall(plat_spec): 23 | return None, None 24 | 25 | monkeypatch.setattr(msvc, '_find_vcvarsall', _find_vcvarsall) 26 | 27 | with pytest.raises(DistutilsPlatformError): 28 | msvc._get_vc_env( 29 | 'wont find this version', 30 | ) 31 | 32 | @pytest.mark.skipif( 33 | not sysconfig.get_platform().startswith("win"), 34 | reason="Only run test for non-mingw Windows platforms", 35 | ) 36 | @pytest.mark.parametrize( 37 | "plat_name, expected", 38 | [ 39 | ("win-arm64", "win-arm64"), 40 | ("win-amd64", "win-amd64"), 41 | (None, get_platform()), 42 | ], 43 | ) 44 | def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected): 45 | """ 46 | Ensure a specified target platform is passed to _get_vcvars_spec. 47 | """ 48 | compiler = msvc.Compiler() 49 | 50 | def _get_vcvars_spec(host_platform, platform): 51 | assert platform == expected 52 | 53 | monkeypatch.setattr(msvc, '_get_vcvars_spec', _get_vcvars_spec) 54 | compiler.initialize(plat_name) 55 | 56 | @needs_winreg 57 | def test_get_vc_env_unicode(self): 58 | test_var = 'ṰḖṤṪ┅ṼẨṜ' 59 | test_value = '₃⁴₅' 60 | 61 | # Ensure we don't early exit from _get_vc_env 62 | old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None) 63 | os.environ[test_var] = test_value 64 | try: 65 | env = msvc._get_vc_env('x86') 66 | assert test_var.lower() in env 67 | assert test_value == env[test_var.lower()] 68 | finally: 69 | os.environ.pop(test_var) 70 | if old_distutils_use_sdk: 71 | os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk 72 | 73 | @needs_winreg 74 | @pytest.mark.parametrize('ver', (2015, 2017)) 75 | def test_get_vc(self, ver): 76 | # This function cannot be mocked, so pass if VC is found 77 | # and skip otherwise. 78 | lookup = getattr(msvc, f'_find_vc{ver}') 79 | expected_version = {2015: 14, 2017: 15}[ver] 80 | version, path = lookup() 81 | if not version: 82 | pytest.skip(f"VS {ver} is not installed") 83 | assert version >= expected_version 84 | assert os.path.isdir(path) 85 | 86 | 87 | class CheckThread(threading.Thread): 88 | exc_info = None 89 | 90 | def run(self): 91 | try: 92 | super().run() 93 | except Exception: 94 | self.exc_info = sys.exc_info() 95 | 96 | def __bool__(self): 97 | return not self.exc_info 98 | 99 | 100 | class TestSpawn: 101 | def test_concurrent_safe(self): 102 | """ 103 | Concurrent calls to spawn should have consistent results. 104 | """ 105 | compiler = msvc.Compiler() 106 | compiler._paths = "expected" 107 | inner_cmd = 'import os; assert os.environ["PATH"] == "expected"' 108 | command = [sys.executable, '-c', inner_cmd] 109 | 110 | threads = [ 111 | CheckThread(target=compiler.spawn, args=[command]) for n in range(100) 112 | ] 113 | for thread in threads: 114 | thread.start() 115 | for thread in threads: 116 | thread.join() 117 | assert all(threads) 118 | 119 | def test_concurrent_safe_fallback(self): 120 | """ 121 | If CCompiler.spawn has been monkey-patched without support 122 | for an env, it should still execute. 123 | """ 124 | from distutils import ccompiler 125 | 126 | compiler = msvc.Compiler() 127 | compiler._paths = "expected" 128 | 129 | def CCompiler_spawn(self, cmd): 130 | "A spawn without an env argument." 131 | assert os.environ["PATH"] == "expected" 132 | 133 | with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn): 134 | compiler.spawn(["n/a"]) 135 | 136 | assert os.environ.get("PATH") != "expected" 137 | -------------------------------------------------------------------------------- /distutils/compilers/C/zos.py: -------------------------------------------------------------------------------- 1 | """distutils.zosccompiler 2 | 3 | Contains the selection of the c & c++ compilers on z/OS. There are several 4 | different c compilers on z/OS, all of them are optional, so the correct 5 | one needs to be chosen based on the users input. This is compatible with 6 | the following compilers: 7 | 8 | IBM C/C++ For Open Enterprise Languages on z/OS 2.0 9 | IBM Open XL C/C++ 1.1 for z/OS 10 | IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5 11 | IBM z/OS XL C/C++ 12 | """ 13 | 14 | import os 15 | 16 | from ... import sysconfig 17 | from ...errors import DistutilsExecError 18 | from . import unix 19 | from .errors import CompileError 20 | 21 | _cc_args = { 22 | 'ibm-openxl': [ 23 | '-m64', 24 | '-fvisibility=default', 25 | '-fzos-le-char-mode=ascii', 26 | '-fno-short-enums', 27 | ], 28 | 'ibm-xlclang': [ 29 | '-q64', 30 | '-qexportall', 31 | '-qascii', 32 | '-qstrict', 33 | '-qnocsect', 34 | '-Wa,asa,goff', 35 | '-Wa,xplink', 36 | '-qgonumber', 37 | '-qenum=int', 38 | '-Wc,DLL', 39 | ], 40 | 'ibm-xlc': [ 41 | '-q64', 42 | '-qexportall', 43 | '-qascii', 44 | '-qstrict', 45 | '-qnocsect', 46 | '-Wa,asa,goff', 47 | '-Wa,xplink', 48 | '-qgonumber', 49 | '-qenum=int', 50 | '-Wc,DLL', 51 | '-qlanglvl=extc99', 52 | ], 53 | } 54 | 55 | _cxx_args = { 56 | 'ibm-openxl': [ 57 | '-m64', 58 | '-fvisibility=default', 59 | '-fzos-le-char-mode=ascii', 60 | '-fno-short-enums', 61 | ], 62 | 'ibm-xlclang': [ 63 | '-q64', 64 | '-qexportall', 65 | '-qascii', 66 | '-qstrict', 67 | '-qnocsect', 68 | '-Wa,asa,goff', 69 | '-Wa,xplink', 70 | '-qgonumber', 71 | '-qenum=int', 72 | '-Wc,DLL', 73 | ], 74 | 'ibm-xlc': [ 75 | '-q64', 76 | '-qexportall', 77 | '-qascii', 78 | '-qstrict', 79 | '-qnocsect', 80 | '-Wa,asa,goff', 81 | '-Wa,xplink', 82 | '-qgonumber', 83 | '-qenum=int', 84 | '-Wc,DLL', 85 | '-qlanglvl=extended0x', 86 | ], 87 | } 88 | 89 | _asm_args = { 90 | 'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'], 91 | 'ibm-xlclang': [], 92 | 'ibm-xlc': [], 93 | } 94 | 95 | _ld_args = { 96 | 'ibm-openxl': [], 97 | 'ibm-xlclang': ['-Wl,dll', '-q64'], 98 | 'ibm-xlc': ['-Wl,dll', '-q64'], 99 | } 100 | 101 | 102 | # Python on z/OS is built with no compiler specific options in it's CFLAGS. 103 | # But each compiler requires it's own specific options to build successfully, 104 | # though some of the options are common between them 105 | class Compiler(unix.Compiler): 106 | src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s'] 107 | _cpp_extensions = ['.cc', '.cpp', '.cxx', '.C'] 108 | _asm_extensions = ['.s'] 109 | 110 | def _get_zos_compiler_name(self): 111 | zos_compiler_names = [ 112 | os.path.basename(binary) 113 | for envvar in ('CC', 'CXX', 'LDSHARED') 114 | if (binary := os.environ.get(envvar, None)) 115 | ] 116 | if len(zos_compiler_names) == 0: 117 | return 'ibm-openxl' 118 | 119 | zos_compilers = {} 120 | for compiler in ( 121 | 'ibm-clang', 122 | 'ibm-clang64', 123 | 'ibm-clang++', 124 | 'ibm-clang++64', 125 | 'clang', 126 | 'clang++', 127 | 'clang-14', 128 | ): 129 | zos_compilers[compiler] = 'ibm-openxl' 130 | 131 | for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'): 132 | zos_compilers[compiler] = 'ibm-xlclang' 133 | 134 | for compiler in ('xlc', 'xlC', 'xlc++'): 135 | zos_compilers[compiler] = 'ibm-xlc' 136 | 137 | return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl') 138 | 139 | def __init__(self, verbose=False, dry_run=False, force=False): 140 | super().__init__(verbose, dry_run, force) 141 | self.zos_compiler = self._get_zos_compiler_name() 142 | sysconfig.customize_compiler(self) 143 | 144 | def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): 145 | local_args = [] 146 | if ext in self._cpp_extensions: 147 | compiler = self.compiler_cxx 148 | local_args.extend(_cxx_args[self.zos_compiler]) 149 | elif ext in self._asm_extensions: 150 | compiler = self.compiler_so 151 | local_args.extend(_cc_args[self.zos_compiler]) 152 | local_args.extend(_asm_args[self.zos_compiler]) 153 | else: 154 | compiler = self.compiler_so 155 | local_args.extend(_cc_args[self.zos_compiler]) 156 | local_args.extend(cc_args) 157 | 158 | try: 159 | self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs) 160 | except DistutilsExecError as msg: 161 | raise CompileError(msg) 162 | 163 | def runtime_library_dir_option(self, dir): 164 | return '-L' + dir 165 | 166 | def link( 167 | self, 168 | target_desc, 169 | objects, 170 | output_filename, 171 | output_dir=None, 172 | libraries=None, 173 | library_dirs=None, 174 | runtime_library_dirs=None, 175 | export_symbols=None, 176 | debug=False, 177 | extra_preargs=None, 178 | extra_postargs=None, 179 | build_temp=None, 180 | target_lang=None, 181 | ): 182 | # For a built module to use functions from cpython, it needs to use Pythons 183 | # side deck file. The side deck is located beside the libpython3.xx.so 184 | ldversion = sysconfig.get_config_var('LDVERSION') 185 | if sysconfig.python_build: 186 | side_deck_path = os.path.join( 187 | sysconfig.get_config_var('abs_builddir'), 188 | f'libpython{ldversion}.x', 189 | ) 190 | else: 191 | side_deck_path = os.path.join( 192 | sysconfig.get_config_var('installed_base'), 193 | sysconfig.get_config_var('platlibdir'), 194 | f'libpython{ldversion}.x', 195 | ) 196 | 197 | if os.path.exists(side_deck_path): 198 | if extra_postargs: 199 | extra_postargs.append(side_deck_path) 200 | else: 201 | extra_postargs = [side_deck_path] 202 | 203 | # Check and replace libraries included side deck files 204 | if runtime_library_dirs: 205 | for dir in runtime_library_dirs: 206 | for library in libraries[:]: 207 | library_side_deck = os.path.join(dir, f'{library}.x') 208 | if os.path.exists(library_side_deck): 209 | libraries.remove(library) 210 | extra_postargs.append(library_side_deck) 211 | break 212 | 213 | # Any required ld args for the given compiler 214 | extra_postargs.extend(_ld_args[self.zos_compiler]) 215 | 216 | super().link( 217 | target_desc, 218 | objects, 219 | output_filename, 220 | output_dir, 221 | libraries, 222 | library_dirs, 223 | runtime_library_dirs, 224 | export_symbols, 225 | debug, 226 | extra_preargs, 227 | extra_postargs, 228 | build_temp, 229 | target_lang, 230 | ) 231 | -------------------------------------------------------------------------------- /distutils/cygwinccompiler.py: -------------------------------------------------------------------------------- 1 | from .compilers.C import cygwin 2 | from .compilers.C.cygwin import ( 3 | CONFIG_H_NOTOK, 4 | CONFIG_H_OK, 5 | CONFIG_H_UNCERTAIN, 6 | check_config_h, 7 | get_msvcr, 8 | is_cygwincc, 9 | ) 10 | 11 | __all__ = [ 12 | 'CONFIG_H_NOTOK', 13 | 'CONFIG_H_OK', 14 | 'CONFIG_H_UNCERTAIN', 15 | 'CygwinCCompiler', 16 | 'Mingw32CCompiler', 17 | 'check_config_h', 18 | 'get_msvcr', 19 | 'is_cygwincc', 20 | ] 21 | 22 | 23 | CygwinCCompiler = cygwin.Compiler 24 | Mingw32CCompiler = cygwin.MinGW32Compiler 25 | 26 | 27 | get_versions = None 28 | """ 29 | A stand-in for the previous get_versions() function to prevent failures 30 | when monkeypatched. See pypa/setuptools#2969. 31 | """ 32 | -------------------------------------------------------------------------------- /distutils/debug.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # If DISTUTILS_DEBUG is anything other than the empty string, we run in 4 | # debug mode. 5 | DEBUG = os.environ.get('DISTUTILS_DEBUG') 6 | -------------------------------------------------------------------------------- /distutils/dep_util.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from . import _modified 4 | 5 | 6 | def __getattr__(name): 7 | if name not in ['newer', 'newer_group', 'newer_pairwise']: 8 | raise AttributeError(name) 9 | warnings.warn( 10 | "dep_util is Deprecated. Use functions from setuptools instead.", 11 | DeprecationWarning, 12 | stacklevel=2, 13 | ) 14 | return getattr(_modified, name) 15 | -------------------------------------------------------------------------------- /distutils/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions used by the Distutils modules. 3 | 4 | Distutils modules may raise these or standard exceptions, 5 | including :exc:`SystemExit`. 6 | """ 7 | 8 | # compiler exceptions aliased for compatibility 9 | from .compilers.C.errors import CompileError as CompileError 10 | from .compilers.C.errors import Error as _Error 11 | from .compilers.C.errors import LibError as LibError 12 | from .compilers.C.errors import LinkError as LinkError 13 | from .compilers.C.errors import PreprocessError as PreprocessError 14 | from .compilers.C.errors import UnknownFileType as _UnknownFileType 15 | 16 | CCompilerError = _Error 17 | UnknownFileError = _UnknownFileType 18 | 19 | 20 | class DistutilsError(Exception): 21 | """The root of all Distutils evil.""" 22 | 23 | pass 24 | 25 | 26 | class DistutilsModuleError(DistutilsError): 27 | """Unable to load an expected module, or to find an expected class 28 | within some module (in particular, command modules and classes).""" 29 | 30 | pass 31 | 32 | 33 | class DistutilsClassError(DistutilsError): 34 | """Some command class (or possibly distribution class, if anyone 35 | feels a need to subclass Distribution) is found not to be holding 36 | up its end of the bargain, ie. implementing some part of the 37 | "command "interface.""" 38 | 39 | pass 40 | 41 | 42 | class DistutilsGetoptError(DistutilsError): 43 | """The option table provided to 'fancy_getopt()' is bogus.""" 44 | 45 | pass 46 | 47 | 48 | class DistutilsArgError(DistutilsError): 49 | """Raised by fancy_getopt in response to getopt.error -- ie. an 50 | error in the command line usage.""" 51 | 52 | pass 53 | 54 | 55 | class DistutilsFileError(DistutilsError): 56 | """Any problems in the filesystem: expected file not found, etc. 57 | Typically this is for problems that we detect before OSError 58 | could be raised.""" 59 | 60 | pass 61 | 62 | 63 | class DistutilsOptionError(DistutilsError): 64 | """Syntactic/semantic errors in command options, such as use of 65 | mutually conflicting options, or inconsistent options, 66 | badly-spelled values, etc. No distinction is made between option 67 | values originating in the setup script, the command line, config 68 | files, or what-have-you -- but if we *know* something originated in 69 | the setup script, we'll raise DistutilsSetupError instead.""" 70 | 71 | pass 72 | 73 | 74 | class DistutilsSetupError(DistutilsError): 75 | """For errors that can be definitely blamed on the setup script, 76 | such as invalid keyword arguments to 'setup()'.""" 77 | 78 | pass 79 | 80 | 81 | class DistutilsPlatformError(DistutilsError): 82 | """We don't know how to do something on the current platform (but 83 | we do know how to do it on some platform) -- eg. trying to compile 84 | C files on a platform not supported by a CCompiler subclass.""" 85 | 86 | pass 87 | 88 | 89 | class DistutilsExecError(DistutilsError): 90 | """Any problems executing an external program (such as the C 91 | compiler, when compiling C files).""" 92 | 93 | pass 94 | 95 | 96 | class DistutilsInternalError(DistutilsError): 97 | """Internal inconsistencies or impossibilities (obviously, this 98 | should never be seen if the code is working!).""" 99 | 100 | pass 101 | 102 | 103 | class DistutilsTemplateError(DistutilsError): 104 | """Syntax error in a file list template.""" 105 | 106 | 107 | class DistutilsByteCompileError(DistutilsError): 108 | """Byte compile error.""" 109 | -------------------------------------------------------------------------------- /distutils/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple log mechanism styled after PEP 282. 3 | 4 | Retained for compatibility and should not be used. 5 | """ 6 | 7 | import logging 8 | import warnings 9 | 10 | from ._log import log as _global_log 11 | 12 | DEBUG = logging.DEBUG 13 | INFO = logging.INFO 14 | WARN = logging.WARN 15 | ERROR = logging.ERROR 16 | FATAL = logging.FATAL 17 | 18 | log = _global_log.log 19 | debug = _global_log.debug 20 | info = _global_log.info 21 | warn = _global_log.warning 22 | error = _global_log.error 23 | fatal = _global_log.fatal 24 | 25 | 26 | def set_threshold(level): 27 | orig = _global_log.level 28 | _global_log.setLevel(level) 29 | return orig 30 | 31 | 32 | def set_verbosity(v): 33 | if v <= 0: 34 | set_threshold(logging.WARN) 35 | elif v == 1: 36 | set_threshold(logging.INFO) 37 | elif v >= 2: 38 | set_threshold(logging.DEBUG) 39 | 40 | 41 | class Log(logging.Logger): 42 | """distutils.log.Log is deprecated, please use an alternative from `logging`.""" 43 | 44 | def __init__(self, threshold=WARN): 45 | warnings.warn(Log.__doc__) # avoid DeprecationWarning to ensure warn is shown 46 | super().__init__(__name__, level=threshold) 47 | 48 | @property 49 | def threshold(self): 50 | return self.level 51 | 52 | @threshold.setter 53 | def threshold(self, level): 54 | self.setLevel(level) 55 | 56 | warn = logging.Logger.warning 57 | -------------------------------------------------------------------------------- /distutils/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /distutils/spawn.py: -------------------------------------------------------------------------------- 1 | """distutils.spawn 2 | 3 | Provides the 'spawn()' function, a front-end to various platform- 4 | specific functions for launching another program in a sub-process. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import os 10 | import platform 11 | import shutil 12 | import subprocess 13 | import sys 14 | import warnings 15 | from collections.abc import Mapping, MutableSequence 16 | from typing import TYPE_CHECKING, TypeVar, overload 17 | 18 | from ._log import log 19 | from .debug import DEBUG 20 | from .errors import DistutilsExecError 21 | 22 | if TYPE_CHECKING: 23 | from subprocess import _ENV 24 | 25 | 26 | _MappingT = TypeVar("_MappingT", bound=Mapping) 27 | 28 | 29 | def _debug(cmd): 30 | """ 31 | Render a subprocess command differently depending on DEBUG. 32 | """ 33 | return cmd if DEBUG else cmd[0] 34 | 35 | 36 | def _inject_macos_ver(env: _MappingT | None) -> _MappingT | dict[str, str | int] | None: 37 | if platform.system() != 'Darwin': 38 | return env 39 | 40 | from .util import MACOSX_VERSION_VAR, get_macosx_target_ver 41 | 42 | target_ver = get_macosx_target_ver() 43 | update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {} 44 | return {**_resolve(env), **update} 45 | 46 | 47 | @overload 48 | def _resolve(env: None) -> os._Environ[str]: ... 49 | @overload 50 | def _resolve(env: _MappingT) -> _MappingT: ... 51 | def _resolve(env: _MappingT | None) -> _MappingT | os._Environ[str]: 52 | return os.environ if env is None else env 53 | 54 | 55 | def spawn( 56 | cmd: MutableSequence[bytes | str | os.PathLike[str]], 57 | search_path: bool = True, 58 | verbose: bool = False, 59 | dry_run: bool = False, 60 | env: _ENV | None = None, 61 | ) -> None: 62 | """Run another program, specified as a command list 'cmd', in a new process. 63 | 64 | 'cmd' is just the argument list for the new process, ie. 65 | cmd[0] is the program to run and cmd[1:] are the rest of its arguments. 66 | There is no way to run a program with a name different from that of its 67 | executable. 68 | 69 | If 'search_path' is true (the default), the system's executable 70 | search path will be used to find the program; otherwise, cmd[0] 71 | must be the exact path to the executable. If 'dry_run' is true, 72 | the command will not actually be run. 73 | 74 | Raise DistutilsExecError if running the program fails in any way; just 75 | return on success. 76 | """ 77 | log.info(subprocess.list2cmdline(cmd)) 78 | if dry_run: 79 | return 80 | 81 | if search_path: 82 | executable = shutil.which(cmd[0]) 83 | if executable is not None: 84 | cmd[0] = executable 85 | 86 | try: 87 | subprocess.check_call(cmd, env=_inject_macos_ver(env)) 88 | except OSError as exc: 89 | raise DistutilsExecError( 90 | f"command {_debug(cmd)!r} failed: {exc.args[-1]}" 91 | ) from exc 92 | except subprocess.CalledProcessError as err: 93 | raise DistutilsExecError( 94 | f"command {_debug(cmd)!r} failed with exit code {err.returncode}" 95 | ) from err 96 | 97 | 98 | def find_executable(executable: str, path: str | None = None) -> str | None: 99 | """Tries to find 'executable' in the directories listed in 'path'. 100 | 101 | A string listing directories separated by 'os.pathsep'; defaults to 102 | os.environ['PATH']. Returns the complete filename or None if not found. 103 | """ 104 | warnings.warn( 105 | 'Use shutil.which instead of find_executable', DeprecationWarning, stacklevel=2 106 | ) 107 | _, ext = os.path.splitext(executable) 108 | if (sys.platform == 'win32') and (ext != '.exe'): 109 | executable = executable + '.exe' 110 | 111 | if os.path.isfile(executable): 112 | return executable 113 | 114 | if path is None: 115 | path = os.environ.get('PATH', None) 116 | # bpo-35755: Don't fall through if PATH is the empty string 117 | if path is None: 118 | try: 119 | path = os.confstr("CS_PATH") 120 | except (AttributeError, ValueError): 121 | # os.confstr() or CS_PATH is not available 122 | path = os.defpath 123 | 124 | # PATH='' doesn't match, whereas PATH=':' looks in the current directory 125 | if not path: 126 | return None 127 | 128 | paths = path.split(os.pathsep) 129 | for p in paths: 130 | f = os.path.join(p, executable) 131 | if os.path.isfile(f): 132 | # the file exists, we have a shot at spawn working 133 | return f 134 | return None 135 | -------------------------------------------------------------------------------- /distutils/tests/Setup.sample: -------------------------------------------------------------------------------- 1 | # Setup file from the pygame project 2 | 3 | #--StartConfig 4 | SDL = -I/usr/include/SDL -D_REENTRANT -lSDL 5 | FONT = -lSDL_ttf 6 | IMAGE = -lSDL_image 7 | MIXER = -lSDL_mixer 8 | SMPEG = -lsmpeg 9 | PNG = -lpng 10 | JPEG = -ljpeg 11 | SCRAP = -lX11 12 | PORTMIDI = -lportmidi 13 | PORTTIME = -lporttime 14 | #--EndConfig 15 | 16 | #DEBUG = -C-W -C-Wall 17 | DEBUG = 18 | 19 | #the following modules are optional. you will want to compile 20 | #everything you can, but you can ignore ones you don't have 21 | #dependencies for, just comment them out 22 | 23 | imageext src/imageext.c $(SDL) $(IMAGE) $(PNG) $(JPEG) $(DEBUG) 24 | font src/font.c $(SDL) $(FONT) $(DEBUG) 25 | mixer src/mixer.c $(SDL) $(MIXER) $(DEBUG) 26 | mixer_music src/music.c $(SDL) $(MIXER) $(DEBUG) 27 | _numericsurfarray src/_numericsurfarray.c $(SDL) $(DEBUG) 28 | _numericsndarray src/_numericsndarray.c $(SDL) $(MIXER) $(DEBUG) 29 | movie src/movie.c $(SDL) $(SMPEG) $(DEBUG) 30 | scrap src/scrap.c $(SDL) $(SCRAP) $(DEBUG) 31 | _camera src/_camera.c src/camera_v4l2.c src/camera_v4l.c $(SDL) $(DEBUG) 32 | pypm src/pypm.c $(SDL) $(PORTMIDI) $(PORTTIME) $(DEBUG) 33 | 34 | GFX = src/SDL_gfx/SDL_gfxPrimitives.c 35 | #GFX = src/SDL_gfx/SDL_gfxBlitFunc.c src/SDL_gfx/SDL_gfxPrimitives.c 36 | gfxdraw src/gfxdraw.c $(SDL) $(GFX) $(DEBUG) 37 | 38 | 39 | 40 | #these modules are required for pygame to run. they only require 41 | #SDL as a dependency. these should not be altered 42 | 43 | base src/base.c $(SDL) $(DEBUG) 44 | cdrom src/cdrom.c $(SDL) $(DEBUG) 45 | color src/color.c $(SDL) $(DEBUG) 46 | constants src/constants.c $(SDL) $(DEBUG) 47 | display src/display.c $(SDL) $(DEBUG) 48 | event src/event.c $(SDL) $(DEBUG) 49 | fastevent src/fastevent.c src/fastevents.c $(SDL) $(DEBUG) 50 | key src/key.c $(SDL) $(DEBUG) 51 | mouse src/mouse.c $(SDL) $(DEBUG) 52 | rect src/rect.c $(SDL) $(DEBUG) 53 | rwobject src/rwobject.c $(SDL) $(DEBUG) 54 | surface src/surface.c src/alphablit.c src/surface_fill.c $(SDL) $(DEBUG) 55 | surflock src/surflock.c $(SDL) $(DEBUG) 56 | time src/time.c $(SDL) $(DEBUG) 57 | joystick src/joystick.c $(SDL) $(DEBUG) 58 | draw src/draw.c $(SDL) $(DEBUG) 59 | image src/image.c $(SDL) $(DEBUG) 60 | overlay src/overlay.c $(SDL) $(DEBUG) 61 | transform src/transform.c src/rotozoom.c src/scale2x.c src/scale_mmx.c $(SDL) $(DEBUG) 62 | mask src/mask.c src/bitmask.c $(SDL) $(DEBUG) 63 | bufferproxy src/bufferproxy.c $(SDL) $(DEBUG) 64 | pixelarray src/pixelarray.c $(SDL) $(DEBUG) 65 | _arraysurfarray src/_arraysurfarray.c $(SDL) $(DEBUG) 66 | 67 | 68 | -------------------------------------------------------------------------------- /distutils/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for distutils. 3 | 4 | Tests for the command classes in the distutils.command package are 5 | included in distutils.tests as well, instead of using a separate 6 | distutils.command.tests package, since command identification is done 7 | by import rather than matching pre-defined names. 8 | """ 9 | 10 | import shutil 11 | from collections.abc import Sequence 12 | 13 | 14 | def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no cover 15 | """Check if the compiler components used to build the interpreter exist. 16 | 17 | Check for the existence of the compiler executables whose names are listed 18 | in 'cmd_names' or all the compiler executables when 'cmd_names' is empty 19 | and return the first missing executable or None when none is found 20 | missing. 21 | 22 | """ 23 | from distutils import ccompiler, errors, sysconfig 24 | 25 | compiler = ccompiler.new_compiler() 26 | sysconfig.customize_compiler(compiler) 27 | if compiler.compiler_type == "msvc": 28 | # MSVC has no executables, so check whether initialization succeeds 29 | try: 30 | compiler.initialize() 31 | except errors.DistutilsPlatformError: 32 | return "msvc" 33 | for name in compiler.executables: 34 | if cmd_names and name not in cmd_names: 35 | continue 36 | cmd = getattr(compiler, name) 37 | if cmd_names: 38 | assert cmd is not None, f"the '{name}' executable is not configured" 39 | elif not cmd: 40 | continue 41 | if shutil.which(cmd[0]) is None: 42 | return cmd[0] 43 | -------------------------------------------------------------------------------- /distutils/tests/compat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/distutils/603b94ec2e7876294345e5bceac276496b369641/distutils/tests/compat/__init__.py -------------------------------------------------------------------------------- /distutils/tests/compat/py39.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 10): 4 | from test.support.import_helper import ( 5 | CleanImport as CleanImport, 6 | ) 7 | from test.support.import_helper import ( 8 | DirsOnSysPath as DirsOnSysPath, 9 | ) 10 | from test.support.os_helper import ( 11 | EnvironmentVarGuard as EnvironmentVarGuard, 12 | ) 13 | from test.support.os_helper import ( 14 | rmtree as rmtree, 15 | ) 16 | from test.support.os_helper import ( 17 | skip_unless_symlink as skip_unless_symlink, 18 | ) 19 | from test.support.os_helper import ( 20 | unlink as unlink, 21 | ) 22 | else: 23 | from test.support import ( 24 | CleanImport as CleanImport, 25 | ) 26 | from test.support import ( 27 | DirsOnSysPath as DirsOnSysPath, 28 | ) 29 | from test.support import ( 30 | EnvironmentVarGuard as EnvironmentVarGuard, 31 | ) 32 | from test.support import ( 33 | rmtree as rmtree, 34 | ) 35 | from test.support import ( 36 | skip_unless_symlink as skip_unless_symlink, 37 | ) 38 | from test.support import ( 39 | unlink as unlink, 40 | ) 41 | -------------------------------------------------------------------------------- /distutils/tests/includetest.rst: -------------------------------------------------------------------------------- 1 | This should be included. 2 | -------------------------------------------------------------------------------- /distutils/tests/support.py: -------------------------------------------------------------------------------- 1 | """Support code for distutils test cases.""" 2 | 3 | import itertools 4 | import os 5 | import pathlib 6 | import shutil 7 | import sys 8 | import sysconfig 9 | import tempfile 10 | from distutils.core import Distribution 11 | 12 | import pytest 13 | from more_itertools import always_iterable 14 | 15 | 16 | @pytest.mark.usefixtures('distutils_managed_tempdir') 17 | class TempdirManager: 18 | """ 19 | Mix-in class that handles temporary directories for test cases. 20 | """ 21 | 22 | def mkdtemp(self): 23 | """Create a temporary directory that will be cleaned up. 24 | 25 | Returns the path of the directory. 26 | """ 27 | d = tempfile.mkdtemp() 28 | self.tempdirs.append(d) 29 | return d 30 | 31 | def write_file(self, path, content='xxx'): 32 | """Writes a file in the given path. 33 | 34 | path can be a string or a sequence. 35 | """ 36 | pathlib.Path(*always_iterable(path)).write_text(content, encoding='utf-8') 37 | 38 | def create_dist(self, pkg_name='foo', **kw): 39 | """Will generate a test environment. 40 | 41 | This function creates: 42 | - a Distribution instance using keywords 43 | - a temporary directory with a package structure 44 | 45 | It returns the package directory and the distribution 46 | instance. 47 | """ 48 | tmp_dir = self.mkdtemp() 49 | pkg_dir = os.path.join(tmp_dir, pkg_name) 50 | os.mkdir(pkg_dir) 51 | dist = Distribution(attrs=kw) 52 | 53 | return pkg_dir, dist 54 | 55 | 56 | class DummyCommand: 57 | """Class to store options for retrieval via set_undefined_options().""" 58 | 59 | def __init__(self, **kwargs): 60 | vars(self).update(kwargs) 61 | 62 | def ensure_finalized(self): 63 | pass 64 | 65 | 66 | def copy_xxmodule_c(directory): 67 | """Helper for tests that need the xxmodule.c source file. 68 | 69 | Example use: 70 | 71 | def test_compile(self): 72 | copy_xxmodule_c(self.tmpdir) 73 | self.assertIn('xxmodule.c', os.listdir(self.tmpdir)) 74 | 75 | If the source file can be found, it will be copied to *directory*. If not, 76 | the test will be skipped. Errors during copy are not caught. 77 | """ 78 | shutil.copy(_get_xxmodule_path(), os.path.join(directory, 'xxmodule.c')) 79 | 80 | 81 | def _get_xxmodule_path(): 82 | source_name = 'xxmodule.c' if sys.version_info > (3, 9) else 'xxmodule-3.8.c' 83 | return os.path.join(os.path.dirname(__file__), source_name) 84 | 85 | 86 | def fixup_build_ext(cmd): 87 | """Function needed to make build_ext tests pass. 88 | 89 | When Python was built with --enable-shared on Unix, -L. is not enough to 90 | find libpython.so, because regrtest runs in a tempdir, not in the 91 | source directory where the .so lives. 92 | 93 | When Python was built with in debug mode on Windows, build_ext commands 94 | need their debug attribute set, and it is not done automatically for 95 | some reason. 96 | 97 | This function handles both of these things. Example use: 98 | 99 | cmd = build_ext(dist) 100 | support.fixup_build_ext(cmd) 101 | cmd.ensure_finalized() 102 | 103 | Unlike most other Unix platforms, Mac OS X embeds absolute paths 104 | to shared libraries into executables, so the fixup is not needed there. 105 | """ 106 | if os.name == 'nt': 107 | cmd.debug = sys.executable.endswith('_d.exe') 108 | elif sysconfig.get_config_var('Py_ENABLE_SHARED'): 109 | # To further add to the shared builds fun on Unix, we can't just add 110 | # library_dirs to the Extension() instance because that doesn't get 111 | # plumbed through to the final compiler command. 112 | runshared = sysconfig.get_config_var('RUNSHARED') 113 | if runshared is None: 114 | cmd.library_dirs = ['.'] 115 | else: 116 | if sys.platform == 'darwin': 117 | cmd.library_dirs = [] 118 | else: 119 | name, equals, value = runshared.partition('=') 120 | cmd.library_dirs = [d for d in value.split(os.pathsep) if d] 121 | 122 | 123 | def combine_markers(cls): 124 | """ 125 | pytest will honor markers as found on the class, but when 126 | markers are on multiple subclasses, only one appears. Use 127 | this decorator to combine those markers. 128 | """ 129 | cls.pytestmark = [ 130 | mark 131 | for base in itertools.chain([cls], cls.__bases__) 132 | for mark in getattr(base, 'pytestmark', []) 133 | ] 134 | return cls 135 | -------------------------------------------------------------------------------- /distutils/tests/test_bdist.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.bdist.""" 2 | 3 | from distutils.command.bdist import bdist 4 | from distutils.tests import support 5 | 6 | 7 | class TestBuild(support.TempdirManager): 8 | def test_formats(self): 9 | # let's create a command and make sure 10 | # we can set the format 11 | dist = self.create_dist()[1] 12 | cmd = bdist(dist) 13 | cmd.formats = ['gztar'] 14 | cmd.ensure_finalized() 15 | assert cmd.formats == ['gztar'] 16 | 17 | # what formats does bdist offer? 18 | formats = [ 19 | 'bztar', 20 | 'gztar', 21 | 'rpm', 22 | 'tar', 23 | 'xztar', 24 | 'zip', 25 | 'ztar', 26 | ] 27 | found = sorted(cmd.format_commands) 28 | assert found == formats 29 | 30 | def test_skip_build(self): 31 | # bug #10946: bdist --skip-build should trickle down to subcommands 32 | dist = self.create_dist()[1] 33 | cmd = bdist(dist) 34 | cmd.skip_build = True 35 | cmd.ensure_finalized() 36 | dist.command_obj['bdist'] = cmd 37 | 38 | names = [ 39 | 'bdist_dumb', 40 | ] # bdist_rpm does not support --skip-build 41 | 42 | for name in names: 43 | subcmd = cmd.get_finalized_command(name) 44 | if getattr(subcmd, '_unsupported', False): 45 | # command is not supported on this build 46 | continue 47 | assert subcmd.skip_build, f'{name} should take --skip-build from bdist' 48 | -------------------------------------------------------------------------------- /distutils/tests/test_bdist_dumb.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.bdist_dumb.""" 2 | 3 | import os 4 | import sys 5 | import zipfile 6 | from distutils.command.bdist_dumb import bdist_dumb 7 | from distutils.core import Distribution 8 | from distutils.tests import support 9 | 10 | import pytest 11 | 12 | SETUP_PY = """\ 13 | from distutils.core import setup 14 | import foo 15 | 16 | setup(name='foo', version='0.1', py_modules=['foo'], 17 | url='xxx', author='xxx', author_email='xxx') 18 | 19 | """ 20 | 21 | 22 | @support.combine_markers 23 | @pytest.mark.usefixtures('save_env') 24 | @pytest.mark.usefixtures('save_argv') 25 | @pytest.mark.usefixtures('save_cwd') 26 | class TestBuildDumb( 27 | support.TempdirManager, 28 | ): 29 | @pytest.mark.usefixtures('needs_zlib') 30 | def test_simple_built(self): 31 | # let's create a simple package 32 | tmp_dir = self.mkdtemp() 33 | pkg_dir = os.path.join(tmp_dir, 'foo') 34 | os.mkdir(pkg_dir) 35 | self.write_file((pkg_dir, 'setup.py'), SETUP_PY) 36 | self.write_file((pkg_dir, 'foo.py'), '#') 37 | self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') 38 | self.write_file((pkg_dir, 'README'), '') 39 | 40 | dist = Distribution({ 41 | 'name': 'foo', 42 | 'version': '0.1', 43 | 'py_modules': ['foo'], 44 | 'url': 'xxx', 45 | 'author': 'xxx', 46 | 'author_email': 'xxx', 47 | }) 48 | dist.script_name = 'setup.py' 49 | os.chdir(pkg_dir) 50 | 51 | sys.argv = ['setup.py'] 52 | cmd = bdist_dumb(dist) 53 | 54 | # so the output is the same no matter 55 | # what is the platform 56 | cmd.format = 'zip' 57 | 58 | cmd.ensure_finalized() 59 | cmd.run() 60 | 61 | # see what we have 62 | dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) 63 | base = f"{dist.get_fullname()}.{cmd.plat_name}.zip" 64 | 65 | assert dist_created == [base] 66 | 67 | # now let's check what we have in the zip file 68 | fp = zipfile.ZipFile(os.path.join('dist', base)) 69 | try: 70 | contents = fp.namelist() 71 | finally: 72 | fp.close() 73 | 74 | contents = sorted(filter(None, map(os.path.basename, contents))) 75 | wanted = ['foo-0.1-py{}.{}.egg-info'.format(*sys.version_info[:2]), 'foo.py'] 76 | if not sys.dont_write_bytecode: 77 | wanted.append(f'foo.{sys.implementation.cache_tag}.pyc') 78 | assert contents == sorted(wanted) 79 | -------------------------------------------------------------------------------- /distutils/tests/test_bdist_rpm.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.bdist_rpm.""" 2 | 3 | import os 4 | import shutil # noqa: F401 5 | import sys 6 | from distutils.command.bdist_rpm import bdist_rpm 7 | from distutils.core import Distribution 8 | from distutils.tests import support 9 | 10 | import pytest 11 | from test.support import requires_zlib 12 | 13 | SETUP_PY = """\ 14 | from distutils.core import setup 15 | import foo 16 | 17 | setup(name='foo', version='0.1', py_modules=['foo'], 18 | url='xxx', author='xxx', author_email='xxx') 19 | 20 | """ 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def sys_executable_encodable(): 25 | try: 26 | sys.executable.encode('UTF-8') 27 | except UnicodeEncodeError: 28 | pytest.skip("sys.executable is not encodable to UTF-8") 29 | 30 | 31 | mac_woes = pytest.mark.skipif( 32 | "not sys.platform.startswith('linux')", 33 | reason='spurious sdtout/stderr output under macOS', 34 | ) 35 | 36 | 37 | @pytest.mark.usefixtures('save_env') 38 | @pytest.mark.usefixtures('save_argv') 39 | @pytest.mark.usefixtures('save_cwd') 40 | class TestBuildRpm( 41 | support.TempdirManager, 42 | ): 43 | @mac_woes 44 | @requires_zlib() 45 | @pytest.mark.skipif("not shutil.which('rpm')") 46 | @pytest.mark.skipif("not shutil.which('rpmbuild')") 47 | def test_quiet(self): 48 | # let's create a package 49 | tmp_dir = self.mkdtemp() 50 | os.environ['HOME'] = tmp_dir # to confine dir '.rpmdb' creation 51 | pkg_dir = os.path.join(tmp_dir, 'foo') 52 | os.mkdir(pkg_dir) 53 | self.write_file((pkg_dir, 'setup.py'), SETUP_PY) 54 | self.write_file((pkg_dir, 'foo.py'), '#') 55 | self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') 56 | self.write_file((pkg_dir, 'README'), '') 57 | 58 | dist = Distribution({ 59 | 'name': 'foo', 60 | 'version': '0.1', 61 | 'py_modules': ['foo'], 62 | 'url': 'xxx', 63 | 'author': 'xxx', 64 | 'author_email': 'xxx', 65 | }) 66 | dist.script_name = 'setup.py' 67 | os.chdir(pkg_dir) 68 | 69 | sys.argv = ['setup.py'] 70 | cmd = bdist_rpm(dist) 71 | cmd.fix_python = True 72 | 73 | # running in quiet mode 74 | cmd.quiet = True 75 | cmd.ensure_finalized() 76 | cmd.run() 77 | 78 | dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) 79 | assert 'foo-0.1-1.noarch.rpm' in dist_created 80 | 81 | # bug #2945: upload ignores bdist_rpm files 82 | assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.src.rpm') in dist.dist_files 83 | assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.noarch.rpm') in dist.dist_files 84 | 85 | @mac_woes 86 | @requires_zlib() 87 | # https://bugs.python.org/issue1533164 88 | @pytest.mark.skipif("not shutil.which('rpm')") 89 | @pytest.mark.skipif("not shutil.which('rpmbuild')") 90 | def test_no_optimize_flag(self): 91 | # let's create a package that breaks bdist_rpm 92 | tmp_dir = self.mkdtemp() 93 | os.environ['HOME'] = tmp_dir # to confine dir '.rpmdb' creation 94 | pkg_dir = os.path.join(tmp_dir, 'foo') 95 | os.mkdir(pkg_dir) 96 | self.write_file((pkg_dir, 'setup.py'), SETUP_PY) 97 | self.write_file((pkg_dir, 'foo.py'), '#') 98 | self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') 99 | self.write_file((pkg_dir, 'README'), '') 100 | 101 | dist = Distribution({ 102 | 'name': 'foo', 103 | 'version': '0.1', 104 | 'py_modules': ['foo'], 105 | 'url': 'xxx', 106 | 'author': 'xxx', 107 | 'author_email': 'xxx', 108 | }) 109 | dist.script_name = 'setup.py' 110 | os.chdir(pkg_dir) 111 | 112 | sys.argv = ['setup.py'] 113 | cmd = bdist_rpm(dist) 114 | cmd.fix_python = True 115 | 116 | cmd.quiet = True 117 | cmd.ensure_finalized() 118 | cmd.run() 119 | 120 | dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) 121 | assert 'foo-0.1-1.noarch.rpm' in dist_created 122 | 123 | # bug #2945: upload ignores bdist_rpm files 124 | assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.src.rpm') in dist.dist_files 125 | assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.noarch.rpm') in dist.dist_files 126 | 127 | os.remove(os.path.join(pkg_dir, 'dist', 'foo-0.1-1.noarch.rpm')) 128 | -------------------------------------------------------------------------------- /distutils/tests/test_build.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.build.""" 2 | 3 | import os 4 | import sys 5 | from distutils.command.build import build 6 | from distutils.tests import support 7 | from sysconfig import get_config_var, get_platform 8 | 9 | 10 | class TestBuild(support.TempdirManager): 11 | def test_finalize_options(self): 12 | pkg_dir, dist = self.create_dist() 13 | cmd = build(dist) 14 | cmd.finalize_options() 15 | 16 | # if not specified, plat_name gets the current platform 17 | assert cmd.plat_name == get_platform() 18 | 19 | # build_purelib is build + lib 20 | wanted = os.path.join(cmd.build_base, 'lib') 21 | assert cmd.build_purelib == wanted 22 | 23 | # build_platlib is 'build/lib.platform-cache_tag[-pydebug]' 24 | # examples: 25 | # build/lib.macosx-10.3-i386-cpython39 26 | plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}' 27 | if get_config_var('Py_GIL_DISABLED'): 28 | plat_spec += 't' 29 | if hasattr(sys, 'gettotalrefcount'): 30 | assert cmd.build_platlib.endswith('-pydebug') 31 | plat_spec += '-pydebug' 32 | wanted = os.path.join(cmd.build_base, 'lib' + plat_spec) 33 | assert cmd.build_platlib == wanted 34 | 35 | # by default, build_lib = build_purelib 36 | assert cmd.build_lib == cmd.build_purelib 37 | 38 | # build_temp is build/temp. 39 | wanted = os.path.join(cmd.build_base, 'temp' + plat_spec) 40 | assert cmd.build_temp == wanted 41 | 42 | # build_scripts is build/scripts-x.x 43 | wanted = os.path.join( 44 | cmd.build_base, f'scripts-{sys.version_info.major}.{sys.version_info.minor}' 45 | ) 46 | assert cmd.build_scripts == wanted 47 | 48 | # executable is os.path.normpath(sys.executable) 49 | assert cmd.executable == os.path.normpath(sys.executable) 50 | -------------------------------------------------------------------------------- /distutils/tests/test_build_clib.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.build_clib.""" 2 | 3 | import os 4 | from distutils.command.build_clib import build_clib 5 | from distutils.errors import DistutilsSetupError 6 | from distutils.tests import missing_compiler_executable, support 7 | 8 | import pytest 9 | 10 | 11 | class TestBuildCLib(support.TempdirManager): 12 | def test_check_library_dist(self): 13 | pkg_dir, dist = self.create_dist() 14 | cmd = build_clib(dist) 15 | 16 | # 'libraries' option must be a list 17 | with pytest.raises(DistutilsSetupError): 18 | cmd.check_library_list('foo') 19 | 20 | # each element of 'libraries' must a 2-tuple 21 | with pytest.raises(DistutilsSetupError): 22 | cmd.check_library_list(['foo1', 'foo2']) 23 | 24 | # first element of each tuple in 'libraries' 25 | # must be a string (the library name) 26 | with pytest.raises(DistutilsSetupError): 27 | cmd.check_library_list([(1, 'foo1'), ('name', 'foo2')]) 28 | 29 | # library name may not contain directory separators 30 | with pytest.raises(DistutilsSetupError): 31 | cmd.check_library_list( 32 | [('name', 'foo1'), ('another/name', 'foo2')], 33 | ) 34 | 35 | # second element of each tuple must be a dictionary (build info) 36 | with pytest.raises(DistutilsSetupError): 37 | cmd.check_library_list( 38 | [('name', {}), ('another', 'foo2')], 39 | ) 40 | 41 | # those work 42 | libs = [('name', {}), ('name', {'ok': 'good'})] 43 | cmd.check_library_list(libs) 44 | 45 | def test_get_source_files(self): 46 | pkg_dir, dist = self.create_dist() 47 | cmd = build_clib(dist) 48 | 49 | # "in 'libraries' option 'sources' must be present and must be 50 | # a list of source filenames 51 | cmd.libraries = [('name', {})] 52 | with pytest.raises(DistutilsSetupError): 53 | cmd.get_source_files() 54 | 55 | cmd.libraries = [('name', {'sources': 1})] 56 | with pytest.raises(DistutilsSetupError): 57 | cmd.get_source_files() 58 | 59 | cmd.libraries = [('name', {'sources': ['a', 'b']})] 60 | assert cmd.get_source_files() == ['a', 'b'] 61 | 62 | cmd.libraries = [('name', {'sources': ('a', 'b')})] 63 | assert cmd.get_source_files() == ['a', 'b'] 64 | 65 | cmd.libraries = [ 66 | ('name', {'sources': ('a', 'b')}), 67 | ('name2', {'sources': ['c', 'd']}), 68 | ] 69 | assert cmd.get_source_files() == ['a', 'b', 'c', 'd'] 70 | 71 | def test_build_libraries(self): 72 | pkg_dir, dist = self.create_dist() 73 | cmd = build_clib(dist) 74 | 75 | class FakeCompiler: 76 | def compile(*args, **kw): 77 | pass 78 | 79 | create_static_lib = compile 80 | 81 | cmd.compiler = FakeCompiler() 82 | 83 | # build_libraries is also doing a bit of typo checking 84 | lib = [('name', {'sources': 'notvalid'})] 85 | with pytest.raises(DistutilsSetupError): 86 | cmd.build_libraries(lib) 87 | 88 | lib = [('name', {'sources': list()})] 89 | cmd.build_libraries(lib) 90 | 91 | lib = [('name', {'sources': tuple()})] 92 | cmd.build_libraries(lib) 93 | 94 | def test_finalize_options(self): 95 | pkg_dir, dist = self.create_dist() 96 | cmd = build_clib(dist) 97 | 98 | cmd.include_dirs = 'one-dir' 99 | cmd.finalize_options() 100 | assert cmd.include_dirs == ['one-dir'] 101 | 102 | cmd.include_dirs = None 103 | cmd.finalize_options() 104 | assert cmd.include_dirs == [] 105 | 106 | cmd.distribution.libraries = 'WONTWORK' 107 | with pytest.raises(DistutilsSetupError): 108 | cmd.finalize_options() 109 | 110 | @pytest.mark.skipif('platform.system() == "Windows"') 111 | def test_run(self): 112 | pkg_dir, dist = self.create_dist() 113 | cmd = build_clib(dist) 114 | 115 | foo_c = os.path.join(pkg_dir, 'foo.c') 116 | self.write_file(foo_c, 'int main(void) { return 1;}\n') 117 | cmd.libraries = [('foo', {'sources': [foo_c]})] 118 | 119 | build_temp = os.path.join(pkg_dir, 'build') 120 | os.mkdir(build_temp) 121 | cmd.build_temp = build_temp 122 | cmd.build_clib = build_temp 123 | 124 | # Before we run the command, we want to make sure 125 | # all commands are present on the system. 126 | ccmd = missing_compiler_executable() 127 | if ccmd is not None: 128 | self.skipTest(f'The {ccmd!r} command is not found') 129 | 130 | # this should work 131 | cmd.run() 132 | 133 | # let's check the result 134 | assert 'libfoo.a' in os.listdir(build_temp) 135 | -------------------------------------------------------------------------------- /distutils/tests/test_build_scripts.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.build_scripts.""" 2 | 3 | import os 4 | import textwrap 5 | from distutils import sysconfig 6 | from distutils.command.build_scripts import build_scripts 7 | from distutils.core import Distribution 8 | from distutils.tests import support 9 | 10 | import jaraco.path 11 | 12 | 13 | class TestBuildScripts(support.TempdirManager): 14 | def test_default_settings(self): 15 | cmd = self.get_build_scripts_cmd("/foo/bar", []) 16 | assert not cmd.force 17 | assert cmd.build_dir is None 18 | 19 | cmd.finalize_options() 20 | 21 | assert cmd.force 22 | assert cmd.build_dir == "/foo/bar" 23 | 24 | def test_build(self): 25 | source = self.mkdtemp() 26 | target = self.mkdtemp() 27 | expected = self.write_sample_scripts(source) 28 | 29 | cmd = self.get_build_scripts_cmd( 30 | target, [os.path.join(source, fn) for fn in expected] 31 | ) 32 | cmd.finalize_options() 33 | cmd.run() 34 | 35 | built = os.listdir(target) 36 | for name in expected: 37 | assert name in built 38 | 39 | def get_build_scripts_cmd(self, target, scripts): 40 | import sys 41 | 42 | dist = Distribution() 43 | dist.scripts = scripts 44 | dist.command_obj["build"] = support.DummyCommand( 45 | build_scripts=target, force=True, executable=sys.executable 46 | ) 47 | return build_scripts(dist) 48 | 49 | @staticmethod 50 | def write_sample_scripts(dir): 51 | spec = { 52 | 'script1.py': textwrap.dedent(""" 53 | #! /usr/bin/env python2.3 54 | # bogus script w/ Python sh-bang 55 | pass 56 | """).lstrip(), 57 | 'script2.py': textwrap.dedent(""" 58 | #!/usr/bin/python 59 | # bogus script w/ Python sh-bang 60 | pass 61 | """).lstrip(), 62 | 'shell.sh': textwrap.dedent(""" 63 | #!/bin/sh 64 | # bogus shell script w/ sh-bang 65 | exit 0 66 | """).lstrip(), 67 | } 68 | jaraco.path.build(spec, dir) 69 | return list(spec) 70 | 71 | def test_version_int(self): 72 | source = self.mkdtemp() 73 | target = self.mkdtemp() 74 | expected = self.write_sample_scripts(source) 75 | 76 | cmd = self.get_build_scripts_cmd( 77 | target, [os.path.join(source, fn) for fn in expected] 78 | ) 79 | cmd.finalize_options() 80 | 81 | # https://bugs.python.org/issue4524 82 | # 83 | # On linux-g++-32 with command line `./configure --enable-ipv6 84 | # --with-suffix=3`, python is compiled okay but the build scripts 85 | # failed when writing the name of the executable 86 | old = sysconfig.get_config_vars().get('VERSION') 87 | sysconfig._config_vars['VERSION'] = 4 88 | try: 89 | cmd.run() 90 | finally: 91 | if old is not None: 92 | sysconfig._config_vars['VERSION'] = old 93 | 94 | built = os.listdir(target) 95 | for name in expected: 96 | assert name in built 97 | -------------------------------------------------------------------------------- /distutils/tests/test_check.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.check.""" 2 | 3 | import distutils.command.check as _check 4 | import importlib 5 | import os 6 | import sys 7 | import textwrap 8 | from distutils.errors import DistutilsSetupError 9 | from distutils.tests import support 10 | 11 | import pytest 12 | 13 | HERE = os.path.dirname(__file__) 14 | 15 | 16 | @pytest.fixture 17 | def hide_pygments(monkeypatch, request): 18 | """ 19 | Clear docutils and hide the presence of pygments. 20 | """ 21 | clear_docutils(monkeypatch) 22 | monkeypatch.setitem(sys.modules, 'pygments', None) 23 | reload_check() 24 | # restore 'check' to its normal state after monkeypatch is undone 25 | request.addfinalizer(reload_check) 26 | 27 | 28 | def clear_docutils(monkeypatch): 29 | docutils_names = [ 30 | name for name in sys.modules if name.partition('.')[0] == 'docutils' 31 | ] 32 | for name in docutils_names: 33 | monkeypatch.delitem(sys.modules, name) 34 | 35 | 36 | def reload_check(): 37 | """ 38 | Reload the 'check' command module to reflect the import state. 39 | """ 40 | importlib.reload(_check) 41 | 42 | 43 | @support.combine_markers 44 | class TestCheck(support.TempdirManager): 45 | def _run(self, metadata=None, cwd=None, **options): 46 | if metadata is None: 47 | metadata = {} 48 | if cwd is not None: 49 | old_dir = os.getcwd() 50 | os.chdir(cwd) 51 | pkg_info, dist = self.create_dist(**metadata) 52 | cmd = _check.check(dist) 53 | cmd.initialize_options() 54 | for name, value in options.items(): 55 | setattr(cmd, name, value) 56 | cmd.ensure_finalized() 57 | cmd.run() 58 | if cwd is not None: 59 | os.chdir(old_dir) 60 | return cmd 61 | 62 | def test_check_metadata(self): 63 | # let's run the command with no metadata at all 64 | # by default, check is checking the metadata 65 | # should have some warnings 66 | cmd = self._run() 67 | assert cmd._warnings == 1 68 | 69 | # now let's add the required fields 70 | # and run it again, to make sure we don't get 71 | # any warning anymore 72 | metadata = { 73 | 'url': 'xxx', 74 | 'author': 'xxx', 75 | 'author_email': 'xxx', 76 | 'name': 'xxx', 77 | 'version': 'xxx', 78 | } 79 | cmd = self._run(metadata) 80 | assert cmd._warnings == 0 81 | 82 | # now with the strict mode, we should 83 | # get an error if there are missing metadata 84 | with pytest.raises(DistutilsSetupError): 85 | self._run({}, **{'strict': 1}) 86 | 87 | # and of course, no error when all metadata are present 88 | cmd = self._run(metadata, strict=True) 89 | assert cmd._warnings == 0 90 | 91 | # now a test with non-ASCII characters 92 | metadata = { 93 | 'url': 'xxx', 94 | 'author': '\u00c9ric', 95 | 'author_email': 'xxx', 96 | 'name': 'xxx', 97 | 'version': 'xxx', 98 | 'description': 'Something about esszet \u00df', 99 | 'long_description': 'More things about esszet \u00df', 100 | } 101 | cmd = self._run(metadata) 102 | assert cmd._warnings == 0 103 | 104 | def test_check_author_maintainer(self): 105 | for kind in ("author", "maintainer"): 106 | # ensure no warning when author_email or maintainer_email is given 107 | # (the spec allows these fields to take the form "Name ") 108 | metadata = { 109 | 'url': 'xxx', 110 | kind + '_email': 'Name ', 111 | 'name': 'xxx', 112 | 'version': 'xxx', 113 | } 114 | cmd = self._run(metadata) 115 | assert cmd._warnings == 0 116 | 117 | # the check should not warn if only email is given 118 | metadata[kind + '_email'] = 'name@email.com' 119 | cmd = self._run(metadata) 120 | assert cmd._warnings == 0 121 | 122 | # the check should not warn if only the name is given 123 | metadata[kind] = "Name" 124 | del metadata[kind + '_email'] 125 | cmd = self._run(metadata) 126 | assert cmd._warnings == 0 127 | 128 | def test_check_document(self): 129 | pkg_info, dist = self.create_dist() 130 | cmd = _check.check(dist) 131 | 132 | # let's see if it detects broken rest 133 | broken_rest = 'title\n===\n\ntest' 134 | msgs = cmd._check_rst_data(broken_rest) 135 | assert len(msgs) == 1 136 | 137 | # and non-broken rest 138 | rest = 'title\n=====\n\ntest' 139 | msgs = cmd._check_rst_data(rest) 140 | assert len(msgs) == 0 141 | 142 | def test_check_restructuredtext(self): 143 | # let's see if it detects broken rest in long_description 144 | broken_rest = 'title\n===\n\ntest' 145 | pkg_info, dist = self.create_dist(long_description=broken_rest) 146 | cmd = _check.check(dist) 147 | cmd.check_restructuredtext() 148 | assert cmd._warnings == 1 149 | 150 | # let's see if we have an error with strict=True 151 | metadata = { 152 | 'url': 'xxx', 153 | 'author': 'xxx', 154 | 'author_email': 'xxx', 155 | 'name': 'xxx', 156 | 'version': 'xxx', 157 | 'long_description': broken_rest, 158 | } 159 | with pytest.raises(DistutilsSetupError): 160 | self._run(metadata, **{'strict': 1, 'restructuredtext': 1}) 161 | 162 | # and non-broken rest, including a non-ASCII character to test #12114 163 | metadata['long_description'] = 'title\n=====\n\ntest \u00df' 164 | cmd = self._run(metadata, strict=True, restructuredtext=True) 165 | assert cmd._warnings == 0 166 | 167 | # check that includes work to test #31292 168 | metadata['long_description'] = 'title\n=====\n\n.. include:: includetest.rst' 169 | cmd = self._run(metadata, cwd=HERE, strict=True, restructuredtext=True) 170 | assert cmd._warnings == 0 171 | 172 | code_examples = [ 173 | textwrap.dedent( 174 | f""" 175 | Here's some code: 176 | 177 | .. {directive}:: python 178 | 179 | def foo(): 180 | pass 181 | """ 182 | ).lstrip() 183 | for directive in ['code', 'code-block'] 184 | ] 185 | 186 | def check_rst_data(self, descr): 187 | pkg_info, dist = self.create_dist(long_description=descr) 188 | cmd = _check.check(dist) 189 | cmd.check_restructuredtext() 190 | return cmd._check_rst_data(descr) 191 | 192 | @pytest.mark.parametrize('descr', code_examples) 193 | def test_check_rst_with_syntax_highlight_pygments(self, descr): 194 | assert self.check_rst_data(descr) == [] 195 | 196 | @pytest.mark.parametrize('descr', code_examples) 197 | def test_check_rst_with_syntax_highlight_no_pygments(self, descr, hide_pygments): 198 | (msg,) = self.check_rst_data(descr) 199 | _, exc, _, _ = msg 200 | assert str(exc) == 'Cannot analyze code. Pygments package not found.' 201 | 202 | def test_check_all(self): 203 | with pytest.raises(DistutilsSetupError): 204 | self._run({}, **{'strict': 1, 'restructuredtext': 1}) 205 | -------------------------------------------------------------------------------- /distutils/tests/test_clean.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.clean.""" 2 | 3 | import os 4 | from distutils.command.clean import clean 5 | from distutils.tests import support 6 | 7 | 8 | class TestClean(support.TempdirManager): 9 | def test_simple_run(self): 10 | pkg_dir, dist = self.create_dist() 11 | cmd = clean(dist) 12 | 13 | # let's add some elements clean should remove 14 | dirs = [ 15 | (d, os.path.join(pkg_dir, d)) 16 | for d in ( 17 | 'build_temp', 18 | 'build_lib', 19 | 'bdist_base', 20 | 'build_scripts', 21 | 'build_base', 22 | ) 23 | ] 24 | 25 | for name, path in dirs: 26 | os.mkdir(path) 27 | setattr(cmd, name, path) 28 | if name == 'build_base': 29 | continue 30 | for f in ('one', 'two', 'three'): 31 | self.write_file(os.path.join(path, f)) 32 | 33 | # let's run the command 34 | cmd.all = 1 35 | cmd.ensure_finalized() 36 | cmd.run() 37 | 38 | # make sure the files where removed 39 | for _name, path in dirs: 40 | assert not os.path.exists(path), f'{path} was not removed' 41 | 42 | # let's run the command again (should spit warnings but succeed) 43 | cmd.all = 1 44 | cmd.ensure_finalized() 45 | cmd.run() 46 | -------------------------------------------------------------------------------- /distutils/tests/test_cmd.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.cmd.""" 2 | 3 | import os 4 | from distutils import debug 5 | from distutils.cmd import Command 6 | from distutils.dist import Distribution 7 | from distutils.errors import DistutilsOptionError 8 | 9 | import pytest 10 | 11 | 12 | class MyCmd(Command): 13 | def initialize_options(self): 14 | pass 15 | 16 | 17 | @pytest.fixture 18 | def cmd(request): 19 | return MyCmd(Distribution()) 20 | 21 | 22 | class TestCommand: 23 | def test_ensure_string_list(self, cmd): 24 | cmd.not_string_list = ['one', 2, 'three'] 25 | cmd.yes_string_list = ['one', 'two', 'three'] 26 | cmd.not_string_list2 = object() 27 | cmd.yes_string_list2 = 'ok' 28 | cmd.ensure_string_list('yes_string_list') 29 | cmd.ensure_string_list('yes_string_list2') 30 | 31 | with pytest.raises(DistutilsOptionError): 32 | cmd.ensure_string_list('not_string_list') 33 | 34 | with pytest.raises(DistutilsOptionError): 35 | cmd.ensure_string_list('not_string_list2') 36 | 37 | cmd.option1 = 'ok,dok' 38 | cmd.ensure_string_list('option1') 39 | assert cmd.option1 == ['ok', 'dok'] 40 | 41 | cmd.option2 = ['xxx', 'www'] 42 | cmd.ensure_string_list('option2') 43 | 44 | cmd.option3 = ['ok', 2] 45 | with pytest.raises(DistutilsOptionError): 46 | cmd.ensure_string_list('option3') 47 | 48 | def test_make_file(self, cmd): 49 | # making sure it raises when infiles is not a string or a list/tuple 50 | with pytest.raises(TypeError): 51 | cmd.make_file(infiles=True, outfile='', func='func', args=()) 52 | 53 | # making sure execute gets called properly 54 | def _execute(func, args, exec_msg, level): 55 | assert exec_msg == 'generating out from in' 56 | 57 | cmd.force = True 58 | cmd.execute = _execute 59 | cmd.make_file(infiles='in', outfile='out', func='func', args=()) 60 | 61 | def test_dump_options(self, cmd): 62 | msgs = [] 63 | 64 | def _announce(msg, level): 65 | msgs.append(msg) 66 | 67 | cmd.announce = _announce 68 | cmd.option1 = 1 69 | cmd.option2 = 1 70 | cmd.user_options = [('option1', '', ''), ('option2', '', '')] 71 | cmd.dump_options() 72 | 73 | wanted = ["command options for 'MyCmd':", ' option1 = 1', ' option2 = 1'] 74 | assert msgs == wanted 75 | 76 | def test_ensure_string(self, cmd): 77 | cmd.option1 = 'ok' 78 | cmd.ensure_string('option1') 79 | 80 | cmd.option2 = None 81 | cmd.ensure_string('option2', 'xxx') 82 | assert hasattr(cmd, 'option2') 83 | 84 | cmd.option3 = 1 85 | with pytest.raises(DistutilsOptionError): 86 | cmd.ensure_string('option3') 87 | 88 | def test_ensure_filename(self, cmd): 89 | cmd.option1 = __file__ 90 | cmd.ensure_filename('option1') 91 | cmd.option2 = 'xxx' 92 | with pytest.raises(DistutilsOptionError): 93 | cmd.ensure_filename('option2') 94 | 95 | def test_ensure_dirname(self, cmd): 96 | cmd.option1 = os.path.dirname(__file__) or os.curdir 97 | cmd.ensure_dirname('option1') 98 | cmd.option2 = 'xxx' 99 | with pytest.raises(DistutilsOptionError): 100 | cmd.ensure_dirname('option2') 101 | 102 | def test_debug_print(self, cmd, capsys, monkeypatch): 103 | cmd.debug_print('xxx') 104 | assert capsys.readouterr().out == '' 105 | monkeypatch.setattr(debug, 'DEBUG', True) 106 | cmd.debug_print('xxx') 107 | assert capsys.readouterr().out == 'xxx\n' 108 | -------------------------------------------------------------------------------- /distutils/tests/test_config_cmd.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.config.""" 2 | 3 | import os 4 | import sys 5 | from distutils._log import log 6 | from distutils.command.config import config, dump_file 7 | from distutils.tests import missing_compiler_executable, support 8 | 9 | import more_itertools 10 | import path 11 | import pytest 12 | 13 | 14 | @pytest.fixture(autouse=True) 15 | def info_log(request, monkeypatch): 16 | self = request.instance 17 | self._logs = [] 18 | monkeypatch.setattr(log, 'info', self._info) 19 | 20 | 21 | @support.combine_markers 22 | class TestConfig(support.TempdirManager): 23 | def _info(self, msg, *args): 24 | for line in msg.splitlines(): 25 | self._logs.append(line) 26 | 27 | def test_dump_file(self): 28 | this_file = path.Path(__file__).with_suffix('.py') 29 | with this_file.open(encoding='utf-8') as f: 30 | numlines = more_itertools.ilen(f) 31 | 32 | dump_file(this_file, 'I am the header') 33 | assert len(self._logs) == numlines + 1 34 | 35 | @pytest.mark.skipif('platform.system() == "Windows"') 36 | def test_search_cpp(self): 37 | cmd = missing_compiler_executable(['preprocessor']) 38 | if cmd is not None: 39 | self.skipTest(f'The {cmd!r} command is not found') 40 | pkg_dir, dist = self.create_dist() 41 | cmd = config(dist) 42 | cmd._check_compiler() 43 | compiler = cmd.compiler 44 | if sys.platform[:3] == "aix" and "xlc" in compiler.preprocessor[0].lower(): 45 | self.skipTest( 46 | 'xlc: The -E option overrides the -P, -o, and -qsyntaxonly options' 47 | ) 48 | 49 | # simple pattern searches 50 | match = cmd.search_cpp(pattern='xxx', body='/* xxx */') 51 | assert match == 0 52 | 53 | match = cmd.search_cpp(pattern='_configtest', body='/* xxx */') 54 | assert match == 1 55 | 56 | def test_finalize_options(self): 57 | # finalize_options does a bit of transformation 58 | # on options 59 | pkg_dir, dist = self.create_dist() 60 | cmd = config(dist) 61 | cmd.include_dirs = f'one{os.pathsep}two' 62 | cmd.libraries = 'one' 63 | cmd.library_dirs = f'three{os.pathsep}four' 64 | cmd.ensure_finalized() 65 | 66 | assert cmd.include_dirs == ['one', 'two'] 67 | assert cmd.libraries == ['one'] 68 | assert cmd.library_dirs == ['three', 'four'] 69 | 70 | def test_clean(self): 71 | # _clean removes files 72 | tmp_dir = self.mkdtemp() 73 | f1 = os.path.join(tmp_dir, 'one') 74 | f2 = os.path.join(tmp_dir, 'two') 75 | 76 | self.write_file(f1, 'xxx') 77 | self.write_file(f2, 'xxx') 78 | 79 | for f in (f1, f2): 80 | assert os.path.exists(f) 81 | 82 | pkg_dir, dist = self.create_dist() 83 | cmd = config(dist) 84 | cmd._clean(f1, f2) 85 | 86 | for f in (f1, f2): 87 | assert not os.path.exists(f) 88 | -------------------------------------------------------------------------------- /distutils/tests/test_core.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.core.""" 2 | 3 | import distutils.core 4 | import io 5 | import os 6 | import sys 7 | from distutils.dist import Distribution 8 | 9 | import pytest 10 | 11 | # setup script that uses __file__ 12 | setup_using___file__ = """\ 13 | 14 | __file__ 15 | 16 | from distutils.core import setup 17 | setup() 18 | """ 19 | 20 | setup_prints_cwd = """\ 21 | 22 | import os 23 | print(os.getcwd()) 24 | 25 | from distutils.core import setup 26 | setup() 27 | """ 28 | 29 | setup_does_nothing = """\ 30 | from distutils.core import setup 31 | setup() 32 | """ 33 | 34 | 35 | setup_defines_subclass = """\ 36 | from distutils.core import setup 37 | from distutils.command.install import install as _install 38 | 39 | class install(_install): 40 | sub_commands = _install.sub_commands + ['cmd'] 41 | 42 | setup(cmdclass={'install': install}) 43 | """ 44 | 45 | setup_within_if_main = """\ 46 | from distutils.core import setup 47 | 48 | def main(): 49 | return setup(name="setup_within_if_main") 50 | 51 | if __name__ == "__main__": 52 | main() 53 | """ 54 | 55 | 56 | @pytest.fixture(autouse=True) 57 | def save_stdout(monkeypatch): 58 | monkeypatch.setattr(sys, 'stdout', sys.stdout) 59 | 60 | 61 | @pytest.fixture 62 | def temp_file(tmp_path): 63 | return tmp_path / 'file' 64 | 65 | 66 | @pytest.mark.usefixtures('save_env') 67 | @pytest.mark.usefixtures('save_argv') 68 | class TestCore: 69 | def test_run_setup_provides_file(self, temp_file): 70 | # Make sure the script can use __file__; if that's missing, the test 71 | # setup.py script will raise NameError. 72 | temp_file.write_text(setup_using___file__, encoding='utf-8') 73 | distutils.core.run_setup(temp_file) 74 | 75 | def test_run_setup_preserves_sys_argv(self, temp_file): 76 | # Make sure run_setup does not clobber sys.argv 77 | argv_copy = sys.argv.copy() 78 | temp_file.write_text(setup_does_nothing, encoding='utf-8') 79 | distutils.core.run_setup(temp_file) 80 | assert sys.argv == argv_copy 81 | 82 | def test_run_setup_defines_subclass(self, temp_file): 83 | # Make sure the script can use __file__; if that's missing, the test 84 | # setup.py script will raise NameError. 85 | temp_file.write_text(setup_defines_subclass, encoding='utf-8') 86 | dist = distutils.core.run_setup(temp_file) 87 | install = dist.get_command_obj('install') 88 | assert 'cmd' in install.sub_commands 89 | 90 | def test_run_setup_uses_current_dir(self, tmp_path): 91 | """ 92 | Test that the setup script is run with the current directory 93 | as its own current directory. 94 | """ 95 | sys.stdout = io.StringIO() 96 | cwd = os.getcwd() 97 | 98 | # Create a directory and write the setup.py file there: 99 | setup_py = tmp_path / 'setup.py' 100 | setup_py.write_text(setup_prints_cwd, encoding='utf-8') 101 | distutils.core.run_setup(setup_py) 102 | 103 | output = sys.stdout.getvalue() 104 | if output.endswith("\n"): 105 | output = output[:-1] 106 | assert cwd == output 107 | 108 | def test_run_setup_within_if_main(self, temp_file): 109 | temp_file.write_text(setup_within_if_main, encoding='utf-8') 110 | dist = distutils.core.run_setup(temp_file, stop_after="config") 111 | assert isinstance(dist, Distribution) 112 | assert dist.get_name() == "setup_within_if_main" 113 | 114 | def test_run_commands(self, temp_file): 115 | sys.argv = ['setup.py', 'build'] 116 | temp_file.write_text(setup_within_if_main, encoding='utf-8') 117 | dist = distutils.core.run_setup(temp_file, stop_after="commandline") 118 | assert 'build' not in dist.have_run 119 | distutils.core.run_commands(dist) 120 | assert 'build' in dist.have_run 121 | 122 | def test_debug_mode(self, capsys, monkeypatch): 123 | # this covers the code called when DEBUG is set 124 | sys.argv = ['setup.py', '--name'] 125 | distutils.core.setup(name='bar') 126 | assert capsys.readouterr().out == 'bar\n' 127 | monkeypatch.setattr(distutils.core, 'DEBUG', True) 128 | distutils.core.setup(name='bar') 129 | wanted = "options (after parsing config files):\n" 130 | assert capsys.readouterr().out.startswith(wanted) 131 | -------------------------------------------------------------------------------- /distutils/tests/test_dir_util.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.dir_util.""" 2 | 3 | import os 4 | import pathlib 5 | import stat 6 | import sys 7 | import unittest.mock as mock 8 | from distutils import dir_util, errors 9 | from distutils.dir_util import ( 10 | copy_tree, 11 | create_tree, 12 | ensure_relative, 13 | mkpath, 14 | remove_tree, 15 | ) 16 | from distutils.tests import support 17 | 18 | import jaraco.path 19 | import path 20 | import pytest 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def stuff(request, monkeypatch, distutils_managed_tempdir): 25 | self = request.instance 26 | tmp_dir = self.mkdtemp() 27 | self.root_target = os.path.join(tmp_dir, 'deep') 28 | self.target = os.path.join(self.root_target, 'here') 29 | self.target2 = os.path.join(tmp_dir, 'deep2') 30 | 31 | 32 | class TestDirUtil(support.TempdirManager): 33 | def test_mkpath_remove_tree_verbosity(self, caplog): 34 | mkpath(self.target, verbose=False) 35 | assert not caplog.records 36 | remove_tree(self.root_target, verbose=False) 37 | 38 | mkpath(self.target, verbose=True) 39 | wanted = [f'creating {self.target}'] 40 | assert caplog.messages == wanted 41 | caplog.clear() 42 | 43 | remove_tree(self.root_target, verbose=True) 44 | wanted = [f"removing '{self.root_target}' (and everything under it)"] 45 | assert caplog.messages == wanted 46 | 47 | @pytest.mark.skipif("platform.system() == 'Windows'") 48 | def test_mkpath_with_custom_mode(self): 49 | # Get and set the current umask value for testing mode bits. 50 | umask = os.umask(0o002) 51 | os.umask(umask) 52 | mkpath(self.target, 0o700) 53 | assert stat.S_IMODE(os.stat(self.target).st_mode) == 0o700 & ~umask 54 | mkpath(self.target2, 0o555) 55 | assert stat.S_IMODE(os.stat(self.target2).st_mode) == 0o555 & ~umask 56 | 57 | def test_create_tree_verbosity(self, caplog): 58 | create_tree(self.root_target, ['one', 'two', 'three'], verbose=False) 59 | assert caplog.messages == [] 60 | remove_tree(self.root_target, verbose=False) 61 | 62 | wanted = [f'creating {self.root_target}'] 63 | create_tree(self.root_target, ['one', 'two', 'three'], verbose=True) 64 | assert caplog.messages == wanted 65 | 66 | remove_tree(self.root_target, verbose=False) 67 | 68 | def test_copy_tree_verbosity(self, caplog): 69 | mkpath(self.target, verbose=False) 70 | 71 | copy_tree(self.target, self.target2, verbose=False) 72 | assert caplog.messages == [] 73 | 74 | remove_tree(self.root_target, verbose=False) 75 | 76 | mkpath(self.target, verbose=False) 77 | a_file = path.Path(self.target) / 'ok.txt' 78 | jaraco.path.build({'ok.txt': 'some content'}, self.target) 79 | 80 | wanted = [f'copying {a_file} -> {self.target2}'] 81 | copy_tree(self.target, self.target2, verbose=True) 82 | assert caplog.messages == wanted 83 | 84 | remove_tree(self.root_target, verbose=False) 85 | remove_tree(self.target2, verbose=False) 86 | 87 | def test_copy_tree_skips_nfs_temp_files(self): 88 | mkpath(self.target, verbose=False) 89 | 90 | jaraco.path.build({'ok.txt': 'some content', '.nfs123abc': ''}, self.target) 91 | 92 | copy_tree(self.target, self.target2) 93 | assert os.listdir(self.target2) == ['ok.txt'] 94 | 95 | remove_tree(self.root_target, verbose=False) 96 | remove_tree(self.target2, verbose=False) 97 | 98 | def test_ensure_relative(self): 99 | if os.sep == '/': 100 | assert ensure_relative('/home/foo') == 'home/foo' 101 | assert ensure_relative('some/path') == 'some/path' 102 | else: # \\ 103 | assert ensure_relative('c:\\home\\foo') == 'c:home\\foo' 104 | assert ensure_relative('home\\foo') == 'home\\foo' 105 | 106 | def test_copy_tree_exception_in_listdir(self): 107 | """ 108 | An exception in listdir should raise a DistutilsFileError 109 | """ 110 | with ( 111 | mock.patch("os.listdir", side_effect=OSError()), 112 | pytest.raises(errors.DistutilsFileError), 113 | ): 114 | src = self.tempdirs[-1] 115 | dir_util.copy_tree(src, None) 116 | 117 | def test_mkpath_exception_uncached(self, monkeypatch, tmp_path): 118 | """ 119 | Caching should not remember failed attempts. 120 | 121 | pypa/distutils#304 122 | """ 123 | 124 | class FailPath(pathlib.Path): 125 | def mkdir(self, *args, **kwargs): 126 | raise OSError("Failed to create directory") 127 | 128 | if sys.version_info < (3, 12): 129 | _flavour = pathlib.Path()._flavour 130 | 131 | target = tmp_path / 'foodir' 132 | 133 | with pytest.raises(errors.DistutilsFileError): 134 | mkpath(FailPath(target)) 135 | 136 | assert not target.exists() 137 | 138 | mkpath(target) 139 | assert target.exists() 140 | -------------------------------------------------------------------------------- /distutils/tests/test_extension.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.extension.""" 2 | 3 | import os 4 | import pathlib 5 | import warnings 6 | from distutils.extension import Extension, read_setup_file 7 | 8 | import pytest 9 | from test.support.warnings_helper import check_warnings 10 | 11 | 12 | class TestExtension: 13 | def test_read_setup_file(self): 14 | # trying to read a Setup file 15 | # (sample extracted from the PyGame project) 16 | setup = os.path.join(os.path.dirname(__file__), 'Setup.sample') 17 | 18 | exts = read_setup_file(setup) 19 | names = [ext.name for ext in exts] 20 | names.sort() 21 | 22 | # here are the extensions read_setup_file should have created 23 | # out of the file 24 | wanted = [ 25 | '_arraysurfarray', 26 | '_camera', 27 | '_numericsndarray', 28 | '_numericsurfarray', 29 | 'base', 30 | 'bufferproxy', 31 | 'cdrom', 32 | 'color', 33 | 'constants', 34 | 'display', 35 | 'draw', 36 | 'event', 37 | 'fastevent', 38 | 'font', 39 | 'gfxdraw', 40 | 'image', 41 | 'imageext', 42 | 'joystick', 43 | 'key', 44 | 'mask', 45 | 'mixer', 46 | 'mixer_music', 47 | 'mouse', 48 | 'movie', 49 | 'overlay', 50 | 'pixelarray', 51 | 'pypm', 52 | 'rect', 53 | 'rwobject', 54 | 'scrap', 55 | 'surface', 56 | 'surflock', 57 | 'time', 58 | 'transform', 59 | ] 60 | 61 | assert names == wanted 62 | 63 | def test_extension_init(self): 64 | # the first argument, which is the name, must be a string 65 | with pytest.raises(TypeError): 66 | Extension(1, []) 67 | ext = Extension('name', []) 68 | assert ext.name == 'name' 69 | 70 | # the second argument, which is the list of files, must 71 | # be an iterable of strings or PathLike objects, and not a string 72 | with pytest.raises(TypeError): 73 | Extension('name', 'file') 74 | with pytest.raises(TypeError): 75 | Extension('name', ['file', 1]) 76 | ext = Extension('name', ['file1', 'file2']) 77 | assert ext.sources == ['file1', 'file2'] 78 | ext = Extension('name', [pathlib.Path('file1'), pathlib.Path('file2')]) 79 | assert ext.sources == ['file1', 'file2'] 80 | 81 | # any non-string iterable of strings or PathLike objects should work 82 | ext = Extension('name', ('file1', 'file2')) # tuple 83 | assert ext.sources == ['file1', 'file2'] 84 | ext = Extension('name', {'file1', 'file2'}) # set 85 | assert sorted(ext.sources) == ['file1', 'file2'] 86 | ext = Extension('name', iter(['file1', 'file2'])) # iterator 87 | assert ext.sources == ['file1', 'file2'] 88 | ext = Extension('name', [pathlib.Path('file1'), 'file2']) # mixed types 89 | assert ext.sources == ['file1', 'file2'] 90 | 91 | # others arguments have defaults 92 | for attr in ( 93 | 'include_dirs', 94 | 'define_macros', 95 | 'undef_macros', 96 | 'library_dirs', 97 | 'libraries', 98 | 'runtime_library_dirs', 99 | 'extra_objects', 100 | 'extra_compile_args', 101 | 'extra_link_args', 102 | 'export_symbols', 103 | 'swig_opts', 104 | 'depends', 105 | ): 106 | assert getattr(ext, attr) == [] 107 | 108 | assert ext.language is None 109 | assert ext.optional is None 110 | 111 | # if there are unknown keyword options, warn about them 112 | with check_warnings() as w: 113 | warnings.simplefilter('always') 114 | ext = Extension('name', ['file1', 'file2'], chic=True) 115 | 116 | assert len(w.warnings) == 1 117 | assert str(w.warnings[0].message) == "Unknown Extension options: 'chic'" 118 | -------------------------------------------------------------------------------- /distutils/tests/test_file_util.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.file_util.""" 2 | 3 | import errno 4 | import os 5 | import unittest.mock as mock 6 | from distutils.errors import DistutilsFileError 7 | from distutils.file_util import copy_file, move_file 8 | 9 | import jaraco.path 10 | import pytest 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def stuff(request, tmp_path): 15 | self = request.instance 16 | self.source = tmp_path / 'f1' 17 | self.target = tmp_path / 'f2' 18 | self.target_dir = tmp_path / 'd1' 19 | 20 | 21 | class TestFileUtil: 22 | def test_move_file_verbosity(self, caplog): 23 | jaraco.path.build({self.source: 'some content'}) 24 | 25 | move_file(self.source, self.target, verbose=False) 26 | assert not caplog.messages 27 | 28 | # back to original state 29 | move_file(self.target, self.source, verbose=False) 30 | 31 | move_file(self.source, self.target, verbose=True) 32 | wanted = [f'moving {self.source} -> {self.target}'] 33 | assert caplog.messages == wanted 34 | 35 | # back to original state 36 | move_file(self.target, self.source, verbose=False) 37 | 38 | caplog.clear() 39 | # now the target is a dir 40 | os.mkdir(self.target_dir) 41 | move_file(self.source, self.target_dir, verbose=True) 42 | wanted = [f'moving {self.source} -> {self.target_dir}'] 43 | assert caplog.messages == wanted 44 | 45 | def test_move_file_exception_unpacking_rename(self): 46 | # see issue 22182 47 | with ( 48 | mock.patch("os.rename", side_effect=OSError("wrong", 1)), 49 | pytest.raises(DistutilsFileError), 50 | ): 51 | jaraco.path.build({self.source: 'spam eggs'}) 52 | move_file(self.source, self.target, verbose=False) 53 | 54 | def test_move_file_exception_unpacking_unlink(self): 55 | # see issue 22182 56 | with ( 57 | mock.patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")), 58 | mock.patch("os.unlink", side_effect=OSError("wrong", 1)), 59 | pytest.raises(DistutilsFileError), 60 | ): 61 | jaraco.path.build({self.source: 'spam eggs'}) 62 | move_file(self.source, self.target, verbose=False) 63 | 64 | def test_copy_file_hard_link(self): 65 | jaraco.path.build({self.source: 'some content'}) 66 | # Check first that copy_file() will not fall back on copying the file 67 | # instead of creating the hard link. 68 | try: 69 | os.link(self.source, self.target) 70 | except OSError as e: 71 | self.skipTest(f'os.link: {e}') 72 | else: 73 | self.target.unlink() 74 | st = os.stat(self.source) 75 | copy_file(self.source, self.target, link='hard') 76 | st2 = os.stat(self.source) 77 | st3 = os.stat(self.target) 78 | assert os.path.samestat(st, st2), (st, st2) 79 | assert os.path.samestat(st2, st3), (st2, st3) 80 | assert self.source.read_text(encoding='utf-8') == 'some content' 81 | 82 | def test_copy_file_hard_link_failure(self): 83 | # If hard linking fails, copy_file() falls back on copying file 84 | # (some special filesystems don't support hard linking even under 85 | # Unix, see issue #8876). 86 | jaraco.path.build({self.source: 'some content'}) 87 | st = os.stat(self.source) 88 | with mock.patch("os.link", side_effect=OSError(0, "linking unsupported")): 89 | copy_file(self.source, self.target, link='hard') 90 | st2 = os.stat(self.source) 91 | st3 = os.stat(self.target) 92 | assert os.path.samestat(st, st2), (st, st2) 93 | assert not os.path.samestat(st2, st3), (st2, st3) 94 | for fn in (self.source, self.target): 95 | assert fn.read_text(encoding='utf-8') == 'some content' 96 | -------------------------------------------------------------------------------- /distutils/tests/test_install_data.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.install_data.""" 2 | 3 | import os 4 | import pathlib 5 | from distutils.command.install_data import install_data 6 | from distutils.tests import support 7 | 8 | import pytest 9 | 10 | 11 | @pytest.mark.usefixtures('save_env') 12 | class TestInstallData( 13 | support.TempdirManager, 14 | ): 15 | def test_simple_run(self): 16 | pkg_dir, dist = self.create_dist() 17 | cmd = install_data(dist) 18 | cmd.install_dir = inst = os.path.join(pkg_dir, 'inst') 19 | 20 | # data_files can contain 21 | # - simple files 22 | # - a Path object 23 | # - a tuple with a path, and a list of file 24 | one = os.path.join(pkg_dir, 'one') 25 | self.write_file(one, 'xxx') 26 | inst2 = os.path.join(pkg_dir, 'inst2') 27 | two = os.path.join(pkg_dir, 'two') 28 | self.write_file(two, 'xxx') 29 | three = pathlib.Path(pkg_dir) / 'three' 30 | self.write_file(three, 'xxx') 31 | 32 | cmd.data_files = [one, (inst2, [two]), three] 33 | assert cmd.get_inputs() == [one, (inst2, [two]), three] 34 | 35 | # let's run the command 36 | cmd.ensure_finalized() 37 | cmd.run() 38 | 39 | # let's check the result 40 | assert len(cmd.get_outputs()) == 3 41 | rthree = os.path.split(one)[-1] 42 | assert os.path.exists(os.path.join(inst, rthree)) 43 | rtwo = os.path.split(two)[-1] 44 | assert os.path.exists(os.path.join(inst2, rtwo)) 45 | rone = os.path.split(one)[-1] 46 | assert os.path.exists(os.path.join(inst, rone)) 47 | cmd.outfiles = [] 48 | 49 | # let's try with warn_dir one 50 | cmd.warn_dir = True 51 | cmd.ensure_finalized() 52 | cmd.run() 53 | 54 | # let's check the result 55 | assert len(cmd.get_outputs()) == 3 56 | assert os.path.exists(os.path.join(inst, rthree)) 57 | assert os.path.exists(os.path.join(inst2, rtwo)) 58 | assert os.path.exists(os.path.join(inst, rone)) 59 | cmd.outfiles = [] 60 | 61 | # now using root and empty dir 62 | cmd.root = os.path.join(pkg_dir, 'root') 63 | inst5 = os.path.join(pkg_dir, 'inst5') 64 | four = os.path.join(cmd.install_dir, 'four') 65 | self.write_file(four, 'xx') 66 | cmd.data_files = [one, (inst2, [two]), three, ('inst5', [four]), (inst5, [])] 67 | cmd.ensure_finalized() 68 | cmd.run() 69 | 70 | # let's check the result 71 | assert len(cmd.get_outputs()) == 5 72 | assert os.path.exists(os.path.join(inst, rthree)) 73 | assert os.path.exists(os.path.join(inst2, rtwo)) 74 | assert os.path.exists(os.path.join(inst, rone)) 75 | -------------------------------------------------------------------------------- /distutils/tests/test_install_headers.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.install_headers.""" 2 | 3 | import os 4 | from distutils.command.install_headers import install_headers 5 | from distutils.tests import support 6 | 7 | import pytest 8 | 9 | 10 | @pytest.mark.usefixtures('save_env') 11 | class TestInstallHeaders( 12 | support.TempdirManager, 13 | ): 14 | def test_simple_run(self): 15 | # we have two headers 16 | header_list = self.mkdtemp() 17 | header1 = os.path.join(header_list, 'header1') 18 | header2 = os.path.join(header_list, 'header2') 19 | self.write_file(header1) 20 | self.write_file(header2) 21 | headers = [header1, header2] 22 | 23 | pkg_dir, dist = self.create_dist(headers=headers) 24 | cmd = install_headers(dist) 25 | assert cmd.get_inputs() == headers 26 | 27 | # let's run the command 28 | cmd.install_dir = os.path.join(pkg_dir, 'inst') 29 | cmd.ensure_finalized() 30 | cmd.run() 31 | 32 | # let's check the results 33 | assert len(cmd.get_outputs()) == 2 34 | -------------------------------------------------------------------------------- /distutils/tests/test_install_lib.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.install_data.""" 2 | 3 | import importlib.util 4 | import os 5 | import sys 6 | from distutils.command.install_lib import install_lib 7 | from distutils.errors import DistutilsOptionError 8 | from distutils.extension import Extension 9 | from distutils.tests import support 10 | 11 | import pytest 12 | 13 | 14 | @support.combine_markers 15 | @pytest.mark.usefixtures('save_env') 16 | class TestInstallLib( 17 | support.TempdirManager, 18 | ): 19 | def test_finalize_options(self): 20 | dist = self.create_dist()[1] 21 | cmd = install_lib(dist) 22 | 23 | cmd.finalize_options() 24 | assert cmd.compile == 1 25 | assert cmd.optimize == 0 26 | 27 | # optimize must be 0, 1, or 2 28 | cmd.optimize = 'foo' 29 | with pytest.raises(DistutilsOptionError): 30 | cmd.finalize_options() 31 | cmd.optimize = '4' 32 | with pytest.raises(DistutilsOptionError): 33 | cmd.finalize_options() 34 | 35 | cmd.optimize = '2' 36 | cmd.finalize_options() 37 | assert cmd.optimize == 2 38 | 39 | @pytest.mark.skipif('sys.dont_write_bytecode') 40 | def test_byte_compile(self): 41 | project_dir, dist = self.create_dist() 42 | os.chdir(project_dir) 43 | cmd = install_lib(dist) 44 | cmd.compile = cmd.optimize = 1 45 | 46 | f = os.path.join(project_dir, 'foo.py') 47 | self.write_file(f, '# python file') 48 | cmd.byte_compile([f]) 49 | pyc_file = importlib.util.cache_from_source('foo.py', optimization='') 50 | pyc_opt_file = importlib.util.cache_from_source( 51 | 'foo.py', optimization=cmd.optimize 52 | ) 53 | assert os.path.exists(pyc_file) 54 | assert os.path.exists(pyc_opt_file) 55 | 56 | def test_get_outputs(self): 57 | project_dir, dist = self.create_dist() 58 | os.chdir(project_dir) 59 | os.mkdir('spam') 60 | cmd = install_lib(dist) 61 | 62 | # setting up a dist environment 63 | cmd.compile = cmd.optimize = 1 64 | cmd.install_dir = self.mkdtemp() 65 | f = os.path.join(project_dir, 'spam', '__init__.py') 66 | self.write_file(f, '# python package') 67 | cmd.distribution.ext_modules = [Extension('foo', ['xxx'])] 68 | cmd.distribution.packages = ['spam'] 69 | cmd.distribution.script_name = 'setup.py' 70 | 71 | # get_outputs should return 4 elements: spam/__init__.py and .pyc, 72 | # foo.import-tag-abiflags.so / foo.pyd 73 | outputs = cmd.get_outputs() 74 | assert len(outputs) == 4, outputs 75 | 76 | def test_get_inputs(self): 77 | project_dir, dist = self.create_dist() 78 | os.chdir(project_dir) 79 | os.mkdir('spam') 80 | cmd = install_lib(dist) 81 | 82 | # setting up a dist environment 83 | cmd.compile = cmd.optimize = 1 84 | cmd.install_dir = self.mkdtemp() 85 | f = os.path.join(project_dir, 'spam', '__init__.py') 86 | self.write_file(f, '# python package') 87 | cmd.distribution.ext_modules = [Extension('foo', ['xxx'])] 88 | cmd.distribution.packages = ['spam'] 89 | cmd.distribution.script_name = 'setup.py' 90 | 91 | # get_inputs should return 2 elements: spam/__init__.py and 92 | # foo.import-tag-abiflags.so / foo.pyd 93 | inputs = cmd.get_inputs() 94 | assert len(inputs) == 2, inputs 95 | 96 | def test_dont_write_bytecode(self, caplog): 97 | # makes sure byte_compile is not used 98 | dist = self.create_dist()[1] 99 | cmd = install_lib(dist) 100 | cmd.compile = True 101 | cmd.optimize = 1 102 | 103 | old_dont_write_bytecode = sys.dont_write_bytecode 104 | sys.dont_write_bytecode = True 105 | try: 106 | cmd.byte_compile([]) 107 | finally: 108 | sys.dont_write_bytecode = old_dont_write_bytecode 109 | 110 | assert 'byte-compiling is disabled' in caplog.messages[0] 111 | -------------------------------------------------------------------------------- /distutils/tests/test_install_scripts.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.command.install_scripts.""" 2 | 3 | import os 4 | from distutils.command.install_scripts import install_scripts 5 | from distutils.core import Distribution 6 | from distutils.tests import support 7 | 8 | from . import test_build_scripts 9 | 10 | 11 | class TestInstallScripts(support.TempdirManager): 12 | def test_default_settings(self): 13 | dist = Distribution() 14 | dist.command_obj["build"] = support.DummyCommand(build_scripts="/foo/bar") 15 | dist.command_obj["install"] = support.DummyCommand( 16 | install_scripts="/splat/funk", 17 | force=True, 18 | skip_build=True, 19 | ) 20 | cmd = install_scripts(dist) 21 | assert not cmd.force 22 | assert not cmd.skip_build 23 | assert cmd.build_dir is None 24 | assert cmd.install_dir is None 25 | 26 | cmd.finalize_options() 27 | 28 | assert cmd.force 29 | assert cmd.skip_build 30 | assert cmd.build_dir == "/foo/bar" 31 | assert cmd.install_dir == "/splat/funk" 32 | 33 | def test_installation(self): 34 | source = self.mkdtemp() 35 | 36 | expected = test_build_scripts.TestBuildScripts.write_sample_scripts(source) 37 | 38 | target = self.mkdtemp() 39 | dist = Distribution() 40 | dist.command_obj["build"] = support.DummyCommand(build_scripts=source) 41 | dist.command_obj["install"] = support.DummyCommand( 42 | install_scripts=target, 43 | force=True, 44 | skip_build=True, 45 | ) 46 | cmd = install_scripts(dist) 47 | cmd.finalize_options() 48 | cmd.run() 49 | 50 | installed = os.listdir(target) 51 | for name in expected: 52 | assert name in installed 53 | -------------------------------------------------------------------------------- /distutils/tests/test_log.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.log""" 2 | 3 | import logging 4 | from distutils._log import log 5 | 6 | 7 | class TestLog: 8 | def test_non_ascii(self, caplog): 9 | caplog.set_level(logging.DEBUG) 10 | log.debug('Dεbug\tMėssãge') 11 | log.fatal('Fαtal\tÈrrōr') 12 | assert caplog.messages == ['Dεbug\tMėssãge', 'Fαtal\tÈrrōr'] 13 | -------------------------------------------------------------------------------- /distutils/tests/test_modified.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils._modified.""" 2 | 3 | import os 4 | import types 5 | from distutils._modified import newer, newer_group, newer_pairwise, newer_pairwise_group 6 | from distutils.errors import DistutilsFileError 7 | from distutils.tests import support 8 | 9 | import pytest 10 | 11 | 12 | class TestDepUtil(support.TempdirManager): 13 | def test_newer(self): 14 | tmpdir = self.mkdtemp() 15 | new_file = os.path.join(tmpdir, 'new') 16 | old_file = os.path.abspath(__file__) 17 | 18 | # Raise DistutilsFileError if 'new_file' does not exist. 19 | with pytest.raises(DistutilsFileError): 20 | newer(new_file, old_file) 21 | 22 | # Return true if 'new_file' exists and is more recently modified than 23 | # 'old_file', or if 'new_file' exists and 'old_file' doesn't. 24 | self.write_file(new_file) 25 | assert newer(new_file, 'I_dont_exist') 26 | assert newer(new_file, old_file) 27 | 28 | # Return false if both exist and 'old_file' is the same age or younger 29 | # than 'new_file'. 30 | assert not newer(old_file, new_file) 31 | 32 | def _setup_1234(self): 33 | tmpdir = self.mkdtemp() 34 | sources = os.path.join(tmpdir, 'sources') 35 | targets = os.path.join(tmpdir, 'targets') 36 | os.mkdir(sources) 37 | os.mkdir(targets) 38 | one = os.path.join(sources, 'one') 39 | two = os.path.join(sources, 'two') 40 | three = os.path.abspath(__file__) # I am the old file 41 | four = os.path.join(targets, 'four') 42 | self.write_file(one) 43 | self.write_file(two) 44 | self.write_file(four) 45 | return one, two, three, four 46 | 47 | def test_newer_pairwise(self): 48 | one, two, three, four = self._setup_1234() 49 | 50 | assert newer_pairwise([one, two], [three, four]) == ([one], [three]) 51 | 52 | def test_newer_pairwise_mismatch(self): 53 | one, two, three, four = self._setup_1234() 54 | 55 | with pytest.raises(ValueError): 56 | newer_pairwise([one], [three, four]) 57 | 58 | with pytest.raises(ValueError): 59 | newer_pairwise([one, two], [three]) 60 | 61 | def test_newer_pairwise_empty(self): 62 | assert newer_pairwise([], []) == ([], []) 63 | 64 | def test_newer_pairwise_fresh(self): 65 | one, two, three, four = self._setup_1234() 66 | 67 | assert newer_pairwise([one, three], [two, four]) == ([], []) 68 | 69 | def test_newer_group(self): 70 | tmpdir = self.mkdtemp() 71 | sources = os.path.join(tmpdir, 'sources') 72 | os.mkdir(sources) 73 | one = os.path.join(sources, 'one') 74 | two = os.path.join(sources, 'two') 75 | three = os.path.join(sources, 'three') 76 | old_file = os.path.abspath(__file__) 77 | 78 | # return true if 'old_file' is out-of-date with respect to any file 79 | # listed in 'sources'. 80 | self.write_file(one) 81 | self.write_file(two) 82 | self.write_file(three) 83 | assert newer_group([one, two, three], old_file) 84 | assert not newer_group([one, two, old_file], three) 85 | 86 | # missing handling 87 | os.remove(one) 88 | with pytest.raises(OSError): 89 | newer_group([one, two, old_file], three) 90 | 91 | assert not newer_group([one, two, old_file], three, missing='ignore') 92 | 93 | assert newer_group([one, two, old_file], three, missing='newer') 94 | 95 | 96 | @pytest.fixture 97 | def groups_target(tmp_path): 98 | """ 99 | Set up some older sources, a target, and newer sources. 100 | 101 | Returns a simple namespace with these values. 102 | """ 103 | filenames = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] 104 | paths = [tmp_path / name for name in filenames] 105 | 106 | for mtime, path in enumerate(paths): 107 | path.write_text('', encoding='utf-8') 108 | 109 | # make sure modification times are sequential 110 | os.utime(path, (mtime, mtime)) 111 | 112 | return types.SimpleNamespace(older=paths[:2], target=paths[2], newer=paths[3:]) 113 | 114 | 115 | def test_newer_pairwise_group(groups_target): 116 | older = newer_pairwise_group([groups_target.older], [groups_target.target]) 117 | newer = newer_pairwise_group([groups_target.newer], [groups_target.target]) 118 | assert older == ([], []) 119 | assert newer == ([groups_target.newer], [groups_target.target]) 120 | 121 | 122 | def test_newer_group_no_sources_no_target(tmp_path): 123 | """ 124 | Consider no sources and no target "newer". 125 | """ 126 | assert newer_group([], str(tmp_path / 'does-not-exist')) 127 | -------------------------------------------------------------------------------- /distutils/tests/test_spawn.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.spawn.""" 2 | 3 | import os 4 | import stat 5 | import sys 6 | import unittest.mock as mock 7 | from distutils.errors import DistutilsExecError 8 | from distutils.spawn import find_executable, spawn 9 | from distutils.tests import support 10 | 11 | import path 12 | import pytest 13 | from test.support import unix_shell 14 | 15 | from .compat import py39 as os_helper 16 | 17 | 18 | class TestSpawn(support.TempdirManager): 19 | @pytest.mark.skipif("os.name not in ('nt', 'posix')") 20 | def test_spawn(self): 21 | tmpdir = self.mkdtemp() 22 | 23 | # creating something executable 24 | # through the shell that returns 1 25 | if sys.platform != 'win32': 26 | exe = os.path.join(tmpdir, 'foo.sh') 27 | self.write_file(exe, f'#!{unix_shell}\nexit 1') 28 | else: 29 | exe = os.path.join(tmpdir, 'foo.bat') 30 | self.write_file(exe, 'exit 1') 31 | 32 | os.chmod(exe, 0o777) 33 | with pytest.raises(DistutilsExecError): 34 | spawn([exe]) 35 | 36 | # now something that works 37 | if sys.platform != 'win32': 38 | exe = os.path.join(tmpdir, 'foo.sh') 39 | self.write_file(exe, f'#!{unix_shell}\nexit 0') 40 | else: 41 | exe = os.path.join(tmpdir, 'foo.bat') 42 | self.write_file(exe, 'exit 0') 43 | 44 | os.chmod(exe, 0o777) 45 | spawn([exe]) # should work without any error 46 | 47 | def test_find_executable(self, tmp_path): 48 | program_path = self._make_executable(tmp_path, '.exe') 49 | program = program_path.name 50 | program_noeext = program_path.with_suffix('').name 51 | filename = str(program_path) 52 | tmp_dir = path.Path(tmp_path) 53 | 54 | # test path parameter 55 | rv = find_executable(program, path=tmp_dir) 56 | assert rv == filename 57 | 58 | if sys.platform == 'win32': 59 | # test without ".exe" extension 60 | rv = find_executable(program_noeext, path=tmp_dir) 61 | assert rv == filename 62 | 63 | # test find in the current directory 64 | with tmp_dir: 65 | rv = find_executable(program) 66 | assert rv == program 67 | 68 | # test non-existent program 69 | dont_exist_program = "dontexist_" + program 70 | rv = find_executable(dont_exist_program, path=tmp_dir) 71 | assert rv is None 72 | 73 | # PATH='': no match, except in the current directory 74 | with os_helper.EnvironmentVarGuard() as env: 75 | env['PATH'] = '' 76 | with ( 77 | mock.patch( 78 | 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True 79 | ), 80 | mock.patch('distutils.spawn.os.defpath', tmp_dir), 81 | ): 82 | rv = find_executable(program) 83 | assert rv is None 84 | 85 | # look in current directory 86 | with tmp_dir: 87 | rv = find_executable(program) 88 | assert rv == program 89 | 90 | # PATH=':': explicitly looks in the current directory 91 | with os_helper.EnvironmentVarGuard() as env: 92 | env['PATH'] = os.pathsep 93 | with ( 94 | mock.patch('distutils.spawn.os.confstr', return_value='', create=True), 95 | mock.patch('distutils.spawn.os.defpath', ''), 96 | ): 97 | rv = find_executable(program) 98 | assert rv is None 99 | 100 | # look in current directory 101 | with tmp_dir: 102 | rv = find_executable(program) 103 | assert rv == program 104 | 105 | # missing PATH: test os.confstr("CS_PATH") and os.defpath 106 | with os_helper.EnvironmentVarGuard() as env: 107 | env.pop('PATH', None) 108 | 109 | # without confstr 110 | with ( 111 | mock.patch( 112 | 'distutils.spawn.os.confstr', side_effect=ValueError, create=True 113 | ), 114 | mock.patch('distutils.spawn.os.defpath', tmp_dir), 115 | ): 116 | rv = find_executable(program) 117 | assert rv == filename 118 | 119 | # with confstr 120 | with ( 121 | mock.patch( 122 | 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True 123 | ), 124 | mock.patch('distutils.spawn.os.defpath', ''), 125 | ): 126 | rv = find_executable(program) 127 | assert rv == filename 128 | 129 | @staticmethod 130 | def _make_executable(tmp_path, ext): 131 | # Give the temporary program a suffix regardless of platform. 132 | # It's needed on Windows and not harmful on others. 133 | program = tmp_path.joinpath('program').with_suffix(ext) 134 | program.write_text("", encoding='utf-8') 135 | program.chmod(stat.S_IXUSR) 136 | return program 137 | 138 | def test_spawn_missing_exe(self): 139 | with pytest.raises(DistutilsExecError) as ctx: 140 | spawn(['does-not-exist']) 141 | assert "command 'does-not-exist' failed" in str(ctx.value) 142 | -------------------------------------------------------------------------------- /distutils/tests/test_text_file.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.text_file.""" 2 | 3 | from distutils.tests import support 4 | from distutils.text_file import TextFile 5 | 6 | import jaraco.path 7 | import path 8 | 9 | TEST_DATA = """# test file 10 | 11 | line 3 \\ 12 | # intervening comment 13 | continues on next line 14 | """ 15 | 16 | 17 | class TestTextFile(support.TempdirManager): 18 | def test_class(self): 19 | # old tests moved from text_file.__main__ 20 | # so they are really called by the buildbots 21 | 22 | # result 1: no fancy options 23 | result1 = [ 24 | '# test file\n', 25 | '\n', 26 | 'line 3 \\\n', 27 | '# intervening comment\n', 28 | ' continues on next line\n', 29 | ] 30 | 31 | # result 2: just strip comments 32 | result2 = ["\n", "line 3 \\\n", " continues on next line\n"] 33 | 34 | # result 3: just strip blank lines 35 | result3 = [ 36 | "# test file\n", 37 | "line 3 \\\n", 38 | "# intervening comment\n", 39 | " continues on next line\n", 40 | ] 41 | 42 | # result 4: default, strip comments, blank lines, 43 | # and trailing whitespace 44 | result4 = ["line 3 \\", " continues on next line"] 45 | 46 | # result 5: strip comments and blanks, plus join lines (but don't 47 | # "collapse" joined lines 48 | result5 = ["line 3 continues on next line"] 49 | 50 | # result 6: strip comments and blanks, plus join lines (and 51 | # "collapse" joined lines 52 | result6 = ["line 3 continues on next line"] 53 | 54 | def test_input(count, description, file, expected_result): 55 | result = file.readlines() 56 | assert result == expected_result 57 | 58 | tmp_path = path.Path(self.mkdtemp()) 59 | filename = tmp_path / 'test.txt' 60 | jaraco.path.build({filename.name: TEST_DATA}, tmp_path) 61 | 62 | in_file = TextFile( 63 | filename, 64 | strip_comments=False, 65 | skip_blanks=False, 66 | lstrip_ws=False, 67 | rstrip_ws=False, 68 | ) 69 | try: 70 | test_input(1, "no processing", in_file, result1) 71 | finally: 72 | in_file.close() 73 | 74 | in_file = TextFile( 75 | filename, 76 | strip_comments=True, 77 | skip_blanks=False, 78 | lstrip_ws=False, 79 | rstrip_ws=False, 80 | ) 81 | try: 82 | test_input(2, "strip comments", in_file, result2) 83 | finally: 84 | in_file.close() 85 | 86 | in_file = TextFile( 87 | filename, 88 | strip_comments=False, 89 | skip_blanks=True, 90 | lstrip_ws=False, 91 | rstrip_ws=False, 92 | ) 93 | try: 94 | test_input(3, "strip blanks", in_file, result3) 95 | finally: 96 | in_file.close() 97 | 98 | in_file = TextFile(filename) 99 | try: 100 | test_input(4, "default processing", in_file, result4) 101 | finally: 102 | in_file.close() 103 | 104 | in_file = TextFile( 105 | filename, 106 | strip_comments=True, 107 | skip_blanks=True, 108 | join_lines=True, 109 | rstrip_ws=True, 110 | ) 111 | try: 112 | test_input(5, "join lines without collapsing", in_file, result5) 113 | finally: 114 | in_file.close() 115 | 116 | in_file = TextFile( 117 | filename, 118 | strip_comments=True, 119 | skip_blanks=True, 120 | join_lines=True, 121 | rstrip_ws=True, 122 | collapse_join=True, 123 | ) 124 | try: 125 | test_input(6, "join lines with collapsing", in_file, result6) 126 | finally: 127 | in_file.close() 128 | -------------------------------------------------------------------------------- /distutils/tests/test_version.py: -------------------------------------------------------------------------------- 1 | """Tests for distutils.version.""" 2 | 3 | import distutils 4 | from distutils.version import LooseVersion, StrictVersion 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def suppress_deprecation(): 11 | with distutils.version.suppress_known_deprecation(): 12 | yield 13 | 14 | 15 | class TestVersion: 16 | def test_prerelease(self): 17 | version = StrictVersion('1.2.3a1') 18 | assert version.version == (1, 2, 3) 19 | assert version.prerelease == ('a', 1) 20 | assert str(version) == '1.2.3a1' 21 | 22 | version = StrictVersion('1.2.0') 23 | assert str(version) == '1.2' 24 | 25 | def test_cmp_strict(self): 26 | versions = ( 27 | ('1.5.1', '1.5.2b2', -1), 28 | ('161', '3.10a', ValueError), 29 | ('8.02', '8.02', 0), 30 | ('3.4j', '1996.07.12', ValueError), 31 | ('3.2.pl0', '3.1.1.6', ValueError), 32 | ('2g6', '11g', ValueError), 33 | ('0.9', '2.2', -1), 34 | ('1.2.1', '1.2', 1), 35 | ('1.1', '1.2.2', -1), 36 | ('1.2', '1.1', 1), 37 | ('1.2.1', '1.2.2', -1), 38 | ('1.2.2', '1.2', 1), 39 | ('1.2', '1.2.2', -1), 40 | ('0.4.0', '0.4', 0), 41 | ('1.13++', '5.5.kw', ValueError), 42 | ) 43 | 44 | for v1, v2, wanted in versions: 45 | try: 46 | res = StrictVersion(v1)._cmp(StrictVersion(v2)) 47 | except ValueError: 48 | if wanted is ValueError: 49 | continue 50 | else: 51 | raise AssertionError(f"cmp({v1}, {v2}) shouldn't raise ValueError") 52 | assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' 53 | res = StrictVersion(v1)._cmp(v2) 54 | assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' 55 | res = StrictVersion(v1)._cmp(object()) 56 | assert res is NotImplemented, ( 57 | f'cmp({v1}, {v2}) should be NotImplemented, got {res}' 58 | ) 59 | 60 | def test_cmp(self): 61 | versions = ( 62 | ('1.5.1', '1.5.2b2', -1), 63 | ('161', '3.10a', 1), 64 | ('8.02', '8.02', 0), 65 | ('3.4j', '1996.07.12', -1), 66 | ('3.2.pl0', '3.1.1.6', 1), 67 | ('2g6', '11g', -1), 68 | ('0.960923', '2.2beta29', -1), 69 | ('1.13++', '5.5.kw', -1), 70 | ) 71 | 72 | for v1, v2, wanted in versions: 73 | res = LooseVersion(v1)._cmp(LooseVersion(v2)) 74 | assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' 75 | res = LooseVersion(v1)._cmp(v2) 76 | assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' 77 | res = LooseVersion(v1)._cmp(object()) 78 | assert res is NotImplemented, ( 79 | f'cmp({v1}, {v2}) should be NotImplemented, got {res}' 80 | ) 81 | -------------------------------------------------------------------------------- /distutils/tests/test_versionpredicate.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pypa/distutils/603b94ec2e7876294345e5bceac276496b369641/distutils/tests/test_versionpredicate.py -------------------------------------------------------------------------------- /distutils/tests/unix_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | import grp 5 | import pwd 6 | except ImportError: 7 | grp = pwd = None 8 | 9 | import pytest 10 | 11 | UNIX_ID_SUPPORT = grp and pwd 12 | UID_0_SUPPORT = UNIX_ID_SUPPORT and sys.platform != "cygwin" 13 | 14 | require_unix_id = pytest.mark.skipif( 15 | not UNIX_ID_SUPPORT, reason="Requires grp and pwd support" 16 | ) 17 | require_uid_0 = pytest.mark.skipif(not UID_0_SUPPORT, reason="Requires UID 0 support") 18 | -------------------------------------------------------------------------------- /distutils/unixccompiler.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from .compilers.C import unix 4 | 5 | UnixCCompiler = unix.Compiler 6 | 7 | # ensure import of unixccompiler implies ccompiler imported 8 | # (pypa/setuptools#4871) 9 | importlib.import_module('distutils.ccompiler') 10 | -------------------------------------------------------------------------------- /distutils/versionpredicate.py: -------------------------------------------------------------------------------- 1 | """Module for parsing and testing package version predicate strings.""" 2 | 3 | import operator 4 | import re 5 | 6 | from . import version 7 | 8 | re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)", re.ASCII) 9 | # (package) (rest) 10 | 11 | re_paren = re.compile(r"^\s*\((.*)\)\s*$") # (list) inside of parentheses 12 | re_splitComparison = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$") 13 | # (comp) (version) 14 | 15 | 16 | def splitUp(pred): 17 | """Parse a single version comparison. 18 | 19 | Return (comparison string, StrictVersion) 20 | """ 21 | res = re_splitComparison.match(pred) 22 | if not res: 23 | raise ValueError(f"bad package restriction syntax: {pred!r}") 24 | comp, verStr = res.groups() 25 | with version.suppress_known_deprecation(): 26 | other = version.StrictVersion(verStr) 27 | return (comp, other) 28 | 29 | 30 | compmap = { 31 | "<": operator.lt, 32 | "<=": operator.le, 33 | "==": operator.eq, 34 | ">": operator.gt, 35 | ">=": operator.ge, 36 | "!=": operator.ne, 37 | } 38 | 39 | 40 | class VersionPredicate: 41 | """Parse and test package version predicates. 42 | 43 | >>> v = VersionPredicate('pyepat.abc (>1.0, <3333.3a1, !=1555.1b3)') 44 | 45 | The `name` attribute provides the full dotted name that is given:: 46 | 47 | >>> v.name 48 | 'pyepat.abc' 49 | 50 | The str() of a `VersionPredicate` provides a normalized 51 | human-readable version of the expression:: 52 | 53 | >>> print(v) 54 | pyepat.abc (> 1.0, < 3333.3a1, != 1555.1b3) 55 | 56 | The `satisfied_by()` method can be used to determine with a given 57 | version number is included in the set described by the version 58 | restrictions:: 59 | 60 | >>> v.satisfied_by('1.1') 61 | True 62 | >>> v.satisfied_by('1.4') 63 | True 64 | >>> v.satisfied_by('1.0') 65 | False 66 | >>> v.satisfied_by('4444.4') 67 | False 68 | >>> v.satisfied_by('1555.1b3') 69 | False 70 | 71 | `VersionPredicate` is flexible in accepting extra whitespace:: 72 | 73 | >>> v = VersionPredicate(' pat( == 0.1 ) ') 74 | >>> v.name 75 | 'pat' 76 | >>> v.satisfied_by('0.1') 77 | True 78 | >>> v.satisfied_by('0.2') 79 | False 80 | 81 | If any version numbers passed in do not conform to the 82 | restrictions of `StrictVersion`, a `ValueError` is raised:: 83 | 84 | >>> v = VersionPredicate('p1.p2.p3.p4(>=1.0, <=1.3a1, !=1.2zb3)') 85 | Traceback (most recent call last): 86 | ... 87 | ValueError: invalid version number '1.2zb3' 88 | 89 | It the module or package name given does not conform to what's 90 | allowed as a legal module or package name, `ValueError` is 91 | raised:: 92 | 93 | >>> v = VersionPredicate('foo-bar') 94 | Traceback (most recent call last): 95 | ... 96 | ValueError: expected parenthesized list: '-bar' 97 | 98 | >>> v = VersionPredicate('foo bar (12.21)') 99 | Traceback (most recent call last): 100 | ... 101 | ValueError: expected parenthesized list: 'bar (12.21)' 102 | 103 | """ 104 | 105 | def __init__(self, versionPredicateStr): 106 | """Parse a version predicate string.""" 107 | # Fields: 108 | # name: package name 109 | # pred: list of (comparison string, StrictVersion) 110 | 111 | versionPredicateStr = versionPredicateStr.strip() 112 | if not versionPredicateStr: 113 | raise ValueError("empty package restriction") 114 | match = re_validPackage.match(versionPredicateStr) 115 | if not match: 116 | raise ValueError(f"bad package name in {versionPredicateStr!r}") 117 | self.name, paren = match.groups() 118 | paren = paren.strip() 119 | if paren: 120 | match = re_paren.match(paren) 121 | if not match: 122 | raise ValueError(f"expected parenthesized list: {paren!r}") 123 | str = match.groups()[0] 124 | self.pred = [splitUp(aPred) for aPred in str.split(",")] 125 | if not self.pred: 126 | raise ValueError(f"empty parenthesized list in {versionPredicateStr!r}") 127 | else: 128 | self.pred = [] 129 | 130 | def __str__(self): 131 | if self.pred: 132 | seq = [cond + " " + str(ver) for cond, ver in self.pred] 133 | return self.name + " (" + ", ".join(seq) + ")" 134 | else: 135 | return self.name 136 | 137 | def satisfied_by(self, version): 138 | """True if version is compatible with all the predicates in self. 139 | The parameter version must be acceptable to the StrictVersion 140 | constructor. It may be either a string or StrictVersion. 141 | """ 142 | for cond, ver in self.pred: 143 | if not compmap[cond](version, ver): 144 | return False 145 | return True 146 | 147 | 148 | _provision_rx = None 149 | 150 | 151 | def split_provision(value): 152 | """Return the name and optional version number of a provision. 153 | 154 | The version number, if given, will be returned as a `StrictVersion` 155 | instance, otherwise it will be `None`. 156 | 157 | >>> split_provision('mypkg') 158 | ('mypkg', None) 159 | >>> split_provision(' mypkg( 1.2 ) ') 160 | ('mypkg', StrictVersion ('1.2')) 161 | """ 162 | global _provision_rx 163 | if _provision_rx is None: 164 | _provision_rx = re.compile( 165 | r"([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(?:\s*\(\s*([^)\s]+)\s*\))?$", re.ASCII 166 | ) 167 | value = value.strip() 168 | m = _provision_rx.match(value) 169 | if not m: 170 | raise ValueError(f"illegal provides specification: {value!r}") 171 | ver = m.group(2) or None 172 | if ver: 173 | with version.suppress_known_deprecation(): 174 | ver = version.StrictVersion(ver) 175 | return m.group(1), ver 176 | -------------------------------------------------------------------------------- /distutils/zosccompiler.py: -------------------------------------------------------------------------------- 1 | from .compilers.C import zos 2 | 3 | zOSCCompiler = zos.Compiler 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | extensions = [ 4 | 'sphinx.ext.autodoc', 5 | 'jaraco.packaging.sphinx', 6 | ] 7 | 8 | master_doc = "index" 9 | html_theme = "furo" 10 | 11 | # Link dates and other references in the changelog 12 | extensions += ['rst.linker'] 13 | link_files = { 14 | '../NEWS.rst': dict( 15 | using=dict(GH='https://github.com'), 16 | replace=[ 17 | dict( 18 | pattern=r'(Issue #|\B#)(?P\d+)', 19 | url='{package_url}/issues/{issue}', 20 | ), 21 | dict( 22 | pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', 23 | with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', 24 | ), 25 | dict( 26 | pattern=r'PEP[- ](?P\d+)', 27 | url='https://peps.python.org/pep-{pep_number:0>4}/', 28 | ), 29 | ], 30 | ) 31 | } 32 | 33 | # Be strict about any broken references 34 | nitpicky = True 35 | nitpick_ignore: list[tuple[str, str]] = [] 36 | 37 | # Include Python intersphinx mapping to prevent failures 38 | # jaraco/skeleton#51 39 | extensions += ['sphinx.ext.intersphinx'] 40 | intersphinx_mapping = { 41 | 'python': ('https://docs.python.org/3', None), 42 | } 43 | 44 | # Preserve authored syntax for defaults 45 | autodoc_preserve_defaults = True 46 | 47 | # Add support for linking usernames, PyPI projects, Wikipedia pages 48 | github_url = 'https://github.com/' 49 | extlinks = { 50 | 'user': (f'{github_url}%s', '@%s'), 51 | 'pypi': ('https://pypi.org/project/%s', '%s'), 52 | 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), 53 | } 54 | extensions += ['sphinx.ext.extlinks'] 55 | 56 | # local 57 | 58 | # for distutils, disable nitpicky 59 | nitpicky = False 60 | -------------------------------------------------------------------------------- /docs/distutils/_setuptools_disclaimer.rst: -------------------------------------------------------------------------------- 1 | .. note:: 2 | 3 | This document is being retained solely until the ``setuptools`` documentation 4 | at https://setuptools.readthedocs.io/en/latest/setuptools.html 5 | independently covers all of the relevant information currently included here. 6 | -------------------------------------------------------------------------------- /docs/distutils/commandref.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | ***************** 4 | Command Reference 5 | ***************** 6 | 7 | .. include:: ./_setuptools_disclaimer.rst 8 | 9 | .. % \section{Building modules: the \protect\command{build} command family} 10 | .. % \label{build-cmds} 11 | .. % \subsubsection{\protect\command{build}} 12 | .. % \label{build-cmd} 13 | .. % \subsubsection{\protect\command{build\_py}} 14 | .. % \label{build-py-cmd} 15 | .. % \subsubsection{\protect\command{build\_ext}} 16 | .. % \label{build-ext-cmd} 17 | .. % \subsubsection{\protect\command{build\_clib}} 18 | .. % \label{build-clib-cmd} 19 | 20 | 21 | .. _install-cmd: 22 | 23 | Installing modules: the :command:`install` command family 24 | ========================================================= 25 | 26 | The install command ensures that the build commands have been run and then runs 27 | the subcommands :command:`install_lib`, :command:`install_data` and 28 | :command:`install_scripts`. 29 | 30 | .. % \subsubsection{\protect\command{install\_lib}} 31 | .. % \label{install-lib-cmd} 32 | 33 | 34 | .. _install-data-cmd: 35 | 36 | :command:`install_data` 37 | ----------------------- 38 | 39 | This command installs all data files provided with the distribution. 40 | 41 | 42 | .. _install-scripts-cmd: 43 | 44 | :command:`install_scripts` 45 | -------------------------- 46 | 47 | This command installs all (Python) scripts in the distribution. 48 | 49 | .. % \subsection{Cleaning up: the \protect\command{clean} command} 50 | .. % \label{clean-cmd} 51 | 52 | 53 | .. _sdist-cmd: 54 | 55 | Creating a source distribution: the :command:`sdist` command 56 | ============================================================ 57 | 58 | .. XXX fragment moved down from above: needs context! 59 | 60 | The manifest template commands are: 61 | 62 | +-------------------------------------------+-----------------------------------------------+ 63 | | Command | Description | 64 | +===========================================+===============================================+ 65 | | :command:`include pat1 pat2 ...` | include all files matching any of the listed | 66 | | | patterns | 67 | +-------------------------------------------+-----------------------------------------------+ 68 | | :command:`exclude pat1 pat2 ...` | exclude all files matching any of the listed | 69 | | | patterns | 70 | +-------------------------------------------+-----------------------------------------------+ 71 | | :command:`recursive-include dir pat1 pat2 | include all files under *dir* matching any of | 72 | | ...` | the listed patterns | 73 | +-------------------------------------------+-----------------------------------------------+ 74 | | :command:`recursive-exclude dir pat1 pat2 | exclude all files under *dir* matching any of | 75 | | ...` | the listed patterns | 76 | +-------------------------------------------+-----------------------------------------------+ 77 | | :command:`global-include pat1 pat2 ...` | include all files anywhere in the source tree | 78 | | | matching --- & any of the listed patterns | 79 | +-------------------------------------------+-----------------------------------------------+ 80 | | :command:`global-exclude pat1 pat2 ...` | exclude all files anywhere in the source tree | 81 | | | matching --- & any of the listed patterns | 82 | +-------------------------------------------+-----------------------------------------------+ 83 | | :command:`prune dir` | exclude all files under *dir* | 84 | +-------------------------------------------+-----------------------------------------------+ 85 | | :command:`graft dir` | include all files under *dir* | 86 | +-------------------------------------------+-----------------------------------------------+ 87 | 88 | The patterns here are Unix-style "glob" patterns: ``*`` matches any sequence of 89 | regular filename characters, ``?`` matches any single regular filename 90 | character, and ``[range]`` matches any of the characters in *range* (e.g., 91 | ``a-z``, ``a-zA-Z``, ``a-f0-9_.``). The definition of "regular filename 92 | character" is platform-specific: on Unix it is anything except slash; on Windows 93 | anything except backslash or colon. 94 | 95 | .. XXX Windows support not there yet 96 | 97 | .. % \section{Creating a built distribution: the 98 | .. % \protect\command{bdist} command family} 99 | .. % \label{bdist-cmds} 100 | 101 | .. % \subsection{\protect\command{bdist}} 102 | .. % \subsection{\protect\command{bdist\_dumb}} 103 | .. % \subsection{\protect\command{bdist\_rpm}} 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/distutils/configfile.rst: -------------------------------------------------------------------------------- 1 | .. _setup-config: 2 | 3 | ************************************ 4 | Writing the Setup Configuration File 5 | ************************************ 6 | 7 | .. include:: ./_setuptools_disclaimer.rst 8 | 9 | Often, it's not possible to write down everything needed to build a distribution 10 | *a priori*: you may need to get some information from the user, or from the 11 | user's system, in order to proceed. As long as that information is fairly 12 | simple---a list of directories to search for C header files or libraries, for 13 | example---then providing a configuration file, :file:`setup.cfg`, for users to 14 | edit is a cheap and easy way to solicit it. Configuration files also let you 15 | provide default values for any command option, which the installer can then 16 | override either on the command-line or by editing the config file. 17 | 18 | The setup configuration file is a useful middle-ground between the setup 19 | script---which, ideally, would be opaque to installers [#]_---and the command-line to 20 | the setup script, which is outside of your control and entirely up to the 21 | installer. In fact, :file:`setup.cfg` (and any other Distutils configuration 22 | files present on the target system) are processed after the contents of the 23 | setup script, but before the command-line. This has several useful 24 | consequences: 25 | 26 | .. % (If you have more advanced needs, such as determining which extensions 27 | .. % to build based on what capabilities are present on the target system, 28 | .. % then you need the Distutils ``auto-configuration'' facility. This 29 | .. % started to appear in Distutils 0.9 but, as of this writing, isn't mature 30 | .. % or stable enough yet for real-world use.) 31 | 32 | * installers can override some of what you put in :file:`setup.py` by editing 33 | :file:`setup.cfg` 34 | 35 | * you can provide non-standard defaults for options that are not easily set in 36 | :file:`setup.py` 37 | 38 | * installers can override anything in :file:`setup.cfg` using the command-line 39 | options to :file:`setup.py` or by pointing :envvar:`DIST_EXTRA_CONFIG` 40 | to another configuration file 41 | 42 | The basic syntax of the configuration file is simple: 43 | 44 | .. code-block:: ini 45 | 46 | [command] 47 | option=value 48 | ... 49 | 50 | where *command* is one of the Distutils commands (e.g. :command:`build_py`, 51 | :command:`install`), and *option* is one of the options that command supports. 52 | Any number of options can be supplied for each command, and any number of 53 | command sections can be included in the file. Blank lines are ignored, as are 54 | comments, which run from a ``'#'`` character until the end of the line. Long 55 | option values can be split across multiple lines simply by indenting the 56 | continuation lines. 57 | 58 | You can find out the list of options supported by a particular command with the 59 | universal :option:`!--help` option, e.g. 60 | 61 | .. code-block:: shell-session 62 | 63 | $ python setup.py --help build_ext 64 | [...] 65 | Options for 'build_ext' command: 66 | --build-lib (-b) directory for compiled extension modules 67 | --build-temp (-t) directory for temporary files (build by-products) 68 | --inplace (-i) ignore build-lib and put compiled extensions into the 69 | source directory alongside your pure Python modules 70 | --include-dirs (-I) list of directories to search for header files 71 | --define (-D) C preprocessor macros to define 72 | --undef (-U) C preprocessor macros to undefine 73 | --swig-opts list of SWIG command line options 74 | [...] 75 | 76 | Note that an option spelled :option:`!--foo-bar` on the command-line is spelled 77 | ``foo_bar`` in configuration files. 78 | 79 | .. _distutils-build-ext-inplace: 80 | 81 | For example, say you want your extensions to be built "in-place"---that is, you 82 | have an extension :mod:`pkg.ext`, and you want the compiled extension file 83 | (:file:`ext.so` on Unix, say) to be put in the same source directory as your 84 | pure Python modules :mod:`pkg.mod1` and :mod:`pkg.mod2`. You can always use the 85 | :option:`!--inplace` option on the command-line to ensure this: 86 | 87 | .. code-block:: sh 88 | 89 | python setup.py build_ext --inplace 90 | 91 | But this requires that you always specify the :command:`build_ext` command 92 | explicitly, and remember to provide :option:`!--inplace`. An easier way is to 93 | "set and forget" this option, by encoding it in :file:`setup.cfg`, the 94 | configuration file for this distribution: 95 | 96 | .. code-block:: ini 97 | 98 | [build_ext] 99 | inplace=true 100 | 101 | This will affect all builds of this module distribution, whether or not you 102 | explicitly specify :command:`build_ext`. If you include :file:`setup.cfg` in 103 | your source distribution, it will also affect end-user builds---which is 104 | probably a bad idea for this option, since always building extensions in-place 105 | would break installation of the module distribution. In certain peculiar cases, 106 | though, modules are built right in their installation directory, so this is 107 | conceivably a useful ability. (Distributing extensions that expect to be built 108 | in their installation directory is almost always a bad idea, though.) 109 | 110 | Another example: certain commands take a lot of options that don't change from 111 | run to run; for example, :command:`bdist_rpm` needs to know everything required 112 | to generate a "spec" file for creating an RPM distribution. Some of this 113 | information comes from the setup script, and some is automatically generated by 114 | the Distutils (such as the list of files installed). But some of it has to be 115 | supplied as options to :command:`bdist_rpm`, which would be very tedious to do 116 | on the command-line for every run. Hence, here is a snippet from the Distutils' 117 | own :file:`setup.cfg`: 118 | 119 | .. code-block:: ini 120 | 121 | [bdist_rpm] 122 | release = 1 123 | packager = Greg Ward 124 | doc_files = CHANGES.txt 125 | README.txt 126 | USAGE.txt 127 | doc/ 128 | examples/ 129 | 130 | Note that the ``doc_files`` option is simply a whitespace-separated string 131 | split across multiple lines for readability. 132 | 133 | 134 | .. rubric:: Footnotes 135 | 136 | .. [#] This ideal probably won't be achieved until auto-configuration is fully 137 | supported by the Distutils. 138 | 139 | -------------------------------------------------------------------------------- /docs/distutils/extending.rst: -------------------------------------------------------------------------------- 1 | .. _extending-distutils: 2 | 3 | ******************* 4 | Extending Distutils 5 | ******************* 6 | 7 | .. include:: ./_setuptools_disclaimer.rst 8 | 9 | Distutils can be extended in various ways. Most extensions take the form of new 10 | commands or replacements for existing commands. New commands may be written to 11 | support new types of platform-specific packaging, for example, while 12 | replacements for existing commands may be made to modify details of how the 13 | command operates on a package. 14 | 15 | Most extensions of the distutils are made within :file:`setup.py` scripts that 16 | want to modify existing commands; many simply add a few file extensions that 17 | should be copied into packages in addition to :file:`.py` files as a 18 | convenience. 19 | 20 | Most distutils command implementations are subclasses of the 21 | :class:`distutils.cmd.Command` class. New commands may directly inherit from 22 | :class:`Command`, while replacements often derive from :class:`Command` 23 | indirectly, directly subclassing the command they are replacing. Commands are 24 | required to derive from :class:`Command`. 25 | 26 | .. % \section{Extending existing commands} 27 | .. % \label{extend-existing} 28 | 29 | .. % \section{Writing new commands} 30 | .. % \label{new-commands} 31 | .. % \XXX{Would an uninstall command be a good example here?} 32 | 33 | 34 | Integrating new commands 35 | ======================== 36 | 37 | There are different ways to integrate new command implementations into 38 | distutils. The most difficult is to lobby for the inclusion of the new features 39 | in distutils itself, and wait for (and require) a version of Python that 40 | provides that support. This is really hard for many reasons. 41 | 42 | The most common, and possibly the most reasonable for most needs, is to include 43 | the new implementations with your :file:`setup.py` script, and cause the 44 | :func:`distutils.core.setup` function use them:: 45 | 46 | from distutils.command.build_py import build_py as _build_py 47 | from distutils.core import setup 48 | 49 | class build_py(_build_py): 50 | """Specialized Python source builder.""" 51 | 52 | # implement whatever needs to be different... 53 | 54 | setup(cmdclass={'build_py': build_py}, 55 | ...) 56 | 57 | This approach is most valuable if the new implementations must be used to use a 58 | particular package, as everyone interested in the package will need to have the 59 | new command implementation. 60 | 61 | Beginning with Python 2.4, a third option is available, intended to allow new 62 | commands to be added which can support existing :file:`setup.py` scripts without 63 | requiring modifications to the Python installation. This is expected to allow 64 | third-party extensions to provide support for additional packaging systems, but 65 | the commands can be used for anything distutils commands can be used for. A new 66 | configuration option, ``command_packages`` (command-line option 67 | :option:`!--command-packages`), can be used to specify additional packages to be 68 | searched for modules implementing commands. Like all distutils options, this 69 | can be specified on the command line or in a configuration file. This option 70 | can only be set in the ``[global]`` section of a configuration file, or before 71 | any commands on the command line. If set in a configuration file, it can be 72 | overridden from the command line; setting it to an empty string on the command 73 | line causes the default to be used. This should never be set in a configuration 74 | file provided with a package. 75 | 76 | This new option can be used to add any number of packages to the list of 77 | packages searched for command implementations; multiple package names should be 78 | separated by commas. When not specified, the search is only performed in the 79 | :mod:`distutils.command` package. When :file:`setup.py` is run with the option 80 | ``--command-packages distcmds,buildcmds``, however, the packages 81 | :mod:`distutils.command`, :mod:`distcmds`, and :mod:`buildcmds` will be searched 82 | in that order. New commands are expected to be implemented in modules of the 83 | same name as the command by classes sharing the same name. Given the example 84 | command line option above, the command :command:`bdist_openpkg` could be 85 | implemented by the class :class:`distcmds.bdist_openpkg.bdist_openpkg` or 86 | :class:`buildcmds.bdist_openpkg.bdist_openpkg`. 87 | 88 | 89 | Adding new distribution types 90 | ============================= 91 | 92 | Commands that create distributions (files in the :file:`dist/` directory) need 93 | to add ``(command, filename)`` pairs to ``self.distribution.dist_files`` so that 94 | :command:`upload` can upload it to PyPI. The *filename* in the pair contains no 95 | path information, only the name of the file itself. In dry-run mode, pairs 96 | should still be added to represent what would have been created. 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/distutils/index.rst: -------------------------------------------------------------------------------- 1 | .. _distutils-index: 2 | 3 | ############################################## 4 | Distributing Python Modules (Legacy version) 5 | ############################################## 6 | 7 | :Authors: Greg Ward, Anthony Baxter 8 | :Email: distutils-sig@python.org 9 | 10 | .. seealso:: 11 | 12 | :ref:`distributing-index` 13 | The up to date module distribution documentations 14 | 15 | .. include:: ./_setuptools_disclaimer.rst 16 | 17 | .. note:: 18 | 19 | This guide only covers the basic tools for building and distributing 20 | extensions that are provided as part of this version of Python. Third party 21 | tools offer easier to use and more secure alternatives. Refer to the `quick 22 | recommendations section `__ 23 | in the Python Packaging User Guide for more information. 24 | 25 | This document describes the Python Distribution Utilities ("Distutils") from 26 | the module developer's point of view, describing the underlying capabilities 27 | that ``setuptools`` builds on to allow Python developers to make Python modules 28 | and extensions readily available to a wider audience. 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :numbered: 33 | 34 | introduction.rst 35 | setupscript.rst 36 | configfile.rst 37 | sourcedist.rst 38 | builtdist.rst 39 | examples.rst 40 | extending.rst 41 | commandref.rst 42 | apiref.rst 43 | -------------------------------------------------------------------------------- /docs/distutils/packageindex.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _package-index: 4 | 5 | ******************************* 6 | The Python Package Index (PyPI) 7 | ******************************* 8 | 9 | The `Python Package Index (PyPI) `_ stores 10 | metadata describing distributions packaged with distutils and 11 | other publishing tools, as well the distribution archives 12 | themselves. 13 | 14 | The best resource for working with PyPI is the 15 | `Python Packaging User Guide `_. 16 | -------------------------------------------------------------------------------- /docs/distutils/uploading.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | *************************************** 4 | Uploading Packages to the Package Index 5 | *************************************** 6 | 7 | See the 8 | `Python Packaging User Guide `_ 9 | for the best guidance on uploading packages. 10 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | History 6 | ******* 7 | 8 | .. include:: ../NEWS (links).rst 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to |project| documentation! 2 | =================================== 3 | 4 | .. sidebar-links:: 5 | :home: 6 | :pypi: 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | history 12 | distutils/index 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | 22 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Is the project well-typed? 3 | strict = False 4 | 5 | # Early opt-in even when strict = False 6 | warn_unused_ignores = True 7 | warn_redundant_casts = True 8 | enable_error_code = ignore-without-code 9 | 10 | # Support namespace packages per https://github.com/python/mypy/issues/14057 11 | explicit_package_bases = True 12 | 13 | disable_error_code = 14 | # Disable due to many false positives 15 | overload-overlap, 16 | 17 | # local 18 | 19 | # TODO: Resolve and re-enable these gradually 20 | operator, 21 | attr-defined, 22 | arg-type, 23 | assignment, 24 | call-overload, 25 | return-value, 26 | index, 27 | type-var, 28 | func-returns-value, 29 | union-attr, 30 | str-bytes-safe, 31 | misc, 32 | has-type, 33 | 34 | # stdlib's test module is not typed on typeshed 35 | [mypy-test.*] 36 | ignore_missing_imports = True 37 | 38 | # https://github.com/jaraco/jaraco.envs/issues/7 39 | # https://github.com/jaraco/jaraco.envs/pull/8 40 | [mypy-jaraco.envs.*] 41 | ignore_missing_imports = True 42 | 43 | # https://github.com/jaraco/jaraco.path/issues/2 44 | # https://github.com/jaraco/jaraco.path/pull/7 45 | [mypy-jaraco.path.*] 46 | ignore_missing_imports = True 47 | 48 | # https://github.com/jaraco/jaraco.text/issues/17 49 | # https://github.com/jaraco/jaraco.text/pull/23 50 | [mypy-jaraco.text.*] 51 | ignore_missing_imports = True 52 | -------------------------------------------------------------------------------- /newsfragments/4874.feature.rst: -------------------------------------------------------------------------------- 1 | Restore access to _get_vc_env with a warning. 2 | -------------------------------------------------------------------------------- /newsfragments/4902.bugfix.rst: -------------------------------------------------------------------------------- 1 | Reverted distutils changes that broke the monkey patching of command classes. 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=77", 4 | "setuptools_scm[toml]>=3.4.1", 5 | # jaraco/skeleton#174 6 | "coherent.licensed", 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [project] 11 | name = "distutils" 12 | authors = [ 13 | { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, 14 | ] 15 | description = "Distribution utilities formerly from standard library" 16 | readme = "README.rst" 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "Intended Audience :: Developers", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3 :: Only", 22 | ] 23 | requires-python = ">=3.9" 24 | license = "MIT" 25 | dependencies = [ 26 | # Setuptools must require these 27 | "packaging", 28 | "jaraco.functools >= 4", 29 | "more_itertools", 30 | ] 31 | dynamic = ["version"] 32 | 33 | [project.urls] 34 | Source = "https://github.com/pypa/distutils" 35 | 36 | [project.optional-dependencies] 37 | test = [ 38 | # upstream 39 | "pytest >= 6, != 8.1.*", 40 | 41 | # local 42 | "pytest >= 7.4.3", # pypa/distutils#186 43 | "jaraco.envs>=2.4", 44 | "jaraco.path", 45 | "jaraco.text", 46 | "path >= 10.6", 47 | "docutils", 48 | "Pygments", 49 | "pyfakefs", 50 | "more_itertools", 51 | 52 | # workaround for pytest-dev/pytest#12490 53 | "pytest < 8.1; python_version < '3.12'", 54 | ] 55 | 56 | doc = [ 57 | # upstream 58 | "sphinx >= 3.5", 59 | "jaraco.packaging >= 9.3", 60 | "rst.linker >= 1.9", 61 | "furo", 62 | "sphinx-lint", 63 | 64 | # local 65 | ] 66 | 67 | check = [ 68 | "pytest-checkdocs >= 2.4", 69 | "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", 70 | ] 71 | 72 | cover = [ 73 | "pytest-cov", 74 | ] 75 | 76 | enabler = [ 77 | "pytest-enabler >= 2.2", 78 | ] 79 | 80 | type = [ 81 | # upstream 82 | "pytest-mypy", 83 | 84 | # local 85 | "types-docutils", 86 | ] 87 | 88 | 89 | [tool.setuptools_scm] 90 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=dist build .tox .eggs 3 | addopts= 4 | --doctest-modules 5 | --import-mode importlib 6 | consider_namespace_packages=true 7 | filterwarnings= 8 | ## upstream 9 | 10 | # Ensure ResourceWarnings are emitted 11 | default::ResourceWarning 12 | 13 | # realpython/pytest-mypy#152 14 | ignore:'encoding' argument not specified::pytest_mypy 15 | 16 | # python/cpython#100750 17 | ignore:'encoding' argument not specified::platform 18 | 19 | # pypa/build#615 20 | ignore:'encoding' argument not specified::build.env 21 | 22 | # dateutil/dateutil#1284 23 | ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz 24 | 25 | ## end upstream 26 | 27 | # acknowledge that TestDistribution isn't a test 28 | ignore:cannot collect test class 'TestDistribution' 29 | ignore:Fallback spawn triggered 30 | 31 | # ignore spurious and unactionable warnings 32 | ignore:The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.:DeprecationWarning: 33 | ignore: The frontend.Option class will be removed in Docutils 0.21 or later.:DeprecationWarning: 34 | 35 | # suppress warnings in deprecated compilers 36 | ignore:(bcpp|msvc9?)compiler is deprecated 37 | 38 | # suppress well known deprecation warning 39 | ignore:distutils.log.Log is deprecated 40 | 41 | # suppress known deprecation 42 | ignore:Use shutil.which instead of find_executable:DeprecationWarning 43 | 44 | # https://sourceforge.net/p/docutils/bugs/490/ 45 | ignore:'encoding' argument not specified::docutils.io 46 | ignore:UTF-8 Mode affects locale.getpreferredencoding()::docutils.io 47 | 48 | # suppress known deprecation 49 | ignore:register command is deprecated 50 | ignore:upload command is deprecated 51 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | extend-select = [ 3 | # upstream 4 | 5 | "C901", # complex-structure 6 | "I", # isort 7 | "PERF401", # manual-list-comprehension 8 | 9 | # Ensure modern type annotation syntax and best practices 10 | # Not including those covered by type-checkers or exclusive to Python 3.11+ 11 | "FA", # flake8-future-annotations 12 | "F404", # late-future-import 13 | "PYI", # flake8-pyi 14 | "UP006", # non-pep585-annotation 15 | "UP007", # non-pep604-annotation 16 | "UP010", # unnecessary-future-import 17 | "UP035", # deprecated-import 18 | "UP037", # quoted-annotation 19 | "UP043", # unnecessary-default-type-args 20 | 21 | # local 22 | "B", 23 | "ISC", 24 | "PERF", 25 | "RUF010", 26 | "RUF100", 27 | "TRY", 28 | "UP", 29 | "YTT", 30 | ] 31 | ignore = [ 32 | # upstream 33 | 34 | # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, 35 | # irrelevant to this project. 36 | "PYI011", # typed-argument-default-in-stub 37 | # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 38 | "W191", 39 | "E111", 40 | "E114", 41 | "E117", 42 | "D206", 43 | "D300", 44 | "Q000", 45 | "Q001", 46 | "Q002", 47 | "Q003", 48 | "COM812", 49 | "COM819", 50 | 51 | # local 52 | "B028", 53 | "B904", 54 | "PERF203", 55 | "TRY003", 56 | "TRY400", 57 | ] 58 | 59 | [format] 60 | # Enable preview to get hugged parenthesis unwrapping and other nice surprises 61 | # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 62 | preview = true 63 | # https://docs.astral.sh/ruff/settings/#format_quote-style 64 | quote-style = "preserve" 65 | -------------------------------------------------------------------------------- /towncrier.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | title_format = "{version}" 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | description = perform primary checks (tests, style, types, coverage) 3 | deps = 4 | setenv = 5 | PYTHONWARNDEFAULTENCODING = 1 6 | # pypa/distutils#99 7 | VIRTUALENV_NO_SETUPTOOLS = 1 8 | pass_env = 9 | DISTUTILS_TEST_DEFAULT_COMPILER 10 | commands = 11 | pytest {posargs} 12 | usedevelop = True 13 | extras = 14 | test 15 | check 16 | cover 17 | enabler 18 | type 19 | 20 | [testenv:diffcov] 21 | description = run tests and check that diff from main is covered 22 | deps = 23 | {[testenv]deps} 24 | diff-cover 25 | commands = 26 | pytest {posargs} --cov-report xml 27 | diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html 28 | diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 29 | 30 | [testenv:docs] 31 | description = build the documentation 32 | extras = 33 | doc 34 | test 35 | changedir = docs 36 | commands = 37 | python -m sphinx -W --keep-going . {toxinidir}/build/html 38 | python -m sphinxlint 39 | 40 | [testenv:finalize] 41 | description = assemble changelog and tag a release 42 | skip_install = True 43 | deps = 44 | towncrier 45 | jaraco.develop >= 7.23 46 | pass_env = * 47 | commands = 48 | python -m jaraco.develop.finalize 49 | 50 | 51 | [testenv:release] 52 | description = publish the package to PyPI and GitHub 53 | skip_install = True 54 | deps = 55 | build 56 | twine>=3 57 | jaraco.develop>=7.1 58 | pass_env = 59 | TWINE_PASSWORD 60 | GITHUB_TOKEN 61 | setenv = 62 | TWINE_USERNAME = {env:TWINE_USERNAME:__token__} 63 | commands = 64 | python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" 65 | python -m build 66 | python -m twine upload dist/* 67 | python -m jaraco.develop.create-github-release 68 | --------------------------------------------------------------------------------