Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] nested class support #767

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions lib/yaml/constructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,9 @@ def find_python_name(self, name, mark, unsafe=False):
if not name:
raise ConstructorError("while constructing a Python object", mark,
"expected non-empty name appended to the tag", mark)
if '.' in name:
if '@' in name: # handles nested objects via __qualname__
module_name, object_name = name.rsplit('@', 1)
elif '.' in name: # handle old-style references
module_name, object_name = name.rsplit('.', 1)
else:
module_name = 'builtins'
Expand All @@ -556,11 +558,16 @@ def find_python_name(self, name, mark, unsafe=False):
raise ConstructorError("while constructing a Python object", mark,
"module %r is not imported" % module_name, mark)
module = sys.modules[module_name]
if not hasattr(module, object_name):

# descend multi-part object_name to support nested classes
cur_obj = module
for attr in object_name.split('.'):
cur_obj = getattr(cur_obj, attr, None)
if not cur_obj:
raise ConstructorError("while constructing a Python object", mark,
"cannot find %r in the module %r"
% (object_name, module.__name__), mark)
return getattr(module, object_name)
% (object_name, module_name), mark)
return cur_obj

def construct_python_name(self, suffix, node):
value = self.construct_scalar(node)
Expand Down
42 changes: 25 additions & 17 deletions lib/yaml/representer.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,24 +336,32 @@ def represent_object(self, data):
else:
tag = 'tag:yaml.org,2002:python/object/apply:'
newobj = False
function_name = '%s.%s' % (function.__module__, function.__name__)
if not args and not listitems and not dictitems \
and isinstance(state, dict) and newobj:
return self.represent_mapping(
'tag:yaml.org,2002:python/object:'+function_name, state)
if not listitems and not dictitems \
and isinstance(state, dict) and not state:
return self.represent_sequence(tag+function_name, args)

value = {}
if args:
value['args'] = args
if state or not isinstance(state, dict):
value['state'] = state
if listitems:
value['listitems'] = listitems
if dictitems:
value['dictitems'] = dictitems
return self.represent_mapping(tag+function_name, value)

represent_impl = self.represent_mapping

if not args and not listitems and not dictitems and isinstance(state, dict) and newobj:
# object supports simple object state w/ __newobj__
tag = 'tag:yaml.org,2002:python/object:'
value = state
elif not listitems and not dictitems and isinstance(state, dict) and not state:
value = args
represent_impl = self.represent_sequence
else:
if args:
value['args'] = args
if state or not isinstance(state, dict):
value['state'] = state
if listitems:
value['listitems'] = listitems
if dictitems:
value['dictitems'] = dictitems

type_qualname = getattr(function, '__qualname__', getattr(function, '__name__', None))
type_separator = '@' if '.' in type_qualname else '.' # if nested class, use @ in tag to disambiguate module name and object qualname
tag = f'{tag}{function.__module__}{type_separator}{type_qualname}'
return represent_impl(tag, value)

def represent_ordered_dict(self, data):
# Provide uniform representation across different Python versions.
Expand Down
2 changes: 2 additions & 0 deletions tests/legacy_tests/data/construct-python-object.code
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
AnObject(1, 'two', [3,3,3]),
AnInstance(1, 'two', [3,3,3]),

NestedOuterObject.NestedInnerObject1.NestedInnerObject2.NestedInnerObject3('hi mom'),

AnObject(1, 'two', [3,3,3]),
AnInstance(1, 'two', [3,3,3]),

Expand Down
1 change: 1 addition & 0 deletions tests/legacy_tests/data/construct-python-object.data
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- !!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] }
- !!python/object:test_constructor@NestedOuterObject.NestedInnerObject1.NestedInnerObject2.NestedInnerObject3 { data: hi mom }

- !!python/object/new:test_constructor.AnObject { args: [1, two], kwds: {baz: [3,3,3]} }
- !!python/object/apply:test_constructor.AnInstance { args: [1, two], kwds: {baz: [3,3,3]} }
Expand Down
12 changes: 11 additions & 1 deletion tests/legacy_tests/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, \
AnObject, AnInstance, NestedOuterObject, AState, ACustomState, InitArgs, InitArgsWithState, \
NewArgs, NewArgsWithState, Reduce, ReduceWithState, Slots, MyInt, MyList, MyDict, \
FixedOffset, today, execute, MyFullLoader

Expand Down Expand Up @@ -128,6 +128,16 @@ def __eq__(self, other):
return type(self) is type(other) and \
(self.foo, self.bar, self.baz) == (other.foo, other.bar, other.baz)

class NestedOuterObject:
class NestedInnerObject1:
class NestedInnerObject2:
class NestedInnerObject3:
def __init__(self, data):
self.data = data
def __eq__(self, other):
return type(self) is type(other) and self.data == other.data


class AnInstance:
def __init__(self, foo=None, bar=None, baz=None):
self.foo = foo
Expand Down