From 4e2e83b18bbb58321d154ab0ac47b2f3afca5ada Mon Sep 17 00:00:00 2001 From: Eli <43382407+eli64s@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:10:55 -0600 Subject: [PATCH] Fix package resource file path bug. --- pyproject.toml | 2 +- readmeai/config/settings.py | 39 ++++++++++++------------ readmeai/config/utils.py | 56 +++++++++++++++++++++++++++++++++++ readmeai/core/utils.py | 25 ---------------- readmeai/generators/badges.py | 24 +++++++-------- tests/config/test_utils.py | 25 ++++++++++++++++ tests/core/test_utils.py | 22 +------------- 7 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 readmeai/config/utils.py create mode 100644 tests/config/test_utils.py diff --git a/pyproject.toml b/pyproject.toml index 11e696dc..7b8b558c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "readmeai" -version = "0.5.061" +version = "0.5.062" description = "👾 Automated README file generator, powered by large language model APIs." authors = ["Eli "] license = "MIT" diff --git a/readmeai/config/settings.py b/readmeai/config/settings.py index 432c2f06..c70ac770 100644 --- a/readmeai/config/settings.py +++ b/readmeai/config/settings.py @@ -8,11 +8,12 @@ from pydantic import BaseModel, DirectoryPath, HttpUrl, validator from readmeai.config.enums import ModelOptions +from readmeai.config.utils import get_resource_path from readmeai.config.validators import GitValidator +from readmeai.exceptions import FileReadError from readmeai.utils.file_handler import FileHandler from readmeai.utils.logger import Logger -_base_dir = Path(__file__).resolve().parent _github_discussions = "https://github.com/eli64s/readme-ai/discussions" _output_file = "readme-ai.md" _logger = Logger(__name__) @@ -121,34 +122,32 @@ class ConfigLoader: def __init__( self, config_file: Union[str, Path] = "config.toml", - package: str = "readmeai/config", - submodule: str = "settings", ) -> None: """Initialize the ConfigLoader.""" - self.package = package - self.submodule = submodule - self.config_path = f"{package}/{submodule}/{config_file}" + self.file_handler = FileHandler() + self.config_file = config_file self.config = self._load_base_config() self._load_all_configs() + def _load_base_config(self) -> Settings: + """Load the base configuration file.""" + config_dict = get_resource_path(self.file_handler, self.config_file) + return Settings.parse_obj(config_dict) + def _load_all_configs(self) -> None: """Load additional configuration files specified in the Settings.""" - for key, file_name in self.config.files.__dict__.items(): + for ( + key, + file_name, + ) in self.config.files.dict().items(): if not file_name.endswith(".toml"): continue - file_path = _base_dir / self.submodule / file_name - log_path = f"{self.package}/{self.submodule}/{file_name}" - - if file_path.exists(): - config_data = FileHandler().read(file_path) + try: + config_data = get_resource_path(self.file_handler, file_name) setattr(self, key, config_data) - _logger.debug(f"Loaded config file @ {log_path}") - else: - setattr(self, key, None) - _logger.warning(f"Config file not found: {log_path}") + _logger.debug(f"Loaded config file: {file_name}") - def _load_base_config(self) -> Settings: - """Load the main configuration file for the readme-ai package.""" - config_dict = FileHandler().read(self.config_path) - return Settings(**config_dict) + except FileReadError as exc: + setattr(self, key, None) + _logger.warning(f"Config file not found: {file_name} - {exc}") diff --git a/readmeai/config/utils.py b/readmeai/config/utils.py new file mode 100644 index 00000000..ff4ec366 --- /dev/null +++ b/readmeai/config/utils.py @@ -0,0 +1,56 @@ +"""Utility methods to read package resources.""" + +from importlib import resources +from pathlib import Path + +from readmeai.exceptions import FileReadError +from readmeai.utils.file_handler import FileHandler +from readmeai.utils.logger import Logger + +_logger = Logger(__name__) + + +def get_resource_path( + handler: FileHandler, + file_path: str, + package: str = "readmeai.config", + submodule: str = "settings", +) -> dict: + """Get configuration dictionary from TOML file.""" + try: + resource_path = resources.files(package).joinpath(submodule, file_path) + _logger.debug(f"Loading file via importlib.resources: {resource_path}") + + except TypeError as exc: + _logger.debug(f"Error using importlib.resources: {exc}") + + try: + import pkg_resources + + submodule = submodule.replace(".", "/") + resource_path = Path( + pkg_resources.resource_filename( + "readmeai", f"{submodule}/{file_path}" + ) + ).resolve() + + except Exception as exc: + _logger.error(f"Error loading file via pkg_resources: {exc}") + + raise FileReadError( + "Error loading file via pkg_resources", str(file_path) + ) from exc + + if not resource_path.exists(): + _logger.error(f"File not found: {str(resource_path)}") + raise FileReadError("File not found", str(resource_path)) + + if str(file_path).endswith(".toml"): + return handler.read_toml(str(resource_path)) + + elif str(file_path).endswith(".json"): + return handler.read_json(str(resource_path)) + + _logger.error(f"Unsupported file format: {file_path}") + + raise FileReadError("Unsupported file format", str(file_path)) diff --git a/readmeai/core/utils.py b/readmeai/core/utils.py index 5c40584f..af109530 100644 --- a/readmeai/core/utils.py +++ b/readmeai/core/utils.py @@ -3,12 +3,10 @@ from __future__ import annotations import os -from importlib import resources from pathlib import Path from readmeai.config.enums import ModelOptions, SecretKeys from readmeai.config.settings import ConfigLoader, Settings -from readmeai.exceptions import FileReadError from readmeai.utils.logger import Logger _logger = Logger(__name__) @@ -33,29 +31,6 @@ def filter_file(config_loader: ConfigLoader, file_path: Path) -> bool: return False -def get_resource_path( - file_name: str, module: str = "settings", package: str = "readmeai.config" -) -> Path: - """ - Get the resource path using importlib.resources for Python >= 3.9 - or fallback to pkg_resources for older Python versions. - """ - try: - full_package_path = f"{package}.{module}".replace("/", ".") - resource_path = resources.files(full_package_path) / file_name - except Exception as exc: - _logger.debug(f"Error using importlib.resources: {exc}") - raise FileReadError( - "Error accessing resource using importlib.resources", - str(file_name), - ) from exc - - if not resource_path.exists(): - raise FileReadError("File not found", str(resource_path)) - - return resource_path - - def set_model_engine(config: Settings) -> None: """Set LLM environment variables based on the specified LLM service.""" llm_engine = config.llm.api diff --git a/readmeai/generators/badges.py b/readmeai/generators/badges.py index a1d25057..0727ad9d 100644 --- a/readmeai/generators/badges.py +++ b/readmeai/generators/badges.py @@ -4,12 +4,12 @@ from readmeai.config.enums import BadgeOptions from readmeai.config.settings import ConfigLoader -from readmeai.core.utils import get_resource_path +from readmeai.config.utils import get_resource_path from readmeai.services.git import GitService from readmeai.utils.file_handler import FileHandler -BASE_DIR = "readmeai" -BASE_SUBDIR = "generators.assets" +_package = "readmeai.generators" +_submodule = "assets" def _format_badges(badges: list[str]) -> str: @@ -67,13 +67,14 @@ def shields_icons( conf: ConfigLoader, dependencies: list, full_name: str, git_host: str ) -> Tuple[str, str]: """ - Generates badges for the README using shields.io icons, referencing - the repository - https://github.com/Aveek-Saha/GitHub-Profile-Badges. + Generates badges for the README using shields.io icons. """ - file_path = get_resource_path( - conf.files.shields_icons, BASE_SUBDIR, BASE_DIR + icons_dict = get_resource_path( + FileHandler(), + conf.files.shields_icons, + _package, + _submodule, ) - icons_dict = FileHandler().read(file_path) default_badges = build_default_badges(conf, full_name, git_host) @@ -109,17 +110,14 @@ def skill_icons(conf: ConfigLoader, dependencies: list) -> str: """ dependencies.extend(["md"]) - file_path = get_resource_path( - conf.files.skill_icons, BASE_SUBDIR, BASE_DIR + icons_dict = get_resource_path( + FileHandler(), conf.files.skill_icons, _package, _submodule ) - icons_dict = FileHandler().read(file_path) skill_icons = [ icon for icon in icons_dict["icons"]["names"] if icon in dependencies ] skill_icons = ",".join(skill_icons) - # per_line = (len(skill_icons) + 2) // 2 - # icon_names = f"{icon_names}" # &perline={per_line}" skill_icons = icons_dict["url"]["base_url"] + skill_icons if conf.md.badge_style == "skills-light": diff --git a/tests/config/test_utils.py b/tests/config/test_utils.py new file mode 100644 index 00000000..54e74a55 --- /dev/null +++ b/tests/config/test_utils.py @@ -0,0 +1,25 @@ +"""Tests for package resources utility methods.""" + +import pytest + +from readmeai.config.utils import get_resource_path +from readmeai.exceptions import FileReadError +from readmeai.utils.file_handler import FileHandler + + +def test_get_resource_path(): + """Test that the resource path is returned correctly.""" + try: + file_path = get_resource_path( + FileHandler(), "config.toml", "readmeai.config", "settings" + ) + assert isinstance(file_path, dict) + except FileReadError as exc: + assert isinstance(exc, FileReadError) + + +def test_file_read_error(): + """Test that the FileReadError is raised correctly.""" + with pytest.raises(Exception) as exc: + get_resource_path(FileHandler(), "config.yaml", "readmeai", "config") + assert isinstance(exc.value, FileReadError) diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index b14c130e..baaa5a53 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -2,18 +2,14 @@ import os from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest +from unittest.mock import patch from readmeai.config.enums import ModelOptions, SecretKeys from readmeai.config.settings import ConfigLoader from readmeai.core.utils import ( - FileReadError, _scan_environ, _set_offline, filter_file, - get_resource_path, set_model_engine, ) @@ -181,19 +177,3 @@ def test_scan_environ_missing(): assert _scan_environ(keys) is False keys = ("VERTEX_LOCATION", "VERTEX_PROJECT") assert _scan_environ(keys) is False - - -@pytest.mark.skip -def test_get_resource_path_with_mock(): - """Test that the resource path is returned correctly using mock.""" - mock_path = "config.toml" - with patch("pathlib.Path.exists", MagicMock(return_value=True)): - resource_path = get_resource_path(mock_path) - assert isinstance(resource_path, Path) - - -def test_file_read_error(): - """Test that the FileReadError is raised correctly.""" - with pytest.raises(Exception) as exc: - get_resource_path("non_existent_file.toml") - assert isinstance(exc.value, FileReadError)