2 Load setuptools configuration from ``setup.cfg`` files.
4 **API will be made private in the future**
6 To read project metadata, consider using
7 ``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
8 For simple scenarios, you can also try parsing the file directly
9 with the help of ``configparser``.
14 from collections
import defaultdict
15 from functools
import partial
16 from functools
import wraps
32 from ..errors
import FileError
, OptionError
33 from ..extern
.packaging
.markers
import default_environment
as marker_env
34 from ..extern
.packaging
.requirements
import InvalidRequirement
, Requirement
35 from ..extern
.packaging
.specifiers
import SpecifierSet
36 from ..extern
.packaging
.version
import InvalidVersion
, Version
37 from ..warnings
import SetuptoolsDeprecationWarning
41 from distutils
.dist
import DistributionMetadata
# noqa
43 from setuptools
.dist
import Distribution
# noqa
45 _Path
= Union
[str, os
.PathLike
]
46 SingleCommandOptions
= Dict
["str", Tuple
["str", Any
]]
47 """Dict that associate the name of the options of a particular command to a
48 tuple. The first element of the tuple indicates the origin of the option value
49 (e.g. the name of the configuration file where it was read from),
50 while the second element of the tuple is the option value itself
52 AllCommandOptions
= Dict
["str", SingleCommandOptions
] # cmd name => its options
53 Target
= TypeVar("Target", bound
=Union
["Distribution", "DistributionMetadata"])
56 def read_configuration(
57 filepath
: _Path
, find_others
=False, ignore_option_errors
=False
59 """Read given configuration file and returns options from it as a dict.
61 :param str|unicode filepath: Path to configuration file
64 :param bool find_others: Whether to search for other configuration files
65 which could be on in various places.
67 :param bool ignore_option_errors: Whether to silently ignore
68 options, values of which could not be resolved (e.g. due to exceptions
69 in directives such as file:, attr:, etc.).
70 If False exceptions are propagated as expected.
74 from setuptools
.dist
import Distribution
77 filenames
= dist
.find_config_files() if find_others
else []
78 handlers
= _apply(dist
, filepath
, filenames
, ignore_option_errors
)
79 return configuration_to_dict(handlers
)
82 def apply_configuration(dist
: "Distribution", filepath
: _Path
) -> "Distribution":
83 """Apply the configuration from a ``setup.cfg`` file into an existing
86 _apply(dist
, filepath
)
87 dist
._finalize
_requires
()
94 other_files
: Iterable
[_Path
] = (),
95 ignore_option_errors
: bool = False,
96 ) -> Tuple
["ConfigHandler", ...]:
97 """Read configuration from ``filepath`` and applies to the ``dist`` object."""
98 from setuptools
.dist
import _Distribution
100 filepath
= os
.path
.abspath(filepath
)
102 if not os
.path
.isfile(filepath
):
103 raise FileError(f
'Configuration file {filepath} does not exist.')
105 current_directory
= os
.getcwd()
106 os
.chdir(os
.path
.dirname(filepath
))
107 filenames
= [*other_files
, filepath
]
110 _Distribution
.parse_config_files(dist
, filenames
=filenames
)
111 handlers
= parse_configuration(
112 dist
, dist
.command_options
, ignore_option_errors
=ignore_option_errors
114 dist
._finalize
_license
_files
()
116 os
.chdir(current_directory
)
121 def _get_option(target_obj
: Target
, key
: str):
123 Given a target object and option key, get that option from
124 the target object, either through a get_{key} method or
125 from an attribute directly.
127 getter_name
= f
'get_{key}'
128 by_attribute
= functools
.partial(getattr, target_obj
, key
)
129 getter
= getattr(target_obj
, getter_name
, by_attribute
)
133 def configuration_to_dict(handlers
: Tuple
["ConfigHandler", ...]) -> dict:
134 """Returns configuration data gathered by given handlers as a dict.
136 :param list[ConfigHandler] handlers: Handlers list,
137 usually from parse_configuration()
141 config_dict
: dict = defaultdict(dict)
143 for handler
in handlers
:
144 for option
in handler
.set_options
:
145 value
= _get_option(handler
.target_obj
, option
)
146 config_dict
[handler
.section_prefix
][option
] = value
151 def parse_configuration(
152 distribution
: "Distribution",
153 command_options
: AllCommandOptions
,
154 ignore_option_errors
=False,
155 ) -> Tuple
["ConfigMetadataHandler", "ConfigOptionsHandler"]:
156 """Performs additional parsing of configuration options
159 Returns a list of used option handlers.
161 :param Distribution distribution:
162 :param dict command_options:
163 :param bool ignore_option_errors: Whether to silently ignore
164 options, values of which could not be resolved (e.g. due to exceptions
165 in directives such as file:, attr:, etc.).
166 If False exceptions are propagated as expected.
169 with expand
.EnsurePackagesDiscovered(distribution
) as ensure_discovered
:
170 options
= ConfigOptionsHandler(
173 ignore_option_errors
,
178 if not distribution
.package_dir
:
179 distribution
.package_dir
= options
.package_dir
# Filled by `find_packages`
181 meta
= ConfigMetadataHandler(
182 distribution
.metadata
,
184 ignore_option_errors
,
186 distribution
.package_dir
,
187 distribution
.src_root
,
190 distribution
._referenced
_files
.update(
191 options
._referenced
_files
, meta
._referenced
_files
197 def _warn_accidental_env_marker_misconfig(label
: str, orig_value
: str, parsed
: list):
198 """Because users sometimes misinterpret this configuration:
200 [options.extras_require]
201 foo = bar;python_version<"4"
203 It looks like one requirement with an environment marker
204 but because there is no newline, it's parsed as two requirements
205 with a semicolon as separator.
208 * input string does not contain a newline AND
209 * parsed result contains two requirements AND
210 * parsing of the two parts from the result ("<first>;<second>")
211 leads in a valid Requirement with a valid marker
212 a UserWarning is shown to inform the user about the possible problem.
214 if "\n" in orig_value
or len(parsed
) != 2:
217 markers
= marker_env().keys()
220 req
= Requirement(parsed
[1])
221 if req
.name
in markers
:
222 _AmbiguousMarker
.emit(field
=label
, req
=parsed
[1])
223 except InvalidRequirement
as ex
:
224 if any(parsed
[1].startswith(marker
) for marker
in markers
):
225 msg
= _AmbiguousMarker
.message(field
=label
, req
=parsed
[1])
226 raise InvalidRequirement(msg
) from ex
229 class ConfigHandler(Generic
[Target
]):
230 """Handles metadata supplied in configuration files."""
233 """Prefix for config sections handled by this handler.
234 Must be provided by class heirs.
238 aliases
: Dict
[str, str] = {}
240 For compatibility with various packages. E.g.: d2to1 and pbr.
241 Note: `-` in keys is replaced with `_` by config parser.
248 options
: AllCommandOptions
,
249 ignore_option_errors
,
250 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
252 self
.ignore_option_errors
= ignore_option_errors
253 self
.target_obj
= target_obj
254 self
.sections
= dict(self
._section
_options
(options
))
255 self
.set_options
: List
[str] = []
256 self
.ensure_discovered
= ensure_discovered
257 self
._referenced
_files
: Set
[str] = set()
258 """After parsing configurations, this property will enumerate
259 all files referenced by the "file:" directive. Private API for setuptools only.
263 def _section_options(cls
, options
: AllCommandOptions
):
264 for full_name
, value
in options
.items():
265 pre
, sep
, name
= full_name
.partition(cls
.section_prefix
)
268 yield name
.lstrip('.'), value
272 """Metadata item name to parser function mapping."""
273 raise NotImplementedError(
274 '%s must provide .parsers property' % self
.__class
__.__name
__
277 def __setitem__(self
, option_name
, value
):
278 target_obj
= self
.target_obj
280 # Translate alias into real name.
281 option_name
= self
.aliases
.get(option_name
, option_name
)
284 current_value
= getattr(target_obj
, option_name
)
285 except AttributeError:
286 raise KeyError(option_name
)
289 # Already inhabited. Skipping.
293 parsed
= self
.parsers
.get(option_name
, lambda x
: x
)(value
)
294 except (Exception,) * self
.ignore_option_errors
:
297 simple_setter
= functools
.partial(target_obj
.__setattr
__, option_name
)
298 setter
= getattr(target_obj
, 'set_%s' % option_name
, simple_setter
)
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 OptionError(f
"Unable to parse option value to dict: {value}")
336 result
[key
.strip()] = val
.strip()
341 def _parse_bool(cls
, value
):
342 """Represents value as boolean.
347 value
= value
.lower()
348 return value
in ('1', 'true', 'yes')
351 def _exclude_files_parser(cls
, key
):
352 """Returns a parser function to make sure field inputs
355 Parses a value after getting the key so error messages are
363 exclude_directive
= 'file:'
364 if value
.startswith(exclude_directive
):
366 'Only strings are accepted for the {0} field, '
367 'files are not accepted'.format(key
)
373 def _parse_file(self
, value
, root_dir
: _Path
):
374 """Represents value as a string, allowing including text
375 from nearest files using `file:` directive.
377 Directive is sandboxed and won't reach anything outside
378 directory with setup.py.
381 file: README.rst, CHANGELOG.md, src/file.txt
386 include_directive
= 'file:'
388 if not isinstance(value
, str):
391 if not value
.startswith(include_directive
):
394 spec
= value
[len(include_directive
) :]
395 filepaths
= [path
.strip() for path
in spec
.split(',')]
396 self
._referenced
_files
.update(filepaths
)
397 return expand
.read_files(filepaths
, root_dir
)
399 def _parse_attr(self
, value
, package_dir
, root_dir
: _Path
):
400 """Represents value as a module attribute.
404 attr: package.module.attr
409 attr_directive
= 'attr:'
410 if not value
.startswith(attr_directive
):
413 attr_desc
= value
.replace(attr_directive
, '')
415 # Make sure package_dir is populated correctly, so `attr:` directives can work
416 package_dir
.update(self
.ensure_discovered
.package_dir
)
417 return expand
.read_attr(attr_desc
, package_dir
, root_dir
)
420 def _get_parser_compound(cls
, *parse_methods
):
421 """Returns parser function to represents value as a list.
423 Parses a value applying given methods one after another.
425 :param parse_methods:
432 for method
in parse_methods
:
433 parsed
= method(parsed
)
440 def _parse_section_to_dict_with_key(cls
, section_options
, values_parser
):
441 """Parses section options into a dictionary.
443 Applies a given parser to each option in a section.
445 :param dict section_options:
446 :param callable values_parser: function with 2 args corresponding to key, value
450 for key
, (_
, val
) in section_options
.items():
451 value
[key
] = values_parser(key
, val
)
455 def _parse_section_to_dict(cls
, section_options
, values_parser
=None):
456 """Parses section options into a dictionary.
458 Optionally applies a given parser to each value.
460 :param dict section_options:
461 :param callable values_parser: function with 1 arg corresponding to option value
464 parser
= (lambda _
, v
: values_parser(v
)) if values_parser
else (lambda _
, v
: v
)
465 return cls
._parse
_section
_to
_dict
_with
_key
(section_options
, parser
)
467 def parse_section(self
, section_options
):
468 """Parses configuration file section.
470 :param dict section_options:
472 for name
, (_
, value
) in section_options
.items():
473 with contextlib
.suppress(KeyError):
474 # Keep silent for a new option may appear anytime.
478 """Parses configuration file items from one
479 or more related sections.
482 for section_name
, section_options
in self
.sections
.items():
484 if section_name
: # [section.option] variant
485 method_postfix
= '_%s' % section_name
487 section_parser_method
: Optional
[Callable
] = getattr(
489 # Dots in section names are translated into dunderscores.
490 ('parse_section%s' % method_postfix
).replace('.', '__'),
494 if section_parser_method
is None:
496 "Unsupported distribution option section: "
497 f
"[{self.section_prefix}.{section_name}]"
500 section_parser_method(section_options
)
502 def _deprecated_config_handler(self
, func
, msg
, **kw
):
503 """this function will wrap around parameters that are deprecated
505 :param msg: deprecation message
506 :param func: function to be wrapped around
510 def config_handler(*args
, **kwargs
):
511 kw
.setdefault("stacklevel", 2)
512 _DeprecatedConfig
.emit("Deprecated config in `setup.cfg`", msg
, **kw
)
513 return func(*args
, **kwargs
)
515 return config_handler
518 class ConfigMetadataHandler(ConfigHandler
["DistributionMetadata"]):
519 section_prefix
= 'metadata'
523 'summary': 'description',
524 'classifier': 'classifiers',
525 'platform': 'platforms',
529 """We need to keep it loose, to be partially compatible with
530 `pbr` and `d2to1` packages which also uses `metadata` section.
536 target_obj
: "DistributionMetadata",
537 options
: AllCommandOptions
,
538 ignore_option_errors
: bool,
539 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
540 package_dir
: Optional
[dict] = None,
541 root_dir
: _Path
= os
.curdir
,
543 super().__init
__(target_obj
, options
, ignore_option_errors
, ensure_discovered
)
544 self
.package_dir
= package_dir
545 self
.root_dir
= root_dir
549 """Metadata item name to parser function mapping."""
550 parse_list
= self
._parse
_list
551 parse_file
= partial(self
._parse
_file
, root_dir
=self
.root_dir
)
552 parse_dict
= self
._parse
_dict
553 exclude_files_parser
= self
._exclude
_files
_parser
556 'platforms': parse_list
,
557 'keywords': parse_list
,
558 'provides': parse_list
,
559 'requires': self
._deprecated
_config
_handler
(
561 "The requires parameter is deprecated, please use "
562 "install_requires for runtime dependencies.",
563 due_date
=(2023, 10, 30),
564 # Warning introduced in 27 Oct 2018
566 'obsoletes': parse_list
,
567 'classifiers': self
._get
_parser
_compound
(parse_file
, parse_list
),
568 'license': exclude_files_parser('license'),
569 'license_file': self
._deprecated
_config
_handler
(
570 exclude_files_parser('license_file'),
571 "The license_file parameter is deprecated, "
572 "use license_files instead.",
573 due_date
=(2023, 10, 30),
574 # Warning introduced in 23 May 2021
576 'license_files': parse_list
,
577 'description': parse_file
,
578 'long_description': parse_file
,
579 'version': self
._parse
_version
,
580 'project_urls': parse_dict
,
583 def _parse_version(self
, value
):
584 """Parses `version` option value.
590 version
= self
._parse
_file
(value
, self
.root_dir
)
593 version
= version
.strip()
594 # Be strict about versions loaded from file because it's easy to
595 # accidentally include newlines and other unintended content
598 except InvalidVersion
:
600 f
'Version loaded from {value} does not '
601 f
'comply with PEP 440: {version}'
606 return expand
.version(self
._parse
_attr
(value
, self
.package_dir
, self
.root_dir
))
609 class ConfigOptionsHandler(ConfigHandler
["Distribution"]):
610 section_prefix
= 'options'
614 target_obj
: "Distribution",
615 options
: AllCommandOptions
,
616 ignore_option_errors
: bool,
617 ensure_discovered
: expand
.EnsurePackagesDiscovered
,
619 super().__init
__(target_obj
, options
, ignore_option_errors
, ensure_discovered
)
620 self
.root_dir
= target_obj
.src_root
621 self
.package_dir
: Dict
[str, str] = {} # To be filled by `find_packages`
624 def _parse_list_semicolon(cls
, value
):
625 return cls
._parse
_list
(value
, separator
=';')
627 def _parse_file_in_root(self
, value
):
628 return self
._parse
_file
(value
, root_dir
=self
.root_dir
)
630 def _parse_requirements_list(self
, label
: str, value
: str):
631 # Parse a requirements list, either by reading in a `file:`, or a list.
632 parsed
= self
._parse
_list
_semicolon
(self
._parse
_file
_in
_root
(value
))
633 _warn_accidental_env_marker_misconfig(label
, value
, parsed
)
634 # Filter it to only include lines that are not comments. `parse_list`
635 # will have stripped each line and filtered out empties.
636 return [line
for line
in parsed
if not line
.startswith("#")]
640 """Metadata item name to parser function mapping."""
641 parse_list
= self
._parse
_list
642 parse_bool
= self
._parse
_bool
643 parse_dict
= self
._parse
_dict
644 parse_cmdclass
= self
._parse
_cmdclass
647 'zip_safe': parse_bool
,
648 'include_package_data': parse_bool
,
649 'package_dir': parse_dict
,
650 'scripts': parse_list
,
651 'eager_resources': parse_list
,
652 'dependency_links': parse_list
,
653 'namespace_packages': self
._deprecated
_config
_handler
(
655 "The namespace_packages parameter is deprecated, "
656 "consider using implicit namespaces instead (PEP 420).",
657 # TODO: define due date, see setuptools.dist:check_nsp.
659 'install_requires': partial(
660 self
._parse
_requirements
_list
, "install_requires"
662 'setup_requires': self
._parse
_list
_semicolon
,
663 'tests_require': self
._parse
_list
_semicolon
,
664 'packages': self
._parse
_packages
,
665 'entry_points': self
._parse
_file
_in
_root
,
666 'py_modules': parse_list
,
667 'python_requires': SpecifierSet
,
668 'cmdclass': parse_cmdclass
,
671 def _parse_cmdclass(self
, value
):
672 package_dir
= self
.ensure_discovered
.package_dir
673 return expand
.cmdclass(self
._parse
_dict
(value
), package_dir
, self
.root_dir
)
675 def _parse_packages(self
, value
):
676 """Parses `packages` option value.
681 find_directives
= ['find:', 'find_namespace:']
682 trimmed_value
= value
.strip()
684 if trimmed_value
not in find_directives
:
685 return self
._parse
_list
(value
)
687 # Read function arguments from a dedicated section.
688 find_kwargs
= self
.parse_section_packages__find(
689 self
.sections
.get('packages.find', {})
693 namespaces
=(trimmed_value
== find_directives
[1]),
694 root_dir
=self
.root_dir
,
695 fill_package_dir
=self
.package_dir
,
698 return expand
.find_packages(**find_kwargs
)
700 def parse_section_packages__find(self
, section_options
):
701 """Parses `packages.find` configuration file section.
703 To be used in conjunction with _parse_packages().
705 :param dict section_options:
707 section_data
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
709 valid_keys
= ['where', 'include', 'exclude']
712 [(k
, v
) for k
, v
in section_data
.items() if k
in valid_keys
and v
]
715 where
= find_kwargs
.get('where')
716 if where
is not None:
717 find_kwargs
['where'] = where
[0] # cast list to single val
721 def parse_section_entry_points(self
, section_options
):
722 """Parses `entry_points` configuration file section.
724 :param dict section_options:
726 parsed
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
727 self
['entry_points'] = parsed
729 def _parse_package_data(self
, section_options
):
730 package_data
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
731 return expand
.canonic_package_data(package_data
)
733 def parse_section_package_data(self
, section_options
):
734 """Parses `package_data` configuration file section.
736 :param dict section_options:
738 self
['package_data'] = self
._parse
_package
_data
(section_options
)
740 def parse_section_exclude_package_data(self
, section_options
):
741 """Parses `exclude_package_data` configuration file section.
743 :param dict section_options:
745 self
['exclude_package_data'] = self
._parse
_package
_data
(section_options
)
747 def parse_section_extras_require(self
, section_options
):
748 """Parses `extras_require` configuration file section.
750 :param dict section_options:
752 parsed
= self
._parse
_section
_to
_dict
_with
_key
(
754 lambda k
, v
: self
._parse
_requirements
_list
(f
"extras_require[{k}]", v
),
757 self
['extras_require'] = parsed
759 def parse_section_data_files(self
, section_options
):
760 """Parses `data_files` configuration file section.
762 :param dict section_options:
764 parsed
= self
._parse
_section
_to
_dict
(section_options
, self
._parse
_list
)
765 self
['data_files'] = expand
.canonic_data_files(parsed
, self
.root_dir
)
768 class _AmbiguousMarker(SetuptoolsDeprecationWarning
):
769 _SUMMARY
= "Ambiguous requirement marker."
771 One of the parsed requirements in `{field}` looks like a valid environment marker:
775 Please make sure that the configuration file is correct.
776 You can use dangling lines to avoid this problem.
778 _SEE_DOCS
= "userguide/declarative_config.html#opt-2"
779 # TODO: should we include due_date here? Initially introduced in 6 Aug 2022.
780 # Does this make sense with latest version of packaging?
783 def message(cls
, **kw
):
784 docs
= f
"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}"
785 return cls
._format
(cls
._SUMMARY
, cls
._DETAILS
, see_url
=docs
, format_args
=kw
)
788 class _DeprecatedConfig(SetuptoolsDeprecationWarning
):
789 _SEE_DOCS
= "userguide/declarative_config.html"