Path: blob/main/test/lib/python3.9/site-packages/setuptools/config/setupcfg.py
4799 views
"""Load setuptools configuration from ``setup.cfg`` files"""1import os23import warnings4import functools5from collections import defaultdict6from functools import partial7from functools import wraps8from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List,9Optional, Tuple, TypeVar, Union)1011from distutils.errors import DistutilsOptionError, DistutilsFileError12from setuptools.extern.packaging.version import Version, InvalidVersion13from setuptools.extern.packaging.specifiers import SpecifierSet14from setuptools._deprecation_warning import SetuptoolsDeprecationWarning1516from . import expand1718if TYPE_CHECKING:19from setuptools.dist import Distribution # noqa20from distutils.dist import DistributionMetadata # noqa2122_Path = Union[str, os.PathLike]23SingleCommandOptions = Dict["str", Tuple["str", Any]]24"""Dict that associate the name of the options of a particular command to a25tuple. The first element of the tuple indicates the origin of the option value26(e.g. the name of the configuration file where it was read from),27while the second element of the tuple is the option value itself28"""29AllCommandOptions = Dict["str", SingleCommandOptions] # cmd name => its options30Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"])313233def read_configuration(34filepath: _Path,35find_others=False,36ignore_option_errors=False37) -> dict:38"""Read given configuration file and returns options from it as a dict.3940:param str|unicode filepath: Path to configuration file41to get options from.4243:param bool find_others: Whether to search for other configuration files44which could be on in various places.4546:param bool ignore_option_errors: Whether to silently ignore47options, values of which could not be resolved (e.g. due to exceptions48in directives such as file:, attr:, etc.).49If False exceptions are propagated as expected.5051:rtype: dict52"""53from setuptools.dist import Distribution5455dist = Distribution()56filenames = dist.find_config_files() if find_others else []57handlers = _apply(dist, filepath, filenames, ignore_option_errors)58return configuration_to_dict(handlers)596061def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":62"""Apply the configuration from a ``setup.cfg`` file into an existing63distribution object.64"""65_apply(dist, filepath)66dist._finalize_requires()67return dist686970def _apply(71dist: "Distribution", filepath: _Path,72other_files: Iterable[_Path] = (),73ignore_option_errors: bool = False,74) -> Tuple["ConfigHandler", ...]:75"""Read configuration from ``filepath`` and applies to the ``dist`` object."""76from setuptools.dist import _Distribution7778filepath = os.path.abspath(filepath)7980if not os.path.isfile(filepath):81raise DistutilsFileError('Configuration file %s does not exist.' % filepath)8283current_directory = os.getcwd()84os.chdir(os.path.dirname(filepath))85filenames = [*other_files, filepath]8687try:88_Distribution.parse_config_files(dist, filenames=filenames)89handlers = parse_configuration(90dist, dist.command_options, ignore_option_errors=ignore_option_errors91)92dist._finalize_license_files()93finally:94os.chdir(current_directory)9596return handlers979899def _get_option(target_obj: Target, key: str):100"""101Given a target object and option key, get that option from102the target object, either through a get_{key} method or103from an attribute directly.104"""105getter_name = 'get_{key}'.format(**locals())106by_attribute = functools.partial(getattr, target_obj, key)107getter = getattr(target_obj, getter_name, by_attribute)108return getter()109110111def configuration_to_dict(handlers: Tuple["ConfigHandler", ...]) -> dict:112"""Returns configuration data gathered by given handlers as a dict.113114:param list[ConfigHandler] handlers: Handlers list,115usually from parse_configuration()116117:rtype: dict118"""119config_dict: dict = defaultdict(dict)120121for handler in handlers:122for option in handler.set_options:123value = _get_option(handler.target_obj, option)124config_dict[handler.section_prefix][option] = value125126return config_dict127128129def parse_configuration(130distribution: "Distribution",131command_options: AllCommandOptions,132ignore_option_errors=False133) -> Tuple["ConfigMetadataHandler", "ConfigOptionsHandler"]:134"""Performs additional parsing of configuration options135for a distribution.136137Returns a list of used option handlers.138139:param Distribution distribution:140:param dict command_options:141:param bool ignore_option_errors: Whether to silently ignore142options, values of which could not be resolved (e.g. due to exceptions143in directives such as file:, attr:, etc.).144If False exceptions are propagated as expected.145:rtype: list146"""147with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered:148options = ConfigOptionsHandler(149distribution,150command_options,151ignore_option_errors,152ensure_discovered,153)154155options.parse()156if not distribution.package_dir:157distribution.package_dir = options.package_dir # Filled by `find_packages`158159meta = ConfigMetadataHandler(160distribution.metadata,161command_options,162ignore_option_errors,163ensure_discovered,164distribution.package_dir,165distribution.src_root,166)167meta.parse()168169return meta, options170171172class ConfigHandler(Generic[Target]):173"""Handles metadata supplied in configuration files."""174175section_prefix: str176"""Prefix for config sections handled by this handler.177Must be provided by class heirs.178179"""180181aliases: Dict[str, str] = {}182"""Options aliases.183For compatibility with various packages. E.g.: d2to1 and pbr.184Note: `-` in keys is replaced with `_` by config parser.185186"""187188def __init__(189self,190target_obj: Target,191options: AllCommandOptions,192ignore_option_errors,193ensure_discovered: expand.EnsurePackagesDiscovered,194):195sections: AllCommandOptions = {}196197section_prefix = self.section_prefix198for section_name, section_options in options.items():199if not section_name.startswith(section_prefix):200continue201202section_name = section_name.replace(section_prefix, '').strip('.')203sections[section_name] = section_options204205self.ignore_option_errors = ignore_option_errors206self.target_obj = target_obj207self.sections = sections208self.set_options: List[str] = []209self.ensure_discovered = ensure_discovered210211@property212def parsers(self):213"""Metadata item name to parser function mapping."""214raise NotImplementedError(215'%s must provide .parsers property' % self.__class__.__name__216)217218def __setitem__(self, option_name, value):219unknown = tuple()220target_obj = self.target_obj221222# Translate alias into real name.223option_name = self.aliases.get(option_name, option_name)224225current_value = getattr(target_obj, option_name, unknown)226227if current_value is unknown:228raise KeyError(option_name)229230if current_value:231# Already inhabited. Skipping.232return233234skip_option = False235parser = self.parsers.get(option_name)236if parser:237try:238value = parser(value)239240except Exception:241skip_option = True242if not self.ignore_option_errors:243raise244245if skip_option:246return247248setter = getattr(target_obj, 'set_%s' % option_name, None)249if setter is None:250setattr(target_obj, option_name, value)251else:252setter(value)253254self.set_options.append(option_name)255256@classmethod257def _parse_list(cls, value, separator=','):258"""Represents value as a list.259260Value is split either by separator (defaults to comma) or by lines.261262:param value:263:param separator: List items separator character.264:rtype: list265"""266if isinstance(value, list): # _get_parser_compound case267return value268269if '\n' in value:270value = value.splitlines()271else:272value = value.split(separator)273274return [chunk.strip() for chunk in value if chunk.strip()]275276@classmethod277def _parse_dict(cls, value):278"""Represents value as a dict.279280:param value:281:rtype: dict282"""283separator = '='284result = {}285for line in cls._parse_list(value):286key, sep, val = line.partition(separator)287if sep != separator:288raise DistutilsOptionError(289'Unable to parse option value to dict: %s' % value290)291result[key.strip()] = val.strip()292293return result294295@classmethod296def _parse_bool(cls, value):297"""Represents value as boolean.298299:param value:300:rtype: bool301"""302value = value.lower()303return value in ('1', 'true', 'yes')304305@classmethod306def _exclude_files_parser(cls, key):307"""Returns a parser function to make sure field inputs308are not files.309310Parses a value after getting the key so error messages are311more informative.312313:param key:314:rtype: callable315"""316317def parser(value):318exclude_directive = 'file:'319if value.startswith(exclude_directive):320raise ValueError(321'Only strings are accepted for the {0} field, '322'files are not accepted'.format(key)323)324return value325326return parser327328@classmethod329def _parse_file(cls, value, root_dir: _Path):330"""Represents value as a string, allowing including text331from nearest files using `file:` directive.332333Directive is sandboxed and won't reach anything outside334directory with setup.py.335336Examples:337file: README.rst, CHANGELOG.md, src/file.txt338339:param str value:340:rtype: str341"""342include_directive = 'file:'343344if not isinstance(value, str):345return value346347if not value.startswith(include_directive):348return value349350spec = value[len(include_directive) :]351filepaths = (path.strip() for path in spec.split(','))352return expand.read_files(filepaths, root_dir)353354def _parse_attr(self, value, package_dir, root_dir: _Path):355"""Represents value as a module attribute.356357Examples:358attr: package.attr359attr: package.module.attr360361:param str value:362:rtype: str363"""364attr_directive = 'attr:'365if not value.startswith(attr_directive):366return value367368attr_desc = value.replace(attr_directive, '')369370# Make sure package_dir is populated correctly, so `attr:` directives can work371package_dir.update(self.ensure_discovered.package_dir)372return expand.read_attr(attr_desc, package_dir, root_dir)373374@classmethod375def _get_parser_compound(cls, *parse_methods):376"""Returns parser function to represents value as a list.377378Parses a value applying given methods one after another.379380:param parse_methods:381:rtype: callable382"""383384def parse(value):385parsed = value386387for method in parse_methods:388parsed = method(parsed)389390return parsed391392return parse393394@classmethod395def _parse_section_to_dict(cls, section_options, values_parser=None):396"""Parses section options into a dictionary.397398Optionally applies a given parser to values.399400:param dict section_options:401:param callable values_parser:402:rtype: dict403"""404value = {}405values_parser = values_parser or (lambda val: val)406for key, (_, val) in section_options.items():407value[key] = values_parser(val)408return value409410def parse_section(self, section_options):411"""Parses configuration file section.412413:param dict section_options:414"""415for (name, (_, value)) in section_options.items():416try:417self[name] = value418419except KeyError:420pass # Keep silent for a new option may appear anytime.421422def parse(self):423"""Parses configuration file items from one424or more related sections.425426"""427for section_name, section_options in self.sections.items():428429method_postfix = ''430if section_name: # [section.option] variant431method_postfix = '_%s' % section_name432433section_parser_method: Optional[Callable] = getattr(434self,435# Dots in section names are translated into dunderscores.436('parse_section%s' % method_postfix).replace('.', '__'),437None,438)439440if section_parser_method is None:441raise DistutilsOptionError(442'Unsupported distribution option section: [%s.%s]'443% (self.section_prefix, section_name)444)445446section_parser_method(section_options)447448def _deprecated_config_handler(self, func, msg, warning_class):449"""this function will wrap around parameters that are deprecated450451:param msg: deprecation message452:param warning_class: class of warning exception to be raised453:param func: function to be wrapped around454"""455456@wraps(func)457def config_handler(*args, **kwargs):458warnings.warn(msg, warning_class)459return func(*args, **kwargs)460461return config_handler462463464class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):465466section_prefix = 'metadata'467468aliases = {469'home_page': 'url',470'summary': 'description',471'classifier': 'classifiers',472'platform': 'platforms',473}474475strict_mode = False476"""We need to keep it loose, to be partially compatible with477`pbr` and `d2to1` packages which also uses `metadata` section.478479"""480481def __init__(482self,483target_obj: "DistributionMetadata",484options: AllCommandOptions,485ignore_option_errors: bool,486ensure_discovered: expand.EnsurePackagesDiscovered,487package_dir: Optional[dict] = None,488root_dir: _Path = os.curdir489):490super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)491self.package_dir = package_dir492self.root_dir = root_dir493494@property495def parsers(self):496"""Metadata item name to parser function mapping."""497parse_list = self._parse_list498parse_file = partial(self._parse_file, root_dir=self.root_dir)499parse_dict = self._parse_dict500exclude_files_parser = self._exclude_files_parser501502return {503'platforms': parse_list,504'keywords': parse_list,505'provides': parse_list,506'requires': self._deprecated_config_handler(507parse_list,508"The requires parameter is deprecated, please use "509"install_requires for runtime dependencies.",510SetuptoolsDeprecationWarning,511),512'obsoletes': parse_list,513'classifiers': self._get_parser_compound(parse_file, parse_list),514'license': exclude_files_parser('license'),515'license_file': self._deprecated_config_handler(516exclude_files_parser('license_file'),517"The license_file parameter is deprecated, "518"use license_files instead.",519SetuptoolsDeprecationWarning,520),521'license_files': parse_list,522'description': parse_file,523'long_description': parse_file,524'version': self._parse_version,525'project_urls': parse_dict,526}527528def _parse_version(self, value):529"""Parses `version` option value.530531:param value:532:rtype: str533534"""535version = self._parse_file(value, self.root_dir)536537if version != value:538version = version.strip()539# Be strict about versions loaded from file because it's easy to540# accidentally include newlines and other unintended content541try:542Version(version)543except InvalidVersion:544tmpl = (545'Version loaded from {value} does not '546'comply with PEP 440: {version}'547)548raise DistutilsOptionError(tmpl.format(**locals()))549550return version551552return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))553554555class ConfigOptionsHandler(ConfigHandler["Distribution"]):556557section_prefix = 'options'558559def __init__(560self,561target_obj: "Distribution",562options: AllCommandOptions,563ignore_option_errors: bool,564ensure_discovered: expand.EnsurePackagesDiscovered,565):566super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)567self.root_dir = target_obj.src_root568self.package_dir: Dict[str, str] = {} # To be filled by `find_packages`569570@property571def parsers(self):572"""Metadata item name to parser function mapping."""573parse_list = self._parse_list574parse_list_semicolon = partial(self._parse_list, separator=';')575parse_bool = self._parse_bool576parse_dict = self._parse_dict577parse_cmdclass = self._parse_cmdclass578parse_file = partial(self._parse_file, root_dir=self.root_dir)579580return {581'zip_safe': parse_bool,582'include_package_data': parse_bool,583'package_dir': parse_dict,584'scripts': parse_list,585'eager_resources': parse_list,586'dependency_links': parse_list,587'namespace_packages': self._deprecated_config_handler(588parse_list,589"The namespace_packages parameter is deprecated, "590"consider using implicit namespaces instead (PEP 420).",591SetuptoolsDeprecationWarning,592),593'install_requires': parse_list_semicolon,594'setup_requires': parse_list_semicolon,595'tests_require': parse_list_semicolon,596'packages': self._parse_packages,597'entry_points': parse_file,598'py_modules': parse_list,599'python_requires': SpecifierSet,600'cmdclass': parse_cmdclass,601}602603def _parse_cmdclass(self, value):604package_dir = self.ensure_discovered.package_dir605return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)606607def _parse_packages(self, value):608"""Parses `packages` option value.609610:param value:611:rtype: list612"""613find_directives = ['find:', 'find_namespace:']614trimmed_value = value.strip()615616if trimmed_value not in find_directives:617return self._parse_list(value)618619# Read function arguments from a dedicated section.620find_kwargs = self.parse_section_packages__find(621self.sections.get('packages.find', {})622)623624find_kwargs.update(625namespaces=(trimmed_value == find_directives[1]),626root_dir=self.root_dir,627fill_package_dir=self.package_dir,628)629630return expand.find_packages(**find_kwargs)631632def parse_section_packages__find(self, section_options):633"""Parses `packages.find` configuration file section.634635To be used in conjunction with _parse_packages().636637:param dict section_options:638"""639section_data = self._parse_section_to_dict(section_options, self._parse_list)640641valid_keys = ['where', 'include', 'exclude']642643find_kwargs = dict(644[(k, v) for k, v in section_data.items() if k in valid_keys and v]645)646647where = find_kwargs.get('where')648if where is not None:649find_kwargs['where'] = where[0] # cast list to single val650651return find_kwargs652653def parse_section_entry_points(self, section_options):654"""Parses `entry_points` configuration file section.655656:param dict section_options:657"""658parsed = self._parse_section_to_dict(section_options, self._parse_list)659self['entry_points'] = parsed660661def _parse_package_data(self, section_options):662package_data = self._parse_section_to_dict(section_options, self._parse_list)663return expand.canonic_package_data(package_data)664665def parse_section_package_data(self, section_options):666"""Parses `package_data` configuration file section.667668:param dict section_options:669"""670self['package_data'] = self._parse_package_data(section_options)671672def parse_section_exclude_package_data(self, section_options):673"""Parses `exclude_package_data` configuration file section.674675:param dict section_options:676"""677self['exclude_package_data'] = self._parse_package_data(section_options)678679def parse_section_extras_require(self, section_options):680"""Parses `extras_require` configuration file section.681682:param dict section_options:683"""684parse_list = partial(self._parse_list, separator=';')685parsed = self._parse_section_to_dict(section_options, parse_list)686self['extras_require'] = parsed687688def parse_section_data_files(self, section_options):689"""Parses `data_files` configuration file section.690691:param dict section_options:692"""693parsed = self._parse_section_to_dict(section_options, self._parse_list)694self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)695696697