Rework actionsmap and m18n init, drop multiple actionsmap support

This commit is contained in:
Alexandre Aubin 2021-11-16 18:15:09 +01:00
parent b2c67369a8
commit 9fcc9630bd
10 changed files with 185 additions and 303 deletions

View file

@ -3,7 +3,6 @@
from moulinette.core import ( from moulinette.core import (
MoulinetteError, MoulinetteError,
Moulinette18n, Moulinette18n,
env,
) )
__title__ = "moulinette" __title__ = "moulinette"
@ -54,35 +53,8 @@ class Moulinette:
return cls._interface return cls._interface
# Package functions
def init(logging_config=None, **kwargs):
"""Package initialization
Initialize directories and global variables. It must be called
before any of package method is used - even the easy access
functions.
Keyword arguments:
- logging_config -- A dict containing logging configuration to load
- **kwargs -- See core.Package
At the end, the global variable 'pkg' will contain a Package
instance. See core.Package for available methods and variables.
"""
import sys
from moulinette.utils.log import configure_logging
configure_logging(logging_config)
# Add library directory to python path
sys.path.insert(0, env["LIB_DIR"])
# Easy access to interfaces # Easy access to interfaces
def api(host="localhost", port=80, routes={}): def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None):
"""Web server (API) interface """Web server (API) interface
Run a HTTP server with the moulinette for an API usage. Run a HTTP server with the moulinette for an API usage.
@ -96,8 +68,16 @@ def api(host="localhost", port=80, routes={}):
""" """
from moulinette.interfaces.api import Interface as Api from moulinette.interfaces.api import Interface as Api
m18n.set_locales_dir(locales_dir)
try: try:
Api(routes=routes).run(host, port) Api(
routes=routes,
actionsmap=actionsmap,
).run(
host,
port
)
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging
@ -110,7 +90,7 @@ def api(host="localhost", port=80, routes={}):
return 0 return 0
def cli(args, top_parser, output_as=None, timeout=None): def cli(args, top_parser, output_as=None, timeout=None, actionsmap=None, locales_dir=None):
"""Command line interface """Command line interface
Execute an action with the moulinette from the CLI and print its Execute an action with the moulinette from the CLI and print its
@ -125,10 +105,18 @@ def cli(args, top_parser, output_as=None, timeout=None):
""" """
from moulinette.interfaces.cli import Interface as Cli from moulinette.interfaces.cli import Interface as Cli
m18n.set_locales_dir(locales_dir)
try: try:
load_only_category = args[0] if args and not args[0].startswith("-") else None load_only_category = args[0] if args and not args[0].startswith("-") else None
Cli(top_parser=top_parser, load_only_category=load_only_category).run( Cli(
args, output_as=output_as, timeout=timeout top_parser=top_parser,
load_only_category=load_only_category,
actionsmap=actionsmap,
).run(
args,
output_as=output_as,
timeout=timeout
) )
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging

View file

