Merge pull request #145 from YunoHost/refactoring2

Remove globals variables magic and add explicit import now
This commit is contained in:
Laurent Peuch 2017-08-05 11:48:06 +02:00 committed by GitHub
commit 1efa5c8b6e
17 changed files with 158 additions and 211 deletions

View file

@ -85,7 +85,7 @@ todo_include_todos = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'default'
html_theme = 'classic'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@ -105,11 +105,11 @@ html_static_path = ['_static']
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html', # needs 'show_related': True theme option to display
# 'about.html',
# 'navigation.html',
# 'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
'donate.html',
# 'donate.html',
]
}

View file

@ -16,6 +16,7 @@ a reference.
:maxdepth: 2
:caption: Contents:
m18n
utils/filesystem
utils/network
utils/process

38
doc/m18n.rst Normal file
View file

@ -0,0 +1,38 @@
Translations using the m18n object
==================================
The moulinette provides a way to do translations and YunoHost uses it. This is
done via the `m18n` object that you can import this way:
::
from moulinette import m18n
The `m18n` object comes with 2 method:
* `m18n.n` to uses for translations within YunoHost
* `m18n.g` to uses for translations within Moulinette itself
Their API is identical.
Here are example of uses:
::
m18n.n('some_translation_key')
m18n.g('some_translation_key')
m18n.n('some_translation_key', string_formating_argument_1=some_variable)
m18n.g('some_translation_key', string_formating_argument_1=some_variable)
The translation key must be present in `locales/en.json` of either YunoHost
(for `.n`) or moulinette (for `.g`).
Docstring
---------
As a reference, here are the docstrings of the m18n class:
.. autoclass:: moulinette.core.Moulinette18n
.. automethod:: moulinette.core.Moulinette18n.n
.. automethod:: moulinette.core.Moulinette18n.g

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from moulinette.core import init_interface, MoulinetteError
from moulinette.core import init_interface, MoulinetteError, MoulinetteSignals, Moulinette18n
from moulinette.globals import DATA_DIR, LIB_DIR, LOCALES_DIR, CACHE_DIR
__title__ = 'moulinette'
__version__ = '0.1'
@ -27,11 +28,17 @@ __credits__ = """
along with this program; if not, see http://www.gnu.org/licenses
"""
__all__ = [
'init', 'api', 'cli',
'init', 'api', 'cli', 'm18n',
'init_interface', 'MoulinetteError',
'DATA_DIR', 'LIB_DIR', 'LOCALES_DIR', 'CACHE_DIR',
]
msignals = MoulinetteSignals()
msettings = dict()
m18n = Moulinette18n()
# Package functions
def init(logging_config=None, **kwargs):
@ -50,22 +57,12 @@ def init(logging_config=None, **kwargs):
"""
import sys
import __builtin__
from moulinette.core import (
Package, Moulinette18n, MoulinetteSignals
)
from moulinette.utils.log import configure_logging
configure_logging(logging_config)
# Define and instantiate global objects
__builtin__.__dict__['pkg'] = Package(**kwargs)
__builtin__.__dict__['m18n'] = Moulinette18n(pkg)
__builtin__.__dict__['msignals'] = MoulinetteSignals()
__builtin__.__dict__['msettings'] = dict()
# Add library directory to python path
sys.path.insert(0, pkg.libdir)
sys.path.insert(0, LIB_DIR)
# Easy access to interfaces

View file

@ -9,6 +9,9 @@ import cPickle as pickle
from time import time
from collections import OrderedDict
from moulinette import m18n, msignals
from moulinette.cache import open_cachefile
from moulinette.globals import CACHE_DIR, DATA_DIR
from moulinette.core import (MoulinetteError, MoulinetteLock)
from moulinette.interfaces import (
BaseActionsMapParser, GLOBAL_SECTION, TO_RETURN_PROP
@ -373,10 +376,10 @@ class ActionsMap(object):
for n in namespaces:
logger.debug("loading actions map namespace '%s'", n)
actionsmap_yml = '%s/actionsmap/%s.yml' % (pkg.datadir, n)
actionsmap_yml = '%s/actionsmap/%s.yml' % (DATA_DIR, n)
actionsmap_yml_stat = os.stat(actionsmap_yml)
actionsmap_pkl = '%s/actionsmap/%s-%d-%d.pkl' % (
pkg.cachedir,
CACHE_DIR,
n,
actionsmap_yml_stat.st_size,
actionsmap_yml_stat.st_mtime
@ -498,7 +501,7 @@ class ActionsMap(object):
"""
namespaces = []
for f in os.listdir('%s/actionsmap' % pkg.datadir):
for f in os.listdir('%s/actionsmap' % DATA_DIR):
if f.endswith('.yml'):
namespaces.append(f[:-4])
return namespaces
@ -524,23 +527,23 @@ class ActionsMap(object):
logger.debug("generating cache for actions map namespace '%s'", n)
# Read actions map from yaml file
am_file = '%s/actionsmap/%s.yml' % (pkg.datadir, n)
am_file = '%s/actionsmap/%s.yml' % (DATA_DIR, n)
with open(am_file, 'r') as f:
actionsmaps[n] = ordered_yaml_load(f)
# at installation, cachedir might not exists
if os.path.exists('%s/actionsmap/' % pkg.cachedir):
if os.path.exists('%s/actionsmap/' % CACHE_DIR):
# clean old cached files
for i in os.listdir('%s/actionsmap/' % pkg.cachedir):
for i in os.listdir('%s/actionsmap/' % CACHE_DIR):
if i.endswith(".pkl"):
os.remove('%s/actionsmap/%s' % (pkg.cachedir, i))
os.remove('%s/actionsmap/%s' % (CACHE_DIR, i))
# Cache actions map into pickle file
am_file_stat = os.stat(am_file)
pkl = '%s-%d-%d.pkl' % (n, am_file_stat.st_size, am_file_stat.st_mtime)
with pkg.open_cachefile(pkl, 'w', subdir='actionsmap') as f:
with open_cachefile(pkl, 'w', subdir='actionsmap') as f:
pickle.dump(actionsmaps[n], f)
return actionsmaps

