mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
typing: add missing type + misc typing fixes
This commit is contained in:
parent
54cc23c90c
commit
37b4eb956d
6 changed files with 72 additions and 59 deletions
13
src/app.py
13
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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue