#!/usr/bin/env python _W='box_settings' _V='default_box_attr' _U='Box is frozen' _T='modify_tuples_box' _S='box_safe_prefix' _R='default_box_none_transform' _Q='__created' _P='box_dots' _O='box_duplicates' _N='ignore' _M='strict' _L='box_recast' _K='box_intact_types' _J='default_box' _I='utf-8' _H='_box_config' _G=True _F='camel_killer_box' _E='conversion_box' _D='frozen_box' _C='__safe_keys' _B=False _A=None import copy,re,string,warnings from collections.abc import Iterable,Mapping,Callable from keyword import kwlist from pathlib import Path from typing import Any,Union,Tuple,List,Dict from dynaconf.vendor import box from.converters import _to_json,_from_json,_from_toml,_to_toml,_from_yaml,_to_yaml,BOX_PARAMETERS from.exceptions import BoxError,BoxKeyError,BoxTypeError,BoxValueError,BoxWarning __all__=['Box'] _first_cap_re=re.compile('(.)([A-Z][a-z]+)') _all_cap_re=re.compile('([a-z0-9])([A-Z])') _list_pos_re=re.compile('\\[(\\d+)\\]') NO_DEFAULT=object() def _camel_killer(attr):B='\\1_\\2';A=attr;A=str(A);C=_first_cap_re.sub(B,A);D=_all_cap_re.sub(B,C);return re.sub(' *_+','_',D.lower()) def _recursive_tuples(iterable,box_class,recreate_tuples=_B,**E): D=recreate_tuples;C=box_class;B=[] for A in iterable: if isinstance(A,dict):B.append(C(A,**E)) elif isinstance(A,list)or D and isinstance(A,tuple):B.append(_recursive_tuples(A,C,D,**E)) else:B.append(A) return tuple(B) def _parse_box_dots(item): A=item for(B,C)in enumerate(A): if C=='[':return A[:B],A[B:] elif C=='.':return A[:B],A[B+1:] raise BoxError('Could not split box dots properly') def _get_box_config():return{_Q:_B,_C:{}} class Box(dict): _protected_keys=['to_dict','to_json','to_yaml','from_yaml','from_json','from_toml','to_toml','merge_update']+[A for A in dir({})if not A.startswith('_')] def __new__(A,*D,box_settings=_A,default_box=_B,default_box_attr=NO_DEFAULT,default_box_none_transform=_G,frozen_box=_B,camel_killer_box=_B,conversion_box=_G,modify_tuples_box=_B,box_safe_prefix='x',box_duplicates=_N,box_intact_types=(),box_recast=_A,box_dots=_B,**E):C=default_box_attr;B=super(Box,A).__new__(A,*D,**E);B._box_config=_get_box_config();B._box_config.update({_J:default_box,_V:A.__class__ if C is NO_DEFAULT else C,_R:default_box_none_transform,_E:conversion_box,_S:box_safe_prefix,_D:frozen_box,_F:camel_killer_box,_T:modify_tuples_box,_O:box_duplicates,_K:tuple(box_intact_types),_L:box_recast,_P:box_dots,_W:box_settings or{}});return B def __init__(A,*B,box_settings=_A,default_box=_B,default_box_attr=NO_DEFAULT,default_box_none_transform=_G,frozen_box=_B,camel_killer_box=_B,conversion_box=_G,modify_tuples_box=_B,box_safe_prefix='x',box_duplicates=_N,box_intact_types=(),box_recast=_A,box_dots=_B,**F): E=default_box_attr;super().__init__();A._box_config=_get_box_config();A._box_config.update({_J:default_box,_V:A.__class__ if E is NO_DEFAULT else E,_R:default_box_none_transform,_E:conversion_box,_S:box_safe_prefix,_D:frozen_box,_F:camel_killer_box,_T:modify_tuples_box,_O:box_duplicates,_K:tuple(box_intact_types),_L:box_recast,_P:box_dots,_W:box_settings or{}}) if not A._box_config[_E]and A._box_config[_O]!=_N:raise BoxError('box_duplicates are only for conversion_boxes') if len(B)==1: if isinstance(B[0],str):raise BoxValueError('Cannot extrapolate Box from string') if isinstance(B[0],Mapping): for(D,C)in B[0].items(): if C is B[0]:C=A if C is _A and A._box_config[_J]and A._box_config[_R]:continue A.__setitem__(D,C) elif isinstance(B[0],Iterable): for(D,C)in B[0]:A.__setitem__(D,C) else:raise BoxValueError('First argument must be mapping or iterable') elif B:raise BoxTypeError(f"Box expected at most 1 argument, got {len(B)}") for(D,C)in F.items(): if B and isinstance(B[0],Mapping)and C is B[0]:C=A A.__setitem__(D,C) A._box_config[_Q]=_G def __add__(C,other): A=other;B=C.copy() if not isinstance(A,dict):raise BoxTypeError(f"Box can only merge two boxes or a box and a dictionary.") B.merge_update(A);return B def __hash__(A): if A._box_config[_D]: B=54321 for C in A.items():B^=hash(C) return B raise BoxTypeError('unhashable type: "Box"') def __dir__(B): D=string.ascii_letters+string.digits+'_';C=set(super().__dir__()) for A in B.keys(): A=str(A) if' 'not in A and A[0]not in string.digits and A not in kwlist: for E in A: if E not in D:break else:C.add(A) for A in B.keys(): if A not in C: if B._box_config[_E]: A=B._safe_attr(A) if A:C.add(A) return list(C) def get(B,key,default=NO_DEFAULT): C=key;A=default if C not in B: if A is NO_DEFAULT: if B._box_config[_J]and B._box_config[_R]:return B.__get_default(C) else:return if isinstance(A,dict)and not isinstance(A,Box):return Box(A,box_settings=B._box_config.get(_W)) if isinstance(A,list)and not isinstance(A,box.BoxList):return box.BoxList(A) return A return B[C] def copy(A):return Box(super().copy(),**A.__box_config()) def __copy__(A):return Box(super().copy(),**A.__box_config()) def __deepcopy__(A,memodict=_A): B=memodict;E=A._box_config[_D];D=A.__box_config();D[_D]=_B;C=A.__class__(**D);B=B or{};B[id(A)]=C for(F,G)in A.items():C[copy.deepcopy(F,B)]=copy.deepcopy(G,B) C._box_config[_D]=E;return C def __setstate__(A,state):B=state;A._box_config=B[_H];A.__dict__.update(B) def keys(A):return super().keys() def values(A):return[A[B]for B in A.keys()] def items(A):return[(B,A[B])for B in A.keys()] def _safe_items(A):return[(B,A._safe_get(B))for B in A.keys()] def __get_default(B,item): A=B._box_config[_V] if A in(B.__class__,dict):C=B.__class__(**B.__box_config()) elif isinstance(A,dict):C=B.__class__(**B.__box_config(),**A) elif isinstance(A,list):C=box.BoxList(**B.__box_config()) elif isinstance(A,Callable):C=A() elif hasattr(A,'copy'):C=A.copy() else:C=A B.__convert_and_store(item,C);return C def __box_config(C): A={} for(B,D)in C._box_config.copy().items(): if not B.startswith('__'):A[B]=D return A def __recast(A,item,value): C=value;B=item if A._box_config[_L]and B in A._box_config[_L]: try:return A._box_config[_L][B](C) except ValueError:raise BoxValueError(f"Cannot convert {C} to {A._box_config[_L][B]}")from _A return C def __convert_and_store(B,item,value): C=item;A=value if B._box_config[_E]:D=B._safe_attr(C);B._box_config[_C][D]=C if isinstance(A,(int,float,str,bytes,bytearray,bool,complex,set,frozenset)):return super().__setitem__(C,A) if B._box_config[_K]and isinstance(A,B._box_config[_K]):return super().__setitem__(C,A) if isinstance(A,dict)and not isinstance(A,Box):A=B.__class__(A,**B.__box_config()) elif isinstance(A,list)and not isinstance(A,box.BoxList): if B._box_config[_D]:A=_recursive_tuples(A,B.__class__,recreate_tuples=B._box_config[_T],**B.__box_config()) else:A=box.BoxList(A,box_class=B.__class__,**B.__box_config()) elif B._box_config[_T]and isinstance(A,tuple):A=_recursive_tuples(A,B.__class__,recreate_tuples=_G,**B.__box_config()) super().__setitem__(C,A) def __getitem__(B,item,_ignore_default=_B): A=item try:return super().__getitem__(A) except KeyError as E: if A==_H:raise BoxKeyError('_box_config should only exist as an attribute and is never defaulted')from _A if B._box_config[_P]and isinstance(A,str)and('.'in A or'['in A): C,F=_parse_box_dots(A) if C in B.keys(): if hasattr(B[C],'__getitem__'):return B[C][F] if B._box_config[_F]and isinstance(A,str): D=_camel_killer(A) if D in B.keys():return super().__getitem__(D) if B._box_config[_J]and not _ignore_default:return B.__get_default(A) raise BoxKeyError(str(E))from _A def __getattr__(A,item): B=item try: try:C=A.__getitem__(B,_ignore_default=_G) except KeyError:C=object.__getattribute__(A,B) except AttributeError as E: if B=='__getstate__':raise BoxKeyError(B)from _A if B==_H:raise BoxError('_box_config key must exist')from _A if A._box_config[_E]: D=A._safe_attr(B) if D in A._box_config[_C]:return A.__getitem__(A._box_config[_C][D]) if A._box_config[_J]:return A.__get_default(B) raise BoxKeyError(str(E))from _A return C def __setitem__(A,key,value): C=value;B=key if B!=_H and A._box_config[_Q]and A._box_config[_D]:raise BoxError(_U) if A._box_config[_P]and isinstance(B,str)and'.'in B: D,E=_parse_box_dots(B) if D in A.keys(): if hasattr(A[D],'__setitem__'):return A[D].__setitem__(E,C) C=A.__recast(B,C) if B not in A.keys()and A._box_config[_F]: if A._box_config[_F]and isinstance(B,str):B=_camel_killer(B) if A._box_config[_E]and A._box_config[_O]!=_N:A._conversion_checks(B) A.__convert_and_store(B,C) def __setattr__(A,key,value): C=value;B=key if B!=_H and A._box_config[_D]and A._box_config[_Q]:raise BoxError(_U) if B in A._protected_keys:raise BoxKeyError(f'Key name "{B}" is protected') if B==_H:return object.__setattr__(A,B,C) C=A.__recast(B,C);D=A._safe_attr(B) if D in A._box_config[_C]:B=A._box_config[_C][D] A.__setitem__(B,C) def __delitem__(A,key): B=key if A._box_config[_D]:raise BoxError(_U) if B not in A.keys()and A._box_config[_P]and isinstance(B,str)and'.'in B: C,E=B.split('.',1) if C in A.keys()and isinstance(A[C],dict):return A[C].__delitem__(E) if B not in A.keys()and A._box_config[_F]: if A._box_config[_F]and isinstance(B,str): for D in A: if _camel_killer(B)==D:B=D;break super().__delitem__(B) def __delattr__(A,item): B=item if A._box_config[_D]:raise BoxError(_U) if B==_H:raise BoxError('"_box_config" is protected') if B in A._protected_keys:raise BoxKeyError(f'Key name "{B}" is protected') try:A.__delitem__(B) except KeyError as D: if A._box_config[_E]: C=A._safe_attr(B) if C in A._box_config[_C]:A.__delitem__(A._box_config[_C][C]);del A._box_config[_C][C];return raise BoxKeyError(D) def pop(B,key,*C): A=key if C: if len(C)!=1:raise BoxError('pop() takes only one optional argument "default"') try:D=B[A] except KeyError:return C[0] else:del B[A];return D try:D=B[A] except KeyError:raise BoxKeyError('{0}'.format(A))from _A else:del B[A];return D def clear(A):super().clear();A._box_config[_C].clear() def popitem(A): try:B=next(A.__iter__()) except StopIteration:raise BoxKeyError('Empty box')from _A return B,A.pop(B) def __repr__(A):return f"" def __str__(A):return str(A.to_dict()) def __iter__(A): for B in A.keys():yield B def __reversed__(A): for B in reversed(list(A.keys())):yield B def to_dict(D): A=dict(D) for(C,B)in A.items(): if B is D:A[C]=A elif isinstance(B,Box):A[C]=B.to_dict() elif isinstance(B,box.BoxList):A[C]=B.to_list() return A def update(C,__m=_A,**D): B=__m if B: if hasattr(B,'keys'): for A in B:C.__convert_and_store(A,B[A]) else: for(A,E)in B:C.__convert_and_store(A,E) for A in D:C.__convert_and_store(A,D[A]) def merge_update(A,__m=_A,**E): C=__m def D(k,v): B=A._box_config[_K]and isinstance(v,A._box_config[_K]) if isinstance(v,dict)and not B: v=A.__class__(v,**A.__box_config()) if k in A and isinstance(A[k],dict): if isinstance(A[k],Box):A[k].merge_update(v) else:A[k].update(v) return if isinstance(v,list)and not B:v=box.BoxList(v,**A.__box_config()) A.__setitem__(k,v) if C: if hasattr(C,'keys'): for B in C:D(B,C[B]) else: for(B,F)in C:D(B,F) for B in E:D(B,E[B]) def setdefault(B,item,default=_A): C=item;A=default if C in B:return B[C] if isinstance(A,dict):A=B.__class__(A,**B.__box_config()) if isinstance(A,list):A=box.BoxList(A,box_class=B.__class__,**B.__box_config()) B[C]=A;return A def _safe_attr(C,attr): B=attr;G=string.ascii_letters+string.digits+'_' if isinstance(B,tuple):B='_'.join([str(A)for A in B]) B=B.decode(_I,_N)if isinstance(B,bytes)else str(B) if C.__box_config()[_F]:B=_camel_killer(B) A=[];D=0 for(E,F)in enumerate(B): if F in G:D=E;A.append(F) elif not A:continue elif D==E-1:A.append('_') A=''.join(A)[:D+1] try:int(A[0]) except(ValueError,IndexError):pass else:A=f"{C.__box_config()[_S]}{A}" if A in kwlist:A=f"{C.__box_config()[_S]}{A}" return A def _conversion_checks(A,item): B=A._safe_attr(item) if B in A._box_config[_C]: C=[f"{item}({B})",f"{A._box_config[_C][B]}({B})"] if A._box_config[_O].startswith('warn'):warnings.warn(f"Duplicate conversion attributes exist: {C}",BoxWarning) else:raise BoxError(f"Duplicate conversion attributes exist: {C}") def to_json(A,filename=_A,encoding=_I,errors=_M,**B):return _to_json(A.to_dict(),filename=filename,encoding=encoding,errors=errors,**B) @classmethod def from_json(E,json_string=_A,filename=_A,encoding=_I,errors=_M,**A): D={} for B in A.copy(): if B in BOX_PARAMETERS:D[B]=A.pop(B) C=_from_json(json_string,filename=filename,encoding=encoding,errors=errors,**A) if not isinstance(C,dict):raise BoxError(f"json data not returned as a dictionary, but rather a {type(C).__name__}") return E(C,**D) def to_yaml(A,filename=_A,default_flow_style=_B,encoding=_I,errors=_M,**B):return _to_yaml(A.to_dict(),filename=filename,default_flow_style=default_flow_style,encoding=encoding,errors=errors,**B) @classmethod def from_yaml(E,yaml_string=_A,filename=_A,encoding=_I,errors=_M,**A): D={} for B in A.copy(): if B in BOX_PARAMETERS:D[B]=A.pop(B) C=_from_yaml(yaml_string=yaml_string,filename=filename,encoding=encoding,errors=errors,**A) if not isinstance(C,dict):raise BoxError(f"yaml data not returned as a dictionary but rather a {type(C).__name__}") return E(C,**D) def to_toml(A,filename=_A,encoding=_I,errors=_M):return _to_toml(A.to_dict(),filename=filename,encoding=encoding,errors=errors) @classmethod def from_toml(D,toml_string=_A,filename=_A,encoding=_I,errors=_M,**B): C={} for A in B.copy(): if A in BOX_PARAMETERS:C[A]=B.pop(A) E=_from_toml(toml_string=toml_string,filename=filename,encoding=encoding,errors=errors);return D(E,**C)