|
14 | 14 | Optional, Tuple, TypeVar, Union)
|
15 | 15 |
|
16 | 16 | from distutils.errors import DistutilsOptionError, DistutilsFileError
|
| 17 | +from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement |
17 | 18 | from setuptools.extern.packaging.version import Version, InvalidVersion
|
18 | 19 | from setuptools.extern.packaging.specifiers import SpecifierSet
|
19 | 20 | from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
|
@@ -174,6 +175,43 @@ def parse_configuration(
|
174 | 175 | return meta, options
|
175 | 176 |
|
176 | 177 |
|
| 178 | +def warn_accidental_env_marker_misconfig(section_name, section_options, parsed): |
| 179 | + """Because users sometimes misinterpret this configuration: |
| 180 | +
|
| 181 | + [options.extras_require] |
| 182 | + foo = bar;python_version<"4" |
| 183 | +
|
| 184 | + It looks like one requirement with an environment marker |
| 185 | + but because there is no newline, it's parsed as two requirements |
| 186 | + with a semicolon as separator. |
| 187 | +
|
| 188 | + Therefore, if: |
| 189 | + * input string does not contain a newline AND |
| 190 | + * parsed result contains two requirements AND |
| 191 | + * parsing of the two parts from the result ("<first>;<second>") |
| 192 | + leads in a valid Requirement with a valid marker |
| 193 | + a UserWarning is shown to inform the user about the possible problem. |
| 194 | + """ |
| 195 | + |
| 196 | + for name, (file, requirements) in section_options.items(): |
| 197 | + if "\n" not in requirements and len(parsed[name]) == 2: |
| 198 | + original_requirements_str = ";".join(parsed[name]) |
| 199 | + try: |
| 200 | + req = Requirement(original_requirements_str) |
| 201 | + except InvalidRequirement: |
| 202 | + pass |
| 203 | + else: |
| 204 | + if req.marker is None: |
| 205 | + continue |
| 206 | + msg = ( |
| 207 | + f"One of the parsed requirements in {section_name} section " |
| 208 | + f"looks like a valid environment marker: '{parsed[name][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 |
| 211 | + ) |
| 212 | + warnings.warn(msg, UserWarning) |
| 213 | + |
| 214 | + |
177 | 215 | class ConfigHandler(Generic[Target]):
|
178 | 216 | """Handles metadata supplied in configuration files."""
|
179 | 217 |
|
@@ -421,6 +459,13 @@ def parse_section(self, section_options):
|
421 | 459 | try:
|
422 | 460 | self[name] = value
|
423 | 461 |
|
| 462 | + if name == "install_requires": |
| 463 | + warn_accidental_env_marker_misconfig( |
| 464 | + "install_requires", |
| 465 | + {name: (_, value)}, |
| 466 | + {name: self.target_obj.install_requires}, |
| 467 | + ) |
| 468 | + |
424 | 469 | except KeyError:
|
425 | 470 | pass # Keep silent for a new option may appear anytime.
|
426 | 471 |
|
@@ -702,6 +747,9 @@ def parse_section_extras_require(self, section_options):
|
702 | 747 | section_options,
|
703 | 748 | self._parse_requirements_list,
|
704 | 749 | )
|
| 750 | + |
| 751 | + warn_accidental_env_marker_misconfig("extras_require", section_options, parsed) |
| 752 | + |
705 | 753 | self['extras_require'] = parsed
|
706 | 754 |
|
707 | 755 | def parse_section_data_files(self, section_options):
|
|
0 commit comments