typing: add missing type + misc typing fixes

This commit is contained in:
axolotle 2023-04-19 18:52:59 +02:00
parent 54cc23c90c
commit 37b4eb956d
6 changed files with 72 additions and 59 deletions

View file

@ -1814,26 +1814,29 @@ class AppConfigPanel(ConfigPanel):
form: "FormModel", form: "FormModel",
previous_settings: dict[str, Any], previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
): ) -> None:
env = {key: str(value) for key, value in form.dict().items()} env = {key: str(value) for key, value in form.dict().items()}
return_content = self._call_config_script("apply", env=env) return_content = self._call_config_script("apply", env=env)
# If the script returned validation error # If the script returned validation error
# raise a ValidationError exception using # raise a ValidationError exception using
# the first key # the first key
if return_content: errors = return_content.get("validation_errors")
for key, message in return_content.get("validation_errors").items(): if errors:
for key, message in errors.items():
raise YunohostValidationError( raise YunohostValidationError(
"app_argument_invalid", "app_argument_invalid",
name=key, name=key,
error=message, 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()} env = {key: str(value) for key, value in form.dict().items()}
self._call_config_script(action_id, env=env) 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 from yunohost.hook import hook_exec
if env is None: if env is None:

View file

@ -720,7 +720,7 @@ class DomainConfigPanel(ConfigPanel):
form: "FormModel", form: "FormModel",
previous_settings: dict[str, Any], previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
): ) -> None:
next_settings = { next_settings = {
k: v for k, v in form.dict().items() if previous_settings.get(k) != v k: v for k, v in form.dict().items() if previous_settings.get(k) != v
} }

View file

@ -469,7 +469,7 @@ class OperationLogger:
This class record logs and metadata like context or start time/end time. 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): def __init__(self, operation, related_to=None, **kwargs):
# TODO add a way to not save password on app installation # TODO add a way to not save password on app installation

View file

@ -136,7 +136,7 @@ class SettingsConfigPanel(ConfigPanel):
save_mode = "diff" save_mode = "diff"
virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} 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") super().__init__("settings")
def get( def get(
@ -150,7 +150,11 @@ class SettingsConfigPanel(ConfigPanel):
return result 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) self.filter_key = parse_filter_key(key)
# Read config panel toml # Read config panel toml
@ -160,8 +164,11 @@ class SettingsConfigPanel(ConfigPanel):
previous_settings = self.form.dict() previous_settings = self.form.dict()
for option in self.config.options: for option in self.config.options:
if not option.readonly and (option.optional or option.default not in {None, ""}): if not option.readonly and (
self.form[option.id] = option.normalize(option.default, option) 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) # FIXME Not sure if this is need (redact call to operation logger does it on all the instances)
# BaseOption.operation_logger = operation_logger # BaseOption.operation_logger = operation_logger
@ -230,7 +237,7 @@ class SettingsConfigPanel(ConfigPanel):
form: "FormModel", form: "FormModel",
previous_settings: dict[str, Any], previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
): ) -> None:
root_password = form.get("root_password", None) root_password = form.get("root_password", None)
root_password_confirm = form.get("root_password_confirm", None) root_password_confirm = form.get("root_password_confirm", None)
passwordless_sudo = form.get("passwordless_sudo", None) passwordless_sudo = form.get("passwordless_sudo", None)

View file

