Meta meta meta
This commit is contained in:
parent
33245b2011
commit
3a51587220
5 changed files with 111 additions and 159 deletions
|
@ -24,10 +24,11 @@ import itertools
|
|||
import operator
|
||||
import uuid
|
||||
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from inspect import getmembers
|
||||
|
||||
from ansible.compat.six import iteritems, string_types
|
||||
from ansible.compat.six import iteritems, string_types, with_metaclass
|
||||
|
||||
from jinja2.exceptions import UndefinedError
|
||||
|
||||
|
@ -44,10 +45,64 @@ except ImportError:
|
|||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
BASE_ATTRIBUTES = {}
|
||||
|
||||
def _generic_g(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))
|
||||
|
||||
class Base:
|
||||
return value
|
||||
|
||||
def _generic_s(prop_name, self, value):
|
||||
self._attributes[prop_name] = value
|
||||
|
||||
def _generic_d(prop_name, self):
|
||||
del self._attributes[prop_name]
|
||||
|
||||
class BaseMeta(type):
|
||||
|
||||
def __new__(cls, name, parents, dct):
|
||||
def _create_attrs(src_dict, dst_dict):
|
||||
keys = list(src_dict.keys())
|
||||
for attr_name in keys:
|
||||
value = src_dict[attr_name]
|
||||
if isinstance(value, Attribute):
|
||||
if attr_name.startswith('_'):
|
||||
attr_name = attr_name[1:]
|
||||
|
||||
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)
|
||||
dst_dict['_valid_attrs'][attr_name] = value
|
||||
dst_dict['_attributes'][attr_name] = value.default
|
||||
|
||||
def _process_parents(parents, dst_dict):
|
||||
for parent in parents:
|
||||
if hasattr(parent, '__dict__'):
|
||||
_create_attrs(parent.__dict__, dst_dict)
|
||||
_process_parents(parent.__bases__, dst_dict)
|
||||
|
||||
dct['_attributes'] = dict()
|
||||
dct['_valid_attrs'] = dict()
|
||||
|
||||
_create_attrs(dct, dct)
|
||||
_process_parents(parents, dct)
|
||||
|
||||
return super(BaseMeta, cls).__new__(cls, name, parents, dct)
|
||||
|
||||
class Base(with_metaclass(BaseMeta, object)):
|
||||
|
||||
# connection/transport
|
||||
_connection = FieldAttribute(isa='string')
|
||||
|
@ -85,85 +140,15 @@ class Base:
|
|||
# every object gets a random uuid:
|
||||
self._uuid = uuid.uuid4()
|
||||
|
||||
# and initialize the base attributes
|
||||
self._initialize_base_attributes()
|
||||
|
||||
self._cached_parent_attrs = dict()
|
||||
# initialize the default field attribute values
|
||||
#self._attributes = dict()
|
||||
#for (name, attr) in iteritems(self._valid_attrs):
|
||||
# self._attributes[name] = attr.default
|
||||
self._attributes = self._attributes.copy()
|
||||
|
||||
# and init vars, avoid using defaults in field declaration as it lives across plays
|
||||
self.vars = dict()
|
||||
|
||||
|
||||
# The following three functions are used to programatically define data
|
||||
# descriptors (aka properties) for the Attributes of all of the playbook
|
||||
# objects (tasks, blocks, plays, etc).
|
||||
#
|
||||
# The function signature is a little strange because of how we define
|
||||
# them. We use partial to give each method the name of the Attribute that
|
||||
# it is for. Since partial prefills the positional arguments at the
|
||||
# beginning of the function we end up with the first positional argument
|
||||
# being allocated to the name instead of to the class instance (self) as
|
||||
# normal. To deal with that we make the property name field the first
|
||||
# positional argument and self the second arg.
|
||||
#
|
||||
# Because these methods are defined inside of the class, they get bound to
|
||||
# the instance when the object is created. After we run partial on them
|
||||
# and put the result back into the class as a property, they get bound
|
||||
# a second time. This leads to self being placed in the arguments twice.
|
||||
# To work around that, we mark the functions as @staticmethod so that the
|
||||
# first binding to the instance doesn't happen.
|
||||
|
||||
@staticmethod
|
||||
def _generic_g(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:
|
||||
if prop_name in self._cached_parent_attrs:
|
||||
value = self._cached_parent_attrs[prop_name]
|
||||
else:
|
||||
value = self._get_parent_attribute(prop_name)
|
||||
# FIXME: temporarily disabling due to bugs
|
||||
#self._cached_parent_attrs[prop_name] = value
|
||||
except AttributeError:
|
||||
pass
|
||||
except KeyError:
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name))
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _generic_s(prop_name, self, value):
|
||||
self._attributes[prop_name] = value
|
||||
|
||||
@staticmethod
|
||||
def _generic_d(prop_name, self):
|
||||
del self._attributes[prop_name]
|
||||
|
||||
def _get_base_attributes(self):
|
||||
'''
|
||||
Returns the list of attributes for this class (or any subclass thereof).
|
||||
If the attribute name starts with an underscore, it is removed
|
||||
'''
|
||||
|
||||
# check cache before retrieving attributes
|
||||
if self.__class__.__name__ in BASE_ATTRIBUTES:
|
||||
return BASE_ATTRIBUTES[self.__class__.__name__]
|
||||
|
||||
# Cache init
|
||||
base_attributes = dict()
|
||||
for (name, value) in getmembers(self.__class__):
|
||||
if isinstance(value, Attribute):
|
||||
if name.startswith('_'):
|
||||
name = name[1:]
|
||||
base_attributes[name] = value
|
||||
BASE_ATTRIBUTES[self.__class__.__name__] = base_attributes
|
||||
return base_attributes
|
||||
|
||||
def dump_me(self, depth=0):
|
||||
if depth == 0:
|
||||
print("DUMPING OBJECT ------------------------------------------------------")
|
||||
|
@ -178,23 +163,6 @@ class Base:
|
|||
if hasattr(self, '_play') and self._play:
|
||||
self._play.dump_me(depth+2)
|
||||
|
||||
def _initialize_base_attributes(self):
|
||||
# each class knows attributes set upon it, see Task.py for example
|
||||
self._attributes = dict()
|
||||
|
||||
for (name, value) in self._get_base_attributes().items():
|
||||
getter = partial(self._generic_g, name)
|
||||
setter = partial(self._generic_s, name)
|
||||
deleter = partial(self._generic_d, name)
|
||||
|
||||
# Place the property into the class so that cls.name is the
|
||||
# property functions.
|
||||
setattr(Base, name, property(getter, setter, deleter))
|
||||
|
||||
# Place the value into the instance so that the property can
|
||||
# process and hold that value.
|
||||
setattr(self, name, value.default)
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
''' infrequently used method to do some pre-processing of legacy terms '''
|
||||
|
||||
|
@ -230,8 +198,7 @@ class Base:
|
|||
|
||||
# Walk all attributes in the class. We sort them based on their priority
|
||||
# so that certain fields can be loaded before others, if they are dependent.
|
||||
base_attributes = self._get_base_attributes()
|
||||
for name, attr in sorted(base_attributes.items(), key=operator.itemgetter(1)):
|
||||
for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)):
|
||||
# copy the value over unless a _load_field method is defined
|
||||
if name in ds:
|
||||
method = getattr(self, '_load_%s' % name, None)
|
||||
|
@ -264,7 +231,7 @@ class Base:
|
|||
not map to attributes for this object.
|
||||
'''
|
||||
|
||||
valid_attrs = frozenset(name for name in self._get_base_attributes())
|
||||
valid_attrs = frozenset(self._valid_attrs.keys())
|
||||
for key in ds:
|
||||
if key not in valid_attrs:
|
||||
raise AnsibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds)
|
||||
|
@ -274,7 +241,7 @@ class Base:
|
|||
|
||||
if not self._validated:
|
||||
# walk all fields in the object
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
for (name, attribute) in iteritems(self._valid_attrs):
|
||||
|
||||
# run validator only if present
|
||||
method = getattr(self, '_validate_%s' % name, None)
|
||||
|
@ -299,7 +266,7 @@ class Base:
|
|||
|
||||
new_me = self.__class__()
|
||||
|
||||
for name in self._get_base_attributes():
|
||||
for name in self._valid_attrs.keys():
|
||||
attr_val = getattr(self, name)
|
||||
if isinstance(attr_val, collections.Sequence):
|
||||
setattr(new_me, name, attr_val[:])
|
||||
|
@ -330,7 +297,7 @@ class Base:
|
|||
# save the omit value for later checking
|
||||
omit_value = templar._available_variables.get('omit')
|
||||
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
for (name, attribute) in iteritems(self._valid_attrs):
|
||||
|
||||
if getattr(self, name) is None:
|
||||
if not attribute.required:
|
||||
|
@ -432,44 +399,6 @@ class Base:
|
|||
|
||||
self._finalized = True
|
||||
|
||||
def serialize(self):
|
||||
'''
|
||||
Serializes the object derived from the base object into
|
||||
a dictionary of values. This only serializes the field
|
||||
attributes for the object, so this may need to be overridden
|
||||
for any classes which wish to add additional items not stored
|
||||
as field attributes.
|
||||
'''
|
||||
|
||||
repr = dict()
|
||||
|
||||
for name in self._get_base_attributes():
|
||||
repr[name] = getattr(self, name)
|
||||
|
||||
# serialize the uuid field
|
||||
repr['uuid'] = getattr(self, '_uuid')
|
||||
|
||||
return repr
|
||||
|
||||
def deserialize(self, data):
|
||||
'''
|
||||
Given a dictionary of values, load up the field attributes for
|
||||
this object. As with serialize(), if there are any non-field
|
||||
attribute data members, this method will need to be overridden
|
||||
and extended.
|
||||
'''
|
||||
|
||||
assert isinstance(data, dict)
|
||||
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
if name in data:
|
||||
setattr(self, name, data[name])
|
||||
else:
|
||||
setattr(self, name, attribute.default)
|
||||
|
||||
# restore the UUID field
|
||||
setattr(self, '_uuid', data.get('uuid'))
|
||||
|
||||
def _load_vars(self, attr, ds):
|
||||
'''
|
||||
Vars in a play can be specified either as a dictionary directly, or
|
||||
|
@ -515,12 +444,43 @@ class Base:
|
|||
if not isinstance(new_value, list):
|
||||
new_value = [ new_value ]
|
||||
|
||||
#return list(set(value + new_value))
|
||||
return [i for i,_ in itertools.groupby(value + new_value) if i is not None]
|
||||
|
||||
def __getstate__(self):
|
||||
return self.serialize()
|
||||
def serialize(self):
|
||||
'''
|
||||
Serializes the object derived from the base object into
|
||||
a dictionary of values. This only serializes the field
|
||||
attributes for the object, so this may need to be overridden
|
||||
for any classes which wish to add additional items not stored
|
||||
as field attributes.
|
||||
'''
|
||||
|
||||
repr = dict()
|
||||
|
||||
for name in self._valid_attrs.keys():
|
||||
repr[name] = getattr(self, name)
|
||||
|
||||
# serialize the uuid field
|
||||
repr['uuid'] = getattr(self, '_uuid')
|
||||
|
||||
return repr
|
||||
|
||||
def deserialize(self, data):
|
||||
'''
|
||||
Given a dictionary of values, load up the field attributes for
|
||||
this object. As with serialize(), if there are any non-field
|
||||
attribute data members, this method will need to be overridden
|
||||
and extended.
|
||||
'''
|
||||
|
||||
assert isinstance(data, dict)
|
||||
|
||||
for (name, attribute) in iteritems(self._valid_attrs):
|
||||
if name in data:
|
||||
setattr(self, name, data[name])
|
||||
else:
|
||||
setattr(self, name, attribute.default)
|
||||
|
||||
# restore the UUID field
|
||||
setattr(self, '_uuid', data.get('uuid'))
|
||||
|
||||
def __setstate__(self, data):
|
||||
self.__init__()
|
||||
self.deserialize(data)
|
||||
|
|
|
@ -202,7 +202,7 @@ class Block(Base, Become, Conditional, Taggable):
|
|||
'''
|
||||
|
||||
data = dict()
|
||||
for attr in self._get_base_attributes():
|
||||
for attr in self._valid_attrs:
|
||||
if attr not in ('block', 'rescue', 'always'):
|
||||
data[attr] = getattr(self, attr)
|
||||
|
||||
|
@ -229,7 +229,7 @@ class Block(Base, Become, Conditional, Taggable):
|
|||
|
||||
# we don't want the full set of attributes (the task lists), as that
|
||||
# would lead to a serialize/deserialize loop
|
||||
for attr in self._get_base_attributes():
|
||||
for attr in self._valid_attrs:
|
||||
if attr in data and attr not in ('block', 'rescue', 'always'):
|
||||
setattr(self, attr, data.get(attr))
|
||||
|
||||
|
@ -324,15 +324,7 @@ class Block(Base, Become, Conditional, Taggable):
|
|||
return value
|
||||
|
||||
def _get_attr_environment(self):
|
||||
'''
|
||||
Override for the 'tags' getattr fetcher, used from Base.
|
||||
'''
|
||||
environment = self._attributes['environment']
|
||||
parent_environment = self._get_parent_attribute('environment', extend=True)
|
||||
if parent_environment is not None:
|
||||
environment = self._extend_value(environment, parent_environment)
|
||||
|
||||
return environment
|
||||
return self._get_parent_attribute('environment')
|
||||
|
||||
def _get_attr_any_errors_fatal(self):
|
||||
'''
|
||||
|
|
|
@ -147,7 +147,7 @@ class Role(Base, Become, Conditional, Taggable):
|
|||
|
||||
# copy over all field attributes, except for when and tags, which
|
||||
# are special cases and need to preserve pre-existing values
|
||||
for (attr_name, _) in iteritems(self._get_base_attributes()):
|
||||
for (attr_name, _) in iteritems(self._valid_attrs):
|
||||
if attr_name not in ('when', 'tags'):
|
||||
setattr(self, attr_name, getattr(role_include, attr_name))
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@ class RoleDefinition(Base, Become, Conditional, Taggable):
|
|||
|
||||
role_def = dict()
|
||||
role_params = dict()
|
||||
base_attribute_names = frozenset(self._get_base_attributes().keys())
|
||||
base_attribute_names = frozenset(self._valid_attrs.keys())
|
||||
for (key, value) in iteritems(ds):
|
||||
# use the list of FieldAttribute values to determine what is and is not
|
||||
# an extra parameter for this role (or sub-class of this role)
|
||||
|
|
|
@ -220,7 +220,7 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
# top level of the task, so we move those into the 'vars' dictionary
|
||||
# here, and show a deprecation message as we will remove this at
|
||||
# some point in the future.
|
||||
if action == 'include' and k not in self._get_base_attributes() and k not in self.DEPRECATED_ATTRIBUTES:
|
||||
if action == 'include' and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES:
|
||||
display.deprecated("Specifying include variables at the top-level of the task is deprecated."
|
||||
" Please see:\nhttp://docs.ansible.com/ansible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n"
|
||||
" for currently supported syntax regarding included files and variables")
|
||||
|
|
Loading…
Reference in a new issue