diff options
| author | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-08-06 20:20:20 +0100 |
|---|---|---|
| committer | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-08-06 20:20:20 +0100 |
| commit | 5aa389c770e0d58eb8c0999f8888fb65845d4f73 (patch) | |
| tree | 8c6cf69160e908d796d2f618045aba7400aa96b9 /setuptools/config | |
| parent | ccca622fa78296503f3c2c7deddca7ecd2b61fc6 (diff) | |
| parent | 5179e8fcd89657d0f9b3660e2a7ec6f6eec9ce36 (diff) | |
| download | python-setuptools-git-5aa389c770e0d58eb8c0999f8888fb65845d4f73.tar.gz | |
Merge 'main' into feature/pep660
Diffstat (limited to 'setuptools/config')
| -rw-r--r-- | setuptools/config/pyprojecttoml.py | 12 | ||||
| -rw-r--r-- | setuptools/config/setupcfg.py | 77 |
2 files changed, 71 insertions, 18 deletions
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 0e9e3c9c..9ff0c87f 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -41,10 +41,14 @@ def validate(config: dict, filepath: _Path) -> bool: try: return validator.validate(config) except validator.ValidationError as ex: - _logger.error(f"configuration error: {ex.summary}") # type: ignore - _logger.debug(ex.details) # type: ignore - error = ValueError(f"invalid pyproject.toml config: {ex.name}") # type: ignore - raise error from None + summary = f"configuration error: {ex.summary}" + if ex.name.strip("`") != "project": + # Probably it is just a field missing/misnamed, not worthy the verbosity... + _logger.debug(summary) + _logger.debug(ex.details) + + error = f"invalid pyproject.toml config: {ex.name}." + raise ValueError(f"{error}\n{summary}") from None def apply_configuration( diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index af128968..c2a974de 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -5,8 +5,9 @@ Load setuptools configuration from ``setup.cfg`` files. """ import os -import warnings +import contextlib import functools +import warnings from collections import defaultdict from functools import partial from functools import wraps @@ -14,6 +15,7 @@ from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar, Union) from distutils.errors import DistutilsOptionError, DistutilsFileError +from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement from setuptools.extern.packaging.version import Version, InvalidVersion from setuptools.extern.packaging.specifiers import SpecifierSet from setuptools._deprecation_warning import SetuptoolsDeprecationWarning @@ -174,6 +176,39 @@ def parse_configuration( 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 + + with contextlib.suppress(InvalidRequirement): + original_requirements_str = ";".join(parsed) + req = Requirement(original_requirements_str) + if req.marker is not None: + msg = ( + f"One of the parsed requirements in `{label}` " + f"looks like a valid environment marker: '{parsed[1]}'\n" + "Make sure that the config is correct and check " + "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#opt-2" # noqa: E501 + ) + warnings.warn(msg, UserWarning) + + class ConfigHandler(Generic[Target]): """Handles metadata supplied in configuration files.""" @@ -397,33 +432,43 @@ class ConfigHandler(Generic[Target]): return parse @classmethod - def _parse_section_to_dict(cls, section_options, values_parser=None): + def _parse_section_to_dict_with_key(cls, section_options, values_parser): """Parses section options into a dictionary. - Optionally applies a given parser to values. + Applies a given parser to each option in a section. :param dict section_options: - :param callable values_parser: + :param callable values_parser: function with 2 args corresponding to key, value :rtype: dict """ value = {} - values_parser = values_parser or (lambda val: val) for key, (_, val) in section_options.items(): - value[key] = values_parser(val) + 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(): - try: + with contextlib.suppress(KeyError): + # Keep silent for a new option may appear anytime. self[name] = value - except KeyError: - pass # Keep silent for a new option may appear anytime. - def parse(self): """Parses configuration file items from one or more related sections. @@ -579,9 +624,10 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): def _parse_file_in_root(self, value): return self._parse_file(value, root_dir=self.root_dir) - def _parse_requirements_list(self, value): + 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("#")] @@ -607,7 +653,9 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): "consider using implicit namespaces instead (PEP 420).", SetuptoolsDeprecationWarning, ), - 'install_requires': self._parse_requirements_list, + '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, @@ -698,10 +746,11 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): :param dict section_options: """ - parsed = self._parse_section_to_dict( + parsed = self._parse_section_to_dict_with_key( section_options, - self._parse_requirements_list, + lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v) ) + self['extras_require'] = parsed def parse_section_data_files(self, section_options): |
