Spaces:
Runtime error
Runtime error
File size: 26,184 Bytes
0c87db7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 |
"""
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"
|