From e35683e90afd4ce2f535ee3962f18ac508dfa086 Mon Sep 17 00:00:00 2001 From: Benexl Date: Sun, 6 Jul 2025 17:40:20 +0300 Subject: [PATCH] fix: config update logic --- fastanime/cli/cli.py | 13 +++++- fastanime/cli/config/generate.py | 27 ++++++----- fastanime/cli/config/loader.py | 1 - fastanime/cli/config/model.py | 46 ++++++++++++------- fastanime/cli/options.py | 33 ++++++++----- fastanime/core/constants.py | 2 +- fastanime/libs/anilist/__init__.py | 4 -- .../providers/anime/allanime/constants.py | 2 +- 8 files changed, 79 insertions(+), 49 deletions(-) diff --git a/fastanime/cli/cli.py b/fastanime/cli/cli.py index e856d2e..9414ebe 100644 --- a/fastanime/cli/cli.py +++ b/fastanime/cli/cli.py @@ -2,6 +2,7 @@ import click from click.core import ParameterSource from .. import __version__ +from ..core.constants import APP_NAME from .config import AppConfig, ConfigLoader from .constants import USER_CONFIG_PATH from .options import options_from_model @@ -15,7 +16,12 @@ commands = { @click.version_option(__version__, "--version") @click.option("--no-config", is_flag=True, help="Don't load the user config file.") -@click.group(cls=LazyGroup, root="fastanime.cli.commands", lazy_subcommands=commands) +@click.group( + cls=LazyGroup, + root="fastanime.cli.commands", + lazy_subcommands=commands, + context_settings=dict(auto_envvar_prefix=APP_NAME), +) @options_from_model(AppConfig) @click.pass_context def cli(ctx: click.Context, no_config: bool, **kwargs): @@ -34,7 +40,10 @@ def cli(ctx: click.Context, no_config: bool, **kwargs): # update app config with command line parameters for param_name, param_value in ctx.params.items(): source = ctx.get_parameter_source(param_name) - if source == ParameterSource.COMMANDLINE: + if ( + source == ParameterSource.ENVIRONMENT + or source == ParameterSource.COMMANDLINE + ): parameter = None for param in ctx.command.params: if param.name == param_name: diff --git a/fastanime/cli/config/generate.py b/fastanime/cli/config/generate.py index c0de869..fd20fc5 100644 --- a/fastanime/cli/config/generate.py +++ b/fastanime/cli/config/generate.py @@ -21,25 +21,27 @@ CONFIG_HEADER = f""" def generate_config_ini_from_app_model(app_model: AppConfig) -> str: """Generate a configuration file content from a Pydantic model.""" - model_schema = AppConfig.model_json_schema() - + model_schema = AppConfig.model_json_schema(mode="serialization") + app_model_dict = app_model.model_dump() config_ini_content = [CONFIG_HEADER] - for section_name, section_model in app_model: - section_class_name = model_schema["properties"][section_name]["$ref"].split( - "/" - )[-1] - section_comment = model_schema["$defs"][section_class_name]["description"] + for section_name, section_dict in app_model_dict.items(): + section_ref = model_schema["properties"][section_name].get("$ref") + if not section_ref: + continue + + section_class_name = section_ref.split("/")[-1] + section_schema = model_schema["$defs"][section_class_name] + section_comment = section_schema.get("description", "") + config_ini_content.append(f"\n#\n# {section_comment}\n#") config_ini_content.append(f"[{section_name}]") - for field_name, field_value in section_model: - description = model_schema["$defs"][section_class_name]["properties"][ - field_name - ].get("description", "") + for field_name, field_value in section_dict.items(): + field_properties = section_schema.get("properties", {}).get(field_name, {}) + description = field_properties.get("description", "") if description: - # Wrap long comments for better readability in the .ini file wrapped_comment = textwrap.fill( description, width=78, @@ -58,4 +60,5 @@ def generate_config_ini_from_app_model(app_model: AppConfig) -> str: value_str = str(field_value) config_ini_content.append(f"{field_name} = {value_str}") + return "\n".join(config_ini_content) diff --git a/fastanime/cli/config/loader.py b/fastanime/cli/config/loader.py index 2f28f34..b82dc18 100644 --- a/fastanime/cli/config/loader.py +++ b/fastanime/cli/config/loader.py @@ -78,7 +78,6 @@ class ConfigLoader: section: dict(self.parser.items(section)) for section in self.parser.sections() } - try: app_config = AppConfig.model_validate(config_dict) return app_config diff --git a/fastanime/cli/config/model.py b/fastanime/cli/config/model.py index e68b246..7d7773c 100644 --- a/fastanime/cli/config/model.py +++ b/fastanime/cli/config/model.py @@ -2,7 +2,7 @@ import os from pathlib import Path from typing import Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, PrivateAttr, computed_field, field_validator from ...core.constants import ( FZF_DEFAULT_OPTS, @@ -23,23 +23,11 @@ class OtherConfig(BaseModel): class FzfConfig(OtherConfig): """Configuration specific to the FZF selector.""" - opts: str = Field( - default_factory=lambda: "\n" - + "\n".join( - [ - f"\t{line}" - for line in FZF_DEFAULT_OPTS.read_text(encoding="utf-8").split() - ] - ), - description="Command-line options to pass to FZF for theming and behavior.", - ) + _opts: str = PrivateAttr(default=FZF_DEFAULT_OPTS.read_text(encoding="utf-8")) header_color: str = Field( default="95,135,175", description="RGB color for the main TUI header." ) - header_ascii_art: str = Field( - default="\n" + "\n".join([f"\t{line}" for line in APP_ASCII_ART.split("\n")]), - description="The ASCII art to display in TUI headers.", - ) + _header_ascii_art: str = PrivateAttr(default=APP_ASCII_ART) preview_header_color: str = Field( default="215,0,95", description="RGB color for preview pane headers." ) @@ -47,6 +35,32 @@ class FzfConfig(OtherConfig): default="208,208,208", description="RGB color for preview pane separators." ) + def __init__(self, **kwargs): + opts = kwargs.pop("opts", None) + header_ascii_art = kwargs.pop("header_ascii_art", None) + + super().__init__(**kwargs) + if opts: + self._opts = opts + if header_ascii_art: + self._header_ascii_art = header_ascii_art + + @computed_field( + description="The FZF options, formatted with leading tabs for the config file." + ) + @property + def opts(self) -> str: + return "\n" + "\n".join([f"\t{line}" for line in self._opts.split()]) + + @computed_field( + description="The ASCII art to display as a header in the FZF interface." + ) + @property + def header_ascii_art(self) -> str: + return "\n" + "\n".join( + [f"\t{line}" for line in self._header_ascii_art.split()] + ) + class RofiConfig(OtherConfig): """Configuration specific to the Rofi selector.""" @@ -203,7 +217,7 @@ class StreamConfig(BaseModel): default="sub", description="Preferred audio/subtitle language type." ) server: str = Field( - default="top", + default="TOP", description="The default server to use from a provider. 'top' uses the first available.", examples=SERVERS_AVAILABLE, ) diff --git a/fastanime/cli/options.py b/fastanime/cli/options.py index 975ee05..4170871 100644 --- a/fastanime/cli/options.py +++ b/fastanime/cli/options.py @@ -9,7 +9,6 @@ from pydantic_core import PydanticUndefined from .config.model import OtherConfig -# Mapping from Python/Pydantic types to Click types TYPE_MAP = { str: click.STRING, int: click.INT, @@ -49,38 +48,29 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl """ decorators = [] - # Check if this model inherits from ExternalTool is_external_tool = issubclass(model, OtherConfig) model_name = model.__name__.lower().replace("config", "") # Introspect the model's fields for field_name, field_info in model.model_fields.items(): - # Handle nested models by calling this function recursively if isinstance(field_info.annotation, type) and issubclass( field_info.annotation, BaseModel ): - # Apply decorators from the nested model with current model as parent nested_decorators = options_from_model(field_info.annotation, field_name) nested_decorator_list = getattr(nested_decorators, "decorators", []) decorators.extend(nested_decorator_list) continue - # Determine the option name for the CLI if is_external_tool: - # For ExternalTool subclasses, use --model_name-field_name format cli_name = f"--{model_name}-{field_name.replace('_', '-')}" else: cli_name = f"--{field_name.replace('_', '-')}" - - # Build the arguments for the click.option decorator kwargs = { "type": _get_click_type(field_info), "help": field_info.description or "", } - # Handle boolean flags (e.g., --foo/--no-foo) if field_info.annotation is bool: - # Set default value for boolean flags if field_info.default is not PydanticUndefined: kwargs["default"] = field_info.default kwargs["show_default"] = True @@ -89,9 +79,7 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl f"{cli_name}/--no-{model_name}-{field_name.replace('_', '-')}" ) else: - # For non-external tools, we use the --no- prefix directly cli_name = f"{cli_name}/--no-{field_name.replace('_', '-')}" - # For other types, set default if one is provided in the model elif field_info.default is not PydanticUndefined: kwargs["default"] = field_info.default kwargs["show_default"] = True @@ -106,6 +94,27 @@ def options_from_model(model: type[BaseModel], parent_name: str = "") -> Callabl ) ) + for field_name, computed_field_info in model.model_computed_fields.items(): + if is_external_tool: + cli_name = f"--{model_name}-{field_name.replace('_', '-')}" + else: + cli_name = f"--{field_name.replace('_', '-')}" + + kwargs = { + "type": TYPE_MAP[computed_field_info.return_type], + "help": computed_field_info.description or "", + } + + decorators.append( + click.option( + cli_name, + cls=ConfigOption, + model_name=model_name, + field_name=field_name, + **kwargs, + ) + ) + def decorator(f: Callable) -> Callable: # Apply the decorators in reverse order to the function for deco in reversed(decorators): diff --git a/fastanime/core/constants.py b/fastanime/core/constants.py index 0b30311..42ece9a 100644 --- a/fastanime/core/constants.py +++ b/fastanime/core/constants.py @@ -1,7 +1,7 @@ import os from importlib import resources -APP_NAME = os.environ.get("FASTANIME_APPNAME", "fastanime") +APP_NAME = os.environ.get("FASTANIME_APP_NAME", "fastanime") try: pkg = resources.files("fastanime") diff --git a/fastanime/libs/anilist/__init__.py b/fastanime/libs/anilist/__init__.py index e692321..8b13789 100644 --- a/fastanime/libs/anilist/__init__.py +++ b/fastanime/libs/anilist/__init__.py @@ -1,5 +1 @@ -""" -his module contains an abstraction for interaction with the anilist api making it easy and efficient -""" -from .api import AniListApi diff --git a/fastanime/libs/providers/anime/allanime/constants.py b/fastanime/libs/providers/anime/allanime/constants.py index c5b4321..451582d 100644 --- a/fastanime/libs/providers/anime/allanime/constants.py +++ b/fastanime/libs/providers/anime/allanime/constants.py @@ -28,7 +28,7 @@ MP4_SERVER_JUICY_STREAM_REGEX = re.compile( ) # graphql files -GQLS = resources.files("fastanime.libs.anime_provider.allanime") +GQLS = resources.files("fastanime.libs.providers.anime.allanime") / "queries" SEARCH_GQL = Path(str(GQLS / "search.gql")) ANIME_GQL = Path(str(GQLS / "anime.gql")) EPISODE_GQL = Path(str(GQLS / "episode.gql"))