Skip to content

Commit bb96c07

Browse files
committed
parse_setup_py: Handle string dependency when we expect list of strings
As described in issue #429, the s3transfer package comes with setup.py where the `extras_require` dictionary does not map to a list of strings (extra requirements), but instead map to a string directly. When s3transfer is installed with this extra (e.g. `pip install s3transfer[crt]`), the setuptools machinery handles this variation gracefully, i.e. it interprets the string as if it was a single-element list containing the same string. An experiment reveals that the same interpretation occurs if a string is passed directly in the `install_requires` argument. This commit teaches FawltyDeps to do the same. Also, if parse_one_req() fails to parse a requirement string, catch that exception (likely an `InvalidRequirement` or some other subclass of ValueError), and convert it into a DependencyParsingError, which is handled more gracefully (i.e. print a warning message and continue).
1 parent fe7787e commit bb96c07

File tree

2 files changed

+31
-8
lines changed

2 files changed

+31
-8
lines changed

fawltydeps/extract_declared_dependencies.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dataclasses import replace
1010
from pathlib import Path
1111
from tempfile import NamedTemporaryFile
12-
from typing import Callable, Iterable, Iterator, NamedTuple, Optional, Tuple
12+
from typing import Callable, Iterable, Iterator, NamedTuple, Optional, Tuple, Union
1313

1414
from pip_requirements_parser import RequirementsFile # type: ignore[import]
1515
from pkg_resources import Requirement
@@ -79,24 +79,32 @@ def parse_setup_py(path: Path) -> Iterator[DeclaredDependency]: # noqa: C901
7979
# resolve any variable references in the arguments to the setup() call.
8080
tracked_vars = VariableTracker(source)
8181

82-
def _extract_deps_from_setup_call( # noqa: C901
82+
def _extract_deps_from_value(
83+
value: Union[str, Iterable[str]],
84+
node: ast.AST,
85+
) -> Iterator[DeclaredDependency]:
86+
if isinstance(value, str): # expected list, but got string
87+
value = [value] # parse as if a single-element list is given
88+
try:
89+
for item in value:
90+
yield parse_one_req(item, source)
91+
except ValueError as e: # parse_one_req() failed
92+
raise DependencyParsingError(node) from e
93+
94+
def _extract_deps_from_setup_call(
8395
node: ast.Call,
8496
) -> Iterator[DeclaredDependency]:
8597
for keyword in node.keywords:
8698
try:
8799
if keyword.arg == "install_requires":
88100
value = tracked_vars.resolve(keyword.value)
89-
if not isinstance(value, list):
90-
raise DependencyParsingError(keyword.value)
91-
for item in value:
92-
yield parse_one_req(item, source)
101+
yield from _extract_deps_from_value(value, keyword.value)
93102
elif keyword.arg == "extras_require":
94103
value = tracked_vars.resolve(keyword.value)
95104
if not isinstance(value, dict):
96105
raise DependencyParsingError(keyword.value)
97106
for items in value.values():
98-
for item in items:
99-
yield parse_one_req(item, source)
107+
yield from _extract_deps_from_value(items, keyword.value)
100108
except (DependencyParsingError, CannotResolve) as exc:
101109
if sys.version_info >= (3, 9):
102110
unparsed_content = ast.unparse(exc.node)

tests/test_extract_declared_dependencies_success.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,21 @@ def setup(**kwargs):
302302
["pandas", "click"],
303303
id="legacy_encoding__succeeds",
304304
),
305+
pytest.param(
306+
"""\
307+
from setuptools import setup
308+
309+
setup(
310+
extras_require={
311+
'crt1': 'botocore1>=1.33.2,<2.0a.0',
312+
'crt2': 'botocore2[crt]',
313+
'crt3': ['botocore3'],
314+
},
315+
)
316+
""",
317+
["botocore1", "botocore2", "botocore3"],
318+
id="extras_with_varying_types",
319+
),
305320
],
306321
)
307322
def test_parse_setup_py(write_tmp_files, file_content, expect_deps):

0 commit comments

Comments
 (0)