From f65130b7c7ecccfe2006e40d2735eeb86639772b Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Mon, 17 Dec 2018 11:53:52 +0000 Subject: [PATCH] target: Introduce TypedKernelConfig Maps Kconfig types to appropriate Python types, and act as a regular mapping with extended API: * tristate and bool values mapped to an Enum * int values to int * hex values to HexInt subclass of int that defaults to parsing and printing in hex format. Implement KernelConfig as a shim on top of TypedKernelConfig so they share most of the code. Code needing a TypedKernelConfig from a KernelConfig producer such as Target.config can trivially access the `typed_config` attribute of KernelConfig objects. --- devlib/target.py | 232 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 210 insertions(+), 22 deletions(-) diff --git a/devlib/target.py b/devlib/target.py index 094fb64..ad0b6d0 100644 --- a/devlib/target.py +++ b/devlib/target.py @@ -30,6 +30,14 @@ import xml.dom.minidom import copy from collections import namedtuple, defaultdict from pipes import quote +from past.types import basestring +from numbers import Number +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + +from enum import Enum from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY from devlib.module import get_module @@ -1763,8 +1771,56 @@ class KernelVersion(object): __repr__ = __str__ -class KernelConfig(object): +class HexInt(int): + """ + Subclass of :class:`int` that uses hexadecimal formatting by default. + """ + def __new__(cls, val=0, base=16): + super_new = super(HexInt, cls).__new__ + if isinstance(val, Number): + return super_new(cls, val) + else: + return super_new(cls, val, base=base) + + def __str__(self): + return hex(self) + + +class KernelConfigTristate(Enum): + YES = 'y' + NO = 'n' + MODULE = 'm' + + def __bool__(self): + """ + Allow using this enum to represent bool Kconfig type, although it is + technically different from tristate. + """ + return self in (self.YES, self.MODULE) + + def __nonzero__(self): + """ + For Python 2.x compatibility. + """ + return self.__bool__() + + @classmethod + def from_str(cls, str_): + for state in cls: + if state.value == str_: + return state + raise ValueError('No kernel config tristate value matches "{}"'.format(str_)) + + +class TypedKernelConfig(Mapping): + """ + Mapping-like typed version of :class:`KernelConfig`. + + Values are either :class:`str`, :class:`int`, + :class:`KernelConfigTristate`, or :class:`HexInt`. ``hex`` Kconfig type is + mapped to :class:`HexInt` and ``bool`` to :class:`KernelConfigTristate`. + """ not_set_regex = re.compile(r'# (\S+) is not set') @staticmethod @@ -1774,17 +1830,93 @@ class KernelConfig(object): name = 'CONFIG_' + name return name - def iteritems(self): - return iter(self._config.items()) + def __init__(self, mapping=None): + mapping = mapping if mapping is not None else {} + self._config = { + # Ensure we use the canonical name of the config keys for internal + # representation + self.get_config_name(k): v + for k, v in dict(mapping).items() + } - def __init__(self, text): - self.text = text - self._config = self._parse_text(text) + @classmethod + def from_str(cls, text): + """ + Build a :class:`TypedKernelConfig` out of the string content of a + Kconfig file. + """ + return cls(cls._parse_text(text)) + + @staticmethod + def _val_to_str(val): + "Convert back values to Kconfig-style string value" + # Special case the gracefully handle the output of get() + if val is None: + return None + elif isinstance(val, KernelConfigTristate): + return val.value + elif isinstance(val, basestring): + return '"{}"'.format(val) + else: + return str(val) + + def __str__(self): + return '\n'.join( + '{}={}'.format(k, self._val_to_str(v)) + for k, v in self.items() + ) + + @staticmethod + def _parse_val(k, v): + """ + Parse a value of types handled by Kconfig: + * string + * bool + * tristate + * hex + * int + + Since bool cannot be distinguished from tristate, tristate is + always used. :meth:`KernelConfigTristate.__bool__` will allow using + it as a bool though, so it should not impact user code. + """ + if not v: + return None + + # Handle "string" type + if v.startswith('"'): + # Strip enclosing " + return v[1:-1] + + else: + try: + # Handles "bool" and "tristate" types + return KernelConfigTristate.from_str(v) + except ValueError: + pass + + try: + # Handles "int" type + return int(v) + except ValueError: + pass + + try: + # Handles "hex" type + return HexInt(v) + except ValueError: + pass + + # If no type could be parsed + raise ValueError('Could not parse Kconfig key: {}={}'.format( + k, v + ), k, v + ) @classmethod def _parse_text(cls, text): config = {} - for line in text.split('\n'): + for line in text.splitlines(): line = line.strip() # skip empty lines @@ -1802,40 +1934,96 @@ class KernelConfig(object): name, value = line.split('=', 1) name = cls.get_config_name(name.strip()) - config[name] = value.strip() + value = cls._parse_val(name, value.strip()) + config[name] = value return config - def get(self, name, strict=False): + def __getitem__(self, name): name = self.get_config_name(name) - res = self._config.get(name) - - if not res and strict: + try: + return self._config[name] + except KeyError: raise KernelConfigKeyError( "{} is not exposed in kernel config".format(name), name ) - return self._config.get(name) + def __iter__(self): + return iter(self._config) + + def __len__(self): + return len(self._config) + + def __contains__(self, name): + name = self.get_config_name(name) + return name in self._config def like(self, name): regex = re.compile(name, re.I) - result = {} - for k, v in self._config.items(): - if regex.search(k): - result[k] = v - return result + return { + k: v for k, v in self.items() + if regex.search(k) + } def is_enabled(self, name): - return self.get(name) == 'y' + return self.get(name) is KernelConfigTristate.YES def is_module(self, name): - return self.get(name) == 'm' + return self.get(name) is KernelConfigTristate.MODULE def is_not_set(self, name): - return self.get(name) == 'n' + return self.get(name) is KernelConfigTristate.NO def has(self, name): - return self.get(name) in ['m', 'y'] + return self.is_enabled(name) or self.is_module(name) + + +class KernelConfig(object): + """ + Backward compatibility shim on top of :class:`TypedKernelConfig`. + + This class does not provide a Mapping API and only return string values. + """ + + def __init__(self, text): + # Expose typed_config as a non-private attribute, so that user code + # needing it can get it from any existing producer of KernelConfig. + self.typed_config = TypedKernelConfig.from_str(text) + # Expose the original text for backward compatibility + self.text = text + + get_config_name = TypedKernelConfig.get_config_name + not_set_regex = TypedKernelConfig.not_set_regex + + def iteritems(self): + for k, v in self.typed_config.items(): + yield (k, self.typed_config._val_to_str(v)) + + def get(self, name, strict=False): + if strict: + val = self.typed_config[name] + else: + val = self.typed_config.get(name) + + return self.typed_config._val_to_str(val) + + def like(self, name): + return { + k: self.typed_config._val_to_str(v) + for k, v in self.typed_config.like(name).items() + } + + def is_enabled(self, name): + return self.typed_config.is_enabled(name) + + def is_module(self, name): + return self.typed_config.is_module(name) + + def is_not_set(self, name): + return self.typed_config.is_not_set(name) + + def has(self, name): + return self.typed_config.has(name) class LocalLinuxTarget(LinuxTarget):