View file

@ -4,6 +4,8 @@ import errno
import gnupg
import logging
from moulinette import m18n
from moulinette.cache import open_cachefile
from moulinette.core import MoulinetteError
logger = logging.getLogger('moulinette.authenticator')
@ -129,8 +131,8 @@ class BaseAuthenticator(object):
def _open_sessionfile(self, session_id, mode='r'):
"""Open a session file for this instance in given mode"""
return pkg.open_cachefile('%s.asc' % session_id, mode,
subdir='session/%s' % self.name)
return open_cachefile('%s.asc' % session_id, mode,
subdir='session/%s' % self.name)
def _store_session(self, session_id, session_hash, password):
"""Store a session and its associated password"""

View file

@ -7,6 +7,7 @@ import logging
import ldap
import ldap.modlist as modlist
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.authenticators import BaseAuthenticator
@ -212,5 +213,5 @@ class Authenticator(BaseAuthenticator):
attr, value)
raise MoulinetteError(errno.EEXIST,
m18n.g('ldap_attribute_already_exists',
attribute=attr, value=value))
attribute=attr, value=value))
return True

43
moulinette/cache.py Normal file
View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
import os
from moulinette.globals import CACHE_DIR
def get_cachedir(subdir='', make_dir=True):
"""Get the path to a cache directory
Return the path to the cache directory from an optional
subdirectory and create it if needed.
Keyword arguments:
- subdir -- A cache subdirectory
- make_dir -- False to not make directory if it not exists
"""
path = os.path.join(CACHE_DIR, subdir)
if make_dir and not os.path.isdir(path):
os.makedirs(path)
return path
def open_cachefile(filename, mode='r', **kwargs):
"""Open a cache file and return a stream
Attempt to open in 'mode' the cache file 'filename' from the
default cache directory and in the subdirectory 'subdir' if
given. Directories are created if needed and a stream is
returned if the file can be written.
Keyword arguments:
- filename -- The cache filename
- mode -- The mode in which the file is opened
- **kwargs -- Optional arguments for get_cachedir
"""
# Set make_dir if not given
kwargs['make_dir'] = kwargs.get('make_dir',
True if mode[0] == 'w' else False)
return open('%s/%s' % (get_cachedir(**kwargs), filename), mode)

View file

