You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
790 lines
26 KiB
790 lines
26 KiB
6 months ago
|
"""
|
||
|
Load setuptools configuration from ``setup.cfg`` files.
|
||
|
|
||
|
**API will be made private in the future**
|
||
|
|
||
|
To read project metadata, consider using
|
||
|
``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
|
||
|
For simple scenarios, you can also try parsing the file directly
|
||
|
with the help of ``configparser``.
|
||
|
"""
|
||
|
import contextlib
|
||
|
import functools
|
||
|
import os
|
||
|
from collections import defaultdict
|
||
|
from functools import partial
|
||
|
from functools import wraps
|
||
|
from typing import (
|
||
|
TYPE_CHECKING,
|
||
|
Callable,
|
||
|
Any,
|
||
|
Dict,
|
||
|
Generic,
|
||
|
Iterable,
|
||
|
List,
|
||
|
Optional,
|
||
|
Set,
|
||
|
Tuple,
|
||
|
TypeVar,
|
||
|
Union,
|
||
|
)
|
||
|
|
||
|
from ..errors import FileError, OptionError
|
||
|
from ..extern.packaging.markers import default_environment as marker_env
|
||
|
from ..extern.packaging.requirements import InvalidRequirement, Requirement
|
||
|
from ..extern.packaging.specifiers import SpecifierSet
|
||
|
from ..extern.packaging.version import InvalidVersion, Version
|
||
|
from ..warnings import SetuptoolsDeprecationWarning
|
||
|
from . import expand
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from distutils.dist import DistributionMetadata # noqa
|
||
|
|
||
|
from setuptools.dist import Distribution # 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 FileError(f'Configuration file {filepath} does not exist.')
|
||
|
|
||
|
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 = f'get_{key}'
|
||
|
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()
|
||
|
distribution._referenced_files.update(
|
||
|
options._referenced_files, meta._referenced_files
|
||
|
)
|
||
|
|
||
|
return meta, options
|
||
|
|
||
|
|
||
|
def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list):
|
||
|
"""Because users sometimes misinterpret this configuration:
|
||
|
|
||
|
[options.extras_require]
|
||
|
foo = bar;python_version<"4"
|
||
|
|
||
|
It looks like one requirement with an environment marker
|
||
|
but because there is no newline, it's parsed as two requirements
|
||
|
with a semicolon as separator.
|
||
|
|
||
|
Therefore, if:
|
||
|
* input string does not contain a newline AND
|
||
|
* parsed result contains two requirements AND
|
||
|
* parsing of the two parts from the result ("<first>;<second>")
|
||
|
leads in a valid Requirement with a valid marker
|
||
|
a UserWarning is shown to inform the user about the possible problem.
|
||
|
"""
|
||
|
if "\n" in orig_value or len(parsed) != 2:
|
||
|
return
|
||
|
|
||
|
markers = marker_env().keys()
|
||
|
|
||
|
try:
|
||
|
req = Requirement(parsed[1])
|
||
|
if req.name in markers:
|
||
|
_AmbiguousMarker.emit(field=label, req=parsed[1])
|
||
|
except InvalidRequirement as ex:
|
||
|
if any(parsed[1].startswith(marker) for marker in markers):
|
||
|
msg = _AmbiguousMarker.message(field=label, req=parsed[1])
|
||
|
raise InvalidRequirement(msg) from ex
|
||
|
|
||
|
|
||
|
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,
|
||
|
):
|
||
|
self.ignore_option_errors = ignore_option_errors
|
||
|
self.target_obj = target_obj
|
||
|
self.sections = dict(self._section_options(options))
|
||
|
self.set_options: List[str] = []
|
||
|
self.ensure_discovered = ensure_discovered
|
||
|
self._referenced_files: Set[str] = set()
|
||
|
"""After parsing configurations, this property will enumerate
|
||
|
all files referenced by the "file:" directive. Private API for setuptools only.
|
||
|
"""
|
||
|
|
||
|
@classmethod
|
||
|
def _section_options(cls, options: AllCommandOptions):
|
||
|
for full_name, value in options.items():
|
||
|
pre, sep, name = full_name.partition(cls.section_prefix)
|
||
|
if pre:
|
||
|
continue
|
||
|
yield name.lstrip('.'), value
|
||
|
|
||
|
@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):
|
||
|
target_obj = self.target_obj
|
||
|
|
||
|
# Translate alias into real name.
|
||
|
option_name = self.aliases.get(option_name, option_name)
|
||
|
|
||
|
try:
|
||
|
current_value = getattr(target_obj, option_name)
|
||
|
except AttributeError:
|
||
|
raise KeyError(option_name)
|
||
|
|
||
|
if current_value:
|
||
|
# Already inhabited. Skipping.
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
parsed = self.parsers.get(option_name, lambda x: x)(value)
|
||
|
except (Exception,) * self.ignore_option_errors:
|
||
|
return
|
||
|
|
||
|
simple_setter = functools.partial(target_obj.__setattr__, option_name)
|
||
|
setter = getattr(target_obj, 'set_%s' % option_name, simple_setter)
|
||
|
setter(parsed)
|
||
|
|
||
|
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 OptionError(f"Unable to parse option value to dict: {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
|
||
|
|
||
|
def _parse_file(self, 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(',')]
|
||
|
self._referenced_files.update(filepaths)
|
||
|
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_with_key(cls, section_options, values_parser):
|
||
|
"""Parses section options into a dictionary.
|
||
|
|
||
|
Applies a given parser to each option in a section.
|
||
|
|
||
|
:param dict section_options:
|
||
|
:param callable values_parser: function with 2 args corresponding to key, value
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
value = {}
|
||
|
for key, (_, val) in section_options.items():
|
||
|
value[key] = values_parser(key, val)
|
||
|
return value
|
||
|
|
||
|
@classmethod
|
||
|
def _parse_section_to_dict(cls, section_options, values_parser=None):
|
||
|
"""Parses section options into a dictionary.
|
||
|
|
||
|
Optionally applies a given parser to each value.
|
||
|
|
||
|
:param dict section_options:
|
||
|
:param callable values_parser: function with 1 arg corresponding to option value
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v)
|
||
|
return cls._parse_section_to_dict_with_key(section_options, parser)
|
||
|
|
||
|
def parse_section(self, section_options):
|
||
|
"""Parses configuration file section.
|
||
|
|
||
|
:param dict section_options:
|
||
|
"""
|
||
|
for name, (_, value) in section_options.items():
|
||
|
with contextlib.suppress(KeyError):
|
||
|
# Keep silent for a new option may appear anytime.
|
||
|
self[name] = value
|
||
|
|
||
|
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 OptionError(
|
||
|
"Unsupported distribution option section: "
|
||
|
f"[{self.section_prefix}.{section_name}]"
|
||
|
)
|
||
|
|
||
|
section_parser_method(section_options)
|
||
|
|
||
|
def _deprecated_config_handler(self, func, msg, **kw):
|
||
|
"""this function will wrap around parameters that are deprecated
|
||
|
|
||
|
:param msg: deprecation message
|
||
|
:param func: function to be wrapped around
|
||
|
"""
|
||
|
|
||
|
@wraps(func)
|
||
|
def config_handler(*args, **kwargs):
|
||
|
kw.setdefault("stacklevel", 2)
|
||
|
_DeprecatedConfig.emit("Deprecated config in `setup.cfg`", msg, **kw)
|
||
|
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.",
|
||
|
due_date=(2023, 10, 30),
|
||
|
# Warning introduced in 27 Oct 2018
|
||
|
),
|
||
|
'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.",
|
||
|
due_date=(2023, 10, 30),
|
||
|
# Warning introduced in 23 May 2021
|
||
|
),
|
||
|
'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:
|
||
|
raise OptionError(
|
||
|
f'Version loaded from {value} does not '
|
||
|
f'comply with PEP 440: {version}'
|
||
|
)
|
||
|
|
||
|
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`
|
||
|
|
||
|
@classmethod
|
||
|
def _parse_list_semicolon(cls, value):
|
||
|
return cls._parse_list(value, separator=';')
|
||
|
|
||
|
def _parse_file_in_root(self, value):
|
||
|
return self._parse_file(value, root_dir=self.root_dir)
|
||
|
|
||
|
def _parse_requirements_list(self, label: str, value: str):
|
||
|
# Parse a requirements list, either by reading in a `file:`, or a list.
|
||
|
parsed = self._parse_list_semicolon(self._parse_file_in_root(value))
|
||
|
_warn_accidental_env_marker_misconfig(label, value, parsed)
|
||
|
# Filter it to only include lines that are not comments. `parse_list`
|
||
|
# will have stripped each line and filtered out empties.
|
||
|
return [line for line in parsed if not line.startswith("#")]
|
||
|
|
||
|
@property
|
||
|
def parsers(self):
|
||
|
"""Metadata item name to parser function mapping."""
|
||
|
parse_list = self._parse_list
|
||
|
parse_bool = self._parse_bool
|
||
|
parse_dict = self._parse_dict
|
||
|
parse_cmdclass = self._parse_cmdclass
|
||
|
|
||
|
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': self._deprecated_config_handler(
|
||
|
parse_list,
|
||
|
"The namespace_packages parameter is deprecated, "
|
||
|
"consider using implicit namespaces instead (PEP 420).",
|
||
|
# TODO: define due date, see setuptools.dist:check_nsp.
|
||
|
),
|
||
|
'install_requires': partial(
|
||
|
self._parse_requirements_list, "install_requires"
|
||
|
),
|
||
|
'setup_requires': self._parse_list_semicolon,
|
||
|
'tests_require': self._parse_list_semicolon,
|
||
|
'packages': self._parse_packages,
|
||
|
'entry_points': self._parse_file_in_root,
|
||
|
'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:
|
||
|
"""
|
||
|
parsed = self._parse_section_to_dict_with_key(
|
||
|
section_options,
|
||
|
lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v),
|
||
|
)
|
||
|
|
||
|
self['extras_require'] = parsed
|
||
|
|
||
|
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)
|
||
|
|
||
|
|
||
|
class _AmbiguousMarker(SetuptoolsDeprecationWarning):
|
||
|
_SUMMARY = "Ambiguous requirement marker."
|
||
|
_DETAILS = """
|
||
|
One of the parsed requirements in `{field}` looks like a valid environment marker:
|
||
|
|
||
|
{req!r}
|
||
|
|
||
|
Please make sure that the configuration file is correct.
|
||
|
You can use dangling lines to avoid this problem.
|
||
|
"""
|
||
|
_SEE_DOCS = "userguide/declarative_config.html#opt-2"
|
||
|
# TODO: should we include due_date here? Initially introduced in 6 Aug 2022.
|
||
|
# Does this make sense with latest version of packaging?
|
||
|
|
||
|
@classmethod
|
||
|
def message(cls, **kw):
|
||
|
docs = f"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}"
|
||
|
return cls._format(cls._SUMMARY, cls._DETAILS, see_url=docs, format_args=kw)
|
||
|
|
||
|
|
||
|
class _DeprecatedConfig(SetuptoolsDeprecationWarning):
|
||
|
_SEE_DOCS = "userguide/declarative_config.html"
|