Include subdirectories tryton.cfg when activating a module
Closes #14625
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Allow to include subdirectories in tryton.cfg
|
||||
* Add route for login / logout with cookie
|
||||
* Add contextual ``_log`` to force logging events
|
||||
* Add notify_user to ModelStorage
|
||||
|
||||
@@ -54,6 +54,15 @@ contain a ``[tryton]`` section with the following keys:
|
||||
They will be loaded in the given order when the module is activated or
|
||||
updated.
|
||||
|
||||
``include_dirs``
|
||||
This is a list of directories containing additional :file:`tryton.cfg`
|
||||
files, organised by line.
|
||||
These files follow the same structure, excluding ``version``, ``depends``
|
||||
and ``extras_depend``.
|
||||
|
||||
``test_include_dirs``
|
||||
It's the same as ``include_dirs``, but only applies when running tests.
|
||||
|
||||
It may contain some ``[register]`` or ``[register <module> <module>]`` sections
|
||||
with the keys ``model``, ``wizard`` and ``report`` defining the ``type_`` and
|
||||
qualified name of class relative to the module as a one per line list to pass
|
||||
|
||||
@@ -30,30 +30,60 @@ MODULES = []
|
||||
|
||||
|
||||
@cache
|
||||
def parse_module_config(name):
|
||||
def parse_module_config(name, path=()):
|
||||
"Return a ConfigParser instance and directory of the module"
|
||||
config = configparser.ConfigParser()
|
||||
config.optionxform = lambda option: option
|
||||
with tools.file_open(os.path.join(name, 'tryton.cfg')) as fp:
|
||||
with tools.file_open(os.path.join(name, *path, 'tryton.cfg')) as fp:
|
||||
config.read_file(fp)
|
||||
directory = os.path.dirname(fp.name)
|
||||
return config, directory
|
||||
|
||||
|
||||
def get_module_info(name):
|
||||
def _get_subdirs(module_config, with_test):
|
||||
tryton_section = module_config['tryton']
|
||||
if 'include_dirs' in tryton_section:
|
||||
included_dirs = tryton_section['include_dirs'].strip().splitlines()
|
||||
else:
|
||||
included_dirs = []
|
||||
if with_test and 'test_include_dirs' in tryton_section:
|
||||
included_dirs.extend(
|
||||
tryton_section['test_include_dirs'].strip().splitlines())
|
||||
return included_dirs
|
||||
|
||||
|
||||
def get_module_info(name, path=(), with_test=False):
|
||||
"Return the content of the tryton.cfg"
|
||||
module_config, directory = parse_module_config(name)
|
||||
module_config, directory = parse_module_config(name, path)
|
||||
if module_config is None:
|
||||
return
|
||||
info = dict(module_config.items('tryton'))
|
||||
info['directory'] = directory
|
||||
for key in ('depends', 'extras_depend', 'xml'):
|
||||
if key in info:
|
||||
info[key] = info[key].strip().splitlines()
|
||||
if key == 'xml':
|
||||
info[key] = [os.path.join(*path, f) for f in info[key]]
|
||||
|
||||
for directory in _get_subdirs(module_config, with_test):
|
||||
mod_info = get_module_info(name, (*path, directory), with_test)
|
||||
if not mod_info:
|
||||
continue
|
||||
for key in ['xml']:
|
||||
if key in mod_info:
|
||||
if key in info:
|
||||
info[key] += mod_info[key]
|
||||
else:
|
||||
info[key] = mod_info[key]
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def get_module_register(name):
|
||||
def get_module_register(name, path=(), with_test=False):
|
||||
"Return classes to register from tryton.cfg"
|
||||
module_config, _ = parse_module_config(name)
|
||||
module_config, _ = parse_module_config(name, path)
|
||||
if module_config is None:
|
||||
return
|
||||
for section in module_config.sections():
|
||||
if section == 'register' or section.startswith('register '):
|
||||
depends = section[len('register'):].strip().split()
|
||||
@@ -62,21 +92,32 @@ def get_module_register(name):
|
||||
continue
|
||||
classes = module_config.get(
|
||||
section, type_).strip().splitlines()
|
||||
if path:
|
||||
prefix = ".".join(path)
|
||||
classes = [f'{prefix}.{c}' for c in classes]
|
||||
yield classes, {
|
||||
'module': name,
|
||||
'type_': type_,
|
||||
'depends': depends,
|
||||
}
|
||||
for directory in _get_subdirs(module_config, with_test):
|
||||
yield from get_module_register(
|
||||
name, (*path, directory), with_test)
|
||||
|
||||
|
||||
def get_module_register_mixin(name):
|
||||
def get_module_register_mixin(name, path=(), with_test=False):
|
||||
"Return classes to register_mixin from tryton.cfg"
|
||||
module_config, _ = parse_module_config(name)
|
||||
module_config, _ = parse_module_config(name, path)
|
||||
if module_config is None:
|
||||
return
|
||||
if module_config.has_section('register_mixin'):
|
||||
for mixin, classinfo in module_config.items('register_mixin'):
|
||||
yield [mixin, classinfo], {
|
||||
'module': name,
|
||||
}
|
||||
for directory in _get_subdirs(module_config, with_test):
|
||||
yield from get_module_register_mixin(
|
||||
name, (*path, directory), with_test)
|
||||
|
||||
|
||||
class Graph(dict):
|
||||
@@ -125,11 +166,11 @@ class Node(list):
|
||||
super().append(node)
|
||||
|
||||
|
||||
def create_graph(modules):
|
||||
def create_graph(modules, with_test=False):
|
||||
all_deps = set()
|
||||
graph = Graph()
|
||||
for module in modules:
|
||||
info = get_module_info(module)
|
||||
info = get_module_info(module, with_test=with_test)
|
||||
deps = info.get('depends', []) + [
|
||||
d for d in info.get('extras_depend', []) if d in modules]
|
||||
node = graph.add(module, deps)
|
||||
@@ -394,15 +435,18 @@ def register_classes(with_test=False):
|
||||
logger.info("tests register")
|
||||
trytond.tests.register()
|
||||
|
||||
for node in create_graph(get_modules(with_test=with_test)):
|
||||
for node in create_graph(
|
||||
get_modules(with_test=with_test), with_test=with_test):
|
||||
module_name = node.name
|
||||
if module_name in base_modules:
|
||||
MODULES.append(module_name)
|
||||
continue
|
||||
logger.info("%s register", module_name)
|
||||
for args, kwargs in get_module_register(module_name):
|
||||
for args, kwargs in get_module_register(
|
||||
module_name, with_test=with_test):
|
||||
Pool.register(*args, **kwargs)
|
||||
for args, kwargs in get_module_register_mixin(module_name):
|
||||
for args, kwargs in get_module_register_mixin(
|
||||
module_name, with_test=with_test):
|
||||
Pool.register_mixin(*args, **kwargs)
|
||||
module = tools.import_module(module_name)
|
||||
if hasattr(module, 'register'):
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of this
|
||||
# repository contains the full copyright notices and license terms.
|
||||
|
||||
import io
|
||||
import os.path
|
||||
import pathlib
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from trytond.modules import (
|
||||
get_module_info, get_module_register, get_module_register_mixin)
|
||||
from trytond.tests.test_tryton import TestCase
|
||||
|
||||
FLAT_MODULE_CFG = """
|
||||
[tryton]
|
||||
depends:
|
||||
flat_a
|
||||
flat_b
|
||||
extras_depend:
|
||||
flat_extra1
|
||||
flat_extra2
|
||||
xml:
|
||||
flat_file1.xml
|
||||
flat_file2.xml
|
||||
test_include_dirs:
|
||||
tests
|
||||
[register]
|
||||
model:
|
||||
flat_model.Model
|
||||
"""
|
||||
|
||||
TEST_MODULE_CFG = """
|
||||
[tryton]
|
||||
xml:
|
||||
test_file.xml
|
||||
[register]
|
||||
model:
|
||||
test_model.TestModel
|
||||
"""
|
||||
|
||||
NESTED_MODULE_CFG = """
|
||||
[tryton]
|
||||
depends:
|
||||
root_a
|
||||
root_b
|
||||
xml:
|
||||
root_file1.xml
|
||||
include_dirs:
|
||||
sub1
|
||||
sub2
|
||||
|
||||
[register]
|
||||
model:
|
||||
root_model.A
|
||||
wizard:
|
||||
root_wizard.wiz_A
|
||||
|
||||
[register_mixin]
|
||||
test.TestMixin: path.to.nested.mixin
|
||||
"""
|
||||
|
||||
NESTED_MODULE_SUB1_CFG = """
|
||||
[tryton]
|
||||
depends:
|
||||
nested_s1_1
|
||||
nested_s1_2
|
||||
xml:
|
||||
nested_file_s1.xml
|
||||
"""
|
||||
|
||||
NESTED_MODULE_SUB2_CFG = """
|
||||
[tryton]
|
||||
xml:
|
||||
nested_file_s2.xml
|
||||
include_dirs:
|
||||
sub
|
||||
|
||||
[register]
|
||||
report:
|
||||
nested_report.report_sub2
|
||||
|
||||
[register_mixin]
|
||||
test.TestMixin: path.to.sub2.mixin
|
||||
"""
|
||||
|
||||
NESTED_MODULE_SUB2SUB_CFG = """
|
||||
[tryton]
|
||||
xml:
|
||||
nested_file_s2_s.xml
|
||||
|
||||
[register depend_s2s]
|
||||
model:
|
||||
nested_model.s2s_A
|
||||
"""
|
||||
|
||||
|
||||
class ModuleRegisterTestCase(TestCase):
|
||||
"Test module registration"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp_modules = tempfile.TemporaryDirectory()
|
||||
|
||||
modules = pathlib.Path(cls.tmp_modules.name)
|
||||
flat = modules / 'flat'
|
||||
tests = flat / 'tests'
|
||||
nested = modules / 'nested'
|
||||
nested_sub1 = nested / 'sub1'
|
||||
nested_sub2 = nested / 'sub2'
|
||||
nested_sub2sub = nested_sub2 / 'sub'
|
||||
|
||||
for path, config in [
|
||||
(flat, FLAT_MODULE_CFG),
|
||||
(tests, TEST_MODULE_CFG),
|
||||
(nested, NESTED_MODULE_CFG),
|
||||
(nested_sub1, NESTED_MODULE_SUB1_CFG),
|
||||
(nested_sub2, NESTED_MODULE_SUB2_CFG),
|
||||
(nested_sub2sub, NESTED_MODULE_SUB2SUB_CFG),
|
||||
]:
|
||||
path.mkdir(parents=True)
|
||||
with open(path / 'tryton.cfg', 'w') as f:
|
||||
f.write(config)
|
||||
|
||||
cls.file_open = patch('trytond.tools.file_open')
|
||||
file_open = cls.file_open.start()
|
||||
file_open.side_effect = lambda name: io.open(modules / name, 'r')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp_modules.cleanup()
|
||||
cls.file_open.stop()
|
||||
|
||||
def test_module_info_flat(self):
|
||||
"Test fetching tryton.cfg information from a flat module"
|
||||
mod_info = get_module_info('flat')
|
||||
|
||||
self.assertEqual(mod_info['depends'], ['flat_a', 'flat_b'])
|
||||
self.assertEqual(
|
||||
mod_info['extras_depend'], ['flat_extra1', 'flat_extra2'])
|
||||
self.assertEqual(
|
||||
mod_info['xml'], ['flat_file1.xml', 'flat_file2.xml'])
|
||||
|
||||
def test_module_info_with_test(self):
|
||||
"Test fetching tryton.cfg information when in a testing context"
|
||||
mod_info = get_module_info('flat', with_test=True)
|
||||
|
||||
self.assertEqual(
|
||||
mod_info['xml'],
|
||||
['flat_file1.xml', 'flat_file2.xml',
|
||||
os.path.join('tests', 'test_file.xml')])
|
||||
|
||||
def test_module_info_nested(self):
|
||||
"Test fetching tryton.cfg information from a nested module"
|
||||
mod_info = get_module_info('nested')
|
||||
|
||||
self.assertEqual(
|
||||
mod_info['xml'],
|
||||
['root_file1.xml',
|
||||
os.path.join('sub1', 'nested_file_s1.xml'),
|
||||
os.path.join('sub2', 'nested_file_s2.xml'),
|
||||
os.path.join('sub2', 'sub', 'nested_file_s2_s.xml'),
|
||||
])
|
||||
|
||||
def test_module_register(self):
|
||||
"Test module registration"
|
||||
self.assertEqual(
|
||||
list(get_module_register('flat')),
|
||||
[
|
||||
(['flat_model.Model'],
|
||||
{'module': 'flat', 'type_': 'model', 'depends': []}),
|
||||
])
|
||||
|
||||
for registration, expected in zip(
|
||||
get_module_register('nested'),
|
||||
[
|
||||
(['root_model.A'],
|
||||
{'module': 'nested', 'type_': 'model', 'depends': []}),
|
||||
(['root_wizard.wiz_A'],
|
||||
{
|
||||
'module': 'nested',
|
||||
'type_': 'wizard',
|
||||
'depends': [],
|
||||
}),
|
||||
(['sub2.nested_report.report_sub2'],
|
||||
{
|
||||
'module': 'nested',
|
||||
'type_': 'report',
|
||||
'depends': [],
|
||||
}),
|
||||
(['sub2.sub.nested_model.s2s_A'],
|
||||
{
|
||||
'module': 'nested',
|
||||
'type_': 'model',
|
||||
'depends': ['depend_s2s'],
|
||||
}),
|
||||
]):
|
||||
with self.subTest(value=expected[0]):
|
||||
self.assertEqual(registration, expected)
|
||||
|
||||
def test_module_register_mixin(self):
|
||||
"Test module registration of mixins"
|
||||
for registration, expected in zip(
|
||||
get_module_register_mixin('nested'),
|
||||
[
|
||||
(['test.TestMixin', 'path.to.nested.mixin'],
|
||||
{'module': 'nested'}),
|
||||
(['test.TestMixin', 'path.to.sub2.mixin'],
|
||||
{'module': 'nested'}),
|
||||
]):
|
||||
with self.subTest(value=expected[0]):
|
||||
self.assertEqual(registration, expected)
|
||||
|
||||
def test_module_register_with_tests(self):
|
||||
"Test module registration in a testing context"
|
||||
self.assertEqual(
|
||||
list(get_module_register('flat', with_test=True)),
|
||||
[
|
||||
(['flat_model.Model'],
|
||||
{'module': 'flat', 'type_': 'model', 'depends': []}),
|
||||
(['tests.test_model.TestModel'],
|
||||
{'module': 'flat', 'type_': 'model', 'depends': []}),
|
||||
])
|
||||
Reference in New Issue
Block a user