diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55c85c5..ade1abe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ jobs: fail-fast: false matrix: python-version: ["3.10", "3.14"] - runs-on: [ubuntu-latest] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/src/twyn/dependency_managers/__init__.py b/src/twyn/dependency_managers/__init__.py index ec965a3..8b13789 100644 --- a/src/twyn/dependency_managers/__init__.py +++ b/src/twyn/dependency_managers/__init__.py @@ -1,15 +1 @@ -from twyn.dependency_managers.managers.npm_dependency_manager import NpmDependencyManager -from twyn.dependency_managers.managers.pypi_dependency_manager import PypiDependencyManager -from twyn.dependency_managers.utils import ( - PACKAGE_ECOSYSTEMS, - get_dependency_manager_from_file, - get_dependency_manager_from_name, -) -__all__ = [ - "NpmDependencyManager", - "PypiDependencyManager", - "get_dependency_manager_from_file", - "get_dependency_manager_from_name", - "PACKAGE_ECOSYSTEMS", -] diff --git a/src/twyn/dependency_managers/managers.py b/src/twyn/dependency_managers/managers.py new file mode 100644 index 0000000..592ef17 --- /dev/null +++ b/src/twyn/dependency_managers/managers.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +from pathlib import Path + +from twyn.dependency_managers.exceptions import NoMatchingDependencyManagerError +from twyn.dependency_parser.parsers.constants import ( + PACKAGE_LOCK_JSON, + POETRY_LOCK, + REQUIREMENTS_TXT, + UV_LOCK, + YARN_LOCK, +) +from twyn.trusted_packages.references.base import AbstractPackageReference +from twyn.trusted_packages.references.top_npm_reference import TopNpmReference +from twyn.trusted_packages.references.top_pypi_reference import TopPyPiReference + + +@dataclass +class DependencyManager: + """Base class for all `DependencyManagers`. + + It acts as a repository, linking programming languages with trusted packages sources and dependency file names. + """ + + name: str + """Name identifier for the package ecosystem.""" + + trusted_packages_source: type[AbstractPackageReference] + """Reference class for trusted packages source.""" + + dependency_files: set[str] + """Set of supported dependency file names.""" + + def matches_dependency_file(self, dependency_file: str) -> bool: + """Check if this manager can handle the given dependency file.""" + return Path(dependency_file).name in self.dependency_files + + def get_alternative_source(self, sources: dict[str, str]) -> str | None: + """Get alternative source URL for this ecosystem from sources dict.""" + return sources.get(self.name) + + +npm_dependency_manager = DependencyManager( + name="npm", + trusted_packages_source=TopNpmReference, + dependency_files={PACKAGE_LOCK_JSON, YARN_LOCK}, +) +pypi_dependency_manager = DependencyManager( + name="pypi", + trusted_packages_source=TopPyPiReference, + dependency_files={UV_LOCK, POETRY_LOCK, REQUIREMENTS_TXT}, +) + +DEPENDENCY_MANAGERS: list[DependencyManager] = [pypi_dependency_manager, npm_dependency_manager] +"""List of available dependency manager classes.""" + +PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS} +"""Set of package ecosystem names from available dependency managers.""" + + +def get_dependency_manager_from_file(dependency_file: str) -> DependencyManager: + """Get dependency manager that can handle the given file.""" + for manager in DEPENDENCY_MANAGERS: + if manager.matches_dependency_file(dependency_file): + return manager + raise NoMatchingDependencyManagerError + + +def get_dependency_manager_from_name(name: str) -> DependencyManager: + """Get dependency manager by ecosystem name.""" + for manager in DEPENDENCY_MANAGERS: + if manager.name == name: + return manager + raise NoMatchingDependencyManagerError diff --git a/src/twyn/dependency_managers/managers/__init__.py b/src/twyn/dependency_managers/managers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/twyn/dependency_managers/managers/base.py b/src/twyn/dependency_managers/managers/base.py deleted file mode 100644 index 0ccfcfe..0000000 --- a/src/twyn/dependency_managers/managers/base.py +++ /dev/null @@ -1,36 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - -from twyn.trusted_packages.references.base import AbstractPackageReference - - -@dataclass -class BaseDependencyManager: - """Base class for all `DependencyManagers`. - - It acts as a repository, linking programming languages with trusted packages sources and dependency file names. - """ - - name: str - """Name identifier for the package ecosystem.""" - trusted_packages_source: type[AbstractPackageReference] - """Reference class for trusted packages source.""" - dependency_files: set[str] - """Set of supported dependency file names.""" - - @classmethod - def matches_dependency_file(cls, dependency_file: str) -> bool: - """Check if this manager can handle the given dependency file.""" - return Path(dependency_file).name in cls.dependency_files - - @classmethod - def matches_ecosystem_name(cls, name: str) -> bool: - """Check if this manager matches the given ecosystem name.""" - return cls.name == Path(name).name.lower() - - @classmethod - def get_alternative_source(cls, sources: dict[str, str]) -> str | None: - """Get alternative source URL for this ecosystem from sources dict.""" - match = [x for x in sources if x == cls.name] - - return sources[match[0]] if match else None diff --git a/src/twyn/dependency_managers/managers/npm_dependency_manager.py b/src/twyn/dependency_managers/managers/npm_dependency_manager.py deleted file mode 100644 index 62a007b..0000000 --- a/src/twyn/dependency_managers/managers/npm_dependency_manager.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass - -from twyn.dependency_managers.managers.base import BaseDependencyManager -from twyn.dependency_parser.parsers.constants import PACKAGE_LOCK_JSON, YARN_LOCK -from twyn.trusted_packages import TopNpmReference - - -@dataclass -class NpmDependencyManager(BaseDependencyManager): - name = "npm" - """Name of the npm package ecosystem.""" - trusted_packages_source = TopNpmReference - """Reference source for trusted npm packages.""" - dependency_files = {PACKAGE_LOCK_JSON, YARN_LOCK} - """Set of supported npm dependency file names.""" diff --git a/src/twyn/dependency_managers/managers/pypi_dependency_manager.py b/src/twyn/dependency_managers/managers/pypi_dependency_manager.py deleted file mode 100644 index af2b947..0000000 --- a/src/twyn/dependency_managers/managers/pypi_dependency_manager.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass - -from twyn.dependency_managers.managers.base import BaseDependencyManager -from twyn.dependency_parser.parsers.constants import POETRY_LOCK, REQUIREMENTS_TXT, UV_LOCK -from twyn.trusted_packages import TopPyPiReference - - -@dataclass -class PypiDependencyManager(BaseDependencyManager): - name = "pypi" - """Name of the PyPI package ecosystem.""" - trusted_packages_source = TopPyPiReference - """Reference source for trusted PyPI packages.""" - dependency_files = {UV_LOCK, POETRY_LOCK, REQUIREMENTS_TXT} - """Set of supported Python dependency file names.""" diff --git a/src/twyn/dependency_managers/utils.py b/src/twyn/dependency_managers/utils.py deleted file mode 100644 index a30b358..0000000 --- a/src/twyn/dependency_managers/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -from twyn.dependency_managers.exceptions import NoMatchingDependencyManagerError -from twyn.dependency_managers.managers.base import BaseDependencyManager -from twyn.dependency_managers.managers.npm_dependency_manager import NpmDependencyManager -from twyn.dependency_managers.managers.pypi_dependency_manager import PypiDependencyManager - -DEPENDENCY_MANAGERS: list[type[BaseDependencyManager]] = [ - PypiDependencyManager, - NpmDependencyManager, -] -"""List of available dependency manager classes.""" - -PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS} -"""Set of package ecosystem names from available dependency managers.""" - - -def get_dependency_manager_from_file(dependency_file: str) -> type[BaseDependencyManager]: - """Get dependency manager that can handle the given file.""" - for manager in DEPENDENCY_MANAGERS: - if manager.matches_dependency_file(dependency_file): - return manager - raise NoMatchingDependencyManagerError - - -def get_dependency_manager_from_name(name: str) -> type[BaseDependencyManager]: - """Get dependency manager by ecosystem name.""" - for manager in DEPENDENCY_MANAGERS: - if manager.matches_ecosystem_name(name): - return manager - raise NoMatchingDependencyManagerError diff --git a/src/twyn/main.py b/src/twyn/main.py index 4376d1c..46bf110 100644 --- a/src/twyn/main.py +++ b/src/twyn/main.py @@ -9,8 +9,7 @@ ) from twyn.config.config_handler import ConfigHandler, TwynConfiguration from twyn.config.exceptions import InvalidSelectorMethodError -from twyn.dependency_managers.managers.base import BaseDependencyManager -from twyn.dependency_managers.utils import ( +from twyn.dependency_managers.managers import ( PACKAGE_ECOSYSTEMS, get_dependency_manager_from_file, get_dependency_manager_from_name, @@ -172,9 +171,10 @@ def _analyze_packages_from_source( typos_by_file = TyposquatCheckResults() dependency_managers = _get_dependency_managers_and_parsers_mapping(dependency_files) - for dependency_manager, parsers in dependency_managers.items(): - source = dependency_manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source}) - top_package_reference = dependency_manager.trusted_packages_source(source, maybe_cache_handler) + for ecosystem_name, parsers in dependency_managers.items(): + manager = get_dependency_manager_from_name(ecosystem_name) + source = manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source}) + top_package_reference = manager.trusted_packages_source(source, maybe_cache_handler) packages_from_source = top_package_reference.get_packages() trusted_packages = TrustedPackages( @@ -262,9 +262,9 @@ def _get_selector_method(selector_method: str) -> SelectorMethod: def _get_dependency_managers_and_parsers_mapping( dependency_files: set[str] | None, -) -> dict[type[BaseDependencyManager], list[AbstractParser]]: +) -> dict[str, list[AbstractParser]]: """Return a dictionary, grouping all files to parse by their DependencyManager.""" - dependency_managers: dict[type[BaseDependencyManager], list[AbstractParser]] = {} + dependency_managers: dict[str, list[AbstractParser]] = {} # No dependencies introduced via the CLI, so the dependecy file was either given or will be auto-detected dependency_selector = DependencySelector(dependency_files) @@ -273,9 +273,9 @@ def _get_dependency_managers_and_parsers_mapping( for parser in dependency_parsers: manager = get_dependency_manager_from_file(parser.file_path) - if manager not in dependency_managers: - dependency_managers[manager] = [] - dependency_managers[manager].append(parser) + if manager.name not in dependency_managers: + dependency_managers[manager.name] = [] + dependency_managers[manager.name].append(parser) return dependency_managers diff --git a/tests/dependency_managers/test_dependency_managers.py b/tests/dependency_managers/test_dependency_managers.py index 500fa1b..3cc947b 100644 --- a/tests/dependency_managers/test_dependency_managers.py +++ b/tests/dependency_managers/test_dependency_managers.py @@ -1,24 +1,18 @@ from unittest.mock import Mock -from twyn.dependency_managers.managers.base import BaseDependencyManager +from twyn.dependency_managers.managers import DependencyManager - -class DummyManager(BaseDependencyManager): - name = "pypi" - trusted_packages_source = Mock() - dependency_files = {"requirements.txt", "poetry.lock"} +manager = DependencyManager( + name="pypi", trusted_packages_source=Mock(), dependency_files={"requirements.txt", "poetry.lock"} +) class TestDependencyManager: def test_matches_dependency_file(self) -> None: - assert DummyManager.matches_dependency_file("requirements.txt") - assert DummyManager.matches_dependency_file("/some/path/poetry.lock") - assert not DummyManager.matches_dependency_file("setup.py") - - def test_matches_language_name(self) -> None: - assert DummyManager.matches_ecosystem_name("pypi") - assert not DummyManager.matches_ecosystem_name("npm") + assert manager.matches_dependency_file("requirements.txt") + assert manager.matches_dependency_file("/some/path/poetry.lock") + assert not manager.matches_dependency_file("setup.py") def test_get_alternative_source_none(self) -> None: sources = {"npm": "npmjs.com"} - assert DummyManager.get_alternative_source(sources) is None + assert manager.get_alternative_source(sources) is None diff --git a/tests/main/test_main.py b/tests/main/test_main.py index f8a59bc..eca9259 100644 --- a/tests/main/test_main.py +++ b/tests/main/test_main.py @@ -8,7 +8,7 @@ from twyn.config.config_handler import ConfigHandler, TwynConfiguration from twyn.config.exceptions import InvalidSelectorMethodError from twyn.dependency_managers.exceptions import NoMatchingDependencyManagerError -from twyn.dependency_managers.utils import ( +from twyn.dependency_managers.managers import ( get_dependency_manager_from_file, get_dependency_manager_from_name, )