@ -16,7 +16,6 @@ from moulinette.core import (
MoulinetteError, MoulinetteError,
MoulinetteLock, MoulinetteLock,
MoulinetteValidationError, MoulinetteValidationError,
env,
) )
from moulinette.interfaces import BaseActionsMapParser, TO_RETURN_PROP from moulinette.interfaces import BaseActionsMapParser, TO_RETURN_PROP
from moulinette.utils.log import start_action_logging from moulinette.utils.log import start_action_logging
@ -382,9 +381,6 @@ class ActionsMap(object):
It is composed by categories which contain one or more action(s). It is composed by categories which contain one or more action(s).
Moreover, the action can have specific argument(s). Moreover, the action can have specific argument(s).
This class allows to manipulate one or several actions maps
associated to a namespace.
Keyword arguments: Keyword arguments:
- top_parser -- A BaseActionsMapParser-derived instance to use for - top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map parsing the actions map
@ -394,84 +390,71 @@ class ActionsMap(object):
purposes... purposes...
""" """
def __init__(self, top_parser, load_only_category=None): def __init__(self, actionsmap_yml, top_parser, load_only_category=None):
assert isinstance(top_parser, BaseActionsMapParser), ( assert isinstance(top_parser, BaseActionsMapParser), (
"Invalid parser class '%s'" % top_parser.__class__.__name__ "Invalid parser class '%s'" % top_parser.__class__.__name__
) )
DATA_DIR = env["DATA_DIR"]
CACHE_DIR = env["CACHE_DIR"]
actionsmaps = OrderedDict()
self.from_cache = False self.from_cache = False
# Iterate over actions map namespaces
for n in self.get_namespaces():
logger.debug("loading actions map namespace '%s'", n)
actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n) logger.debug("loading actions map")
actionsmap_yml_stat = os.stat(actionsmap_yml)
actionsmap_pkl = "%s/actionsmap/%s-%d-%d.pkl" % (
CACHE_DIR,
n,
actionsmap_yml_stat.st_size,
actionsmap_yml_stat.st_mtime,
)
def generate_cache(): actionsmap_yml_dir = os.path.dirname(actionsmap_yml)
actionsmap_yml_file = os.path.basename(actionsmap_yml)
actionsmap_yml_stat = os.stat(actionsmap_yml)
# Iterate over actions map namespaces actionsmap_pkl = f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.{actionsmap_yml_stat.st_size}-{actionsmap_yml_stat.st_mtime}.pkl"
logger.debug("generating cache for actions map namespace '%s'", n)
# Read actions map from yaml file def generate_cache():
actionsmap = read_yaml(actionsmap_yml)
# Delete old cache files logger.debug("generating cache for actions map")
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, n)):
os.remove(old_cache)
# at installation, cachedir might not exists # Read actions map from yaml file
dir_ = os.path.dirname(actionsmap_pkl) actionsmap = read_yaml(actionsmap_yml)
if not os.path.isdir(dir_):
os.makedirs(dir_)
# Cache actions map into pickle file # Delete old cache files
with open(actionsmap_pkl, "wb") as f: for old_cache in glob.glob(f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.*.pkl"):
pickle.dump(actionsmap, f) os.remove(old_cache)
return actionsmap # at installation, cachedir might not exists
dir_ = os.path.dirname(actionsmap_pkl)
if not os.path.isdir(dir_):
os.makedirs(dir_)
if os.path.exists(actionsmap_pkl): # Cache actions map into pickle file
try: with open(actionsmap_pkl, "wb") as f:
# Attempt to load cache pickle.dump(actionsmap, f)
with open(actionsmap_pkl, "rb") as f:
actionsmaps[n] = pickle.load(f)
self.from_cache = True return actionsmap
# TODO: Switch to python3 and catch proper exception
except (IOError, EOFError):
actionsmaps[n] = generate_cache()
else: # cache file doesn't exists
actionsmaps[n] = generate_cache()
# If load_only_category is set, and *if* the target category if os.path.exists(actionsmap_pkl):
# is in the actionsmap, we'll load only that one. try:
# If we filter it even if it doesn't exist, we'll end up with a # Attempt to load cache
# weird help message when we do a typo in the category name.. with open(actionsmap_pkl, "rb") as f:
if load_only_category and load_only_category in actionsmaps[n]: actionsmap = pickle.load(f)
actionsmaps[n] = {
k: v
for k, v in actionsmaps[n].items()
if k in [load_only_category, "_global"]
}
# Load translations self.from_cache = True
m18n.load_namespace(n) # TODO: Switch to python3 and catch proper exception
except (IOError, EOFError):
actionsmap = generate_cache()
else: # cache file doesn't exists
actionsmap = generate_cache()
# If load_only_category is set, and *if* the target category
# is in the actionsmap, we'll load only that one.
# If we filter it even if it doesn't exist, we'll end up with a
# weird help message when we do a typo in the category name..
if load_only_category and load_only_category in actionsmap:
actionsmap = {
k: v
for k, v in actionsmap.items()
if k in [load_only_category, "_global"]
}
# Generate parsers # Generate parsers
self.extraparser = ExtraArgumentParser(top_parser.interface) self.extraparser = ExtraArgumentParser(top_parser.interface)
self.parser = self._construct_parser(actionsmaps, top_parser) self.parser = self._construct_parser(actionsmap, top_parser)
def get_authenticator(self, auth_method): def get_authenticator(self, auth_method):
@ -479,7 +462,7 @@ class ActionsMap(object):
auth_method = self.default_authentication auth_method = self.default_authentication
# Load and initialize the authenticator module # Load and initialize the authenticator module
auth_module = "%s.authenticators.%s" % (self.main_namespace, auth_method) auth_module = "%s.authenticators.%s" % (self.namespace, auth_method)
logger.debug(f"Loading auth module {auth_module}") logger.debug(f"Loading auth module {auth_module}")
try: try:
mod = import_module(auth_module) mod = import_module(auth_module)
@ -591,7 +574,6 @@ class ActionsMap(object):
logger.debug("processing action [%s]: %s", log_id, full_action_name) logger.debug("processing action [%s]: %s", log_id, full_action_name)
# Load translation and process the action # Load translation and process the action
m18n.load_namespace(namespace)
start = time() start = time()
try: try:
return func(**arguments) return func(**arguments)
@ -599,43 +581,14 @@ class ActionsMap(object):
stop = time() stop = time()
logger.debug("action [%s] executed in %.3fs", log_id, stop - start) logger.debug("action [%s] executed in %.3fs", log_id, stop - start)
@staticmethod
def get_namespaces():
"""
Retrieve available actions map namespaces
Returns:
A list of available namespaces
"""
namespaces = []
DATA_DIR = env["DATA_DIR"]
# This var is ['*'] by default but could be set for example to
# ['yunohost', 'yml_*']
NAMESPACE_PATTERNS = env["NAMESPACES"].split()
# Look for all files that match the given patterns in the actionsmap dir
for namespace_pattern in NAMESPACE_PATTERNS:
namespaces.extend(
glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))
)
# Keep only the filenames with extension
namespaces = [os.path.basename(n)[:-4] for n in namespaces]
return namespaces
# Private methods # Private methods
def _construct_parser(self, actionsmaps, top_parser): def _construct_parser(self, actionsmap, top_parser):
""" """
Construct the parser with the actions map Construct the parser with the actions map
Keyword arguments: Keyword arguments:
- actionsmaps -- A dict of multi-level dictionnary of - actionsmap -- A dictionnary of categories/actions/arguments list
categories/actions/arguments list for each namespaces
- top_parser -- A BaseActionsMapParser-derived instance to use for - top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map parsing the actions map
@ -658,52 +611,85 @@ class ActionsMap(object):
# * namespace define the top "name", for us it will always be # * namespace define the top "name", for us it will always be
# "yunohost" and there well be only this one # "yunohost" and there well be only this one
# * actionsmap is the actual actionsmap that we care about # * actionsmap is the actual actionsmap that we care about
for namespace, actionsmap in actionsmaps.items():
# Retrieve global parameters
_global = actionsmap.pop("_global", {})
if _global: # Retrieve global parameters
if getattr(self, "main_namespace", None) is not None: _global = actionsmap.pop("_global", {})
raise MoulinetteError(
"It is not possible to have several namespaces with a _global section"
)
else:
self.main_namespace = namespace
self.name = _global["name"]
self.default_authentication = _global["authentication"][
interface_type
]
if top_parser.has_global_parser(): self.namespace = _global["namespace"]
top_parser.add_global_arguments(_global["arguments"]) self.cookie_name = _global["cookie_name"]
self.default_authentication = _global["authentication"][
interface_type
]
if not hasattr(self, "main_namespace"): if top_parser.has_global_parser():
raise MoulinetteError("Did not found the main namespace", raw_msg=True) top_parser.add_global_arguments(_global["arguments"])
for namespace, actionsmap in actionsmaps.items(): # category_name is stuff like "user", "domain", "hooks"...
# category_name is stuff like "user", "domain", "hooks"... # category_values is the values of this category (like actions)
# category_values is the values of this category (like actions) for category_name, category_values in actionsmap.items():
for category_name, category_values in actionsmap.items():
actions = category_values.pop("actions", {}) actions = category_values.pop("actions", {})
subcategories = category_values.pop("subcategories", {}) subcategories = category_values.pop("subcategories", {})
# Get category parser # Get category parser
category_parser = top_parser.add_category_parser( category_parser = top_parser.add_category_parser(
category_name, **category_values category_name, **category_values
)
# action_name is like "list" of "domain list"
# action_options are the values
for action_name, action_options in actions.items():
arguments = action_options.pop("arguments", {})
authentication = action_options.pop("authentication", {})
tid = (self.namespace, category_name, action_name)
# Get action parser
action_parser = category_parser.add_action_parser(
action_name, tid, **action_options
) )
# action_name is like "list" of "domain list" if action_parser is None: # No parser for the action
continue
# Store action identifier and add arguments
action_parser.set_defaults(_tid=tid)
action_parser.add_arguments(
arguments,
extraparser=self.extraparser,
format_arg_names=top_parser.format_arg_names,
validate_extra=validate_extra,
)
action_parser.authentication = self.default_authentication
if interface_type in authentication:
action_parser.authentication = authentication[interface_type]
# subcategory_name is like "cert" in "domain cert status"
# subcategory_values is the values of this subcategory (like actions)
for subcategory_name, subcategory_values in subcategories.items():
actions = subcategory_values.pop("actions")
# Get subcategory parser
subcategory_parser = category_parser.add_subcategory_parser(
subcategory_name, **subcategory_values
)
# action_name is like "status" of "domain cert status"
# action_options are the values # action_options are the values
for action_name, action_options in actions.items(): for action_name, action_options in actions.items():
arguments = action_options.pop("arguments", {}) arguments = action_options.pop("arguments", {})
authentication = action_options.pop("authentication", {}) authentication = action_options.pop("authentication", {})
tid = (namespace, category_name, action_name) tid = (self.namespace, category_name, subcategory_name, action_name)
# Get action parser try:
action_parser = category_parser.add_action_parser( # Get action parser
action_name, tid, **action_options action_parser = subcategory_parser.add_action_parser(
) action_name, tid, **action_options
)
except AttributeError:
# No parser for the action
continue
if action_parser is None: # No parser for the action if action_parser is None: # No parser for the action
continue continue
@ -719,52 +705,9 @@ class ActionsMap(object):
action_parser.authentication = self.default_authentication action_parser.authentication = self.default_authentication
if interface_type in authentication: if interface_type in authentication:
action_parser.authentication = authentication[interface_type] action_parser.authentication = authentication[
interface_type
# subcategory_name is like "cert" in "domain cert status" ]
# subcategory_values is the values of this subcategory (like actions)
for subcategory_name, subcategory_values in subcategories.items():
actions = subcategory_values.pop("actions")
# Get subcategory parser
subcategory_parser = category_parser.add_subcategory_parser(
subcategory_name, **subcategory_values
)
# action_name is like "status" of "domain cert status"
# action_options are the values
for action_name, action_options in actions.items():
arguments = action_options.pop("arguments", {})
authentication = action_options.pop("authentication", {})
tid = (namespace, category_name, subcategory_name, action_name)
try:
# Get action parser
action_parser = subcategory_parser.add_action_parser(
action_name, tid, **action_options
)
except AttributeError:
# No parser for the action
continue
if action_parser is None: # No parser for the action
continue
# Store action identifier and add arguments
action_parser.set_defaults(_tid=tid)
action_parser.add_arguments(
arguments,
extraparser=self.extraparser,
format_arg_names=top_parser.format_arg_names,
validate_extra=validate_extra,
)
action_parser.authentication = self.default_authentication
if interface_type in authentication:
action_parser.authentication = authentication[
interface_type
]
logger.debug("building parser took %.3fs", time() - start) logger.debug("building parser took %.3fs", time() - start)
return top_parser return top_parser

View file

@ -9,24 +9,10 @@ import moulinette
logger = logging.getLogger("moulinette.core") logger = logging.getLogger("moulinette.core")
env = {
"DATA_DIR": "/usr/share/moulinette",
"LIB_DIR": "/usr/lib/moulinette",
"LOCALES_DIR": "/usr/share/moulinette/locale",
"CACHE_DIR": "/var/cache/moulinette",
"NAMESPACES": "*", # By default we'll load every namespace we find
}
for key in env.keys():
value_from_environ = os.environ.get(f"MOULINETTE_{key}")
if value_from_environ:
env[key] = value_from_environ
def during_unittests_run(): def during_unittests_run():
return "TESTS_RUN" in os.environ return "TESTS_RUN" in os.environ
# Internationalization ------------------------------------------------- # Internationalization -------------------------------------------------
@ -51,11 +37,7 @@ class Translator(object):
# Attempt to load default translations # Attempt to load default translations
if not self._load_translations(default_locale): if not self._load_translations(default_locale):
logger.error( logger.error(
"unable to load locale '%s' from '%s'. Does the file '%s/%s.json' exists?", f"unable to load locale '{default_locale}' from '{locale_dir}'. Does the file '{locale_dir}/{default_locale}.json' exists?",
default_locale,
locale_dir,
locale_dir,
default_locale,
) )
self.default_locale = default_locale self.default_locale = default_locale
@ -207,44 +189,23 @@ class Moulinette18n(object):
self.default_locale = default_locale self.default_locale = default_locale
self.locale = default_locale self.locale = default_locale
self.locales_dir = env["LOCALES_DIR"]
# Init global translator # Init global translator
self._global = Translator(self.locales_dir, default_locale) global_locale_dir = "/usr/share/moulinette/locales"
if during_unittests_run():
global_locale_dir = os.path.dirname(__file__) + "/../locales"
# Define namespace related variables self._global = Translator(global_locale_dir, default_locale)
self._namespaces = {}
self._current_namespace = None
def load_namespace(self, namespace): def set_locales_dir(self, locales_dir):
"""Load the namespace to use
Load and set translations of a given namespace. Those translations self.translator = Translator(locales_dir, self.default_locale)
are accessible with Moulinette18n.n().
Keyword arguments:
- namespace -- The namespace to load
"""
if namespace not in self._namespaces:
# Create new Translator object
lib_dir = env["LIB_DIR"]
translator = Translator(
"%s/%s/locales" % (lib_dir, namespace), self.default_locale
)
translator.set_locale(self.locale)
self._namespaces[namespace] = translator
# Set current namespace
self._current_namespace = namespace
def set_locale(self, locale): def set_locale(self, locale):
"""Set the locale to use""" """Set the locale to use"""
self.locale = locale
self.locale = locale
self._global.set_locale(locale) self._global.set_locale(locale)
for n in self._namespaces.values(): self.translator.set_locale(locale)
n.set_locale(locale)
def g(self, key: str, *args, **kwargs) -> str: def g(self, key: str, *args, **kwargs) -> str:
"""Retrieve proper translation for a moulinette key """Retrieve proper translation for a moulinette key
@ -269,7 +230,7 @@ class Moulinette18n(object):
- key -- The key to translate - key -- The key to translate
""" """
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs) return self.translator.translate(key, *args, **kwargs)
def key_exists(self, key: str) -> bool: def key_exists(self, key: str) -> bool:
"""Check if a key exists in the translation files """Check if a key exists in the translation files
@ -278,7 +239,7 @@ class Moulinette18n(object):
- key -- The key to translate - key -- The key to translate
""" """
return self._namespaces[self._current_namespace].key_exists(key) return self.translator.key_exists(key)
# Moulinette core classes ---------------------------------------------- # Moulinette core classes ----------------------------------------------