@ -21,7 +21,7 @@ import os
import re import re
from collections import OrderedDict from collections import OrderedDict
from logging import getLogger 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 from pydantic import BaseModel, Extra, validator
@ -32,7 +32,6 @@ from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.form import ( from yunohost.utils.form import (
AnyOption, AnyOption,
BaseInputOption, BaseInputOption,
BaseOption,
BaseReadonlyOption, BaseReadonlyOption,
FileOption, FileOption,
OptionsModel, OptionsModel,
@ -70,7 +69,7 @@ class ContainerModel(BaseModel):
services: list[str] = [] services: list[str] = []
help: Union[Translation, None] = None 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. Translate `ask` and `name` attributes of panels and section.
This is in place mutation. This is in place mutation.
@ -111,16 +110,16 @@ class SectionModel(ContainerModel, OptionsModel):
) )
@property @property
def is_action_section(self): def is_action_section(self) -> bool:
return any([option.type is OptionType.button for option in self.options]) 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): if isinstance(self.visible, bool):
return self.visible return self.visible
return evaluate_simple_js_expression(self.visible, context=context) 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 `Container`'s `translate` for self translation
+ Call to `OptionsContainer`'s `translate_options` for options 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 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 Recursivly mutate translatable attributes to their translation
""" """
@ -181,14 +180,14 @@ class ConfigPanelModel(BaseModel):
super().__init__(version=version, i18n=i18n, panels=panels) super().__init__(version=version, i18n=i18n, panels=panels)
@property @property
def sections(self): def sections(self) -> Iterator[SectionModel]:
"""Convinient prop to iter on all sections""" """Convinient prop to iter on all sections"""
for panel in self.panels: for panel in self.panels:
for section in panel.sections: for section in panel.sections:
yield section yield section
@property @property
def options(self): def options(self) -> Iterator[AnyOption]:
"""Convinient prop to iter on all options""" """Convinient prop to iter on all options"""
for section in self.sections: for section in self.sections:
for option in section.options: for option in section.options:
@ -231,7 +230,7 @@ class ConfigPanelModel(BaseModel):
for option in section.options: for option in section.options:
yield (panel, section, option) yield (panel, section, option)
def translate(self): def translate(self) -> None:
""" """
Recursivly mutate translatable attributes to their translation Recursivly mutate translatable attributes to their translation
""" """
@ -239,7 +238,7 @@ class ConfigPanelModel(BaseModel):
panel.translate(self.i18n) panel.translate(self.i18n)
@validator("version", always=True) @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: if value < CONFIG_PANEL_VERSION_SUPPORTED:
raise ValueError( raise ValueError(
f"Config panels version '{value}' are no longer supported." f"Config panels version '{value}' are no longer supported."
@ -302,7 +301,9 @@ class ConfigPanel:
entities = [] entities = []
return 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.entity = entity
self.config_path = config_path self.config_path = config_path
if not config_path: if not config_path:
@ -350,7 +351,7 @@ class ConfigPanel:
if option is None: if option is None:
# FIXME i18n # FIXME i18n
raise YunohostValidationError( 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): if isinstance(option, BaseReadonlyOption):
@ -398,7 +399,7 @@ class ConfigPanel:
args: Union[str, None] = None, args: Union[str, None] = None,
args_file: Union[str, None] = None, args_file: Union[str, None] = None,
operation_logger: Union["OperationLogger", None] = None, operation_logger: Union["OperationLogger", None] = None,
): ) -> None:
self.filter_key = parse_filter_key(key) self.filter_key = parse_filter_key(key)
panel_id, section_id, option_id = self.filter_key panel_id, section_id, option_id = self.filter_key
@ -466,7 +467,7 @@ class ConfigPanel:
if operation_logger: if operation_logger:
operation_logger.success() operation_logger.success()
def list_actions(self): def list_actions(self) -> dict[str, str]:
actions = {} actions = {}
# FIXME : meh, loading the entire config panel is again going to cause # FIXME : meh, loading the entire config panel is again going to cause
@ -486,7 +487,7 @@ class ConfigPanel:
args: Union[str, None] = None, args: Union[str, None] = None,
args_file: Union[str, None] = None, args_file: Union[str, None] = None,
operation_logger: Union["OperationLogger", None] = None, operation_logger: Union["OperationLogger", None] = None,
): ) -> None:
# #
# FIXME : this stuff looks a lot like set() ... # FIXME : this stuff looks a lot like set() ...
# #
@ -666,7 +667,7 @@ class ConfigPanel:
def _ask( def _ask(
self, self,
config: ConfigPanelModel, config: ConfigPanelModel,
settings: "FormModel", form: "FormModel",
prefilled_answers: dict[str, Any] = {}, prefilled_answers: dict[str, Any] = {},
action_id: Union[str, None] = None, action_id: Union[str, None] = None,
hooks: "Hooks" = {}, hooks: "Hooks" = {},
@ -709,22 +710,22 @@ class ConfigPanel:
if option.type is not OptionType.button or option.id == action_id if option.type is not OptionType.button or option.id == action_id
] ]
settings = prompt_or_validate_form( form = prompt_or_validate_form(
options, options,
settings, form,
prefilled_answers=prefilled_answers, prefilled_answers=prefilled_answers,
context=context, context=context,
hooks=hooks, hooks=hooks,
) )
return settings return form
def _apply( def _apply(
self, self,
form: "FormModel", form: "FormModel",
previous_settings: dict[str, Any], previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
) -> dict[str, Any]: ) -> None:
""" """
Save settings in yaml file. Save settings in yaml file.
If `save_mode` is `"diff"` (which is the default), only values that are 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 # Save the settings to the .yaml file
write_to_yaml(self.save_path, current_settings) write_to_yaml(self.save_path, current_settings)
return current_settings def _run_action(self, form: "FormModel", action_id: str) -> None:
def _run_action(self, form: "FormModel", action_id: str):
raise NotImplementedError() raise NotImplementedError()
def _reload_services(self): def _reload_services(self) -> None:
from yunohost.service import service_reload_or_restart 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: if services_to_reload:
logger.info("Reloading services...") logger.info("Reloading services...")

