Source code

Revision control

Copy as Markdown

Other Tools

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
from compare_locales import mozpath
from .matcher import Matcher
class ExcludeError(ValueError):
pass
class ProjectConfig:
'''Abstraction of l10n project configuration data.
'''
def __init__(self, path):
self.filter_py = None # legacy filter code
# {
# 'l10n': pattern,
# 'reference': pattern, # optional
# 'locales': [], # optional
# 'test': [], # optional
# }
self.path = path
self.root = None
self.paths = []
self.rules = []
self.locales = None
# cache for all_locales, as that's not in `filter`
self._all_locales = None
self.environ = {}
self.children = []
self.excludes = []
self._cache = None
def same(self, other):
'''Equality test, ignoring locales.
'''
if other.__class__ is not self.__class__:
return False
if len(self.children) != len(other.children):
return False
for prop in ('path', 'root', 'paths', 'rules', 'environ'):
if getattr(self, prop) != getattr(other, prop):
return False
for this_child, other_child in zip(self.children, other.children):
if not this_child.same(other_child):
return False
return True
def set_root(self, basepath):
if self.path is None:
self.root = None
return
self.root = mozpath.abspath(
mozpath.join(mozpath.dirname(self.path), basepath)
)
def add_environment(self, **kwargs):
self.environ.update(kwargs)
def add_paths(self, *paths):
'''Add path dictionaries to this config.
The dictionaries must have a `l10n` key. For monolingual files,
`reference` is also required.
An optional key `test` is allowed to enable additional tests for this
path pattern.
'''
self._all_locales = None # clear cache
for d in paths:
rv = {
'l10n': Matcher(d['l10n'], env=self.environ, root=self.root),
'module': d.get('module')
}
if 'reference' in d:
rv['reference'] = Matcher(
d['reference'], env=self.environ, root=self.root
)
if 'test' in d:
rv['test'] = d['test']
if 'locales' in d:
rv['locales'] = d['locales'][:]
self.paths.append(rv)
def set_filter_py(self, filter_function):
'''Set legacy filter.py code.
Assert that no rules are set.
Also, normalize output already here.
'''
assert not self.rules
def filter_(module, path, entity=None):
try:
rv = filter_function(module, path, entity=entity)
except BaseException: # we really want to handle EVERYTHING here
return 'error'
rv = {
True: 'error',
False: 'ignore',
'report': 'warning'
}.get(rv, rv)
assert rv in ('error', 'ignore', 'warning', None)
return rv
self.filter_py = filter_
def add_rules(self, *rules):
'''Add rules to filter on.
Assert that there's no legacy filter.py code hooked up.
'''
assert self.filter_py is None
for rule in rules:
self.rules.extend(self._compile_rule(rule))
def add_child(self, child):
self._all_locales = None # clear cache
if child.excludes:
raise ExcludeError(
'Included configs cannot declare their own excludes.'
)
self.children.append(child)
def exclude(self, child):
for config in child.configs:
if config.excludes:
raise ExcludeError(
'Excluded configs cannot declare their own excludes.'
)
self.excludes.append(child)
def set_locales(self, locales, deep=False):
self._all_locales = None # clear cache
self.locales = locales
if not deep:
return
for child in self.children:
child.set_locales(locales, deep=deep)
@property
def configs(self):
'Recursively get all configs in this project and its children'
yield self
for child in self.children:
yield from child.configs
@property
def all_locales(self):
'Recursively get all locales in this project and its paths'
if self._all_locales is None:
all_locales = set()
for config in self.configs:
if config.locales is not None:
all_locales.update(config.locales)
for paths in config.paths:
if 'locales' in paths:
all_locales.update(paths['locales'])
self._all_locales = sorted(all_locales)
return self._all_locales
def filter(self, l10n_file, entity=None):
'''Filter a localization file or entities within, according to
this configuration file.'''
if l10n_file.locale not in self.all_locales:
return 'ignore'
if self.filter_py is not None:
return self.filter_py(l10n_file.module, l10n_file.file,
entity=entity)
rv = self._filter(l10n_file, entity=entity)
if rv is None:
return 'ignore'
return rv
class FilterCache:
def __init__(self, locale):
self.locale = locale
self.rules = []
self.l10n_paths = []
def cache(self, locale):
if self._cache and self._cache.locale == locale:
return self._cache
self._cache = self.FilterCache(locale)
for paths in self.paths:
if 'locales' in paths and locale not in paths['locales']:
continue
self._cache.l10n_paths.append(paths['l10n'].with_env({
"locale": locale
}))
for rule in self.rules:
cached_rule = rule.copy()
cached_rule['path'] = rule['path'].with_env({
"locale": locale
})
self._cache.rules.append(cached_rule)
return self._cache
def _filter(self, l10n_file, entity=None):
if any(
exclude.filter(l10n_file) == 'error'
for exclude in self.excludes
):
return
actions = {
child._filter(l10n_file, entity=entity)
for child in self.children}
if 'error' in actions:
# return early if we know we'll error
return 'error'
cached = self.cache(l10n_file.locale)
if any(p.match(l10n_file.fullpath) for p in cached.l10n_paths):
action = 'error'
for rule in reversed(cached.rules):
if not rule['path'].match(l10n_file.fullpath):
continue
if ('key' in rule) ^ (entity is not None):
# key/file mismatch, not a matching rule
continue
if 'key' in rule and not rule['key'].match(entity):
continue
action = rule['action']
break
actions.add(action)
if 'error' in actions:
return 'error'
if 'warning' in actions:
return 'warning'
if 'ignore' in actions:
return 'ignore'
def _compile_rule(self, rule):
assert 'path' in rule
if isinstance(rule['path'], list):
for path in rule['path']:
_rule = rule.copy()
_rule['path'] = Matcher(path, env=self.environ, root=self.root)
yield from self._compile_rule(_rule)
return
if isinstance(rule['path'], str):
rule['path'] = Matcher(
rule['path'], env=self.environ, root=self.root
)
if 'key' not in rule:
yield rule
return
if not isinstance(rule['key'], str):
for key in rule['key']:
_rule = rule.copy()
_rule['key'] = key
yield from self._compile_rule(_rule)
return
rule = rule.copy()
key = rule['key']
if key.startswith('re:'):
key = key[3:]
else:
key = re.escape(key) + '$'
rule['key'] = re.compile(key)
yield rule