View file

@ -237,14 +237,14 @@ class _HTTPArgumentParser(object):
class Session: class Session:
secret = random_ascii() secret = random_ascii()
actionsmap_name = None # This is later set to the actionsmap name cookie_name = None # This is later set to the actionsmap name
def set_infos(infos): def set_infos(infos):
assert isinstance(infos, dict) assert isinstance(infos, dict)
response.set_cookie( response.set_cookie(
f"session.{Session.actionsmap_name}", f"session.{Session.cookie_name}",
infos, infos,
secure=True, secure=True,
secret=Session.secret, secret=Session.secret,
@ -256,7 +256,7 @@ class Session:
try: try:
infos = request.get_cookie( infos = request.get_cookie(
f"session.{Session.actionsmap_name}", secret=Session.secret, default={} f"session.{Session.cookie_name}", secret=Session.secret, default={}
) )
except Exception: except Exception:
if not raise_if_no_session_exists: if not raise_if_no_session_exists:
@ -271,8 +271,8 @@ class Session:
@staticmethod @staticmethod
def delete_infos(): def delete_infos():
response.set_cookie(f"session.{Session.actionsmap_name}", "", max_age=-1) response.set_cookie(f"session.{Session.cookie_name}", "", max_age=-1)
response.delete_cookie(f"session.{Session.actionsmap_name}") response.delete_cookie(f"session.{Session.cookie_name}")
class _ActionsMapPlugin(object): class _ActionsMapPlugin(object):
@ -294,7 +294,7 @@ class _ActionsMapPlugin(object):
self.actionsmap = actionsmap self.actionsmap = actionsmap
self.log_queues = log_queues self.log_queues = log_queues
Session.actionsmap_name = actionsmap.name Session.cookie_name = actionsmap.cookie_name
def setup(self, app): def setup(self, app):
"""Setup plugin on the application """Setup plugin on the application
@ -734,9 +734,9 @@ class Interface:
type = "api" type = "api"
def __init__(self, routes={}): def __init__(self, routes={}, actionsmap=None):
actionsmap = ActionsMap(ActionsMapParser()) actionsmap = ActionsMap(actionsmap, ActionsMapParser())
# Attempt to retrieve log queues from an APIQueueHandler # Attempt to retrieve log queues from an APIQueueHandler
handler = log.getHandlersByClass(APIQueueHandler, limit=1) handler = log.getHandlersByClass(APIQueueHandler, limit=1)

View file

@ -461,12 +461,13 @@ class Interface:
type = "cli" type = "cli"
def __init__(self, top_parser=None, load_only_category=None): def __init__(self, top_parser=None, load_only_category=None, actionsmap=None, locales_dir=None):
# Set user locale # Set user locale
m18n.set_locale(get_locale()) m18n.set_locale(get_locale())
self.actionsmap = ActionsMap( self.actionsmap = ActionsMap(
actionsmap,
ActionsMapParser(top_parser=top_parser), ActionsMapParser(top_parser=top_parser),
load_only_category=load_only_category, load_only_category=load_only_category,
) )

View file

@ -3,5 +3,4 @@ addopts = --cov=moulinette -s -v --no-cov-on-fail
norecursedirs = dist doc build .tox .eggs norecursedirs = dist doc build .tox .eggs
testpaths = test/ testpaths = test/
env = env =
MOULINETTE_LOCALES_DIR = {PWD}/locales
TESTS_RUN = True TESTS_RUN = True

View file

@ -5,7 +5,6 @@ import sys
import subprocess import subprocess
from setuptools import setup, find_packages from setuptools import setup, find_packages
from moulinette import env
version = ( version = (
subprocess.check_output( subprocess.check_output(
@ -15,8 +14,6 @@ version = (
.strip() .strip()
) )
LOCALES_DIR = env["LOCALES_DIR"]
# Extend installation # Extend installation
locale_files = [] locale_files = []
@ -62,7 +59,7 @@ setup(
url="https://yunohost.org", url="https://yunohost.org",
license="AGPL", license="AGPL",
packages=find_packages(exclude=["test"]), packages=find_packages(exclude=["test"]),
data_files=[(LOCALES_DIR, locale_files)], data_files=[("/usr/share/moulinette/locales", locale_files)],
python_requires=">=3.7.*, <3.10", python_requires=">=3.7.*, <3.10",
install_requires=install_deps, install_requires=install_deps,
tests_require=test_deps, tests_require=test_deps,

View file

@ -3,7 +3,8 @@
# Global parameters # # Global parameters #
############################# #############################
_global: _global:
name: moulitest namespace: moulitest
cookie_name: moulitest
authentication: authentication:
api: dummy api: dummy
cli: dummy cli: dummy

View file

@ -1,24 +1,15 @@
"""Pytest fixtures for testing.""" """Pytest fixtures for testing."""
import sys
import toml import toml
import yaml import yaml
import json import json
import os import os
import shutil import shutil
import pytest import pytest
def patch_init(moulinette):
"""Configure moulinette to use the YunoHost namespace."""
old_init = moulinette.core.Moulinette18n.__init__
def monkey_path_i18n_init(self, package, default_locale="en"):
old_init(self, package, default_locale)
self.load_namespace("moulinette")
moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init
def patch_translate(moulinette): def patch_translate(moulinette):
"""Configure translator to raise errors when there are missing keys.""" """Configure translator to raise errors when there are missing keys."""
old_translate = moulinette.core.Translator.translate old_translate = moulinette.core.Translator.translate
@ -38,7 +29,7 @@ def patch_translate(moulinette):
moulinette.core.Moulinette18n.g = new_m18nn moulinette.core.Moulinette18n.g = new_m18nn
def patch_logging(moulinette): def logging_configuration(moulinette):
"""Configure logging to use the custom logger.""" """Configure logging to use the custom logger."""
handlers = set(["tty", "api"]) handlers = set(["tty", "api"])
root_handlers = set(handlers) root_handlers = set(handlers)
@ -85,27 +76,28 @@ def patch_lock(moulinette):
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def moulinette(tmp_path_factory): def moulinette(tmp_path_factory):
import moulinette import moulinette
import moulinette.core
from moulinette.utils.log import configure_logging
# Can't call the namespace just 'test' because # Can't call the namespace just 'test' because
# that would lead to some "import test" not importing the right stuff # that would lead to some "import test" not importing the right stuff
namespace = "moulitest" namespace = "moulitest"
tmp_cache = str(tmp_path_factory.mktemp("cache")) tmp_dir = str(tmp_path_factory.mktemp(namespace))
tmp_data = str(tmp_path_factory.mktemp("data")) shutil.copy("./test/actionsmap/moulitest.yml", f"{tmp_dir}/moulitest.yml")
tmp_lib = str(tmp_path_factory.mktemp("lib")) shutil.copytree("./test/src", f"{tmp_dir}/lib/{namespace}/")
moulinette.env["CACHE_DIR"] = tmp_cache shutil.copytree("./test/locales", f"{tmp_dir}/locales")
moulinette.env["DATA_DIR"] = tmp_data sys.path.insert(0, f"{tmp_dir}/lib")
moulinette.env["LIB_DIR"] = tmp_lib
shutil.copytree("./test/actionsmap", "%s/actionsmap" % tmp_data)
shutil.copytree("./test/src", "%s/%s" % (tmp_lib, namespace))
shutil.copytree("./test/locales", "%s/%s/locales" % (tmp_lib, namespace))
patch_init(moulinette)
patch_translate(moulinette) patch_translate(moulinette)
patch_lock(moulinette) patch_lock(moulinette)
logging = patch_logging(moulinette)
moulinette.init(logging_config=logging, _from_source=False) configure_logging(logging_configuration(moulinette))
moulinette.m18n.set_locales_dir(f"{tmp_dir}/locales")
# Dirty hack to pass this path to Api() and Cli() init later
moulinette._actionsmap_path = f"{tmp_dir}/moulitest.yml"
return moulinette return moulinette
@ -125,7 +117,7 @@ def moulinette_webapi(moulinette):
from moulinette.interfaces.api import Interface as Api from moulinette.interfaces.api import Interface as Api
return TestApp(Api(routes={})._app) return TestApp(Api(routes={}, actionsmap=moulinette._actionsmap_path)._app)
@pytest.fixture @pytest.fixture
@ -142,7 +134,7 @@ def moulinette_cli(moulinette, mocker):
mocker.patch("os.isatty", return_value=True) mocker.patch("os.isatty", return_value=True)
from moulinette.interfaces.cli import Interface as Cli from moulinette.interfaces.cli import Interface as Cli
cli = Cli(top_parser=parser) cli = Cli(top_parser=parser, actionsmap=moulinette._actionsmap_path)
mocker.stopall() mocker.stopall()
return cli return cli

View file

@ -164,7 +164,7 @@ def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
from moulinette.interfaces.api import ActionsMapParser from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser()) amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
with pytest.raises(MoulinetteError) as exception: with pytest.raises(MoulinetteError) as exception:
amap.get_authenticator("unknown") amap.get_authenticator("unknown")
@ -233,9 +233,9 @@ def test_actions_map_api():
from moulinette.interfaces.api import ActionsMapParser from moulinette.interfaces.api import ActionsMapParser
parser = ActionsMapParser() parser = ActionsMapParser()
amap = ActionsMap(parser) amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
assert amap.main_namespace == "moulitest" assert amap.namespace == "moulitest"
assert amap.default_authentication == "dummy" assert amap.default_authentication == "dummy"
assert ("GET", "/test-auth/default") in amap.parser.routes assert ("GET", "/test-auth/default") in amap.parser.routes
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
@ -248,7 +248,7 @@ def test_actions_map_api():
def test_actions_map_import_error(mocker): def test_actions_map_import_error(mocker):
from moulinette.interfaces.api import ActionsMapParser from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser()) amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
from moulinette.core import MoulinetteLock from moulinette.core import MoulinetteLock
@ -287,9 +287,9 @@ def test_actions_map_cli():
) )
parser = ActionsMapParser(top_parser=top_parser) parser = ActionsMapParser(top_parser=top_parser)
amap = ActionsMap(parser) amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
assert amap.main_namespace == "moulitest" assert amap.namespace == "moulitest"
assert amap.default_authentication == "dummy" assert amap.default_authentication == "dummy"
assert "testauth" in amap.parser._subparsers.choices assert "testauth" in amap.parser._subparsers.choices
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices