Skip to content

Commit 506e7e7

Browse files
frenzymadnessabravalheri
authored andcommitted
Add warning for potential extras_require misconfiguration
Fixes: #3467
1 parent d90cf84 commit 506e7e7

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

setuptools/config/setupcfg.py

+48
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Optional, Tuple, TypeVar, Union)
1515

1616
from distutils.errors import DistutilsOptionError, DistutilsFileError
17+
from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement
1718
from setuptools.extern.packaging.version import Version, InvalidVersion
1819
from setuptools.extern.packaging.specifiers import SpecifierSet
1920
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
@@ -174,6 +175,43 @@ def parse_configuration(
174175
return meta, options
175176

176177

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+
177215
class ConfigHandler(Generic[Target]):
178216
"""Handles metadata supplied in configuration files."""
179217

@@ -421,6 +459,13 @@ def parse_section(self, section_options):
421459
try:
422460
self[name] = value
423461

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+
424469
except KeyError:
425470
pass # Keep silent for a new option may appear anytime.
426471

@@ -702,6 +747,9 @@ def parse_section_extras_require(self, section_options):
702747
section_options,
703748
self._parse_requirements_list,
704749
)
750+
751+
warn_accidental_env_marker_misconfig("extras_require", section_options, parsed)
752+
705753
self['extras_require'] = parsed
706754

707755
def parse_section_data_files(self, section_options):

setuptools/tests/config/test_setupcfg.py

+45
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,51 @@ def test_extras_require(self, tmpdir):
716716
}
717717
assert dist.metadata.provides_extras == set(['pdf', 'rest'])
718718

719+
@pytest.mark.parametrize(
720+
"config",
721+
[
722+
"[options.extras_require]\nfoo = bar;python_version<'3'",
723+
"[options.extras_require]\nfoo = bar;os_name=='linux'",
724+
"[options.extras_require]\nfoo = bar;python_version<'3'\n",
725+
"[options.extras_require]\nfoo = bar;os_name=='linux'\n",
726+
"[options]\ninstall_requires = bar;python_version<'3'",
727+
"[options]\ninstall_requires = bar;os_name=='linux'",
728+
"[options]\ninstall_requires = bar;python_version<'3'\n",
729+
"[options]\ninstall_requires = bar;os_name=='linux'\n",
730+
],
731+
)
732+
def test_warn_accidental_env_marker_misconfig(self, config, tmpdir):
733+
fake_env(tmpdir, config)
734+
match = (
735+
r"One of the parsed requirements in (install_requires|extras_require) "
736+
"section looks like a valid environment marker.*"
737+
)
738+
with pytest.warns(UserWarning, match=match):
739+
with get_dist(tmpdir) as _:
740+
pass
741+
742+
@pytest.mark.parametrize(
743+
"config",
744+
[
745+
"[options.extras_require]\nfoo =\n bar;python_version<'3'",
746+
"[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy",
747+
"[options.extras_require]\nfoo =\n bar;python_version<'3'\n",
748+
"[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy\n",
749+
"[options.extras_require]\nfoo =\n bar\n python_version<'3'\n",
750+
"[options]\ninstall_requires =\n bar;python_version<'3'",
751+
"[options]\ninstall_requires = bar;baz\nboo = xxx;yyy",
752+
"[options]\ninstall_requires =\n bar;python_version<'3'\n",
753+
"[options]\ninstall_requires = bar;baz\nboo = xxx;yyy\n",
754+
"[options]\ninstall_requires =\n bar\n python_version<'3'\n",
755+
],
756+
)
757+
def test_nowarn_accidental_env_marker_misconfig(self, config, tmpdir, recwarn):
758+
fake_env(tmpdir, config)
759+
with get_dist(tmpdir) as _:
760+
pass
761+
# The examples are valid, no warnings shown
762+
assert not any(w.category == UserWarning for w in recwarn)
763+
719764
def test_dash_preserved_extras_require(self, tmpdir):
720765
fake_env(tmpdir, '[options.extras_require]\n' 'foo-a = foo\n' 'foo_b = test\n')
721766

0 commit comments

Comments
 (0)