|
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