aboutsummaryrefslogtreecommitdiff
path: root/setuptools/config/setupcfg.py
diff options
context:
space:
mode:
authorDan Willemsen <dwillemsen@google.com>2022-03-26 04:06:48 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-03-26 04:06:48 +0000
commite3ce094dab748df046ee1a7ab039ed6f48fafab3 (patch)
tree690b5fb2994f47324dd4298e676335d4555ee267 /setuptools/config/setupcfg.py
parentdf3578c2a35ac125ee592a3f800065e72f6e77c9 (diff)
parent5a8b6adcfa54d474a9ad1a772d65bf6716217993 (diff)
downloadsetuptools-e3ce094dab748df046ee1a7ab039ed6f48fafab3.tar.gz
Upgrade to setuptools 61.1.0 am: adad21eb06 am: 30dd327dfb am: d25f34ce2b am: 5a8b6adcfa
Original change: https://android-review.googlesource.com/c/platform/external/python/setuptools/+/2045746 Change-Id: Ia9263955741ff22662dc01ca557c179254efa6e0 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'setuptools/config/setupcfg.py')
-rw-r--r--setuptools/config/setupcfg.py690
1 files changed, 690 insertions, 0 deletions
diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
new file mode 100644
index 0000000..5ecf626
--- /dev/null
+++ b/setuptools/config/setupcfg.py
@@ -0,0 +1,690 @@
+"""Load setuptools configuration from ``setup.cfg`` files"""
+import os
+
+import warnings
+import functools
+from collections import defaultdict
+from functools import partial
+from functools import wraps
+from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List,
+ Optional, Tuple, TypeVar, Union)
+
+from distutils.errors import DistutilsOptionError, DistutilsFileError
+from setuptools.extern.packaging.version import Version, InvalidVersion
+from setuptools.extern.packaging.specifiers import SpecifierSet
+
+from . import expand
+
+if TYPE_CHECKING:
+ from setuptools.dist import Distribution # noqa
+ from distutils.dist import DistributionMetadata # noqa
+
+_Path = Union[str, os.PathLike]
+SingleCommandOptions = Dict["str", Tuple["str", Any]]
+"""Dict that associate the name of the options of a particular command to a
+tuple. The first element of the tuple indicates the origin of the option value
+(e.g. the name of the configuration file where it was read from),
+while the second element of the tuple is the option value itself
+"""
+AllCommandOptions = Dict["str", SingleCommandOptions] # cmd name => its options
+Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"])
+
+
+def read_configuration(
+ filepath: _Path,
+ find_others=False,
+ ignore_option_errors=False
+) -> dict:
+ """Read given configuration file and returns options from it as a dict.
+
+ :param str|unicode filepath: Path to configuration file
+ to get options from.
+
+ :param bool find_others: Whether to search for other configuration files
+ which could be on in various places.
+
+ :param bool ignore_option_errors: Whether to silently ignore
+ options, values of which could not be resolved (e.g. due to exceptions
+ in directives such as file:, attr:, etc.).
+ If False exceptions are propagated as expected.
+
+ :rtype: dict
+ """
+ from setuptools.dist import Distribution
+
+ dist = Distribution()
+ filenames = dist.find_config_files() if find_others else []
+ handlers = _apply(dist, filepath, filenames, ignore_option_errors)
+ return configuration_to_dict(handlers)
+
+
+def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
+ """Apply the configuration from a ``setup.cfg`` file into an existing
+ distribution object.
+ """
+ _apply(dist, filepath)
+ dist._finalize_requires()
+ return dist
+
+
+def _apply(
+ dist: "Distribution", filepath: _Path,
+ other_files: Iterable[_Path] = (),
+ ignore_option_errors: bool = False
+) -> Tuple["ConfigHandler", ...]:
+ """Read configuration from ``filepath`` and applies to the ``dist`` object."""
+ from setuptools.dist import _Distribution
+
+ filepath = os.path.abspath(filepath)
+
+ if not os.path.isfile(filepath):
+ raise DistutilsFileError('Configuration file %s does not exist.' % filepath)
+
+ current_directory = os.getcwd()
+ os.chdir(os.path.dirname(filepath))
+ filenames = [*other_files, filepath]
+
+ try:
+ _Distribution.parse_config_files(dist, filenames=filenames)
+ handlers = parse_configuration(
+ dist, dist.command_options, ignore_option_errors=ignore_option_errors
+ )
+ dist._finalize_license_files()
+ finally:
+ os.chdir(current_directory)
+
+ return handlers
+
+
+def _get_option(target_obj: Target, key: str):
+ """
+ Given a target object and option key, get that option from
+ the target object, either through a get_{key} method or
+ from an attribute directly.
+ """
+ getter_name = 'get_{key}'.format(**locals())
+ by_attribute = functools.partial(getattr, target_obj, key)
+ getter = getattr(target_obj, getter_name, by_attribute)
+ return getter()
+
+
+def configuration_to_dict(handlers: Tuple["ConfigHandler", ...]) -> dict:
+ """Returns configuration data gathered by given handlers as a dict.
+
+ :param list[ConfigHandler] handlers: Handlers list,
+ usually from parse_configuration()
+
+ :rtype: dict
+ """
+ config_dict: dict = defaultdict(dict)
+
+ for handler in handlers:
+ for option in handler.set_options:
+ value = _get_option(handler.target_obj, option)
+ config_dict[handler.section_prefix][option] = value
+
+ return config_dict
+
+
+def parse_configuration(
+ distribution: "Distribution",
+ command_options: AllCommandOptions,
+ ignore_option_errors=False
+) -> Tuple["ConfigMetadataHandler", "ConfigOptionsHandler"]:
+ """Performs additional parsing of configuration options
+ for a distribution.
+
+ Returns a list of used option handlers.
+
+ :param Distribution distribution:
+ :param dict command_options:
+ :param bool ignore_option_errors: Whether to silently ignore
+ options, values of which could not be resolved (e.g. due to exceptions
+ in directives such as file:, attr:, etc.).
+ If False exceptions are propagated as expected.
+ :rtype: list
+ """
+ with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered:
+ options = ConfigOptionsHandler(
+ distribution,
+ command_options,
+ ignore_option_errors,
+ ensure_discovered,
+ )
+
+ options.parse()
+ if not distribution.package_dir:
+ distribution.package_dir = options.package_dir # Filled by `find_packages`
+
+ meta = ConfigMetadataHandler(
+ distribution.metadata,
+ command_options,
+ ignore_option_errors,
+ ensure_discovered,
+ distribution.package_dir,
+ distribution.src_root,
+ )
+ meta.parse()
+
+ return meta, options
+
+
+class ConfigHandler(Generic[Target]):
+ """Handles metadata supplied in configuration files."""
+
+ section_prefix: str
+ """Prefix for config sections handled by this handler.
+ Must be provided by class heirs.
+
+ """
+
+ aliases: Dict[str, str] = {}
+ """Options aliases.
+ For compatibility with various packages. E.g.: d2to1 and pbr.
+ Note: `-` in keys is replaced with `_` by config parser.
+
+ """
+
+ def __init__(
+ self,
+ target_obj: Target,
+ options: AllCommandOptions,
+ ignore_option_errors,
+ ensure_discovered: expand.EnsurePackagesDiscovered,
+ ):
+ sections: AllCommandOptions = {}
+
+ section_prefix = self.section_prefix
+ for section_name, section_options in options.items():
+ if not section_name.startswith(section_prefix):
+ continue
+
+ section_name = section_name.replace(section_prefix, '').strip('.')
+ sections[section_name] = section_options
+
+ self.ignore_option_errors = ignore_option_errors
+ self.target_obj = target_obj
+ self.sections = sections
+ self.set_options: List[str] = []
+ self.ensure_discovered = ensure_discovered
+
+ @property
+ def parsers(self):
+ """Metadata item name to parser function mapping."""
+ raise NotImplementedError(
+ '%s must provide .parsers property' % self.__class__.__name__
+ )
+
+ def __setitem__(self, option_name, value):
+ unknown = tuple()
+ target_obj = self.target_obj
+
+ # Translate alias into real name.
+ option_name = self.aliases.get(option_name, option_name)
+
+ current_value = getattr(target_obj, option_name, unknown)
+
+ if current_value is unknown:
+ raise KeyError(option_name)
+
+ if current_value:
+ # Already inhabited. Skipping.
+ return
+
+ skip_option = False
+ parser = self.parsers.get(option_name)
+ if parser:
+ try:
+ value = parser(value)
+
+ except Exception:
+ skip_option = True
+ if not self.ignore_option_errors:
+ raise
+
+ if skip_option:
+ return
+
+ setter = getattr(target_obj, 'set_%s' % option_name, None)
+ if setter is None:
+ setattr(target_obj, option_name, value)
+ else:
+ setter(value)
+
+ self.set_options.append(option_name)
+
+ @classmethod
+ def _parse_list(cls, value, separator=','):
+ """Represents value as a list.
+
+ Value is split either by separator (defaults to comma) or by lines.
+
+ :param value:
+ :param separator: List items separator character.
+ :rtype: list
+ """
+ if isinstance(value, list): # _get_parser_compound case
+ return value
+
+ if '\n' in value:
+ value = value.splitlines()
+ else:
+ value = value.split(separator)
+
+ return [chunk.strip() for chunk in value if chunk.strip()]
+
+ @classmethod
+ def _parse_dict(cls, value):
+ """Represents value as a dict.
+
+ :param value:
+ :rtype: dict
+ """
+ separator = '='
+ result = {}
+ for line in cls._parse_list(value):
+ key, sep, val = line.partition(separator)
+ if sep != separator:
+ raise DistutilsOptionError(
+ 'Unable to parse option value to dict: %s' % value
+ )
+ result[key.strip()] = val.strip()
+
+ return result
+
+ @classmethod
+ def _parse_bool(cls, value):
+ """Represents value as boolean.
+
+ :param value:
+ :rtype: bool
+ """
+ value = value.lower()
+ return value in ('1', 'true', 'yes')
+
+ @classmethod
+ def _exclude_files_parser(cls, key):
+ """Returns a parser function to make sure field inputs
+ are not files.
+
+ Parses a value after getting the key so error messages are
+ more informative.
+
+ :param key:
+ :rtype: callable
+ """
+
+ def parser(value):
+ exclude_directive = 'file:'
+ if value.startswith(exclude_directive):
+ raise ValueError(
+ 'Only strings are accepted for the {0} field, '
+ 'files are not accepted'.format(key)
+ )
+ return value
+
+ return parser
+
+ @classmethod
+ def _parse_file(cls, value, root_dir: _Path):
+ """Represents value as a string, allowing including text
+ from nearest files using `file:` directive.
+
+ Directive is sandboxed and won't reach anything outside
+ directory with setup.py.
+
+ Examples:
+ file: README.rst, CHANGELOG.md, src/file.txt
+
+ :param str value:
+ :rtype: str
+ """
+ include_directive = 'file:'
+
+ if not isinstance(value, str):
+ return value
+
+ if not value.startswith(include_directive):
+ return value
+
+ spec = value[len(include_directive) :]
+ filepaths = (path.strip() for path in spec.split(','))
+ return expand.read_files(filepaths, root_dir)
+
+ def _parse_attr(self, value, package_dir, root_dir: _Path):
+ """Represents value as a module attribute.
+
+ Examples:
+ attr: package.attr
+ attr: package.module.attr
+
+ :param str value:
+ :rtype: str
+ """
+ attr_directive = 'attr:'
+ if not value.startswith(attr_directive):
+ return value
+
+ attr_desc = value.replace(attr_directive, '')
+
+ # Make sure package_dir is populated correctly, so `attr:` directives can work
+ package_dir.update(self.ensure_discovered.package_dir)
+ return expand.read_attr(attr_desc, package_dir, root_dir)
+
+ @classmethod
+ def _get_parser_compound(cls, *parse_methods):
+ """Returns parser function to represents value as a list.
+
+ Parses a value applying given methods one after another.
+
+ :param parse_methods:
+ :rtype: callable
+ """
+
+ def parse(value):
+ parsed = value
+
+ for method in parse_methods:
+ parsed = method(parsed)
+
+ return parsed
+
+ return parse
+
+ @classmethod
+ def _parse_section_to_dict(cls, section_options, values_parser=None):
+ """Parses section options into a dictionary.
+
+ Optionally applies a given parser to values.
+
+ :param dict section_options:
+ :param callable values_parser:
+ :rtype: dict
+ """
+ value = {}
+ values_parser = values_parser or (lambda val: val)
+ for key, (_, val) in section_options.items():
+ value[key] = values_parser(val)
+ return value
+
+ def parse_section(self, section_options):
+ """Parses configuration file section.
+
+ :param dict section_options:
+ """
+ for (name, (_, value)) in section_options.items():
+ try:
+ self[name] = value
+
+ except KeyError:
+ pass # Keep silent for a new option may appear anytime.
+
+ def parse(self):
+ """Parses configuration file items from one
+ or more related sections.
+
+ """
+ for section_name, section_options in self.sections.items():
+
+ method_postfix = ''
+ if section_name: # [section.option] variant
+ method_postfix = '_%s' % section_name
+
+ section_parser_method: Optional[Callable] = getattr(
+ self,
+ # Dots in section names are translated into dunderscores.
+ ('parse_section%s' % method_postfix).replace('.', '__'),
+ None,
+ )
+
+ if section_parser_method is None:
+ raise DistutilsOptionError(
+ 'Unsupported distribution option section: [%s.%s]'
+ % (self.section_prefix, section_name)
+ )
+
+ section_parser_method(section_options)
+
+ def _deprecated_config_handler(self, func, msg, warning_class):
+ """this function will wrap around parameters that are deprecated
+
+ :param msg: deprecation message
+ :param warning_class: class of warning exception to be raised
+ :param func: function to be wrapped around
+ """
+
+ @wraps(func)
+ def config_handler(*args, **kwargs):
+ warnings.warn(msg, warning_class)
+ return func(*args, **kwargs)
+
+ return config_handler
+
+
+class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
+
+ section_prefix = 'metadata'
+
+ aliases = {
+ 'home_page': 'url',
+ 'summary': 'description',
+ 'classifier': 'classifiers',
+ 'platform': 'platforms',
+ }
+
+ strict_mode = False
+ """We need to keep it loose, to be partially compatible with
+ `pbr` and `d2to1` packages which also uses `metadata` section.
+
+ """
+
+ def __init__(
+ self,
+ target_obj: "DistributionMetadata",
+ options: AllCommandOptions,
+ ignore_option_errors: bool,
+ ensure_discovered: expand.EnsurePackagesDiscovered,
+ package_dir: Optional[dict] = None,
+ root_dir: _Path = os.curdir
+ ):
+ super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
+ self.package_dir = package_dir
+ self.root_dir = root_dir
+
+ @property
+ def parsers(self):
+ """Metadata item name to parser function mapping."""
+ parse_list = self._parse_list
+ parse_file = partial(self._parse_file, root_dir=self.root_dir)
+ parse_dict = self._parse_dict
+ exclude_files_parser = self._exclude_files_parser
+
+ return {
+ 'platforms': parse_list,
+ 'keywords': parse_list,
+ 'provides': parse_list,
+ 'requires': self._deprecated_config_handler(
+ parse_list,
+ "The requires parameter is deprecated, please use "
+ "install_requires for runtime dependencies.",
+ DeprecationWarning,
+ ),
+ 'obsoletes': parse_list,
+ 'classifiers': self._get_parser_compound(parse_file, parse_list),
+ 'license': exclude_files_parser('license'),
+ 'license_file': self._deprecated_config_handler(
+ exclude_files_parser('license_file'),
+ "The license_file parameter is deprecated, "
+ "use license_files instead.",
+ DeprecationWarning,
+ ),
+ 'license_files': parse_list,
+ 'description': parse_file,
+ 'long_description': parse_file,
+ 'version': self._parse_version,
+ 'project_urls': parse_dict,
+ }
+
+ def _parse_version(self, value):
+ """Parses `version` option value.
+
+ :param value:
+ :rtype: str
+
+ """
+ version = self._parse_file(value, self.root_dir)
+
+ if version != value:
+ version = version.strip()
+ # Be strict about versions loaded from file because it's easy to
+ # accidentally include newlines and other unintended content
+ try:
+ Version(version)
+ except InvalidVersion:
+ tmpl = (
+ 'Version loaded from {value} does not '
+ 'comply with PEP 440: {version}'
+ )
+ raise DistutilsOptionError(tmpl.format(**locals()))
+
+ return version
+
+ return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))
+
+
+class ConfigOptionsHandler(ConfigHandler["Distribution"]):
+
+ section_prefix = 'options'
+
+ def __init__(
+ self,
+ target_obj: "Distribution",
+ options: AllCommandOptions,
+ ignore_option_errors: bool,
+ ensure_discovered: expand.EnsurePackagesDiscovered,
+ ):
+ super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
+ self.root_dir = target_obj.src_root
+ self.package_dir: Dict[str, str] = {} # To be filled by `find_packages`
+
+ @property
+ def parsers(self):
+ """Metadata item name to parser function mapping."""
+ parse_list = self._parse_list
+ parse_list_semicolon = partial(self._parse_list, separator=';')
+ parse_bool = self._parse_bool
+ parse_dict = self._parse_dict
+ parse_cmdclass = self._parse_cmdclass
+ parse_file = partial(self._parse_file, root_dir=self.root_dir)
+
+ return {
+ 'zip_safe': parse_bool,
+ 'include_package_data': parse_bool,
+ 'package_dir': parse_dict,
+ 'scripts': parse_list,
+ 'eager_resources': parse_list,
+ 'dependency_links': parse_list,
+ 'namespace_packages': parse_list,
+ 'install_requires': parse_list_semicolon,
+ 'setup_requires': parse_list_semicolon,
+ 'tests_require': parse_list_semicolon,
+ 'packages': self._parse_packages,
+ 'entry_points': parse_file,
+ 'py_modules': parse_list,
+ 'python_requires': SpecifierSet,
+ 'cmdclass': parse_cmdclass,
+ }
+
+ def _parse_cmdclass(self, value):
+ package_dir = self.ensure_discovered.package_dir
+ return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)
+
+ def _parse_packages(self, value):
+ """Parses `packages` option value.
+
+ :param value:
+ :rtype: list
+ """
+ find_directives = ['find:', 'find_namespace:']
+ trimmed_value = value.strip()
+
+ if trimmed_value not in find_directives:
+ return self._parse_list(value)
+
+ # Read function arguments from a dedicated section.
+ find_kwargs = self.parse_section_packages__find(
+ self.sections.get('packages.find', {})
+ )
+
+ find_kwargs.update(
+ namespaces=(trimmed_value == find_directives[1]),
+ root_dir=self.root_dir,
+ fill_package_dir=self.package_dir,
+ )
+
+ return expand.find_packages(**find_kwargs)
+
+ def parse_section_packages__find(self, section_options):
+ """Parses `packages.find` configuration file section.
+
+ To be used in conjunction with _parse_packages().
+
+ :param dict section_options:
+ """
+ section_data = self._parse_section_to_dict(section_options, self._parse_list)
+
+ valid_keys = ['where', 'include', 'exclude']
+
+ find_kwargs = dict(
+ [(k, v) for k, v in section_data.items() if k in valid_keys and v]
+ )
+
+ where = find_kwargs.get('where')
+ if where is not None:
+ find_kwargs['where'] = where[0] # cast list to single val
+
+ return find_kwargs
+
+ def parse_section_entry_points(self, section_options):
+ """Parses `entry_points` configuration file section.
+
+ :param dict section_options:
+ """
+ parsed = self._parse_section_to_dict(section_options, self._parse_list)
+ self['entry_points'] = parsed
+
+ def _parse_package_data(self, section_options):
+ package_data = self._parse_section_to_dict(section_options, self._parse_list)
+ return expand.canonic_package_data(package_data)
+
+ def parse_section_package_data(self, section_options):
+ """Parses `package_data` configuration file section.
+
+ :param dict section_options:
+ """
+ self['package_data'] = self._parse_package_data(section_options)
+
+ def parse_section_exclude_package_data(self, section_options):
+ """Parses `exclude_package_data` configuration file section.
+
+ :param dict section_options:
+ """
+ self['exclude_package_data'] = self._parse_package_data(section_options)
+
+ def parse_section_extras_require(self, section_options):
+ """Parses `extras_require` configuration file section.
+
+ :param dict section_options:
+ """
+ parse_list = partial(self._parse_list, separator=';')
+ self['extras_require'] = self._parse_section_to_dict(
+ section_options, parse_list
+ )
+
+ def parse_section_data_files(self, section_options):
+ """Parses `data_files` configuration file section.
+
+ :param dict section_options:
+ """
+ parsed = self._parse_section_to_dict(section_options, self._parse_list)
+ self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)