diff --git a/lib/yaml/constructor.py b/lib/yaml/constructor.py index 619acd30..14d31f75 100644 --- a/lib/yaml/constructor.py +++ b/lib/yaml/constructor.py @@ -416,6 +416,7 @@ def construct_yaml_map(self, node): def construct_yaml_object(self, node, cls): data = cls.__new__(cls) yield data + # FIXME: __getstate__/__setstate__ support non-mapping state if hasattr(data, '__setstate__'): state = self.construct_mapping(node, deep=True) data.__setstate__(state) diff --git a/lib/yaml/representer.py b/lib/yaml/representer.py index 808ca06d..782e2f0f 100644 --- a/lib/yaml/representer.py +++ b/lib/yaml/representer.py @@ -224,7 +224,19 @@ def represent_yaml_object(self, tag, data, cls, flow_style=None): if hasattr(data, '__getstate__'): state = data.__getstate__() else: + # FIXME: this isn't always possible (eg, __slots__) state = data.__dict__.copy() + + # fix for https://github.com/yaml/pyyaml/issues/692 + # __getstate__() has always supported non-mapping state blobs, and as of Python 3.11 returns None by default for + # objects with no instance attrs. Ideally, we'd attempt to serialize and round-trip a null scalar for full + # fidelity with the types supported by __getstate__/__setstate__, but we can't be guaranteed that it'll be + # interpreted as such. For compatibility with existing behavior on older Pythons, we'll continue to represent + # this case as an empty mapping. + # (see https://github.com/python/cpython/issues/70766 for a specific case) + if state is None: + state = {} + return self.represent_mapping(tag, state, flow_style=flow_style) def represent_undefined(self, data): diff --git a/tests/data/construct-python-object.code b/tests/data/construct-python-object.code index 9e611e43..4ffd44a2 100644 --- a/tests/data/construct-python-object.code +++ b/tests/data/construct-python-object.code @@ -1,4 +1,5 @@ [ +EmptyObject(), AnObject(1, 'two', [3,3,3]), AnInstance(1, 'two', [3,3,3]), diff --git a/tests/data/construct-python-object.data b/tests/data/construct-python-object.data index 66797e4c..49c03922 100644 --- a/tests/data/construct-python-object.data +++ b/tests/data/construct-python-object.data @@ -1,3 +1,4 @@ +- !emptyobj {} - !!python/object:test_constructor.AnObject { foo: 1, bar: two, baz: [3,3,3] } - !!python/object:test_constructor.AnInstance { foo: 1, bar: two, baz: [3,3,3] } diff --git a/tests/lib/test_constructor.py b/tests/lib/test_constructor.py index 0783a21b..3dc724ec 100644 --- a/tests/lib/test_constructor.py +++ b/tests/lib/test_constructor.py @@ -15,7 +15,7 @@ def execute(code): def _make_objects(): global MyLoader, MyDumper, MyTestClass1, MyTestClass2, MyTestClass3, YAMLObject1, YAMLObject2, \ - AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState, \ + EmptyObject, AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState, \ NewArgs, NewArgsWithState, Reduce, ReduceWithState, Slots, MyInt, MyList, MyDict, \ FixedOffset, today, execute, MyFullLoader @@ -128,6 +128,20 @@ def __eq__(self, other): return type(self) is type(other) and \ (self.foo, self.bar, self.baz) == (other.foo, other.bar, other.baz) + # Python 3.11+ implements __getstate__() returning `None` on objects with no attrs https://github.com/python/cpython/issues/70766 + class EmptyObject(yaml.YAMLObject): + yaml_tag = '!emptyobj' + yaml_loader = MyLoader + yaml_dumper = MyDumper + + def __eq__(self, other): + return type(other) is EmptyObject + + # simulate Python 3.11 behavior with a generated __getstate__ returning None on objects with no attrs + def __getstate__(self): + return None + + class AnInstance: def __init__(self, foo=None, bar=None, baz=None): self.foo = foo