Skip to content

Instantly share code, notes, and snippets.

@layus
Created February 18, 2025 05:00
Show Gist options
  • Save layus/bbb1a4c699f3d48ff18f08ec76486558 to your computer and use it in GitHub Desktop.
Save layus/bbb1a4c699f3d48ff18f08ec76486558 to your computer and use it in GitHub Desktop.
diff --git a/fawltydeps/check.py b/fawltydeps/check.py
index f48139d..8c67173 100644
--- a/fawltydeps/check.py
+++ b/fawltydeps/check.py
@@ -2,9 +2,13 @@
import logging
from itertools import groupby
-from typing import Dict, List
+from typing import Dict, List, Iterable
-from fawltydeps.packages import Package
+from fawltydeps.packages import (
+ BasePackageResolver,
+ Package,
+ resolve_dependencies,
+)
from fawltydeps.settings import Settings
from fawltydeps.types import (
DeclaredDependency,
@@ -15,10 +19,16 @@ from fawltydeps.types import (
logger = logging.getLogger(__name__)
+def _candidates(import_name: str, known_packages: Iterable[BasePackageResolver]) -> List[Package]:
+ for resolver in known_packages:
+ for package in resolver.all_packages():
+ if import_name in package.import_names:
+ yield package
def calculate_undeclared(
imports: List[ParsedImport],
resolved_deps: Dict[str, Package],
+ known_packages: Iterable[BasePackageResolver],
settings: Settings,
) -> List[UndeclaredDependency]:
"""Calculate which imports are not covered by declared dependencies.
@@ -35,7 +45,7 @@ def calculate_undeclared(
]
undeclared.sort(key=lambda i: i.name) # groupby requires pre-sorting
return [
- UndeclaredDependency(name, [i.source for i in imports])
+ UndeclaredDependency(name, [i.source for i in imports], list(_candidates(name, known_packages)))
for name, imports in groupby(undeclared, key=lambda i: i.name)
]
diff --git a/fawltydeps/cli_parser.py b/fawltydeps/cli_parser.py
index 5cf2f10..8441aa0 100644
--- a/fawltydeps/cli_parser.py
+++ b/fawltydeps/cli_parser.py
@@ -223,6 +223,11 @@ def populate_parser_paths_options(parser: argparse._ActionsContainer) -> None:
" defined by the user."
),
)
+ parser.add_argument(
+ "--base_path",
+ type=Path,
+ metavar="BASE_PATH",
+ )
def populate_parser_configuration(parser: argparse._ActionsContainer) -> None:
diff --git a/fawltydeps/dir_traversal.py b/fawltydeps/dir_traversal.py
index 1a8ffe8..77e3e88 100644
--- a/fawltydeps/dir_traversal.py
+++ b/fawltydeps/dir_traversal.py
@@ -129,7 +129,9 @@ class DirectoryTraversal(Generic[T]):
instance, it will _not_ be re-traversed.
"""
if not dir_path.is_dir():
- raise NotADirectoryError(dir_path)
+ #raise NotADirectoryError(dir_path)
+ return
+ dir_path = Path()
dir_id = DirId.from_path(dir_path)
self.to_traverse[dir_path] = dir_id
self.attached.setdefault(dir_id, []).extend(attach_data)
diff --git a/fawltydeps/main.py b/fawltydeps/main.py
index fb467d6..99cf21f 100644
--- a/fawltydeps/main.py
+++ b/fawltydeps/main.py
@@ -135,27 +135,32 @@ class Analysis:
)
)
+ @property
+ @calculated_once
+ def resolvers(self) -> Iterable[BasePackageResolver]:
+ pyenv_srcs = {src for src in self.sources if isinstance(src, PyEnvSource)}
+ return list(setup_resolvers(
+ custom_mapping_files=self.settings.custom_mapping_file,
+ custom_mapping=self.settings.custom_mapping,
+ pyenv_srcs=pyenv_srcs,
+ use_current_env=True,
+ install_deps=self.settings.install_deps,
+ ))
+
@property
@calculated_once
def resolved_deps(self) -> Dict[str, Package]:
"""The resolved mapping of dependency names to provided import names."""
- pyenv_srcs = {src for src in self.sources if isinstance(src, PyEnvSource)}
return resolve_dependencies(
(dep.name for dep in self.declared_deps),
- setup_resolvers(
- custom_mapping_files=self.settings.custom_mapping_file,
- custom_mapping=self.settings.custom_mapping,
- pyenv_srcs=pyenv_srcs,
- use_current_env=True,
- install_deps=self.settings.install_deps,
- ),
+ self.resolvers,
)
@property
@calculated_once
def undeclared_deps(self) -> List[UndeclaredDependency]:
"""The import statements for which no declared dependency is found."""
- return calculate_undeclared(self.imports, self.resolved_deps, self.settings)
+ return calculate_undeclared(self.imports, self.resolved_deps, self.resolvers, self.settings)
@property
@calculated_once
diff --git a/fawltydeps/packages.py b/fawltydeps/packages.py
index efb0e72..26b7605 100644
--- a/fawltydeps/packages.py
+++ b/fawltydeps/packages.py
@@ -116,6 +116,9 @@ class BasePackageResolver(ABC):
"""
raise NotImplementedError
+ def all_packages(self) -> Iterable[Package]:
+ return []
+
def accumulate_mappings(
resolved_with: Type[BasePackageResolver],
@@ -205,6 +208,8 @@ class UserDefinedMapping(BasePackageResolver):
if Package.normalize_name(name) in self.packages
}
+ def all_packages(self) -> Iterable[Package]:
+ return self.packages.values()
class InstalledPackageResolver(BasePackageResolver):
"""Lookup imports exposed by packages installed in a Python environment."""
@@ -282,6 +287,9 @@ class InstalledPackageResolver(BasePackageResolver):
if Package.normalize_name(name) in self.packages
}
+ def all_packages(self) -> Iterable[Package]:
+ return self.packages.values()
+
class SysPathPackageResolver(InstalledPackageResolver):
"""Lookup imports exposed by packages installed in sys.path."""
@@ -639,7 +647,9 @@ def validate_pyenv_source(path: Path) -> Optional[Set[PyEnvSource]]:
- Raise UnparseablePathError if the given path is not a directory.
"""
if not path.is_dir():
- raise UnparseablePathError(ctx="Not a directory!", path=path)
+ #raise UnparseablePathError(ctx="Not a directory!", path=path)
+ # maybe just a warning ?
+ return None
try:
return pyenv_sources(path)
except ValueError:
diff --git a/fawltydeps/settings.py b/fawltydeps/settings.py
index dbf1467..8327ac8 100644
--- a/fawltydeps/settings.py
+++ b/fawltydeps/settings.py
@@ -156,6 +156,7 @@ class Settings(BaseSettings):
"""
actions: Set[Action] = {Action.REPORT_UNDECLARED, Action.REPORT_UNUSED}
+ base_path: Optional[Path] = None
output_format: OutputFormat = OutputFormat.HUMAN_SUMMARY
code: Set[PathOrSpecial] = {Path()}
deps: Set[Path] = {Path()}
diff --git a/fawltydeps/traverse_project.py b/fawltydeps/traverse_project.py
index e48406d..90fb558 100644
--- a/fawltydeps/traverse_project.py
+++ b/fawltydeps/traverse_project.py
@@ -102,7 +102,7 @@ def find_sources( # noqa: C901, PLR0912, PLR0915
for path_or_special in settings.code if CodeSource in source_types else []:
# exceptions raised by validate_code_source() are propagated here
- validated: Optional[Source] = validate_code_source(path_or_special)
+ validated: Optional[Source] = validate_code_source(path_or_special, settings.base_path)
if validated is not None: # parse-able file given directly
logger.debug(f"find_sources() Found {validated}")
yield validated
diff --git a/fawltydeps/types.py b/fawltydeps/types.py
index bd3d909..f429826 100644
--- a/fawltydeps/types.py
+++ b/fawltydeps/types.py
@@ -286,6 +286,7 @@ class UndeclaredDependency:
name: str
references: List[Location]
+ candidates: List[Package]
def render(self, *, include_references: bool) -> str:
"""Return a human-readable string representation.
diff --git a/fawltydeps/utils.py b/fawltydeps/utils.py
index 4639247..82ec15f 100644
--- a/fawltydeps/utils.py
+++ b/fawltydeps/utils.py
@@ -27,9 +27,10 @@ def version() -> str:
def dirs_between(parent: Path, child: Path) -> Iterator[Path]:
"""Yield directories between 'parent' and 'child', inclusive."""
- yield child
- if child != parent:
- yield from dirs_between(parent, child.parent)
+ return [ p for p in child.parents if str(p).startswith(str(parent))] or [parent]
+ #yield child
+ #if child != parent:
+ # yield from dirs_between(parent, child.parent)
def hide_dataclass_fields(instance: object, *field_names: str) -> None:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment