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] 968 … 969 ``` 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