diff --git a/src/twyn/base/constants.py b/src/twyn/base/constants.py index 16cd161..607e325 100644 --- a/src/twyn/base/constants.py +++ b/src/twyn/base/constants.py @@ -12,15 +12,20 @@ MANUAL_INPUT_SOURCE = "manual_input" +"""Source identifier for manually provided dependencies.""" SELECTOR_METHOD_MAPPING: dict[str, type[selectors.AbstractSelector]] = { "first-letter": selectors.FirstLetterExact, "nearby-letter": selectors.FirstLetterNearbyInKeyboard, "all": selectors.AllSimilar, } +"""Mapping of selector method names to their corresponding classes.""" SELECTOR_METHOD_KEYS = set(SELECTOR_METHOD_MAPPING.keys()) +"""Set of available selector method names.""" + SelectorMethod = Literal["first-letter", "nearby-letter", "all"] +"""Type alias for valid selector method strings.""" DEPENDENCY_FILE_MAPPING: dict[str, type[AbstractParser]] = { "requirements.txt": dependency_parser.RequirementsTxtParser, @@ -29,13 +34,24 @@ "package-lock.json": dependency_parser.PackageLockJsonParser, "yarn.lock": dependency_parser.YarnLockParser, } +"""Mapping of dependency file names to their parser classes.""" DEFAULT_SELECTOR_METHOD = "all" +"""Default method for selecting similar packages.""" + DEFAULT_PROJECT_TOML_FILE = "pyproject.toml" +"""Default filename for project configuration.""" + DEFAULT_TWYN_TOML_FILE = "twyn.toml" +"""Default filename for Twyn-specific configuration.""" + DEFAULT_USE_CACHE = True +"""Default setting for cache usage.""" + DEFAULT_RECURSIVE = False +"""Default setting for recursive processing.""" PackageEcosystems: TypeAlias = Literal["pypi", "npm"] +"""Type alias for supported package ecosystems.""" diff --git a/src/twyn/base/exceptions.py b/src/twyn/base/exceptions.py index f8af9cd..eb0a4c2 100644 --- a/src/twyn/base/exceptions.py +++ b/src/twyn/base/exceptions.py @@ -29,6 +29,7 @@ def __init__(self, message: str = "") -> None: super().__init__(message) def show(self, file: Optional[IO[Any]] = None) -> None: + """Display the error message.""" logger.debug(self.format_message(), exc_info=True) logger.error(self.format_message(), exc_info=False) diff --git a/src/twyn/base/utils.py b/src/twyn/base/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/twyn/cli.py b/src/twyn/cli.py index 7af1948..2d4dd67 100644 --- a/src/twyn/cli.py +++ b/src/twyn/cli.py @@ -39,6 +39,7 @@ @click.group() @click.version_option(__version__, "--version") def entry_point() -> None: + """Provide main CLI entry point for Twyn.""" pass @@ -188,6 +189,7 @@ def run( # noqa: C901 @entry_point.group() def allowlist() -> None: + """Manage package allowlist configuration.""" pass @@ -195,6 +197,7 @@ def allowlist() -> None: @click.option("--config", type=click.STRING) @click.argument("package_name") def add(package_name: str, config: str) -> None: + """Add package to allowlist.""" fh = FileHandler(config or ConfigHandler.get_default_config_file_path()) ConfigHandler(fh).add_package_to_allowlist(package_name) @@ -203,12 +206,14 @@ def add(package_name: str, config: str) -> None: @click.option("--config", type=click.STRING) @click.argument("package_name") def remove(package_name: str, config: str) -> None: + """Remove package from allowlist.""" fh = FileHandler(config or DEFAULT_PROJECT_TOML_FILE) ConfigHandler(fh).remove_package_from_allowlist(package_name) @entry_point.group() def cache() -> None: + """Manage cache operations.""" pass diff --git a/src/twyn/config/config_handler.py b/src/twyn/config/config_handler.py index 9c03966..8b73f19 100644 --- a/src/twyn/config/config_handler.py +++ b/src/twyn/config/config_handler.py @@ -33,13 +33,21 @@ class TwynConfiguration: """Fully resolved configuration for Twyn.""" dependency_files: set[str] + """Set of dependency file paths to analyze.""" selector_method: str + """Method for selecting similar packages.""" allowlist: set[str] + """Set of package names to allow without checking.""" pypi_source: Optional[str] + """Alternative PyPI source URL.""" npm_source: Optional[str] + """Alternative npm source URL.""" use_cache: bool + """Whether to use cached trusted packages.""" package_ecosystem: Optional[PackageEcosystems] + """Target package ecosystem for analysis.""" recursive: Optional[bool] + """Whether to recursively search for dependency files.""" @dataclass @@ -47,13 +55,21 @@ class ReadTwynConfiguration: """Configuration for twyn as set by the user. It may have None values.""" dependency_files: Optional[set[str]] = field(default_factory=set) + """Optional set of dependency file paths to analyze.""" selector_method: Optional[str] = None + """Optional method for selecting similar packages.""" allowlist: set[str] = field(default_factory=set) + """Set of package names to allow without checking.""" pypi_source: Optional[str] = None + """Optional alternative PyPI source URL.""" npm_source: Optional[str] = None + """Optional alternative npm source URL.""" use_cache: Optional[bool] = None + """Optional setting for using cached trusted packages.""" package_ecosystem: Optional[PackageEcosystems] = None + """Optional target package ecosystem for analysis.""" recursive: Optional[bool] = None + """Optional setting for recursive dependency file search.""" class ConfigHandler: @@ -200,11 +216,13 @@ def _write_config(self, toml: TOMLDocument, config: ReadTwynConfiguration) -> No self._write_toml(toml) def _write_toml(self, toml: TOMLDocument) -> None: + """Write TOML document to file.""" if not self.file_handler: raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform write operation.") self.file_handler.write(dumps(toml)) def _read_toml(self) -> TOMLDocument: + """Read TOML document from file.""" if not self.file_handler: raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform read operation.") try: @@ -227,6 +245,8 @@ def get_default_config_file_path() -> str: def _serialize_config(x: Any) -> Union[Any, str, list[Any]]: + """Serialize configuration values for TOML format.""" + def _value_to_for_config(v: Any) -> Union[str, list[Any], Any]: if isinstance(v, Enum): return v.name diff --git a/src/twyn/dependency_managers/dependency_manager.py b/src/twyn/dependency_managers/dependency_manager.py deleted file mode 100644 index 525270d..0000000 --- a/src/twyn/dependency_managers/dependency_manager.py +++ /dev/null @@ -1,21 +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] -PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS} - - -def get_dependency_manager_from_file(dependency_file: str) -> type[BaseDependencyManager]: - 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]: - for manager in DEPENDENCY_MANAGERS: - if manager.matches_ecosystem_name(name): - return manager - raise NoMatchingDependencyManagerError diff --git a/src/twyn/dependency_managers/managers/base.py b/src/twyn/dependency_managers/managers/base.py index 3b9c766..89789ea 100644 --- a/src/twyn/dependency_managers/managers/base.py +++ b/src/twyn/dependency_managers/managers/base.py @@ -13,19 +13,25 @@ class BaseDependencyManager: """ 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]) -> Optional[str]: + """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 index c2dfdc7..62a007b 100644 --- a/src/twyn/dependency_managers/managers/npm_dependency_manager.py +++ b/src/twyn/dependency_managers/managers/npm_dependency_manager.py @@ -8,5 +8,8 @@ @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 index 59b5251..af2b947 100644 --- a/src/twyn/dependency_managers/managers/pypi_dependency_manager.py +++ b/src/twyn/dependency_managers/managers/pypi_dependency_manager.py @@ -8,5 +8,8 @@ @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 index db2984c..a30b358 100644 --- a/src/twyn/dependency_managers/utils.py +++ b/src/twyn/dependency_managers/utils.py @@ -3,8 +3,18 @@ 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 @@ -12,14 +22,8 @@ def get_dependency_manager_from_file(dependency_file: str) -> type[BaseDependenc 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 - - -DEPENDENCY_MANAGERS: list[type[BaseDependencyManager]] = [ - PypiDependencyManager, - NpmDependencyManager, -] -PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS} diff --git a/src/twyn/dependency_parser/dependency_selector.py b/src/twyn/dependency_parser/dependency_selector.py index 82b839f..186513b 100644 --- a/src/twyn/dependency_parser/dependency_selector.py +++ b/src/twyn/dependency_parser/dependency_selector.py @@ -12,11 +12,14 @@ class DependencySelector: + """Select and provide parsers for dependency files.""" + def __init__(self, dependency_files: Optional[set[str]] = None, root_path: str = ".") -> None: self.dependency_files = dependency_files or set() self.root_path = root_path def auto_detect_dependency_file_parser(self) -> list[AbstractParser]: + """Automatically detect and return parsers for dependency files.""" parsers: list[AbstractParser] = [] root = Path(self.root_path) for path in root.rglob("*"): @@ -37,6 +40,7 @@ def auto_detect_dependency_file_parser(self) -> list[AbstractParser]: return parsers def get_dependency_file_parsers_from_file_name(self) -> list[AbstractParser]: + """Get parsers for dependency files based on their names.""" parsers = [] for dependency_file in self.dependency_files: for known_dependency_file_name in DEPENDENCY_FILE_MAPPING: @@ -49,6 +53,7 @@ def get_dependency_file_parsers_from_file_name(self) -> list[AbstractParser]: return parsers def get_dependency_parsers(self) -> list[AbstractParser]: + """Get appropriate dependency parsers based on configuration.""" if self.dependency_files: logger.debug("Dependency file provided. Assigning a parser.") return self.get_dependency_file_parsers_from_file_name() diff --git a/src/twyn/dependency_parser/parsers/abstract_parser.py b/src/twyn/dependency_parser/parsers/abstract_parser.py index 742f9b2..ec735b4 100644 --- a/src/twyn/dependency_parser/parsers/abstract_parser.py +++ b/src/twyn/dependency_parser/parsers/abstract_parser.py @@ -20,9 +20,11 @@ def __init__(self, file_path: str) -> None: self.file_handler = FileHandler(file_path=self.file_path) def __str__(self) -> str: + """Return string representation of parser class name.""" return self.__class__.__name__ def file_exists(self) -> bool: + """Check if dependency file exists.""" return self.file_handler.exists() @abstractmethod diff --git a/src/twyn/dependency_parser/parsers/constants.py b/src/twyn/dependency_parser/parsers/constants.py index 070c12c..d384917 100644 --- a/src/twyn/dependency_parser/parsers/constants.py +++ b/src/twyn/dependency_parser/parsers/constants.py @@ -1,5 +1,14 @@ UV_LOCK = "uv.lock" +"""Filename for uv package manager lock files.""" + PACKAGE_LOCK_JSON = "package-lock.json" +"""Filename for npm package lock files.""" + POETRY_LOCK = "poetry.lock" +"""Filename for Poetry dependency lock files.""" + REQUIREMENTS_TXT = "requirements.txt" +"""Filename for pip requirements files.""" + YARN_LOCK = "yarn.lock" +"""Filename for Yarn package lock files.""" diff --git a/src/twyn/dependency_parser/parsers/package_lock_json.py b/src/twyn/dependency_parser/parsers/package_lock_json.py index 0e6c6f3..c862657 100644 --- a/src/twyn/dependency_parser/parsers/package_lock_json.py +++ b/src/twyn/dependency_parser/parsers/package_lock_json.py @@ -35,6 +35,7 @@ def parse(self) -> set[str]: return result def _collect_deps(self, dep_tree: dict[str, Any], collected: set[str]): + """Recursively collect dependencies from dependency tree.""" for name, info in dep_tree.items(): collected.add(name) if "dependencies" in info: diff --git a/src/twyn/dependency_parser/parsers/requirements_txt_parser.py b/src/twyn/dependency_parser/parsers/requirements_txt_parser.py index dfad5d2..672e3ba 100644 --- a/src/twyn/dependency_parser/parsers/requirements_txt_parser.py +++ b/src/twyn/dependency_parser/parsers/requirements_txt_parser.py @@ -24,6 +24,7 @@ class RequirementsTxtParser(AbstractParser): """, re.VERBOSE, ) + """Regular expression pattern for parsing requirement specifications.""" def __init__(self, file_path: str = REQUIREMENTS_TXT) -> None: super().__init__(file_path) @@ -37,6 +38,7 @@ def parse(self) -> set[str]: return self._parse_internal(self.file_path, seen_files=set()) def _parse_internal(self, source: Union[str, Path], seen_files: set[Path]) -> set[str]: + """Parse requirements file and handle includes recursively.""" packages: set[str] = set() base_dir = Path(source).parent if isinstance(source, Path) else Path(".") @@ -74,6 +76,7 @@ def _parse_internal(self, source: Union[str, Path], seen_files: set[Path]) -> se @staticmethod def _is_valid_line(line: str) -> bool: + """Check if line is valid for parsing.""" return ( bool(line) and not line.startswith("#") diff --git a/src/twyn/dependency_parser/parsers/yarn_lock_parser.py b/src/twyn/dependency_parser/parsers/yarn_lock_parser.py index c390b6e..0b95400 100644 --- a/src/twyn/dependency_parser/parsers/yarn_lock_parser.py +++ b/src/twyn/dependency_parser/parsers/yarn_lock_parser.py @@ -13,6 +13,7 @@ def __init__(self, file_path: str = YARN_LOCK) -> None: super().__init__(file_path) def parse(self) -> set[str]: + """Parse yarn lock file and return package names.""" with self.file_handler.open() as fp: # We want to find out if it's a v1 or v2 file. # we will check maximum on the first 20 lines in order to guess @@ -29,7 +30,7 @@ def parse(self) -> set[str]: raise InvalidFileFormatError def _parse_v1(self, fp: TextIO) -> set[str]: - """Parse a yarn.lock file and return all the dependencies in it.""" + """Parse a yarn.lock file (v1) and return all the dependencies in it.""" # Match the entire line up to the colon (allows multiple quoted keys) key_line_re = re.compile(r"^(?P[^ \t].*?):\s*$") names = set() diff --git a/src/twyn/file_handler/file_handler.py b/src/twyn/file_handler/file_handler.py index fe92193..95c2908 100644 --- a/src/twyn/file_handler/file_handler.py +++ b/src/twyn/file_handler/file_handler.py @@ -11,13 +11,17 @@ class FileHandler: + """Handle file operations for reading and writing.""" + def __init__(self, file_path: str) -> None: self.file_path = self._get_file_path(file_path) def is_handler_of_file(self, name: str) -> bool: + """Check if this handler manages the specified file.""" return self._get_file_path(name) == self.file_path def read(self) -> str: + """Read file content as string.""" self._raise_for_file_exists() content = self.file_path.read_text() @@ -27,6 +31,7 @@ def read(self) -> str: @contextmanager def open(self, mode="r") -> Iterator[TextIO]: + """Open file with context manager.""" self._raise_for_file_exists() with self.file_path.open(mode) as fp: @@ -34,6 +39,7 @@ def open(self, mode="r") -> Iterator[TextIO]: logger.debug("Successfully read content from local dependencies file") def exists(self) -> bool: + """Check if file exists and is a valid file.""" try: self._raise_for_file_exists() except (PathNotFoundError, PathIsNotFileError): @@ -41,6 +47,7 @@ def exists(self) -> bool: return True def _raise_for_file_exists(self) -> None: + """Raise appropriate exception if file doesn't exist or isn't a file.""" if not self.file_path.exists(): raise PathNotFoundError @@ -48,9 +55,11 @@ def _raise_for_file_exists(self) -> None: raise PathIsNotFileError def write(self, data: str) -> None: + """Write data to file.""" self.file_path.write_text(data) def delete(self, delete_parent_dir: bool = False) -> None: + """Delete file and optionally its parent directory.""" if not self.exists(): logger.info("File does not exist, nothing to delete") return @@ -68,4 +77,5 @@ def delete(self, delete_parent_dir: bool = False) -> None: ) def _get_file_path(self, file_path: str) -> Path: + """Convert string path to absolute Path object.""" return Path(os.path.abspath(os.path.join(os.getcwd(), file_path))) diff --git a/src/twyn/similarity/algorithm.py b/src/twyn/similarity/algorithm.py index 659f9dd..c25ba37 100644 --- a/src/twyn/similarity/algorithm.py +++ b/src/twyn/similarity/algorithm.py @@ -11,10 +11,16 @@ class SimilarityThreshold: + """Define threshold values for similarity comparison.""" + LENGTH_CUTOFF = 5 + """Length threshold for determining word categorization.""" MIN_VALUE = 1.0 + """Minimum similarity threshold value.""" MAX_FOR_SHORT_WORDS = 1.0 + """Maximum similarity threshold for short words.""" MAX_FOR_LONG_WORDS = 2.0 + """Maximum similarity threshold for long words.""" def __init__(self, max: float) -> None: self.min = self.MIN_VALUE @@ -25,6 +31,7 @@ def __init__(self, max: float) -> None: @classmethod def from_name(cls, name: str) -> SimilarityThreshold: + """Create threshold based on name length.""" name_length = len(name) if name_length <= cls.LENGTH_CUTOFF: logger.debug("max length of %s selected for %s", cls.MAX_FOR_SHORT_WORDS, name) @@ -34,6 +41,7 @@ def from_name(cls, name: str) -> SimilarityThreshold: return cls(max=cls.MAX_FOR_LONG_WORDS) # we allow more typos if the name is longer def is_inside_threshold(self, value: float) -> bool: + """Check if value is within threshold bounds.""" return self.min <= value <= self.max @@ -60,4 +68,5 @@ class EditDistance(AbstractSimilarityAlgorithm): """Levenshtein algorithm that computes the edit distance between words.""" def _run_algorithm(self, first_sequence: str, second_sequence: str) -> int: + """Compute Damerau-Levenshtein distance between sequences.""" return DamerauLevenshtein.distance(s1=first_sequence, s2=second_sequence) diff --git a/src/twyn/similarity/exceptions.py b/src/twyn/similarity/exceptions.py index dc3aae8..839e032 100644 --- a/src/twyn/similarity/exceptions.py +++ b/src/twyn/similarity/exceptions.py @@ -5,9 +5,11 @@ class DistanceAlgorithmError(TwynError): """Exception raised while running distance algorithm.""" message = "Exception raised while running distance algorithm" + """Default error message for distance algorithm failures.""" class ThresholdError(TwynError, ValueError): """Exception raised when minimum threshold is greater than maximum threshold.""" message = "Minimum threshold cannot be greater than maximum threshold." + """Default error message for invalid threshold values.""" diff --git a/src/twyn/trusted_packages/cache_handler.py b/src/twyn/trusted_packages/cache_handler.py index 881e646..0d9e51b 100644 --- a/src/twyn/trusted_packages/cache_handler.py +++ b/src/twyn/trusted_packages/cache_handler.py @@ -16,11 +16,14 @@ class CacheEntry(BaseModel): saved_date: str + """ISO format date string when the cache entry was saved.""" packages: set[str] + """Set of trusted package names.""" @field_validator("saved_date") @classmethod def validate_saved_date(cls, v: str) -> str: + """Validate the date format for cache entries.""" try: datetime.fromisoformat(v) except (ValueError, TypeError) as e: diff --git a/src/twyn/trusted_packages/constants.py b/src/twyn/trusted_packages/constants.py index ac2ec2d..87212ff 100644 --- a/src/twyn/trusted_packages/constants.py +++ b/src/twyn/trusted_packages/constants.py @@ -1,6 +1,9 @@ # Cache configuration constants CACHE_DIR = ".twyn" +"""Directory name for storing cache files.""" + TRUSTED_PACKAGES_MAX_RETENTION_DAYS = 30 +"""Maximum number of days to retain trusted packages in cache.""" ADJACENCY_MATRIX = { @@ -42,3 +45,4 @@ "m": ["j", "k", "n"], "@": ["1", "2", "3", "q", "w"], } +"""Keyboard adjacency matrix mapping each key to its neighboring keys.""" diff --git a/src/twyn/trusted_packages/models.py b/src/twyn/trusted_packages/models.py index 5c94d2e..2719041 100644 --- a/src/twyn/trusted_packages/models.py +++ b/src/twyn/trusted_packages/models.py @@ -7,9 +7,12 @@ class TyposquatCheckResultEntry(BaseModel): """Represents the result of analyzing a dependency for a possible typosquat.""" dependency: str + """Name of the dependency being checked.""" similars: list[str] = [] + """List of similar package names that might be typosquats.""" def __bool__(self) -> bool: + """Check if this result entry contains any similar packages.""" return bool(self.similars) def add(self, similar_name: str) -> None: @@ -19,12 +22,16 @@ def add(self, similar_name: str) -> None: class TyposquatCheckResultFromSource(BaseModel): errors: list[TyposquatCheckResultEntry] = [] + """List of typosquat check result entries.""" source: str + """Source identifier for the dependency file or input.""" def __bool__(self) -> bool: + """Check if this result contains any errors.""" return bool(self.errors) def __contains__(self, value: TyposquatCheckResultEntry) -> bool: + """Check if the given entry is in the errors list.""" if not isinstance(value, TyposquatCheckResultEntry): return False return value in self.errors @@ -36,8 +43,10 @@ def get_typosquats(self) -> set[str]: class TyposquatCheckResults(BaseModel): results: list[TyposquatCheckResultFromSource] = [] + """List of typosquat check results from different sources.""" def __bool__(self) -> bool: + """Check if this result collection contains any results.""" return bool(self.results) def get_results_from_source(self, source: str) -> Optional[TyposquatCheckResultFromSource]: diff --git a/src/twyn/trusted_packages/references/base.py b/src/twyn/trusted_packages/references/base.py index 90fd14f..6dca7ba 100644 --- a/src/twyn/trusted_packages/references/base.py +++ b/src/twyn/trusted_packages/references/base.py @@ -24,6 +24,7 @@ class AbstractPackageReference: """ DEFAULT_SOURCE: str + """Default URL source for fetching trusted packages.""" def __init__(self, source: Optional[str] = None, cache_handler: Union[CacheHandler, None] = None) -> None: self.source = source or self.DEFAULT_SOURCE @@ -35,6 +36,7 @@ def normalize_packages(packages: set[str]) -> set[str]: """Normalize package names to make sure they're valid within the package manager context.""" def _download(self) -> dict[str, Any]: + """Download data from the source URL.""" response = requests.get(self.source) response.raise_for_status() diff --git a/src/twyn/trusted_packages/references/top_npm_reference.py b/src/twyn/trusted_packages/references/top_npm_reference.py index 30105ba..e6ca71c 100644 --- a/src/twyn/trusted_packages/references/top_npm_reference.py +++ b/src/twyn/trusted_packages/references/top_npm_reference.py @@ -17,6 +17,7 @@ class TopNpmReference(AbstractPackageReference): DEFAULT_SOURCE: str = ( "https://raw.githubusercontent.com/elementsinteractive/twyn/refs/heads/main/dependencies/npm.json" ) + """Default URL for fetching top npm packages.""" @override @staticmethod diff --git a/src/twyn/trusted_packages/references/top_pypi_reference.py b/src/twyn/trusted_packages/references/top_pypi_reference.py index 6f1b6ca..671c1bc 100644 --- a/src/twyn/trusted_packages/references/top_pypi_reference.py +++ b/src/twyn/trusted_packages/references/top_pypi_reference.py @@ -17,6 +17,7 @@ class TopPyPiReference(AbstractPackageReference): DEFAULT_SOURCE: str = ( "https://raw.githubusercontent.com/elementsinteractive/twyn/refs/heads/main/dependencies/pypi.json" ) + """Default URL for fetching top PyPI packages.""" @override @staticmethod diff --git a/src/twyn/trusted_packages/selectors.py b/src/twyn/trusted_packages/selectors.py index 42441fe..7d97a6b 100644 --- a/src/twyn/trusted_packages/selectors.py +++ b/src/twyn/trusted_packages/selectors.py @@ -20,7 +20,8 @@ class AbstractSelector(ABC): def select_similar_names(self, names: _PackageNames, name: str) -> Iterable[str]: """Override this to select names that are similar to the provided one.""" - def __str__(self): + def __str__(self) -> str: + """Return the class name as string representation.""" return self.__class__.__name__ @@ -28,12 +29,14 @@ class FirstLetterNearbyInKeyboard(AbstractSelector): """Selects names that start with a letter that is nearby in an English Keyboard.""" def select_similar_names(self, names: _PackageNames, name: str) -> Iterable[str]: + """Select package names with first letters nearby on keyboard.""" candidate_characters = self._get_candidate_characters(name[0]) for letter in candidate_characters: yield from names.get(letter, []) @staticmethod def _get_candidate_characters(character: str) -> list[str]: + """Get keyboard adjacent characters for the given character.""" if character not in ADJACENCY_MATRIX: raise CharacterNotInMatrixError(f"Character '{character}' not supported") @@ -44,6 +47,7 @@ class FirstLetterExact(AbstractSelector): """Selects names that share the same first letter.""" def select_similar_names(self, names: _PackageNames, name: str) -> Iterable[str]: + """Select package names that start with the same letter.""" yield from names[name[0]] @@ -51,5 +55,6 @@ class AllSimilar(AbstractSelector): """Consider all names to be similar.""" def select_similar_names(self, names: _PackageNames, name: str) -> Iterable[str]: + """Return all available package names as candidates.""" for candidates in names.values(): yield from candidates diff --git a/src/twyn/trusted_packages/trusted_packages.py b/src/twyn/trusted_packages/trusted_packages.py index 5c5e556..f8e6c5d 100644 --- a/src/twyn/trusted_packages/trusted_packages.py +++ b/src/twyn/trusted_packages/trusted_packages.py @@ -9,6 +9,7 @@ from twyn.trusted_packages.selectors import AbstractSelector _PackageNames = defaultdict[str, set[str]] +"""Type alias for mapping package names by ecosystem.""" class TrustedPackages: @@ -27,6 +28,7 @@ def __init__( self.algorithm = algorithm def __contains__(self, obj: Any) -> bool: + """Check if an object exists in the trusted packages.""" if isinstance(obj, str): return obj in self.names[obj[0]] return False