schema.py (4296B)
1 from enum import Enum 2 from dataclasses import dataclass 3 from fnmatch import fnmatchcase 4 from functools import cached_property 5 from typing import Any, Dict, Sequence, Union 6 7 from ..schema import SchemaValue, validate_dict 8 9 """ 10 YAML filename for meta files 11 """ 12 WEB_FEATURES_YML_FILENAME = "WEB_FEATURES.yml" 13 14 # File prefix to indicate that this FeatureFile should run in EXCLUDE mode. 15 EXCLUSION_PREFIX = "!" 16 17 18 class SpecialFileEnum(Enum): 19 """All files recursively""" 20 RECURSIVE = "**" 21 22 23 class FileMatchingMode(Enum): 24 """Defines how a FeatureFile pattern is used for matching.""" 25 INCLUDE = 1 # Include files that match the pattern 26 EXCLUDE = 2 # Exclude files that match the pattern 27 28 class FeatureFile(str): 29 @cached_property 30 def matching_mode(self) -> FileMatchingMode: 31 """Determines if the pattern should include or exclude matches.""" 32 return FileMatchingMode.EXCLUDE if self.startswith(EXCLUSION_PREFIX) else FileMatchingMode.INCLUDE 33 34 @cached_property 35 def processed_filename(self) -> str: 36 """Removes the exclusion prefix "!" from the pattern.""" 37 # TODO. After moving to Python3.9, use: return self.removeprefix(EXCLUSION_PREFIX) 38 return self[len(EXCLUSION_PREFIX):] if self.startswith(EXCLUSION_PREFIX) else self 39 40 def match_files(self, base_filenames: Sequence[str]) -> Sequence[str]: 41 """ 42 Given the input base file names, returns the subset of base file names 43 that match the given FeatureFile based on matching_mode. 44 If the FeatureFile contains any number of "*" characters, fnmatch is 45 used check each file name. 46 If the FeatureFile does not contain any "*" characters, the base file name 47 must match the FeatureFile exactly 48 :param base_filenames: The list of filenames to check against the FeatureFile 49 :return: List of matching file names that match FeatureFile 50 """ 51 result = [] 52 # If our file name contains a wildcard, use fnmatch 53 if "*" in self: 54 for base_filename in base_filenames: 55 if fnmatchcase(base_filename, self.processed_filename): 56 result.append(base_filename) 57 elif self.processed_filename in base_filenames: 58 result.append(self.processed_filename) 59 return result 60 61 62 @dataclass 63 class FeatureEntry: 64 files: Union[Sequence[FeatureFile], SpecialFileEnum] 65 """The web-features key""" 66 name: str 67 68 _required_keys = {"files", "name"} 69 70 def __init__(self, obj: Dict[str, Any]): 71 """ 72 Converts the provided dictionary to an instance of FeatureEntry 73 :param obj: The object that will be converted to a FeatureEntry. 74 :return: An instance of FeatureEntry 75 :raises ValueError: If there are unexpected keys or missing required keys. 76 """ 77 validate_dict(obj, FeatureEntry._required_keys) 78 self.files = SchemaValue.from_union([ 79 lambda x: SchemaValue.from_list(SchemaValue.from_class(FeatureFile), x), 80 SpecialFileEnum], obj.get("files")) 81 self.name = SchemaValue.from_str(obj.get("name")) 82 # If "**" is used, it should be the only item. Not in a list. 83 if isinstance(self.files, list) and SpecialFileEnum.RECURSIVE.value in self.files: 84 raise ValueError(f'Feature {self.name} contains "**" in a list. It should be `files: "**"`') 85 86 87 def does_feature_apply_recursively(self) -> bool: 88 if isinstance(self.files, SpecialFileEnum) and self.files == SpecialFileEnum.RECURSIVE: 89 return True 90 return False 91 92 93 @dataclass 94 class WebFeaturesFile: 95 """List of features""" 96 features: Sequence[FeatureEntry] 97 98 _required_keys = {"features"} 99 100 def __init__(self, obj: Dict[str, Any]): 101 """ 102 Converts the provided dictionary to an instance of WebFeaturesFile 103 :param obj: The object that will be converted to a WebFeaturesFile. 104 :return: An instance of WebFeaturesFile 105 :raises ValueError: If there are unexpected keys or missing required keys. 106 """ 107 validate_dict(obj, WebFeaturesFile._required_keys) 108 self.features = SchemaValue.from_list( 109 lambda raw_feature: FeatureEntry(SchemaValue.from_dict(raw_feature)), obj.get("features"))