View file

@ -32,7 +32,7 @@ from typing import (
Annotated, Annotated,
Any, Any,
Callable, Callable,
List, Iterable,
Literal, Literal,
Mapping, Mapping,
Type, Type,
@ -207,7 +207,7 @@ def js_to_python(expr):
return py_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(): if not expr.strip():
return False return False
node = ast.parse(js_to_python(expr), mode="eval").body node = ast.parse(js_to_python(expr), mode="eval").body
@ -650,7 +650,7 @@ class NumberOption(BaseInputOption):
_none_as_empty_str = False _none_as_empty_str = False
@staticmethod @staticmethod
def normalize(value, option={}): def normalize(value, option={}) -> Union[int, None]:
if isinstance(value, int): if isinstance(value, int):
return value return value
@ -704,7 +704,7 @@ class BooleanOption(BaseInputOption):
_none_as_empty_str = False _none_as_empty_str = False
@staticmethod @staticmethod
def humanize(value, option={}): def humanize(value, option={}) -> str:
option = option.dict() if isinstance(option, BaseOption) else option option = option.dict() if isinstance(option, BaseOption) else option
yes = option.get("yes", 1) yes = option.get("yes", 1)
@ -727,7 +727,7 @@ class BooleanOption(BaseInputOption):
) )
@staticmethod @staticmethod
def normalize(value, option={}): def normalize(value, option={}) -> Any:
option = option.dict() if isinstance(option, BaseOption) else option option = option.dict() if isinstance(option, BaseOption) else option
if isinstance(value, str): if isinstance(value, str):
@ -844,7 +844,7 @@ class WebPathOption(BaseInputOption):
_annotation = str _annotation = str
@staticmethod @staticmethod
def normalize(value, option={}): def normalize(value, option={}) -> str:
option = option.dict() if isinstance(option, BaseOption) else option option = option.dict() if isinstance(option, BaseOption) else option
if value is None: if value is None:
@ -892,14 +892,14 @@ class FileOption(BaseInputOption):
_upload_dirs: set[str] = set() _upload_dirs: set[str] = set()
@classmethod @classmethod
def clean_upload_dirs(cls): def clean_upload_dirs(cls) -> None:
# Delete files uploaded from API # Delete files uploaded from API
for upload_dir in cls._upload_dirs: for upload_dir in cls._upload_dirs:
if os.path.exists(upload_dir): if os.path.exists(upload_dir):
shutil.rmtree(upload_dir) shutil.rmtree(upload_dir)
@classmethod @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 from base64 import b64decode
if not value: if not value:
@ -967,7 +967,6 @@ class BaseChoicesOption(BaseInputOption):
choices = ( choices = (
self.choices if isinstance(self.choices, list) else self.choices.keys() 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 Literal[tuple(choices)]
return self._annotation return self._annotation
@ -1006,6 +1005,7 @@ class BaseChoicesOption(BaseInputOption):
class SelectOption(BaseChoicesOption): class SelectOption(BaseChoicesOption):
type: Literal[OptionType.select] = OptionType.select type: Literal[OptionType.select] = OptionType.select
filter: Literal[None] = None
choices: Union[dict[str, Any], list[Any]] choices: Union[dict[str, Any], list[Any]]
default: Union[str, None] default: Union[str, None]
_annotation = str _annotation = str
@ -1013,13 +1013,14 @@ class SelectOption(BaseChoicesOption):
class TagsOption(BaseChoicesOption): class TagsOption(BaseChoicesOption):
type: Literal[OptionType.tags] = OptionType.tags type: Literal[OptionType.tags] = OptionType.tags
filter: Literal[None] = None
choices: Union[list[str], None] = None choices: Union[list[str], None] = None
pattern: Union[Pattern, None] = None pattern: Union[Pattern, None] = None
default: Union[str, list[str], None] default: Union[str, list[str], None]
_annotation = str _annotation = str
@staticmethod @staticmethod
def humanize(value, option={}): def humanize(value, option={}) -> str:
if isinstance(value, list): if isinstance(value, list):
return ",".join(str(v) for v in value) return ",".join(str(v) for v in value)
if not value: if not value:
@ -1027,7 +1028,7 @@ class TagsOption(BaseChoicesOption):
return value return value
@staticmethod @staticmethod
def normalize(value, option={}): def normalize(value, option={}) -> str:
if isinstance(value, list): if isinstance(value, list):
return ",".join(str(v) for v in value) return ",".join(str(v) for v in value)
if isinstance(value, str): if isinstance(value, str):
@ -1037,7 +1038,7 @@ class TagsOption(BaseChoicesOption):
return value return value
@property @property
def _dynamic_annotation(self): def _dynamic_annotation(self) -> Type[str]:
# TODO use Literal when serialization is seperated from validation # TODO use Literal when serialization is seperated from validation
# if self.choices is not None: # if self.choices is not None:
# return Literal[tuple(self.choices)] # return Literal[tuple(self.choices)]
@ -1120,7 +1121,7 @@ class DomainOption(BaseChoicesOption):
return _get_maindomain() return _get_maindomain()
@staticmethod @staticmethod
def normalize(value, option={}): def normalize(value, option={}) -> str:
if value.startswith("https://"): if value.startswith("https://"):
value = value[len("https://") :] value = value[len("https://") :]
elif value.startswith("http://"): elif value.startswith("http://"):
@ -1314,7 +1315,9 @@ class OptionsModel(BaseModel):
options: list[Annotated[AnyOption, Field(discriminator="type")]] options: list[Annotated[AnyOption, Field(discriminator="type")]]
@staticmethod @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 [ return [
option option
| { | {
@ -1329,7 +1332,7 @@ class OptionsModel(BaseModel):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(options=self.options_dict_to_list(kwargs)) 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 Mutate in place translatable attributes of options to their translations
""" """
@ -1359,7 +1362,7 @@ class FormModel(BaseModel):
validate_assignment = True validate_assignment = True
extra = Extra.ignore extra = Extra.ignore
def __getitem__(self, name: str): def __getitem__(self, name: str) -> Any:
# FIXME # FIXME
# if a FormModel's required field is not instancied with a value, it is # if a FormModel's required field is not instancied with a value, it is
# not available as an attr and therefor triggers an `AttributeError` # not available as an attr and therefor triggers an `AttributeError`
@ -1372,7 +1375,7 @@ class FormModel(BaseModel):
return getattr(self, name) return getattr(self, name)
def __setitem__(self, name: str, value: Any): def __setitem__(self, name: str, value: Any) -> None:
setattr(self, name, value) setattr(self, name, value)
def get(self, attr: str, default: Any = None) -> Any: def get(self, attr: str, default: Any = None) -> Any:
@ -1382,7 +1385,9 @@ class FormModel(BaseModel):
return default 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. Returns a dynamic pydantic model class that can be used as a form.
Parsing/validation occurs at instanciation and assignements. Parsing/validation occurs at instanciation and assignements.
@ -1468,7 +1473,7 @@ MAX_RETRIES = 4
def prompt_or_validate_form( def prompt_or_validate_form(
options: list[AnyOption], options: Iterable[AnyOption],
form: FormModel, form: FormModel,
prefilled_answers: dict[str, Any] = {}, prefilled_answers: dict[str, Any] = {},
context: Context = {}, context: Context = {},
@ -1503,7 +1508,6 @@ def prompt_or_validate_form(
if isinstance(option, BaseReadonlyOption) or option.readonly: if isinstance(option, BaseReadonlyOption) or option.readonly:
if isinstance(option, BaseInputOption): if isinstance(option, BaseInputOption):
# FIXME normalized needed, form[option.id] should already be normalized
# only update the context with the value # only update the context with the value
context[option.id] = option.normalize(form[option.id]) context[option.id] = option.normalize(form[option.id])
@ -1623,7 +1627,7 @@ def ask_questions_and_parse_answers(
return (model.options, form) 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 = [] out = []
for raw_question in raw_questions: for raw_question in raw_questions: