diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cbbda..68fdb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,26 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.1] - 2024-01-19 + +### Fixed +- More unit tests, fix link in meta, uses Path instead of str for paths + ## [3.0.0] - 2024-01-19 +### Fixed +- only truncate long version on console output +- removed three dependencies (toml, hypothesis, importlib-metadata) + ### Added - filter an audit by tags - install_docs, install_command to show what to do when there are problems - `single` to validate one tool without a config file - `.html` output -## Fixed -- only truncate long version on console output -- removed three dependencies (toml, hypothesis, importlib-metadata) - - ## [2.0.0] - 2024-01-19 ### Fixed - Cache now adds gitingore and clears files older than 30 days on startup - Audit is now a sub command with arguments. -### Breaking +### Changed - Gnu options with dashes, no underscores - Global opts that apply to only some commands move to subparser - check_only_for_existence is now schema type "existence" snapshot_version is now schema type "snapshot" diff --git a/Makefile b/Makefile index 9875e6e..570a592 100644 --- a/Makefile +++ b/Makefile @@ -29,11 +29,11 @@ clean: clean-pyc clean-test # tests can't be expected to pass if dependencies aren't installed. # tests are often slow and linting is fast, so run tests on linted code. -test: clean .build_history/pylint .build_history/bandit poetry.lock +test: clean poetry.lock @echo "Running unit tests" $(VENV) pytest --doctest-modules cli_tool_audit $(VENV) python -m unittest discover - $(VENV) py.test tests --cov=cli_tool_audit --cov-report=html --cov-fail-under 63 + $(VENV) py.test tests -vv -n auto --cov=cli_tool_audit --cov-report=html --cov-fail-under 75 $(VENV) bash basic_test.sh @@ -123,8 +123,12 @@ check_changelog: # pipx install keepachangelog-manager $(VENV) changelogmanager validate -check_all: check_docs check_md check_spelling +check_all: check_docs check_md check_spelling check_changelog audit check_own_ver: # Can it verify itself? - $(VENV) python dog_food_check.py \ No newline at end of file + $(VENV) python dog_food_check.py + +audit: + # $(VENV) python -m cli_tool_audit audit + $(VENV) tool_audit single cli_tool_audit --version=">=2.0.0" \ No newline at end of file diff --git a/cli_tool_audit/__about__.py b/cli_tool_audit/__about__.py index 037ec92..2be8271 100644 --- a/cli_tool_audit/__about__.py +++ b/cli_tool_audit/__about__.py @@ -14,7 +14,7 @@ ] __title__ = "cli_tool_audit" -__version__ = "3.0.0" +__version__ = "3.0.1" __description__ = "Audit for existence and version number of cli tools." __author__ = "Matthew Martin" __author_email__ = "matthewdeanmartin@gmail.com" diff --git a/cli_tool_audit/__main__.py b/cli_tool_audit/__main__.py index a5c6c51..6cdd928 100644 --- a/cli_tool_audit/__main__.py +++ b/cli_tool_audit/__main__.py @@ -7,18 +7,19 @@ import sys from collections.abc import Sequence from dataclasses import fields +from pathlib import Path from typing import Any, Optional import cli_tool_audit.config_manager as config_manager import cli_tool_audit.freeze as freeze import cli_tool_audit.interactive as interactive import cli_tool_audit.logging_config as logging_config +import cli_tool_audit.models as models import cli_tool_audit.view_npm_stress_test as demo_npm import cli_tool_audit.view_pipx_stress_test as demo_pipx import cli_tool_audit.view_venv_stress_test as demo_venv import cli_tool_audit.views as views from cli_tool_audit.__about__ import __description__, __version__ -from cli_tool_audit.models import CliToolConfig logger = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def handle_read(args: argparse.Namespace) -> None: Args: args: The args from the command line. """ - manager = config_manager.ConfigManager(args.config) + manager = config_manager.ConfigManager(Path(args.config)) manager.read_config() for tool, config in manager.tools.items(): print(f"{tool}") @@ -46,7 +47,7 @@ def handle_create(args: argparse.Namespace) -> None: """ kwargs = reduce_args_tool_cli_tool_config_args(args) - manager = config_manager.ConfigManager(args.config) + manager = config_manager.ConfigManager(Path(args.config)) manager.create_tool_config(args.tool, kwargs) print(f"Tool {args.tool} created.") @@ -62,7 +63,7 @@ def reduce_args_tool_cli_tool_config_args(args: argparse.Namespace) -> dict[str, """ kwargs = {} args_dict = vars(args) - cli_tool_fields = {f.name for f in fields(config_manager.CliToolConfig)} + cli_tool_fields = {f.name for f in fields(models.CliToolConfig)} for key, value in args_dict.items(): if key in cli_tool_fields: kwargs[key] = value @@ -71,7 +72,7 @@ def reduce_args_tool_cli_tool_config_args(args: argparse.Namespace) -> dict[str, def handle_update(args: argparse.Namespace) -> None: kwargs = reduce_args_tool_cli_tool_config_args(args) - manager = config_manager.ConfigManager(args.config) + manager = config_manager.ConfigManager(Path(args.config)) manager.update_tool_config(args.tool, {k: v for k, v in kwargs.items() if k != "tool"}) print(f"Tool {args.tool} updated.") @@ -82,7 +83,7 @@ def handle_delete(args: argparse.Namespace) -> None: Args: args: The args from the command line. """ - manager = config_manager.ConfigManager(args.config) + manager = config_manager.ConfigManager(Path(args.config)) manager.delete_tool_config(args.tool) print(f"Tool {args.tool} deleted.") @@ -93,7 +94,7 @@ def handle_interactive(args: argparse.Namespace) -> None: Args: args: The args from the command line. """ - manager = config_manager.ConfigManager(args.config) + manager = config_manager.ConfigManager(Path(args.config)) interactive.interactive_config_manager(manager) @@ -119,7 +120,7 @@ def handle_audit(args: argparse.Namespace) -> None: args: The args from the command line. """ views.report_from_pyproject_toml( - file_path=args.config, + file_path=Path(args.config), exit_code_on_failure=not args.never_fail, file_format=args.format, no_cache=args.no_cache, @@ -135,7 +136,7 @@ def handle_single(args): Args: args: The args from the command line. """ - config = CliToolConfig( + config = models.CliToolConfig( name=args.tool, version=args.version, version_switch=args.version_switch, schema=args.schema, if_os=args.if_os ) views.report_from_pyproject_toml( diff --git a/cli_tool_audit/audit_cache.py b/cli_tool_audit/audit_cache.py index 724ab45..aabb4ae 100644 --- a/cli_tool_audit/audit_cache.py +++ b/cli_tool_audit/audit_cache.py @@ -10,8 +10,10 @@ from typing import Any, Optional import cli_tool_audit.audit_manager as audit_manager -from cli_tool_audit.json_utils import custom_json_serializer -from cli_tool_audit.models import CliToolConfig, SchemaType, ToolCheckResult +import cli_tool_audit.json_utils as json_utils +import cli_tool_audit.models as models + +__all__ = ["AuditFacade"] def custom_json_deserializer(data: dict[str, Any]) -> dict[str, Any]: @@ -28,8 +30,8 @@ def custom_json_deserializer(data: dict[str, Any]) -> dict[str, Any]: if "tool_config" in data and data["tool_config"]: for key, value in data["tool_config"].items(): if isinstance(value, str) and key == "schema": - data["tool_config"][key] = SchemaType(value) - data["tool_config"] = CliToolConfig(**data["tool_config"]) + data["tool_config"][key] = models.SchemaType(value) + data["tool_config"] = models.CliToolConfig(**data["tool_config"]) return data @@ -37,14 +39,14 @@ def custom_json_deserializer(data: dict[str, Any]) -> dict[str, Any]: class AuditFacade: - def __init__(self, cache_dir: Optional[str] = None) -> None: + def __init__(self, cache_dir: Optional[Path] = None) -> None: """ Initialize the facade. Args: cache_dir (Optional[str], optional): The directory to use for caching. Defaults to None. """ self.audit_manager = audit_manager.AuditManager() - self.cache_dir = Path(cache_dir) if cache_dir else Path.cwd() / ".cli_tool_audit_cache" + self.cache_dir = cache_dir if cache_dir else Path.cwd() / ".cli_tool_audit_cache" self.cache_dir.mkdir(exist_ok=True) with open(self.cache_dir / ".gitignore", "w", encoding="utf-8") as file: file.write("*\n!.gitignore\n") @@ -64,11 +66,11 @@ def clear_old_cache_files(self) -> None: if cache_file.exists(): cache_file.unlink(missing_ok=True) # Delete the file - def get_cache_filename(self, tool_config: CliToolConfig) -> Path: + def get_cache_filename(self, tool_config: models.CliToolConfig) -> Path: """ Get the cache filename for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cache filename for. + tool_config (models.CliToolConfig): The tool to get the cache filename for. Returns: Path: The cache filename. @@ -77,21 +79,21 @@ def get_cache_filename(self, tool_config: CliToolConfig) -> Path: the_hash = tool_config.cache_hash() return self.cache_dir / f"{sanitized_name}_{the_hash}.json" - def read_from_cache(self, tool_config: CliToolConfig) -> Optional[ToolCheckResult]: + def read_from_cache(self, tool_config: models.CliToolConfig) -> Optional[models.ToolCheckResult]: """ Read the cached result for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cached result for. + tool_config (models.CliToolConfig): The tool to get the cached result for. Returns: - Optional[ToolCheckResult]: The cached result or None if not found. + Optional[models.ToolCheckResult]: The cached result or None if not found. """ cache_file = self.get_cache_filename(tool_config) if cache_file.exists(): logger.debug(f"Cache hit for {tool_config.name}") try: with open(cache_file, encoding="utf-8") as file: - hit = ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) + hit = models.ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) self.cache_hit = True return hit except TypeError: @@ -102,26 +104,26 @@ def read_from_cache(self, tool_config: CliToolConfig) -> Optional[ToolCheckResul self.cache_hit = False return None - def write_to_cache(self, tool_config: CliToolConfig, result: ToolCheckResult) -> None: + def write_to_cache(self, tool_config: models.CliToolConfig, result: models.ToolCheckResult) -> None: """ Write the given result to the cache. Args: - tool_config (CliToolConfig): The tool to write the result for. - result (ToolCheckResult): The result to write. + tool_config (models.CliToolConfig): The tool to write the result for. + result (models.ToolCheckResult): The result to write. """ cache_file = self.get_cache_filename(tool_config) with open(cache_file, "w", encoding="utf-8") as file: logger.debug(f"Caching {tool_config.name}") - json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=custom_json_serializer) + json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=json_utils.custom_json_serializer) - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ cached_result = self.read_from_cache(tool_config) if cached_result: diff --git a/cli_tool_audit/audit_manager.py b/cli_tool_audit/audit_manager.py index b77e531..a791185 100644 --- a/cli_tool_audit/audit_manager.py +++ b/cli_tool_audit/audit_manager.py @@ -10,17 +10,19 @@ import sys from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional +from typing import Literal, Optional import packaging.specifiers as packaging_specifiers import packaging.version as packaging from semver import Version from whichcraft import which -from cli_tool_audit.compatibility import check_compatibility +import cli_tool_audit.compatibility as compatibility +import cli_tool_audit.models as models +import cli_tool_audit.version_parsing as version_parsing from cli_tool_audit.known_switches import KNOWN_SWITCHES -from cli_tool_audit.models import CliToolConfig, SchemaType, ToolAvailabilityResult, ToolCheckResult -from cli_tool_audit.version_parsing import two_pass_semver_parse + +ExistenceVersionStatus = Literal["Found", "Not Found"] logger = logging.getLogger(__name__) @@ -73,7 +75,7 @@ def parse_version(self, version_string: str) -> Optional[Version]: Returns: Optional[Version]: The parsed version or None if the version string is invalid. """ - return two_pass_semver_parse(version_string) + return version_parsing.two_pass_semver_parse(version_string) def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: """ @@ -87,7 +89,7 @@ def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: if not self.found_version: return VersionResult(is_compatible=False, clean_format="Invalid Format") desired_version = desired_version or "0.0.0" - compatible_result, _version_object = check_compatibility(desired_version, str(self.found_version)) + compatible_result, _version_object = compatibility.check_compatibility(desired_version, str(self.found_version)) self.result = VersionResult( is_compatible=compatible_result == "Compatible", clean_format=str(self.found_version) @@ -109,7 +111,7 @@ def format_report(self, desired_version: str) -> str: class ExistenceVersionChecker(VersionChecker): - def __init__(self, version_string: str) -> None: + def __init__(self, version_string: ExistenceVersionStatus) -> None: """ Check if a tool exists. Args: @@ -119,7 +121,7 @@ def __init__(self, version_string: str) -> None: raise ValueError(f"version_string must be 'Found' or 'Not Found', not {version_string}") self.found_version = version_string - def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: + def check_compatibility(self, desired_version: str) -> VersionResult: """ Check if the tool exists. Args: @@ -257,20 +259,20 @@ class AuditManager: Class to audit a tool, abstract base class to allow for supporting different version schemas. """ - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ tool, config = tool_config.name, tool_config if config.if_os and not sys.platform.startswith(config.if_os): # This isn't very transparent about what just happened - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, is_needed_for_os=False, desired_version=config.version or "0.0.0", @@ -285,17 +287,17 @@ def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: ) result = self.call_tool( tool, - config.schema or SchemaType.SEMVER, + config.schema or models.SchemaType.SEMVER, config.version_switch or "--version", ) # Not pretty. - if config.schema == SchemaType.EXISTENCE: + if config.schema == models.SchemaType.EXISTENCE: existence_checker = ExistenceVersionChecker("Found" if result.is_available else "Not Found") version_result = existence_checker.check_compatibility("Found") compatibility_report = existence_checker.format_report("Found") desired_version = "*" - elif config.schema == SchemaType.SNAPSHOT: + elif config.schema == models.SchemaType.SNAPSHOT: snapshot_checker = SnapshotVersionChecker(result.version or "") version_result = snapshot_checker.check_compatibility(config.version) compatibility_report = snapshot_checker.format_report(config.version or "") @@ -311,12 +313,12 @@ def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: compatibility_report = semver_checker.format_report(config.version or "0.0.0") desired_version = config.version or "*" - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, desired_version=desired_version, is_needed_for_os=True, is_available=result.is_available, - is_snapshot=bool(config.schema == SchemaType.SNAPSHOT), + is_snapshot=bool(config.schema == models.SchemaType.SNAPSHOT), found_version=result.version, parsed_version=version_result.clean_format, is_compatible=compatibility_report, @@ -328,9 +330,9 @@ def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: def call_tool( self, tool_name: str, - schema: SchemaType, + schema: models.SchemaType, version_switch: str = "--version", - ) -> ToolAvailabilityResult: + ) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. @@ -349,10 +351,10 @@ def call_tool( last_modified = self.get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -384,9 +386,9 @@ def call_tool( logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified) + return models.ToolAvailabilityResult(True, is_broken, version, last_modified) def get_command_last_modified_date(self, tool_name: str) -> Optional[datetime.datetime]: """ diff --git a/cli_tool_audit/call_and_compatible.py b/cli_tool_audit/call_and_compatible.py index 0c3f30c..c7b7dce 100644 --- a/cli_tool_audit/call_and_compatible.py +++ b/cli_tool_audit/call_and_compatible.py @@ -4,31 +4,31 @@ import sys import threading -from cli_tool_audit.audit_cache import AuditFacade -from cli_tool_audit.audit_manager import AuditManager -from cli_tool_audit.models import CliToolConfig, ToolCheckResult +import cli_tool_audit.audit_cache as audit_cache +import cli_tool_audit.audit_manager as audit_manager +import cli_tool_audit.models as models # Old interface def check_tool_wrapper( - tool_info: tuple[str, CliToolConfig, threading.Lock, bool], -) -> ToolCheckResult: + tool_info: tuple[str, models.CliToolConfig, threading.Lock, bool], +) -> models.ToolCheckResult: """ Wrapper function for check_tool_availability() that returns a ToolCheckResult object. Args: - tool_info (tuple[str, CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the + tool_info (tuple[str, models.CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the CliToolConfig object, a lock, and a boolean indicating if the cache is enabled. Returns: - ToolCheckResult: A ToolCheckResult object. + models.ToolCheckResult: A ToolCheckResult object. """ tool, config, lock, enable_cache = tool_info if config.if_os and not sys.platform.startswith(config.if_os): # This isn't very transparent about what just happened - return ToolCheckResult( + return models.ToolCheckResult( is_needed_for_os=False, tool=tool, desired_version=config.version or "0.0.0", @@ -46,9 +46,9 @@ def check_tool_wrapper( config.version_switch = config.version_switch or "--version" if enable_cache: - cached_manager = AuditFacade() + cached_manager = audit_cache.AuditFacade() with lock: return cached_manager.call_and_check(tool_config=config) - manager = AuditManager() + manager = audit_manager.AuditManager() return manager.call_and_check(tool_config=config) diff --git a/cli_tool_audit/call_tools.py b/cli_tool_audit/call_tools.py index dedc75f..a6c962a 100644 --- a/cli_tool_audit/call_tools.py +++ b/cli_tool_audit/call_tools.py @@ -19,8 +19,8 @@ # pylint: disable=no-name-in-module from whichcraft import which +import cli_tool_audit.models as models from cli_tool_audit.known_switches import KNOWN_SWITCHES -from cli_tool_audit.models import SchemaType, ToolAvailabilityResult logger = logging.getLogger(__name__) @@ -48,20 +48,20 @@ def get_command_last_modified_date(tool_name: str) -> Optional[datetime.datetime def check_tool_availability( tool_name: str, - schema: SchemaType, + schema: models.SchemaType, version_switch: str = "--version", -) -> ToolAvailabilityResult: +) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. Args: tool_name (str): The name of the tool to check. - schema (SchemaType): The schema to use for the version. + schema (models.SchemaType): The schema to use for the version. version_switch (str): The switch to get the tool version. Defaults to '--version'. Returns: - ToolAvailabilityResult: An object containing the availability and version of the tool. + models.ToolAvailabilityResult: An object containing the availability and version of the tool. """ # Check if the tool is in the system's PATH is_broken = True @@ -69,10 +69,10 @@ def check_tool_availability( last_modified = get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -104,26 +104,10 @@ def check_tool_availability( logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified) + return models.ToolAvailabilityResult(True, is_broken, version, last_modified) if __name__ == "__main__": print(get_command_last_modified_date("asdfpipx")) - # check_tool_availability("isort") - # - # def run() -> None: - # """Example""" - # # Example usage - # file_path = "../pyproject.toml" - # cli_tools = read_config(file_path) - # - # for tool, config in cli_tools.items(): - # result = check_tool_availability(tool, config.version_switch or "--version") - # print( - # f"{tool}: {'Available' if result.is_available else 'Not Available'}" - # f" - Version:\n{result.version if result.version else 'N/A'}" - # ) - # - # run() diff --git a/cli_tool_audit/compatibility.py b/cli_tool_audit/compatibility.py index 8628c13..1d54d55 100644 --- a/cli_tool_audit/compatibility.py +++ b/cli_tool_audit/compatibility.py @@ -7,8 +7,8 @@ from semver import Version -from cli_tool_audit.compatibility_complex import check_range_compatibility -from cli_tool_audit.version_parsing import two_pass_semver_parse +import cli_tool_audit.compatibility_complex as compatibility_complex +import cli_tool_audit.version_parsing as version_parsing logger = logging.getLogger(__name__) @@ -86,13 +86,13 @@ def check_compatibility(desired_version: str, found_version: Optional[str]) -> t # Handle non-semver match patterns symbols, desired_version_text = split_version_match_pattern(desired_version) - clean_desired_version = two_pass_semver_parse(desired_version_text) + clean_desired_version = version_parsing.two_pass_semver_parse(desired_version_text) if clean_desired_version: desired_version = f"{symbols}{clean_desired_version}" found_semversion = None try: - found_semversion = two_pass_semver_parse(found_version) + found_semversion = version_parsing.two_pass_semver_parse(found_version) if found_semversion is None: logger.warning(f"SemVer failed to parse {desired_version}/{found_version}") is_compatible = CANT_TELL @@ -100,7 +100,7 @@ def check_compatibility(desired_version: str, found_version: Optional[str]) -> t # not picky, short circuit the logic. is_compatible = "Compatible" elif desired_version.startswith("^") or desired_version.startswith("~") or "*" in desired_version: - is_compatible = check_range_compatibility(desired_version, found_semversion) + is_compatible = compatibility_complex.check_range_compatibility(desired_version, found_semversion) elif found_semversion.match(desired_version): is_compatible = "Compatible" else: diff --git a/cli_tool_audit/config_manager.py b/cli_tool_audit/config_manager.py index b66356f..620270a 100644 --- a/cli_tool_audit/config_manager.py +++ b/cli_tool_audit/config_manager.py @@ -1,11 +1,12 @@ import copy import os +from pathlib import Path from typing import Any, cast import toml import tomlkit -from cli_tool_audit.models import CliToolConfig, SchemaType +import cli_tool_audit.models as models class ConfigManager: @@ -13,13 +14,13 @@ class ConfigManager: Manage the config file. """ - def __init__(self, config_path: str) -> None: + def __init__(self, config_path: Path) -> None: """ Args: - config_path (str): The path to the toml file. + config_path (Path): The path to the toml file. """ self.config_path = config_path - self.tools: dict[str, CliToolConfig] = {} + self.tools: dict[str, models.CliToolConfig] = {} def read_config(self) -> bool: """ @@ -28,8 +29,8 @@ def read_config(self) -> bool: Returns: bool: True if the cli-tools section exists, False otherwise. """ - if os.path.exists(self.config_path): - with open(self.config_path, encoding="utf-8") as file: + if self.config_path.exists(): + with open(str(self.config_path), encoding="utf-8") as file: # okay this is too hard. # from tomlkit.items import Item # class SchemaTypeItem(Item): @@ -49,19 +50,19 @@ def read_config(self) -> bool: tools_config = config.get("tool", {}).get("cli-tools", {}) for tool_name, settings in tools_config.items(): if settings.get("only_check_existence"): - settings["schema"] = SchemaType.EXISTENCE + settings["schema"] = models.SchemaType.EXISTENCE del settings["only_check_existence"] elif settings.get("version_snapshot"): - settings["schema"] = SchemaType.SNAPSHOT + settings["schema"] = models.SchemaType.SNAPSHOT settings["version"] = settings.get("version_snapshot") del settings["version_snapshot"] settings["name"] = tool_name if settings.get("schema"): - settings["schema"] = SchemaType(settings["schema"].lower()) + settings["schema"] = models.SchemaType(settings["schema"].lower()) else: - settings["schema"] = SchemaType.SEMVER - self.tools[tool_name] = CliToolConfig(**settings) + settings["schema"] = models.SchemaType.SEMVER + self.tools[tool_name] = models.CliToolConfig(**settings) return bool(self.tools) def create_tool_config(self, tool_name: str, config: dict) -> None: @@ -80,7 +81,7 @@ def create_tool_config(self, tool_name: str, config: dict) -> None: if tool_name in self.tools: raise ValueError(f"Tool {tool_name} already exists") config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) self._save_config() def update_tool_config(self, tool_name: str, config: dict) -> None: @@ -112,11 +113,11 @@ def create_update_tool_config(self, tool_name: str, config: dict) -> None: self.read_config() if tool_name not in self.tools: config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) else: for key, value in config.items(): if key == "schema": - value = str(SchemaType(value)) + value = str(models.SchemaType(value)) setattr(self.tools[tool_name], key, value) self._save_config() @@ -173,6 +174,10 @@ def _save_config(self) -> None: if __name__ == "__main__": # Usage example - config_manager = ConfigManager("../pyproject.toml") - c = config_manager.read_config() - print(c) + def run() -> None: + """Example""" + config_manager = ConfigManager(Path("../pyproject.toml")) + c = config_manager.read_config() + print(c) + + run() diff --git a/cli_tool_audit/config_reader.py b/cli_tool_audit/config_reader.py index 1e5d069..680ca3d 100644 --- a/cli_tool_audit/config_reader.py +++ b/cli_tool_audit/config_reader.py @@ -1,29 +1,29 @@ """ Read list of tools from config. """ - import logging +from pathlib import Path -from cli_tool_audit.config_manager import ConfigManager -from cli_tool_audit.models import CliToolConfig +import cli_tool_audit.config_manager as config_manager +import cli_tool_audit.models as models logger = logging.getLogger(__name__) -def read_config(file_path: str) -> dict[str, CliToolConfig]: +def read_config(file_path: Path) -> dict[str, models.CliToolConfig]: """ Read the cli-tools section from a pyproject.toml file. Args: - file_path (str): The path to the pyproject.toml file. + file_path (Path): The path to the pyproject.toml file. Returns: - dict[str, CliToolConfig]: A dictionary with the cli-tools section. + dict[str, models.CliToolConfig]: A dictionary with the cli-tools section. """ # pylint: disable=broad-exception-caught try: logger.debug(f"Loading config from {file_path}") - manager = ConfigManager(file_path) + manager = config_manager.ConfigManager(file_path) found = manager.read_config() if not found: logger.warning("Config section not found, expected [tool.cli-tools] with values") diff --git a/cli_tool_audit/freeze.py b/cli_tool_audit/freeze.py index 6018ea6..f98fafd 100644 --- a/cli_tool_audit/freeze.py +++ b/cli_tool_audit/freeze.py @@ -3,13 +3,14 @@ """ import os import tempfile +from pathlib import Path import cli_tool_audit.call_tools as call_tools -from cli_tool_audit.config_manager import ConfigManager -from cli_tool_audit.models import SchemaType +import cli_tool_audit.config_manager as cm +import cli_tool_audit.models as models -def freeze_requirements(tool_names: list[str], schema: SchemaType) -> dict[str, call_tools.ToolAvailabilityResult]: +def freeze_requirements(tool_names: list[str], schema: models.SchemaType) -> dict[str, models.ToolAvailabilityResult]: """ Capture the current version of a list of tools. @@ -27,24 +28,24 @@ def freeze_requirements(tool_names: list[str], schema: SchemaType) -> dict[str, return results -def freeze_to_config(tool_names: list[str], config_path: str, schema: SchemaType) -> None: +def freeze_to_config(tool_names: list[str], config_path: Path, schema: models.SchemaType) -> None: """ Capture the current version of a list of tools and write them to a config file. Args: tool_names (list[str]): A list of tool names. - config_path (str): The path to the config file. + config_path (Path): The path to the config file. schema (SchemaType): The schema to use for the version. """ results = freeze_requirements(tool_names, schema=schema) - config_manager = ConfigManager(config_path) + config_manager = cm.ConfigManager(config_path) config_manager.read_config() for tool_name, result in results.items(): if result.is_available and result.version: config_manager.create_update_tool_config(tool_name, {"version": result.version}) -def freeze_to_screen(tool_names: list[str], schema: SchemaType) -> None: +def freeze_to_screen(tool_names: list[str], schema: models.SchemaType) -> None: """ Capture the current version of a list of tools, write them to a temp config file, and print the 'cli-tools' section of the config. @@ -58,7 +59,7 @@ def freeze_to_screen(tool_names: list[str], schema: SchemaType) -> None: # Create a temporary directory and file with tempfile.TemporaryDirectory() as temp_dir: temp_config_path = os.path.join(temp_dir, "temp.toml") - config_manager = ConfigManager(temp_config_path) + config_manager = cm.ConfigManager(Path(temp_config_path)) config_manager.read_config() for tool_name, result in results.items(): @@ -76,4 +77,4 @@ def freeze_to_screen(tool_names: list[str], schema: SchemaType) -> None: if __name__ == "__main__": - freeze_to_screen(["python", "pip", "poetry"], schema=SchemaType.SNAPSHOT) + freeze_to_screen(["python", "pip", "poetry"], schema=models.SchemaType.SNAPSHOT) diff --git a/cli_tool_audit/interactive.py b/cli_tool_audit/interactive.py index 9693265..4d7bbde 100644 --- a/cli_tool_audit/interactive.py +++ b/cli_tool_audit/interactive.py @@ -3,11 +3,11 @@ """ from typing import Union -from cli_tool_audit.config_manager import ConfigManager +import cli_tool_audit.config_manager as cm from cli_tool_audit.models import SchemaType -def interactive_config_manager(config_manager: ConfigManager) -> None: +def interactive_config_manager(config_manager: cm.ConfigManager) -> None: """ Interactively manage tool configurations. diff --git a/cli_tool_audit/policy.py b/cli_tool_audit/policy.py index 0c0b443..b8d22b7 100644 --- a/cli_tool_audit/policy.py +++ b/cli_tool_audit/policy.py @@ -1,15 +1,15 @@ """ Apply various policies to the results of the tool checks. """ -from cli_tool_audit.call_and_compatible import ToolCheckResult +import cli_tool_audit.models as models -def apply_policy(results: list[ToolCheckResult]) -> bool: +def apply_policy(results: list[models.ToolCheckResult]) -> bool: """ Pretty print the results of the validation. Args: - results (list[ToolCheckResult]): A list of ToolCheckResult objects. + results (list[models.ToolCheckResult]): A list of ToolCheckResult objects. Returns: bool: True if any of the tools failed validation, False otherwise. diff --git a/cli_tool_audit/view_npm_stress_test.py b/cli_tool_audit/view_npm_stress_test.py index e1760bb..5284454 100644 --- a/cli_tool_audit/view_npm_stress_test.py +++ b/cli_tool_audit/view_npm_stress_test.py @@ -11,8 +11,9 @@ from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig def list_global_npm_executables() -> list[str]: @@ -52,7 +53,7 @@ def report_for_npm_tools(max_count: int = -1) -> None: app_cmd = app + ".cmd" else: app_cmd = app - config = CliToolConfig(app_cmd) + config = models.CliToolConfig(app_cmd) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[app_cmd] = config @@ -70,10 +71,10 @@ def report_for_npm_tools(max_count: int = -1) -> None: # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] diff --git a/cli_tool_audit/view_pipx_stress_test.py b/cli_tool_audit/view_pipx_stress_test.py index 1be7d08..b8540d2 100644 --- a/cli_tool_audit/view_pipx_stress_test.py +++ b/cli_tool_audit/view_pipx_stress_test.py @@ -11,16 +11,20 @@ from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig + # Dummy lock for switch to ProcessPoolExecutor -# class Dummy: -# def __enter__(self): -# pass -# -# def __exit__(self, exc_type, exc_value, traceback): -# pass +class DummyLock: + """For testing""" + + def __enter__(self): + """For testing""" + + def __exit__(self, exc_type, exc_value, traceback): + """For testing""" def get_pipx_list() -> Any: @@ -76,7 +80,7 @@ def report_for_pipx_tools(max_count: int = -1) -> None: if app in ("yated.exe", "calcure.exe", "yated", "calcure", "dedlin.exe", "dedlin"): # These launch interactive process & then time out. continue - config = CliToolConfig(app) + config = models.CliToolConfig(app) config.version_switch = "--version" config.version = f">={expected_version}" cli_tools[app] = config @@ -95,10 +99,10 @@ def report_for_pipx_tools(max_count: int = -1) -> None: # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: # Submit tasks to the executor - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] diff --git a/cli_tool_audit/view_venv_stress_test.py b/cli_tool_audit/view_venv_stress_test.py index e050a5e..5379df0 100644 --- a/cli_tool_audit/view_venv_stress_test.py +++ b/cli_tool_audit/view_venv_stress_test.py @@ -13,8 +13,9 @@ # pylint: disable=no-name-in-module from whichcraft import which +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig def get_executables_in_venv(venv_path: str) -> list[str]: @@ -51,7 +52,7 @@ def report_for_venv_tools(max_count: int = -1) -> None: cli_tools = {} count = 0 for executable in get_executables_in_venv(str(venv_dir)): - config = CliToolConfig(executable) + config = models.CliToolConfig(executable) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[executable] = config @@ -69,10 +70,10 @@ def report_for_venv_tools(max_count: int = -1) -> None: # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] diff --git a/cli_tool_audit/views.py b/cli_tool_audit/views.py index 6a80dc1..2830e02 100644 --- a/cli_tool_audit/views.py +++ b/cli_tool_audit/views.py @@ -6,6 +6,7 @@ import logging import os from concurrent.futures import ThreadPoolExecutor +from pathlib import Path from threading import Lock from typing import Optional, Union @@ -14,11 +15,11 @@ from prettytable.colortable import ColorTable, Themes from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible import cli_tool_audit.config_reader as config_reader +import cli_tool_audit.json_utils as json_utils +import cli_tool_audit.models as models import cli_tool_audit.policy as policy -from cli_tool_audit.call_and_compatible import ToolCheckResult, check_tool_wrapper -from cli_tool_audit.json_utils import custom_json_serializer -from cli_tool_audit.models import CliToolConfig colorama.init(convert=True) @@ -26,22 +27,22 @@ def validate( - file_path: str = "pyproject.toml", + file_path: Path = Path("pyproject.toml"), no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False, -) -> list[ToolCheckResult]: +) -> list[models.ToolCheckResult]: """ Validate the tools in the pyproject.toml file. Args: - file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". + file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". no_cache (bool, optional): If True, don't use the cache. Defaults to False. tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None. disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False. Returns: - list[ToolCheckResult]: A list of ToolCheckResult objects. + list[models.ToolCheckResult]: A list of ToolCheckResult objects. """ if tags is None: tags = [] @@ -50,22 +51,22 @@ def validate( def process_tools( - cli_tools: dict[str, CliToolConfig], + cli_tools: dict[str, models.CliToolConfig], no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False, -) -> list[ToolCheckResult]: +) -> list[models.ToolCheckResult]: """ Process the tools from a dictionary of CliToolConfig objects. Args: - cli_tools (dict[str, CliToolConfig]): A dictionary of tool names and CliToolConfig objects. + cli_tools (dict[str, models.CliToolConfig]): A dictionary of tool names and CliToolConfig objects. no_cache (bool, optional): If True, don't use the cache. Defaults to False. tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None. disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False. Returns: - list[ToolCheckResult]: A list of ToolCheckResult objects. + list[models.ToolCheckResult]: A list of ToolCheckResult objects. """ if tags: print(tags) @@ -88,11 +89,11 @@ def process_tools( # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: with ThreadPoolExecutor(max_workers=num_cpus) as executor: - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") or disable_progress_bar + disable = should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as pbar: # Submit tasks to the executor futures = [ - executor.submit(check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] @@ -104,8 +105,8 @@ def process_tools( def report_from_pyproject_toml( - file_path: Optional[str] = "pyproject.toml", - config_as_dict: Optional[dict[str, CliToolConfig]] = None, + file_path: Optional[Path] = Path("pyproject.toml"), + config_as_dict: Optional[dict[str, models.CliToolConfig]] = None, exit_code_on_failure: bool = True, file_format: str = "table", no_cache: bool = False, @@ -116,8 +117,8 @@ def report_from_pyproject_toml( Report on the compatibility of the tools in the pyproject.toml file. Args: - file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". - config_as_dict (Optional[dict[str, CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. + file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". + config_as_dict (Optional[dict[str, models.CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. exit_code_on_failure (bool, optional): If True, exit with return value of 1 if validation fails. Defaults to True. file_format (str, optional): The format of the output. Defaults to "table". no_cache (bool, optional): If True, don't use the cache. Defaults to False. @@ -136,9 +137,9 @@ def report_from_pyproject_toml( results = process_tools(config_as_dict, no_cache, tags, disable_progress_bar=file_format != "table") elif file_path: # Handle config file searching. - if not os.path.exists(file_path): - one_up = os.path.join("..", file_path) - if os.path.exists(one_up): + if not file_path.exists(): + one_up = ".." / file_path + if one_up.exists(): file_path = one_up results = validate(file_path, no_cache=no_cache, tags=tags, disable_progress_bar=file_format != "table") else: @@ -152,9 +153,9 @@ def report_from_pyproject_toml( failed = policy.apply_policy(results) if file_format == "json": - print(json.dumps([result.__dict__ for result in results], indent=4, default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], indent=4, default=json_utils.custom_json_serializer)) elif file_format == "json-compact": - print(json.dumps([result.__dict__ for result in results], default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], default=json_utils.custom_json_serializer)) elif file_format == "xml": print("") for result in results: @@ -194,13 +195,13 @@ def report_from_pyproject_toml( def pretty_print_results( - results: list[ToolCheckResult], truncate_long_versions: bool, include_docs: bool + results: list[models.ToolCheckResult], truncate_long_versions: bool, include_docs: bool ) -> Union[PrettyTable, ColorTable]: """ Pretty print the results of the validation. Args: - results (list[ToolCheckResult]): A list of ToolCheckResult objects. + results (list[models.ToolCheckResult]): A list of ToolCheckResult objects. truncate_long_versions (bool): If True, truncate long versions. Defaults to False. include_docs (bool): If True, include install command and install docs. Defaults to False. @@ -223,6 +224,11 @@ def pretty_print_results( found_version = result.found_version[0:25].strip() if result.found_version else "" else: found_version = result.found_version or "" + + try: + last_modified = result.last_modified.strftime("%m/%d/%y") if result.last_modified else "" + except ValueError: + last_modified = str(result.last_modified) row_data = [ result.tool, found_version or "", @@ -230,7 +236,7 @@ def pretty_print_results( result.desired_version or "", # "Yes" if result.is_compatible == "Compatible" else result.is_compatible, result.status() or "", - result.last_modified.strftime("%m/%d/%y") if result.last_modified else "", + last_modified, ] if include_docs: row_data.append(result.tool_config.install_command or "") @@ -249,5 +255,10 @@ def pretty_print_results( return table +def should_show_progress_bar(cli_tools) -> Optional[bool]: + disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + return True if disable else None + + if __name__ == "__main__": report_from_pyproject_toml() diff --git a/docs/EnvironmentVariables.md b/docs/EnvironmentVariables.md index 78a36e1..41ab291 100644 --- a/docs/EnvironmentVariables.md +++ b/docs/EnvironmentVariables.md @@ -3,10 +3,13 @@ You can modify the apps behavior with environment variables. ## `NO_COLOR` + Disables progress bar, colored tables and color logging. ## `CI` + Same as NO_COLOR ## `CLI_TOOL_AUDIT_TIMEOUT` -This is how long a the application will wait for a tool to reply to a version query, defaults to 15 seconds. \ No newline at end of file + +This is how long a the application will wait for a tool to reply to a version query, defaults to 15 seconds. diff --git a/docs/TODO.md b/docs/TODO.md index d906d73..08afddf 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,13 +2,9 @@ ## Docs -- examples in epilogue +- examples in epilogue for each subcommand - Separate out the huge subcommand --help text with examples -## Split out about/metadata creation tool? - -- Generate metadata for more source documents? (e.g. setup.cfg, setup.py, PEP pyproject toml meta) - ## policies - pass if found, even if broken - DONE. @@ -17,7 +13,13 @@ ## features -- check against current app (pyproject vs --version), i.e. dogfooding, let cli_tool_audit check if verstrings are right. +- Freeze +- import from known formats, e.g. pyproject.toml, cargo.toml, etc. + +## Build + +- dog fooding: check against current app (pyproject vs --version), i.e. dogfooding, let cli_tool_audit check if + verstrings are right. FAILED. subprocess.run() can't see the cli command ## version sources @@ -31,7 +33,5 @@ ## Tests -- hypothesis testing - - switch to pathlib so that hypothesis testing isn't against a string - include some tools that don't exist - These scenarios: https://packaging.python.org/en/latest/specifications/version-specifiers/ diff --git a/docs/cli_tool_audit/audit_cache.html b/docs/cli_tool_audit/audit_cache.html index defafc3..1c23543 100644 --- a/docs/cli_tool_audit/audit_cache.html +++ b/docs/cli_tool_audit/audit_cache.html @@ -39,8 +39,10 @@

