1from typing import Iterable
2from collections import OrderedDict
3from functools import wraps, partial
4from itertools import chain
5import re
6import importlib
7import inspect
8import types
9import json
10import logging
11
12from fontbakery.errors import NamespaceError, SetupError, CircularAliasError
13from fontbakery.callable import (
14    FontbakeryCallable,
15    FontBakeryCheck,
16    FontBakeryCondition,
17    FontBakeryExpectedValue,
18)
19from fontbakery.configuration import Configuration
20from fontbakery.message import Message
21from fontbakery.section import Section
22from fontbakery.utils import is_negated
23from fontbakery.status import DEBUG
24
25
26def get_module_profile(module, name=None):
27    """
28    Get or create a profile from a module and return it.
29
30    If the name `module.profile` is present the value of that is returned.
31    Otherwise, if the name `module.profile_factory` is present, a new profile
32    is created using `module.profile_factory` and then `profile.auto_register`
33    is called with the module namespace.
34    If neither name is defined, the module is not considered a profile-module
35    and None is returned.
36
37    TODO: describe the `name` argument and better define the signature of `profile_factory`.
38
39    The `module` argument is expected to behave like a python module.
40    The optional `name` argument is used when `profile_factory` is called to
41    give a name to the default section of the new profile. If name is not
42    present `module.__name__` is the fallback.
43
44    `profile_factory` is called like this:
45        `profile = module.profile_factory(default_section=default_section)`
46
47    """
48    try:
49        # if profile is defined we just use it
50        return module.profile
51    except AttributeError:  # > 'module' object has no attribute 'profile'
52        # try to create one on the fly.
53        # e.g. module.__name__ == "fontbakery.profiles.cmap"
54        if "profile_factory" not in module.__dict__:
55            return None
56        default_section = Section(name or module.__name__)
57        profile = module.profile_factory(
58            default_section=default_section, module_spec=module.__spec__
59        )
60        profile.auto_register(module.__dict__)
61        return profile
62
63
64class Profile:
65    """
66    Profiles may specify default configuration values (used to parameterize
67    checks), which are then overridden by values in the user's configuration
68    file.
69    """
70    configuration_defaults = {}
71
72    def __init__(
73        self,
74        sections=None,
75        iterargs=None,
76        derived_iterables=None,
77        conditions=None,
78        aliases=None,
79        expected_values=None,
80        default_section=None,
81        check_skip_filter=None,
82        profile_tag=None,
83        module_spec=None,
84    ):
85        """
86          sections: a list of sections, which are ideally ordered sets of
87              individual checks.
88              It makes no sense to have checks repeatedly, they yield the same
89              results anyway, thus we don't allow this.
90          iterargs: maping 'singular' variable names to the iterable in values
91              e.g.: `{'font': 'fonts'}` in this case fonts must be iterable AND
92              'font' may not be a value NOR a condition name.
93          derived_iterables: a dictionary {"plural": ("singular", bool simple)}
94              where singular points to a condition, that consumes directly or indirectly
95              iterargs. plural will be a list of all values the condition produces
96              with all combination of it's iterargs.
97              If simple is False, the result returns tuples of: (iterars, value)
98              where iterargs is a tuple of ('iterargname', number index)
99              Especially for cases where only one iterarg is involved, simple
100              can be set to True and the result list will just contain the values.
101              Example:
102
103              @condition
104              def ttFont(font):
105                  return TTFont(font)
106
107              values={'fonts': ['font_0', 'font_1']}
108              iterargs={'font': 'fonts'}
109
110              derived_iterables={'ttFonts': ('ttFont', True)}
111              # Then:
112              ttfons = (
113                  <TTFont object from font_0>
114                , <TTFont object from font_1>
115              )
116
117              # However
118              derived_iterables={'ttFonts': ('ttFont', False)}
119              ttfons = [
120                  ((('font', 0), ), <TTFont object from font_0>)
121                , ((('font', 1), ), <TTFont object from font_1>)
122              ]
123
124        We will:
125          a) get all needed values/variable names from here
126          b) add some validation, so that we know the values match
127             our expectations! These values must be treated as user input!
128        """
129        self._namespace = {
130            "config": "config"  # Filled in by checkrunner
131        }
132
133        self.iterargs = {}
134        if iterargs:
135            self._add_dict_to_namespace("iterargs", iterargs)
136
137        self.derived_iterables = {}
138        if derived_iterables:
139            self._add_dict_to_namespace("derived_iterables", derived_iterables)
140
141        self.aliases = {}
142        if aliases:
143            self._add_dict_to_namespace("aliases", aliases)
144
145        self.conditions = {}
146        if conditions:
147            self._add_dict_to_namespace("conditions", conditions)
148
149        self.expected_values = {}
150        if expected_values:
151            self._add_dict_to_namespace("expected_values", expected_values)
152
153        self._check_registry = {}
154        self._sections = OrderedDict()
155        if sections:
156            for section in sections:
157                self.add_section(section)
158
159        if not default_section:
160            default_section = (
161                sections[0] if sections and len(sections) else Section("Default")
162            )
163        self._default_section = default_section
164        self.add_section(self._default_section)
165
166        # currently only used for new check ids in self.check_log_override
167        # only a-z everything else is deleted
168        self.profile_tag = re.sub(
169            r"[^a-z]", "", (profile_tag or self._default_section.name).lower()
170        )
171
172        self._check_skip_filter = check_skip_filter
173
174        # Used in multiprocessing because pickling the profiles fail on
175        # Mac and Windows. See: googlefonts/fontbakery#2982
176        # module_locator can actually a module.__spec__ but also just a dict
177        # self.module_locator will always be just a dict
178        if module_spec is None:
179            # This is a bit of a hack, but the idea is to reduce boilerplate
180            # when writing modules that directly define a profile.
181            try:
182                frame = inspect.currentframe().f_back
183                while frame:
184                    # Note, if __spec__ is a local variable we shpuld be at a
185                    # module top level. It should also be the correct ModuleSpec
186                    # according to how we do this "usually" (as documented and
187                    # practiced as far as I'm aware of), e.g. the profile module
188                    # defines a profile object directly by calling a Profile constructor
189                    # (e.g. profies.ufo_sources) or indirectly via a profile_factory
190                    # (e.g. profiles.google_fonts). Otherwise, if this fails
191                    # or finds a wrong ModuleSpec, there's still the option to
192                    # pass module_spec as an argument (module.__spec__), which is
193                    # actually demonstrated in get_module_profile.
194                    if "__spec__" in frame.f_locals:
195                        module_spec = frame.f_locals["__spec__"]
196                        if module_spec and isinstance(
197                            module_spec, importlib.machinery.ModuleSpec
198                        ):
199                            break
200                        module_spec = None  # reset
201                    frame = frame.f_back
202            finally:
203                del frame
204
205        # If not module_spec: this is only a problem in multiprocessing, in
206        # that case we'll be failing to access this with an AttributeError.
207        if module_spec is not None:
208            self.module_locator = dict(name=module_spec.name, origin=module_spec.origin)
209
210    _valid_namespace_types = {
211        "iterargs": "iterarg",
212        "derived_iterables": "derived_iterable",
213        "aliases": "alias",
214        "conditions": "condition",
215        "expected_values": "expected_value",
216    }
217
218    @property
219    def sections(self):
220        return self._sections.values()
221
222    def _add_dict_to_namespace(self, type, data):
223        for key, value in data.items():
224            self.add_to_namespace(
225                type, key, value, force=getattr(value, "force", False)
226            )
227
228    def add_to_namespace(self, type, name, value, force=False):
229        if type not in self._valid_namespace_types:
230            valid_types = ", ".join(self._valid_namespace_types)
231            raise TypeError(f'Unknow type "{type}"' f" Valid types are: {valid_types}")
232
233        if name in self._namespace:
234            registered_type = self._namespace[name]
235            registered_value = getattr(self, registered_type)[name]
236            if type == registered_type and registered_value == value:
237                # if the registered equals: skip silently. Registering the same
238                # value multiple times is allowed, so we can easily expand profiles
239                # that define (partly) the same entries
240                return
241
242            if not force:
243                msg = (
244                    f'Name "{name}" is already registered'
245                    f' in "{registered_type}" (value: {registered_value}).'
246                    f' Requested registering in "{type}" (value: {value}).'
247                )
248                raise NamespaceError(msg)
249            else:
250                # clean the old type up
251                del getattr(self, registered_type)[name]
252
253        self._namespace[name] = type
254        target = getattr(self, type)
255        target[name] = value
256
257    def test_dependencies(self):
258        """Raises SetupError if profile uses any names that are not declared
259        in the its namespace.
260        """
261        seen = set()
262        failed = []
263        # make this simple, collect all used names
264        for section_name, section in self._sections.items():
265            for check in section.checks:
266                dependencies = list(check.args)
267                if hasattr(check, "conditions"):
268                    dependencies += [
269                        name for negated, name in map(is_negated, check.conditions)
270                    ]
271
272                while dependencies:
273                    name = dependencies.pop()
274                    if name in seen:
275                        continue
276                    seen.add(name)
277                    if name not in self._namespace:
278                        failed.append(name)
279                        continue
280                    # if this is a condition, expand its dependencies
281                    condition = self.conditions.get(name, None)
282                    if condition is not None:
283                        dependencies += condition.args
284        if len(failed):
285            comma_separated = ", ".join(failed)
286            raise SetupError(
287                f"Profile uses names that are not declared"
288                f" in its namespace: {comma_separated}."
289            )
290
291    def test_expected_checks(self, expected_check_ids, exclusive=False):
292        """Self-test to make a sure profile maintainer is aware of changes in
293        the profile.
294        Raises SetupError if expected check ids are missing in the profile (removed)
295        If `exclusive=True` also raises SetupError if check ids are in the
296        profile that are not in expected_check_ids (newly added).
297
298        This is handy if `profile.auto_register` is used and the profile maintainer
299        is looking for a high level of control over the profile contents,
300        especially for a warning when the profile contents have changed after an
301        update.
302        """
303        s = set()
304        duplicates = set(x for x in expected_check_ids if x in s or s.add(x))
305        if len(duplicates):
306            raise SetupError(
307                "Profile has duplicated entries in its list"
308                " of expected check IDs:\n" + "\n".join(duplicates)
309            )
310
311        expected_check_ids = set(expected_check_ids)
312        registered_checks = set(self._check_registry.keys())
313        missing_checks = expected_check_ids - registered_checks
314        unexpected_checks = None
315        if exclusive:
316            unexpected_checks = registered_checks - expected_check_ids
317        message = []
318        if missing_checks:
319            message.append("missing checks: {};".format(", ".join(missing_checks)))
320        if unexpected_checks:
321            message.append(
322                "unexpected checks: {};".format(", ".join(unexpected_checks))
323            )
324        if message:
325            raise SetupError(
326                "Profile fails expected checks test:\n" + "\n".join(message)
327            )
328
329        def is_numerical_id(checkid):
330            try:
331                int(checkid.split("/")[-1])
332                return True
333            except:
334                return False
335
336        numerical_check_ids = [c for c in registered_checks if is_numerical_id(c)]
337        if numerical_check_ids:
338            list_of_checks = "\t- " + "\n\t- ".join(numerical_check_ids)
339            raise SetupError(
340                f"\n"
341                f"\n"
342                f"Numerical check IDs must be renamed to keyword-based IDs:\n"
343                f"{list_of_checks}\n"
344                f"\n"
345                f"See also: https://github.com/googlefonts/fontbakery/issues/2238\n"
346                f"\n"
347            )
348
349    def resolve_alias(self, original_name):
350        name = original_name
351        seen = set()
352        path = []
353        while name in self.aliases:
354            if name in seen:
355                names = " -> ".join(path)
356                raise CircularAliasError(
357                    f'Alias for "{original_name}" has'
358                    f" a circular reference in {names}"
359                )
360            seen.add(name)
361            path.append(name)
362            name = self.aliases[name]
363        return name
364
365    def validate_values(self, values):
366        """
367        Validate values if they are registered as expected_values and present.
368
369        * If they are not registered they shouldn't be used anywhere at all
370          because profile can self check (profile.check_dependencies) for
371          missing/undefined dependencies.
372
373        * If they are not present in values but registered as expected_values
374          either the expected value has a default value OR a request for that
375          name will raise a KeyError on runtime. We don't know if all expected
376          values are actually needed/used, thus this fails late.
377        """
378        messages = []
379        for name, value in values.items():
380            if name not in self.expected_values:
381                continue
382            valid, message = self.expected_values[name].validate(value)
383            if valid:
384                continue
385            messages.append(f"{name}: {message} (value: {value})")
386        if len(messages):
387            return False, "\n".join(messages)
388        return True, None
389
390    def get_type(self, name, *args):
391        has_fallback = bool(args)
392        if has_fallback:
393            fallback = args[0]
394
395        if not name in self._namespace:
396            if has_fallback:
397                return fallback
398            raise KeyError(name)
399
400        return self._namespace[name]
401
402    def get(self, name, *args):
403        has_fallback = bool(args)
404        if has_fallback:
405            fallback = args[0]
406
407        try:
408            target_type = self.get_type(name)
409        except KeyError:
410            if not has_fallback:
411                raise
412            return fallback
413
414        target = getattr(self, target_type)
415        if name not in target:
416            if has_fallback:
417                return fallback
418            raise KeyError(name)
419        return target[name]
420
421    def has(self, name):
422        marker_fallback = object()
423        val = self.get(name, marker_fallback)
424        return val is not marker_fallback
425
426    def _get_aggregate_args(self, item, key):
427        """
428        Get all arguments or mandatory arguments of the item.
429
430        Item is a check or a condition, which means it can be dependent on
431        more conditions, this climbs down all the way.
432        """
433        if not key in ("args", "mandatoryArgs"):
434            raise TypeError(f'key must be "args" or "mandatoryArgs", got {key}')
435        dependencies = list(getattr(item, key))
436        if hasattr(item, "conditions"):
437            dependencies += [name for negated, name in map(is_negated, item.conditions)]
438        args = set()
439        while dependencies:
440            name = dependencies.pop()
441            if name in args:
442                continue
443            args.add(name)
444            # if this is a condition, expand its dependencies
445            c = self.conditions.get(name, None)
446            if c is None:
447                continue
448            dependencies += [
449                dependency for dependency in getattr(c, key) if dependency not in args
450            ]
451        return args
452
453    def get_iterargs(self, item):
454        """ Returns a tuple of all iterags for item, sorted by name."""
455        # iterargs should always be mandatory, unless there's a good reason
456        # not to, which I can't think of right now.
457
458        args = self._get_aggregate_args(item, "mandatoryArgs")
459        return tuple(sorted([arg for arg in args if arg in self.iterargs]))
460
461    def _analyze_checks(self, all_args, checks):
462        args = list(all_args)
463        args.reverse()
464        # (check, signature, scope)
465        scopes = [(check, tuple(), tuple()) for check in checks]
466        aggregatedArgs = {
467            "args": {
468                check.name: self._get_aggregate_args(check, "args") for check in checks
469            },
470            "mandatoryArgs": {
471                check.name: self._get_aggregate_args(check, "mandatoryArgs")
472                for check in checks
473            },
474        }
475        saturated = []
476        while args:
477            new_scopes = []
478            # args_set must contain all current args, hence it's before the pop
479            args_set = set(args)
480            arg = args.pop()
481            for check, signature, scope in scopes:
482                if not len(aggregatedArgs["args"][check.name] & args_set):
483                    # there's no args no more or no arguments of check are
484                    # in args
485                    target = saturated
486                elif (
487                    arg == "*check"
488                    or arg in aggregatedArgs["mandatoryArgs"][check.name]
489                ):
490                    signature += (1,)
491                    scope += (arg,)
492                    target = new_scopes
493                else:
494                    # there's still a tail of args and check requires one of the
495                    # args in tail but not the current arg
496                    signature += (0,)
497                    target = new_scopes
498                target.append((check, signature, scope))
499            scopes = new_scopes
500        return saturated + scopes
501
502    def _execute_section(self, iterargs, section, items):
503        if section is None:
504            # base case: terminate recursion
505            for check, signature, scope in items:
506                yield check, []
507        elif not section[0]:
508            # no sectioning on this level
509            for item in self._execute_scopes(iterargs, items):
510                yield item
511        elif section[1] == "*check":
512            # enforce sectioning by check
513            for section_item in items:
514                for item in self._execute_scopes(iterargs, [section_item]):
515                    yield item
516        else:
517            # section by gen_arg, i.e. ammend with changing arg.
518            _, gen_arg = section
519            for index in range(iterargs[gen_arg]):
520                for check, args in self._execute_scopes(iterargs, items):
521                    yield check, [(gen_arg, index)] + args
522
523    def _execute_scopes(self, iterargs, scopes):
524        generators = []
525        items = []
526        current_section = None
527        last_section = None
528        seen = set()
529        for check, signature, scope in scopes:
530            if len(signature):
531                # items are left
532                if signature[0]:
533                    gen_arg = scope[0]
534                    scope = scope[1:]
535                    current_section = True, gen_arg
536                else:
537                    current_section = False, None
538                signature = signature[1:]
539            else:
540                current_section = None
541
542            assert (
543                current_section not in seen
544            ), f"Scopes are badly sorted. {current_section} in {seen}"
545
546            if current_section != last_section:
547                if len(items):
548                    # flush items
549                    generators.append(
550                        self._execute_section(iterargs, last_section, items)
551                    )
552                    items = []
553                    seen.add(last_section)
554                last_section = current_section
555            items.append((check, signature, scope))
556        # clean up left overs
557        if len(items):
558            generators.append(self._execute_section(iterargs, current_section, items))
559
560        for item in chain(*generators):
561            yield item
562
563    def _section_execution_order(
564        self,
565        section,
566        iterargs,
567        reverse=False,
568        custom_order=None,
569        explicit_checks: Iterable = None,
570        exclude_checks: Iterable = None,
571    ):
572        """
573        order must:
574          a) contain all variable args (we're appending missing ones)
575          b) not contian duplictates (we're removing repeated items)
576
577        order may contain *iterargs otherwise it is appended
578        to the end
579
580        order may contain "*check" otherwise, it is like *check is appended
581        to the end (Not done explicitly though).
582        """
583        stack = list(custom_order) if custom_order is not None else list(section.order)
584        if "*iterargs" not in stack:
585            stack.append("*iterargs")
586        stack.reverse()
587
588        full_order = []
589        seen = set()
590        while len(stack):
591            item = stack.pop()
592            if item in seen:
593                continue
594            seen.add(item)
595            if item == "*iterargs":
596                all_iterargs = list(iterargs.keys())
597                # assuming there is a meaningful order
598                all_iterargs.reverse()
599                stack += all_iterargs
600                continue
601            full_order.append(item)
602
603        # Filter down checks. Checks to exclude are filtered for last as the user
604        # might e.g. want to include all tests with "kerning" in the ID, except for
605        # "kerning_something". explicit_checks could then be ["kerning"] and
606        # exclude_checks ["something"].
607        checks = section.checks
608        if explicit_checks:
609            checks = [
610                check
611                for check in checks
612                if any(include_string in check.id for include_string in explicit_checks)
613            ]
614        if exclude_checks:
615            checks = [
616                check
617                for check in checks
618                if not any(
619                    exclude_string in check.id for exclude_string in exclude_checks
620                )
621            ]
622
623        scopes = self._analyze_checks(full_order, checks)
624        key = lambda item: item[1]  # check, signature, scope = item
625        scopes.sort(key=key, reverse=reverse)
626
627        for check, args in self._execute_scopes(iterargs, scopes):
628            # this is the iterargs tuple that will be used as a key for caching
629            # and so on. we could sort it, to ensure it yields in the same
630            # cache locations always, but then again, it is already in a well
631            # defined order, by clustering.
632            yield check, tuple(args)
633
634    def execution_order(
635        self, iterargs, custom_order=None, explicit_checks=None, exclude_checks=None
636    ):
637        # TODO: a custom_order per section may become necessary one day
638        explicit_checks = set() if not explicit_checks else set(explicit_checks)
639        for _, section in self._sections.items():
640            for check, section_iterargs in self._section_execution_order(
641                section,
642                iterargs,
643                custom_order=custom_order,
644                explicit_checks=explicit_checks,
645                exclude_checks=exclude_checks,
646            ):
647                yield (section, check, section_iterargs)
648
649    def _register_check(self, section, func):
650        other_section = self._check_registry.get(func.id, None)
651        if other_section:
652            other_check = other_section.get_check(func.id)
653            if other_check is func:
654                if other_section is not section:
655                    logging.debug(
656                        "Check {} is already registered in {}, skipping "
657                        "register in {}.".format(func, other_section, section)
658                    )
659                return False  # skipped
660            else:
661                raise SetupError(
662                    f'Check id "{func}" is not unique!'
663                    f" It is already registered in {other_section} and"
664                    f" registration for that id is now requested in {section}."
665                    f" BUT the current check is a different object than"
666                    f" the registered check."
667                )
668        self._check_registry[func.id] = section
669        return True
670
671    def _unregister_check(self, section, check_id):
672        assert (
673            section == self._check_registry[check_id]
674        ), "Registered section must match"
675        del self._check_registry[check_id]
676        return True
677
678    def remove_check(self, check_id):
679        section = self._check_registry[check_id]
680        section.remove_check(check_id)
681
682    def check_log_override(
683        self,
684        override_check_id
685        # see def check_log_override
686        ,
687        *args,
688        **kwds,
689    ):
690        new_id = f"{override_check_id}:{self.profile_tag}"
691        old_check, section = self.get_check(override_check_id)
692        new_check = check_log_override(old_check, new_id, *args, **kwds)
693        section.replace_check(override_check_id, new_check)
694        return new_check
695
696    def get_check(self, check_id):
697        section = self._check_registry[check_id]
698        return section.get_check(check_id), section
699
700    def add_section(self, section):
701        key = str(section)
702        if key in self._sections:
703            # the string representation of a section must be unique.
704            # string representations of section and check will be used as unique keys
705            if self._sections[key] is not section:
706                raise SetupError(f"A section with key {section} is already registered")
707            return
708        self._sections[key] = section
709        section.on_add_check(self._register_check)
710        section.on_remove_check(self._unregister_check)
711
712        for check in section.checks:
713            self._register_check(section, check)
714
715    def _get_section(self, key):
716        return self._sections[key]
717
718    def _add_check(self, section, func):
719        self.add_section(section)
720        section.add_check(func)
721        return func
722
723    def register_check(self, section=None, *args, **kwds):
724        """
725        Usage:
726        # register in default section
727        @profile.register_check
728        @check(id='com.example.fontbakery/check/0')
729        def my_check():
730          yield PASS, 'example'
731
732        # register in `special_section` also register that section in the profile
733        @profile.register_check(special_section)
734        @check(id='com.example.fontbakery/check/0')
735        def my_check():
736          yield PASS, 'example'
737
738        """
739        if section and len(kwds) == 0 and callable(section):
740            func = section
741            section = self._default_section
742            return self._add_check(section, func)
743        else:
744            return partial(self._add_check, section)
745
746    def _add_condition(self, condition, name=None):
747        self.add_to_namespace(
748            "conditions", name or condition.name, condition, force=condition.force
749        )
750        return condition
751
752    def register_condition(self, *args, **kwds):
753        """
754        Usage:
755
756        @profile.register_condition
757        @condition
758        def myCondition():
759          return 123
760
761        #or
762
763        @profile.register_condition(name='my_condition')
764        @condition
765        def myCondition():
766          return 123
767        """
768        if len(args) == 1 and len(kwds) == 0 and callable(args[0]):
769            return self._add_condition(args[0])
770        else:
771            return partial(self._add_condition, *args, **kwds)
772
773    def register_expected_value(self, expected_value, name=None):
774        name = name or expected_value.name
775        self.add_to_namespace(
776            "expected_values", name, expected_value, force=expected_value.force
777        )
778        return True
779
780    def _get_package(self, symbol_table):
781        package = symbol_table.get("__package__", None)
782        if package is not None:
783            return package
784        name = symbol_table.get("__name__", None)
785        if name is None or not "." in name:
786            return None
787        return name.rpartition(".")[0]
788
789    def _load_profile_imports(self, symbol_table):
790        """
791        profile_imports is a list of module names or tuples
792        of (module_name, names to import)
793        in the form of ('.', names) it behaces like:
794        from . import name1, name2, name3
795        or similarly
796        import .name1, .name2, .name3
797
798        i.e. "name" in names becomes ".name"
799        """
800        results = []
801        if "profile_imports" not in symbol_table:
802            return results
803
804        package = self._get_package(symbol_table)
805        profile_imports = symbol_table["profile_imports"]
806
807        for item in profile_imports:
808            if isinstance(item, str):
809                # import the whole module
810                module_name, names = (item, None)
811            else:
812                # expecting a 2 items tuple or list
813                # import only the names from the module
814                module_name, names = item
815
816            if "." in module_name and len(set(module_name)) == 1 and names is not None:
817                # if you execute `from . import mod` from a module in the pkg package
818                # then you will end up importing pkg.mod
819                module_names = [f"{module_name}{name}" for name in names]
820                names = None
821            else:
822                module_names = [module_name]
823
824            for module_name in module_names:
825                module = importlib.import_module(module_name, package=package)
826                if names is None:
827                    results.append(module)
828                else:
829                    #  1. check if the imported module has an attribute by that name
830                    #  2. if not, attempt to import a submodule with that name
831                    #  3. if the attribute is not found, ImportError is raised.
832                    #  …
833                    for name in names:
834                        try:
835                            results.append(getattr(module, name))
836                        except AttributeError:
837                            # attempt to import a submodule with that name
838                            sub_module_name = ".".join([module_name, name])
839                            sub_module = importlib.import_module(
840                                sub_module_name, package=package
841                            )
842                            results.append(sub_module)
843        return results
844
845    def auto_register(self, symbol_table, filter_func=None, profile_imports=None):
846        """Register items from `symbol_table` in the profile.
847
848        Get all items from `symbol_table` dict and from `symbol_table.profile_imports`
849        if it is present. If they an item is an instance of FontBakeryCheck,
850        FontBakeryCondition or FontBakeryExpectedValue and register it in
851        the default section.
852        If an item is a python module, try to get a profile using `get_module_profile(item)`
853        and then using `merge_profile`;
854        If the profile_imports kwarg is given, it is used instead of the one taken from
855        the module namespace.
856
857        To register the current module use explicitly:
858          `profile.auto_register(globals())`
859          OR maybe: `profile.auto_register(sys.modules[__name__].__dict__)`
860        To register an imported module explicitly:
861          `profile.auto_register(module.__dict__)`
862
863        if filter_func is defined it is called like:
864        filter_func(type, name_or_id, item)
865        where
866        type: one of "check", "module", "condition", "expected_value", "iterarg",
867              "derived_iterable", "alias"
868        name_or_id: the name at which the item will be registered.
869              if type == 'check': the check.id
870              if type == 'module': the module name (module.__name__)
871        item: the item to be registered
872        if filter_func returns a falsy value for an item, the item will
873        not be registered.
874        """
875        if profile_imports:
876            symbol_table = symbol_table.copy()  # Avoid messing with original table
877            symbol_table["profile_imports"] = profile_imports
878
879        all_items = list(symbol_table.values()) + self._load_profile_imports(
880            symbol_table
881        )
882        namespace_types = (FontBakeryCondition, FontBakeryExpectedValue)
883        namespace_items = []
884
885        for item in all_items:
886            if isinstance(item, namespace_types):
887                # register these after all modules have been registered. That way,
888                # "local" items can optionally force override items registered
889                # previously by modules.
890                namespace_items.append(item)
891            elif isinstance(item, FontBakeryCheck):
892                if filter_func and not filter_func("check", item.id, item):
893                    continue
894                self.register_check(item)
895            elif isinstance(item, types.ModuleType):
896                if filter_func and not filter_func("module", item.__name__, item):
897                    continue
898                profile = get_module_profile(item)
899                if profile:
900                    self.merge_profile(profile, filter_func=filter_func)
901
902        for item in namespace_items:
903            if isinstance(item, FontBakeryCondition):
904                if filter_func and not filter_func("condition", item.name, item):
905                    continue
906                self.register_condition(item)
907            elif isinstance(item, FontBakeryExpectedValue):
908                if filter_func and not filter_func("expected_value", item.name, item):
909                    continue
910                self.register_expected_value(item)
911
912    def merge_profile(self, profile, filter_func=None):
913        """Copy all namespace items from profile to self.
914
915        Namespace items are: 'iterargs', 'derived_iterables', 'aliases',
916                             'conditions', 'expected_values'
917
918        Don't change any contents of profile ever!
919        That means sections are cloned not used directly
920
921        filter_func: see description in auto_register
922        """
923        # 'iterargs', 'derived_iterables', 'aliases', 'conditions', 'expected_values'
924        for ns_type in self._valid_namespace_types:
925            # this will raise a NamespaceError if an item of profile.{ns_type}
926            # is already registered.
927            ns_dict = getattr(profile, ns_type)
928            if filter_func:
929                ns_type_singular = self._valid_namespace_types[ns_type]
930                ns_dict = {
931                    name: item
932                    for name, item in ns_dict.items()
933                    if filter_func(ns_type_singular, name, item)
934                }
935            self._add_dict_to_namespace(ns_type, ns_dict)
936
937        check_filter_func = (
938            None
939            if not filter_func
940            else lambda check: filter_func("check", check.id, check)
941        )
942        for section in profile.sections:
943            my_section = self._sections.get(str(section), None)
944            if not len(section.checks):
945                continue
946            if my_section is None:
947                # create a new section: don't change other module/profile contents
948                my_section = section.clone(check_filter_func)
949                self.add_section(my_section)
950            else:
951                # order, description are not updated
952                my_section.merge_section(section, check_filter_func)
953
954    @property
955    def check_skip_filter(self):
956        """ return the current check_skip_filter function or None """
957        return self._check_skip_filter
958
959    @check_skip_filter.setter
960    def check_skip_filter(self, check_skip_filter):
961        """ Set a check_skip_filter function.
962
963        A check_skip_filter has a signature like:
964
965        ```
966        def check_skip_filter(check_id: str, **iterargsDict : dict) \
967                                    -> Tuple[accepted: bool, message: str]
968969        ```
970
971        If present, this function is called just before a check
972        with `check_id` is executed. `iterargsDict` is a dictionary
973        containing key:value pairs of the "iterable arguments" that will be
974        applied for that check execution.
975
976        If the returned `accepted` is falsy, the check will be SKIPed using
977        `message` for reporting.
978
979        There's no full resolution of all check arguments at this point,
980        That can be achieved with the `conditions=[]` argument of the
981        check constructor/decorator. This is for more general filtering.
982        """
983        self._check_skip_filter = check_skip_filter
984
985    def serialize_identity(self, identity):
986        """Return a json string that can also  be used as a key.
987
988        The JSON is explicitly unambiguous in the item order
989        entries (dictionaries are not ordered usually)
990        Otherwise it is valid JSON
991        """
992        section, check, iterargs = identity
993        values = map(
994            # separators are without space, which is the default in JavaScript;
995            # just in case we need to make these keys in JS.
996            partial(json.dumps, separators=(",", ":"))
997            # iterargs are sorted, because it doesn't matter for the result
998            # but it gives more predictable keys.
999            # Though, arguably, the order generated by the profile is also good
1000            # and conveys insights on how the order came to be (clustering of
1001            # iterargs). `sorted(iterargs)` however is more robust over time,
1002            # the keys will be the same, even if the sorting order changes.
1003            ,
1004            [str(section), check.id, sorted(iterargs)],
1005        )
1006        return '{{"section":{},"check":{},"iterargs":{}}}'.format(*values)
1007
1008    def deserialize_identity(self, key):
1009        item = json.loads(key)
1010        section = self._get_section(item["section"])
1011        check, _ = self.get_check(item["check"])
1012        # tuple of tuples instead list of lists
1013        iterargs = tuple(tuple(item) for item in item["iterargs"])
1014        return section, check, iterargs
1015
1016    def serialize_order(self, order):
1017        return map(self.serialize_identity, order)
1018
1019    def deserialize_order(self, serialized_order):
1020        return tuple(self.deserialize_identity(item) for item in serialized_order)
1021
1022    def setup_argparse(self, argument_parser):
1023        """
1024        Set up custom arguments needed for this profile.
1025        Return a list of keys that will be set to the `values` dictonary
1026        """
1027        pass
1028
1029    def get_deep_check_dependencies(self, check):
1030        seen = set()
1031        dependencies = list(check.args)
1032        if hasattr(check, "conditions"):
1033            dependencies += [
1034                name for negated, name in map(is_negated, check.conditions)
1035            ]
1036        while dependencies:
1037            name = dependencies.pop()
1038            if name in seen:
1039                continue
1040            seen.add(name)
1041            condition = self.conditions.get(name, None)
1042            if condition is not None:
1043                dependencies += condition.args
1044        return seen
1045
1046    @property
1047    def checks(self):
1048        for section in self.sections:
1049            for check in section.checks:
1050                yield check
1051
1052    def get_checks_by_dependencies(self, *dependencies, subset=False):
1053        deps = set(dependencies)  # faster membership checking
1054        result = []
1055        for check in self.checks:
1056            check_deps = self.get_deep_check_dependencies(check)
1057            if (subset and deps.issubset(check_deps)) or (
1058                not subset and len(deps.intersection(check_deps))
1059            ):
1060                result.append(check)
1061        return result
1062
1063    def merge_default_config(self, user_config):
1064        """
1065        Forms a configuration object based on defaults provided by the profile,
1066        overridden by values in the user's configuration file.
1067        """
1068        copy = Configuration(**self.configuration_defaults)
1069        copy.update(user_config)
1070        return copy
1071
1072
1073def _check_log_override(overrides, status, message):
1074    # These constants are merely meant to be used
1075    # so that the check_override declarations are more readable:
1076    from fontbakery.message import (KEEP_ORIGINAL_STATUS,
1077                                    KEEP_ORIGINAL_MESSAGE)
1078    result_status = status
1079    result_message = message
1080    override = False
1081    for override_target, new_status, new_message_string in overrides:
1082        # Override is only possible by matching message.code
1083        if (
1084            not hasattr(result_message, "code")
1085            or result_message.code != override_target
1086        ):
1087            continue
1088        override = True
1089        if new_status is not KEEP_ORIGINAL_STATUS:
1090            result_status = new_status
1091        if new_message_string is not KEEP_ORIGINAL_MESSAGE:
1092            # If it looks like an instance of Message we reuse the code,
1093            # as it is the same condition this makes totally sense.
1094            result_message = Message(result_message.code, new_message_string)
1095        # Break the for loop, we had a successful override.
1096        break
1097    return override, result_status, result_message
1098
1099
1100def check_log_override(check, new_id, overrides, reason=None):
1101    """Returns a new FontBakeryCheck that is decorating (wrapping) check,
1102    but with overrides applied to returned statuses when they match.
1103
1104    The new FontBakeryCheck is always a generator check, even if the old
1105    check is just a normal function that returns (instead of yields)
1106    its result. Also, the new check yields an INFO Status for each
1107    overridden original status.
1108
1109    Arguments:
1110
1111    check: the FontBakeryCheck to be decorated
1112    new_id: string, must be unique of course and should not(!) be check.id
1113            as we essentially create a new, different check.
1114    overrides: a tuple of override triple-tuples
1115               ((override_target, new_status, new_message_string), ...)
1116               override_target: string, specific Message.code
1117               new_status: Status or None, keep old status
1118               new_message_string: string or None, keep old message
1119    """
1120
1121    @wraps(check)  # defines __wrapped__
1122    def override_wrapper(*args, **kwds):
1123        # A check can be either a normal function that returns one Status or a
1124        # generator that yields one or more. The latter will return a generator
1125        # object that we can detect with types.GeneratorType.
1126        result = check(*args, **kwds)  # Might raise.
1127        if not isinstance(result, types.GeneratorType):
1128            # Now it iterates
1129            # make these always iterators, it's nicer to handle
1130            # also we can mix-in new status messages
1131            result = (result,)
1132        # Iterate over sub-results one-by-one, list(result) would abort on
1133        # encountering the first exception.
1134        for (status, message) in result:  # Might raise.
1135            overriden, result_status, result_message = _check_log_override(
1136                overrides, status, message
1137            )
1138            if overriden:
1139                # nothing changed (despite of a match in override rules)
1140                if result_status == status and result_message == message:
1141                    yield DEBUG, (
1142                        "A check status override rule matched but"
1143                        " did not change the resulting status."
1144                    )
1145                # Both changed
1146                elif result_status != status and result_message != message:
1147                    yield DEBUG, (
1148                        f"Overridden check status and message,"
1149                        f" original: {status} {message}"
1150                    )
1151                # Only status changed
1152                elif result_status != status and result_message == message:
1153                    yield DEBUG, f"Overridden check status, original: {status}"
1154                # Only message changed
1155                elif result_status == status and result_message != message:
1156                    yield DEBUG, f"Overridden check message, original: {message}"
1157
1158            yield result_status, result_message
1159
1160    # Make the callable here and return that.
1161    new_check = FontBakeryCheck(
1162        override_wrapper,
1163        new_id
1164        # Untouched, the reason for this checks existence stays the same!
1165        ,
1166        rationale=check.rationale
1167        # the "Derived ..." part should be prominent, so we always see it
1168        ,
1169        description=f"{check.description} (derived from {check.id})"
1170        # ONLY if there's a reason for derivation, otherwise will take
1171        # the documentation from the __doc__ string of check.
1172        ,
1173        documentation=(f"{reason}\n" f"\n" f"{check.documentation}")
1174        if reason and check.documentation
1175        else (reason or check.documentation or None),
1176    )
1177
1178    # reconstruct a proper doc string from the changes we made.
1179    # This is really backwards! But, it's so fundamental how python doc
1180    # strings work, that I think it's solid enough.
1181    new_check.__doc__ = f"{new_check.description}\n" f"\n" f"{new_check.documentation}"
1182    return new_check
1183