Skip to content

Commit

Permalink
fix dumping of empty YAMLObject on Python 3.11+
Browse files Browse the repository at this point in the history
* translate __getstate__ returning `None` to empty mapping (arguably wrong, but backward-compatibie)
* adds constructor/representer tests to exercise this case
  • Loading branch information
nitzmahone committed Aug 29, 2023
1 parent 957ae4d commit 3789d31
Show file tree
Hide file tree
Showing 5 changed files with 30 additions and 1 deletion.
1 change: 1 addition & 0 deletions lib/yaml/constructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions lib/yaml/representer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions tests/data/construct-python-object.code
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
EmptyObject(),
AnObject(1, 'two', [3,3,3]),
AnInstance(1, 'two', [3,3,3]),

Expand Down
1 change: 1 addition & 0 deletions tests/data/construct-python-object.data
Original file line number Diff line number Diff line change
@@ -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] }

Expand Down
16 changes: 15 additions & 1 deletion tests/lib/test_constructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 3789d31

Please sign in to comment.