import typing from logging import NullHandler, getLogger logger = getLogger(__name__) logger.addHandler(NullHandler()) T = typing.TypeVar('T') _visible_chars_table = dict.fromkeys(range(32)) def _is_unknown(value: typing.Any) -> bool: return isinstance(value, str) and (not value or value.lower() == 'unknown') class Reportable(typing.Generic[T]): """Reportable abstract class.""" def __init__( self, *args: str, description: typing.Optional[str] = None, reportable: bool = True, ): """Initialize the object.""" self.names = args self._description = description self.reportable = reportable @property def description(self) -> str: """Rule description.""" return self._description or '|'.join(self.names) def report(self, value: typing.Union[str, T], context: typing.MutableMapping) -> None: """Report unknown value.""" if not value or not self.reportable: return if 'report' in context: report_map = context['report'].setdefault(self.description, {}) if value not in report_map: report_map[value] = context['path'] logger.info('Invalid %s: %r', self.description, value) class Property(Reportable[T]): """Property class.""" def __init__( self, *args: str, default: typing.Optional[T] = None, private: bool = False, description: typing.Optional[str] = None, delimiter: str = ' / ', **kwargs, ): """Init method.""" super().__init__(*args, description=description, **kwargs) self.default = default self.private = private # Used to detect duplicated values. e.g.: en / en or High@L4.0 / High@L4.0 or Progressive / Progressive self.delimiter = delimiter @classmethod def _extract_value(cls, track: typing.Mapping, name: str, names: typing.List[str]): if len(names) == 2: parent_value = track.get(names[0], track.get(names[0].upper(), {})) return parent_value.get(names[1], parent_value.get(names[1].upper())) return track.get(name, track.get(name.upper())) def extract_value( self, track: typing.Mapping, context: typing.MutableMapping, ) -> typing.Optional[T]: """Extract the property value from a given track.""" for name in self.names: names = name.split('.') value = self._extract_value(track, name, names) if value is None: if self.default is None: continue value = self.default if isinstance(value, bytes): value = value.decode() if isinstance(value, str): value = value.translate(_visible_chars_table).strip() if _is_unknown(value): continue value = self._deduplicate(value) result = self.handle(value, context) if result is not None and not _is_unknown(result): return result return None @classmethod def _deduplicate(cls, value: str) -> str: values = value.split(' / ') if len(values) == 2 and values[0] == values[1]: return values[0] return value def handle(self, value: T, context: typing.MutableMapping) -> typing.Optional[T]: """Return the value without any modification.""" return value class Configurable(Property[T]): """Configurable property where values are in a config mapping.""" def __init__(self, config: typing.Mapping[str, typing.Mapping], *args: str, config_key: typing.Optional[str] = None, **kwargs): """Init method.""" super().__init__(*args, **kwargs) self.mapping = getattr(config, config_key or self.__class__.__name__) if config else {} @classmethod def _extract_key(cls, value: str) -> typing.Union[str, bool]: return value.upper() @classmethod def _extract_fallback_key(cls, value: str, key: str) -> typing.Optional[T]: return None def _lookup( self, key: str, context: typing.MutableMapping, ) -> typing.Union[T, None, bool]: result = self.mapping.get(key) if result is not None: result = getattr(result, context.get('profile') or 'default') return result if result != '__ignored__' else False return None def handle(self, value, context): """Return Variable or Constant.""" key = self._extract_key(value) if key is False: return result = self._lookup(key, context) if result is False: return while not result and key: key = self._extract_fallback_key(value, key) result = self._lookup(key, context) if result is False: return if not result: self.report(value, context) return result class MultiValue(Property): """Property with multiple values.""" def __init__(self, prop: typing.Optional[Property] = None, delimiter='/', single=False, handler=None, name=None, **kwargs): """Init method.""" super().__init__(*(prop.names if prop else (name,)), **kwargs) self.prop = prop self.delimiter = delimiter self.single = single self.handler = handler def handle( self, value: str, context: typing.MutableMapping, ) -> typing.Union[T, typing.List[T]]: """Handle properties with multiple values.""" if self.handler: call = self.handler elif self.prop: call = self.prop.handle else: raise NotImplementedError('No handler available') result = call(value, context) if result is not None: return result if isinstance(value, list): if len(value) == 1: values = self._split(value[0], self.delimiter) else: values = value else: values = self._split(value, self.delimiter) if values is None: return call(values, context) if len(values) > 1 and not self.single: results = [call(item, context) if not _is_unknown(item) else None for item in values] results = [r for r in results if r is not None] if results: return results return call(values[0], context) @classmethod def _split( cls, value: typing.Optional[T], delimiter: str = '/', ) -> typing.Optional[typing.List[str]]: if value is None: return None return [x.strip() for x in str(value).split(delimiter)] class Rule(Reportable[T]): """Rule abstract class.""" def __init__(self, name: str, private=False, override=False, **kwargs): """Initialize the object.""" super().__init__(name, **kwargs) self.private = private self.override = override def execute(self, props, pv_props, context: typing.Mapping): """How to execute a rule.""" raise NotImplementedError