def __init_subclass__(mcs, **kwargs: Any) -> None: # We are just going to hijack the children registry for # subclasses instead here... if mcs.__name__ in StaticData: existing = getattr(StaticData, mcs.__name__) raise StaticTypeConflict(mcs.__name__, existing) StaticData.register(mcs) logger.info('%s registered with %s', utils.type_name(mcs), utils.type_name(StaticData))
def jsonify(obj: Any, camel_case_keys: bool = True, arg_struct: bool = True) -> JSONType: """ "JSON-ify" object. Attemps to serialise Python object in a fashion that would make JSON serialisation and deserialisation easier. :param obj: Python object :param camel_case_keys: Use camelCase keys :param arg_struct: Provide structure with arguments for re-creation :return: "JSON-ified" object """ if dataclasses.is_dataclass(obj): return _jsonify_dataclass(obj, camel_case_keys=camel_case_keys, arg_struct=arg_struct) if not isinstance(obj, (str, int, float, bool)) and obj is not None: logger.warning('Unsupported type in jsonify: %s (%r)', type_name(obj), obj) return obj
def unjsonify(json: JSONType, camel_case_keys: bool = True) -> Any: """ "un-JSON-ify" object. Attempts to deserialise a previously "JSON-ified" Python object back to its original Python object state. TODO: dispatching depending on type :param json: "JSON-ified" object :param camel_case_keys: Use camelCase keys :return: Python object """ _uj = functools.partial(unjsonify, camel_case_keys=camel_case_keys) # Return basic types as-is if isinstance(json, (str, int, float, bool)) or json is None: return json # Recursively process collections if isinstance(json, Sequence): return [_uj(j) for j in json] if isinstance(json, Mapping): mapping = {_uj(k): _uj(v) for k, v in json.items()} # Check if a special type, otherwise return as mapping module = mapping.pop(MODULE_KEY, None) name = mapping.pop(NAME_KEY, None) if module is None or name is None: if camel_case_keys: return { snake_case(k) if isinstance(k, str) else k: v for k, v in mapping.items() } return mapping try: cls = getattr(importlib.import_module(module), name) except (ImportError, AttributeError) as e: raise ValueError(f'Could not locate: {module}.{name}') from e # If we explicitly have JSON deserialisation method, use it if isinstance(cls, type) and issubclass(cls, JSONMixin): return cls.from_json(mapping) # If we have an enum, get correct one if isinstance(cls, type) and issubclass(cls, Enum): return getattr(cls, mapping['name']) # Float takes no keyword args if cls is float: return float(mapping['x']) # Otherwise use as kwargs try: if camel_case_keys: return cls( **{ snake_case(k) if isinstance(k, str) else k: v for k, v in mapping.items() }) return cls(**mapping) except (TypeError, ValueError) as e: raise ValueError(f'Bad arguments for {type_name(cls)}: {e}') from e logger.warning('Unsupported type in unjsonify: %s (%r)', type_name(json), json) return json
def __new__(mcs, name: str, bases: Tuple[Type[Any]] = tuple(), class_dict: Optional[Dict[str, Any]] = None, **kwargs) -> StaticData: """ Create a new partially fixed static data container. :param name: Container name :param bases: Container bases :param class_dict: Container class dict :param kwargs: Kwargs for container class, if dynamically generated """ if class_dict is None: class_dict = kwargs logger.info('Programmatically creating %r as instance of %s...', name, utils.type_name(mcs)) else: class_dict.update(kwargs) logger.info('Creating %r as instance of %s...', name, utils.type_name(mcs)) # Check for existing name if name in mcs: existing = getattr(mcs, name) existing_class_dict = {k: getattr(existing, k) for k in existing.__static_fields__} # This check will fail if we have any dunders available, # making this only work for programatically defined classes. if class_dict == existing_class_dict: return existing raise StaticInstanceConflict(name, existing) # Sort out attributes static_attrs = {} dynamic_attrs = {} for attr, hint in mcs.__annotations__.items(): if attr.startswith('_'): continue if getattr(mcs, attr, None) is STATIC: static_attrs[attr] = hint else: dynamic_attrs[attr] = hint # Check for missing and unexpected attrs missing = static_attrs.keys() - class_dict.keys() if missing: raise ExpectedStaticAttr(name, missing) unexpected = class_dict.keys() & dynamic_attrs if unexpected: raise UnexpectedStaticAttr(name, unexpected) # Extend static attrs with additional class_dict values full_static = static_attrs.keys() | {k for k in class_dict.keys() if k not in dynamic_attrs and not k.startswith('_')} # Instantiate object new = super().__new__(mcs, name, bases, class_dict) # Create setattr to block frozen attrs from being overridden @functools.wraps(new.__setattr__) def _setattr(self_: StaticData, name_: str, value_: Any) -> None: if name_ in self_.__static_fields__: raise AttributeError(f'Frozen attribute: {name_!r}') # pylint: disable=bad-super-call super(new, self_).__setattr__(name_, value_) # Create half-frozen dataclass new.__annotations__ = dynamic_attrs new.__static_fields__ = full_static new.__setattr__ = _setattr new = dataclasses.dataclass(new) # Register instance mcs.register(new) logger.info('%s registered with %s', utils.type_name(new), utils.type_name(mcs)) return new
def test_type_name_qualname(obj: Any, expected: str) -> None: assert utils.type_name(obj, local_name=True) == expected
def test_type_name(obj: Any, expected: str) -> None: assert utils.type_name(obj) == expected