"""Provide common functions shared among the modules."""
# This code has been automatically generated by aas-core-codegen.
# Do NOT edit or append.
import collections.abc
import re
from typing import Any, NoReturn, Optional, Sequence
[docs]def assert_never(value: NoReturn) -> NoReturn:
"""
Signal to mypy to perform an exhaustive matching.
Please see the following page for more details:
https://hakibenita.com/python-mypy-exhaustive-checking
"""
raise AssertionError(f"Unhandled value: {value} ({type(value).__name__})")
[docs]def try_to_cast_to_array_like(value: Any) -> Optional[Sequence[Any]]:
"""
Try to cast :paramref:`value` to something like a JSON array.
A mapping and a set are refused, as neither is a JSON array, and so are
a ``str``, a ``bytes`` and a ``bytearray``, which Python counts among
the sequences but JSON does not.
Only a sequence is admitted, and never an arbitrary iterable. A one-shot
iterable such as a generator is spent by the first walk over it, so
a verification followed by a serialization would silently give an empty
array -- this function is the single place where all the modules agree on
what an array is, so the refusal has to hold for all of them.
>>> assert try_to_cast_to_array_like(True) is None
>>> assert try_to_cast_to_array_like(0) is None
>>> assert try_to_cast_to_array_like(2.2) is None
>>> assert try_to_cast_to_array_like("hello") is None
>>> assert try_to_cast_to_array_like(b"hello") is None
>>> try_to_cast_to_array_like([1, 2])
[1, 2]
>>> assert try_to_cast_to_array_like({"a": 3}) is None
>>> assert try_to_cast_to_array_like(collections.OrderedDict()) is None
>>> try_to_cast_to_array_like(range(1, 2))
range(1, 2)
>>> try_to_cast_to_array_like((1, 2))
(1, 2)
>>> assert try_to_cast_to_array_like({1, 2, 3}) is None
>>> assert try_to_cast_to_array_like(iter([1, 2])) is None
"""
# NOTE (mristin):
# A ``list`` is what :py:mod:`json` gives us, and ``isinstance`` against
# a concrete class costs a fraction of ``isinstance`` against the abstract
# :py:class:`collections.abc.Sequence`, so we shortcut it here.
if isinstance(value, list):
return value
if isinstance(value, (str, bytes, bytearray)):
return None
if isinstance(value, collections.abc.Sequence):
return value
return None
#: Match an optional sign followed by the ASCII digits.
#:
#: Mind the explicit ``[0-9]``: ``\d`` would match a digit of any script.
_SAFE_INT_RE = re.compile(r"[-+]?[0-9]+")
[docs]def parse_safe_int(text: str) -> int:
"""
Parse :paramref:`text` as a safe integer.
The meta-model calls ``int`` on strings, and this is its transpilation.
We are stricter than the built-in :py:func:`int` so that all the SDKs behave
the same. We accept only an optional sign followed by the ASCII digits, and
only the safe integers, *i.e.*, the integers within ``-(2 ** 53 - 1)`` and
``2 ** 53 - 1``, which a double-precision floating-point number represents
exactly.
:param text: to be parsed
:return: parsed integer
:raise: :py:class:`ValueError` if :paramref:`text` is not a safe integer
>>> parse_safe_int("-0042")
-42
"""
# NOTE (mristin):
# Mind that it is checked with ``fullmatch`` and not with ``match``: ``$``
# would also match just before a trailing newline.
if _SAFE_INT_RE.fullmatch(text) is None:
raise ValueError(
f"Expected an optional sign followed by the ASCII digits, "
f"but got: {text!r}"
)
# NOTE (mristin):
# We count the significant digits before we call ``int``, as the conversion
# is quadratic in the length of the text, and Python refuses a text of more
# than 4300 digits, leading zeros included.
digits = text.lstrip("+-").lstrip("0")
if len(digits) > 16:
raise ValueError(
f"Expected a safe integer, but got a text out of its range: {text!r}"
)
value = int(digits) if len(digits) > 0 else 0
# NOTE (mristin):
# This is 2 ** 53 - 1.
if value > 9007199254740991:
raise ValueError(
f"Expected a safe integer, but got a text out of its range: {text!r}"
)
return -value if text[0] == "-" else value
[docs]def try_to_convert_int_to_float(value: int) -> Optional[float]:
"""
Try to convert :paramref:`value` to the ``float`` which JSON prescribes.
JSON knows a single numeric type, so an ``int`` has to become a ``float``
on its way into and out of a JSON-able value. The conversion has to be
exact: a ``float`` which does not convert back to :paramref:`value` would
quietly say a different number than the one which was given, so such
a value is refused instead of rounded.
Mind that a ``bool`` is an ``int`` in Python. The callers all sort out
the booleans before they come here.
>>> try_to_convert_int_to_float(1)
1.0
>>> try_to_convert_int_to_float(-3)
-3.0
>>> assert try_to_convert_int_to_float(2 ** 53 + 1) is None
>>> assert try_to_convert_int_to_float(10 ** 400) is None
"""
try:
result = float(value)
except OverflowError:
# NOTE (mristin):
# A Python ``int`` is unbounded, so one can be far too large for
# a ``float`` to hold at all.
return None
if int(result) != value:
return None
return result
# This code has been automatically generated by aas-core-codegen.
# Do NOT edit or append.