Module cli_tool_audit.audit_cache

from typing import Any, Optional import cli_tool_audit.audit_manager as audit_manager -from cli_tool_audit.json_utils import custom_json_serializer -from cli_tool_audit.models import CliToolConfig, SchemaType, ToolCheckResult +import cli_tool_audit.json_utils as json_utils +import cli_tool_audit.models as models + +__all__ = ["AuditFacade"] def custom_json_deserializer(data: dict[str, Any]) -> dict[str, Any]: @@ -57,8 +59,8 @@

Module cli_tool_audit.audit_cache

if "tool_config" in data and data["tool_config"]: for key, value in data["tool_config"].items(): if isinstance(value, str) and key == "schema": - data["tool_config"][key] = SchemaType(value) - data["tool_config"] = CliToolConfig(**data["tool_config"]) + data["tool_config"][key] = models.SchemaType(value) + data["tool_config"] = models.CliToolConfig(**data["tool_config"]) return data @@ -66,14 +68,14 @@

Module cli_tool_audit.audit_cache

class AuditFacade: - def __init__(self, cache_dir: Optional[str] = None) -> None: + def __init__(self, cache_dir: Optional[Path] = None) -> None: """ Initialize the facade. Args: cache_dir (Optional[str], optional): The directory to use for caching. Defaults to None. """ self.audit_manager = audit_manager.AuditManager() - self.cache_dir = Path(cache_dir) if cache_dir else Path.cwd() / ".cli_tool_audit_cache" + self.cache_dir = cache_dir if cache_dir else Path.cwd() / ".cli_tool_audit_cache" self.cache_dir.mkdir(exist_ok=True) with open(self.cache_dir / ".gitignore", "w", encoding="utf-8") as file: file.write("*\n!.gitignore\n") @@ -93,11 +95,11 @@

