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",
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:

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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...")

View file

@ -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: