Path: blob/main/singlestoredb/utils/xdict.py
469 views
#!/usr/bin/env python1#2# Copyright SAS Institute3#4# Licensed under the Apache License, Version 2.0 (the License);5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.15#16# This file originally copied from https://github.com/sassoftware/python-swat17#18"""Dictionary that allows setting nested keys by period (.) delimited strings."""19import copy20import re21from typing import Any22from typing import Dict23from typing import ItemsView24from typing import Iterable25from typing import KeysView26from typing import List27from typing import Tuple28from typing import ValuesView293031def _is_compound_key(key: str) -> bool:32"""33Check for a compound key name.3435Parameters36----------37key : string38The key name to check3940Returns41-------42True43If the key is compound (i.e., contains a '.')44False45If the key is not compound4647"""48return isinstance(key, str) and '.' in key495051class xdict(Dict[str, Any]):52"""53Nested dictionary that allows setting of nested keys using '.' delimited strings.5455Keys with a '.' in them automatically get split into separate keys.56Each '.' in a key represents another level of nesting in the resulting57dictionary.5859Parameters60----------61*args, **kwargs : Arbitrary arguments and keyword arguments62Same arguments as `dict`6364Returns65-------66xdict object6768Examples69--------70>>> dct = xdict()71>>> dct['a.b.c'] = 10072{'a': {'b': {'c': 100}}}7374"""7576_dir: List[str]7778def __init__(self, *args: Any, **kwargs: Any):79super(xdict, self).__init__()80self.update(*args, **kwargs)8182def __dir__(self) -> Iterable[str]:83"""Return keys in the dict."""84if hasattr(self, '_dir') and self._dir:85return list(self._dir)86return super(xdict, self).__dir__()8788def set_dir_values(self, values: List[str]) -> None:89"""90Set the valid values for keys to display in tab-completion.9192Parameters93----------94values : list of strs95The values to display9697"""98super(xdict, self).__setattr__('_dir', values)99100def set_doc(self, docstring: str) -> None:101"""Set the docstring for the xdict."""102super(xdict, self).__setattr__('__doc__', docstring)103104def __copy__(self) -> 'xdict':105"""Return a copy of self."""106return type(self)(**self)107108def __deepcopy__(self, memo: Any) -> 'xdict':109"""Return a deep copy of self."""110out = type(self)()111for key, value in self.items():112if isinstance(value, (dict, list, tuple, set)):113value = copy.deepcopy(value)114out[key] = value115return out116117@classmethod118def from_json(cls, jsonstr: str) -> 'xdict':119"""120Create an xdict object from a JSON string.121122Parameters123----------124jsonstr : string125Valid JSON string that represents an object126127Returns128-------129xdict object130131"""132import json133out = cls()134out.update(json.loads(jsonstr))135return out136137def __setitem__(self, key: str, value: Any) -> Any:138"""Set a key/value pair in an xdict object."""139if isinstance(value, dict) and not isinstance(value, type(self)):140value = type(self)(value)141if _is_compound_key(key):142return self._xset(key, value)143return super(xdict, self).__setitem__(key, value)144145def _xset(self, key: str, value: Any) -> Any:146"""147Set a key/value pair allowing nested levels in the key.148149Parameters150----------151key : any152Key value, if it is a string delimited by periods (.), each153period represents another level of nesting of xdict objects.154value : any155Data value156157"""158if isinstance(value, dict) and not isinstance(value, type(self)):159value = type(self)(value)160if _is_compound_key(key):161current, key = key.split('.', 1)162if current not in self:163self[current] = type(self)()164return self[current]._xset(key, value)165self[key] = value166return None167168def setdefault(self, key: str, *default: Any) -> Any:169"""Return keyed value, or set it to `default` if missing."""170if _is_compound_key(key):171try:172return self[key]173except KeyError:174if default:175new_default = default[0]176if isinstance(default, dict) and not isinstance(default, type(self)):177new_default = type(self)(default)178else:179new_default = None180self[key] = new_default181return new_default182return super(xdict, self).setdefault(key, *default)183184def __contains__(self, key: object) -> bool:185"""Does the xdict contain `key`?."""186if super(xdict, self).__contains__(key):187return True188return key in self.allkeys()189190has_key = __contains__191192def __getitem__(self, key: str) -> Any:193"""Get value stored at `key`."""194if _is_compound_key(key):195return self._xget(key)196return super(xdict, self).__getitem__(key)197198def _xget(self, key: str, *default: Any) -> Any:199"""200Return keyed value, or `default` if missing201202Parameters203----------204key : any205Key to look up206*default : any207Default value to return if key is missing208209Returns210-------211Any212213"""214if _is_compound_key(key):215current, key = key.split('.', 1)216try:217return self[current]._xget(key)218except KeyError:219if default:220return default[0]221raise KeyError(key)222return self[key]223224def get(self, key: str, *default: Any) -> Any:225"""Return keyed value, or `default` if missing."""226if _is_compound_key(key):227return self._xget(key, *default)228return super(xdict, self).get(key, *default)229230def __delitem__(self, key: str) -> Any:231"""Deleted keyed item."""232if _is_compound_key(key):233return self._xdel(key)234super(xdict, self).__delitem__(key)235236def _xdel(self, key: str) -> Any:237"""238Delete keyed item.239240Parameters241----------242key : any243Key to delete. If it is a string that is period (.) delimited,244each period represents another level of nesting of xdict objects.245246"""247if _is_compound_key(key):248current, key = key.split('.', 1)249try:250return self[current]._xdel(key)251except KeyError:252raise KeyError(key)253del self[key]254255def pop(self, key: str, *default: Any) -> Any:256"""Remove and return value stored at `key`."""257try:258out = self[key]259del self[key]260return out261except KeyError:262if default:263return default[0]264raise KeyError(key)265266def _flatten(267self,268dct: Dict[str, Any],269output: Dict[str, Any],270prefix: str = '',271) -> None:272"""273Create a new dict with keys flattened to period (.) delimited keys274275Parameters276----------277dct : dict278The dictionary to flatten279output : dict280The resulting dictionary (used internally in recursion)281prefix : string282Key prefix built from upper levels of nesting283284Returns285-------286dict287288"""289if prefix:290prefix = prefix + '.'291for key, value in dct.items():292if isinstance(value, dict):293if isinstance(key, int):294intkey = '%s[%s]' % (re.sub(r'\.$', r'', prefix), key)295self._flatten(value, prefix=intkey, output=output)296else:297self._flatten(value, prefix=prefix + key, output=output)298else:299if isinstance(key, int):300intkey = '%s[%s]' % (re.sub(r'\.$', r'', prefix), key)301output[intkey] = value302else:303output[prefix + key] = value304305def flattened(self) -> Dict[str, Any]:306"""Return an xdict with keys flattened to period (.) delimited strings."""307output: Dict[str, Any] = {}308self._flatten(self, output)309return output310311def allkeys(self) -> List[str]:312"""Return a list of all possible keys (even sub-keys) in the xdict."""313out = set()314for key in self.flatkeys():315out.add(key)316while '.' in key:317key = key.rsplit('.', 1)[0]318out.add(key)319if '[' in key:320out.add(re.sub(r'\[\d+\]', r'', key))321return list(out)322323def flatkeys(self) -> List[str]:324"""Return a list of flattened keys in the xdict."""325return list(self.flattened().keys())326327def flatvalues(self) -> List[Any]:328"""Return a list of flattened values in the xdict."""329return list(self.flattened().values())330331def flatitems(self) -> List[Tuple[str, Any]]:332"""Return tuples of flattened key/value pairs."""333return list(self.flattened().items())334335def iterflatkeys(self) -> Iterable[str]:336"""Return iterator of flattened keys."""337return iter(self.flattened().keys())338339def iterflatvalues(self) -> Iterable[Any]:340"""Return iterator of flattened values."""341return iter(self.flattened().values())342343def iterflatitems(self) -> Iterable[Tuple[str, Any]]:344"""Return iterator of flattened items."""345return iter(self.flattened().items())346347def viewflatkeys(self) -> KeysView[str]:348"""Return view of flattened keys."""349return self.flattened().keys()350351def viewflatvalues(self) -> ValuesView[Any]:352"""Return view of flattened values."""353return self.flattened().values()354355def viewflatitems(self) -> ItemsView[str, Any]:356"""Return view of flattened items."""357return self.flattened().items()358359def update(self, *args: Any, **kwargs: Any) -> None:360"""Merge the key/value pairs into `self`."""361for arg in args:362if isinstance(arg, dict):363for key, value in arg.items():364self._xset(key, value)365else:366for key, value in arg:367self._xset(key, value)368for key, value in kwargs.items():369self._xset(key, value)370371def to_json(self) -> str:372"""373Convert an xdict object to a JSON string.374375Returns376-------377str378379"""380import json381return json.dumps(self)382383384class xadict(xdict):385"""An xdict that also allows setting/getting/deleting keys as attributes."""386387getdoc = None388trait_names = None389390def _getAttributeNames(self) -> None:391"""Block this from creating attributes."""392return393394def __delattr__(self, key: str) -> Any:395"""Delete the attribute stored at `key`."""396if key.startswith('_') and key.endswith('_'):397return super(xadict, self).__delattr__(key)398del self[key]399400def __getattr__(self, key: str) -> Any:401"""Get the attribute store at `key`."""402if key.startswith('_') and key.endswith('_'):403return super(xadict, self).__getattr__(key) # type: ignore404try:405return self[key]406except KeyError:407dct = type(self)()408self[key] = dct409return dct410return None411412def __getitem__(self, key: str) -> Any:413"""Get item of an integer creates a new dict."""414if isinstance(key, int) and key not in self:415out = type(self)()416self[key] = out417return out418return super(xadict, self).__getitem__(key)419420def __setattr__(self, key: str, value: Any) -> Any:421"""Set the attribute stored at `key`."""422if key.startswith('_') and key.endswith('_'):423return super(xadict, self).__setattr__(key, value)424self[key] = value425426427