Module cli_tool_audit.audit_cache

if cache_file.exists(): cache_file.unlink(missing_ok=True) # Delete the file - def get_cache_filename(self, tool_config: CliToolConfig) -> Path: + def get_cache_filename(self, tool_config: models.CliToolConfig) -> Path: """ Get the cache filename for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cache filename for. + tool_config (models.CliToolConfig): The tool to get the cache filename for. Returns: Path: The cache filename. @@ -106,21 +108,21 @@

Module cli_tool_audit.audit_cache

the_hash = tool_config.cache_hash() return self.cache_dir / f"{sanitized_name}_{the_hash}.json" - def read_from_cache(self, tool_config: CliToolConfig) -> Optional[ToolCheckResult]: + def read_from_cache(self, tool_config: models.CliToolConfig) -> Optional[models.ToolCheckResult]: """ Read the cached result for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cached result for. + tool_config (models.CliToolConfig): The tool to get the cached result for. Returns: - Optional[ToolCheckResult]: The cached result or None if not found. + Optional[models.ToolCheckResult]: The cached result or None if not found. """ cache_file = self.get_cache_filename(tool_config) if cache_file.exists(): logger.debug(f"Cache hit for {tool_config.name}") try: with open(cache_file, encoding="utf-8") as file: - hit = ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) + hit = models.ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) self.cache_hit = True return hit except TypeError: @@ -131,26 +133,26 @@

Module cli_tool_audit.audit_cache

self.cache_hit = False return None - def write_to_cache(self, tool_config: CliToolConfig, result: ToolCheckResult) -> None: + def write_to_cache(self, tool_config: models.CliToolConfig, result: models.ToolCheckResult) -> None: """ Write the given result to the cache. Args: - tool_config (CliToolConfig): The tool to write the result for. - result (ToolCheckResult): The result to write. + tool_config (models.CliToolConfig): The tool to write the result for. + result (models.ToolCheckResult): The result to write. """ cache_file = self.get_cache_filename(tool_config) with open(cache_file, "w", encoding="utf-8") as file: logger.debug(f"Caching {tool_config.name}") - json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=custom_json_serializer) + json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=json_utils.custom_json_serializer) - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ cached_result = self.read_from_cache(tool_config) if cached_result: @@ -168,54 +170,13 @@

Module cli_tool_audit.audit_cache

-

Functions

-
-
-def custom_json_deserializer(data: dict[str, typing.Any]) ‑> dict[str, typing.Any] -
-
-

Custom JSON deserializer for objects not deserializable by default json code.

-

Args

-
-
data : dict[str,Any]
-
The object to deserialize.
-
-

Returns

-
-
dict[str,Any]
-
A JSON deserializable representation of the object.
-
-
- -Expand source code - -
def custom_json_deserializer(data: dict[str, Any]) -> dict[str, Any]:
-    """
-    Custom JSON deserializer for objects not deserializable by default json code.
-    Args:
-        data (dict[str,Any]): The object to deserialize.
-
-    Returns:
-        dict[str,Any]: A JSON deserializable representation of the object.
-    """
-    if "last_modified" in data and data["last_modified"]:
-        data["last_modified"] = datetime.datetime.fromisoformat(data["last_modified"])
-    if "tool_config" in data and data["tool_config"]:
-        for key, value in data["tool_config"].items():
-            if isinstance(value, str) and key == "schema":
-                data["tool_config"][key] = SchemaType(value)
-        data["tool_config"] = CliToolConfig(**data["tool_config"])
-    return data
-
-
-

Classes

class AuditFacade -(cache_dir: Optional[str] = None) +(cache_dir: Optional[pathlib.Path] = None)

Initialize the facade.

@@ -229,14 +190,14 @@

Args

Expand source code
class AuditFacade:
-    def __init__(self, cache_dir: Optional[str] = None) -> None:
+    def __init__(self, cache_dir: Optional[Path] = None) -> None:
         """
         Initialize the facade.
         Args:
             cache_dir (Optional[str], optional): The directory to use for caching. Defaults to None.
         """
         self.audit_manager = audit_manager.AuditManager()
-        self.cache_dir = Path(cache_dir) if cache_dir else Path.cwd() / ".cli_tool_audit_cache"
+        self.cache_dir = cache_dir if cache_dir else Path.cwd() / ".cli_tool_audit_cache"
         self.cache_dir.mkdir(exist_ok=True)
         with open(self.cache_dir / ".gitignore", "w", encoding="utf-8") as file:
             file.write("*\n!.gitignore\n")
@@ -256,11 +217,11 @@ 

Args

if cache_file.exists(): cache_file.unlink(missing_ok=True) # Delete the file - def get_cache_filename(self, tool_config: CliToolConfig) -> Path: + def get_cache_filename(self, tool_config: models.CliToolConfig) -> Path: """ Get the cache filename for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cache filename for. + tool_config (models.CliToolConfig): The tool to get the cache filename for. Returns: Path: The cache filename. @@ -269,21 +230,21 @@

Args

the_hash = tool_config.cache_hash() return self.cache_dir / f"{sanitized_name}_{the_hash}.json" - def read_from_cache(self, tool_config: CliToolConfig) -> Optional[ToolCheckResult]: + def read_from_cache(self, tool_config: models.CliToolConfig) -> Optional[models.ToolCheckResult]: """ Read the cached result for the given tool. Args: - tool_config (CliToolConfig): The tool to get the cached result for. + tool_config (models.CliToolConfig): The tool to get the cached result for. Returns: - Optional[ToolCheckResult]: The cached result or None if not found. + Optional[models.ToolCheckResult]: The cached result or None if not found. """ cache_file = self.get_cache_filename(tool_config) if cache_file.exists(): logger.debug(f"Cache hit for {tool_config.name}") try: with open(cache_file, encoding="utf-8") as file: - hit = ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) + hit = models.ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer)) self.cache_hit = True return hit except TypeError: @@ -294,26 +255,26 @@

Args

self.cache_hit = False return None - def write_to_cache(self, tool_config: CliToolConfig, result: ToolCheckResult) -> None: + def write_to_cache(self, tool_config: models.CliToolConfig, result: models.ToolCheckResult) -> None: """ Write the given result to the cache. Args: - tool_config (CliToolConfig): The tool to write the result for. - result (ToolCheckResult): The result to write. + tool_config (models.CliToolConfig): The tool to write the result for. + result (models.ToolCheckResult): The result to write. """ cache_file = self.get_cache_filename(tool_config) with open(cache_file, "w", encoding="utf-8") as file: logger.debug(f"Caching {tool_config.name}") - json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=custom_json_serializer) + json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=json_utils.custom_json_serializer) - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ cached_result = self.read_from_cache(tool_config) if cached_result: @@ -334,26 +295,26 @@

Methods

Call and check the given tool.

Args

-
tool_config : CliToolConfig
+
tool_config : models.CliToolConfig
The tool to call and check.

Returns

-
ToolCheckResult
+
models.ToolCheckResult
The result of the check.
Expand source code -
def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult:
+
def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult:
     """
     Call and check the given tool.
     Args:
-        tool_config (CliToolConfig): The tool to call and check.
+        tool_config (models.CliToolConfig): The tool to call and check.
 
     Returns:
-        ToolCheckResult: The result of the check.
+        models.ToolCheckResult: The result of the check.
     """
     cached_result = self.read_from_cache(tool_config)
     if cached_result:
@@ -395,7 +356,7 @@ 

Returns

Get the cache filename for the given tool.

Args

-
tool_config : CliToolConfig
+
tool_config : models.CliToolConfig
The tool to get the cache filename for.

Returns

@@ -407,11 +368,11 @@

Returns

Expand source code -
def get_cache_filename(self, tool_config: CliToolConfig) -> Path:
+
def get_cache_filename(self, tool_config: models.CliToolConfig) -> Path:
     """
     Get the cache filename for the given tool.
     Args:
-        tool_config (CliToolConfig): The tool to get the cache filename for.
+        tool_config (models.CliToolConfig): The tool to get the cache filename for.
 
     Returns:
         Path: The cache filename.
@@ -428,33 +389,33 @@ 

Returns

Read the cached result for the given tool.

Args

-
tool_config : CliToolConfig
+
tool_config : models.CliToolConfig
The tool to get the cached result for.

Returns

-
Optional[ToolCheckResult]
+
Optional[models.ToolCheckResult]
The cached result or None if not found.
Expand source code -
def read_from_cache(self, tool_config: CliToolConfig) -> Optional[ToolCheckResult]:
+
def read_from_cache(self, tool_config: models.CliToolConfig) -> Optional[models.ToolCheckResult]:
     """
     Read the cached result for the given tool.
     Args:
-        tool_config (CliToolConfig): The tool to get the cached result for.
+        tool_config (models.CliToolConfig): The tool to get the cached result for.
 
     Returns:
-        Optional[ToolCheckResult]: The cached result or None if not found.
+        Optional[models.ToolCheckResult]: The cached result or None if not found.
     """
     cache_file = self.get_cache_filename(tool_config)
     if cache_file.exists():
         logger.debug(f"Cache hit for {tool_config.name}")
         try:
             with open(cache_file, encoding="utf-8") as file:
-                hit = ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer))
+                hit = models.ToolCheckResult(**json.load(file, object_hook=custom_json_deserializer))
                 self.cache_hit = True
                 return hit
         except TypeError:
@@ -473,26 +434,26 @@ 

Returns

Write the given result to the cache.

Args

-
tool_config : CliToolConfig
+
tool_config : models.CliToolConfig
The tool to write the result for.
-
result : ToolCheckResult
+
result : models.ToolCheckResult
The result to write.
Expand source code -
def write_to_cache(self, tool_config: CliToolConfig, result: ToolCheckResult) -> None:
+
def write_to_cache(self, tool_config: models.CliToolConfig, result: models.ToolCheckResult) -> None:
     """
     Write the given result to the cache.
     Args:
