7
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-06-01 05:40:48 +00:00

fix: stricter type checking when loading plugins

This allows us to get rid of a few `cast(...)` statements.

This kind of check would have avoided this issue:
https://discuss.overhang.io/t/cant-enable-keycloak-oauth2-backend-with-yml-plugin/1380
This commit is contained in:
Régis Behmo 2021-03-29 09:48:53 +02:00
parent d184bb2bda
commit 740e6baf2e

View File

@ -3,9 +3,10 @@ from copy import deepcopy
from glob import glob
import importlib
import os
from typing import cast, Any, Dict, Iterator, List, Optional, Tuple, Type, Union
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
import appdirs
import click
import pkg_resources
from . import exceptions
@ -53,20 +54,125 @@ class BasePlugin:
def __init__(self, name: str, obj: Any) -> None:
self.name = name
self.config = cast(
Dict[str, Dict[str, Any]], get_callable_attr(obj, "config", {})
)
self.patches = cast(
Dict[str, str], get_callable_attr(obj, "patches", default={})
)
self.hooks = cast(
Dict[str, Union[Dict[str, str], List[str]]],
get_callable_attr(obj, "hooks", default={}),
)
self.templates_root = cast(
Optional[str], get_callable_attr(obj, "templates", default=None)
)
self.command = getattr(obj, "command", None)
self.config = self.load_config(obj, self.name)
self.patches = self.load_patches(obj, self.name)
self.hooks = self.load_hooks(obj, self.name)
templates_root = get_callable_attr(obj, "templates", default=None)
if templates_root is not None:
assert isinstance(templates_root, str)
self.templates_root = templates_root
command = getattr(obj, "command", None)
if command is not None:
assert isinstance(command, click.Command)
self.command: click.Command = command
@staticmethod
def load_config(obj: Any, plugin_name: str) -> Dict[str, Dict[str, Any]]:
"""
Load config and check types.
"""
config = get_callable_attr(obj, "config", {})
if not isinstance(config, dict):
raise exceptions.TutorError(
"Invalid config in plugin {}. Expected dict, got {}.".format(
plugin_name, config.__class__
)
)
for name, subconfig in config.items():
if not isinstance(name, str):
raise exceptions.TutorError(
"Invalid config entry '{}' in plugin {}. Expected str, got {}.".format(
name, plugin_name, config.__class__
)
)
if not isinstance(subconfig, dict):
raise exceptions.TutorError(
"Invalid config entry '{}' in plugin {}. Expected str keys, got {}.".format(
name, plugin_name, config.__class__
)
)
for key in subconfig.keys():
if not isinstance(key, str):
raise exceptions.TutorError(
"Invalid config entry '{}.{}' in plugin {}. Expected str, got {}.".format(
name, key, plugin_name, key.__class__
)
)
return config
@staticmethod
def load_patches(obj: Any, plugin_name: str) -> Dict[str, str]:
"""
Load patches and check the types are right.
"""
patches = get_callable_attr(obj, "patches", {})
if not isinstance(patches, dict):
raise exceptions.TutorError(
"Invalid patches in plugin {}. Expected dict, got {}.".format(
plugin_name, patches.__class__
)
)
for patch_name, content in patches.items():
if not isinstance(patch_name, str):
raise exceptions.TutorError(
"Invalid patch name '{}' in plugin {}. Expected str, got {}.".format(
patch_name, plugin_name, patch_name.__class__
)
)
if not isinstance(content, str):
raise exceptions.TutorError(
"Invalid patch '{}' in plugin {}. Expected str, got {}.".format(
patch_name, plugin_name, content.__class__
)
)
return patches
@staticmethod
def load_hooks(
obj: Any, plugin_name: str
) -> Dict[str, Union[Dict[str, str], List[str]]]:
"""
Load hooks and check types.
"""
hooks = get_callable_attr(obj, "hooks", default={})
if not isinstance(hooks, dict):
raise exceptions.TutorError(
"Invalid hooks in plugin {}. Expected dict, got {}.".format(
plugin_name, hooks.__class__
)
)
for hook_name, hook in hooks.items():
if not isinstance(hook_name, str):
raise exceptions.TutorError(
"Invalid hook name '{}' in plugin {}. Expected str, got {}.".format(
hook_name, plugin_name, hook_name.__class__
)
)
if isinstance(hook, list):
for service in hook:
if not isinstance(service, str):
raise exceptions.TutorError(
"Invalid service in hook '{}' from plugin {}. Expected str, got {}.".format(
hook_name, plugin_name, service.__class__
)
)
elif isinstance(hook, dict):
for name, value in hook.items():
if not isinstance(name, str) or not isinstance(value, str):
raise exceptions.TutorError(
"Invalid hook '{}' in plugin {}. Only str -> str entries are supported.".format(
hook_name, plugin_name
)
)
else:
raise exceptions.TutorError(
"Invalid hook '{}' in plugin {}. Expected dict or list, got {}.".format(
hook_name, plugin_name, hook.__class__
)
)
return hooks
def config_key(self, key: str) -> str:
"""
@ -201,7 +307,10 @@ class Plugins:
def __init__(self, config: Dict[str, Any]):
self.config = deepcopy(config)
# patches has the following structure:
# {patch_name -> {plugin_name -> "content"}}
self.patches: Dict[str, Dict[str, str]] = {}
# some hooks have a dict-like structure, like "build", others are list of services.
self.hooks: Dict[str, Dict[str, Union[Dict[str, str], List[str]]]] = {}
self.template_roots: Dict[str, str] = {}