1 | from packson.JsonTypeInfo import Id,As, getTypeWrappingInfo, getClassId
|
---|
2 | import importlib
|
---|
3 | from packson.JsonSubTypes import getSubTypes
|
---|
4 | from typing import Dict, _GenericAlias, cast, Tuple
|
---|
5 | import inspect
|
---|
6 | from inspect import _empty
|
---|
7 | from inspect import Parameter
|
---|
8 | '''
|
---|
9 | Tools that are working just with a class
|
---|
10 | '''
|
---|
11 |
|
---|
12 | def isPrimitive(clas):
|
---|
13 | '''
|
---|
14 | @return true if clas is primitive: int, float, etc.
|
---|
15 | These are also the class types that can be used for dictionary keys.
|
---|
16 | list can't be primitive because we must check the type of the list elements and
|
---|
17 | use the appropriate parser for them.
|
---|
18 | '''
|
---|
19 | return clas in (int, float, bool, str, complex, range, bytes, bytearray)
|
---|
20 |
|
---|
21 | def getInitArgs( clas) -> Dict[str, object]:
|
---|
22 | '''
|
---|
23 | @param clas a class object that can be instantiated
|
---|
24 | @return dict with all arguments of clas __init__ and
|
---|
25 | their classes.
|
---|
26 | @throws ValueError if not all args are annotated with a type.
|
---|
27 | '''
|
---|
28 | if not hasattr(clas, '__init__'):
|
---|
29 | return {}
|
---|
30 | f=getattr(clas,'__init__')
|
---|
31 | argclasses = {name:param._annotation for name,param in inspect.signature(f).parameters.items()}
|
---|
32 | argclasses.pop('self')
|
---|
33 | # _empty is indicates an undefined type in the typing system.
|
---|
34 | untyped = [ name for name,param in argclasses.items() if param==_empty ]
|
---|
35 | #untyped = set(argclasses.keys()) - set(['self'])
|
---|
36 | if len(untyped)>0:
|
---|
37 | raise ValueError("init function of Class "+str(clas)+\
|
---|
38 | " must have all arguments typed, but found untyped "+str(untyped))
|
---|
39 | return argclasses
|
---|
40 |
|
---|
41 | def str_to_class(fullclasspath:str)->object:
|
---|
42 | '''
|
---|
43 | @param fullclasspath : full path specification to a class,
|
---|
44 | so that we can locate and load it.
|
---|
45 | @return a new class, loaded from the given string.
|
---|
46 | '''
|
---|
47 | x=fullclasspath.rfind(".")
|
---|
48 | module_name=""
|
---|
49 | if x>0:
|
---|
50 | module_name=fullclasspath[0:x]
|
---|
51 | # load the module, will raise ImportError if module cannot be loaded
|
---|
52 | m = importlib.import_module(module_name)
|
---|
53 | # get the class, will raise AttributeError if class cannot be found
|
---|
54 | c = getattr(m, fullclasspath[x+1:])
|
---|
55 | return c
|
---|
56 |
|
---|
57 |
|
---|
58 | def id2class(classid:str, realclasses:list):
|
---|
59 | '''
|
---|
60 | @param classid the class id , coming from the json
|
---|
61 | @param use the Id
|
---|
62 | @param realclasses the list of real classes that are allowed
|
---|
63 | @return the real class that has the requested id
|
---|
64 | '''
|
---|
65 | for clas in realclasses:
|
---|
66 | if classid==getClassId(clas):
|
---|
67 | return clas
|
---|
68 | raise ValueError("There is no class with id "+ classid+" in " + str(realclasses) )
|
---|
69 |
|
---|
70 | def addTypeInfo( clas, jdict:dict)->dict:
|
---|
71 | '''
|
---|
72 | @param clas the class of the object to be serialized
|
---|
73 | @jdict an already json-ized object dict, but without type wrapper
|
---|
74 | @return res, but with type info added
|
---|
75 | '''
|
---|
76 | (use, include) = getTypeWrappingInfo(clas)
|
---|
77 | if use == Id.NONE:
|
---|
78 | return jdict
|
---|
79 | classid = getClassId(clas)
|
---|
80 |
|
---|
81 | if include == As.WRAPPER_OBJECT:
|
---|
82 | return { classid:jdict }
|
---|
83 | else:
|
---|
84 | raise ValueError("Not implemented include type "+str(include))
|
---|
85 |
|
---|
86 |
|
---|
87 |
|
---|
88 |
|
---|
89 | def getActualClass(data:dict,clas)->tuple:
|
---|
90 | '''
|
---|
91 | @param data the json dict to be deserialized. It should contain the
|
---|
92 | class type info.
|
---|
93 | @param clas the expected class. This class IS annotated with jsonsubtypes but
|
---|
94 | clas may differ from the originally annotated class (it may just have inherited the annotation)
|
---|
95 | @return tuple (actualdata,actualclass). with the actualclass contained in the data. It must be one
|
---|
96 | of the classes in the __jsonsubtypes__ of class. actualdata is stripped of the type data contained
|
---|
97 | in the original data dict. ASSUMES typewrappinginfo is set
|
---|
98 | @throws if typewrapping info is not set.
|
---|
99 | '''
|
---|
100 | (use, include) = getTypeWrappingInfo(clas)
|
---|
101 |
|
---|
102 | if getSubTypes(clas):
|
---|
103 | (_annotatedclass, subclasslist) = getSubTypes(clas)
|
---|
104 | # we could not load the real classes earlier
|
---|
105 | # because they did not yet exist at parse time.
|
---|
106 | realclasses=[str_to_class(classname) for classname in subclasslist]
|
---|
107 | realclasses.append(clas)
|
---|
108 | else:
|
---|
109 | # class has no subtypes annotation, it can be only the indicated class.
|
---|
110 | realclasses=[clas]
|
---|
111 | '''
|
---|
112 | Algorithm: this ignores annotatedclass.
|
---|
113 | We first try to find which of the subclasslist is actually in the data.
|
---|
114 | Then we just check if that is subclass of clas.
|
---|
115 | '''
|
---|
116 | if include==As.WRAPPER_OBJECT:
|
---|
117 | if len(data.keys())!=1:
|
---|
118 | raise ValueError("WRAPPER_OBJECT requies 1 key (class id) but found "+str(data.keys()))
|
---|
119 | classid = next(iter(data.keys()))
|
---|
120 | actualdata = data[classid]
|
---|
121 | else:
|
---|
122 | raise ValueError("Not implemented: deserialization with include "+str(include))
|
---|
123 |
|
---|
124 | # find back the matching full classname
|
---|
125 | actualclas = id2class(classid, realclasses)
|
---|
126 |
|
---|
127 | # We found a class that matches the header.
|
---|
128 | # but the clas requested might be more restrictive as we may be in a subclass
|
---|
129 | #FIXME can we do this test once, somewhere, for all of the realclasses?
|
---|
130 | if not issubclass(actualclas, clas):
|
---|
131 | raise ValueError("The class ID ("+str(actualclas)+" is not implementing the requested class "+str(clas))
|
---|
132 | return (actualdata, actualclas)
|
---|
133 |
|
---|
134 | def getListClass(listofobj:set)->object:
|
---|
135 | '''
|
---|
136 | @param listofobj a list/set/tuple with at least 1 object
|
---|
137 | @return class of the list elements. All elements must be same class
|
---|
138 | @throws ValueError if that is not the case
|
---|
139 | '''
|
---|
140 | if len(listofobj)==0:
|
---|
141 | raise ValueError("bug, getListClass called with empty list")
|
---|
142 | clas=type(next(iter(listofobj))) # object may be a list, dict, set
|
---|
143 | for obj in listofobj:
|
---|
144 | if not type(obj)==clas:
|
---|
145 | raise ValueError("Expected element of type "+str(clas)+"but found "+str(obj))
|
---|
146 | return clas
|
---|
147 |
|
---|