diff --git a/discos_client/client.py b/discos_client/client.py index fd781ec..d11523b 100644 --- a/discos_client/client.py +++ b/discos_client/client.py @@ -219,16 +219,34 @@ def __format__(self, spec: str) -> str: | 'i' - indented JSON \ with optional indentation level (default is 2) | 'f' - full representation with metadata + | 'm' - metadata only representation :return: A JSON formatted string. """ - fmt_spec = spec[1:] if spec.startswith("f") else spec - fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("f") else fmt_spec + has_f = "f" in spec + has_m = "m" in spec + + if has_f and has_m: + raise ValueError( + "Format specifier cannot contain both 'f' and 'm'." + ) + + if has_f: + fmt_spec = spec[1:] if spec.startswith("f") else spec + fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("f") else fmt_spec + elif has_m: + fmt_spec = spec[1:] if spec.startswith("m") else spec + fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("m") else fmt_spec + else: + fmt_spec = spec indent = None separators = None - default = DISCOSNamespace.__full_dict__ if fmt_spec != spec \ + default = ( + DISCOSNamespace.__full_dict__ if has_f + else DISCOSNamespace.__metadata_dict__ if has_m else DISCOSNamespace.__message_dict__ + ) if fmt_spec == "": pass diff --git a/discos_client/namespace.py b/discos_client/namespace.py index 5578601..c1a7c23 100644 --- a/discos_client/namespace.py +++ b/discos_client/namespace.py @@ -4,7 +4,8 @@ from copy import deepcopy from collections.abc import Iterable from typing import Any, Callable, Iterator -from .utils import delegated_operations, delegated_comparisons, public_dict +from .utils import delegated_operations, delegated_comparisons +from .utils import public_dict, META_KEYS __all__ = ["DISCOSNamespace"] @@ -23,12 +24,6 @@ class DISCOSNamespace: __typename__ = "DISCOSNamespace" __private__ = ( - "type", - "title", - "description", - "enum", - "unit", - "format", "_lock", "_observers", "_observers_lock", @@ -429,6 +424,7 @@ def __format__(self, spec: str) -> str: | 'i' - indented JSON \ with optional indentation level (default is 2) | 'f' - full representation with metadata + | 'm' - metadata only representation :return: A JSON formatted string. :raise ValueError: If the format specifier is unknown or malformed. @@ -437,13 +433,30 @@ def __format__(self, spec: str) -> str: with self._lock: return format(self._value, spec) - fmt_spec = spec[1:] if spec.startswith("f") else spec - fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("f") else fmt_spec + has_f = "f" in spec + has_m = "m" in spec + + if has_f and has_m: + raise ValueError( + "Format specifier cannot contain both 'f' and 'm'." + ) + + if has_f: + fmt_spec = spec[1:] if spec.startswith("f") else spec + fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("f") else fmt_spec + elif has_m: + fmt_spec = spec[1:] if spec.startswith("m") else spec + fmt_spec = fmt_spec[:-1] if fmt_spec.endswith("m") else fmt_spec + else: + fmt_spec = spec indent = None separators = None - default = \ - self.__full_dict__ if fmt_spec != spec else self.__message_dict__ + default = ( + self.__full_dict__ if has_f + else self.__metadata_dict__ if has_m + else self.__message_dict__ + ) if fmt_spec == "": pass @@ -555,7 +568,7 @@ def unwrap(value: Any) -> Any: return unwrap(cls.__get_value__(value)) retval = {} for k, v in vars(value).items(): - if k in cls.__private__: + if k in cls.__private__ or k in META_KEYS: continue retval[k] = unwrap(v) return retval @@ -564,6 +577,24 @@ def unwrap(value: Any) -> Any: return value return unwrap(obj) + @classmethod + def __metadata_dict__(cls, obj: DISCOSNamespace) -> dict[str, Any]: + """ + Return only the metadata dictionary, removing pure message values. + + :param obj: The object to convert. + :return: A dictionary containing only schema/metadata fields. + """ + def strip(value: Any) -> Any: + if isinstance(value, dict): + return { + k: strip(v) for k, v in value.items() if k != "value" + } + if isinstance(value, (list, tuple)): + return [strip(v) for v in value] + return value + return strip(public_dict(obj, cls.__is__, cls.__get_value__)) + @classmethod def __value_repr__(cls, obj: Any) -> Any: """ diff --git a/docs/schemas/example.json b/docs/schemas/example.json index f058d38..86ad288 100644 --- a/docs/schemas/example.json +++ b/docs/schemas/example.json @@ -20,11 +20,11 @@ }, "type": { "type": "string", - "description": "JSON Schema type of the value (e.g., number, integer, string)." + "description": "JSON Schema type of the value (e.g., number, boolean, string)." }, "format": { "type": "string", - "description": "Optional JSON Schema format annotation (e.g., int64, date-time)." + "description": "Optional JSON Schema format annotation (e.g., date-time)." }, "enum": { "type": "string", diff --git a/tests/test_client.py b/tests/test_client.py index 4cb71a8..f212e5e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -166,6 +166,18 @@ def test_format(self): str(ex.exception), "Unknown format code '.3f' for DISCOSClient" ) + with self.assertRaises(ValueError) as ex: + _ = f"{client:.3m}" + self.assertEqual( + str(ex.exception), + "Unknown format code '.3m' for DISCOSClient" + ) + with self.assertRaises(ValueError) as ex: + _ = f"{client:fm}" + self.assertEqual( + str(ex.exception), + "Format specifier cannot contain both 'f' and 'm'." + ) with self.assertRaises(ValueError) as ex: _ = f"{client:0i}" self.assertEqual( diff --git a/tests/test_namespace.py b/tests/test_namespace.py index ecc36c0..e20dd3f 100644 --- a/tests/test_namespace.py +++ b/tests/test_namespace.py @@ -167,7 +167,7 @@ def test_format(self): a = 1.234 ns = DISCOSNamespace(value=a) self.assertEqual(f"{ns:.3f}", f"{a:.3f}") - b = {"a": a, "enum": ["a", "b"]} + b = {"a": {"title": "a", "value": a}, "enum": ["a", "b"]} ns = DISCOSNamespace(**b) with self.assertRaises(ValueError) as ex: _ = f"{ns:.3f}" @@ -185,6 +185,12 @@ def test_format(self): str(ex.exception), "Unknown format code '3c' for DISCOSNamespace" ) + with self.assertRaises(ValueError) as ex: + _ = f"{ns:fm}" + self.assertEqual( + str(ex.exception), + "Format specifier cannot contain both 'f' and 'm'." + ) self.assertEqual( f"{ns:i}", json.dumps({"a": a}, indent=2) @@ -193,6 +199,12 @@ def test_format(self): f"{ns:f}", json.dumps(b) ) + b_ = deepcopy(b) + b_["a"].pop("value") + self.assertEqual( + f"{ns:m}", + json.dumps(b_) + ) for indent in range(1, 10): self.assertEqual( f"{ns:{indent}i}",