-        tool_config (CliToolConfig): The tool to write the result for.
-        result (ToolCheckResult): The result to write.
+        tool_config (models.CliToolConfig): The tool to write the result for.
+        result (models.ToolCheckResult): The result to write.
     """
     cache_file = self.get_cache_filename(tool_config)
     with open(cache_file, "w", encoding="utf-8") as file:
         logger.debug(f"Caching {tool_config.name}")
-        json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=custom_json_serializer)
+ json.dump(result.__dict__, file, ensure_ascii=False, indent=4, default=json_utils.custom_json_serializer)
@@ -511,11 +472,6 @@

Index

  • cli_tool_audit
  • -
  • Functions

    - -
  • Classes

    • diff --git a/docs/cli_tool_audit/audit_manager.html b/docs/cli_tool_audit/audit_manager.html index 8947d7c..8e190c7 100644 --- a/docs/cli_tool_audit/audit_manager.html +++ b/docs/cli_tool_audit/audit_manager.html @@ -40,17 +40,19 @@

      Module cli_tool_audit.audit_manager

      import sys from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional +from typing import Literal, Optional import packaging.specifiers as packaging_specifiers import packaging.version as packaging from semver import Version from whichcraft import which -from cli_tool_audit.compatibility import check_compatibility +import cli_tool_audit.compatibility as compatibility +import cli_tool_audit.models as models +import cli_tool_audit.version_parsing as version_parsing from cli_tool_audit.known_switches import KNOWN_SWITCHES -from cli_tool_audit.models import CliToolConfig, SchemaType, ToolAvailabilityResult, ToolCheckResult -from cli_tool_audit.version_parsing import two_pass_semver_parse + +ExistenceVersionStatus = Literal["Found", "Not Found"] logger = logging.getLogger(__name__) @@ -103,7 +105,7 @@

      Module cli_tool_audit.audit_manager

      Returns: Optional[Version]: The parsed version or None if the version string is invalid. """ - return two_pass_semver_parse(version_string) + return version_parsing.two_pass_semver_parse(version_string) def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: """ @@ -117,7 +119,7 @@

      Module cli_tool_audit.audit_manager

      if not self.found_version: return VersionResult(is_compatible=False, clean_format="Invalid Format") desired_version = desired_version or "0.0.0" - compatible_result, _version_object = check_compatibility(desired_version, str(self.found_version)) + compatible_result, _version_object = compatibility.check_compatibility(desired_version, str(self.found_version)) self.result = VersionResult( is_compatible=compatible_result == "Compatible", clean_format=str(self.found_version) @@ -139,7 +141,7 @@

      Module cli_tool_audit.audit_manager

      class ExistenceVersionChecker(VersionChecker): - def __init__(self, version_string: str) -> None: + def __init__(self, version_string: ExistenceVersionStatus) -> None: """ Check if a tool exists. Args: @@ -149,7 +151,7 @@

      Module cli_tool_audit.audit_manager

      raise ValueError(f"version_string must be 'Found' or 'Not Found', not {version_string}") self.found_version = version_string - def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: + def check_compatibility(self, desired_version: str) -> VersionResult: """ Check if the tool exists. Args: @@ -287,20 +289,20 @@

      Module cli_tool_audit.audit_manager

      Class to audit a tool, abstract base class to allow for supporting different version schemas. """ - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ tool, config = tool_config.name, tool_config if config.if_os and not sys.platform.startswith(config.if_os): # This isn't very transparent about what just happened - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, is_needed_for_os=False, desired_version=config.version or "0.0.0", @@ -315,17 +317,17 @@

      Module cli_tool_audit.audit_manager

      ) result = self.call_tool( tool, - config.schema or SchemaType.SEMVER, + config.schema or models.SchemaType.SEMVER, config.version_switch or "--version", ) # Not pretty. - if config.schema == SchemaType.EXISTENCE: + if config.schema == models.SchemaType.EXISTENCE: existence_checker = ExistenceVersionChecker("Found" if result.is_available else "Not Found") version_result = existence_checker.check_compatibility("Found") compatibility_report = existence_checker.format_report("Found") desired_version = "*" - elif config.schema == SchemaType.SNAPSHOT: + elif config.schema == models.SchemaType.SNAPSHOT: snapshot_checker = SnapshotVersionChecker(result.version or "") version_result = snapshot_checker.check_compatibility(config.version) compatibility_report = snapshot_checker.format_report(config.version or "") @@ -341,12 +343,12 @@

      Module cli_tool_audit.audit_manager

      compatibility_report = semver_checker.format_report(config.version or "0.0.0") desired_version = config.version or "*" - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, desired_version=desired_version, is_needed_for_os=True, is_available=result.is_available, - is_snapshot=bool(config.schema == SchemaType.SNAPSHOT), + is_snapshot=bool(config.schema == models.SchemaType.SNAPSHOT), found_version=result.version, parsed_version=version_result.clean_format, is_compatible=compatibility_report, @@ -358,9 +360,9 @@

      Module cli_tool_audit.audit_manager

      def call_tool( self, tool_name: str, - schema: SchemaType, + schema: models.SchemaType, version_switch: str = "--version", - ) -> ToolAvailabilityResult: + ) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. @@ -379,10 +381,10 @@

      Module cli_tool_audit.audit_manager

      last_modified = self.get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -394,7 +396,7 @@

      Module cli_tool_audit.audit_manager

      # pylint: disable=broad-exception-caught try: command = [tool_name, version_switch] - timeout= int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) + timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) result = subprocess.run( command, capture_output=True, text=True, timeout=timeout, shell=False, check=True ) # nosec @@ -414,9 +416,9 @@

      Module cli_tool_audit.audit_manager

      logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified) + return models.ToolAvailabilityResult(True, is_broken, version, last_modified) def get_command_last_modified_date(self, tool_name: str) -> Optional[datetime.datetime]: """ @@ -462,20 +464,20 @@

      Classes

      Class to audit a tool, abstract base class to allow for supporting different version schemas. """ - def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult: + def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult: """ Call and check the given tool. Args: - tool_config (CliToolConfig): The tool to call and check. + tool_config (models.CliToolConfig): The tool to call and check. Returns: - ToolCheckResult: The result of the check. + models.ToolCheckResult: The result of the check. """ tool, config = tool_config.name, tool_config if config.if_os and not sys.platform.startswith(config.if_os): # This isn't very transparent about what just happened - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, is_needed_for_os=False, desired_version=config.version or "0.0.0", @@ -490,17 +492,17 @@

      Classes

      ) result = self.call_tool( tool, - config.schema or SchemaType.SEMVER, + config.schema or models.SchemaType.SEMVER, config.version_switch or "--version", ) # Not pretty. - if config.schema == SchemaType.EXISTENCE: + if config.schema == models.SchemaType.EXISTENCE: existence_checker = ExistenceVersionChecker("Found" if result.is_available else "Not Found") version_result = existence_checker.check_compatibility("Found") compatibility_report = existence_checker.format_report("Found") desired_version = "*" - elif config.schema == SchemaType.SNAPSHOT: + elif config.schema == models.SchemaType.SNAPSHOT: snapshot_checker = SnapshotVersionChecker(result.version or "") version_result = snapshot_checker.check_compatibility(config.version) compatibility_report = snapshot_checker.format_report(config.version or "") @@ -516,12 +518,12 @@

      Classes

      compatibility_report = semver_checker.format_report(config.version or "0.0.0") desired_version = config.version or "*" - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, desired_version=desired_version, is_needed_for_os=True, is_available=result.is_available, - is_snapshot=bool(config.schema == SchemaType.SNAPSHOT), + is_snapshot=bool(config.schema == models.SchemaType.SNAPSHOT), found_version=result.version, parsed_version=version_result.clean_format, is_compatible=compatibility_report, @@ -533,9 +535,9 @@

      Classes

      def call_tool( self, tool_name: str, - schema: SchemaType, + schema: models.SchemaType, version_switch: str = "--version", - ) -> ToolAvailabilityResult: + ) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. @@ -554,10 +556,10 @@

      Classes

      last_modified = self.get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -569,7 +571,7 @@

      Classes

      # pylint: disable=broad-exception-caught try: command = [tool_name, version_switch] - timeout= int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) + timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) result = subprocess.run( command, capture_output=True, text=True, timeout=timeout, shell=False, check=True ) # nosec @@ -589,9 +591,9 @@

      Classes

      logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified) + return models.ToolAvailabilityResult(True, is_broken, version, last_modified) def get_command_last_modified_date(self, tool_name: str) -> Optional[datetime.datetime]: """ @@ -622,32 +624,32 @@

      Methods

      Call and check the given tool.

      Args

      -
      tool_config : CliToolConfig
      +
      tool_config : models.CliToolConfig
      The tool to call and check.

      Returns

      -
      ToolCheckResult
      +
      models.ToolCheckResult
      The result of the check.
      Expand source code -
      def call_and_check(self, tool_config: CliToolConfig) -> ToolCheckResult:
      +
      def call_and_check(self, tool_config: models.CliToolConfig) -> models.ToolCheckResult:
           """
           Call and check the given tool.
           Args:
      -        tool_config (CliToolConfig): The tool to call and check.
      +        tool_config (models.CliToolConfig): The tool to call and check.
       
           Returns:
      -        ToolCheckResult: The result of the check.
      +        models.ToolCheckResult: The result of the check.
           """
           tool, config = tool_config.name, tool_config
       
           if config.if_os and not sys.platform.startswith(config.if_os):
               # This isn't very transparent about what just happened
      -        return ToolCheckResult(
      +        return models.ToolCheckResult(
                   tool=tool,
                   is_needed_for_os=False,
                   desired_version=config.version or "0.0.0",
      @@ -662,17 +664,17 @@ 

      Returns

      ) result = self.call_tool( tool, - config.schema or SchemaType.SEMVER, + config.schema or models.SchemaType.SEMVER, config.version_switch or "--version", ) # Not pretty. - if config.schema == SchemaType.EXISTENCE: + if config.schema == models.SchemaType.EXISTENCE: existence_checker = ExistenceVersionChecker("Found" if result.is_available else "Not Found") version_result = existence_checker.check_compatibility("Found") compatibility_report = existence_checker.format_report("Found") desired_version = "*" - elif config.schema == SchemaType.SNAPSHOT: + elif config.schema == models.SchemaType.SNAPSHOT: snapshot_checker = SnapshotVersionChecker(result.version or "") version_result = snapshot_checker.check_compatibility(config.version) compatibility_report = snapshot_checker.format_report(config.version or "") @@ -688,12 +690,12 @@

      Returns

      compatibility_report = semver_checker.format_report(config.version or "0.0.0") desired_version = config.version or "*" - return ToolCheckResult( + return models.ToolCheckResult( tool=tool, desired_version=desired_version, is_needed_for_os=True, is_available=result.is_available, - is_snapshot=bool(config.schema == SchemaType.SNAPSHOT), + is_snapshot=bool(config.schema == models.SchemaType.SNAPSHOT), found_version=result.version, parsed_version=version_result.clean_format, is_compatible=compatibility_report, @@ -729,9 +731,9 @@

      Returns

      def call_tool(
           self,
           tool_name: str,
      -    schema: SchemaType,
      +    schema: models.SchemaType,
           version_switch: str = "--version",
      -) -> ToolAvailabilityResult:
      +) -> models.ToolAvailabilityResult:
           """
           Check if a tool is available in the system's PATH and if possible, determine a version number.
       
      @@ -750,10 +752,10 @@ 

      Returns

      last_modified = self.get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -765,7 +767,7 @@

      Returns

      # pylint: disable=broad-exception-caught try: command = [tool_name, version_switch] - timeout= int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) + timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) result = subprocess.run( command, capture_output=True, text=True, timeout=timeout, shell=False, check=True ) # nosec @@ -785,9 +787,9 @@

      Returns

      logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified)
      + return models.ToolAvailabilityResult(True, is_broken, version, last_modified)
      @@ -834,7 +836,7 @@

      Returns

      class ExistenceVersionChecker -(version_string: str) +(version_string: Literal['Found', 'Not Found'])

      Abstract base class for checking if a version is compatible with a desired version.

      @@ -849,7 +851,7 @@

      Args

      Expand source code
      class ExistenceVersionChecker(VersionChecker):
      -    def __init__(self, version_string: str) -> None:
      +    def __init__(self, version_string: ExistenceVersionStatus) -> None:
               """
               Check if a tool exists.
               Args:
      @@ -859,7 +861,7 @@ 

      Args

      raise ValueError(f"version_string must be 'Found' or 'Not Found', not {version_string}") self.found_version = version_string - def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: + def check_compatibility(self, desired_version: str) -> VersionResult: """ Check if the tool exists. Args: @@ -889,7 +891,7 @@

      Ancestors

      Methods

      -def check_compatibility(self, desired_version: Optional[str]) ‑> VersionResult +def check_compatibility(self, desired_version: str) ‑> VersionResult

      Check if the tool exists.

      @@ -907,7 +909,7 @@

      Returns

      Expand source code -
      def check_compatibility(self, desired_version: Optional[str]) -> VersionResult:
      +
      def check_compatibility(self, desired_version: str) -> VersionResult:
           """
           Check if the tool exists.
           Args:
      @@ -1120,7 +1122,7 @@ 

      Inherited members

      Returns: Optional[Version]: The parsed version or None if the version string is invalid. """ - return two_pass_semver_parse(version_string) + return version_parsing.two_pass_semver_parse(version_string) def check_compatibility(self, desired_version: Optional[str]) -> VersionResult: """ @@ -1134,7 +1136,7 @@

      Inherited members

      if not self.found_version: return VersionResult(is_compatible=False, clean_format="Invalid Format") desired_version = desired_version or "0.0.0" - compatible_result, _version_object = check_compatibility(desired_version, str(self.found_version)) + compatible_result, _version_object = compatibility.check_compatibility(desired_version, str(self.found_version)) self.result = VersionResult( is_compatible=compatible_result == "Compatible", clean_format=str(self.found_version) @@ -1192,7 +1194,7 @@

      Returns

      if not self.found_version: return VersionResult(is_compatible=False, clean_format="Invalid Format") desired_version = desired_version or "0.0.0" - compatible_result, _version_object = check_compatibility(desired_version, str(self.found_version)) + compatible_result, _version_object = compatibility.check_compatibility(desired_version, str(self.found_version)) self.result = VersionResult( is_compatible=compatible_result == "Compatible", clean_format=str(self.found_version) @@ -1228,7 +1230,7 @@

      Returns

      Returns: Optional[Version]: The parsed version or None if the version string is invalid. """ - return two_pass_semver_parse(version_string)
      + return version_parsing.two_pass_semver_parse(version_string)
      diff --git a/docs/cli_tool_audit/call_and_compatible.html b/docs/cli_tool_audit/call_and_compatible.html index 744d9f3..bdbfe27 100644 --- a/docs/cli_tool_audit/call_and_compatible.html +++ b/docs/cli_tool_audit/call_and_compatible.html @@ -33,31 +33,31 @@

      Module cli_tool_audit.call_and_compatible

      import sys import threading -from cli_tool_audit.audit_cache import AuditFacade -from cli_tool_audit.audit_manager import AuditManager -from cli_tool_audit.models import CliToolConfig, ToolCheckResult +import cli_tool_audit.audit_cache as audit_cache +import cli_tool_audit.audit_manager as audit_manager +import cli_tool_audit.models as models # Old interface def check_tool_wrapper( - tool_info: tuple[str, CliToolConfig, threading.Lock, bool], -) -> ToolCheckResult: + tool_info: tuple[str, models.CliToolConfig, threading.Lock, bool], +) -> models.ToolCheckResult: """ Wrapper function for check_tool_availability() that returns a ToolCheckResult object. Args: - tool_info (tuple[str, CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the + tool_info (tuple[str, models.CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the CliToolConfig object, a lock, and a boolean indicating if the cache is enabled. Returns: - ToolCheckResult: A ToolCheckResult object. + models.ToolCheckResult: A ToolCheckResult object. """ tool, config, lock, enable_cache = tool_info if config.if_os and not sys.platform.startswith(config.if_os): # This isn't very transparent about what just happened - return ToolCheckResult( + return models.ToolCheckResult( is_needed_for_os=False, tool=tool, desired_version=config.version or "0.0.0", @@ -75,11 +75,11 @@

      Module cli_tool_audit.call_and_compatible

      config.version_switch = config.version_switch or "--version" if enable_cache: - cached_manager = AuditFacade() + cached_manager = audit_cache.AuditFacade() with lock: return cached_manager.call_and_check(tool_config=config) - manager = AuditManager() + manager = audit_manager.AuditManager() return manager.call_and_check(tool_config=config)
  • @@ -97,13 +97,13 @@

    Functions

    Wrapper function for check_tool_availability() that returns a ToolCheckResult object.

    Args

    -
    tool_info : tuple[str, CliToolConfig, threading.Lock, bool]
    +
    tool_info : tuple[str, models.CliToolConfig, threading.Lock, bool]
    A tuple containing the tool name, the

    CliToolConfig object, a lock, and a boolean indicating if the cache is enabled.

    Returns

    -
    ToolCheckResult
    +
    models.ToolCheckResult
    A ToolCheckResult object.
    @@ -111,23 +111,23 @@

    Returns

    Expand source code
    def check_tool_wrapper(
    -    tool_info: tuple[str, CliToolConfig, threading.Lock, bool],
    -) -> ToolCheckResult:
    +    tool_info: tuple[str, models.CliToolConfig, threading.Lock, bool],
    +) -> models.ToolCheckResult:
         """
         Wrapper function for check_tool_availability() that returns a ToolCheckResult object.
     
         Args:
    -        tool_info (tuple[str, CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the
    +        tool_info (tuple[str, models.CliToolConfig, threading.Lock, bool]): A tuple containing the tool name, the
             CliToolConfig object, a lock, and a boolean indicating if the cache is enabled.
     
         Returns:
    -        ToolCheckResult: A ToolCheckResult object.
    +        models.ToolCheckResult: A ToolCheckResult object.
         """
         tool, config, lock, enable_cache = tool_info
     
         if config.if_os and not sys.platform.startswith(config.if_os):
             # This isn't very transparent about what just happened
    -        return ToolCheckResult(
    +        return models.ToolCheckResult(
                 is_needed_for_os=False,
                 tool=tool,
                 desired_version=config.version or "0.0.0",
    @@ -145,11 +145,11 @@ 

    Returns

    config.version_switch = config.version_switch or "--version" if enable_cache: - cached_manager = AuditFacade() + cached_manager = audit_cache.AuditFacade() with lock: return cached_manager.call_and_check(tool_config=config) - manager = AuditManager() + manager = audit_manager.AuditManager() return manager.call_and_check(tool_config=config)
    diff --git a/docs/cli_tool_audit/call_tools.html b/docs/cli_tool_audit/call_tools.html index 8ba3135..1dbe028 100644 --- a/docs/cli_tool_audit/call_tools.html +++ b/docs/cli_tool_audit/call_tools.html @@ -54,8 +54,8 @@

    Module cli_tool_audit.call_tools

    # pylint: disable=no-name-in-module from whichcraft import which +import cli_tool_audit.models as models from cli_tool_audit.known_switches import KNOWN_SWITCHES -from cli_tool_audit.models import SchemaType, ToolAvailabilityResult logger = logging.getLogger(__name__) @@ -83,20 +83,20 @@

    Module cli_tool_audit.call_tools

    def check_tool_availability( tool_name: str, - schema: SchemaType, + schema: models.SchemaType, version_switch: str = "--version", -) -> ToolAvailabilityResult: +) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. Args: tool_name (str): The name of the tool to check. - schema (SchemaType): The schema to use for the version. + schema (models.SchemaType): The schema to use for the version. version_switch (str): The switch to get the tool version. Defaults to '--version'. Returns: - ToolAvailabilityResult: An object containing the availability and version of the tool. + models.ToolAvailabilityResult: An object containing the availability and version of the tool. """ # Check if the tool is in the system's PATH is_broken = True @@ -104,10 +104,10 @@

    Module cli_tool_audit.call_tools

    last_modified = get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -120,7 +120,9 @@

    Module cli_tool_audit.call_tools

    try: command = [tool_name, version_switch] timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) - result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, shell=False, check=True) # nosec + result = subprocess.run( + command, capture_output=True, text=True, timeout=timeout, shell=False, check=True + ) # nosec # Sometimes version is on line 2 or later. version = result.stdout.strip() if not version: @@ -137,29 +139,13 @@

    Module cli_tool_audit.call_tools

    logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified) + return models.ToolAvailabilityResult(True, is_broken, version, last_modified) if __name__ == "__main__": - print(get_command_last_modified_date("asdfpipx")) - # check_tool_availability("isort") - # - # def run() -> None: - # """Example""" - # # Example usage - # file_path = "../pyproject.toml" - # cli_tools = read_config(file_path) - # - # for tool, config in cli_tools.items(): - # result = check_tool_availability(tool, config.version_switch or "--version") - # print( - # f"{tool}: {'Available' if result.is_available else 'Not Available'}" - # f" - Version:\n{result.version if result.version else 'N/A'}" - # ) - # - # run()
    + print(get_command_last_modified_date("asdfpipx"))
    @@ -178,14 +164,14 @@

    Args

    tool_name : str
    The name of the tool to check.
    -
    schema : SchemaType
    +
    schema : models.SchemaType
    The schema to use for the version.
    version_switch : str
    The switch to get the tool version. Defaults to '–version'.

    Returns

    -
    ToolAvailabilityResult
    +
    models.ToolAvailabilityResult
    An object containing the availability and version of the tool.
    @@ -194,20 +180,20 @@

    Returns

    def check_tool_availability(
         tool_name: str,
    -    schema: SchemaType,
    +    schema: models.SchemaType,
         version_switch: str = "--version",
    -) -> ToolAvailabilityResult:
    +) -> models.ToolAvailabilityResult:
         """
         Check if a tool is available in the system's PATH and if possible, determine a version number.
     
         Args:
             tool_name (str): The name of the tool to check.
    -        schema (SchemaType): The schema to use for the version.
    +        schema (models.SchemaType): The schema to use for the version.
             version_switch (str): The switch to get the tool version. Defaults to '--version'.
     
     
         Returns:
    -        ToolAvailabilityResult: An object containing the availability and version of the tool.
    +        models.ToolAvailabilityResult: An object containing the availability and version of the tool.
         """
         # Check if the tool is in the system's PATH
         is_broken = True
    @@ -215,10 +201,10 @@ 

    Returns

    last_modified = get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -231,7 +217,9 @@

    Returns

    try: command = [tool_name, version_switch] timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) - result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, shell=False, check=True) # nosec + result = subprocess.run( + command, capture_output=True, text=True, timeout=timeout, shell=False, check=True + ) # nosec # Sometimes version is on line 2 or later. version = result.stdout.strip() if not version: @@ -248,9 +236,9 @@

    Returns

    logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) - return ToolAvailabilityResult(True, is_broken, version, last_modified)
    + return models.ToolAvailabilityResult(True, is_broken, version, last_modified)
    diff --git a/docs/cli_tool_audit/compatibility.html b/docs/cli_tool_audit/compatibility.html index 8897930..568e392 100644 --- a/docs/cli_tool_audit/compatibility.html +++ b/docs/cli_tool_audit/compatibility.html @@ -36,8 +36,8 @@

    Module cli_tool_audit.compatibility

    from semver import Version -from cli_tool_audit.compatibility_complex import check_range_compatibility -from cli_tool_audit.version_parsing import two_pass_semver_parse +import cli_tool_audit.compatibility_complex as compatibility_complex +import cli_tool_audit.version_parsing as version_parsing logger = logging.getLogger(__name__) @@ -115,13 +115,13 @@

    Module cli_tool_audit.compatibility

    # Handle non-semver match patterns symbols, desired_version_text = split_version_match_pattern(desired_version) - clean_desired_version = two_pass_semver_parse(desired_version_text) + clean_desired_version = version_parsing.two_pass_semver_parse(desired_version_text) if clean_desired_version: desired_version = f"{symbols}{clean_desired_version}" found_semversion = None try: - found_semversion = two_pass_semver_parse(found_version) + found_semversion = version_parsing.two_pass_semver_parse(found_version) if found_semversion is None: logger.warning(f"SemVer failed to parse {desired_version}/{found_version}") is_compatible = CANT_TELL @@ -129,7 +129,7 @@

    Module cli_tool_audit.compatibility

    # not picky, short circuit the logic. is_compatible = "Compatible" elif desired_version.startswith("^") or desired_version.startswith("~") or "*" in desired_version: - is_compatible = check_range_compatibility(desired_version, found_semversion) + is_compatible = compatibility_complex.check_range_compatibility(desired_version, found_semversion) elif found_semversion.match(desired_version): is_compatible = "Compatible" else: @@ -215,13 +215,13 @@

    Examples

    # Handle non-semver match patterns symbols, desired_version_text = split_version_match_pattern(desired_version) - clean_desired_version = two_pass_semver_parse(desired_version_text) + clean_desired_version = version_parsing.two_pass_semver_parse(desired_version_text) if clean_desired_version: desired_version = f"{symbols}{clean_desired_version}" found_semversion = None try: - found_semversion = two_pass_semver_parse(found_version) + found_semversion = version_parsing.two_pass_semver_parse(found_version) if found_semversion is None: logger.warning(f"SemVer failed to parse {desired_version}/{found_version}") is_compatible = CANT_TELL @@ -229,7 +229,7 @@

    Examples

    # not picky, short circuit the logic. is_compatible = "Compatible" elif desired_version.startswith("^") or desired_version.startswith("~") or "*" in desired_version: - is_compatible = check_range_compatibility(desired_version, found_semversion) + is_compatible = compatibility_complex.check_range_compatibility(desired_version, found_semversion) elif found_semversion.match(desired_version): is_compatible = "Compatible" else: diff --git a/docs/cli_tool_audit/config_manager.html b/docs/cli_tool_audit/config_manager.html index 05ba167..c699164 100644 --- a/docs/cli_tool_audit/config_manager.html +++ b/docs/cli_tool_audit/config_manager.html @@ -28,11 +28,13 @@

    Module cli_tool_audit.config_manager

    import copy
     import os
    +from pathlib import Path
     from typing import Any, cast
     
    +import toml
     import tomlkit
     
    -from cli_tool_audit.models import CliToolConfig, SchemaType
    +import cli_tool_audit.models as models
     
     
     class ConfigManager:
    @@ -40,13 +42,13 @@ 

    Module cli_tool_audit.config_manager

    Manage the config file. """ - def __init__(self, config_path: str) -> None: + def __init__(self, config_path: Path) -> None: """ Args: - config_path (str): The path to the toml file. + config_path (Path): The path to the toml file. """ self.config_path = config_path - self.tools: dict[str, CliToolConfig] = {} + self.tools: dict[str, models.CliToolConfig] = {} def read_config(self) -> bool: """ @@ -55,25 +57,40 @@

    Module cli_tool_audit.config_manager

    Returns: bool: True if the cli-tools section exists, False otherwise. """ - if os.path.exists(self.config_path): - with open(self.config_path, encoding="utf-8") as file: - config = tomlkit.load(file) + if self.config_path.exists(): + with open(str(self.config_path), encoding="utf-8") as file: + # okay this is too hard. + # from tomlkit.items import Item + # class SchemaTypeItem(Item): + # def __init__(self, value:SchemaType): + # self.value = value + # def as_string(self): + # return str(self.value) + # + # def encoder(value): + # if isinstance(value, SchemaType): + # return SchemaTypeItem(value.value) + # raise TypeError(f"Unknown type {type(value)}") + # tomlkit.register_encoder(lambda x: x.value) + # TODO: switch to tomkit and clone the config/settings + # so that we can use it like ordinary python + config = toml.load(file) tools_config = config.get("tool", {}).get("cli-tools", {}) for tool_name, settings in tools_config.items(): if settings.get("only_check_existence"): - settings["schema"] = SchemaType.EXISTENCE + settings["schema"] = models.SchemaType.EXISTENCE del settings["only_check_existence"] elif settings.get("version_snapshot"): - settings["schema"] = SchemaType.SNAPSHOT + settings["schema"] = models.SchemaType.SNAPSHOT settings["version"] = settings.get("version_snapshot") del settings["version_snapshot"] settings["name"] = tool_name if settings.get("schema"): - settings["schema"] = SchemaType(settings["schema"].lower()) + settings["schema"] = models.SchemaType(settings["schema"].lower()) else: - settings["schema"] = SchemaType.SEMVER - self.tools[tool_name] = CliToolConfig(**settings) + settings["schema"] = models.SchemaType.SEMVER + self.tools[tool_name] = models.CliToolConfig(**settings) return bool(self.tools) def create_tool_config(self, tool_name: str, config: dict) -> None: @@ -92,7 +109,7 @@

    Module cli_tool_audit.config_manager

    if tool_name in self.tools: raise ValueError(f"Tool {tool_name} already exists") config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) self._save_config() def update_tool_config(self, tool_name: str, config: dict) -> None: @@ -124,11 +141,11 @@

    Module cli_tool_audit.config_manager

    self.read_config() if tool_name not in self.tools: config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) else: for key, value in config.items(): if key == "schema": - value = str(SchemaType(value)) + value = str(models.SchemaType(value)) setattr(self.tools[tool_name], key, value) self._save_config() @@ -185,9 +202,13 @@

    Module cli_tool_audit.config_manager

    if __name__ == "__main__": # Usage example - config_manager = ConfigManager("../pyproject.toml") - c = config_manager.read_config() - print(c)
    + def run() -> None: + """Example""" + config_manager = ConfigManager(Path("../pyproject.toml")) + c = config_manager.read_config() + print(c) + + run()
    @@ -201,13 +222,13 @@

    Classes

    class ConfigManager -(config_path: str) +(config_path: pathlib.Path)

    Manage the config file.

    Args

    -
    config_path : str
    +
    config_path : Path
    The path to the toml file.
    @@ -219,13 +240,13 @@

    Args

    Manage the config file. """ - def __init__(self, config_path: str) -> None: + def __init__(self, config_path: Path) -> None: """ Args: - config_path (str): The path to the toml file. + config_path (Path): The path to the toml file. """ self.config_path = config_path - self.tools: dict[str, CliToolConfig] = {} + self.tools: dict[str, models.CliToolConfig] = {} def read_config(self) -> bool: """ @@ -234,25 +255,40 @@

    Args

    Returns: bool: True if the cli-tools section exists, False otherwise. """ - if os.path.exists(self.config_path): - with open(self.config_path, encoding="utf-8") as file: - config = tomlkit.load(file) + if self.config_path.exists(): + with open(str(self.config_path), encoding="utf-8") as file: + # okay this is too hard. + # from tomlkit.items import Item + # class SchemaTypeItem(Item): + # def __init__(self, value:SchemaType): + # self.value = value + # def as_string(self): + # return str(self.value) + # + # def encoder(value): + # if isinstance(value, SchemaType): + # return SchemaTypeItem(value.value) + # raise TypeError(f"Unknown type {type(value)}") + # tomlkit.register_encoder(lambda x: x.value) + # TODO: switch to tomkit and clone the config/settings + # so that we can use it like ordinary python + config = toml.load(file) tools_config = config.get("tool", {}).get("cli-tools", {}) for tool_name, settings in tools_config.items(): if settings.get("only_check_existence"): - settings["schema"] = SchemaType.EXISTENCE + settings["schema"] = models.SchemaType.EXISTENCE del settings["only_check_existence"] elif settings.get("version_snapshot"): - settings["schema"] = SchemaType.SNAPSHOT + settings["schema"] = models.SchemaType.SNAPSHOT settings["version"] = settings.get("version_snapshot") del settings["version_snapshot"] settings["name"] = tool_name if settings.get("schema"): - settings["schema"] = SchemaType(settings["schema"].lower()) + settings["schema"] = models.SchemaType(settings["schema"].lower()) else: - settings["schema"] = SchemaType.SEMVER - self.tools[tool_name] = CliToolConfig(**settings) + settings["schema"] = models.SchemaType.SEMVER + self.tools[tool_name] = models.CliToolConfig(**settings) return bool(self.tools) def create_tool_config(self, tool_name: str, config: dict) -> None: @@ -271,7 +307,7 @@

    Args

    if tool_name in self.tools: raise ValueError(f"Tool {tool_name} already exists") config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) self._save_config() def update_tool_config(self, tool_name: str, config: dict) -> None: @@ -303,11 +339,11 @@

    Args

    self.read_config() if tool_name not in self.tools: config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) else: for key, value in config.items(): if key == "schema": - value = str(SchemaType(value)) + value = str(models.SchemaType(value)) setattr(self.tools[tool_name], key, value) self._save_config() @@ -400,7 +436,7 @@

    Raises

    if tool_name in self.tools: raise ValueError(f"Tool {tool_name} already exists") config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) self._save_config()
    @@ -431,11 +467,11 @@

    Args

    self.read_config() if tool_name not in self.tools: config["name"] = tool_name - self.tools[tool_name] = CliToolConfig(**config) + self.tools[tool_name] = models.CliToolConfig(**config) else: for key, value in config.items(): if key == "schema": - value = str(SchemaType(value)) + value = str(models.SchemaType(value)) setattr(self.tools[tool_name], key, value) self._save_config() @@ -489,25 +525,40 @@

    Returns

    Returns: bool: True if the cli-tools section exists, False otherwise. """ - if os.path.exists(self.config_path): - with open(self.config_path, encoding="utf-8") as file: - config = tomlkit.load(file) + if self.config_path.exists(): + with open(str(self.config_path), encoding="utf-8") as file: + # okay this is too hard. + # from tomlkit.items import Item + # class SchemaTypeItem(Item): + # def __init__(self, value:SchemaType): + # self.value = value + # def as_string(self): + # return str(self.value) + # + # def encoder(value): + # if isinstance(value, SchemaType): + # return SchemaTypeItem(value.value) + # raise TypeError(f"Unknown type {type(value)}") + # tomlkit.register_encoder(lambda x: x.value) + # TODO: switch to tomkit and clone the config/settings + # so that we can use it like ordinary python + config = toml.load(file) tools_config = config.get("tool", {}).get("cli-tools", {}) for tool_name, settings in tools_config.items(): if settings.get("only_check_existence"): - settings["schema"] = SchemaType.EXISTENCE + settings["schema"] = models.SchemaType.EXISTENCE del settings["only_check_existence"] elif settings.get("version_snapshot"): - settings["schema"] = SchemaType.SNAPSHOT + settings["schema"] = models.SchemaType.SNAPSHOT settings["version"] = settings.get("version_snapshot") del settings["version_snapshot"] settings["name"] = tool_name if settings.get("schema"): - settings["schema"] = SchemaType(settings["schema"].lower()) + settings["schema"] = models.SchemaType(settings["schema"].lower()) else: - settings["schema"] = SchemaType.SEMVER - self.tools[tool_name] = CliToolConfig(**settings) + settings["schema"] = models.SchemaType.SEMVER + self.tools[tool_name] = models.CliToolConfig(**settings) return bool(self.tools) diff --git a/docs/cli_tool_audit/config_reader.html b/docs/cli_tool_audit/config_reader.html index 48547f6..dd6173d 100644 --- a/docs/cli_tool_audit/config_reader.html +++ b/docs/cli_tool_audit/config_reader.html @@ -30,29 +30,29 @@

    Module cli_tool_audit.config_reader

    """
     Read list of tools from config.
     """
    -
     import logging
    +from pathlib import Path
     
    -from cli_tool_audit.config_manager import ConfigManager
    -from cli_tool_audit.models import CliToolConfig
    +import cli_tool_audit.config_manager as config_manager
    +import cli_tool_audit.models as models
     
     logger = logging.getLogger(__name__)
     
     
    -def read_config(file_path: str) -> dict[str, CliToolConfig]:
    +def read_config(file_path: Path) -> dict[str, models.CliToolConfig]:
         """
         Read the cli-tools section from a pyproject.toml file.
     
         Args:
    -        file_path (str): The path to the pyproject.toml file.
    +        file_path (Path): The path to the pyproject.toml file.
     
         Returns:
    -        dict[str, CliToolConfig]: A dictionary with the cli-tools section.
    +        dict[str, models.CliToolConfig]: A dictionary with the cli-tools section.
         """
         # pylint: disable=broad-exception-caught
         try:
             logger.debug(f"Loading config from {file_path}")
    -        manager = ConfigManager(file_path)
    +        manager = config_manager.ConfigManager(file_path)
             found = manager.read_config()
             if not found:
                 logger.warning("Config section not found, expected [tool.cli-tools] with values")
    @@ -71,38 +71,38 @@ 

    Module cli_tool_audit.config_reader

    Functions

    -def read_config(file_path: str) ‑> dict[str, CliToolConfig] +def read_config(file_path: pathlib.Path) ‑> dict[str, CliToolConfig]

    Read the cli-tools section from a pyproject.toml file.

    Args

    -
    file_path : str
    +
    file_path : Path
    The path to the pyproject.toml file.

    Returns

    -
    dict[str, CliToolConfig]
    +
    dict[str, models.CliToolConfig]
    A dictionary with the cli-tools section.
    Expand source code -
    def read_config(file_path: str) -> dict[str, CliToolConfig]:
    +
    def read_config(file_path: Path) -> dict[str, models.CliToolConfig]:
         """
         Read the cli-tools section from a pyproject.toml file.
     
         Args:
    -        file_path (str): The path to the pyproject.toml file.
    +        file_path (Path): The path to the pyproject.toml file.
     
         Returns:
    -        dict[str, CliToolConfig]: A dictionary with the cli-tools section.
    +        dict[str, models.CliToolConfig]: A dictionary with the cli-tools section.
         """
         # pylint: disable=broad-exception-caught
         try:
             logger.debug(f"Loading config from {file_path}")
    -        manager = ConfigManager(file_path)
    +        manager = config_manager.ConfigManager(file_path)
             found = manager.read_config()
             if not found:
                 logger.warning("Config section not found, expected [tool.cli-tools] with values")
    diff --git a/docs/cli_tool_audit/freeze.html b/docs/cli_tool_audit/freeze.html
    index 6af5af7..9a4696f 100644
    --- a/docs/cli_tool_audit/freeze.html
    +++ b/docs/cli_tool_audit/freeze.html
    @@ -32,13 +32,14 @@ 

    Module cli_tool_audit.freeze

    """ import os import tempfile +from pathlib import Path import cli_tool_audit.call_tools as call_tools -from cli_tool_audit.config_manager import ConfigManager -from cli_tool_audit.models import SchemaType +import cli_tool_audit.config_manager as cm +import cli_tool_audit.models as models -def freeze_requirements(tool_names: list[str], schema: SchemaType) -> dict[str, call_tools.ToolAvailabilityResult]: +def freeze_requirements(tool_names: list[str], schema: models.SchemaType) -> dict[str, models.ToolAvailabilityResult]: """ Capture the current version of a list of tools. @@ -56,24 +57,24 @@

    Module cli_tool_audit.freeze

    return results -def freeze_to_config(tool_names: list[str], config_path: str, schema: SchemaType) -> None: +def freeze_to_config(tool_names: list[str], config_path: Path, schema: models.SchemaType) -> None: """ Capture the current version of a list of tools and write them to a config file. Args: tool_names (list[str]): A list of tool names. - config_path (str): The path to the config file. + config_path (Path): The path to the config file. schema (SchemaType): The schema to use for the version. """ results = freeze_requirements(tool_names, schema=schema) - config_manager = ConfigManager(config_path) + config_manager = cm.ConfigManager(config_path) config_manager.read_config() for tool_name, result in results.items(): if result.is_available and result.version: config_manager.create_update_tool_config(tool_name, {"version": result.version}) -def freeze_to_screen(tool_names: list[str], schema: SchemaType) -> None: +def freeze_to_screen(tool_names: list[str], schema: models.SchemaType) -> None: """ Capture the current version of a list of tools, write them to a temp config file, and print the 'cli-tools' section of the config. @@ -87,7 +88,7 @@

    Module cli_tool_audit.freeze

    # Create a temporary directory and file with tempfile.TemporaryDirectory() as temp_dir: temp_config_path = os.path.join(temp_dir, "temp.toml") - config_manager = ConfigManager(temp_config_path) + config_manager = cm.ConfigManager(Path(temp_config_path)) config_manager.read_config() for tool_name, result in results.items(): @@ -105,7 +106,7 @@

    Module cli_tool_audit.freeze

    if __name__ == "__main__": - freeze_to_screen(["python", "pip", "poetry"], schema=SchemaType.SNAPSHOT)
    + freeze_to_screen(["python", "pip", "poetry"], schema=models.SchemaType.SNAPSHOT)
    @@ -136,7 +137,7 @@

    Returns

    Expand source code -
    def freeze_requirements(tool_names: list[str], schema: SchemaType) -> dict[str, call_tools.ToolAvailabilityResult]:
    +
    def freeze_requirements(tool_names: list[str], schema: models.SchemaType) -> dict[str, models.ToolAvailabilityResult]:
         """
         Capture the current version of a list of tools.
     
    @@ -155,7 +156,7 @@ 

    Returns

    -def freeze_to_config(tool_names: list[str], config_path: str, schema: SchemaType) ‑> None +def freeze_to_config(tool_names: list[str], config_path: pathlib.Path, schema: SchemaType) ‑> None

    Capture the current version of a list of tools and write them to a config file.

    @@ -163,7 +164,7 @@

    Args

    tool_names : list[str]
    A list of tool names.
    -
    config_path : str
    +
    config_path : Path
    The path to the config file.
    schema : SchemaType
    The schema to use for the version.
    @@ -172,17 +173,17 @@

    Args

    Expand source code -
    def freeze_to_config(tool_names: list[str], config_path: str, schema: SchemaType) -> None:
    +
    def freeze_to_config(tool_names: list[str], config_path: Path, schema: models.SchemaType) -> None:
         """
         Capture the current version of a list of tools and write them to a config file.
     
         Args:
             tool_names (list[str]): A list of tool names.
    -        config_path (str): The path to the config file.
    +        config_path (Path): The path to the config file.
             schema (SchemaType): The schema to use for the version.
         """
         results = freeze_requirements(tool_names, schema=schema)
    -    config_manager = ConfigManager(config_path)
    +    config_manager = cm.ConfigManager(config_path)
         config_manager.read_config()
         for tool_name, result in results.items():
             if result.is_available and result.version:
    @@ -206,7 +207,7 @@ 

    Args

    Expand source code -
    def freeze_to_screen(tool_names: list[str], schema: SchemaType) -> None:
    +
    def freeze_to_screen(tool_names: list[str], schema: models.SchemaType) -> None:
         """
         Capture the current version of a list of tools, write them to a temp config file,
         and print the 'cli-tools' section of the config.
    @@ -220,7 +221,7 @@ 

    Args

    # Create a temporary directory and file with tempfile.TemporaryDirectory() as temp_dir: temp_config_path = os.path.join(temp_dir, "temp.toml") - config_manager = ConfigManager(temp_config_path) + config_manager = cm.ConfigManager(Path(temp_config_path)) config_manager.read_config() for tool_name, result in results.items(): diff --git a/docs/cli_tool_audit/index.html b/docs/cli_tool_audit/index.html index 3293807..079a290 100644 --- a/docs/cli_tool_audit/index.html +++ b/docs/cli_tool_audit/index.html @@ -232,6 +232,11 @@

    Changelog

    The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

    [3.0.0] - 2024-01-19

    +

    Fixed

    +
      +
    • only truncate long version on console output
    • +
    • removed three dependencies (toml, hypothesis, importlib-metadata)
    • +

    Added

    • filter an audit by tags
    • @@ -239,18 +244,13 @@

      Added

    • single to validate one tool without a config file
    • .html output
    -

    Fixed

    -
      -
    • only truncate long version on console output
    • -
    • removed three dependencies (toml, hypothesis, importlib-metadata)
    • -

    [2.0.0] - 2024-01-19

    Fixed

    • Cache now adds gitingore and clears files older than 30 days on startup
    • Audit is now a sub command with arguments.
    -

    Breaking

    +

    Changed

    • Gnu options with dashes, no underscores
    • Global opts that apply to only some commands move to subparser
    • @@ -359,13 +359,14 @@

      Added

      .. include:: ../CHANGELOG.md """ -__all__ = ["validate", "read_config", "check_tool_availability", "__version__", "__about__"] +__all__ = ["validate", "process_tools", "read_config", "check_tool_availability", "models", "__version__", "__about__"] import cli_tool_audit.__about__ as __about__ +import cli_tool_audit.models as models from cli_tool_audit.__about__ import __version__ from cli_tool_audit.call_tools import check_tool_availability from cli_tool_audit.config_reader import read_config -from cli_tool_audit.views import validate
    +from cli_tool_audit.views import process_tools, validate
    @@ -467,14 +468,14 @@

    Args

    tool_name : str
    The name of the tool to check.
    -
    schema : SchemaType
    +
    schema : SchemaType
    The schema to use for the version.
    version_switch : str
    The switch to get the tool version. Defaults to '–version'.

    Returns

    -
    ToolAvailabilityResult
    +
    ToolAvailabilityResult
    An object containing the availability and version of the tool.
    @@ -483,20 +484,20 @@

    Returns

    def check_tool_availability(
         tool_name: str,
    -    schema: SchemaType,
    +    schema: models.SchemaType,
         version_switch: str = "--version",
    -) -> ToolAvailabilityResult:
    +) -> models.ToolAvailabilityResult:
         """
         Check if a tool is available in the system's PATH and if possible, determine a version number.
     
         Args:
             tool_name (str): The name of the tool to check.
    -        schema (SchemaType): The schema to use for the version.
    +        schema (models.SchemaType): The schema to use for the version.
             version_switch (str): The switch to get the tool version. Defaults to '--version'.
     
     
         Returns:
    -        ToolAvailabilityResult: An object containing the availability and version of the tool.
    +        models.ToolAvailabilityResult: An object containing the availability and version of the tool.
         """
         # Check if the tool is in the system's PATH
         is_broken = True
    @@ -504,10 +505,10 @@ 

    Returns

    last_modified = get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) - if schema == SchemaType.EXISTENCE: + return models.ToolAvailabilityResult(False, True, None, last_modified) + if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") - return ToolAvailabilityResult(True, False, None, last_modified) + return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. @@ -520,7 +521,9 @@

    Returns

    try: command = [tool_name, version_switch] timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) - result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, shell=False, check=True) # nosec + result = subprocess.run( + command, capture_output=True, text=True, timeout=timeout, shell=False, check=True + ) # nosec # Sometimes version is on line 2 or later. version = result.stdout.strip() if not version: @@ -537,44 +540,123 @@

    Returns

    logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") - return ToolAvailabilityResult(False, True, None, last_modified) + return models.ToolAvailabilityResult(False, True, None, last_modified) + + return models.ToolAvailabilityResult(True, is_broken, version, last_modified)
    +
    + +
    +def process_tools(cli_tools: dict[str, CliToolConfig], no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False) ‑> list[ToolCheckResult] +
    +
    +

    Process the tools from a dictionary of CliToolConfig objects.

    +

    Args

    +
    +
    cli_tools : dict[str, CliToolConfig]
    +
    A dictionary of tool names and CliToolConfig objects.
    +
    no_cache : bool, optional
    +
    If True, don't use the cache. Defaults to False.
    +
    tags : Optional[list[str]], optional
    +
    Only check tools with these tags. Defaults to None.
    +
    disable_progress_bar : bool, optional
    +
    If True, disable the progress bar. Defaults to False.
    +
    +

    Returns

    +
    +
    list[ToolCheckResult]
    +
    A list of ToolCheckResult objects.
    +
    +
    + +Expand source code + +
    def process_tools(
    +    cli_tools: dict[str, models.CliToolConfig],
    +    no_cache: bool = False,
    +    tags: Optional[list[str]] = None,
    +    disable_progress_bar: bool = False,
    +) -> list[models.ToolCheckResult]:
    +    """
    +    Process the tools from a dictionary of CliToolConfig objects.
     
    -    return ToolAvailabilityResult(True, is_broken, version, last_modified)
    + Args: + cli_tools (dict[str, models.CliToolConfig]): A dictionary of tool names and CliToolConfig objects. + no_cache (bool, optional): If True, don't use the cache. Defaults to False. + tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None. + disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False. + + Returns: + list[models.ToolCheckResult]: A list of ToolCheckResult objects. + """ + if tags: + print(tags) + cli_tools = { + tool: config + for tool, config in cli_tools.items() + if config.tags and any(tag in config.tags for tag in tags) + } + + # Determine the number of available CPUs + num_cpus = os.cpu_count() + + enable_cache = len(cli_tools) >= 5 + # Create a ThreadPoolExecutor with one thread per CPU + + if no_cache: + enable_cache = False + lock = Lock() + # Threaded appears faster. + # lock = Dummy() + # with ProcessPoolExecutor(max_workers=num_cpus) as executor: + with ThreadPoolExecutor(max_workers=num_cpus) as executor: + disable = should_show_progress_bar(cli_tools) + with tqdm(total=len(cli_tools), disable=disable) as pbar: + # Submit tasks to the executor + futures = [ + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) + for tool, config in cli_tools.items() + ] + results = [] + for future in concurrent.futures.as_completed(futures): + result = future.result() + pbar.update(1) + results.append(result) + return results
    -def read_config(file_path: str) ‑> dict[str, CliToolConfig] +def read_config(file_path: pathlib.Path) ‑> dict[str, CliToolConfig]

    Read the cli-tools section from a pyproject.toml file.

    Args

    -
    file_path : str
    +
    file_path : Path
    The path to the pyproject.toml file.

    Returns

    -
    dict[str, CliToolConfig]
    +
    dict[str, CliToolConfig]
    A dictionary with the cli-tools section.
    Expand source code -
    def read_config(file_path: str) -> dict[str, CliToolConfig]:
    +
    def read_config(file_path: Path) -> dict[str, models.CliToolConfig]:
         """
         Read the cli-tools section from a pyproject.toml file.
     
         Args:
    -        file_path (str): The path to the pyproject.toml file.
    +        file_path (Path): The path to the pyproject.toml file.
     
         Returns:
    -        dict[str, CliToolConfig]: A dictionary with the cli-tools section.
    +        dict[str, models.CliToolConfig]: A dictionary with the cli-tools section.
         """
         # pylint: disable=broad-exception-caught
         try:
             logger.debug(f"Loading config from {file_path}")
    -        manager = ConfigManager(file_path)
    +        manager = config_manager.ConfigManager(file_path)
             found = manager.read_config()
             if not found:
                 logger.warning("Config section not found, expected [tool.cli-tools] with values")
    @@ -586,13 +668,13 @@ 

    Returns

    -def validate(file_path: str = 'pyproject.toml', no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False) ‑> list[ToolCheckResult] +def validate(file_path: pathlib.Path = WindowsPath('pyproject.toml'), no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False) ‑> list[ToolCheckResult]

    Validate the tools in the pyproject.toml file.

    Args

    -
    file_path : str, optional
    +
    file_path : Path, optional
    The path to the pyproject.toml file. Defaults to "pyproject.toml".
    no_cache : bool, optional
    If True, don't use the cache. Defaults to False.
    @@ -603,7 +685,7 @@

    Args

    Returns

    -
    list[ToolCheckResult]
    +
    list[ToolCheckResult]
    A list of ToolCheckResult objects.
    @@ -611,22 +693,22 @@

    Returns

    Expand source code
    def validate(
    -    file_path: str = "pyproject.toml",
    +    file_path: Path = Path("pyproject.toml"),
         no_cache: bool = False,
         tags: Optional[list[str]] = None,
         disable_progress_bar: bool = False,
    -) -> list[ToolCheckResult]:
    +) -> list[models.ToolCheckResult]:
         """
         Validate the tools in the pyproject.toml file.
     
         Args:
    -        file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml".
    +        file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml".
             no_cache (bool, optional): If True, don't use the cache. Defaults to False.
             tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None.
             disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False.
     
         Returns:
    -        list[ToolCheckResult]: A list of ToolCheckResult objects.
    +        list[models.ToolCheckResult]: A list of ToolCheckResult objects.
         """
         if tags is None:
             tags = []
    @@ -670,13 +752,13 @@ 

    Index

  • Changelog
    • [3.0.0] - 2024-01-19
    • -
    • Fixed
    • [2.0.0] - 2024-01-19
    • [1.2.0] - 2024-01-17
        @@ -756,6 +838,7 @@

        Index

      • Functions

        diff --git a/docs/cli_tool_audit/interactive.html b/docs/cli_tool_audit/interactive.html index 7d4ab98..58a5ccc 100644 --- a/docs/cli_tool_audit/interactive.html +++ b/docs/cli_tool_audit/interactive.html @@ -32,11 +32,11 @@

        Module cli_tool_audit.interactive

        """ from typing import Union -from cli_tool_audit.config_manager import ConfigManager +import cli_tool_audit.config_manager as cm from cli_tool_audit.models import SchemaType -def interactive_config_manager(config_manager: ConfigManager) -> None: +def interactive_config_manager(config_manager: cm.ConfigManager) -> None: """ Interactively manage tool configurations. @@ -124,7 +124,7 @@

        Args

        Expand source code -
        def interactive_config_manager(config_manager: ConfigManager) -> None:
        +
        def interactive_config_manager(config_manager: cm.ConfigManager) -> None:
             """
             Interactively manage tool configurations.
         
        diff --git a/docs/cli_tool_audit/policy.html b/docs/cli_tool_audit/policy.html
        index 07c8fc9..9e9977a 100644
        --- a/docs/cli_tool_audit/policy.html
        +++ b/docs/cli_tool_audit/policy.html
        @@ -30,15 +30,15 @@ 

        Module cli_tool_audit.policy

        """
         Apply various policies to the results of the tool checks.
         """
        -from cli_tool_audit.call_and_compatible import ToolCheckResult
        +import cli_tool_audit.models as models
         
         
        -def apply_policy(results: list[ToolCheckResult]) -> bool:
        +def apply_policy(results: list[models.ToolCheckResult]) -> bool:
             """
             Pretty print the results of the validation.
         
             Args:
        -        results (list[ToolCheckResult]): A list of ToolCheckResult objects.
        +        results (list[models.ToolCheckResult]): A list of ToolCheckResult objects.
         
             Returns:
                 bool: True if any of the tools failed validation, False otherwise.
        @@ -70,7 +70,7 @@ 

        Functions

        Pretty print the results of the validation.

        Args

        -
        results : list[ToolCheckResult]
        +
        results : list[models.ToolCheckResult]
        A list of ToolCheckResult objects.

        Returns

        @@ -82,12 +82,12 @@

        Returns

        Expand source code -
        def apply_policy(results: list[ToolCheckResult]) -> bool:
        +
        def apply_policy(results: list[models.ToolCheckResult]) -> bool:
             """
             Pretty print the results of the validation.
         
             Args:
        -        results (list[ToolCheckResult]): A list of ToolCheckResult objects.
        +        results (list[models.ToolCheckResult]): A list of ToolCheckResult objects.
         
             Returns:
                 bool: True if any of the tools failed validation, False otherwise.
        diff --git a/docs/cli_tool_audit/view_npm_stress_test.html b/docs/cli_tool_audit/view_npm_stress_test.html
        index 1ca6228..76b8deb 100644
        --- a/docs/cli_tool_audit/view_npm_stress_test.html
        +++ b/docs/cli_tool_audit/view_npm_stress_test.html
        @@ -41,8 +41,9 @@ 

        Module cli_tool_audit.view_npm_stress_test

        from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig def list_global_npm_executables() -> list[str]: @@ -82,7 +83,7 @@

        Module cli_tool_audit.view_npm_stress_test

        app_cmd = app + ".cmd" else: app_cmd = app - config = CliToolConfig(app_cmd) + config = models.CliToolConfig(app_cmd) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[app_cmd] = config @@ -100,10 +101,10 @@

        Module cli_tool_audit.view_npm_stress_test

        # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] @@ -193,7 +194,7 @@

        Args

        app_cmd = app + ".cmd" else: app_cmd = app - config = CliToolConfig(app_cmd) + config = models.CliToolConfig(app_cmd) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[app_cmd] = config @@ -211,10 +212,10 @@

        Args

        # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] diff --git a/docs/cli_tool_audit/view_pipx_stress_test.html b/docs/cli_tool_audit/view_pipx_stress_test.html index 8cce11e..1bd855b 100644 --- a/docs/cli_tool_audit/view_pipx_stress_test.html +++ b/docs/cli_tool_audit/view_pipx_stress_test.html @@ -40,16 +40,20 @@

        Module cli_tool_audit.view_pipx_stress_test

        from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig + # Dummy lock for switch to ProcessPoolExecutor -# class Dummy: -# def __enter__(self): -# pass -# -# def __exit__(self, exc_type, exc_value, traceback): -# pass +class DummyLock: + """For testing""" + + def __enter__(self): + """For testing""" + + def __exit__(self, exc_type, exc_value, traceback): + """For testing""" def get_pipx_list() -> Any: @@ -105,7 +109,7 @@

        Module cli_tool_audit.view_pipx_stress_test

        if app in ("yated.exe", "calcure.exe", "yated", "calcure", "dedlin.exe", "dedlin"): # These launch interactive process & then time out. continue - config = CliToolConfig(app) + config = models.CliToolConfig(app) config.version_switch = "--version" config.version = f">={expected_version}" cli_tools[app] = config @@ -124,10 +128,10 @@

        Module cli_tool_audit.view_pipx_stress_test

        # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: # Submit tasks to the executor - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] @@ -251,7 +255,7 @@

        Args

        if app in ("yated.exe", "calcure.exe", "yated", "calcure", "dedlin.exe", "dedlin"): # These launch interactive process & then time out. continue - config = CliToolConfig(app) + config = models.CliToolConfig(app) config.version_switch = "--version" config.version = f">={expected_version}" cli_tools[app] = config @@ -270,10 +274,10 @@

        Args

        # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: # Submit tasks to the executor - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] @@ -289,6 +293,28 @@

        Args

  • +

    Classes

    +
    +
    +class DummyLock +
    +
    +

    For testing

    +
    + +Expand source code + +
    class DummyLock:
    +    """For testing"""
    +
    +    def __enter__(self):
    +        """For testing"""
    +
    +    def __exit__(self, exc_type, exc_value, traceback):
    +        """For testing"""
    +
    +
    +
    diff --git a/docs/cli_tool_audit/view_venv_stress_test.html b/docs/cli_tool_audit/view_venv_stress_test.html index 41e1cf3..ef2bc5c 100644 --- a/docs/cli_tool_audit/view_venv_stress_test.html +++ b/docs/cli_tool_audit/view_venv_stress_test.html @@ -42,8 +42,9 @@

    Module cli_tool_audit.view_venv_stress_test

    # pylint: disable=no-name-in-module from whichcraft import which +import cli_tool_audit.call_and_compatible as call_and_compatible +import cli_tool_audit.models as models import cli_tool_audit.views as views -from cli_tool_audit.models import CliToolConfig def get_executables_in_venv(venv_path: str) -> list[str]: @@ -80,7 +81,7 @@

    Module cli_tool_audit.view_venv_stress_test

    cli_tools = {} count = 0 for executable in get_executables_in_venv(str(venv_dir)): - config = CliToolConfig(executable) + config = models.CliToolConfig(executable) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[executable] = config @@ -98,10 +99,10 @@

    Module cli_tool_audit.view_venv_stress_test

    # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] @@ -191,7 +192,7 @@

    Args

    cli_tools = {} count = 0 for executable in get_executables_in_venv(str(venv_dir)): - config = CliToolConfig(executable) + config = models.CliToolConfig(executable) config.version_switch = "--version" config.version = ">=0.0.0" cli_tools[executable] = config @@ -209,10 +210,10 @@

    Args

    # Submit tasks to the executor lock = Lock() # lock = Dummy() - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + disable = views.should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as progress_bar: futures = [ - executor.submit(views.check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] diff --git a/docs/cli_tool_audit/views.html b/docs/cli_tool_audit/views.html index 5268b1a..af6e7c1 100644 --- a/docs/cli_tool_audit/views.html +++ b/docs/cli_tool_audit/views.html @@ -35,6 +35,7 @@

    Module cli_tool_audit.views

    import logging import os from concurrent.futures import ThreadPoolExecutor +from pathlib import Path from threading import Lock from typing import Optional, Union @@ -43,11 +44,11 @@

    Module cli_tool_audit.views

    from prettytable.colortable import ColorTable, Themes from tqdm import tqdm +import cli_tool_audit.call_and_compatible as call_and_compatible import cli_tool_audit.config_reader as config_reader +import cli_tool_audit.json_utils as json_utils +import cli_tool_audit.models as models import cli_tool_audit.policy as policy -from cli_tool_audit.call_and_compatible import ToolCheckResult, check_tool_wrapper -from cli_tool_audit.json_utils import custom_json_serializer -from cli_tool_audit.models import CliToolConfig colorama.init(convert=True) @@ -55,22 +56,22 @@

    Module cli_tool_audit.views

    def validate( - file_path: str = "pyproject.toml", + file_path: Path = Path("pyproject.toml"), no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False, -) -> list[ToolCheckResult]: +) -> list[models.ToolCheckResult]: """ Validate the tools in the pyproject.toml file. Args: - file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". + file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". no_cache (bool, optional): If True, don't use the cache. Defaults to False. tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None. disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False. Returns: - list[ToolCheckResult]: A list of ToolCheckResult objects. + list[models.ToolCheckResult]: A list of ToolCheckResult objects. """ if tags is None: tags = [] @@ -79,22 +80,22 @@

    Module cli_tool_audit.views

    def process_tools( - cli_tools: dict[str, CliToolConfig], + cli_tools: dict[str, models.CliToolConfig], no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False, -) -> list[ToolCheckResult]: +) -> list[models.ToolCheckResult]: """ Process the tools from a dictionary of CliToolConfig objects. Args: - cli_tools (dict[str, CliToolConfig]): A dictionary of tool names and CliToolConfig objects. + cli_tools (dict[str, models.CliToolConfig]): A dictionary of tool names and CliToolConfig objects. no_cache (bool, optional): If True, don't use the cache. Defaults to False. tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None. disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False. Returns: - list[ToolCheckResult]: A list of ToolCheckResult objects. + list[models.ToolCheckResult]: A list of ToolCheckResult objects. """ if tags: print(tags) @@ -117,11 +118,11 @@

    Module cli_tool_audit.views

    # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: with ThreadPoolExecutor(max_workers=num_cpus) as executor: - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") or disable_progress_bar + disable = should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as pbar: # Submit tasks to the executor futures = [ - executor.submit(check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] @@ -133,8 +134,8 @@

    Module cli_tool_audit.views

    def report_from_pyproject_toml( - file_path: Optional[str] = "pyproject.toml", - config_as_dict: Optional[dict[str, CliToolConfig]] = None, + file_path: Optional[Path] = Path("pyproject.toml"), + config_as_dict: Optional[dict[str, models.CliToolConfig]] = None, exit_code_on_failure: bool = True, file_format: str = "table", no_cache: bool = False, @@ -145,8 +146,8 @@

    Module cli_tool_audit.views

    Report on the compatibility of the tools in the pyproject.toml file. Args: - file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". - config_as_dict (Optional[dict[str, CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. + file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". + config_as_dict (Optional[dict[str, models.CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. exit_code_on_failure (bool, optional): If True, exit with return value of 1 if validation fails. Defaults to True. file_format (str, optional): The format of the output. Defaults to "table". no_cache (bool, optional): If True, don't use the cache. Defaults to False. @@ -165,9 +166,9 @@

    Module cli_tool_audit.views

    results = process_tools(config_as_dict, no_cache, tags, disable_progress_bar=file_format != "table") elif file_path: # Handle config file searching. - if not os.path.exists(file_path): - one_up = os.path.join("..", file_path) - if os.path.exists(one_up): + if not file_path.exists(): + one_up = ".." / file_path + if one_up.exists(): file_path = one_up results = validate(file_path, no_cache=no_cache, tags=tags, disable_progress_bar=file_format != "table") else: @@ -181,9 +182,9 @@

    Module cli_tool_audit.views

    failed = policy.apply_policy(results) if file_format == "json": - print(json.dumps([result.__dict__ for result in results], indent=4, default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], indent=4, default=json_utils.custom_json_serializer)) elif file_format == "json-compact": - print(json.dumps([result.__dict__ for result in results], default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], default=json_utils.custom_json_serializer)) elif file_format == "xml": print("<results>") for result in results: @@ -223,13 +224,13 @@

    Module cli_tool_audit.views

    def pretty_print_results( - results: list[ToolCheckResult], truncate_long_versions: bool, include_docs: bool + results: list[models.ToolCheckResult], truncate_long_versions: bool, include_docs: bool ) -> Union[PrettyTable, ColorTable]: """ Pretty print the results of the validation. Args: - results (list[ToolCheckResult]): A list of ToolCheckResult objects. + results (list[models.ToolCheckResult]): A list of ToolCheckResult objects. truncate_long_versions (bool): If True, truncate long versions. Defaults to False. include_docs (bool): If True, include install command and install docs. Defaults to False. @@ -252,6 +253,11 @@

    Module cli_tool_audit.views

    found_version = result.found_version[0:25].strip() if result.found_version else "" else: found_version = result.found_version or "" + + try: + last_modified = result.last_modified.strftime("%m/%d/%y") if result.last_modified else "" + except ValueError: + last_modified = str(result.last_modified) row_data = [ result.tool, found_version or "", @@ -259,7 +265,7 @@

    Module cli_tool_audit.views

    result.desired_version or "", # "Yes" if result.is_compatible == "Compatible" else result.is_compatible, result.status() or "", - result.last_modified.strftime("%m/%d/%y") if result.last_modified else "", + last_modified, ] if include_docs: row_data.append(result.tool_config.install_command or "") @@ -278,6 +284,11 @@

    Module cli_tool_audit.views

    return table +def should_show_progress_bar(cli_tools) -> Optional[bool]: + disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") + return True if disable else None + + if __name__ == "__main__": report_from_pyproject_toml()
    @@ -296,7 +307,7 @@

    Functions

    Pretty print the results of the validation.

    Args

    -
    results : list[ToolCheckResult]
    +
    results : list[models.ToolCheckResult]
    A list of ToolCheckResult objects.
    truncate_long_versions : bool
    If True, truncate long versions. Defaults to False.
    @@ -313,13 +324,13 @@

    Returns

    Expand source code
    def pretty_print_results(
    -    results: list[ToolCheckResult], truncate_long_versions: bool, include_docs: bool
    +    results: list[models.ToolCheckResult], truncate_long_versions: bool, include_docs: bool
     ) -> Union[PrettyTable, ColorTable]:
         """
         Pretty print the results of the validation.
     
         Args:
    -        results (list[ToolCheckResult]): A list of ToolCheckResult objects.
    +        results (list[models.ToolCheckResult]): A list of ToolCheckResult objects.
             truncate_long_versions (bool): If True, truncate long versions. Defaults to False.
             include_docs (bool): If True, include install command and install docs. Defaults to False.
     
    @@ -342,6 +353,11 @@ 

    Returns

    found_version = result.found_version[0:25].strip() if result.found_version else "" else: found_version = result.found_version or "" + + try: + last_modified = result.last_modified.strftime("%m/%d/%y") if result.last_modified else "" + except ValueError: + last_modified = str(result.last_modified) row_data = [ result.tool, found_version or "", @@ -349,7 +365,7 @@

    Returns

    result.desired_version or "", # "Yes" if result.is_compatible == "Compatible" else result.is_compatible, result.status() or "", - result.last_modified.strftime("%m/%d/%y") if result.last_modified else "", + last_modified, ] if include_docs: row_data.append(result.tool_config.install_command or "") @@ -375,7 +391,7 @@

    Returns

    Process the tools from a dictionary of CliToolConfig objects.

    Args

    -
    cli_tools : dict[str, CliToolConfig]
    +
    cli_tools : dict[str, models.CliToolConfig]
    A dictionary of tool names and CliToolConfig objects.
    no_cache : bool, optional
    If True, don't use the cache. Defaults to False.
    @@ -386,7 +402,7 @@

    Args

    Returns

    -
    list[ToolCheckResult]
    +
    list[models.ToolCheckResult]
    A list of ToolCheckResult objects.
    @@ -394,22 +410,22 @@

    Returns

    Expand source code
    def process_tools(
    -    cli_tools: dict[str, CliToolConfig],
    +    cli_tools: dict[str, models.CliToolConfig],
         no_cache: bool = False,
         tags: Optional[list[str]] = None,
         disable_progress_bar: bool = False,
    -) -> list[ToolCheckResult]:
    +) -> list[models.ToolCheckResult]:
         """
         Process the tools from a dictionary of CliToolConfig objects.
     
         Args:
    -        cli_tools (dict[str, CliToolConfig]): A dictionary of tool names and CliToolConfig objects.
    +        cli_tools (dict[str, models.CliToolConfig]): A dictionary of tool names and CliToolConfig objects.
             no_cache (bool, optional): If True, don't use the cache. Defaults to False.
             tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None.
             disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False.
     
         Returns:
    -        list[ToolCheckResult]: A list of ToolCheckResult objects.
    +        list[models.ToolCheckResult]: A list of ToolCheckResult objects.
         """
         if tags:
             print(tags)
    @@ -432,11 +448,11 @@ 

    Returns

    # lock = Dummy() # with ProcessPoolExecutor(max_workers=num_cpus) as executor: with ThreadPoolExecutor(max_workers=num_cpus) as executor: - disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR") or disable_progress_bar + disable = should_show_progress_bar(cli_tools) with tqdm(total=len(cli_tools), disable=disable) as pbar: # Submit tasks to the executor futures = [ - executor.submit(check_tool_wrapper, (tool, config, lock, enable_cache)) + executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache)) for tool, config in cli_tools.items() ] results = [] @@ -448,15 +464,15 @@

    Returns

    -def report_from_pyproject_toml(file_path: Optional[str] = 'pyproject.toml', config_as_dict: Optional[dict[str, CliToolConfig]] = None, exit_code_on_failure: bool = True, file_format: str = 'table', no_cache: bool = False, tags: Optional[list[str]] = None, only_errors: bool = False) ‑> int +def report_from_pyproject_toml(file_path: Optional[pathlib.Path] = WindowsPath('pyproject.toml'), config_as_dict: Optional[dict[str, CliToolConfig]] = None, exit_code_on_failure: bool = True, file_format: str = 'table', no_cache: bool = False, tags: Optional[list[str]] = None, only_errors: bool = False) ‑> int

    Report on the compatibility of the tools in the pyproject.toml file.

    Args

    -
    file_path : str, optional
    +
    file_path : Path, optional
    The path to the pyproject.toml file. Defaults to "pyproject.toml".
    -
    config_as_dict : Optional[dict[str, CliToolConfig]], optional
    +
    config_as_dict : Optional[dict[str, models.CliToolConfig]], optional
    A dictionary of tool names and CliToolConfig objects. Defaults to None.
    exit_code_on_failure : bool, optional
    If True, exit with return value of 1 if validation fails. Defaults to True.
    @@ -479,8 +495,8 @@

    Returns

    Expand source code
    def report_from_pyproject_toml(
    -    file_path: Optional[str] = "pyproject.toml",
    -    config_as_dict: Optional[dict[str, CliToolConfig]] = None,
    +    file_path: Optional[Path] = Path("pyproject.toml"),
    +    config_as_dict: Optional[dict[str, models.CliToolConfig]] = None,
         exit_code_on_failure: bool = True,
         file_format: str = "table",
         no_cache: bool = False,
    @@ -491,8 +507,8 @@ 

    Returns

    Report on the compatibility of the tools in the pyproject.toml file. Args: - file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". - config_as_dict (Optional[dict[str, CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. + file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml". + config_as_dict (Optional[dict[str, models.CliToolConfig]], optional): A dictionary of tool names and CliToolConfig objects. Defaults to None. exit_code_on_failure (bool, optional): If True, exit with return value of 1 if validation fails. Defaults to True. file_format (str, optional): The format of the output. Defaults to "table". no_cache (bool, optional): If True, don't use the cache. Defaults to False. @@ -511,9 +527,9 @@

    Returns

    results = process_tools(config_as_dict, no_cache, tags, disable_progress_bar=file_format != "table") elif file_path: # Handle config file searching. - if not os.path.exists(file_path): - one_up = os.path.join("..", file_path) - if os.path.exists(one_up): + if not file_path.exists(): + one_up = ".." / file_path + if one_up.exists(): file_path = one_up results = validate(file_path, no_cache=no_cache, tags=tags, disable_progress_bar=file_format != "table") else: @@ -527,9 +543,9 @@

    Returns

    failed = policy.apply_policy(results) if file_format == "json": - print(json.dumps([result.__dict__ for result in results], indent=4, default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], indent=4, default=json_utils.custom_json_serializer)) elif file_format == "json-compact": - print(json.dumps([result.__dict__ for result in results], default=custom_json_serializer)) + print(json.dumps([result.__dict__ for result in results], default=json_utils.custom_json_serializer)) elif file_format == "xml": print("<results>") for result in results: @@ -568,14 +584,28 @@

    Returns

    return 0
    +
    +def should_show_progress_bar(cli_tools) ‑> Optional[bool] +
    +
    +
    +
    + +Expand source code + +
    def should_show_progress_bar(cli_tools) -> Optional[bool]:
    +    disable = len(cli_tools) < 5 or os.environ.get("CI") or os.environ.get("NO_COLOR")
    +    return True if disable else None
    +
    +
    -def validate(file_path: str = 'pyproject.toml', no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False) ‑> list[ToolCheckResult] +def validate(file_path: pathlib.Path = WindowsPath('pyproject.toml'), no_cache: bool = False, tags: Optional[list[str]] = None, disable_progress_bar: bool = False) ‑> list[ToolCheckResult]

    Validate the tools in the pyproject.toml file.

    Args

    -
    file_path : str, optional
    +
    file_path : Path, optional
    The path to the pyproject.toml file. Defaults to "pyproject.toml".
    no_cache : bool, optional
    If True, don't use the cache. Defaults to False.
    @@ -586,7 +616,7 @@

    Args

    Returns

    -
    list[ToolCheckResult]
    +
    list[models.ToolCheckResult]
    A list of ToolCheckResult objects.
    @@ -594,22 +624,22 @@

    Returns

    Expand source code
    def validate(
    -    file_path: str = "pyproject.toml",
    +    file_path: Path = Path("pyproject.toml"),
         no_cache: bool = False,
         tags: Optional[list[str]] = None,
         disable_progress_bar: bool = False,
    -) -> list[ToolCheckResult]:
    +) -> list[models.ToolCheckResult]:
         """
         Validate the tools in the pyproject.toml file.
     
         Args:
    -        file_path (str, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml".
    +        file_path (Path, optional): The path to the pyproject.toml file. Defaults to "pyproject.toml".
             no_cache (bool, optional): If True, don't use the cache. Defaults to False.
             tags (Optional[list[str]], optional): Only check tools with these tags. Defaults to None.
             disable_progress_bar (bool, optional): If True, disable the progress bar. Defaults to False.
     
         Returns:
    -        list[ToolCheckResult]: A list of ToolCheckResult objects.
    +        list[models.ToolCheckResult]: A list of ToolCheckResult objects.
         """
         if tags is None:
             tags = []
    @@ -638,6 +668,7 @@ 

    Index

  • pretty_print_results
  • process_tools
  • report_from_pyproject_toml
  • +
  • should_show_progress_bar
  • validate
  • diff --git a/gen_hypothesis.py b/gen_hypothesis.py new file mode 100644 index 0000000..49defb5 --- /dev/null +++ b/gen_hypothesis.py @@ -0,0 +1,47 @@ +import pkgutil +import subprocess +from pathlib import Path +from typing import Any +import cli_tool_audit + +def get_submodules(module: Any) -> list[str]: + """ + Discovers all submodules in the given module. + + Args: + module: The module to discover submodules in. + + Returns: + A list of submodule names. + """ + package_path = module.__path__ + return [name for _, name, _ in pkgutil.iter_modules(package_path)] + + +def generate_hypothesis_tests(module: Any) -> None: + """ + Generates and executes Hypothesis test commands for each submodule in the given module. + + Args: + module: The module to generate tests for. + """ + submodules = get_submodules(module) + base_command = ['hypothesis', 'write', '--style', 'pytest', '--annotate'] + output_dir = Path('tests/test_hypothesis/') + output_dir.mkdir(parents=True, exist_ok=True) + + for submodule in submodules: + module_name = f'{module.__name__}.{submodule}' + test_file_name = f'test_{submodule}.py' + output_path = output_dir / test_file_name + command = base_command + [module_name] + + with open(output_path, 'w') as file: + print(command) + subprocess.run(command, stdout=file, text=True, check=True) + + +# Example usage: + +if __name__ == '__main__': + generate_hypothesis_tests(cli_tool_audit) diff --git a/gen_hypothesis_dupes.py b/gen_hypothesis_dupes.py new file mode 100644 index 0000000..23d114c --- /dev/null +++ b/gen_hypothesis_dupes.py @@ -0,0 +1,33 @@ +import re +from collections import Counter +from pathlib import Path + + +def count_test_functions(test_dir: str) -> None: + """ + Counts the occurrences of each test function in .py files within the specified directory. + + Args: + test_dir: The directory containing the test files. + """ + test_files = Path(test_dir).glob('test_*.py') + test_function_pattern = re.compile(r'def (test_[a-zA-Z0-9_]*\()') + + function_counts = Counter() + + for test_file in test_files: + with open(test_file) as file: + content = file.read() + function_names = test_function_pattern.findall(content) + function_names = [name[:-1] for name in function_names] # Remove trailing '(' from function names + function_counts.update(function_names) + + # Filter and print functions that are tested more than once + for function_name, count in function_counts.items(): + if count > 1: + print(f"{function_name}: {count} times") + + +if __name__ == '__main__': + # Example usage: + count_test_functions('tests/test_hypothesis') diff --git a/poetry.lock b/poetry.lock index 6957fa4..db426ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -522,6 +522,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.7" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "filelock" version = "3.13.1" @@ -1334,6 +1348,60 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-randomly" +version = "3.15.0" +description = "Pytest plugin to randomly order tests and control random.seed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, + {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +pytest = "*" + +[[package]] +name = "pytest-sugar" +version = "0.9.7" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +files = [ + {file = "pytest-sugar-0.9.7.tar.gz", hash = "sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46"}, + {file = "pytest_sugar-0.9.7-py2.py3-none-any.whl", hash = "sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + +[[package]] +name = "pytest-xdist" +version = "3.5.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1515,6 +1583,20 @@ files = [ [package.extras] widechars = ["wcwidth"] +[[package]] +name = "termcolor" +version = "2.4.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.8" +files = [ + {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, + {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "toml" version = "0.10.2" @@ -1617,6 +1699,17 @@ files = [ {file = "types_toml-0.10.8.7-py3-none-any.whl", hash = "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"}, ] +[[package]] +name = "types-tqdm" +version = "4.66.0.20240106" +description = "Typing stubs for tqdm" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-tqdm-4.66.0.20240106.tar.gz", hash = "sha256:7acf4aade5bad3ded76eb829783f9961b1c2187948eaa6dd1ae8644dff95a938"}, + {file = "types_tqdm-4.66.0.20240106-py3-none-any.whl", hash = "sha256:7459b0f441b969735685645a5d8480f7912b10d05ab45f99a2db8a8e45cb550b"}, +] + [[package]] name = "typing-extensions" version = "4.9.0" @@ -1807,4 +1900,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "f013834106f58c72d5e3135ef3a7cabbaaa9f8e21be0d04e19ac6c3663cc9de1" +content-hash = "1657fc6230fe29357fb04e8e9fda0dc16e295669b0d54828e9d690bd266cac41" diff --git a/private_dictionary.txt b/private_dictionary.txt index 4b33087..e63b344 100644 --- a/private_dictionary.txt +++ b/private_dictionary.txt @@ -28,4 +28,9 @@ deserializable dataclasses schemas CHANGELOG -UseCases \ No newline at end of file +UseCases +PrettyTable +CliToolConfig +ToolAvailabilityResult +SchemaType +EnvironmentVariables \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7db0c70..1976e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cli_tool_audit" -version = "3.0.0" +version = "3.0.1" description = "Audit for existence and version number of cli tools." authors = ["Matthew Martin "] keywords = ["cli tooling", "version numbers", ] @@ -33,7 +33,7 @@ documentation = "https://matthewdeanmartin.github.io/cli_tool_audit/cli_tool_aud [tool.poetry.urls] "Bug Tracker" = "https://github.com/matthewdeanmartin/cli_tool_audit/issues" -"Change Log" = "https://github.com/matthewdeanmartin/cli_tool_audit/blob/main/CHANGES.md" +"Change Log" = "https://github.com/matthewdeanmartin/cli_tool_audit/blob/main/CHANGELOG.md" [tool.poetry.scripts] cli_tool_audit = 'cli_tool_audit.__main__:main' @@ -69,6 +69,10 @@ tomlkit = ">=0.12.3" black = ">=23.11.0" pytest = ">=7.4.3" pytest-cov = ">=4.1.0" +pytest-xdist =">=3.5.0" +pytest-randomly=">=3.15.0" +pytest-sugar =">=0.9.7" + hypothesis = ">=6.96.0" tox = "*" pylint = ">=3.0.2" @@ -78,6 +82,7 @@ ruff = ">=0.1.9" mypy = ">=1.8.0" types-toml = "*" types-colorama = "*" +types-tqdm = "*" # more testing # pytest-snapshot = ">=0.9.0" diff --git a/tests/test_cache.py b/tests/test_cache.py index e630966..645e141 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -3,8 +3,8 @@ import pytest -from cli_tool_audit.audit_cache import AuditFacade, CliToolConfig, ToolCheckResult, custom_json_deserializer -from cli_tool_audit.models import SchemaType +from cli_tool_audit.audit_cache import AuditFacade, custom_json_deserializer +from cli_tool_audit.models import CliToolConfig, SchemaType, ToolCheckResult @pytest.fixture diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 88874df..e63bfb0 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -22,7 +22,7 @@ def config_file(tmp_path): def test_read_config(config_file): - config_manager = ConfigManager(str(config_file)) + config_manager = ConfigManager(config_file) config_manager.read_config() assert config_manager.tools["foobar"].version == ">=1.0.0" assert config_manager.tools["foo"].version_switch == "version" @@ -30,21 +30,21 @@ def test_read_config(config_file): def test_create_tool_config(config_file): - config_manager = ConfigManager(str(config_file)) + config_manager = ConfigManager(config_file) config_manager.create_tool_config("newtool", {"version": ">=2.0.0"}) assert "newtool" in config_manager.tools assert config_manager.tools["newtool"].version == ">=2.0.0" def test_update_tool_config(config_file): - config_manager = ConfigManager(str(config_file)) + config_manager = ConfigManager(config_file) # config_manager.read_config() config_manager.update_tool_config("foobar", {"version": ">=1.1.0"}) assert config_manager.tools["foobar"].version == ">=1.1.0" def test_delete_tool_config(config_file): - config_manager = ConfigManager(str(config_file)) + config_manager = ConfigManager(config_file) # config_manager.read_config() config_manager.delete_tool_config("foobar") assert "foobar" not in config_manager.tools diff --git a/tests/test_hypothesis/test___main__.py b/tests/test_hypothesis/test___main__.py new file mode 100644 index 0000000..f2dbc14 --- /dev/null +++ b/tests/test_hypothesis/test___main__.py @@ -0,0 +1,77 @@ +# # This test code was written by the `hypothesis.extra.ghostwriter` module +# # and is provided under the Creative Commons Zero public domain dedication. +# +# import argparse +# import cli_tool_audit.__main__ +# import collections.abc +# import typing +# from argparse import ArgumentParser, Namespace +# from hypothesis import given, strategies as st +# +# # TODO: replace st.nothing() with appropriate strategies +# +# +# @given(interactive_parser=st.nothing()) +# def test_fuzz_add_config_to_subparser(interactive_parser) -> None: +# cli_tool_audit.add_config_to_subparser(interactive_parser=interactive_parser) +# +# +# @given(audit_parser=st.nothing()) +# def test_fuzz_add_formats(audit_parser) -> None: +# cli_tool_audit.add_formats(audit_parser=audit_parser) +# +# +# @given(parser=st.nothing()) +# def test_fuzz_add_schema_argument(parser) -> None: +# cli_tool_audit.add_schema_argument(parser=parser) +# +# +# @given(parser=st.from_type(argparse.ArgumentParser)) +# def test_fuzz_add_update_args(parser: argparse.ArgumentParser) -> None: +# cli_tool_audit.add_update_args(parser=parser) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_audit(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_audit(args=args) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_create(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_create(args=args) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_delete(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_delete(args=args) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_interactive(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_interactive(args=args) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_read(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_read(args=args) +# +# +# @given(args=st.nothing()) +# def test_fuzz_handle_single(args) -> None: +# cli_tool_audit.handle_single(args=args) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_handle_update(args: argparse.Namespace) -> None: +# cli_tool_audit.handle_update(args=args) +# +# +# @given(argv=st.one_of(st.none(), st.lists(st.text()))) +# def test_fuzz_main(argv: typing.Optional[collections.abc.Sequence[str]]) -> None: +# cli_tool_audit.main(argv=argv) +# +# +# @given(args=st.from_type(argparse.Namespace)) +# def test_fuzz_reduce_args_tool_cli_tool_config_args(args: argparse.Namespace) -> None: +# cli_tool_audit.reduce_args_tool_cli_tool_config_args(args=args) +# diff --git a/tests/test_hypothesis/test_audit_cache.py b/tests/test_hypothesis/test_audit_cache.py new file mode 100644 index 0000000..fc6ab92 --- /dev/null +++ b/tests/test_hypothesis/test_audit_cache.py @@ -0,0 +1,29 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +import cli_tool_audit.audit_cache + +# +# @given(cache_dir=st.one_of(st.none(), st.text())) +# def test_fuzz_AuditFacade(cache_dir: typing.Optional[str]) -> None: +# cli_tool_audit.audit_cache.AuditFacade(cache_dir=cache_dir) + + +@pytest.mark.parametrize("cache_dir", [None, "subdir"]) +@given(data=st.none()) # why do we need data? +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_fuzz_AuditFacade(tmp_path, cache_dir, data): + if cache_dir is not None: + # Create a subdirectory or file in the temporary directory + cache_dir = tmp_path / cache_dir + cache_dir.mkdir(exist_ok=True) + else: + cache_dir = None + + # Call the function with the generated path or None + cli_tool_audit.audit_cache.AuditFacade(cache_dir=cache_dir) diff --git a/tests/test_hypothesis/test_audit_manager.py b/tests/test_hypothesis/test_audit_manager.py new file mode 100644 index 0000000..124d630 --- /dev/null +++ b/tests/test_hypothesis/test_audit_manager.py @@ -0,0 +1,32 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.audit_manager + + +@given(version_string=st.sampled_from(["Found", "Not Found"])) +def test_fuzz_ExistenceVersionChecker(version_string: str) -> None: + cli_tool_audit.audit_manager.ExistenceVersionChecker(version_string=version_string) + + +@given(version_string=st.sampled_from(["0.1.2", "2.3.4"])) +def test_fuzz_Pep440VersionChecker(version_string: str) -> None: + cli_tool_audit.audit_manager.Pep440VersionChecker(version_string=version_string) + + +@given(version_string=st.text()) +def test_fuzz_SemVerChecker(version_string: str) -> None: + cli_tool_audit.audit_manager.SemVerChecker(version_string=version_string) + + +@given(version_string=st.text()) +def test_fuzz_SnapshotVersionChecker(version_string: str) -> None: + cli_tool_audit.audit_manager.SnapshotVersionChecker(version_string=version_string) + + +@given(is_compatible=st.booleans(), clean_format=st.text()) +def test_fuzz_VersionResult(is_compatible: bool, clean_format: str) -> None: + cli_tool_audit.audit_manager.VersionResult(is_compatible=is_compatible, clean_format=clean_format) diff --git a/tests/test_hypothesis/test_call_and_compatible.py b/tests/test_hypothesis/test_call_and_compatible.py new file mode 100644 index 0000000..182cec4 --- /dev/null +++ b/tests/test_hypothesis/test_call_and_compatible.py @@ -0,0 +1,18 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + + +# needs to be some real tools, not just random strings. +# @given( +# tool_info=st.from_type( +# tuple[str, cli_tool_audit.models.CliToolConfig, DummyLock, bool] +# ) +# ) +# def test_fuzz_check_tool_wrapper( +# tool_info: tuple[ +# str, cli_tool_audit.models.CliToolConfig, DummyLock, bool +# ] +# ) -> None: +# cli_tool_audit.call_and_compatible.check_tool_wrapper(tool_info=tool_info) diff --git a/tests/test_hypothesis/test_call_tools.py b/tests/test_hypothesis/test_call_tools.py new file mode 100644 index 0000000..18d721b --- /dev/null +++ b/tests/test_hypothesis/test_call_tools.py @@ -0,0 +1,30 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +import cli_tool_audit.call_tools +import cli_tool_audit.known_switches as ks +import cli_tool_audit.models + +# +# @given( +# tool_name=st.text(), +# schema=st.sampled_from(cli_tool_audit.models.SchemaType), +# version_switch=st.text(), +# ) +# def test_fuzz_check_tool_availability( +# tool_name: str, schema: cli_tool_audit.models.SchemaType, version_switch: str +# ) -> None: +# cli_tool_audit.call_tools.check_tool_availability( +# tool_name=tool_name, schema=schema, version_switch=version_switch +# ) + +ks.KNOWN_SWITCHES = list(ks.KNOWN_SWITCHES.keys()) + + +@given(tool_name=st.sampled_from(ks.KNOWN_SWITCHES)) +@settings(suppress_health_check=[HealthCheck.too_slow]) +def test_fuzz_get_command_last_modified_date(tool_name: str) -> None: + cli_tool_audit.call_tools.get_command_last_modified_date(tool_name=tool_name) diff --git a/tests/test_hypothesis/test_compatibility.py b/tests/test_hypothesis/test_compatibility.py new file mode 100644 index 0000000..a054d42 --- /dev/null +++ b/tests/test_hypothesis/test_compatibility.py @@ -0,0 +1,19 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +import typing + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.compatibility + + +@given(desired_version=st.text(), found_version=st.one_of(st.none(), st.text())) +def test_fuzz_check_compatibility(desired_version: str, found_version: typing.Optional[str]) -> None: + cli_tool_audit.compatibility.check_compatibility(desired_version=desired_version, found_version=found_version) + + +@given(pattern=st.text()) +def test_fuzz_split_version_match_pattern(pattern: str) -> None: + cli_tool_audit.compatibility.split_version_match_pattern(pattern=pattern) diff --git a/tests/test_hypothesis/test_compatibility_complex.py b/tests/test_hypothesis/test_compatibility_complex.py new file mode 100644 index 0000000..c8b7466 --- /dev/null +++ b/tests/test_hypothesis/test_compatibility_complex.py @@ -0,0 +1,62 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.compatibility_complex + +# +# @given( +# desired_version=st.text(), +# found_semversion=st.builds( +# Version, +# build=st.one_of( +# st.none(), st.one_of(st.none(), st.integers(), st.binary(), st.text()) +# ), +# major=st.one_of( +# st.booleans(), +# st.integers(), +# st.floats(), +# st.uuids(), +# st.decimals(), +# st.from_regex("\\A-?\\d+\\Z").filter(functools.partial(can_cast, int)), +# ), +# minor=st.one_of( +# st.just(0), +# st.one_of( +# st.booleans(), +# st.integers(), +# st.floats(), +# st.uuids(), +# st.decimals(), +# st.from_regex("\\A-?\\d+\\Z").filter(functools.partial(can_cast, int)), +# ), +# ), +# patch=st.one_of( +# st.just(0), +# st.one_of( +# st.booleans(), +# st.integers(), +# st.floats(), +# st.uuids(), +# st.decimals(), +# st.from_regex("\\A-?\\d+\\Z").filter(functools.partial(can_cast, int)), +# ), +# ), +# prerelease=st.one_of( +# st.none(), st.one_of(st.none(), st.integers(), st.binary(), st.text()) +# ), +# ), +# ) +# def test_fuzz_check_range_compatibility( +# desired_version: str, found_semversion: semver.Version +# ) -> None: +# cli_tool_audit.compatibility_complex.check_range_compatibility( +# desired_version=desired_version, found_semversion=found_semversion +# ) + + +@given(version_range=st.sampled_from(["^0.1.2", "~2.3.4", "*"])) +def test_fuzz_convert_version_range(version_range: str) -> None: + cli_tool_audit.compatibility_complex.convert_version_range(version_range=version_range) diff --git a/tests/test_hypothesis/test_config_manager.py b/tests/test_hypothesis/test_config_manager.py new file mode 100644 index 0000000..c8933ff --- /dev/null +++ b/tests/test_hypothesis/test_config_manager.py @@ -0,0 +1,17 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + +# Random path tests don't mean anything? +# @pytest.mark.parametrize('config_path', [None, 'subdir']) +# @given(data=st.data()) +# def test_fuzz_ConfigManager(tmp_path, config_path: str, data) -> None: +# if config_path is not None: +# # Create a subdirectory or file in the temporary directory +# config_dir = tmp_path / config_path +# config_dir.mkdir(exist_ok=True) +# else: +# config_dir = None +# cli_tool_audit.config_manager.ConfigManager(config_path=config_path) +# diff --git a/tests/test_hypothesis/test_config_reader.py b/tests/test_hypothesis/test_config_reader.py new file mode 100644 index 0000000..0497b8a --- /dev/null +++ b/tests/test_hypothesis/test_config_reader.py @@ -0,0 +1,14 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from pathlib import Path + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.config_reader + + +@given(file_path=st.sampled_from([Path("."), Path("../pyproject.toml"), Path("audit.toml")])) +def test_fuzz_read_config(file_path: Path) -> None: + cli_tool_audit.config_reader.read_config(file_path=file_path) diff --git a/tests/test_hypothesis/test_freeze.py b/tests/test_hypothesis/test_freeze.py new file mode 100644 index 0000000..a754ede --- /dev/null +++ b/tests/test_hypothesis/test_freeze.py @@ -0,0 +1,40 @@ +# # This test code was written by the `hypothesis.extra.ghostwriter` module +# # and is provided under the Creative Commons Zero public domain dedication. +# +# import cli_tool_audit.freeze +# import cli_tool_audit.models +# from hypothesis import given, strategies as st +# +# +# @given( +# tool_names=st.lists(st.text()), +# schema=st.sampled_from(cli_tool_audit.models.SchemaType), +# ) +# def test_fuzz_freeze_requirements( +# tool_names: list[str], schema: cli_tool_audit.models.SchemaType +# ) -> None: +# cli_tool_audit.freeze.freeze_requirements(tool_names=tool_names, schema=schema) +# +# +# @given( +# tool_names=st.lists(st.text()), +# config_path=st.text(), +# schema=st.sampled_from(cli_tool_audit.models.SchemaType), +# ) +# def test_fuzz_freeze_to_config( +# tool_names: list[str], config_path: str, schema: cli_tool_audit.models.SchemaType +# ) -> None: +# cli_tool_audit.freeze.freeze_to_config( +# tool_names=tool_names, config_path=config_path, schema=schema +# ) +# +# +# @given( +# tool_names=st.lists(st.text()), +# schema=st.sampled_from(cli_tool_audit.models.SchemaType), +# ) +# def test_fuzz_freeze_to_screen( +# tool_names: list[str], schema: cli_tool_audit.models.SchemaType +# ) -> None: +# cli_tool_audit.freeze.freeze_to_screen(tool_names=tool_names, schema=schema) +# diff --git a/tests/test_hypothesis/test_interactive.py b/tests/test_hypothesis/test_interactive.py new file mode 100644 index 0000000..db13aeb --- /dev/null +++ b/tests/test_hypothesis/test_interactive.py @@ -0,0 +1,10 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + +# @given(config_manager=st.builds(ConfigManager, config_path=st.text())) +# def test_fuzz_interactive_config_manager( +# config_manager: cli_tool_audit.config_manager.ConfigManager, +# ) -> None: +# cli_tool_audit.interactive.interactive_config_manager(config_manager=config_manager) diff --git a/tests/test_hypothesis/test_json_utils.py b/tests/test_hypothesis/test_json_utils.py new file mode 100644 index 0000000..93a2e4f --- /dev/null +++ b/tests/test_hypothesis/test_json_utils.py @@ -0,0 +1,13 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. +import typing + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.json_utils + + +@given(o=st.sampled_from(cli_tool_audit.models.SchemaType)) +def test_fuzz_custom_json_serializer(o: typing.Any) -> None: + cli_tool_audit.json_utils.custom_json_serializer(o=o) diff --git a/tests/test_hypothesis/test_known_switches.py b/tests/test_hypothesis/test_known_switches.py new file mode 100644 index 0000000..9912ab1 --- /dev/null +++ b/tests/test_hypothesis/test_known_switches.py @@ -0,0 +1,2 @@ +# Found no testable functions in +# set() from (,) diff --git a/tests/test_hypothesis/test_logging_config.py b/tests/test_hypothesis/test_logging_config.py new file mode 100644 index 0000000..86813a2 --- /dev/null +++ b/tests/test_hypothesis/test_logging_config.py @@ -0,0 +1,12 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.logging_config + + +@given(level=st.text()) +def test_fuzz_generate_config(level: str) -> None: + cli_tool_audit.logging_config.generate_config(level=level) diff --git a/tests/test_hypothesis/test_models.py b/tests/test_hypothesis/test_models.py new file mode 100644 index 0000000..d254ff5 --- /dev/null +++ b/tests/test_hypothesis/test_models.py @@ -0,0 +1,117 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +import datetime +import typing + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.models +from cli_tool_audit.models import CliToolConfig + + +@given( + name=st.text(), + version=st.one_of(st.none(), st.text()), + version_switch=st.one_of(st.none(), st.text()), + schema=st.one_of(st.none(), st.sampled_from(cli_tool_audit.models.SchemaType)), + if_os=st.one_of(st.none(), st.text()), + tags=st.one_of(st.none(), st.lists(st.text())), + install_command=st.one_of(st.none(), st.text()), + install_docs=st.one_of(st.none(), st.text()), +) +def test_fuzz_CliToolConfig( + name: str, + version: typing.Optional[str], + version_switch: typing.Optional[str], + schema: typing.Optional[cli_tool_audit.models.SchemaType], + if_os: typing.Optional[str], + tags: typing.Optional[list[str]], + install_command: typing.Optional[str], + install_docs: typing.Optional[str], +) -> None: + cli_tool_audit.models.CliToolConfig( + name=name, + version=version, + version_switch=version_switch, + schema=schema, + if_os=if_os, + tags=tags, + install_command=install_command, + install_docs=install_docs, + ) + + +@given( + is_available=st.booleans(), + is_broken=st.booleans(), + version=st.one_of(st.none(), st.text()), + last_modified=st.one_of(st.none(), st.datetimes()), +) +def test_fuzz_ToolAvailabilityResult( + is_available: bool, + is_broken: bool, + version: typing.Optional[str], + last_modified: typing.Optional[datetime.datetime], +) -> None: + cli_tool_audit.models.ToolAvailabilityResult( + is_available=is_available, + is_broken=is_broken, + version=version, + last_modified=last_modified, + ) + + +@given( + tool=st.text(), + desired_version=st.text(), + is_needed_for_os=st.booleans(), + is_available=st.booleans(), + is_snapshot=st.booleans(), + found_version=st.one_of(st.none(), st.text()), + parsed_version=st.one_of(st.none(), st.text()), + is_compatible=st.text(), + is_broken=st.booleans(), + last_modified=st.one_of(st.none(), st.datetimes()), + tool_config=st.builds( + CliToolConfig, + if_os=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_command=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_docs=st.one_of(st.none(), st.one_of(st.none(), st.text())), + name=st.text(), + schema=st.one_of( + st.none(), + st.one_of(st.none(), st.sampled_from(cli_tool_audit.models.SchemaType)), + ), + tags=st.one_of(st.none(), st.one_of(st.none(), st.lists(st.text()))), + version=st.one_of(st.none(), st.one_of(st.none(), st.text())), + version_switch=st.one_of(st.none(), st.one_of(st.none(), st.text())), + ), +) +def test_fuzz_ToolCheckResult( + tool: str, + desired_version: str, + is_needed_for_os: bool, + is_available: bool, + is_snapshot: bool, + found_version: typing.Optional[str], + parsed_version: typing.Optional[str], + is_compatible: str, + is_broken: bool, + last_modified: typing.Optional[datetime.datetime], + tool_config: cli_tool_audit.models.CliToolConfig, +) -> None: + cli_tool_audit.models.ToolCheckResult( + tool=tool, + desired_version=desired_version, + is_needed_for_os=is_needed_for_os, + is_available=is_available, + is_snapshot=is_snapshot, + found_version=found_version, + parsed_version=parsed_version, + is_compatible=is_compatible, + is_broken=is_broken, + last_modified=last_modified, + tool_config=tool_config, + ) diff --git a/tests/test_hypothesis/test_policy.py b/tests/test_hypothesis/test_policy.py new file mode 100644 index 0000000..91ff427 --- /dev/null +++ b/tests/test_hypothesis/test_policy.py @@ -0,0 +1,46 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.models +import cli_tool_audit.policy +from cli_tool_audit.models import CliToolConfig, ToolCheckResult + + +@given( + results=st.lists( + st.builds( + ToolCheckResult, + desired_version=st.text(), + found_version=st.one_of(st.none(), st.text()), + is_available=st.booleans(), + is_broken=st.booleans(), + is_compatible=st.text(), + is_needed_for_os=st.booleans(), + is_snapshot=st.booleans(), + last_modified=st.one_of(st.none(), st.datetimes()), + parsed_version=st.one_of(st.none(), st.text()), + tool=st.text(), + tool_config=st.builds( + CliToolConfig, + if_os=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_command=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_docs=st.one_of(st.none(), st.one_of(st.none(), st.text())), + name=st.text(), + schema=st.one_of( + st.none(), + st.one_of(st.none(), st.sampled_from(cli_tool_audit.models.SchemaType)), + ), + tags=st.one_of(st.none(), st.one_of(st.none(), st.lists(st.text()))), + version=st.one_of(st.none(), st.one_of(st.none(), st.text())), + version_switch=st.one_of(st.none(), st.one_of(st.none(), st.text())), + ), + ) + ) +) +def test_fuzz_apply_policy( + results: list[cli_tool_audit.models.ToolCheckResult], +) -> None: + cli_tool_audit.policy.apply_policy(results=results) diff --git a/tests/test_hypothesis/test_version_parsing.py b/tests/test_hypothesis/test_version_parsing.py index 304cbf5..a8ad5c6 100644 --- a/tests/test_hypothesis/test_version_parsing.py +++ b/tests/test_hypothesis/test_version_parsing.py @@ -1,15 +1,17 @@ # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. +import packaging.version from hypothesis import given from hypothesis import strategies as st +from packaging.version import Version import cli_tool_audit.version_parsing -# Random strings are not valid versions, duh. -# @given(version=st.builds(Version, version=st.text())) -# def test_fuzz_convert2semver(version: packaging.version.Version) -> None: -# cli_tool_audit.version_parsing.convert2semver(version=version) + +@given(version=st.builds(Version, version=st.sampled_from(["0.1.2", "2.3.4"]))) +def test_fuzz_convert2semver(version: packaging.version.Version) -> None: + cli_tool_audit.version_parsing.convert2semver(version=version) @given(input_string=st.text()) diff --git a/tests/test_hypothesis/test_view_npm_stress_test.py b/tests/test_hypothesis/test_view_npm_stress_test.py new file mode 100644 index 0000000..569c9cd --- /dev/null +++ b/tests/test_hypothesis/test_view_npm_stress_test.py @@ -0,0 +1,9 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + +# @given(max_count=st.integers()) +# def test_fuzz_report_for_npm_tools(max_count: int) -> None: +# cli_tool_audit.view_npm_stress_test.report_for_npm_tools(max_count=max_count) +# diff --git a/tests/test_hypothesis/test_view_pipx_stress_test.py b/tests/test_hypothesis/test_view_pipx_stress_test.py new file mode 100644 index 0000000..5e2b200 --- /dev/null +++ b/tests/test_hypothesis/test_view_pipx_stress_test.py @@ -0,0 +1,15 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + + +# @given(pipx_data=st.from_type(dict[str, typing.Any])) +# def test_fuzz_extract_apps(pipx_data: dict[str, typing.Any]) -> None: +# cli_tool_audit.view_pipx_stress_test.extract_apps(pipx_data=pipx_data) + +# +# @given(max_count=st.integers()) +# def test_fuzz_report_for_pipx_tools(max_count: int) -> None: +# cli_tool_audit.view_pipx_stress_test.report_for_pipx_tools(max_count=max_count) +# diff --git a/tests/test_hypothesis/test_view_venv_stress_test.py b/tests/test_hypothesis/test_view_venv_stress_test.py new file mode 100644 index 0000000..8ab7073 --- /dev/null +++ b/tests/test_hypothesis/test_view_venv_stress_test.py @@ -0,0 +1,14 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + + +# @given(venv_path=".") +# def test_fuzz_get_executables_in_venv(venv_path: str) -> None: +# cli_tool_audit.view_venv_stress_test.get_executables_in_venv(venv_path=venv_path) + + +# @given(max_count=st.integers()) +# def test_fuzz_report_for_venv_tools(max_count: int) -> None: +# cli_tool_audit.view_venv_stress_test.report_for_venv_tools(max_count=max_count) +# diff --git a/tests/test_hypothesis/test_views.py b/tests/test_hypothesis/test_views.py new file mode 100644 index 0000000..e8d3d0d --- /dev/null +++ b/tests/test_hypothesis/test_views.py @@ -0,0 +1,166 @@ +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +import typing +from pathlib import Path + +from hypothesis import given +from hypothesis import strategies as st + +import cli_tool_audit.models +import cli_tool_audit.views +from cli_tool_audit.models import CliToolConfig, ToolCheckResult + + +@given( + results=st.lists( + st.builds( + ToolCheckResult, + desired_version=st.sampled_from(["0.1.2", "2.3.4", "*", ">1.2.3"]), + found_version=st.one_of(st.none(), st.sampled_from(["0.1.2", "2.3.4", "1.2.3"])), + is_available=st.booleans(), + is_broken=st.booleans(), + is_compatible=st.text(), + is_needed_for_os=st.booleans(), + is_snapshot=st.booleans(), + last_modified=st.one_of(st.none(), st.datetimes()), + parsed_version=st.one_of(st.none(), st.sampled_from(["0.1.2", "2.3.4", "1.2.3"])), + tool=st.text(), + tool_config=st.builds( + CliToolConfig, + if_os=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_command=st.one_of(st.none(), st.one_of(st.none(), st.text())), + install_docs=st.one_of(st.none(), st.one_of(st.none(), st.text())), + name=st.text(), + schema=st.one_of( + st.none(), + st.one_of(st.none(), st.sampled_from(cli_tool_audit.models.SchemaType)), + ), + tags=st.one_of(st.none(), st.one_of(st.none(), st.lists(st.text()))), + version=st.one_of(st.none(), st.one_of(st.none(), st.sampled_from(["0.1.2", "2.3.4", "1.2.3"]))), + version_switch=st.one_of(st.none(), st.one_of(st.none(), st.sampled_from(["--verion", "-v"]))), + ), + ) + ), + truncate_long_versions=st.booleans(), + include_docs=st.booleans(), +) +def test_fuzz_pretty_print_results( + results: list[cli_tool_audit.models.ToolCheckResult], + truncate_long_versions: bool, + include_docs: bool, +) -> None: + cli_tool_audit.views.pretty_print_results( + results=results, + truncate_long_versions=truncate_long_versions, + include_docs=include_docs, + ) + + +# Slow! +# @given( +# cli_tools=st.dictionaries( +# keys=st.text(), +# values=st.builds( +# CliToolConfig, +# if_os=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# install_command=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# install_docs=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# name=st.text(), +# schema=st.one_of( +# st.none(), +# st.one_of(st.none(), st.sampled_from(cli_tool_audit.models.SchemaType)), +# ), +# tags=st.one_of(st.none(), st.one_of(st.none(), st.lists(st.text()))), +# version=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# version_switch=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# ), +# ), +# no_cache=st.booleans(), +# tags=st.one_of(st.none(), st.lists(st.text())), +# disable_progress_bar=st.booleans(), +# ) +# def test_fuzz_process_tools( +# cli_tools: dict[str, cli_tool_audit.models.CliToolConfig], +# no_cache: bool, +# tags: typing.Optional[list[str]], +# disable_progress_bar: bool, +# ) -> None: +# cli_tool_audit.views.process_tools( +# cli_tools=cli_tools, +# no_cache=no_cache, +# tags=tags, +# disable_progress_bar=disable_progress_bar, +# ) + +# Hangs +# @given( +# file_path=st.one_of(st.none(), st.sampled_from([Path("."), Path("audit.toml")])), +# config_as_dict=st.one_of( +# st.none(), +# st.dictionaries( +# keys=st.text(), +# values=st.builds( +# CliToolConfig, +# if_os=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# install_command=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# install_docs=st.one_of(st.none(), st.one_of(st.none(), st.text())), +# name=st.sampled_from(["python", "pip"]), +# schema=st.one_of( +# st.none(), +# st.one_of( +# st.none(), st.sampled_from(cli_tool_audit.models.SchemaType) +# ), +# ), +# tags=st.one_of(st.none(), st.one_of(st.none(), st.lists(st.text()))), +# version=st.one_of(st.none(), st.one_of(st.none(), st.sampled_from(["0.1.2", "2.3.4", "1.2.3"]))), +# version_switch=st.one_of(st.none(), st.one_of(st.none(), st.sampled_from(["--verion", "-v"]))), +# ), +# ), +# ), +# exit_code_on_failure=st.booleans(), +# file_format=st.sampled_from(["toml", "json", "yaml", "xml", "html", "csv","json-lines","table"]), +# no_cache=st.booleans(), +# tags=st.one_of(st.none(), st.lists(st.text())), +# only_errors=st.booleans(), +# ) +# @settings(suppress_health_check=[HealthCheck.too_slow]) +# def test_fuzz_report_from_pyproject_toml( +# file_path: typing.Optional[Path], +# config_as_dict: typing.Optional[dict[str, cli_tool_audit.models.CliToolConfig]], +# exit_code_on_failure: bool, +# file_format: str, +# no_cache: bool, +# tags: typing.Optional[list[str]], +# only_errors: bool, +# ) -> None: +# if file_path is not None and config_as_dict is not None: +# cli_tool_audit.views.report_from_pyproject_toml( +# file_path=file_path, +# config_as_dict=config_as_dict, +# exit_code_on_failure=exit_code_on_failure, +# file_format=file_format, +# no_cache=no_cache, +# tags=tags, +# only_errors=only_errors, +# ) + + +@given( + file_path=st.sampled_from([Path("."), Path("../pyproject.toml"), Path("audit.toml")]), + no_cache=st.booleans(), + tags=st.one_of(st.none(), st.lists(st.text())), + disable_progress_bar=st.booleans(), +) +def test_fuzz_validate( + file_path: Path, + no_cache: bool, + tags: typing.Optional[list[str]], + disable_progress_bar: bool, +) -> None: + cli_tool_audit.views.validate( + file_path=file_path, + no_cache=no_cache, + tags=tags, + disable_progress_bar=disable_progress_bar, + ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 28871db..518957c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,3 +1,5 @@ +from pathlib import Path + import cli_tool_audit @@ -5,9 +7,11 @@ def test_with_live_tools() -> None: """Example""" # Example usage file_path = "../pyproject.toml" - cli_tools = cli_tool_audit.read_config(file_path) + cli_tools = cli_tool_audit.read_config(Path(file_path)) + count = 0 for tool, config in cli_tools.items(): + count += 1 result = cli_tool_audit.check_tool_availability( tool, schema=config.schema, version_switch=config.version_switch ) @@ -15,3 +19,5 @@ def test_with_live_tools() -> None: f"{tool}: {'Available' if result.is_available else 'Not Available'}" f" - Version: { result.version if result.version else 'N/A'}" ) + if count >= 5: + break diff --git a/tests/test_interactive.py b/tests/test_interactive.py index c888c36..8990f66 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -10,7 +10,7 @@ def test_interactive_config_manager(tmp_path): config_path.write_text("") # Instantiate ConfigManager with the temporary config file path - config_manager = ConfigManager(str(config_path)) + config_manager = ConfigManager(config_path) # Mock inputs for the interactive_config_manager function user_inputs = iter( @@ -30,7 +30,7 @@ def test_interactive_config_manager(tmp_path): interactive_config_manager(config_manager) # Read the config file to verify changes - with open(config_path) as file: + with open(config_path, encoding="utf-8") as file: config_data = file.read() # Asserts to validate if the config file was updated correctly