2 Load setuptools configuration from ``setup.cfg`` files.
4 **API will be made private in the future**
11 from collections
import defaultdict
12 from functools
import partial
13 from functools
import wraps
14 from typing
import (TYPE_CHECKING
, Callable
, Any
, Dict
, Generic
, Iterable
, List
,
15 Optional
, Set
, Tuple
, TypeVar
, Union
)
17 from distutils
.errors
import DistutilsOptionError
, DistutilsFileError
18 from setuptools
.extern
.packaging
.requirements
import Requirement
, InvalidRequirement
19 from setuptools
.extern
.packaging
.version
import Version
, InvalidVersion
20 from setuptools
.extern
.packaging
.specifiers
import SpecifierSet
21 from setuptools
._deprecation
_warning
import SetuptoolsDeprecationWarning
26 from setuptools
.dist
import Distribution
# noqa
27 from distutils
.dist
import DistributionMetadata
# noqa
29 _Path
= Union
[str, os
.PathLike
]
30 SingleCommandOptions
= Dict
["str", Tuple
["str", Any
]]
31 """Dict that associate the name of the options of a particular command to a
32 tuple. The first element of the tuple indicates the origin of the option value
33 (e.g. the name of the configuration file where it was read from),
34 while the second element of the tuple is the option value itself
36 AllCommandOptions
= Dict
["str", SingleCommandOptions
] # cmd name => its options
37 Target
= TypeVar("Target", bound
=Union
["Distribution", "DistributionMetadata"])
40 def read_configuration(
43 ignore_option_errors
=False
45 """Read given configuration file and returns options from it as a dict.
47 :param str|unicode filepath: Path to configuration file
50 :param bool find_others: Whether to search for other configuration files
51 which could be on in various places.
53 :param bool ignore_option_errors: Whether to silently ignore
54 options, values of which could not be resolved (e.g. due to exceptions
55 in directives such as file:, attr:, etc.).
56 If False exceptions are propagated as expected.
60 from setuptools
.dist
import Distribution
63 filenames
= dist
.find_config_files() if find_others
else []
64 handlers
= _apply(dist
, filepath
, filenames
, ignore_option_errors
)
65 return configuration_to_dict(handlers
)
68 def apply_configuration(dist
: "Distribution", filepath
: _Path
) -> "Distribution":
69 """Apply the configuration from a ``setup.cfg`` file into an existing
72 _apply(dist
, filepath
)
73 dist
._finalize
_requires
()
78 dist
: "Distribution", filepath
: _Path
,
79 other_files
: Iterable
[_Path
] = (),
80 ignore_option_errors
: bool = False,
81 ) -> Tuple
["ConfigHandler", ...]:
82 """Read configuration from ``filepath`` and applies to the ``dist`` object."""
83 from setuptools
.dist
import _Distribution
85 filepath
= os
.path
.abspath(filepath
)
87 if not os
.path
.isfile(filepath
):
88 raise DistutilsFileError('Configuration file %s does not exist.' % filepath
)
90 current_directory
= os
.getcwd()
91 os
.chdir(os
.path
.dirname(filepath
))
92 filenames
= [*other_files
, filepath
]
95 _Distribution
.parse_config_files(dist
, filenames
=filenames
)
96 handlers
= parse_configuration(
97 dist
, dist
.command_options
, ignore_option_errors
=ignore_option_errors
99 dist
._finalize
_license
_files
()
101 os
.chdir(current_directory
)
106 def _get_option(target_obj
: Target
, key
: str):
108 Given a target object and option key, get that option from
109 the target object, either through a get_{key} method or
110 from an attribute directly.
112 getter_name
= 'get_{key}'.format(**locals())
113 by_attribute
= functools
.partial(getattr, target_obj
, key
)
114 getter
= getattr(target_obj
, getter_name
, by_attribute
)
118 def configuration_to_dict(handlers
: Tuple
["ConfigHandler", ...]) -> dict:
119 """Returns configuration data gathered by given handlers as a dict.
121 :param list[ConfigHandler] handlers: Handlers list,
122 usually from parse_configuration()
126 config_dict
: dict = defaultdict(dict)
128 for handler
in handlers
:
129 for option
in handler
.set_options
:
130 value
= _get_option(handler
.target_obj
, option
)
131 config_dict
[handler
.section_prefix
][option
] = value
136 def parse_configuration(
137 distribution
: "Distribution",
138 command_options
: AllCommandOptions
,
139 ignore_option_errors
=False
140 ) -> Tuple
["ConfigMetadataHandler", "ConfigOptionsHandler"]:
141 """Performs additional parsing of configuration options
144 Returns a list of used option handlers.
146 :param Distribution distribution:
147 :param dict command_options:
148 :param bool ignore_option_errors: Whether to silently ignore
149 options, values of which could not be resolved (e.g. due to exceptions
150 in directives such as file:, attr:, etc.).
151 If False exceptions are propagated as expected.
154 with expand
.EnsurePackagesDiscovered(distribution
) as ensure_discovered
:
155 options
= ConfigOptionsHandler(
158 ignore_option_errors
,
163 if not distribution
.package_dir
:
164 distribution
.package_dir
= options
.package_dir
# Filled by `find_packages`
166 meta
= ConfigMetadataHandler(
167 distribution
.metadata
,
169 ignore_option_errors
,
171 distribution
.package_dir
,
172 distribution
.src_root
,
175 distribution
._referenced
_files
.update(
176 options
._referenced
_files
, meta
._referenced
_files
182 def _warn_accidental_env_marker_misconfig(label
: str, orig_value
: str, parsed
: list):
183 """Because users sometimes misinterpret this configuration:
185 [options.extras_require]
186 foo = bar;python_version<"4"
188 It looks like one requirement with an environment marker
189 but because there is no newline, it's parsed as two requirements
190 with a semicolon as separator.
193 * input string does not contain a newline AND
194 * parsed result contains two requirements AND
195 * parsing of the two parts from the result ("<first>;<second>")
196 leads in a valid Requirement with a valid marker
197 a UserWarning is shown to inform the user about the possible problem.
199 if "\n" in orig_value
or len(parsed
) != 2:
202 with contextlib
.suppress(InvalidRequirement
):
203 original_requirements_str
= ";".join(parsed
)
204 req
= Requirement(original_requirements_str
)
205 if req
.marker
is not None:
207 f
"One of the parsed requirements in `{label}` "
208 f
"looks like a valid environment marker: '{parsed[1]}'\n"
209 "Make sure that the config is correct and check "
210 "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#opt-2" # noqa: E501
212 warnings
.warn(msg
, UserWarning)
215 class ConfigHandler(Generic
[Target
]):
216 """Handles metadata supplied in configuration files."""
219 """Prefix for config sections handled by this handler.
220 Must be provided by class heirs.
224 aliases
: Dict
[str, str] = {}
226 For compatibility with various packages. E.g.: d2to1 and pbr.
227 Note: `-` in keys is replaced with `_` by config parser.
234 options
: AllCommandOptions
,
235 ignore_option_errors
,
236 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
238 sections
: AllCommandOptions
= {}
240 section_prefix
= self
.section_prefix
241 for section_name
, section_options
in options
.items():
242 if not section_name
.startswith(section_prefix
):
245 section_name
= section_name
.replace(section_prefix
, '').strip('.')
246 sections
[section_name
] = section_options
248 self
.ignore_option_errors
= ignore_option_errors
249 self
.target_obj
= target_obj
250 self
.sections
= sections
251 self
.set_options
: List
[str] = []
252 self
.ensure_discovered
= ensure_discovered
253 self
._referenced
_files
: Set
[str] = set()
254 """After parsing configurations, this property will enumerate
255 all files referenced by the "file:" directive. Private API for setuptools only.
260 """Metadata item name to parser function mapping."""
261 raise NotImplementedError(
262 '%s must provide .parsers property' % self
.__class
__.__name
__
265 def __setitem__(self
, option_name
, value
):
267 target_obj
= self
.target_obj
269 # Translate alias into real name.
270 option_name
= self
.aliases
.get(option_name
, option_name
)
272 current_value
= getattr(target_obj
, option_name
, unknown
)
274 if current_value
is unknown
:
275 raise KeyError(option_name
)
278 # Already inhabited. Skipping.
282 parser
= self
.parsers
.get(option_name
)
285 value
= parser(value
)
289 if not self
.ignore_option_errors
:
295 setter
= getattr(target_obj
, 'set_%s' % option_name
, None)
297 setattr(target_obj
, option_name
, value
)
301 self
.set_options
.append(option_name
)
304 def _parse_list(cls
, value
, separator
=','):
305 """Represents value as a list.
307 Value is split either by separator (defaults to comma) or by lines.
310 :param separator: List items separator character.
313 if isinstance(value
, list): # _get_parser_compound case
317 value
= value
.splitlines()
319 value
= value
.split(separator
)
321 return [chunk
.strip() for chunk
in value
if chunk
.strip()]
324 def _parse_dict(cls
, value
):
325 """Represents value as a dict.
332 for line
in cls
._parse
_list
(value
):
333 key
, sep
, val
= line
.partition(separator
)
335 raise DistutilsOptionError(
336 'Unable to parse option value to dict: %s' % value
338 result
[key
.strip()] = val
.strip()
343 def _parse_bool(cls
, value
):
344 """Represents value as boolean.
349 value
= value
.lower()
350 return value
in ('1', 'true', 'yes')
353 def _exclude_files_parser(cls
, key
):
354 """Returns a parser function to make sure field inputs
357 Parses a value after getting the key so error messages are
365 exclude_directive
= 'file:'
366 if value
.startswith(exclude_directive
):
368 'Only strings are accepted for the {0} field, '
369 'files are not accepted'.format(key
)
375 def _parse_file(self
, value
, root_dir
: _Path
):
376 """Represents value as a string, allowing including text
377 from nearest files using `file:` directive.
379 Directive is sandboxed and won't reach anything outside
380 directory with setup.py.
383 file: README.rst, CHANGELOG.md, src/file.txt
388 include_directive
= 'file:'
390 if not isinstance(value
, str):
393 if not value
.startswith(include_directive
):
396 spec
= value
[len(include_directive
) :]
397 filepaths
= [path
.strip() for path
in spec
.split(',')]
398 self
._referenced
_files
.update(filepaths
)
399 return expand
.read_files(filepaths
, root_dir
)
401 def _parse_attr(self
, value
, package_dir
, root_dir
: _Path
):
402 """Represents value as a module attribute.
406 attr: package.module.attr
411 attr_directive
= 'attr:'
412 if not value
.startswith(attr_directive
):
415 attr_desc
= value
.replace(attr_directive
, '')
417 # Make sure package_dir is populated correctly, so `attr:` directives can work
418 package_dir
.update(self
.ensure_discovered
.package_dir
)
419 return expand
.read_attr(attr_desc
, package_dir
, root_dir
)
422 def _get_parser_compound(cls
, *parse_methods
):
423 """Returns parser function to represents value as a list.
425 Parses a value applying given methods one after another.
427 :param parse_methods:
434 for method
in parse_methods
:
435 parsed
= method(parsed
)
442 def _parse_section_to_dict_with_key(cls
, section_options
, values_parser
):
443 """Parses section options into a dictionary.
445 Applies a given parser to each option in a section.
447 :param dict section_options:
448 :param callable values_parser: function with 2 args corresponding to key, value
452 for key
, (_
, val
) in section_options
.items():
453 value
[key
] = values_parser(key
, val
)
457 def _parse_section_to_dict(cls
, section_options
, values_parser
=None):
458 """Parses section options into a dictionary.
460 Optionally applies a given parser to each value.
462 :param dict section_options:
463 :param callable values_parser: function with 1 arg corresponding to option value
466 parser
= (lambda _
, v
: values_parser(v
)) if values_parser
else (lambda _
, v
: v
)
467 return cls
._parse
_section
_to
_dict
_with
_key
(section_options
, parser
)
469 def parse_section(self
, section_options
):
470 """Parses configuration file section.
472 :param dict section_options:
474 for (name
, (_
, value
)) in section_options
.items():
475 with contextlib
.suppress(KeyError):
476 # Keep silent for a new option may appear anytime.
480 """Parses configuration file items from one
481 or more related sections.
484 for section_name
, section_options
in self
.sections
.items():
487 if section_name
: # [section.option] variant
488 method_postfix
= '_%s' % section_name
490 section_parser_method
: Optional
[Callable
] = getattr(
492 # Dots in section names are translated into dunderscores.
493 ('parse_section%s' % method_postfix
).replace('.', '__'),
497 if section_parser_method
is None:
498 raise DistutilsOptionError(
499 'Unsupported distribution option section: [%s.%s]'
500 % (self
.section_prefix
, section_name
)
503 section_parser_method(section_options
)
505 def _deprecated_config_handler(self
, func
, msg
, warning_class
):
506 """this function will wrap around parameters that are deprecated
508 :param msg: deprecation message
509 :param warning_class: class of warning exception to be raised
510 :param func: function to be wrapped around
514 def config_handler(*args
, **kwargs
):
515 warnings
.warn(msg
, warning_class
)
516 return func(*args
, **kwargs
)
518 return config_handler
521 class ConfigMetadataHandler(ConfigHandler
["DistributionMetadata"]):
523 section_prefix
= 'metadata'
527 'summary': 'description',
528 'classifier': 'classifiers',
529 'platform': 'platforms',
533 """We need to keep it loose, to be partially compatible with
534 `pbr` and `d2to1` packages which also uses `metadata` section.
540 target_obj
: "DistributionMetadata",
541 options
: AllCommandOptions
,
542 ignore_option_errors
: bool,
543 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
544 package_dir
: Optional
[dict] = None,
545 root_dir
: _Path
= os
.curdir
547 super().__init
__(target_obj
, options
, ignore_option_errors
, ensure_discovered
)
548 self
.package_dir
= package_dir
549 self
.root_dir
= root_dir
553 """Metadata item name to parser function mapping."""
554 parse_list
= self
._parse
_list
555 parse_file
= partial(self
._parse
_file
, root_dir
=self
.root_dir
)
556 parse_dict
= self
._parse
_dict
557 exclude_files_parser
= self
._exclude
_files
_parser
560 'platforms': parse_list
,
561 'keywords': parse_list
,
562 'provides': parse_list
,
563 'requires': self
._deprecated
_config
_handler
(
565 "The requires parameter is deprecated, please use "
566 "install_requires for runtime dependencies.",
567 SetuptoolsDeprecationWarning
,
569 'obsoletes': parse_list
,
570 'classifiers': self
._get
_parser
_compound
(parse_file
, parse_list
),
571 'license': exclude_files_parser('license'),
572 'license_file': self
._deprecated
_config
_handler
(
573 exclude_files_parser('license_file'),
574 "The license_file parameter is deprecated, "
575 "use license_files instead.",
576 SetuptoolsDeprecationWarning
,
578 'license_files': parse_list
,
579 'description': parse_file
,
580 'long_description': parse_file
,
581 'version': self
._parse
_version
,
582 'project_urls': parse_dict
,
585 def _parse_version(self
, value
):
586 """Parses `version` option value.
592 version
= self
._parse
_file
(value
, self
.root_dir
)
595 version
= version
.strip()
596 # Be strict about versions loaded from file because it's easy to
597 # accidentally include newlines and other unintended content
600 except InvalidVersion
:
602 'Version loaded from {value} does not '
603 'comply with PEP 440: {version}'
605 raise DistutilsOptionError(tmpl
.format(**locals()))
609 return expand
.version(self
._parse
_attr
(value
, self
.package_dir
, self
.root_dir
))
612 class ConfigOptionsHandler(ConfigHandler
["Distribution"]):
614 section_prefix
= 'options'
618 target_obj
: "Distribution",
619 options
: AllCommandOptions
,
620 ignore_option_errors
: bool,
621 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
623 super().__init
__(target_obj
, options
, ignore_option_errors
, ensure_discovered
)
624 self
.root_dir
= target_obj
.src_root
625 self
.package_dir
: Dict
[str, str] = {} # To be filled by `find_packages`
628 def _parse_list_semicolon(cls
, value
):
629 return cls
._parse
_list
(value
, separator
=';')
631 def _parse_file_in_root(self
, value
):
632 return self
._parse
_file
(value
, root_dir
=self
.root_dir
)
634 def _parse_requirements_list(self
, label
: str, value
: str):
635 # Parse a requirements list, either by reading in a `file:`, or a list.
636 parsed
= self
._parse
_list
_semicolon
(self
._parse
_file
_in
_root
(value
))
637 _warn_accidental_env_marker_misconfig(label
, value
, parsed
)
638 # Filter it to only include lines that are not comments. `parse_list`
639 # will have stripped each line and filtered out empties.
640 return [line
for line
in parsed
if not line
.startswith("#")]
644 """Metadata item name to parser function mapping."""
645 parse_list
= self
._parse
_list
646 parse_bool
= self
._parse
_bool
647 parse_dict
= self
._parse
_dict
648 parse_cmdclass
= self
._parse
_cmdclass
651 'zip_safe': parse_bool
,
652 'include_package_data': parse_bool
,
653 'package_dir': parse_dict
,
654 'scripts': parse_list
,
655 'eager_resources': parse_list
,
656 'dependency_links': parse_list
,
657 'namespace_packages': self
._deprecated
_config
_handler
(
659 "The namespace_packages parameter is deprecated, "
660 "consider using implicit namespaces instead (PEP 420).",
661 SetuptoolsDeprecationWarning
,
663 'install_requires': partial(
664 self
._parse
_requirements
_list
, "install_requires"
666 'setup_requires': self
._parse
_list
_semicolon
,
667 'tests_require': self
._parse
_list
_semicolon
,
668 'packages': self
._parse
_packages
,
669 'entry_points': self
._parse
_file
_in
_root
,
670 'py_modules': parse_list
,
671 'python_requires': SpecifierSet
,
672 'cmdclass': parse_cmdclass
,
675 def _parse_cmdclass(self
, value
):
676 package_dir
= self
.ensure_discovered
.package_dir
677 return expand
.cmdclass(self
._parse
_dict
(value
), package_dir
, self
.root_dir
)
679 def _parse_packages(self
, value
):
680 """Parses `packages` option value.
685 find_directives
= ['find:', 'find_namespace:']
686 trimmed_value
= value
.strip()
688 if trimmed_value
not in find_directives
:
689 return self
._parse
_list
(value
)
691 # Read function arguments from a dedicated section.
692 find_kwargs
= self
.parse_section_packages__find(
693 self
.sections
.get('packages.find', {})
697 namespaces
=(trimmed_value
== find_directives
[1]),
698 root_dir
=self
.root_dir
,
699 fill_package_dir
=self
.package_dir
,
702 return expand
.find_packages(**find_kwargs
)
704 def parse_section_packages__find(self
, section_options
):
705 """Parses `packages.find` configuration file section.
707 To be used in conjunction with _parse_packages().
709 :param dict section_options:
711 section_data
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
713 valid_keys
= ['where', 'include', 'exclude']
716 [(k
, v
) for k
, v
in section_data
.items() if k
in valid_keys
and v
]
719 where
= find_kwargs
.get('where')
720 if where
is not None:
721 find_kwargs
['where'] = where
[0] # cast list to single val
725 def parse_section_entry_points(self
, section_options
):
726 """Parses `entry_points` configuration file section.
728 :param dict section_options:
730 parsed
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
731 self
['entry_points'] = parsed
733 def _parse_package_data(self
, section_options
):
734 package_data
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
735 return expand
.canonic_package_data(package_data
)
737 def parse_section_package_data(self
, section_options
):
738 """Parses `package_data` configuration file section.
740 :param dict section_options:
742 self
['package_data'] = self
._parse
_package
_data
(section_options
)
744 def parse_section_exclude_package_data(self
, section_options
):
745 """Parses `exclude_package_data` configuration file section.
747 :param dict section_options:
749 self
['exclude_package_data'] = self
._parse
_package
_data
(section_options
)
751 def parse_section_extras_require(self
, section_options
):
752 """Parses `extras_require` configuration file section.
754 :param dict section_options:
756 parsed
= self
._parse
_section
_to
_dict
_with
_key
(
758 lambda k
, v
: self
._parse
_requirements
_list
(f
"extras_require[{k}]", v
)
761 self
['extras_require'] = parsed
763 def parse_section_data_files(self
, section_options
):
764 """Parses `data_files` configuration file section.
766 :param dict section_options:
768 parsed
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
769 self
['data_files'] = expand
.canonic_data_files(parsed
, self
.root_dir
)