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"))
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"))