Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
dist.py1125 linesDownload Raw Back to setuptools
1from __future__ import annotations2 3import functools4import io5import itertools6import numbers7import os8import re9import sys10from collections.abc import Iterable, Iterator, MutableMapping, Sequence11from glob import glob12from pathlib import Path13from typing import TYPE_CHECKING, Any, Union14 15from more_itertools import partition, unique_everseen16from packaging.markers import InvalidMarker, Marker17from packaging.specifiers import InvalidSpecifier, SpecifierSet18from packaging.version import Version19 20from . import (21    _entry_points,22    _reqs,23    _static,24    command as _,  # noqa: F401 # imported for side-effects25)26from ._importlib import metadata27from ._normalization import _canonicalize_license_expression28from ._path import StrPath29from ._reqs import _StrOrIter30from .config import pyprojecttoml, setupcfg31from .discovery import ConfigDiscovery32from .errors import InvalidConfigError33from .monkey import get_unpatched34from .warnings import InformationOnly, SetuptoolsDeprecationWarning35 36import distutils.cmd37import distutils.command38import distutils.core39import distutils.dist40import distutils.log41from distutils.debug import DEBUG42from distutils.errors import DistutilsOptionError, DistutilsSetupError43from distutils.fancy_getopt import translate_longopt44from distutils.util import strtobool45 46if TYPE_CHECKING:47    from typing_extensions import TypeAlias48 49 50__all__ = ['Distribution']51 52_sequence = tuple, list53"""54:meta private:55 56Supported iterable types that are known to be:57- ordered (which `set` isn't)58- not match a str (which `Sequence[str]` does)59- not imply a nested type (like `dict`)60for use with `isinstance`.61"""62_Sequence: TypeAlias = Union[tuple[str, ...], list[str]]63# This is how stringifying _Sequence would look in Python 3.1064_sequence_type_repr = "tuple[str, ...] | list[str]"65_OrderedStrSequence: TypeAlias = Union[str, dict[str, Any], Sequence[str]]66"""67:meta private:68Avoid single-use iterable. Disallow sets.69A poor approximation of an OrderedSequence (dict doesn't match a Sequence).70"""71 72 73def __getattr__(name: str) -> Any:  # pragma: no cover74    if name == "sequence":75        SetuptoolsDeprecationWarning.emit(76            "`setuptools.dist.sequence` is an internal implementation detail.",77            "Please define your own `sequence = tuple, list` instead.",78            due_date=(2025, 8, 28),  # Originally added on 2024-08-2779        )80        return _sequence81    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")82 83 84def check_importable(dist, attr, value):85    try:86        ep = metadata.EntryPoint(value=value, name=None, group=None)87        assert not ep.extras88    except (TypeError, ValueError, AttributeError, AssertionError) as e:89        raise DistutilsSetupError(90            f"{attr!r} must be importable 'module:attrs' string (got {value!r})"91        ) from e92 93 94def assert_string_list(dist, attr: str, value: _Sequence) -> None:95    """Verify that value is a string list"""96    try:97        # verify that value is a list or tuple to exclude unordered98        # or single-use iterables99        assert isinstance(value, _sequence)100        # verify that elements of value are strings101        assert ''.join(value) != value102    except (TypeError, ValueError, AttributeError, AssertionError) as e:103        raise DistutilsSetupError(104            f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})"105        ) from e106 107 108def check_nsp(dist, attr, value):109    """Verify that namespace packages are valid"""110    ns_packages = value111    assert_string_list(dist, attr, ns_packages)112    for nsp in ns_packages:113        if not dist.has_contents_for(nsp):114            raise DistutilsSetupError(115                f"Distribution contains no modules or packages for namespace package {nsp!r}"116            )117        parent, _sep, _child = nsp.rpartition('.')118        if parent and parent not in ns_packages:119            distutils.log.warn(120                "WARNING: %r is declared as a package namespace, but %r"121                " is not: please correct this in setup.py",122                nsp,123                parent,124            )125        SetuptoolsDeprecationWarning.emit(126            "The namespace_packages parameter is deprecated.",127            "Please replace its usage with implicit namespaces (PEP 420).",128            see_docs="references/keywords.html#keyword-namespace-packages",129            # TODO: define due_date, it may break old packages that are no longer130            # maintained (e.g. sphinxcontrib extensions) when installed from source.131            # Warning officially introduced in May 2022, however the deprecation132            # was mentioned much earlier in the docs (May 2020, see #2149).133        )134 135 136def check_extras(dist, attr, value):137    """Verify that extras_require mapping is valid"""138    try:139        list(itertools.starmap(_check_extra, value.items()))140    except (TypeError, ValueError, AttributeError) as e:141        raise DistutilsSetupError(142            "'extras_require' must be a dictionary whose values are "143            "strings or lists of strings containing valid project/version "144            "requirement specifiers."145        ) from e146 147 148def _check_extra(extra, reqs):149    _name, _sep, marker = extra.partition(':')150    try:151        _check_marker(marker)152    except InvalidMarker:153        msg = f"Invalid environment marker: {marker} ({extra!r})"154        raise DistutilsSetupError(msg) from None155    list(_reqs.parse(reqs))156 157 158def _check_marker(marker):159    if not marker:160        return161    m = Marker(marker)162    m.evaluate()163 164 165def assert_bool(dist, attr, value):166    """Verify that value is True, False, 0, or 1"""167    if bool(value) != value:168        raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})")169 170 171def invalid_unless_false(dist, attr, value):172    if not value:173        DistDeprecationWarning.emit(f"{attr} is ignored.")174        # TODO: should there be a `due_date` here?175        return176    raise DistutilsSetupError(f"{attr} is invalid.")177 178 179def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None:180    """Verify that install_requires is a valid requirements list"""181    try:182        list(_reqs.parse(value))183        if isinstance(value, set):184            raise TypeError("Unordered types are not allowed")185    except (TypeError, ValueError) as error:186        msg = (187            f"{attr!r} must be a string or iterable of strings "188            f"containing valid project/version requirement specifiers; {error}"189        )190        raise DistutilsSetupError(msg) from error191 192 193def check_specifier(dist, attr, value):194    """Verify that value is a valid version specifier"""195    try:196        SpecifierSet(value)197    except (InvalidSpecifier, AttributeError) as error:198        msg = f"{attr!r} must be a string containing valid version specifiers; {error}"199        raise DistutilsSetupError(msg) from error200 201 202def check_entry_points(dist, attr, value):203    """Verify that entry_points map is parseable"""204    try:205        _entry_points.load(value)206    except Exception as e:207        raise DistutilsSetupError(e) from e208 209 210def check_package_data(dist, attr, value):211    """Verify that value is a dictionary of package names to glob lists"""212    if not isinstance(value, dict):213        raise DistutilsSetupError(214            f"{attr!r} must be a dictionary mapping package names to lists of "215            "string wildcard patterns"216        )217    for k, v in value.items():218        if not isinstance(k, str):219            raise DistutilsSetupError(220                f"keys of {attr!r} dict must be strings (got {k!r})"221            )222        assert_string_list(dist, f'values of {attr!r} dict', v)223 224 225def check_packages(dist, attr, value):226    for pkgname in value:227        if not re.match(r'\w+(\.\w+)*', pkgname):228            distutils.log.warn(229                "WARNING: %r not a valid package name; please use only "230                ".-separated package names in setup.py",231                pkgname,232            )233 234 235if TYPE_CHECKING:236    # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962237    from distutils.core import Distribution as _Distribution238else:239    _Distribution = get_unpatched(distutils.core.Distribution)240 241 242class Distribution(_Distribution):243    """Distribution with support for tests and package data244 245    This is an enhanced version of 'distutils.dist.Distribution' that246    effectively adds the following new optional keyword arguments to 'setup()':247 248     'install_requires' -- a string or sequence of strings specifying project249        versions that the distribution requires when installed, in the format250        used by 'pkg_resources.require()'.  They will be installed251        automatically when the package is installed.  If you wish to use252        packages that are not available in PyPI, or want to give your users an253        alternate download location, you can add a 'find_links' option to the254        '[easy_install]' section of your project's 'setup.cfg' file, and then255        setuptools will scan the listed web pages for links that satisfy the256        requirements.257 258     'extras_require' -- a dictionary mapping names of optional "extras" to the259        additional requirement(s) that using those extras incurs. For example,260        this::261 262            extras_require = dict(reST = ["docutils>=0.3", "reSTedit"])263 264        indicates that the distribution can optionally provide an extra265        capability called "reST", but it can only be used if docutils and266        reSTedit are installed.  If the user installs your package using267        EasyInstall and requests one of your extras, the corresponding268        additional requirements will be installed if needed.269 270     'package_data' -- a dictionary mapping package names to lists of filenames271        or globs to use to find data files contained in the named packages.272        If the dictionary has filenames or globs listed under '""' (the empty273        string), those names will be searched for in every package, in addition274        to any names for the specific package.  Data files found using these275        names/globs will be installed along with the package, in the same276        location as the package.  Note that globs are allowed to reference277        the contents of non-package subdirectories, as long as you use '/' as278        a path separator.  (Globs are automatically converted to279        platform-specific paths at runtime.)280 281    In addition to these new keywords, this class also has several new methods282    for manipulating the distribution's contents.  For example, the 'include()'283    and 'exclude()' methods can be thought of as in-place add and subtract284    commands that add or remove packages, modules, extensions, and so on from285    the distribution.286    """287 288    _DISTUTILS_UNSUPPORTED_METADATA = {289        'long_description_content_type': lambda: None,290        'project_urls': dict,291        'provides_extras': dict,  # behaves like an ordered set292        'license_expression': lambda: None,293        'license_file': lambda: None,294        'license_files': lambda: None,295        'install_requires': list,296        'extras_require': dict,297    }298 299    # Used by build_py, editable_wheel and install_lib commands for legacy namespaces300    namespace_packages: list[str]  #: :meta private: DEPRECATED301 302    # Any: Dynamic assignment results in Incompatible types in assignment303    def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None:304        have_package_data = hasattr(self, "package_data")305        if not have_package_data:306            self.package_data: dict[str, list[str]] = {}307        attrs = attrs or {}308        self.dist_files: list[tuple[str, str, str]] = []309        self.include_package_data: bool | None = None310        self.exclude_package_data: dict[str, list[str]] | None = None311        # Filter-out setuptools' specific options.312        self.src_root: str | None = attrs.pop("src_root", None)313        self.dependency_links: list[str] = attrs.pop('dependency_links', [])314        self.setup_requires: list[str] = attrs.pop('setup_requires', [])315        for ep in metadata.entry_points(group='distutils.setup_keywords'):316            vars(self).setdefault(ep.name, None)317 318        metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA)319        metadata_only -= {"install_requires", "extras_require"}320        dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only}321        _Distribution.__init__(self, dist_attrs)322 323        # Private API (setuptools-use only, not restricted to Distribution)324        # Stores files that are referenced by the configuration and need to be in the325        # sdist (e.g. `version = file: VERSION.txt`)326        self._referenced_files = set[str]()327 328        self.set_defaults = ConfigDiscovery(self)329 330        self._set_metadata_defaults(attrs)331 332        self.metadata.version = self._normalize_version(self.metadata.version)333        self._finalize_requires()334 335    def _validate_metadata(self):336        required = {"name"}337        provided = {338            key339            for key in vars(self.metadata)340            if getattr(self.metadata, key, None) is not None341        }342        missing = required - provided343 344        if missing:345            msg = f"Required package metadata is missing: {missing}"346            raise DistutilsSetupError(msg)347 348    def _set_metadata_defaults(self, attrs):349        """350        Fill-in missing metadata fields not supported by distutils.351        Some fields may have been set by other tools (e.g. pbr).352        Those fields (vars(self.metadata)) take precedence to353        supplied attrs.354        """355        for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():356            vars(self.metadata).setdefault(option, attrs.get(option, default()))357 358    @staticmethod359    def _normalize_version(version):360        from . import sic361 362        if isinstance(version, numbers.Number):363            # Some people apparently take "version number" too literally :)364            version = str(version)365        elif isinstance(version, sic) or version is None:366            return version367 368        normalized = str(Version(version))369        if version != normalized:370            InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")371            return normalized372        return version373 374    def _finalize_requires(self):375        """376        Set `metadata.python_requires` and fix environment markers377        in `install_requires` and `extras_require`.378        """379        if getattr(self, 'python_requires', None):380            self.metadata.python_requires = self.python_requires381 382        self._normalize_requires()383        self.metadata.install_requires = self.install_requires384        self.metadata.extras_require = self.extras_require385 386        if self.extras_require:387            for extra in self.extras_require.keys():388                # Setuptools allows a weird "<name>:<env markers> syntax for extras389                extra = extra.split(':')[0]390                if extra:391                    self.metadata.provides_extras.setdefault(extra)392 393    def _normalize_requires(self):394        """Make sure requirement-related attributes exist and are normalized"""395        install_requires = getattr(self, "install_requires", None) or []396        extras_require = getattr(self, "extras_require", None) or {}397 398        # Preserve the "static"-ness of values parsed from config files399        list_ = _static.List if _static.is_static(install_requires) else list400        self.install_requires = list_(map(str, _reqs.parse(install_requires)))401 402        dict_ = _static.Dict if _static.is_static(extras_require) else dict403        self.extras_require = dict_(404            (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items()405        )406 407    def _finalize_license_expression(self) -> None:408        """409        Normalize license and license_expression.410        >>> dist = Distribution({"license_expression": _static.Str("mit aNd  gpl-3.0-OR-later")})411        >>> _static.is_static(dist.metadata.license_expression)412        True413        >>> dist._finalize_license_expression()414        >>> _static.is_static(dist.metadata.license_expression)  # preserve "static-ness"415        True416        >>> print(dist.metadata.license_expression)417        MIT AND GPL-3.0-or-later418        """419        classifiers = self.metadata.get_classifiers()420        license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")]421 422        license_expr = self.metadata.license_expression423        if license_expr:424            str_ = _static.Str if _static.is_static(license_expr) else str425            normalized = str_(_canonicalize_license_expression(license_expr))426            if license_expr != normalized:427                InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'")428                self.metadata.license_expression = normalized429            if license_classifiers:430                raise InvalidConfigError(431                    "License classifiers have been superseded by license expressions "432                    "(see https://peps.python.org/pep-0639/). Please remove:\n\n"433                    + "\n".join(license_classifiers),434                )435        elif license_classifiers:436            pypa_guides = "guides/writing-pyproject-toml/#license"437            SetuptoolsDeprecationWarning.emit(438                "License classifiers are deprecated.",439                "Please consider removing the following classifiers in favor of a "440                "SPDX license expression:\n\n" + "\n".join(license_classifiers),441                see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",442                # Warning introduced on 2025-02-17443                # TODO: Should we add a due date? It may affect old/unmaintained444                #       packages in the ecosystem and cause problems...445            )446 447    def _finalize_license_files(self) -> None:448        """Compute names of all license files which should be included."""449        license_files: list[str] | None = self.metadata.license_files450        patterns = license_files or []451 452        license_file: str | None = self.metadata.license_file453        if license_file and license_file not in patterns:454            patterns.append(license_file)455 456        if license_files is None and license_file is None:457            # Default patterns match the ones wheel uses458            # See https://wheel.readthedocs.io/en/stable/user_guide.html459            # -> 'Including license files in the generated wheel file'460            patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']461            files = self._expand_patterns(patterns, enforce_match=False)462        else:  # Patterns explicitly given by the user463            files = self._expand_patterns(patterns, enforce_match=True)464 465        self.metadata.license_files = list(unique_everseen(files))466 467    @classmethod468    def _expand_patterns(469        cls, patterns: list[str], enforce_match: bool = True470    ) -> Iterator[str]:471        """472        >>> getfixture('sample_project_cwd')473        >>> list(Distribution._expand_patterns(['LICENSE.txt']))474        ['LICENSE.txt']475        >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))476        ['pyproject.toml', 'LICENSE.txt']477        >>> list(Distribution._expand_patterns(['src/**/*.dat']))478        ['src/sample/package_data.dat']479        """480        return (481            path.replace(os.sep, "/")482            for pattern in patterns483            for path in sorted(cls._find_pattern(pattern, enforce_match))484            if not path.endswith('~') and os.path.isfile(path)485        )486 487    @staticmethod488    def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]:489        r"""490        >>> getfixture('sample_project_cwd')491        >>> Distribution._find_pattern("LICENSE.txt")492        ['LICENSE.txt']493        >>> Distribution._find_pattern("/LICENSE.MIT")494        Traceback (most recent call last):495        ...496        setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative...497        >>> Distribution._find_pattern("../LICENSE.MIT")498        Traceback (most recent call last):499        ...500        setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern '../LICENSE.MIT' cannot contain '..'...501        >>> Distribution._find_pattern("LICEN{CSE*")502        Traceback (most recent call last):503        ...504        setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters...505        """506        pypa_guides = "specifications/glob-patterns/"507        if ".." in pattern:508            SetuptoolsDeprecationWarning.emit(509                f"Pattern {pattern!r} cannot contain '..'",510                """511                Please ensure the files specified are contained by the root512                of the Python package (normally marked by `pyproject.toml`).513                """,514                see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",515                due_date=(2027, 2, 18),  # Introduced in 2025-03-20516                # Replace with InvalidConfigError after deprecation517            )518        if pattern.startswith((os.sep, "/")) or ":\\" in pattern:519            raise InvalidConfigError(520                f"Pattern {pattern!r} should be relative and must not start with '/'"521            )522        if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None:523            SetuptoolsDeprecationWarning.emit(524                "Please provide a valid glob pattern.",525                "Pattern {pattern!r} contains invalid characters.",526                pattern=pattern,527                see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",528                due_date=(2027, 2, 18),  # Introduced in 2025-02-20529            )530 531        found = glob(pattern, recursive=True)532 533        if enforce_match and not found:534            SetuptoolsDeprecationWarning.emit(535                "Cannot find any files for the given pattern.",536                "Pattern {pattern!r} did not match any files.",537                pattern=pattern,538                due_date=(2027, 2, 18),  # Introduced in 2025-02-20539                # PEP 639 requires us to error, but as a transition period540                # we will only issue a warning to give people time to prepare.541                # After the transition, this should raise an InvalidConfigError.542            )543        return found544 545    # FIXME: 'Distribution._parse_config_files' is too complex (14)546    def _parse_config_files(self, filenames=None):  # noqa: C901547        """548        Adapted from distutils.dist.Distribution.parse_config_files,549        this method provides the same functionality in subtly-improved550        ways.551        """552        from configparser import ConfigParser553 554        # Ignore install directory options if we have a venv555        ignore_options = (556            []557            if sys.prefix == sys.base_prefix558            else [559                'install-base',560                'install-platbase',561                'install-lib',562                'install-platlib',563                'install-purelib',564                'install-headers',565                'install-scripts',566                'install-data',567                'prefix',568                'exec-prefix',569                'home',570                'user',571                'root',572            ]573        )574 575        ignore_options = frozenset(ignore_options)576 577        if filenames is None:578            filenames = self.find_config_files()579 580        if DEBUG:581            self.announce("Distribution.parse_config_files():")582 583        parser = ConfigParser()584        parser.optionxform = str585        for filename in filenames:586            with open(filename, encoding='utf-8') as reader:587                if DEBUG:588                    self.announce("  reading {filename}".format(**locals()))589                parser.read_file(reader)590            for section in parser.sections():591                options = parser.options(section)592                opt_dict = self.get_option_dict(section)593 594                for opt in options:595                    if opt == '__name__' or opt in ignore_options:596                        continue597 598                    val = parser.get(section, opt)599                    opt = self._enforce_underscore(opt, section)600                    opt = self._enforce_option_lowercase(opt, section)601                    opt_dict[opt] = (filename, val)602 603            # Make the ConfigParser forget everything (so we retain604            # the original filenames that options come from)605            parser.__init__()606 607        if 'global' not in self.command_options:608            return609 610        # If there was a "global" section in the config file, use it611        # to set Distribution options.612 613        for opt, (src, val) in self.command_options['global'].items():614            alias = self.negative_opt.get(opt)615            if alias:616                val = not strtobool(val)617            elif opt == 'verbose':618                val = strtobool(val)619 620            try:621                setattr(self, alias or opt, val)622            except ValueError as e:623                raise DistutilsOptionError(e) from e624 625    def _enforce_underscore(self, opt: str, section: str) -> str:626        if "-" not in opt or self._skip_setupcfg_normalization(section):627            return opt628 629        underscore_opt = opt.replace('-', '_')630        affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""631        SetuptoolsDeprecationWarning.emit(632            f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), "633            f"please use the underscore name {underscore_opt!r} instead.",634            f"""635            Usage of dash-separated {opt!r} will not be supported in future636            versions. Please use the underscore name {underscore_opt!r} instead.637            {affected}638 639            Available configuration options are listed in:640            https://setuptools.pypa.io/en/latest/userguide/declarative_config.html641            """,642            see_url="https://github.com/pypa/setuptools/discussions/5011",643            due_date=(2026, 3, 3),644            # Warning initially introduced in 3 Mar 2021645        )646        return underscore_opt647 648    def _enforce_option_lowercase(self, opt: str, section: str) -> str:649        if opt.islower() or self._skip_setupcfg_normalization(section):650            return opt651 652        lowercase_opt = opt.lower()653        affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""654        SetuptoolsDeprecationWarning.emit(655            f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), "656            f"please use lowercase {lowercase_opt!r} instead.",657            f"""658            Usage of uppercase key {opt!r} in {section!r} will not be supported in659            future versions. Please use lowercase {lowercase_opt!r} instead.660            {affected}661 662            Available configuration options are listed in:663            https://setuptools.pypa.io/en/latest/userguide/declarative_config.html664            """,665            see_url="https://github.com/pypa/setuptools/discussions/5011",666            due_date=(2026, 3, 3),667            # Warning initially introduced in 6 Mar 2021668        )669        return lowercase_opt670 671    def _skip_setupcfg_normalization(self, section: str) -> bool:672        skip = (673            'options.extras_require',674            'options.data_files',675            'options.entry_points',676            'options.package_data',677            'options.exclude_package_data',678        )679        return section in skip or not self._is_setuptools_section(section)680 681    def _is_setuptools_section(self, section: str) -> bool:682        return (683            section == "metadata"684            or section.startswith("options")685            or section in _setuptools_commands()686        )687 688    # FIXME: 'Distribution._set_command_options' is too complex (14)689    def _set_command_options(self, command_obj, option_dict=None):  # noqa: C901690        """691        Set the options for 'command_obj' from 'option_dict'.  Basically692        this means copying elements of a dictionary ('option_dict') to693        attributes of an instance ('command').694 695        'command_obj' must be a Command instance.  If 'option_dict' is not696        supplied, uses the standard option dictionary for this command697        (from 'self.command_options').698 699        (Adopted from distutils.dist.Distribution._set_command_options)700        """701        command_name = command_obj.get_command_name()702        if option_dict is None:703            option_dict = self.get_option_dict(command_name)704 705        if DEBUG:706            self.announce(f"  setting options for '{command_name}' command:")707        for option, (source, value) in option_dict.items():708            if DEBUG:709                self.announce(f"    {option} = {value} (from {source})")710            try:711                bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]712            except AttributeError:713                bool_opts = []714            try:715                neg_opt = command_obj.negative_opt716            except AttributeError:717                neg_opt = {}718 719            try:720                is_string = isinstance(value, str)721                if option in neg_opt and is_string:722                    setattr(command_obj, neg_opt[option], not strtobool(value))723                elif option in bool_opts and is_string:724                    setattr(command_obj, option, strtobool(value))725                elif hasattr(command_obj, option):726                    setattr(command_obj, option, value)727                else:728                    raise DistutilsOptionError(729                        f"error in {source}: command '{command_name}' has no such option '{option}'"730                    )731            except ValueError as e:732                raise DistutilsOptionError(e) from e733 734    def _get_project_config_files(self, filenames: Iterable[StrPath] | None):735        """Add default file and split between INI and TOML"""736        tomlfiles = []737        standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")738        if filenames is not None:739            parts = partition(lambda f: Path(f).suffix == ".toml", filenames)740            filenames = list(parts[0])  # 1st element => predicate is False741            tomlfiles = list(parts[1])  # 2nd element => predicate is True742        elif standard_project_metadata.exists():743            tomlfiles = [standard_project_metadata]744        return filenames, tomlfiles745 746    def parse_config_files(747        self,748        filenames: Iterable[StrPath] | None = None,749        ignore_option_errors: bool = False,750    ) -> None:751        """Parses configuration files from various levels752        and loads configuration.753        """754        inifiles, tomlfiles = self._get_project_config_files(filenames)755 756        self._parse_config_files(filenames=inifiles)757 758        setupcfg.parse_configuration(759            self, self.command_options, ignore_option_errors=ignore_option_errors760        )761        for filename in tomlfiles:762            pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)763 764        self._finalize_requires()765        self._finalize_license_expression()766        self._finalize_license_files()767 768    def fetch_build_eggs(self, requires: _StrOrIter) -> list[metadata.Distribution]:769        """Resolve pre-setup requirements"""770        from .installer import _fetch_build_eggs771 772        return _fetch_build_eggs(self, requires)773 774    def finalize_options(self) -> None:775        """776        Allow plugins to apply arbitrary operations to the777        distribution. Each hook may optionally define a 'order'778        to influence the order of execution. Smaller numbers779        go first and the default is 0.780        """781        group = 'setuptools.finalize_distribution_options'782 783        def by_order(hook):784            return getattr(hook, 'order', 0)785 786        defined = metadata.entry_points(group=group)787        filtered = itertools.filterfalse(self._removed, defined)788        loaded = map(lambda e: e.load(), filtered)789        for ep in sorted(loaded, key=by_order):790            ep(self)791 792    @staticmethod793    def _removed(ep):794        """795        When removing an entry point, if metadata is loaded796        from an older version of Setuptools, that removed797        entry point will attempt to be loaded and will fail.798        See #2765 for more details.799        """800        removed = {801            # removed 2021-09-05802            '2to3_doctests',803        }804        return ep.name in removed805 806    def _finalize_setup_keywords(self):807        for ep in metadata.entry_points(group='distutils.setup_keywords'):808            value = getattr(self, ep.name, None)809            if value is not None:810                ep.load()(self, ep.name, value)811 812    def get_egg_cache_dir(self) -> str:813        from . import windows_support814 815        egg_cache_dir = os.path.join(os.curdir, '.eggs')816        if not os.path.exists(egg_cache_dir):817            os.mkdir(egg_cache_dir)818            windows_support.hide_file(egg_cache_dir)819            readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')820            with open(readme_txt_filename, 'w', encoding="utf-8") as f:821                f.write(822                    'This directory contains eggs that were downloaded '823                    'by setuptools to build, test, and run plug-ins.\n\n'824                )825                f.write(826                    'This directory caches those eggs to prevent '827                    'repeated downloads.\n\n'828                )829                f.write('However, it is safe to delete this directory.\n\n')830 831        return egg_cache_dir832 833    def fetch_build_egg(self, req):834        """Fetch an egg needed for building"""835        from .installer import fetch_build_egg836 837        return fetch_build_egg(self, req)838 839    def get_command_class(self, command: str) -> type[distutils.cmd.Command]:  # type: ignore[override] # Not doing complex overrides yet840        """Pluggable version of get_command_class()"""841        if command in self.cmdclass:842            return self.cmdclass[command]843 844        # Special case bdist_wheel so it's never loaded from "wheel"845        if command == 'bdist_wheel':846            from .command.bdist_wheel import bdist_wheel847 848            return bdist_wheel849 850        eps = metadata.entry_points(group='distutils.commands', name=command)851        for ep in eps:852            self.cmdclass[command] = cmdclass = ep.load()853            return cmdclass854        else:855            return _Distribution.get_command_class(self, command)856 857    def print_commands(self):858        for ep in metadata.entry_points(group='distutils.commands'):859            if ep.name not in self.cmdclass:860                cmdclass = ep.load()861                self.cmdclass[ep.name] = cmdclass862        return _Distribution.print_commands(self)863 864    def get_command_list(self):865        for ep in metadata.entry_points(group='distutils.commands'):866            if ep.name not in self.cmdclass:867                cmdclass = ep.load()868                self.cmdclass[ep.name] = cmdclass869        return _Distribution.get_command_list(self)870 871    def include(self, **attrs) -> None:872        """Add items to distribution that are named in keyword arguments873 874        For example, 'dist.include(py_modules=["x"])' would add 'x' to875        the distribution's 'py_modules' attribute, if it was not already876        there.877 878        Currently, this method only supports inclusion for attributes that are879        lists or tuples.  If you need to add support for adding to other880        attributes in this or a subclass, you can add an '_include_X' method,881        where 'X' is the name of the attribute.  The method will be called with882        the value passed to 'include()'.  So, 'dist.include(foo={"bar":"baz"})'883        will try to call 'dist._include_foo({"bar":"baz"})', which can then884        handle whatever special inclusion logic is needed.885        """886        for k, v in attrs.items():887            include = getattr(self, '_include_' + k, None)888            if include:889                include(v)890            else:891                self._include_misc(k, v)892 893    def exclude_package(self, package: str) -> None:894        """Remove packages, modules, and extensions in named package"""895 896        pfx = package + '.'897        if self.packages:898            self.packages = [899                p for p in self.packages if p != package and not p.startswith(pfx)900            ]901 902        if self.py_modules:903            self.py_modules = [904                p for p in self.py_modules if p != package and not p.startswith(pfx)905            ]906 907        if self.ext_modules:908            self.ext_modules = [909                p910                for p in self.ext_modules911                if p.name != package and not p.name.startswith(pfx)912            ]913 914    def has_contents_for(self, package: str) -> bool:915        """Return true if 'exclude_package(package)' would do something"""916 917        pfx = package + '.'918 919        for p in self.iter_distribution_names():920            if p == package or p.startswith(pfx):921                return True922 923        return False924 925    def _exclude_misc(self, name: str, value: _Sequence) -> None:926        """Handle 'exclude()' for list/tuple attrs without a special handler"""927        if not isinstance(value, _sequence):928            raise DistutilsSetupError(929                f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"930            )931        try:932            old = getattr(self, name)933        except AttributeError as e:934            raise DistutilsSetupError(f"{name}: No such distribution setting") from e935        if old is not None and not isinstance(old, _sequence):936            raise DistutilsSetupError(937                name + ": this setting cannot be changed via include/exclude"938            )939        elif old:940            setattr(self, name, [item for item in old if item not in value])941 942    def _include_misc(self, name: str, value: _Sequence) -> None:943        """Handle 'include()' for list/tuple attrs without a special handler"""944 945        if not isinstance(value, _sequence):946            raise DistutilsSetupError(947                f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"948            )949        try:950            old = getattr(self, name)951        except AttributeError as e:952            raise DistutilsSetupError(f"{name}: No such distribution setting") from e953        if old is None:954            setattr(self, name, value)955        elif not isinstance(old, _sequence):956            raise DistutilsSetupError(957                name + ": this setting cannot be changed via include/exclude"958            )959        else:960            new = [item for item in value if item not in old]961            setattr(self, name, list(old) + new)962 963    def exclude(self, **attrs) -> None:964        """Remove items from distribution that are named in keyword arguments965 966        For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from967        the distribution's 'py_modules' attribute.  Excluding packages uses968        the 'exclude_package()' method, so all of the package's contained969        packages, modules, and extensions are also excluded.970 971        Currently, this method only supports exclusion from attributes that are972        lists or tuples.  If you need to add support for excluding from other973        attributes in this or a subclass, you can add an '_exclude_X' method,974        where 'X' is the name of the attribute.  The method will be called with975        the value passed to 'exclude()'.  So, 'dist.exclude(foo={"bar":"baz"})'976        will try to call 'dist._exclude_foo({"bar":"baz"})', which can then977        handle whatever special exclusion logic is needed.978        """979        for k, v in attrs.items():980            exclude = getattr(self, '_exclude_' + k, None)981            if exclude:982                exclude(v)983            else:984                self._exclude_misc(k, v)985 986    def _exclude_packages(self, packages: _Sequence) -> None:987        if not isinstance(packages, _sequence):988            raise DistutilsSetupError(989                f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})"990            )991        list(map(self.exclude_package, packages))992 993    def _parse_command_opts(self, parser, args):994        # Remove --with-X/--without-X options when processing command args995        self.global_options = self.__class__.global_options996        self.negative_opt = self.__class__.negative_opt997 998        # First, expand any aliases999        command = args[0]1000        aliases = self.get_option_dict('aliases')1001        while command in aliases:1002            _src, alias = aliases[command]1003            del aliases[command]  # ensure each alias can expand only once!1004            import shlex1005 1006            args[:1] = shlex.split(alias, True)1007            command = args[0]1008 1009        nargs = _Distribution._parse_command_opts(self, parser, args)1010 1011        # Handle commands that want to consume all remaining arguments1012        cmd_class = self.get_command_class(command)1013        if getattr(cmd_class, 'command_consumes_arguments', None):1014            self.get_option_dict(command)['args'] = ("command line", nargs)1015            if nargs is not None:1016                return []1017 1018        return nargs1019 1020    def get_cmdline_options(self) -> dict[str, dict[str, str | None]]:1021        """Return a '{cmd: {opt:val}}' map of all command-line options1022 1023        Option names are all long, but do not include the leading '--', and1024        contain dashes rather than underscores.  If the option doesn't take1025        an argument (e.g. '--quiet'), the 'val' is 'None'.1026 1027        Note that options provided by config files are intentionally excluded.1028        """1029 1030        d: dict[str, dict[str, str | None]] = {}1031 1032        for cmd, opts in self.command_options.items():1033            val: str | None1034            for opt, (src, val) in opts.items():1035                if src != "command line":1036                    continue1037 1038                opt = opt.replace('_', '-')1039 1040                if val == 0:1041                    cmdobj = self.get_command_obj(cmd)1042                    neg_opt = self.negative_opt.copy()1043                    neg_opt.update(getattr(cmdobj, 'negative_opt', {}))1044                    for neg, pos in neg_opt.items():1045                        if pos == opt:1046                            opt = neg1047                            val = None1048                            break1049                    else:1050                        raise AssertionError("Shouldn't be able to get here")1051 1052                elif val == 1:1053                    val = None1054 1055                d.setdefault(cmd, {})[opt] = val1056 1057        return d1058 1059    def iter_distribution_names(self) -> Iterator[str]:1060        """Yield all packages, modules, and extension names in distribution"""1061 1062        yield from self.packages or ()1063 1064        yield from self.py_modules or ()1065 1066        for ext in self.ext_modules or ():1067            if isinstance(ext, tuple):1068                name, _buildinfo = ext1069            else:1070                name = ext.name1071            name = name.removesuffix('module')1072            yield name1073 1074    def handle_display_options(self, option_order):1075        """If there were any non-global "display-only" options1076        (--help-commands or the metadata display options) on the command1077        line, display the requested info and return true; else return1078        false.1079        """1080        import sys1081 1082        if self.help_commands:1083            return _Distribution.handle_display_options(self, option_order)1084 1085        # Stdout may be StringIO (e.g. in tests)1086        if not isinstance(sys.stdout, io.TextIOWrapper):1087            return _Distribution.handle_display_options(self, option_order)1088 1089        # Don't wrap stdout if utf-8 is already the encoding. Provides1090        #  workaround for #334.1091        if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):1092            return _Distribution.handle_display_options(self, option_order)1093 1094        # Print metadata in UTF-8 no matter the platform1095        encoding = sys.stdout.encoding1096        sys.stdout.reconfigure(encoding='utf-8')1097        try:1098            return _Distribution.handle_display_options(self, option_order)1099        finally:1100            sys.stdout.reconfigure(encoding=encoding)1101 1102    def run_command(self, command) -> None:1103        self.set_defaults()1104        # Postpone defaults until all explicit configuration is considered1105        # (setup() args, config files, command line and plugins)1106 1107        super().run_command(command)1108 1109 1110@functools.cache1111def _setuptools_commands() -> set[str]:1112    try:1113        # Use older API for importlib.metadata compatibility1114        entry_points = metadata.distribution('setuptools').entry_points1115        eps: Iterable[str] = (ep.name for ep in entry_points)1116    except metadata.PackageNotFoundError:1117        # during bootstrapping, distribution doesn't exist1118        eps = []1119    return {*distutils.command.__all__, *eps}1120 1121 1122class DistDeprecationWarning(SetuptoolsDeprecationWarning):1123    """Class for warning about deprecations in dist in1124    setuptools. Not ignored by default, unlike DeprecationWarning."""1125 
codekingpro/portable-devtools · Team Ai