Skip to content

Commit e5835ec

Browse files
committed
extract_declared_dependencies: Move parser_choice into DepsSource
The parser choice was passed into both validate_deps_source() (to help select which files become DepsSource objects), and into parse_source() (to select the parser to be used for each DepsSource object). This is redundant when we instead can record the parser_choice as part of the DepsSource object.
1 parent 5cdb76d commit e5835ec

5 files changed

+61
-79
lines changed

fawltydeps/extract_declared_dependencies.py

+37-65
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,11 @@ class ParsingStrategy(NamedTuple):
296296
execute: Callable[[Path], Iterator[DeclaredDependency]]
297297

298298

299-
def first_applicable_parser(
300-
path: Path,
301-
) -> Optional[Tuple[ParserChoice, ParsingStrategy]]:
299+
def first_applicable_parser(path: Path) -> Optional[ParserChoice]:
302300
"""Find the first applicable parser choice for given path."""
303301
return next(
304302
(
305-
(choice, parser)
303+
choice
306304
for choice, parser in PARSER_CHOICES.items()
307305
if parser.applies_to_path(path)
308306
),
@@ -328,44 +326,31 @@ def first_applicable_parser(
328326
}
329327

330328

331-
def parse_source(
332-
src: DepsSource, parser_choice: Optional[ParserChoice] = None
333-
) -> Iterator[DeclaredDependency]:
329+
def parse_source(src: DepsSource) -> Iterator[DeclaredDependency]:
334330
"""Extract dependencies (package names) from supported file types.
335331
336-
Pass a path from which to discover and parse dependency declarations. Pass
337-
a directory to traverse that directory tree to find and automatically parse
338-
any supported files.
332+
Pass a DepsSource objects which specifies the path to the file containing
333+
the dependency declarations, as well as a parser choice to select the
334+
parsing strategy for this file.
339335
340336
Generate (i.e. yield) a DeclaredDependency object for each dependency found.
341337
There is no guaranteed ordering on the generated dependencies.
342338
"""
343-
assert src.path.is_file() # sanity check
344-
if parser_choice is not None:
345-
parser = PARSER_CHOICES[parser_choice]
346-
if not parser.applies_to_path(src.path):
347-
logger.warning(
348-
f"Manually applying parser '{parser_choice}' to dependencies: {src.path}"
349-
)
350-
else:
351-
choice_and_parser = first_applicable_parser(src.path)
352-
if choice_and_parser is None: # nothing found. SHOULD NOT HAPPEN!
353-
raise UnparseablePathException(
354-
ctx="Parsing given dependencies path isn't supported", path=src.path
355-
)
356-
parser = choice_and_parser[1]
339+
parser = PARSER_CHOICES[src.parser_choice]
340+
if not parser.applies_to_path(src.path):
341+
logger.warning(
342+
f"Manually applying parser '{src.parser_choice}' to dependencies: {src.path}"
343+
)
357344
yield from parser.execute(src.path)
358345

359346

360-
def parse_sources(
361-
sources: Iterable[DepsSource], parser_choice: Optional[ParserChoice] = None
362-
) -> Iterator[DeclaredDependency]:
347+
def parse_sources(sources: Iterable[DepsSource]) -> Iterator[DeclaredDependency]:
363348
"""Extract dependencies (package names) from supported file types.
364349
365350
Pass sources from which to parse dependency declarations.
366351
"""
367352
for source in sources:
368-
yield from parse_source(source, parser_choice=parser_choice)
353+
yield from parse_source(source)
369354

370355

371356
def validate_deps_source(
@@ -381,43 +366,30 @@ def validate_deps_source(
381366
parseable files within.
382367
- Raise UnparseablePathException if the given path cannot be parsed.
383368
384-
The given 'parser_choice' and 'filter_by_parser' change which files we
385-
consider valid sources:
386-
387-
- If parser_choice is None, we will consider any file path that matches one
388-
of the PARSER_CHOICES above a valid file.
389-
- If parser_choice is not None, and filter_by_parser is False, we assume
390-
that the given file path was explicitly passed by the user, and meant to
391-
be parsed with the given parser_choice, _even_ when the file path does not
392-
nominally match this parser choice: We return the file path as a valid
393-
source regardless of its file name/suffix.
394-
- If parser_choice is not None, and filter_by_parser is True, we assume that
395-
the given file path was encountered while walking a directory passed by
396-
the user. In this case, we will only consider the file valid if it matches
397-
the given parser choice.
369+
The given 'parser_choice' and 'filter_by_parser' determine which file paths
370+
we consider valid sources, and how they are parsed: With parser_choice=None,
371+
a file path will use the first matching parser in PARSER_CHOICES above, if
372+
any. Otherwise - when parser_choice is specified - the file must either
373+
match this parser (filter_by_parser=True), or this parser will be forced
374+
even if the file does not match (filter_by_parser=False).
398375
"""
399-
if path.is_file():
400-
choice_and_parser: Optional[Tuple[ParserChoice, ParsingStrategy]] = None
401-
if parser_choice is not None: # user wants a specific parser
402-
strategy = PARSER_CHOICES[parser_choice]
403-
if filter_by_parser: # but only if the file matches
404-
if strategy.applies_to_path(path):
405-
choice_and_parser = (parser_choice, strategy)
406-
else:
407-
raise UnparseablePathException(
408-
ctx=f"Path does not match {parser_choice} parser", path=path
409-
)
410-
else: # user wants us to use this parser no matter what
411-
choice_and_parser = (parser_choice, strategy)
412-
else: # no parser chosen, automatically determine parser for this path
413-
choice_and_parser = first_applicable_parser(path)
414-
if choice_and_parser is None: # no suitable parser found
415-
raise UnparseablePathException(
416-
ctx="Parsing given dependencies path isn't supported", path=path
417-
)
418-
return DepsSource(path)
419376
if path.is_dir():
420377
return None
421-
raise UnparseablePathException(
422-
ctx="Dependencies declaration path is neither dir nor file", path=path
423-
)
378+
if not path.is_file():
379+
raise UnparseablePathException(
380+
ctx="Dependencies declaration path is neither dir nor file", path=path
381+
)
382+
383+
if parser_choice is not None: # user wants a specific parser
384+
if filter_by_parser: # but only if the file matches
385+
if not PARSER_CHOICES[parser_choice].applies_to_path(path):
386+
raise UnparseablePathException(
387+
ctx=f"Path does not match {parser_choice} parser", path=path
388+
)
389+
else: # no parser chosen, automatically determine parser for this path
390+
parser_choice = first_applicable_parser(path)
391+
if parser_choice is None: # no suitable parser given
392+
raise UnparseablePathException(
393+
ctx="Parsing given dependencies path isn't supported", path=path
394+
)
395+
return DepsSource(path, parser_choice)

fawltydeps/main.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ def declared_deps(self) -> List[DeclaredDependency]:
9999
"""The list of declared dependencies parsed from this project."""
100100
return list(
101101
extract_declared_dependencies.parse_sources(
102-
(src for src in self.sources if isinstance(src, DepsSource)),
103-
self.settings.deps_parser_choice,
102+
(src for src in self.sources if isinstance(src, DepsSource))
104103
)
105104
)
106105

fawltydeps/types.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,17 @@ def __post_init__(self) -> None:
6363

6464
@dataclass(frozen=True, eq=True, order=True)
6565
class DepsSource:
66-
"""A source to be parsed for declared dependencies."""
66+
"""A source to be parsed for declared dependencies.
67+
68+
Also include which declared dependencies parser we have chosen to use for
69+
this file.
70+
"""
6771

6872
path: Path
73+
parser_choice: ParserChoice
74+
75+
def __post_init__(self) -> None:
76+
assert self.path.is_file() # sanity check
6977

7078

7179
Source = Union[CodeSource, DepsSource]

tests/test_deps_parser_determination.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
import pytest
77

8-
from fawltydeps.extract_declared_dependencies import parse_source, parse_sources
8+
from fawltydeps.extract_declared_dependencies import (
9+
parse_source,
10+
parse_sources,
11+
validate_deps_source,
12+
)
913
from fawltydeps.settings import ParserChoice, Settings
1014
from fawltydeps.traverse_project import find_sources
1115
from fawltydeps.types import DepsSource
@@ -44,7 +48,7 @@ def test_explicit_parse_strategy__mismatch_yields_appropriate_logging(
4448
deps_path.touch()
4549
caplog.set_level(logging.WARNING)
4650
# Execute here just for side effect (log).
47-
list(parse_source(DepsSource(deps_path), parser_choice))
51+
list(parse_source(DepsSource(deps_path, parser_choice)))
4852
if has_log:
4953
assert (
5054
f"Manually applying parser '{parser_choice}' to dependencies: {deps_path}"
@@ -75,7 +79,9 @@ def test_filepath_inference(
7579
"""Parser choice finalization function can choose based on deps filename."""
7680
deps_path = tmp_path / deps_file_name
7781
assert deps_path.is_file() # precondition
78-
obs_deps = collect_dep_names(parse_source(DepsSource(deps_path)))
82+
src = validate_deps_source(deps_path)
83+
assert src is not None
84+
obs_deps = collect_dep_names(parse_source(src))
7985
assert_unordered_equivalence(obs_deps, exp_deps)
8086

8187

@@ -103,9 +109,7 @@ def test_extract_from_directory_applies_manual_parser_choice_iff_choice_applies(
103109
):
104110
settings = Settings(code=set(), deps={tmp_path}, deps_parser_choice=parser_choice)
105111
deps_sources = list(find_sources(settings, {DepsSource}))
106-
obs_deps = collect_dep_names(
107-
parse_sources(deps_sources, parser_choice=parser_choice)
108-
)
112+
obs_deps = collect_dep_names(parse_sources(deps_sources))
109113
assert_unordered_equivalence(obs_deps, exp_deps)
110114

111115

@@ -151,9 +155,7 @@ def test_extract_from_file_applies_manual_choice_even_if_mismatched(
151155
new_path = tmp_path / fn2
152156
shutil.move(old_path, new_path)
153157
caplog.set_level(logging.WARNING)
154-
obs_deps = collect_dep_names(
155-
parse_source(DepsSource(new_path), parser_choice=parser_choice)
156-
)
158+
obs_deps = collect_dep_names(parse_source(DepsSource(new_path, parser_choice)))
157159
assert_unordered_equivalence(obs_deps, exp_deps)
158160
exp_msg = f"Manually applying parser '{parser_choice}' to dependencies: {new_path}"
159161
assert exp_msg in caplog.text

tests/test_extract_declared_dependencies_success.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
parse_setup_cfg,
99
parse_setup_py,
1010
parse_sources,
11+
validate_deps_source,
1112
)
1213
from fawltydeps.settings import Settings
1314
from fawltydeps.traverse_project import find_sources
@@ -454,7 +455,7 @@ def test_parse_sources__parse_only_requirements_from_subdir__returns_list(
454455
"tensorflow",
455456
]
456457
path = project_with_setup_and_requirements / "subdir/requirements.txt"
457-
actual = collect_dep_names(parse_sources([DepsSource(path)]))
458+
actual = collect_dep_names(parse_sources([validate_deps_source(path)]))
458459
assert_unordered_equivalence(actual, expect)
459460

460461

@@ -582,5 +583,5 @@ def test_parse_requirements_per_req_options(tmp_path, deps_file_content, exp_dep
582583
# originally motivated by #114 (A dep can span multiple lines.)
583584
deps_path = tmp_path / "requirements.txt"
584585
deps_path.write_text(dedent(deps_file_content))
585-
obs_deps = collect_dep_names(parse_sources([DepsSource(deps_path)]))
586+
obs_deps = collect_dep_names(parse_sources([validate_deps_source(deps_path)]))
586587
assert_unordered_equivalence(obs_deps, exp_deps)

0 commit comments

Comments
 (0)