|
1 | 1 | import inspect
|
2 | 2 | from copy import deepcopy
|
3 |
| -from typing import Optional, TYPE_CHECKING, Union, Type, TypeVar, Dict, Any |
| 3 | +from typing import Optional, TYPE_CHECKING, Union, Type, TypeVar, Dict, Any, Tuple |
4 | 4 |
|
5 | 5 | from ravendb.primitives import constants
|
6 | 6 | from ravendb.documents.session.document_info import DocumentInfo
|
@@ -136,86 +136,49 @@ def convert_to_entity_by_key(
|
136 | 136 |
|
137 | 137 | @staticmethod
|
138 | 138 | def convert_to_entity(
|
139 |
| - document: dict, |
140 |
| - object_type: [_T], |
| 139 | + document: Dict[str, Any], |
| 140 | + object_type: Type[_T], |
141 | 141 | conventions: "DocumentConventions",
|
142 | 142 | session: Optional["InMemoryDocumentSessionOperations"] = None,
|
143 | 143 | key: str = None,
|
144 | 144 | ) -> _T:
|
145 |
| - # This method has two steps - extract the type (I), and then convert it into the entity (II) |
146 |
| - # todo: Separate it into two different functions and isolate the return statements from the first part |
147 |
| - |
148 |
| - # I. Extract the object type |
149 | 145 | metadata = document.get("@metadata")
|
150 | 146 | document_deepcopy = deepcopy(document)
|
151 | 147 |
|
152 |
| - # 1. Get type from metadata |
153 |
| - type_from_metadata = conventions.try_get_type_from_metadata(metadata) |
154 |
| - is_projection = False |
155 |
| - key = key or metadata.get(constants.Documents.Metadata.ID, None) |
| 148 | + object_type, is_projection, should_update_metadata_python_type = EntityToJsonUtils.determine_object_type( |
| 149 | + document, conventions, object_type, metadata |
| 150 | + ) |
156 | 151 |
|
157 |
| - # 1.1 Check if passed object type (or extracted from metadata) is a dictionary and if document isn't a dict |
158 |
| - if object_type == dict or (object_type is None and type_from_metadata == "builtins.dict"): |
| 152 | + if object_type is dict: |
159 | 153 | EntityToJsonUtils.invoke_after_conversion_to_entity_event(session, key, object_type, document_deepcopy)
|
160 | 154 | return document_deepcopy
|
161 | 155 |
|
162 |
| - # 1.2 If there's no object type in metadata |
163 |
| - if type_from_metadata is None: |
164 |
| - # 1.2.1 Try to set it with passed object type |
165 |
| - if object_type is not None: |
166 |
| - metadata["Raven-Python-Type"] = "{0}.{1}".format(object_type.__module__, object_type.__name__) |
167 |
| - # 1.2.2 no type defined on document or during load, return a dict |
168 |
| - else: |
169 |
| - dyn = DynamicStructure(**document_deepcopy) |
170 |
| - EntityToJsonUtils.invoke_after_conversion_to_entity_event(session, key, object_type, document_deepcopy) |
171 |
| - return dyn |
| 156 | + if object_type is DynamicStructure: |
| 157 | + dyn = DynamicStructure(**document_deepcopy) |
| 158 | + EntityToJsonUtils.invoke_after_conversion_to_entity_event(session, key, object_type, document_deepcopy) |
| 159 | + return dyn |
172 | 160 |
|
173 |
| - # 2. There was a type in the metadata |
174 |
| - else: |
175 |
| - object_from_metadata = Utils.import_class(type_from_metadata) |
176 |
| - |
177 |
| - # 2.0 Check if the user wants to cast dict to type |
178 |
| - if object_from_metadata == dict and object_type != dict: |
179 |
| - pass |
180 |
| - # 2.1 Import was successful |
181 |
| - elif object_from_metadata is not None: |
182 |
| - # 2.1.1 Set object_type to successfully imported type/ from metadata inherits from passed object_type |
183 |
| - if object_type is None or Utils.is_inherit(object_type, object_from_metadata): |
184 |
| - object_type = object_from_metadata |
185 |
| - |
186 |
| - # 2.1.2 Passed type is not a type from metadata, neither there's no inheritance - probably projection |
187 |
| - elif object_type is not object_from_metadata: |
188 |
| - is_projection = True |
189 |
| - if not all([name in object_from_metadata.__dict__ for name in object_type.__dict__]): |
190 |
| - raise exceptions.InvalidOperationException( |
191 |
| - f"Cannot covert document from type {object_from_metadata} to {object_type}" |
192 |
| - ) |
193 |
| - |
194 |
| - # We have object type set - it was either extracted or passed through args |
| 161 | + if should_update_metadata_python_type: |
| 162 | + EntityToJsonUtils.set_python_type_in_metadata(metadata, object_type) |
195 | 163 |
|
196 | 164 | # Fire before conversion to entity events
|
197 | 165 | if session:
|
198 | 166 | session.before_conversion_to_entity_invoke(
|
199 | 167 | BeforeConversionToEntityEventArgs(session, key, object_type, document_deepcopy)
|
200 | 168 | )
|
201 | 169 |
|
202 |
| - # II. Conversion to entity part |
| 170 | + # Conversion to entity |
203 | 171 |
|
204 |
| - # By custom defined 'from_json' serializer class method |
205 |
| - # todo: make separate interface to do from_json |
206 | 172 | if "from_json" in object_type.__dict__ and inspect.ismethod(object_type.from_json):
|
| 173 | + # By custom defined 'from_json' serializer class method |
207 | 174 | entity = object_type.from_json(document_deepcopy)
|
208 |
| - |
209 |
| - # By projection |
210 | 175 | elif is_projection:
|
211 | 176 | entity = DynamicStructure(**document_deepcopy)
|
212 | 177 | entity.__class__ = object_type
|
213 | 178 | try:
|
214 | 179 | entity = Utils.initialize_object(document_deepcopy, object_type)
|
215 | 180 | except TypeError as e:
|
216 | 181 | raise InvalidOperationException("Probably projection error", e)
|
217 |
| - |
218 |
| - # Happy path - successful extraction of the type from metadata, if not - got object_type passed to arguments |
219 | 182 | else:
|
220 | 183 | entity = Utils.convert_json_dict_to_object(document_deepcopy, object_type)
|
221 | 184 |
|
@@ -278,3 +241,69 @@ def write_metadata(json_node: dict, document_info: DocumentInfo):
|
278 | 241 |
|
279 | 242 | if set_metadata:
|
280 | 243 | json_node.update({constants.Documents.Metadata.KEY: metadata_node})
|
| 244 | + |
| 245 | + @staticmethod |
| 246 | + def determine_object_type( |
| 247 | + document: Dict[str, Any], |
| 248 | + conventions: DocumentConventions, |
| 249 | + object_type_from_user: Optional[Type[Any]] = None, |
| 250 | + metadata: Dict[str, Any] = None, |
| 251 | + ) -> Tuple[ |
| 252 | + Type[Union[Any, DynamicStructure, Dict[str, Any]]], bool, bool |
| 253 | + ]: # -> object_type, is_projection, should_update_metadata_python_type |
| 254 | + # Try to extract the object type from the metadata |
| 255 | + type_name_from_metadata = conventions.try_get_type_from_metadata(metadata) |
| 256 | + |
| 257 | + # Check if user needs dictionary or if we can return dictionary |
| 258 | + if object_type_from_user is dict or ( |
| 259 | + object_type_from_user is None and type_name_from_metadata == "builtins.dict" |
| 260 | + ): |
| 261 | + return dict, False, False |
| 262 | + |
| 263 | + # No Python type in metadata |
| 264 | + if type_name_from_metadata is None: |
| 265 | + if object_type_from_user is not None: |
| 266 | + # Try using passed object type |
| 267 | + return object_type_from_user, False, True |
| 268 | + else: |
| 269 | + # No type defined, but the user didn't explicitly say that they need a dict - return DynamicStructure |
| 270 | + return DynamicStructure, False, False |
| 271 | + |
| 272 | + # Python object type is in the metadata |
| 273 | + object_type_from_metadata = Utils.import_class(type_name_from_metadata) |
| 274 | + if object_type_from_metadata is None: |
| 275 | + # Unsuccessful import means the document has been probably stored within other Python project |
| 276 | + # or the original object class has been removed - essentially we have only object_type to rely on |
| 277 | + if object_type_from_user is None: |
| 278 | + raise RuntimeError( |
| 279 | + f"Cannot import class '{type_name_from_metadata}' to convert '{document}' to an object, " |
| 280 | + f"it might be removed from your project. Provide an alternative object type " |
| 281 | + f"to convert the document to or pass 'dict' to receive dictionary JSON representation." |
| 282 | + ) |
| 283 | + return object_type_from_user, False, False |
| 284 | + |
| 285 | + # Successfully imported the class from metadata - but before conversion check for projections and inheritance |
| 286 | + |
| 287 | + # Maybe user wants to cast from dict to their type |
| 288 | + if object_type_from_metadata is dict: |
| 289 | + return object_type_from_user, False, False |
| 290 | + |
| 291 | + # User doesn't need projection, or class from metadata is a child of a class provided by user |
| 292 | + # We can safely use class from metadata |
| 293 | + if object_type_from_user is None or Utils.is_inherit(object_type_from_user, object_type_from_metadata): |
| 294 | + return object_type_from_metadata, False, False |
| 295 | + |
| 296 | + # Passed type is not a type from metadata, neither there's no inheritance - probably projection |
| 297 | + elif object_type_from_user is not object_type_from_metadata: |
| 298 | + if not all([name in object_type_from_metadata.__dict__ for name in object_type_from_user.__dict__]): |
| 299 | + # Document from database and object_type from user aren't compatible |
| 300 | + raise exceptions.InvalidOperationException( |
| 301 | + f"Cannot covert document from type {object_type_from_metadata} to {object_type_from_user}" |
| 302 | + ) |
| 303 | + |
| 304 | + # Projection |
| 305 | + return object_type_from_user, True, False |
| 306 | + |
| 307 | + @staticmethod |
| 308 | + def set_python_type_in_metadata(metadata: Dict[str, Any], object_type: Type[Any]) -> None: |
| 309 | + metadata["Raven-Python-Type"] = "{0}.{1}".format(object_type.__module__, object_type.__name__) |
0 commit comments