Skip to content

Commit

Permalink
WIP nested class support
Browse files Browse the repository at this point in the history
* could be done with existing "all dots" tag syntax instead of the `@` separator, but discarding the explicit distinction between the module/package and type name makes the import/construction logic a lot less precise when dealing with nested types.
  • Loading branch information
nitzmahone committed Nov 27, 2023
1 parent 48838a3 commit da99b81
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 22 deletions.
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

0 comments on commit da99b81

Please sign in to comment.