from enum import Enum from dataclasses import dataclass from fnmatch import fnmatchcase from functools import cached_property from typing import Any, Dict, Sequence, Union from ..schema import SchemaValue, validate_dict """ YAML filename for meta files """ WEB_FEATURES_YML_FILENAME = "WEB_FEATURES.yml" # File prefix to indicate that this FeatureFile should run in EXCLUDE mode. EXCLUSION_PREFIX = "!" class SpecialFileEnum(Enum): """All files recursively""" RECURSIVE = "**" class FileMatchingMode(Enum): """Defines how a FeatureFile pattern is used for matching.""" INCLUDE = 1 # Include files that match the pattern EXCLUDE = 2 # Exclude files that match the pattern class FeatureFile(str): @cached_property def matching_mode(self) -> FileMatchingMode: """Determines if the pattern should include or exclude matches.""" return FileMatchingMode.EXCLUDE if self.startswith(EXCLUSION_PREFIX) else FileMatchingMode.INCLUDE @cached_property def processed_filename(self) -> str: """Removes the exclusion prefix "!" from the pattern.""" # TODO. After moving to Python3.9, use: return self.removeprefix(EXCLUSION_PREFIX) return self[len(EXCLUSION_PREFIX):] if self.startswith(EXCLUSION_PREFIX) else self def match_files(self, base_filenames: Sequence[str]) -> Sequence[str]: """ Given the input base file names, returns the subset of base file names that match the given FeatureFile based on matching_mode. If the FeatureFile contains any number of "*" characters, fnmatch is used check each file name. If the FeatureFile does not contain any "*" characters, the base file name must match the FeatureFile exactly :param base_filenames: The list of filenames to check against the FeatureFile :return: List of matching file names that match FeatureFile """ result = [] # If our file name contains a wildcard, use fnmatch if "*" in self: for base_filename in base_filenames: if fnmatchcase(base_filename, self.processed_filename): result.append(base_filename) elif self.processed_filename in base_filenames: result.append(self.processed_filename) return result @dataclass class FeatureEntry: files: Union[Sequence[FeatureFile], SpecialFileEnum] """The web-features key""" name: str _required_keys = {"files", "name"} def __init__(self, obj: Dict[str, Any]): """ Converts the provided dictionary to an instance of FeatureEntry :param obj: The object that will be converted to a FeatureEntry. :return: An instance of FeatureEntry :raises ValueError: If there are unexpected keys or missing required keys. """ validate_dict(obj, FeatureEntry._required_keys) self.files = SchemaValue.from_union([ lambda x: SchemaValue.from_list(SchemaValue.from_class(FeatureFile), x), SpecialFileEnum], obj.get("files")) self.name = SchemaValue.from_str(obj.get("name")) # If "**" is used, it should be the only item. Not in a list. if isinstance(self.files, list) and SpecialFileEnum.RECURSIVE.value in self.files: raise ValueError(f'Feature {self.name} contains "**" in a list. It should be `files: "**"`') def does_feature_apply_recursively(self) -> bool: if isinstance(self.files, SpecialFileEnum) and self.files == SpecialFileEnum.RECURSIVE: return True return False @dataclass class WebFeaturesFile: """List of features""" features: Sequence[FeatureEntry] _required_keys = {"features"} def __init__(self, obj: Dict[str, Any]): """ Converts the provided dictionary to an instance of WebFeaturesFile :param obj: The object that will be converted to a WebFeaturesFile. :return: An instance of WebFeaturesFile :raises ValueError: If there are unexpected keys or missing required keys. """ validate_dict(obj, WebFeaturesFile._required_keys) self.features = SchemaValue.from_list( lambda raw_feature: FeatureEntry(SchemaValue.from_dict(raw_feature)), obj.get("features"))