From 37b4eb956da2afe63e1c8bc4480d4fa4654fc7ca Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 18:52:59 +0200 Subject: [PATCH] typing: add missing type + misc typing fixes --- src/app.py | 13 ++++++---- src/domain.py | 2 +- src/log.py | 2 +- src/settings.py | 17 ++++++++++---- src/utils/configpanel.py | 51 ++++++++++++++++++++-------------------- src/utils/form.py | 46 +++++++++++++++++++----------------- 6 files changed, 72 insertions(+), 59 deletions(-) diff --git a/src/app.py b/src/app.py index 970a66fb2..0514066c9 100644 --- a/src/app.py +++ b/src/app.py @@ -1814,26 +1814,29 @@ class AppConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: env = {key: str(value) for key, value in form.dict().items()} return_content = self._call_config_script("apply", env=env) # If the script returned validation error # raise a ValidationError exception using # the first key - if return_content: - for key, message in return_content.get("validation_errors").items(): + errors = return_content.get("validation_errors") + if errors: + for key, message in errors.items(): raise YunohostValidationError( "app_argument_invalid", name=key, error=message, ) - def _run_action(self, form: "FormModel", action_id: str): + def _run_action(self, form: "FormModel", action_id: str) -> None: env = {key: str(value) for key, value in form.dict().items()} self._call_config_script(action_id, env=env) - def _call_config_script(self, action, env=None): + def _call_config_script( + self, action: str, env: Union[dict[str, Any], None] = None + ) -> dict[str, Any]: from yunohost.hook import hook_exec if env is None: diff --git a/src/domain.py b/src/domain.py index f0531e624..892220a68 100644 --- a/src/domain.py +++ b/src/domain.py @@ -720,7 +720,7 @@ class DomainConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: next_settings = { k: v for k, v in form.dict().items() if previous_settings.get(k) != v } diff --git a/src/log.py b/src/log.py index 13683d8ef..5a72411d8 100644 --- a/src/log.py +++ b/src/log.py @@ -469,7 +469,7 @@ class OperationLogger: This class record logs and metadata like context or start time/end time. """ - _instances: List[object] = [] + _instances: List["OperationLogger"] = [] def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation diff --git a/src/settings.py b/src/settings.py index 5f645e3dc..f70f9df61 100644 --- a/src/settings.py +++ b/src/settings.py @@ -136,7 +136,7 @@ class SettingsConfigPanel(ConfigPanel): save_mode = "diff" virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} - def __init__(self, config_path=None, save_path=None, creation=False): + def __init__(self, config_path=None, save_path=None, creation=False) -> None: super().__init__("settings") def get( @@ -150,7 +150,11 @@ class SettingsConfigPanel(ConfigPanel): return result - def reset(self, key: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None,): + def reset( + self, + key: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: self.filter_key = parse_filter_key(key) # Read config panel toml @@ -160,8 +164,11 @@ class SettingsConfigPanel(ConfigPanel): previous_settings = self.form.dict() for option in self.config.options: - if not option.readonly and (option.optional or option.default not in {None, ""}): - self.form[option.id] = option.normalize(option.default, option) + if not option.readonly and ( + option.optional or option.default not in {None, ""} + ): + # FIXME Mypy complains about option.default not being a valid type for normalize but this should be ok + self.form[option.id] = option.normalize(option.default, option) # type: ignore # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) # BaseOption.operation_logger = operation_logger @@ -230,7 +237,7 @@ class SettingsConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: root_password = form.get("root_password", None) root_password_confirm = form.get("root_password_confirm", None) passwordless_sudo = form.get("passwordless_sudo", None) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index b48046ca3..b23df6ddd 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -21,7 +21,7 @@ import os import re from collections import OrderedDict from logging import getLogger -from typing import TYPE_CHECKING, Any, Literal, Sequence, Type, Union +from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union from pydantic import BaseModel, Extra, validator @@ -32,7 +32,6 @@ from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( AnyOption, BaseInputOption, - BaseOption, BaseReadonlyOption, FileOption, OptionsModel, @@ -70,7 +69,7 @@ class ContainerModel(BaseModel): services: list[str] = [] help: Union[Translation, None] = None - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Translate `ask` and `name` attributes of panels and section. This is in place mutation. @@ -111,16 +110,16 @@ class SectionModel(ContainerModel, OptionsModel): ) @property - def is_action_section(self): + def is_action_section(self) -> bool: return any([option.type is OptionType.button for option in self.options]) - def is_visible(self, context: dict[str, Any]): + def is_visible(self, context: dict[str, Any]) -> bool: if isinstance(self.visible, bool): return self.visible return evaluate_simple_js_expression(self.visible, context=context) - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Call to `Container`'s `translate` for self translation + Call to `OptionsContainer`'s `translate_options` for options translation @@ -151,7 +150,7 @@ class PanelModel(ContainerModel): id=id, name=name, services=services, help=help, sections=sections ) - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Recursivly mutate translatable attributes to their translation """ @@ -181,14 +180,14 @@ class ConfigPanelModel(BaseModel): super().__init__(version=version, i18n=i18n, panels=panels) @property - def sections(self): + def sections(self) -> Iterator[SectionModel]: """Convinient prop to iter on all sections""" for panel in self.panels: for section in panel.sections: yield section @property - def options(self): + def options(self) -> Iterator[AnyOption]: """Convinient prop to iter on all options""" for section in self.sections: for option in section.options: @@ -231,7 +230,7 @@ class ConfigPanelModel(BaseModel): for option in section.options: yield (panel, section, option) - def translate(self): + def translate(self) -> None: """ Recursivly mutate translatable attributes to their translation """ @@ -239,7 +238,7 @@ class ConfigPanelModel(BaseModel): panel.translate(self.i18n) @validator("version", always=True) - def check_version(cls, value, field: "ModelField"): + def check_version(cls, value: float, field: "ModelField") -> float: if value < CONFIG_PANEL_VERSION_SUPPORTED: raise ValueError( f"Config panels version '{value}' are no longer supported." @@ -302,7 +301,9 @@ class ConfigPanel: entities = [] return entities - def __init__(self, entity, config_path=None, save_path=None, creation=False): + def __init__( + self, entity, config_path=None, save_path=None, creation=False + ) -> None: self.entity = entity self.config_path = config_path if not config_path: @@ -350,7 +351,7 @@ class ConfigPanel: if option is None: # FIXME i18n raise YunohostValidationError( - f"Couldn't find any option with id {option_id}" + f"Couldn't find any option with id {option_id}", raw_msg=True ) if isinstance(option, BaseReadonlyOption): @@ -398,7 +399,7 @@ class ConfigPanel: args: Union[str, None] = None, args_file: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None, - ): + ) -> None: self.filter_key = parse_filter_key(key) panel_id, section_id, option_id = self.filter_key @@ -466,7 +467,7 @@ class ConfigPanel: if operation_logger: operation_logger.success() - def list_actions(self): + def list_actions(self) -> dict[str, str]: actions = {} # FIXME : meh, loading the entire config panel is again going to cause @@ -486,7 +487,7 @@ class ConfigPanel: args: Union[str, None] = None, args_file: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None, - ): + ) -> None: # # FIXME : this stuff looks a lot like set() ... # @@ -666,7 +667,7 @@ class ConfigPanel: def _ask( self, config: ConfigPanelModel, - settings: "FormModel", + form: "FormModel", prefilled_answers: dict[str, Any] = {}, action_id: Union[str, None] = None, hooks: "Hooks" = {}, @@ -709,22 +710,22 @@ class ConfigPanel: if option.type is not OptionType.button or option.id == action_id ] - settings = prompt_or_validate_form( + form = prompt_or_validate_form( options, - settings, + form, prefilled_answers=prefilled_answers, context=context, hooks=hooks, ) - return settings + return form def _apply( self, form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ) -> dict[str, Any]: + ) -> None: """ Save settings in yaml file. If `save_mode` is `"diff"` (which is the default), only values that are @@ -764,15 +765,13 @@ class ConfigPanel: # Save the settings to the .yaml file write_to_yaml(self.save_path, current_settings) - return current_settings - - def _run_action(self, form: "FormModel", action_id: str): + def _run_action(self, form: "FormModel", action_id: str) -> None: raise NotImplementedError() - def _reload_services(self): + def _reload_services(self) -> None: from yunohost.service import service_reload_or_restart - services_to_reload = self.config.services + services_to_reload = self.config.services if self.config else [] if services_to_reload: logger.info("Reloading services...") diff --git a/src/utils/form.py b/src/utils/form.py index d8ff4b8c7..4d62b0a29 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -32,7 +32,7 @@ from typing import ( Annotated, Any, Callable, - List, + Iterable, Literal, Mapping, Type, @@ -207,7 +207,7 @@ def js_to_python(expr): return py_expr -def evaluate_simple_js_expression(expr, context={}): +def evaluate_simple_js_expression(expr: str, context: dict[str, Any] = {}) -> bool: if not expr.strip(): return False node = ast.parse(js_to_python(expr), mode="eval").body @@ -650,7 +650,7 @@ class NumberOption(BaseInputOption): _none_as_empty_str = False @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> Union[int, None]: if isinstance(value, int): return value @@ -704,7 +704,7 @@ class BooleanOption(BaseInputOption): _none_as_empty_str = False @staticmethod - def humanize(value, option={}): + def humanize(value, option={}) -> str: option = option.dict() if isinstance(option, BaseOption) else option yes = option.get("yes", 1) @@ -727,7 +727,7 @@ class BooleanOption(BaseInputOption): ) @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> Any: option = option.dict() if isinstance(option, BaseOption) else option if isinstance(value, str): @@ -844,7 +844,7 @@ class WebPathOption(BaseInputOption): _annotation = str @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: option = option.dict() if isinstance(option, BaseOption) else option if value is None: @@ -892,14 +892,14 @@ class FileOption(BaseInputOption): _upload_dirs: set[str] = set() @classmethod - def clean_upload_dirs(cls): + def clean_upload_dirs(cls) -> None: # Delete files uploaded from API for upload_dir in cls._upload_dirs: if os.path.exists(upload_dir): shutil.rmtree(upload_dir) @classmethod - def _value_post_validator(cls, value: Any, field: "ModelField") -> Any: + def _value_post_validator(cls, value: Any, field: "ModelField") -> str: from base64 import b64decode if not value: @@ -967,7 +967,6 @@ class BaseChoicesOption(BaseInputOption): choices = ( self.choices if isinstance(self.choices, list) else self.choices.keys() ) - # FIXME in case of dict, try to parse keys with `item_type` (at least number) return Literal[tuple(choices)] return self._annotation @@ -1006,6 +1005,7 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): type: Literal[OptionType.select] = OptionType.select + filter: Literal[None] = None choices: Union[dict[str, Any], list[Any]] default: Union[str, None] _annotation = str @@ -1013,13 +1013,14 @@ class SelectOption(BaseChoicesOption): class TagsOption(BaseChoicesOption): type: Literal[OptionType.tags] = OptionType.tags + filter: Literal[None] = None choices: Union[list[str], None] = None pattern: Union[Pattern, None] = None default: Union[str, list[str], None] _annotation = str @staticmethod - def humanize(value, option={}): + def humanize(value, option={}) -> str: if isinstance(value, list): return ",".join(str(v) for v in value) if not value: @@ -1027,7 +1028,7 @@ class TagsOption(BaseChoicesOption): return value @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: if isinstance(value, list): return ",".join(str(v) for v in value) if isinstance(value, str): @@ -1037,7 +1038,7 @@ class TagsOption(BaseChoicesOption): return value @property - def _dynamic_annotation(self): + def _dynamic_annotation(self) -> Type[str]: # TODO use Literal when serialization is seperated from validation # if self.choices is not None: # return Literal[tuple(self.choices)] @@ -1120,7 +1121,7 @@ class DomainOption(BaseChoicesOption): return _get_maindomain() @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: if value.startswith("https://"): value = value[len("https://") :] elif value.startswith("http://"): @@ -1314,7 +1315,9 @@ class OptionsModel(BaseModel): options: list[Annotated[AnyOption, Field(discriminator="type")]] @staticmethod - def options_dict_to_list(options: dict[str, Any], optional: bool = False): + def options_dict_to_list( + options: dict[str, Any], optional: bool = False + ) -> list[dict[str, Any]]: return [ option | { @@ -1329,7 +1332,7 @@ class OptionsModel(BaseModel): def __init__(self, **kwargs) -> None: super().__init__(options=self.options_dict_to_list(kwargs)) - def translate_options(self, i18n_key: Union[str, None] = None): + def translate_options(self, i18n_key: Union[str, None] = None) -> None: """ Mutate in place translatable attributes of options to their translations """ @@ -1359,7 +1362,7 @@ class FormModel(BaseModel): validate_assignment = True extra = Extra.ignore - def __getitem__(self, name: str): + def __getitem__(self, name: str) -> Any: # FIXME # if a FormModel's required field is not instancied with a value, it is # not available as an attr and therefor triggers an `AttributeError` @@ -1372,7 +1375,7 @@ class FormModel(BaseModel): return getattr(self, name) - def __setitem__(self, name: str, value: Any): + def __setitem__(self, name: str, value: Any) -> None: setattr(self, name, value) def get(self, attr: str, default: Any = None) -> Any: @@ -1382,7 +1385,9 @@ class FormModel(BaseModel): return default -def build_form(options: list[AnyOption], name: str = "DynamicForm") -> Type[FormModel]: +def build_form( + options: Iterable[AnyOption], name: str = "DynamicForm" +) -> Type[FormModel]: """ Returns a dynamic pydantic model class that can be used as a form. Parsing/validation occurs at instanciation and assignements. @@ -1468,7 +1473,7 @@ MAX_RETRIES = 4 def prompt_or_validate_form( - options: list[AnyOption], + options: Iterable[AnyOption], form: FormModel, prefilled_answers: dict[str, Any] = {}, context: Context = {}, @@ -1503,7 +1508,6 @@ def prompt_or_validate_form( if isinstance(option, BaseReadonlyOption) or option.readonly: if isinstance(option, BaseInputOption): - # FIXME normalized needed, form[option.id] should already be normalized # only update the context with the value context[option.id] = option.normalize(form[option.id]) @@ -1623,7 +1627,7 @@ def ask_questions_and_parse_answers( return (model.options, form) -def hydrate_questions_with_choices(raw_questions: List) -> List: +def hydrate_questions_with_choices(raw_questions: list[dict[str, Any]]) -> list[dict[str, Any]]: out = [] for raw_question in raw_questions: