diff --git a/lib/ansible/playbook/attribute.py b/lib/ansible/playbook/attribute.py index 9b4e6f26db..3f55936ff6 100644 --- a/lib/ansible/playbook/attribute.py +++ b/lib/ansible/playbook/attribute.py @@ -23,7 +23,7 @@ from copy import deepcopy class Attribute: - def __init__(self, isa=None, private=False, default=None, required=False, listof=None, priority=0, class_type=None, always_post_validate=False): + def __init__(self, isa=None, private=False, default=None, required=False, listof=None, priority=0, class_type=None, always_post_validate=False, inherit=True): self.isa = isa self.private = private @@ -33,6 +33,7 @@ class Attribute: self.priority = priority self.class_type = class_type self.always_post_validate = always_post_validate + self.inherit = inherit if default is not None and self.isa in ('list', 'dict', 'set'): self.default = deepcopy(default) diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index e35cd904e7..2447a72824 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -47,19 +47,28 @@ except ImportError: def _generic_g(prop_name, self): + try: + return self._attributes[prop_name] + except KeyError: + raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + +def _generic_g_method(prop_name, self): method = "_get_attr_%s" % prop_name try: - value = getattr(self, method)() - except AttributeError: - try: - value = self._attributes[prop_name] - if value is None and not self._finalized: - try: - value = self._get_parent_attribute(prop_name) - except AttributeError: - pass - except KeyError: - raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + return getattr(self, method)() + except KeyError: + raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + +def _generic_g_parent(prop_name, self): + try: + value = self._attributes[prop_name] + if value is None and not self._finalized: + try: + value = self._get_parent_attribute(prop_name) + except AttributeError: + pass + except KeyError: + raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) return value @@ -71,8 +80,19 @@ def _generic_d(prop_name, self): class BaseMeta(type): + """ + Metaclass for the Base object, which is used to construct the class + attributes based on the FieldAttributes available. + """ + def __new__(cls, name, parents, dct): def _create_attrs(src_dict, dst_dict): + ''' + Helper method which creates the attributes based on those in the + source dictionary of attributes. This also populates the other + attributes used to keep track of these attributes and via the + getter/setter/deleter methods. + ''' keys = list(src_dict.keys()) for attr_name in keys: value = src_dict[attr_name] @@ -80,8 +100,19 @@ class BaseMeta(type): if attr_name.startswith('_'): attr_name = attr_name[1:] - getter = partial(_generic_g, attr_name) - setter = partial(_generic_s, attr_name) + # here we selectively assign the getter based on a few + # things, such as whether we have a _get_attr_ + # method, or if the attribute is marked as not inheriting + # its value from a parent object + method = "_get_attr_%s" % attr_name + if method in src_dict or method in dst_dict: + getter = partial(_generic_g_method, attr_name) + elif ('_get_parent_attribute' in src_dict or '_get_parent_attribute' in dst_dict) and value.inherit: + getter = partial(_generic_g_parent, attr_name) + else: + getter = partial(_generic_g, attr_name) + + setter = partial(_generic_s, attr_name) deleter = partial(_generic_d, attr_name) dst_dict[attr_name] = property(getter, setter, deleter) @@ -89,14 +120,21 @@ class BaseMeta(type): dst_dict['_attributes'][attr_name] = value.default def _process_parents(parents, dst_dict): + ''' + Helper method which creates attributes from all parent objects + recursively on through grandparent objects + ''' for parent in parents: if hasattr(parent, '__dict__'): _create_attrs(parent.__dict__, dst_dict) _process_parents(parent.__bases__, dst_dict) + # create some additional class attributes dct['_attributes'] = dict() dct['_valid_attrs'] = dict() + # now create the attributes based on the FieldAttributes + # available, including from parent (and grandparent) objects _create_attrs(dct, dct) _process_parents(parents, dct) @@ -140,10 +178,10 @@ class Base(with_metaclass(BaseMeta, object)): # every object gets a random uuid: self._uuid = uuid.uuid4() - # initialize the default field attribute values - #self._attributes = dict() - #for (name, attr) in iteritems(self._valid_attrs): - # self._attributes[name] = attr.default + # we create a copy of the attributes here due to the fact that + # it was intialized as a class param in the meta class, so we + # need a unique object here (all members contained within are + # unique already). self._attributes = self._attributes.copy() # and init vars, avoid using defaults in field declaration as it lives across plays diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py index 9b6fd94a3d..cde41172f6 100644 --- a/lib/ansible/playbook/block.py +++ b/lib/ansible/playbook/block.py @@ -30,9 +30,9 @@ from ansible.playbook.taggable import Taggable class Block(Base, Become, Conditional, Taggable): - _block = FieldAttribute(isa='list', default=[]) - _rescue = FieldAttribute(isa='list', default=[]) - _always = FieldAttribute(isa='list', default=[]) + _block = FieldAttribute(isa='list', default=[], inherit=False) + _rescue = FieldAttribute(isa='list', default=[], inherit=False) + _always = FieldAttribute(isa='list', default=[], inherit=False) _delegate_to = FieldAttribute(isa='list') _delegate_facts = FieldAttribute(isa='bool', default=False) _any_errors_fatal = FieldAttribute(isa='bool')