diff --git a/CHANGES.rst b/CHANGES.rst index 1676bbd..89c3e2d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ 3.0.1 (unreleased) ================== -- Nothing changed yet. +- Document the ``to_json_representation`` variants and add one + that guarantees sorted keys. Make the "fast" variant not dependent + on second-chance externalization. 3.0.0 (2026-05-07) diff --git a/docs/basics.rst b/docs/basics.rst index b5f5af2..f3f1e7d 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -556,16 +556,18 @@ keyword arguments to change that: >>> as_bytes = to_external_representation(address, EXT_REPR_JSON, sort_keys=False, as_str=False) >>> assert isinstance(as_bytes, bytes) -There are also some convenience functions, but note that these do not -use the registered utility, they directly invoke the default utility: +There are also some convenience functions. The "fast" and "sorted" +variants bypass utility lookup and directly use the default ``JsonRepresenter``. >>> from nti.externalization import to_json_representation >>> from nti.externalization import to_json_representation_fast + >>> from nti.externalization import to_json_representation_sorted >>> to_json_representation(address) '{"Class":"Address",... >>> as_bytes = to_json_representation_fast(address) >>> assert isinstance(as_bytes, bytes) - + >>> to_json_representation_sorted(address) + '{"Class":"Address","city":"Cupertino",... Loading from a string doesn't have a shortcut, we need to use the utility: diff --git a/src/nti/externalization/__init__.py b/src/nti/externalization/__init__.py index cbc5bb9..3571f4f 100644 --- a/src/nti/externalization/__init__.py +++ b/src/nti/externalization/__init__.py @@ -10,6 +10,7 @@ 'update_from_external_object', 'to_json_representation_fast', 'to_json_representation', + 'to_json_representation_sorted', ] from nti.externalization.externalization import to_external_object @@ -17,6 +18,7 @@ from nti.externalization.representation import to_external_representation from nti.externalization.representation import to_json_representation_fast +from nti.externalization.representation import to_json_representation_sorted from nti.externalization.representation import to_json_representation from nti.externalization.internalization import new_from_external_object from nti.externalization.internalization import update_from_external_object diff --git a/src/nti/externalization/representation.py b/src/nti/externalization/representation.py index 4ee476d..de77958 100644 --- a/src/nti/externalization/representation.py +++ b/src/nti/externalization/representation.py @@ -35,17 +35,26 @@ class POSError(Exception): __all__ = [ 'to_external_representation', 'to_json_representation', + 'to_json_representation_fast', + 'to_json_representation_sorted', 'WithRepr', + 'JsonRepresenter', + 'YamlRepresenter', ] # Driver functions +def _to_external_representation(obj, io, name=_NotGiven, + **repr_kwargs) -> str|bytes: + + ext = toExternalObject(obj, name=name) + return io.dump(ext, **repr_kwargs) def to_external_representation(obj, ext_format=EXT_REPR_JSON, name=_NotGiven, registry=_NotGiven, - **repr_kwargs): + **repr_kwargs) -> str|bytes: """ - to_external_representation(obj, ext_format='json', name=NotGiven) -> str + to_external_representation(obj, ext_format='json', name=NotGiven, **repr_kwargs) -> str|bytes Transforms (and returns) the *obj* into its external (string) representation. @@ -73,20 +82,52 @@ def to_external_representation(obj, ext_format=EXT_REPR_JSON, # the externalization process itself, but we would wind up traversing # parts of the datastructure more than necessary. Here we traverse # the whole thing exactly twice. - ext = toExternalObject(obj, name=name) - return component.getUtility( + io = component.getUtility( IExternalObjectRepresenter, name=ext_format - ).dump(ext, **repr_kwargs) + ) + + return _to_external_representation(obj, io, name, **repr_kwargs) -def to_json_representation(obj): +def to_json_representation(obj) -> str: """ A convenience function that calls :func:`to_external_representation` with `.EXT_REPR_JSON`. """ return to_external_representation(obj, EXT_REPR_JSON) +def to_json_representation_fast(obj) -> bytes: + """ + A convenience function that calls + :func:`to_external_representation` with `.EXT_REPR_JSON` + and additional parameters to optimize for speed. + + Note that this bypasses utility lookup and directly + uses :class:`JsonRepresenter` + + .. versionadded:: 3.0.0 + .. versionchanged:: NEXT + Now properly externalizes the object instead of relying on + the second-chance externalization mechanism. + """ + return _to_external_representation(obj, JsonRepresenter, + sort_keys=False, as_str=False) + +def to_json_representation_sorted(obj) -> str: + """ + Like `to_json_representation`, but guarantees that + the keys are sorted. This is slower, but may be + helpful in tests that do string comparisons. + + Note that this bypasses utility lookup and directly + uses :class:`JsonRepresenter` + + .. versionadded:: NEXT + """ + return _to_external_representation(obj, JsonRepresenter, + sort_keys=True) + # JSON @@ -110,10 +151,15 @@ def _second_pass_to_external_object(obj): @interface.named(EXT_REPR_JSON) @interface.implementer(IExternalObjectIO) class JsonRepresenter(object): + """ + Default IO object using ``orjson`` for JSON input/output. + """ @staticmethod - def dump(obj, fp=None, sort_keys=False, as_str=True, **_unused): + def dump(obj, fp=None, sort_keys=False, as_str=True, **_unused) -> str|bytes: """ + dump(obj, fp=None, sort_keys=False, as_str=True) -> str|bytes + Given an object that is known to already be in an externalized form, convert it to JSON. This can be about 10% faster then requiring a pass across all the sub-objects of the object to check that they are in external @@ -127,6 +173,8 @@ def dump(obj, fp=None, sort_keys=False, as_str=True, **_unused): If set to false, then a bytes object will be returned (and written to any *fp*). Bytes is orjson's native output format. + Other keyword arguments are ignored. + """ result = orjson.dumps(obj, option=orjson.OPT_SORT_KEYS if sort_keys else 0, @@ -137,14 +185,15 @@ def dump(obj, fp=None, sort_keys=False, as_str=True, **_unused): return fp.write(result) return result - @classmethod - def dump_fast(cls, obj): - return cls.dump(obj, sort_keys=False, as_str=False) - def load(self, stream): return orjson.loads(stream) + +# This is meant for dumping already externalized objects, but +# because of the second_pass_to_external_object default, +# it will actually dump any dumpable object by first externalizing +# it. Try not to rely on that. to_json_representation_externalized = JsonRepresenter.dump -to_json_representation_fast = JsonRepresenter.dump_fast + # YAML @@ -211,8 +260,17 @@ def construct_yaml_str(self, node): @interface.named(EXT_REPR_YAML) @interface.implementer(IExternalObjectIO) class YamlRepresenter(object): + """ + Default IO object using ``yaml`` for object input/output. + """ + + @staticmethod + def dump(obj, fp=None, **_unused) -> str: + """ + dump(obj, fp=None) -> str - def dump(self, obj, fp=None, **_unused): + Other keyword arguments are ignored. + """ # The default_flow_style changed in PyYaml 5.1 from None to False. # Using False produces multi-line, indented, verbose output. While being human readable, # this consumes space and eliminates simple parsing with JSON. Using True @@ -221,7 +279,8 @@ def dump(self, obj, fp=None, **_unused): # https://github.com/yaml/pyyaml/issues/199 return yaml.dump(obj, stream=fp, Dumper=_ExtDumper, default_flow_style=True) - def load(self, stream): + @staticmethod + def load(stream): return yaml.load(stream, Loader=_UnicodeLoader)