Source code for syside.json

  1"""
  2Convenience module intending to match the standard library ``json`` module.
  3"""
  4
  5from collections.abc import Iterable
  6from typing import Callable, overload
  7import typing
  8from uuid import UUID
  9from warnings import warn
 10from dataclasses import dataclass
 11
 12from .._loading import BaseModel, Environment
 13from .. import core as syside
 14
 15
[docs] 16@dataclass 17class SerializationError(Exception): 18 """ 19 Error serializing element to SysML v2 JSON. 20 """ 21 22 report: syside.SerdeReport[syside.Element] 23 """Messages emitted during serialization of ``Element`` tree."""
24 25 26type DeserializationReport = syside.SerdeReport[ 27 syside.Element | str | syside.DocumentSegment 28] 29"""Messages emitted during deserialization of ``Element`` tree.""" 30 31
[docs] 32class SerdeError(Exception): 33 """ 34 Class for exceptions from serialization and deserialization 35 """
36 37
[docs] 38@dataclass 39class DeserializationError(SerdeError): 40 """ 41 Error deserializing document from SysML v2 JSON. 42 """ 43 44 model: syside.DeserializedModel 45 """ 46 The deserialized model. 47 48 May be incomplete due to errors. 49 """ 50 report: DeserializationReport 51 """Messages emitted during deserialization of ``Element`` tree.""" 52 53 def __str__(self) -> str: 54 return str(self.report)
55 56
[docs] 57@dataclass 58class ProjectDeserializationError(SerdeError): 59 """ 60 Error deserializing project from SysML v2 JSON. 61 """ 62 63 models: list[syside.DeserializedModel] 64 """ 65 The deserialized models in this project. 66 67 Models may be incomplete due to errors. 68 """ 69 reports: list[DeserializationReport] 70 """ 71 Messages emitted during project deserialization. 72 73 Each entry corresponds to an entry in ``models`` at the same index. 74 """ 75 76 def __str__(self) -> str: 77 return "\n".join(str(report) for report in self.reports)
78 79
[docs] 80class SerdeWarning(Warning): 81 """ 82 Class for warnings from serialization and deserialization 83 """
84 85
[docs] 86def dumps( 87 element: syside.Element, 88 options: syside.SerializationOptions, 89 indent: int = 2, 90 use_spaces: bool = True, 91 final_new_line: bool = True, 92 include_cross_ref_uris: bool = True, 93) -> str: 94 """ 95 Serialize ``element`` to a SysML v2 JSON ``str``. 96 97 See the documentation of the :py:class:`SerializationOptions 98 <syside.SerializationOptions>` class for documentation of the possible 99 options. The options object constructed with 100 :py:meth:`SerializationOptions.minimal 101 <syside.SerializationOptions.minimal>` instructs to produce a minimal JSON 102 without any redundant elements that results in significantly smaller JSONs. 103 Examples of redundant information that is avoided using minimal 104 configuration are: 105 106 + including fields for null values; 107 + including fields whose values match the default values; 108 + including redefined fields that are duplicates of redefining fields; 109 + including derived fields that can be computed from minimal JSON (for 110 example, the result value of evaluating an expression); 111 + including implied relationships. 112 113 .. note:: 114 115 Syside does not construct all derived properties yet. Therefore, setting 116 ``options.include_derived`` to ``True`` may result in a JSON that does 117 not satisfy the schema. 118 119 :param element: 120 The SysML v2 element to be serialized to SysML v2 JSON. 121 :param options: 122 The serialization options to use when serializing SysML v2 to JSON. 123 :param indent: 124 How many space or tab characters to use for indenting the JSON. 125 :param use_spaces: 126 Whether use spaces or tabs for indentation. 127 :param final_new_line: 128 Whether to add a newline character at the end of the generated string. 129 :param include_cross_ref_uris: 130 Whether to add potentially relative URIs as ``@uri`` property to 131 references of Elements from documents other than the one owning 132 ``element``. Note that while such references are non-standard, they 133 match the behaviour of XMI exports in Pilot implementation which use 134 relative URIs for references instead of plain element IDs. 135 :return: 136 ``element`` serialized as JSON. 137 """ 138 139 writer = syside.JsonStringWriter( 140 indent=indent, 141 use_spaces=use_spaces, 142 final_new_line=final_new_line, 143 include_cross_ref_uris=include_cross_ref_uris, 144 ) 145 146 report = syside.serialize(element, writer, options) 147 148 if not report: 149 raise SerializationError(report) 150 151 for msg in report.messages: 152 if msg.severity == syside.DiagnosticSeverity.Warning: 153 warn(msg.message, category=SerdeWarning, stacklevel=2) 154 155 return writer.result
156 157 158type JsonSourceNew = tuple[str | syside.Url, str] 159"""A pair of ``(url, json)`` to deserialize ``json`` into a new document with ``url``.""" 160type JsonSourceInto = tuple[syside.Document, str] 161"""A pair of ``(document, json)`` to deserialize ``json`` into an existing ``document``.""" 162 163 164def _deserialize_document( 165 s: str, 166 document: str | syside.Url | syside.Document, 167 reader: syside.JsonReader, 168 attributes: syside.AttributeMap | None = None, 169) -> tuple[ 170 syside.DeserializedModel, 171 DeserializationReport, 172 syside.AttributeMap, 173]: 174 new_doc: syside.SharedMutex[syside.Document] | None = None 175 if isinstance(document, str): 176 document = syside.Url(document) 177 if isinstance(document, syside.Url): 178 ext = document.path.rsplit(".", 1)[-1].lower() 179 if ext == "sysml": 180 lang = syside.ModelLanguage.SysML 181 elif ext == "kerml": 182 lang = syside.ModelLanguage.KerML 183 else: 184 raise ValueError(f"Unknown document language, could not infer from '{ext}'") 185 new_doc = syside.Document.create_st(url=document, language=lang) 186 with new_doc.lock() as doc: 187 document = doc # appease pylint 188 189 with reader.bind(s) as json: 190 if attributes is None: 191 attributes = json.attribute_hint() 192 if attributes is None: 193 raise ValueError("Cannot deserialize model with unmapped attributes") 194 195 model, report = syside.deserialize(document, json, attributes) 196 197 return model, report, attributes 198 199 200_STDLIB_MAP: syside.IdMap | None = None 201 202 203def _map_from_env(docs: Iterable[syside.SharedMutex[syside.Document]]) -> syside.IdMap: 204 ids = syside.IdMap() 205 for mutex in docs: 206 with mutex.lock() as doc: 207 ids.insert_or_assign(doc) 208 return ids 209 210 211def _loads_project( 212 s: Iterable[JsonSourceNew | JsonSourceInto], 213 environment: Environment | None = None, 214 resolve: Callable[[str, UUID], syside.Element | None] | None = None, 215 attributes: syside.AttributeMap | None = None, 216) -> tuple[ 217 BaseModel, 218 list[tuple[syside.DeserializedModel, DeserializationReport]], 219]: 220 reader = syside.JsonReader() 221 222 init: list[tuple[syside.DeserializedModel, DeserializationReport]] = [] 223 for doc, src in s: 224 model, report, attributes = _deserialize_document(src, doc, reader, attributes) 225 init.append((model, report)) 226 227 passed = all(report.passed() for _, report in init) 228 if not passed: 229 raise ProjectDeserializationError( 230 models=[model for model, _ in init], reports=[report for _, report in init] 231 ) 232 233 for _, report in init: 234 for msg in report.messages: 235 if ( 236 msg.severity == syside.DiagnosticSeverity.Warning 237 and not msg.message.startswith("Could not find reference") 238 ): 239 warn(msg.message, category=SerdeWarning, stacklevel=3) 240 241 base: syside.IdMap | None = None 242 if environment is None or environment is getattr(Environment, "_STDLIB", None): 243 environment = Environment.get_default() 244 # ignore pylint, we use global variable for caching 245 global _STDLIB_MAP # pylint: disable=global-statement 246 if _STDLIB_MAP is None: 247 _STDLIB_MAP = _map_from_env(environment.documents) 248 base = _STDLIB_MAP 249 elif not resolve: 250 # defer environment map creation as it may be relatively expensive and 251 # duplicate user provided ``resolve`` 252 base = _map_from_env(environment.documents) 253 254 local: syside.IdMap | None = None 255 if len(init) > 1: 256 # small optimization as 1 document will have resolved its own references 257 # automatically 258 local = syside.IdMap() 259 for model, _ in init: 260 local.insert_or_assign(model.document) 261 262 def resolver(url: str, uuid: UUID) -> syside.Element | None: 263 if local and (result := local(url, uuid)): 264 return result 265 266 if resolve and (result := resolve(url, uuid)): 267 return result 268 269 nonlocal base 270 if base is None: 271 base = _map_from_env(environment.documents) 272 273 if result := base(url, uuid): 274 return result 275 276 return None 277 278 index = environment.index() 279 for i, (model, _) in enumerate(init): 280 init[i] = (model, model.link(resolver)[0]) 281 syside.collect_exports(model.document) 282 index.insert(model.document) 283 284 passed = all(report.passed() for _, report in init) 285 if not passed: 286 raise ProjectDeserializationError( 287 models=[model for model, _ in init], reports=[report for _, report in init] 288 ) 289 290 return BaseModel( 291 result=None, 292 environment=environment, 293 documents=[model.document.mutex for model, _ in init], 294 lib=environment.lib, 295 index=index, 296 ), init 297 298 299def _loads_document( 300 s: str, 301 document: syside.Document | syside.Url | str, 302 attributes: syside.AttributeMap | None = None, 303) -> ( 304 syside.DeserializedModel 305 | tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]] 306): 307 reader = syside.JsonReader() 308 309 model, report, _ = _deserialize_document(s, document, reader, attributes) 310 311 if not report: 312 raise DeserializationError(model, report) 313 314 for msg in report.messages: 315 if msg.severity == syside.DiagnosticSeverity.Warning: 316 warn(msg.message, category=SerdeWarning, stacklevel=3) 317 318 if not isinstance(document, syside.Document): 319 return model, model.document.mutex 320 return model 321 322 323@overload 324def loads( 325 s: str, 326 document: syside.Document, 327 attributes: syside.AttributeMap | None = None, 328) -> syside.DeserializedModel: 329 """ 330 Deserialize a model from ``s`` into an already existing ``document``. 331 332 Root node will be inferred as: 333 334 1. The first ``Namespace`` (not subtype) without an owning relationship. 335 2. The first ``Element`` that has no serialized owning related element or owning relationship, 336 starting from the first element in the JSON array, and following owning elements up. 337 3. The first element in the array otherwise. 338 339 :param s: 340 The string contained serialized SysML model in JSON array. 341 :param document: 342 The document the model will be deserialized into. 343 :param attributes: 344 Attribute mapping of ``s``. If none provided, this will attempt to infer 345 a corresponding mapping or raise a ``ValueError``. 346 :return: 347 Model deserialized from JSON array. Note that references into other 348 documents will not be resolved, users will need to resolve them by 349 calling ``link`` on the returned model. See also :py:class:`IdMap 350 <syside.IdMap>`. 351 """ 352 353 354@overload 355def loads( 356 s: str, 357 document: syside.Url | str, 358 attributes: syside.AttributeMap | None = None, 359) -> tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]]: 360 """ 361 Create a new ``document`` and deserialize a model from ``s`` into it. 362 363 Root node will be inferred as: 364 365 1. The first ``Namespace`` (not subtype) without an owning relationship. 366 2. The first ``Element`` that has no serialized owning related element or owning relationship, 367 starting from the first element in the JSON array, and following owning elements up. 368 3. The first element in the array otherwise. 369 370 :param s: 371 The string contained serialized SysML model in JSON array. 372 :param document: 373 A URI in the form of :py:class:`Url <syside.Url>` or a string, new 374 document will be created with. If URI path has no extension, or the 375 extension does not match ``sysml`` or ``kerml``, ``ValueError`` is 376 raised. 377 :param attributes: 378 Attribute mapping of ``s``. If none provided, this will attempt to infer 379 a corresponding mapping or raise a ``ValueError``. 380 :return: 381 Model deserialized from JSON array and the newly created document. Note that 382 references into other documents will not be resolved, users will need to 383 resolve them by calling ``link`` on the returned model. See also 384 :py:class:`IdMap <syside.IdMap>`. 385 """ 386 387 388@overload 389def loads( 390 s: Iterable[JsonSourceNew | JsonSourceInto], 391 /, 392 environment: Environment | None = None, 393 resolve: Callable[[str, UUID], syside.Element | None] | None = None, 394 attributes: syside.AttributeMap | None = None, 395) -> tuple[ 396 BaseModel, 397 list[tuple[syside.DeserializedModel, DeserializationReport]], 398]: 399 """ 400 Deserialize a project of multiple documents from ``s``. 401 402 This is effectively calling ``loads(src, document, attributes) for document, 403 src in s`` and performing the link step afterwards. See also other overloads 404 of ``loads``. 405 406 :param s: 407 Projects sources to deserialize from. If providing a URL string or a 408 ``Url``, new documents will be created for corresponding sources, 409 otherwise deserialization will be performed into the provided 410 ``Documents``. 411 :param environment: 412 ``Environment`` this project depends on. Defaults to the bundled 413 standard library. The ``environment`` will be used to attempt to resolve 414 missing references in the deserialized project. 415 :param resolve: 416 User-provided reference resolution callback that takes priority over 417 ``environment``. See :py:meth:`DeserializedModel.link 418 <syside.DeserializedModel.link>` for more details. 419 :param attributes: 420 Attribute mapping of ``s``. If none provided, this will attempt to infer 421 a corresponding mapping or raise a ``ValueError``. 422 :return: 423 A tuple of project deserialized from JSON sources, and deserialization 424 results 425 :raises ProjectDeserializationError: 426 If either the deserialization or the reference resolution had errors. 427 """ 428 429
[docs] 430def loads( 431 s: str | Iterable[JsonSourceNew | JsonSourceInto], 432 *args: typing.Any, 433 **kwargs: typing.Any, 434) -> ( 435 syside.DeserializedModel 436 | tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]] 437 | tuple[ 438 BaseModel, 439 list[tuple[syside.DeserializedModel, DeserializationReport]], 440 ] 441): 442 """loads implementation""" 443 if isinstance(s, str): 444 return _loads_document(s, *args, **kwargs) 445 return _loads_project(s, *args, **kwargs)