trisquel-icecat/icecat/l10n/compare-locales/compare_locales/paths/project.py

239 lines
8.3 KiB
Python

# 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