Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
setupcfg.py783 linesDownload Raw Back to config
1"""2Load setuptools configuration from ``setup.cfg`` files.3 4**API will be made private in the future**5 6To read project metadata, consider using7``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).8For simple scenarios, you can also try parsing the file directly9with the help of ``configparser``.10"""11 12from __future__ import annotations13 14import contextlib15import functools16import os17from abc import abstractmethod18from collections import defaultdict19from collections.abc import Iterable, Iterator20from functools import partial, wraps21from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, cast22 23from packaging.markers import default_environment as marker_env24from packaging.requirements import InvalidRequirement, Requirement25from packaging.version import InvalidVersion, Version26 27from .. import _static28from .._path import StrPath29from ..errors import FileError, OptionError30from ..warnings import SetuptoolsDeprecationWarning31from . import expand32 33if TYPE_CHECKING:34    from typing_extensions import TypeAlias35 36    from setuptools.dist import Distribution37 38    from distutils.dist import DistributionMetadata39 40SingleCommandOptions: TypeAlias = dict[str, tuple[str, Any]]41"""Dict that associate the name of the options of a particular command to a42tuple. The first element of the tuple indicates the origin of the option value43(e.g. the name of the configuration file where it was read from),44while the second element of the tuple is the option value itself45"""46AllCommandOptions: TypeAlias = dict[str, SingleCommandOptions]47"""cmd name => its options"""48Target = TypeVar("Target", "Distribution", "DistributionMetadata")49 50 51def read_configuration(52    filepath: StrPath, find_others: bool = False, ignore_option_errors: bool = False53) -> dict:54    """Read given configuration file and returns options from it as a dict.55 56    :param str|unicode filepath: Path to configuration file57        to get options from.58 59    :param bool find_others: Whether to search for other configuration files60        which could be on in various places.61 62    :param bool ignore_option_errors: Whether to silently ignore63        options, values of which could not be resolved (e.g. due to exceptions64        in directives such as file:, attr:, etc.).65        If False exceptions are propagated as expected.66 67    :rtype: dict68    """69    from setuptools.dist import Distribution70 71    dist = Distribution()72    filenames = dist.find_config_files() if find_others else []73    handlers = _apply(dist, filepath, filenames, ignore_option_errors)74    return configuration_to_dict(handlers)75 76 77def apply_configuration(dist: Distribution, filepath: StrPath) -> Distribution:78    """Apply the configuration from a ``setup.cfg`` file into an existing79    distribution object.80    """81    _apply(dist, filepath)82    dist._finalize_requires()83    return dist84 85 86def _apply(87    dist: Distribution,88    filepath: StrPath,89    other_files: Iterable[StrPath] = (),90    ignore_option_errors: bool = False,91) -> tuple[ConfigMetadataHandler, ConfigOptionsHandler]:92    """Read configuration from ``filepath`` and applies to the ``dist`` object."""93    from setuptools.dist import _Distribution94 95    filepath = os.path.abspath(filepath)96 97    if not os.path.isfile(filepath):98        raise FileError(f'Configuration file {filepath} does not exist.')99 100    current_directory = os.getcwd()101    os.chdir(os.path.dirname(filepath))102    filenames = [*other_files, filepath]103 104    try:105        # TODO: Temporary cast until mypy 1.12 is released with upstream fixes from typeshed106        _Distribution.parse_config_files(dist, filenames=cast(list[str], filenames))107        handlers = parse_configuration(108            dist, dist.command_options, ignore_option_errors=ignore_option_errors109        )110        dist._finalize_license_files()111    finally:112        os.chdir(current_directory)113 114    return handlers115 116 117def _get_option(target_obj: Distribution | DistributionMetadata, key: str):118    """119    Given a target object and option key, get that option from120    the target object, either through a get_{key} method or121    from an attribute directly.122    """123    getter_name = f'get_{key}'124    by_attribute = functools.partial(getattr, target_obj, key)125    getter = getattr(target_obj, getter_name, by_attribute)126    return getter()127 128 129def configuration_to_dict(130    handlers: Iterable[131        ConfigHandler[Distribution] | ConfigHandler[DistributionMetadata]132    ],133) -> dict:134    """Returns configuration data gathered by given handlers as a dict.135 136    :param Iterable[ConfigHandler] handlers: Handlers list,137        usually from parse_configuration()138 139    :rtype: dict140    """141    config_dict: dict = defaultdict(dict)142 143    for handler in handlers:144        for option in handler.set_options:145            value = _get_option(handler.target_obj, option)146            config_dict[handler.section_prefix][option] = value147 148    return config_dict149 150 151def parse_configuration(152    distribution: Distribution,153    command_options: AllCommandOptions,154    ignore_option_errors: bool = False,155) -> tuple[ConfigMetadataHandler, ConfigOptionsHandler]:156    """Performs additional parsing of configuration options157    for a distribution.158 159    Returns a list of used option handlers.160 161    :param Distribution distribution:162    :param dict command_options:163    :param bool ignore_option_errors: Whether to silently ignore164        options, values of which could not be resolved (e.g. due to exceptions165        in directives such as file:, attr:, etc.).166        If False exceptions are propagated as expected.167    :rtype: list168    """169    with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered:170        options = ConfigOptionsHandler(171            distribution,172            command_options,173            ignore_option_errors,174            ensure_discovered,175        )176 177        options.parse()178        if not distribution.package_dir:179            distribution.package_dir = options.package_dir  # Filled by `find_packages`180 181        meta = ConfigMetadataHandler(182            distribution.metadata,183            command_options,184            ignore_option_errors,185            ensure_discovered,186            distribution.package_dir,187            distribution.src_root,188        )189        meta.parse()190        distribution._referenced_files.update(191            options._referenced_files, meta._referenced_files192        )193 194    return meta, options195 196 197def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list):198    """Because users sometimes misinterpret this configuration:199 200    [options.extras_require]201    foo = bar;python_version<"4"202 203    It looks like one requirement with an environment marker204    but because there is no newline, it's parsed as two requirements205    with a semicolon as separator.206 207    Therefore, if:208        * input string does not contain a newline AND209        * parsed result contains two requirements AND210        * parsing of the two parts from the result ("<first>;<second>")211        leads in a valid Requirement with a valid marker212    a UserWarning is shown to inform the user about the possible problem.213    """214    if "\n" in orig_value or len(parsed) != 2:215        return216 217    markers = marker_env().keys()218 219    try:220        req = Requirement(parsed[1])221        if req.name in markers:222            _AmbiguousMarker.emit(field=label, req=parsed[1])223    except InvalidRequirement as ex:224        if any(parsed[1].startswith(marker) for marker in markers):225            msg = _AmbiguousMarker.message(field=label, req=parsed[1])226            raise InvalidRequirement(msg) from ex227 228 229class ConfigHandler(Generic[Target]):230    """Handles metadata supplied in configuration files."""231 232    section_prefix: str233    """Prefix for config sections handled by this handler.234    Must be provided by class heirs.235 236    """237 238    aliases: ClassVar[dict[str, str]] = {}239    """Options aliases.240    For compatibility with various packages. E.g.: d2to1 and pbr.241    Note: `-` in keys is replaced with `_` by config parser.242 243    """244 245    def __init__(246        self,247        target_obj: Target,248        options: AllCommandOptions,249        ignore_option_errors,250        ensure_discovered: expand.EnsurePackagesDiscovered,251    ) -> None:252        self.ignore_option_errors = ignore_option_errors253        self.target_obj: Target = target_obj254        self.sections = dict(self._section_options(options))255        self.set_options: list[str] = []256        self.ensure_discovered = ensure_discovered257        self._referenced_files = set[str]()258        """After parsing configurations, this property will enumerate259        all files referenced by the "file:" directive. Private API for setuptools only.260        """261 262    @classmethod263    def _section_options(264        cls, options: AllCommandOptions265    ) -> Iterator[tuple[str, SingleCommandOptions]]:266        for full_name, value in options.items():267            pre, _sep, name = full_name.partition(cls.section_prefix)268            if pre:269                continue270            yield name.lstrip('.'), value271 272    @property273    @abstractmethod274    def parsers(self) -> dict[str, Callable]:275        """Metadata item name to parser function mapping."""276        raise NotImplementedError(277            f'{self.__class__.__name__} must provide .parsers property'278        )279 280    def __setitem__(self, option_name, value) -> None:281        target_obj = self.target_obj282 283        # Translate alias into real name.284        option_name = self.aliases.get(option_name, option_name)285 286        try:287            current_value = getattr(target_obj, option_name)288        except AttributeError as e:289            raise KeyError(option_name) from e290 291        if current_value:292            # Already inhabited. Skipping.293            return294 295        try:296            parsed = self.parsers.get(option_name, lambda x: x)(value)297        except (Exception,) * self.ignore_option_errors:298            return299 300        simple_setter = functools.partial(target_obj.__setattr__, option_name)301        setter = getattr(target_obj, f"set_{option_name}", simple_setter)302        setter(parsed)303 304        self.set_options.append(option_name)305 306    @classmethod307    def _parse_list(cls, value, separator=','):308        """Represents value as a list.309 310        Value is split either by separator (defaults to comma) or by lines.311 312        :param value:313        :param separator: List items separator character.314        :rtype: list315        """316        if isinstance(value, list):  # _get_parser_compound case317            return value318 319        if '\n' in value:320            value = value.splitlines()321        else:322            value = value.split(separator)323 324        return [chunk.strip() for chunk in value if chunk.strip()]325 326    @classmethod327    def _parse_dict(cls, value):328        """Represents value as a dict.329 330        :param value:331        :rtype: dict332        """333        separator = '='334        result = {}335        for line in cls._parse_list(value):336            key, sep, val = line.partition(separator)337            if sep != separator:338                raise OptionError(f"Unable to parse option value to dict: {value}")339            result[key.strip()] = val.strip()340 341        return result342 343    @classmethod344    def _parse_bool(cls, value):345        """Represents value as boolean.346 347        :param value:348        :rtype: bool349        """350        value = value.lower()351        return value in ('1', 'true', 'yes')352 353    @classmethod354    def _exclude_files_parser(cls, key):355        """Returns a parser function to make sure field inputs356        are not files.357 358        Parses a value after getting the key so error messages are359        more informative.360 361        :param key:362        :rtype: callable363        """364 365        def parser(value):366            exclude_directive = 'file:'367            if value.startswith(exclude_directive):368                raise ValueError(369                    f'Only strings are accepted for the {key} field, '370                    'files are not accepted'371                )372            return _static.Str(value)373 374        return parser375 376    def _parse_file(self, value, root_dir: StrPath | None):377        """Represents value as a string, allowing including text378        from nearest files using `file:` directive.379 380        Directive is sandboxed and won't reach anything outside381        directory with setup.py.382 383        Examples:384            file: README.rst, CHANGELOG.md, src/file.txt385 386        :param str value:387        :rtype: str388        """389        include_directive = 'file:'390 391        if not isinstance(value, str):392            return value393 394        if not value.startswith(include_directive):395            return _static.Str(value)396 397        spec = value[len(include_directive) :]398        filepaths = [path.strip() for path in spec.split(',')]399        self._referenced_files.update(filepaths)400        # XXX: Is marking as static contents coming from files too optimistic?401        return _static.Str(expand.read_files(filepaths, root_dir))402 403    def _parse_attr(self, value, package_dir, root_dir: StrPath):404        """Represents value as a module attribute.405 406        Examples:407            attr: package.attr408            attr: package.module.attr409 410        :param str value:411        :rtype: str412        """413        attr_directive = 'attr:'414        if not value.startswith(attr_directive):415            return _static.Str(value)416 417        attr_desc = value.replace(attr_directive, '')418 419        # Make sure package_dir is populated correctly, so `attr:` directives can work420        package_dir.update(self.ensure_discovered.package_dir)421        return expand.read_attr(attr_desc, package_dir, root_dir)422 423    @classmethod424    def _get_parser_compound(cls, *parse_methods):425        """Returns parser function to represents value as a list.426 427        Parses a value applying given methods one after another.428 429        :param parse_methods:430        :rtype: callable431        """432 433        def parse(value):434            parsed = value435 436            for method in parse_methods:437                parsed = method(parsed)438 439            return parsed440 441        return parse442 443    @classmethod444    def _parse_section_to_dict_with_key(cls, section_options, values_parser):445        """Parses section options into a dictionary.446 447        Applies a given parser to each option in a section.448 449        :param dict section_options:450        :param callable values_parser: function with 2 args corresponding to key, value451        :rtype: dict452        """453        value = {}454        for key, (_, val) in section_options.items():455            value[key] = values_parser(key, val)456        return value457 458    @classmethod459    def _parse_section_to_dict(cls, section_options, values_parser=None):460        """Parses section options into a dictionary.461 462        Optionally applies a given parser to each value.463 464        :param dict section_options:465        :param callable values_parser: function with 1 arg corresponding to option value466        :rtype: dict467        """468        parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v)469        return cls._parse_section_to_dict_with_key(section_options, parser)470 471    def parse_section(self, section_options) -> None:472        """Parses configuration file section.473 474        :param dict section_options:475        """476        for name, (_, value) in section_options.items():477            with contextlib.suppress(KeyError):478                # Keep silent for a new option may appear anytime.479                self[name] = value480 481    def parse(self) -> None:482        """Parses configuration file items from one483        or more related sections.484 485        """486        for section_name, section_options in self.sections.items():487            method_postfix = ''488            if section_name:  # [section.option] variant489                method_postfix = f"_{section_name}"490 491            section_parser_method: Callable | None = getattr(492                self,493                # Dots in section names are translated into dunderscores.494                f'parse_section{method_postfix}'.replace('.', '__'),495                None,496            )497 498            if section_parser_method is None:499                raise OptionError(500                    "Unsupported distribution option section: "501                    f"[{self.section_prefix}.{section_name}]"502                )503 504            section_parser_method(section_options)505 506    def _deprecated_config_handler(self, func, msg, **kw):507        """this function will wrap around parameters that are deprecated508 509        :param msg: deprecation message510        :param func: function to be wrapped around511        """512 513        @wraps(func)514        def config_handler(*args, **kwargs):515            kw.setdefault("stacklevel", 2)516            _DeprecatedConfig.emit("Deprecated config in `setup.cfg`", msg, **kw)517            return func(*args, **kwargs)518 519        return config_handler520 521 522class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):523    section_prefix = 'metadata'524 525    aliases = {526        'home_page': 'url',527        'summary': 'description',528        'classifier': 'classifiers',529        'platform': 'platforms',530    }531 532    strict_mode = False533    """We need to keep it loose, to be partially compatible with534    `pbr` and `d2to1` packages which also uses `metadata` section.535 536    """537 538    def __init__(539        self,540        target_obj: DistributionMetadata,541        options: AllCommandOptions,542        ignore_option_errors: bool,543        ensure_discovered: expand.EnsurePackagesDiscovered,544        package_dir: dict | None = None,545        root_dir: StrPath | None = os.curdir,546    ) -> None:547        super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)548        self.package_dir = package_dir549        self.root_dir = root_dir550 551    @property552    def parsers(self) -> dict[str, Callable]:553        """Metadata item name to parser function mapping."""554        parse_list_static = self._get_parser_compound(self._parse_list, _static.List)555        parse_dict_static = self._get_parser_compound(self._parse_dict, _static.Dict)556        parse_file = partial(self._parse_file, root_dir=self.root_dir)557        exclude_files_parser = self._exclude_files_parser558 559        return {560            'author': _static.Str,561            'author_email': _static.Str,562            'maintainer': _static.Str,563            'maintainer_email': _static.Str,564            'platforms': parse_list_static,565            'keywords': parse_list_static,566            'provides': parse_list_static,567            'obsoletes': parse_list_static,568            'classifiers': self._get_parser_compound(parse_file, parse_list_static),569            'license': exclude_files_parser('license'),570            'license_files': parse_list_static,571            'description': parse_file,572            'long_description': parse_file,573            'long_description_content_type': _static.Str,574            'version': self._parse_version,  # Cannot be marked as dynamic575            'url': _static.Str,576            'project_urls': parse_dict_static,577        }578 579    def _parse_version(self, value):580        """Parses `version` option value.581 582        :param value:583        :rtype: str584 585        """586        version = self._parse_file(value, self.root_dir)587 588        if version != value:589            version = version.strip()590            # Be strict about versions loaded from file because it's easy to591            # accidentally include newlines and other unintended content592            try:593                Version(version)594            except InvalidVersion as e:595                raise OptionError(596                    f'Version loaded from {value} does not '597                    f'comply with PEP 440: {version}'598                ) from e599 600            return version601 602        return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))603 604 605class ConfigOptionsHandler(ConfigHandler["Distribution"]):606    section_prefix = 'options'607 608    def __init__(609        self,610        target_obj: Distribution,611        options: AllCommandOptions,612        ignore_option_errors: bool,613        ensure_discovered: expand.EnsurePackagesDiscovered,614    ) -> None:615        super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)616        self.root_dir = target_obj.src_root617        self.package_dir: dict[str, str] = {}  # To be filled by `find_packages`618 619    @classmethod620    def _parse_list_semicolon(cls, value):621        return cls._parse_list(value, separator=';')622 623    def _parse_file_in_root(self, value):624        return self._parse_file(value, root_dir=self.root_dir)625 626    def _parse_requirements_list(self, label: str, value: str):627        # Parse a requirements list, either by reading in a `file:`, or a list.628        parsed = self._parse_list_semicolon(self._parse_file_in_root(value))629        _warn_accidental_env_marker_misconfig(label, value, parsed)630        # Filter it to only include lines that are not comments. `parse_list`631        # will have stripped each line and filtered out empties.632        return _static.List(line for line in parsed if not line.startswith("#"))633        # ^-- Use `_static.List` to mark a non-`Dynamic` Core Metadata634 635    @property636    def parsers(self) -> dict[str, Callable]:637        """Metadata item name to parser function mapping."""638        parse_list = self._parse_list639        parse_bool = self._parse_bool640        parse_cmdclass = self._parse_cmdclass641 642        return {643            'zip_safe': parse_bool,644            'include_package_data': parse_bool,645            'package_dir': self._parse_dict,646            'scripts': parse_list,647            'eager_resources': parse_list,648            'dependency_links': parse_list,649            'namespace_packages': self._deprecated_config_handler(650                parse_list,651                "The namespace_packages parameter is deprecated, "652                "consider using implicit namespaces instead (PEP 420).",653                # TODO: define due date, see setuptools.dist:check_nsp.654            ),655            'install_requires': partial(  # Core Metadata656                self._parse_requirements_list, "install_requires"657            ),658            'setup_requires': self._parse_list_semicolon,659            'packages': self._parse_packages,660            'entry_points': self._parse_file_in_root,661            'py_modules': parse_list,662            'python_requires': _static.SpecifierSet,  # Core Metadata663            'cmdclass': parse_cmdclass,664        }665 666    def _parse_cmdclass(self, value):667        package_dir = self.ensure_discovered.package_dir668        return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)669 670    def _parse_packages(self, value):671        """Parses `packages` option value.672 673        :param value:674        :rtype: list675        """676        find_directives = ['find:', 'find_namespace:']677        trimmed_value = value.strip()678 679        if trimmed_value not in find_directives:680            return self._parse_list(value)681 682        # Read function arguments from a dedicated section.683        find_kwargs = self.parse_section_packages__find(684            self.sections.get('packages.find', {})685        )686 687        find_kwargs.update(688            namespaces=(trimmed_value == find_directives[1]),689            root_dir=self.root_dir,690            fill_package_dir=self.package_dir,691        )692 693        return expand.find_packages(**find_kwargs)694 695    def parse_section_packages__find(self, section_options):696        """Parses `packages.find` configuration file section.697 698        To be used in conjunction with _parse_packages().699 700        :param dict section_options:701        """702        section_data = self._parse_section_to_dict(section_options, self._parse_list)703 704        valid_keys = ['where', 'include', 'exclude']705        find_kwargs = {k: v for k, v in section_data.items() if k in valid_keys and v}706 707        where = find_kwargs.get('where')708        if where is not None:709            find_kwargs['where'] = where[0]  # cast list to single val710 711        return find_kwargs712 713    def parse_section_entry_points(self, section_options) -> None:714        """Parses `entry_points` configuration file section.715 716        :param dict section_options:717        """718        parsed = self._parse_section_to_dict(section_options, self._parse_list)719        self['entry_points'] = parsed720 721    def _parse_package_data(self, section_options):722        package_data = self._parse_section_to_dict(section_options, self._parse_list)723        return expand.canonic_package_data(package_data)724 725    def parse_section_package_data(self, section_options) -> None:726        """Parses `package_data` configuration file section.727 728        :param dict section_options:729        """730        self['package_data'] = self._parse_package_data(section_options)731 732    def parse_section_exclude_package_data(self, section_options) -> None:733        """Parses `exclude_package_data` configuration file section.734 735        :param dict section_options:736        """737        self['exclude_package_data'] = self._parse_package_data(section_options)738 739    def parse_section_extras_require(self, section_options) -> None:  # Core Metadata740        """Parses `extras_require` configuration file section.741 742        :param dict section_options:743        """744        parsed = self._parse_section_to_dict_with_key(745            section_options,746            lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v),747        )748 749        self['extras_require'] = _static.Dict(parsed)750        # ^-- Use `_static.Dict` to mark a non-`Dynamic` Core Metadata751 752    def parse_section_data_files(self, section_options) -> None:753        """Parses `data_files` configuration file section.754 755        :param dict section_options:756        """757        parsed = self._parse_section_to_dict(section_options, self._parse_list)758        self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)759 760 761class _AmbiguousMarker(SetuptoolsDeprecationWarning):762    _SUMMARY = "Ambiguous requirement marker."763    _DETAILS = """764    One of the parsed requirements in `{field}` looks like a valid environment marker:765 766        {req!r}767 768    Please make sure that the configuration file is correct.769    You can use dangling lines to avoid this problem.770    """771    _SEE_DOCS = "userguide/declarative_config.html#opt-2"772    # TODO: should we include due_date here? Initially introduced in 6 Aug 2022.773    # Does this make sense with latest version of packaging?774 775    @classmethod776    def message(cls, **kw):777        docs = f"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}"778        return cls._format(cls._SUMMARY, cls._DETAILS, see_url=docs, format_args=kw)779 780 781class _DeprecatedConfig(SetuptoolsDeprecationWarning):782    _SEE_DOCS = "userguide/declarative_config.html"783 
codekingpro/portable-devtools · Team Ai