@ -8,114 +8,14 @@ import logging
from importlib import import_module
import moulinette
from moulinette.globals import LOCALES_DIR, LIB_DIR
from moulinette.cache import get_cachedir
logger = logging.getLogger('moulinette.core')
# Package manipulation -------------------------------------------------
class Package(object):
"""Package representation and easy access methods
Initialize directories and variables for the package and give them
easy access.
Keyword arguments:
- _from_source -- Either the package is running from source or
not (only for debugging)
"""
def __init__(self, _from_source=False):
if _from_source:
import sys
logger.debug('initialize Package object running from source')
# Retrieve source's base directory
basedir = os.path.abspath(os.path.dirname(sys.argv[0]) + '/../')
# Set local directories
self._datadir = '%s/data' % basedir
self._libdir = '%s/lib' % basedir
self._localedir = '%s/locales' % basedir
self._cachedir = '%s/cache' % basedir
else:
import package
# Set system directories
self._datadir = package.datadir
self._libdir = package.libdir
self._localedir = package.localedir
self._cachedir = package.cachedir
def __setattr__(self, name, value):
if name[0] == '_' and name in self.__dict__:
# Deny reassignation of package directories
logger.error("cannot reassign Package variable '%s'", name)
return
self.__dict__[name] = value
# Easy access to package directories
@property
def datadir(self):
"""Return the data directory of the package"""
return self._datadir
@property
def libdir(self):
"""Return the lib directory of the package"""
return self._libdir
@property
def localedir(self):
"""Return the locale directory of the package"""
return self._localedir
@property
def cachedir(self):
"""Return the cache directory of the package"""
return self._cachedir
# Additional methods
def get_cachedir(self, subdir='', make_dir=True):
"""Get the path to a cache directory
Return the path to the cache directory from an optional
subdirectory and create it if needed.
Keyword arguments:
- subdir -- A cache subdirectory
- make_dir -- False to not make directory if it not exists
"""
path = os.path.join(self.cachedir, subdir)
if make_dir and not os.path.isdir(path):
os.makedirs(path)
return path
def open_cachefile(self, filename, mode='r', **kwargs):
"""Open a cache file and return a stream
Attempt to open in 'mode' the cache file 'filename' from the
default cache directory and in the subdirectory 'subdir' if
given. Directories are created if needed and a stream is
returned if the file can be written.
Keyword arguments:
- filename -- The cache filename
- mode -- The mode in which the file is opened
- **kwargs -- Optional arguments for get_cachedir
"""
# Set make_dir if not given
kwargs['make_dir'] = kwargs.get('make_dir',
True if mode[0] == 'w' else False)
return open('%s/%s' % (self.get_cachedir(**kwargs), filename), mode)
# Internationalization -------------------------------------------------
class Translator(object):
@ -187,20 +87,15 @@ class Translator(object):
- key -- The key to translate
"""
def _load_key(locale):
value = self._translations[locale][key]
return value.encode('utf-8').format(*args, **kwargs)
if key in self._translations.get(self.locale, {}):
return self._translations[self.locale][key].encode('utf-8').format(*args, **kwargs)
if self.default_locale != self.locale and key in self._translations.get(self.default_locale, {}):
logger.info("untranslated key '%s' for locale '%s'",
key, self.locale)
return self._translations[self.default_locale][key].encode('utf-8').format(*args, **kwargs)
try:
return _load_key(self.locale)
except (KeyError, IndexError):
if self.default_locale != self.locale:
logger.info("untranslated key '%s' for locale '%s'",
key, self.locale)
try:
return _load_key(self.default_locale)
except:
pass
logger.exception("unable to retrieve key '%s' for default locale '%s'",
key, self.default_locale)
return key
@ -244,23 +139,17 @@ class Moulinette18n(object):
"""
def __init__(self, package, default_locale='en'):
def __init__(self, default_locale='en'):
self.default_locale = default_locale
self.locale = default_locale
self.pkg = package
# Init global translator
self._global = Translator(self.pkg.localedir, default_locale)
self._global = Translator(LOCALES_DIR, default_locale)
# Define namespace related variables
self._namespaces = {}
self._current_namespace = None
@property
def _namespace(self):
"""Return current namespace's Translator object"""
return self._namespaces[self._current_namespace]
def load_namespace(self, namespace):
"""Load the namespace to use
@ -273,10 +162,10 @@ class Moulinette18n(object):
"""
if namespace not in self._namespaces:
# Create new Translator object
n = Translator('%s/%s/locales' % (self.pkg.libdir, namespace),
self.default_locale)
n.set_locale(self.locale)
self._namespaces[namespace] = n
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
@ -312,12 +201,7 @@ class Moulinette18n(object):
- key -- The key to translate
"""
try:
return self._namespace.translate(key, *args, **kwargs)
except:
logger.exception("cannot translate key '%s' for namespace '%s'",
key, self._current_namespace)
return key
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs)
class MoulinetteSignals(object):
@ -450,7 +334,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
mod = import_module('moulinette.interfaces.%s' % name)
except ImportError:
logger.exception("unable to load interface '%s'", name)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
else:
try:
# Retrieve interface classes
@ -458,7 +342,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
interface = mod.Interface
except AttributeError:
logger.exception("unable to retrieve classes of interface '%s'", name)
raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
raise MoulinetteError(errno.EIO, moulinette.m18n.g('error_see_log'))
# Instantiate or retrieve ActionsMap
if isinstance(actionsmap, dict):
@ -467,7 +351,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
amap = actionsmap
else:
logger.error("invalid actionsmap value %r", actionsmap)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
return interface(amap, **kwargs)
@ -488,7 +372,7 @@ def init_authenticator((vendor, name), kwargs={}):
mod = import_module('moulinette.authenticators.%s' % vendor)
except ImportError:
logger.exception("unable to load authenticator vendor '%s'", vendor)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
else:
return mod.Authenticator(name, **kwargs)
@ -504,7 +388,7 @@ def clean_session(session_id, profiles=[]):
- profiles -- A list of profiles to clean
"""
sessiondir = pkg.get_cachedir('session')
sessiondir = get_cachedir('session')
if not profiles:
profiles = os.listdir(sessiondir)
@ -578,7 +462,7 @@ class MoulinetteLock(object):
if self.timeout is not None and (time.time() - start_time) > self.timeout:
raise MoulinetteError(errno.EBUSY,
m18n.g('instance_already_running'))
moulinette.m18n.g('instance_already_running'))
# Wait before checking again
time.sleep(self.interval)
@ -605,8 +489,8 @@ class MoulinetteLock(object):
except IOError:
raise MoulinetteError(
errno.EPERM, '%s. %s.'.format(
m18n.g('permission_denied'),
m18n.g('root_required')))
moulinette.m18n.g('permission_denied'),
moulinette.m18n.g('root_required')))
def __enter__(self):
if not self._locked:

4
moulinette/globals.py Normal file
View file

@ -0,0 +1,4 @@
DATA_DIR = '/usr/share/moulinette'
LIB_DIR = '/usr/lib/moulinette'
LOCALES_DIR = '/usr/share/moulinette/locale'
CACHE_DIR = '/var/cache/moulinette'

View file

@ -7,6 +7,7 @@ import logging
import argparse
from collections import deque
from moulinette import msignals, msettings, m18n
from moulinette.core import (init_authenticator, MoulinetteError)
logger = logging.getLogger('moulinette.interface')

View file

@ -13,6 +13,7 @@ from geventwebsocket import WebSocketError
from bottle import run, request, response, Bottle, HTTPResponse
from moulinette import msignals, m18n, DATA_DIR
from moulinette.core import MoulinetteError, clean_session
from moulinette.interfaces import (
BaseActionsMapParser, BaseInterface, ExtendedArgumentParser,
@ -780,11 +781,11 @@ class Interface(BaseInterface):
"""
if category is None:
with open('%s/../doc/resources.json' % pkg.datadir) as f:
with open('%s/../doc/resources.json' % DATA_DIR) as f:
return f.read()
try:
with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f:
with open('%s/../doc/%s.json' % (DATA_DIR, category)) as f:
return f.read()
except IOError:
return None

View file

@ -11,6 +11,7 @@ from collections import OrderedDict
import argcomplete
from moulinette import msignals, m18n
from moulinette.core import MoulinetteError
from moulinette.interfaces import (
BaseActionsMapParser, BaseInterface, ExtendedArgumentParser,

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Public constants defined during build
"""Package's data directory (e.g. /usr/share/moulinette)"""
datadir = '%PKGDATADIR%'
"""Package's library directory (e.g. /usr/lib/moulinette)"""
libdir = '%PKGLIBDIR%'
"""Locale directory for the package (e.g. /usr/share/moulinette/locale)"""
localedir = '%PKGLOCALEDIR%'
"""Cache directory for the package (e.g. /var/cache/moulinette)"""
cachedir = '%PKGCACHEDIR%'

View file

@ -5,6 +5,8 @@ import json
import grp
from pwd import getpwnam
from moulinette import m18n
from moulinette.globals import CACHE_DIR
from moulinette.core import MoulinetteError
# Files & directories --------------------------------------------------

View file

@ -2,6 +2,7 @@ import errno
import requests
import json
from moulinette import m18n
from moulinette.core import MoulinetteError

View file

@ -3,36 +3,19 @@ import os
import sys
from distutils.core import setup
from distutils.dir_util import mkpath
from distutils.sysconfig import PREFIX
# Define package directories
datadir = os.path.join(PREFIX, 'share/moulinette')
libdir = os.path.join(PREFIX, 'lib/moulinette')
localedir = os.path.join(datadir, 'locale')
cachedir = '/var/cache/moulinette'
from moulinette.globals import LOCALES_DIR
# Extend installation
locale_files = []
if "install" in sys.argv:
# Evaluate locale files
for f in os.listdir('locales'):
if f.endswith('.json'):
locale_files.append('locales/%s' % f)
# Generate package.py
package = open('moulinette/package.py.in').read()
package = package.replace('%PKGDATADIR%', datadir) \
.replace('%PKGLIBDIR%', libdir) \
.replace('%PKGLOCALEDIR%', localedir) \
.replace('%PKGCACHEDIR%', cachedir)
with open('moulinette/package.py', 'w') as f:
f.write(package)
# Create needed directories
# mkpath(libdir, mode=0755, verbose=1)
# mkpath(os.path.join(datadir, 'actionsmap'), mode=0755, verbose=1)
setup(name='Moulinette',
version='2.0.0',
@ -47,5 +30,5 @@ setup(name='Moulinette',
'moulinette.interfaces',
'moulinette.utils',
],
data_files=[(localedir, locale_files)]
data_files=[(LOCALES_DIR, locale_files)]
)