#!/usr/bin/python # # Copyright (C) 2012 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """ A set of classes (models) each closely representing an XML node in the metadata_properties.xml file. Node: Base class for most nodes. Entry: A node corresponding to <entry> elements. Clone: A node corresponding to <clone> elements. Kind: A node corresponding to <dynamic>, <static>, <controls> elements. InnerNamespace: A node corresponding to a <namespace> nested under a <kind>. OuterNamespace: A node corresponding to a <namespace> with <kind> children. Section: A node corresponding to a <section> element. Enum: A class corresponding an <enum> element within an <entry> Value: A class corresponding to a <value> element within an Enum Metadata: Root node that also provides tree construction functionality. Tag: A node corresponding to a top level <tag> element. """ import sys import itertools from collections import OrderedDict class Node(object): """ Base class for most nodes that are part of the Metadata graph. Attributes (Read-Only): parent: An edge to a parent Node. name: A string describing the name, usually but not always the 'name' attribute of the corresponding XML node. """ def __init__(self): self._parent = None self._name = None @property def parent(self): return self._parent @property def name(self): return self._name def find_all(self, pred): """ Find all descendants that match the predicate. Args: pred: a predicate function that acts as a filter for a Node Yields: A sequence of all descendants for which pred(node) is true, in a pre-order visit order. """ if pred(self): yield self if self._get_children() is None: return for i in self._get_children(): for j in i.find_all(pred): yield j def find_first(self, pred): """ Find the first descendant that matches the predicate. Args: pred: a predicate function that acts as a filter for a Node Returns: The first Node from find_all(pred), or None if there were no results. """ for i in self.find_all(pred): return i return None def find_parent_first(self, pred): """ Find the first ancestor that matches the predicate. Args: pred: A predicate function that acts as a filter for a Node Returns: The first ancestor closest to the node for which pred(node) is true. """ for i in self.find_parents(pred): return i return None def find_parents(self, pred): """ Find all ancestors that match the predicate. Args: pred: A predicate function that acts as a filter for a Node Yields: A sequence of all ancestors (closest to furthest) from the node, where pred(node) is true. """ parent = self.parent while parent is not None: if pred(parent): yield parent parent = parent.parent def sort_children(self): """ Sorts the immediate children in-place. """ self._sort_by_name(self._children) def _sort_by_name(self, what): what.sort(key=lambda x: x.name) def _get_name(self): return lambda x: x.name # Iterate over all children nodes. None when node doesn't support children. def _get_children(self): return (i for i in self._children) def _children_name_map_matching(self, match=lambda x: True): d = {} for i in _get_children(): if match(i): d[i.name] = i return d @staticmethod def _dictionary_by_name(values): d = OrderedDict() for i in values: d[i.name] = i return d def validate_tree(self): """ Sanity check the tree recursively, ensuring for a node n, all children's parents are also n. Returns: True if validation succeeds, False otherwise. """ succ = True children = self._get_children() if children is None: return True for child in self._get_children(): if child.parent != self: print >> sys.stderr, ("ERROR: Node '%s' doesn't match the parent" + \ "(expected: %s, actual %s)") \ %(child, self, child.parent) succ = False succ = child.validate_tree() and succ return succ def __str__(self): return "<%s name='%s'>" %(self.__class__, self.name) class Metadata(Node): """ A node corresponding to a <metadata> entry. Attributes (Read-Only): parent: An edge to the parent Node. This is always None for Metadata. outer_namespaces: A sequence of immediate OuterNamespace children. tags: A sequence of all Tag instances available in the graph. """ def __init__(self): """ Initialize with no children. Use insert_* functions and then construct_graph() to build up the Metadata from some source. """ # Private self._entries = [] # kind => { name => entry } self._entry_map = { 'static': {}, 'dynamic': {}, 'controls': {} } self._entries_ordered = [] # list of ordered Entry/Clone instances self._clones = [] # Public (Read Only) self._parent = None self._outer_namespaces = None self._tags = [] @property def outer_namespaces(self): if self._outer_namespaces is None: return None else: return (i for i in self._outer_namespaces) @property def tags(self): return (i for i in self._tags) def _get_properties(self): for i in self._entries: yield i for i in self._clones: yield i def insert_tag(self, tag, description=""): """ Insert a tag into the metadata. Args: tag: A string identifier for a tag. description: A string description for a tag. Example: metadata.insert_tag("BC", "Backwards Compatibility for old API") Remarks: Subsequent calls to insert_tag with the same tag are safe (they will be ignored). """ tag_ids = [tg.name for tg in self.tags if tg.name == tag] if not tag_ids: self._tags.append(Tag(tag, self, description)) def insert_entry(self, entry): """ Insert an entry into the metadata. Args: entry: A key-value dictionary describing an entry. Refer to Entry#__init__ for the keys required/optional. Remarks: Subsequent calls to insert_entry with the same entry+kind name are safe (they will be ignored). """ e = Entry(**entry) self._entries.append(e) self._entry_map[e.kind][e.name] = e self._entries_ordered.append(e) def insert_clone(self, clone): """ Insert a clone into the metadata. Args: clone: A key-value dictionary describing a clone. Refer to Clone#__init__ for the keys required/optional. Remarks: Subsequent calls to insert_clone with the same clone+kind name are safe (they will be ignored). Also the target entry need not be inserted ahead of the clone entry. """ entry_name = clone['name'] # figure out corresponding entry later. allow clone insert, entry insert entry = None c = Clone(entry, **clone) self._entry_map[c.kind][c.name] = c self._clones.append(c) self._entries_ordered.append(c) def prune_clones(self): """ Remove all clones that don't point to an existing entry. Remarks: This should be called after all insert_entry/insert_clone calls have finished. """ remove_list = [] for p in self._clones: if p.entry is None: remove_list.append(p) for p in remove_list: # remove from parent's entries list if p.parent is not None: p.parent._entries.remove(p) # remove from parents' _leafs list for ancestor in p.find_parents(lambda x: not isinstance(x, MetadataSet)): ancestor._leafs.remove(p) # remove from global list self._clones.remove(p) self._entry_map[p.kind].pop(p.name) self._entries_ordered.remove(p) # After all entries/clones are inserted, # invoke this to generate the parent/child node graph all these objects def construct_graph(self): """ Generate the graph recursively, after which all Entry nodes will be accessible recursively by crawling through the outer_namespaces sequence. Remarks: This is safe to be called multiple times at any time. It should be done at least once or there will be no graph. """ self.validate_tree() self._construct_tags() self.validate_tree() self._construct_clones() self.validate_tree() self._construct_outer_namespaces() self.validate_tree() def _construct_tags(self): tag_dict = self._dictionary_by_name(self.tags) for p in self._get_properties(): p._tags = [] for tag_id in p._tag_ids: tag = tag_dict.get(tag_id) if tag not in p._tags: p._tags.append(tag) if p not in tag.entries: tag._entries.append(p) def _construct_clones(self): for p in self._clones: target_kind = p.target_kind target_entry = self._entry_map[target_kind].get(p.name) p._entry = target_entry # should not throw if we pass validation # but can happen when importing obsolete CSV entries if target_entry is None: print >> sys.stderr, ("WARNING: Clone entry '%s' target kind '%s'" + \ " has no corresponding entry") \ %(p.name, p.target_kind) def _construct_outer_namespaces(self): if self._outer_namespaces is None: #the first time this runs self._outer_namespaces = [] root = self._dictionary_by_name(self._outer_namespaces) for ons_name, ons in root.iteritems(): ons._leafs = [] for p in self._entries_ordered: ons_name = p.get_outer_namespace() ons = root.get(ons_name, OuterNamespace(ons_name, self)) root[ons_name] = ons if p not in ons._leafs: ons._leafs.append(p) for ons_name, ons in root.iteritems(): ons.validate_tree() self._construct_sections(ons) if ons not in self._outer_namespaces: self._outer_namespaces.append(ons) ons.validate_tree() def _construct_sections(self, outer_namespace): sections_dict = self._dictionary_by_name(outer_namespace.sections) for sec_name, sec in sections_dict.iteritems(): sec._leafs = [] sec.validate_tree() for p in outer_namespace._leafs: does_exist = sections_dict.get(p.get_section()) sec = sections_dict.get(p.get_section(), \ Section(p.get_section(), outer_namespace)) sections_dict[p.get_section()] = sec sec.validate_tree() if p not in sec._leafs: sec._leafs.append(p) for sec_name, sec in sections_dict.iteritems(): if not sec.validate_tree(): print >> sys.stderr, ("ERROR: Failed to validate tree in " + \ "construct_sections (start), with section = '%s'")\ %(sec) self._construct_kinds(sec) if sec not in outer_namespace.sections: outer_namespace._sections.append(sec) if not sec.validate_tree(): print >> sys.stderr, ("ERROR: Failed to validate tree in " + \ "construct_sections (end), with section = '%s'") \ %(sec) # 'controls', 'static' 'dynamic'. etc def _construct_kinds(self, section): for kind in section.kinds: kind._leafs = [] section.validate_tree() group_entry_by_kind = itertools.groupby(section._leafs, lambda x: x.kind) leaf_it = ((k, g) for k, g in group_entry_by_kind) # allow multiple kinds with the same name. merge if adjacent # e.g. dynamic,dynamic,static,static,dynamic -> dynamic,static,dynamic # this helps maintain ABI compatibility when adding an entry in a new kind for idx, (kind_name, entry_it) in enumerate(leaf_it): if idx >= len(section._kinds): kind = Kind(kind_name, section) section._kinds.append(kind) section.validate_tree() kind = section._kinds[idx] for p in entry_it: if p not in kind._leafs: kind._leafs.append(p) for kind in section._kinds: kind.validate_tree() self._construct_inner_namespaces(kind) kind.validate_tree() self._construct_entries(kind) kind.validate_tree() if not section.validate_tree(): print >> sys.stderr, ("ERROR: Failed to validate tree in " + \ "construct_kinds, with kind = '%s'") %(kind) if not kind.validate_tree(): print >> sys.stderr, ("ERROR: Failed to validate tree in " + \ "construct_kinds, with kind = '%s'") %(kind) def _construct_inner_namespaces(self, parent, depth=0): #parent is InnerNamespace or Kind ins_dict = self._dictionary_by_name(parent.namespaces) for name, ins in ins_dict.iteritems(): ins._leafs = [] for p in parent._leafs: ins_list = p.get_inner_namespace_list() if len(ins_list) > depth: ins_str = ins_list[depth] ins = ins_dict.get(ins_str, InnerNamespace(ins_str, parent)) ins_dict[ins_str] = ins if p not in ins._leafs: ins._leafs.append(p) for name, ins in ins_dict.iteritems(): ins.validate_tree() # construct children INS self._construct_inner_namespaces(ins, depth + 1) ins.validate_tree() # construct children entries self._construct_entries(ins, depth + 1) if ins not in parent.namespaces: parent._namespaces.append(ins) if not ins.validate_tree(): print >> sys.stderr, ("ERROR: Failed to validate tree in " + \ "construct_inner_namespaces, with ins = '%s'") \ %(ins) # doesnt construct the entries, so much as links them def _construct_entries(self, parent, depth=0): #parent is InnerNamespace or Kind entry_dict = self._dictionary_by_name(parent.entries) for p in parent._leafs: ins_list = p.get_inner_namespace_list() if len(ins_list) == depth: entry = entry_dict.get(p.name, p) entry_dict[p.name] = entry for name, entry in entry_dict.iteritems(): old_parent = entry.parent entry._parent = parent if entry not in parent.entries: parent._entries.append(entry) if old_parent is not None and old_parent != parent: print >> sys.stderr, ("ERROR: Parent changed from '%s' to '%s' for " + \ "entry '%s'") \ %(old_parent.name, parent.name, entry.name) def _get_children(self): if self.outer_namespaces is not None: for i in self.outer_namespaces: yield i if self.tags is not None: for i in self.tags: yield i class Tag(Node): """ A tag Node corresponding to a top-level <tag> element. Attributes (Read-Only): name: alias for id id: The name of the tag, e.g. for <tag id="BC"/> id = 'BC' description: The description of the tag, the contents of the <tag> element. parent: An edge to the parent, which is always the Metadata root node. entries: A sequence of edges to entries/clones that are using this Tag. """ def __init__(self, name, parent, description=""): self._name = name # 'id' attribute in XML self._id = name self._description = description self._parent = parent # all entries that have this tag, including clones self._entries = [] # filled in by Metadata#construct_tags @property def id(self): return self._id @property def description(self): return self._description @property def entries(self): return (i for i in self._entries) def _get_children(self): return None class OuterNamespace(Node): """ A node corresponding to a <namespace> element under <metadata> Attributes (Read-Only): name: The name attribute of the <namespace name="foo"> element. parent: An edge to the parent, which is always the Metadata root node. sections: A sequence of Section children. """ def __init__(self, name, parent, sections=[]): self._name = name self._parent = parent # MetadataSet self._sections = sections[:] self._leafs = [] self._children = self._sections @property def sections(self): return (i for i in self._sections) class Section(Node): """ A node corresponding to a <section> element under <namespace> Attributes (Read-Only): name: The name attribute of the <section name="foo"> element. parent: An edge to the parent, which is always an OuterNamespace instance. description: A string description of the section, or None. kinds: A sequence of Kind children. merged_kinds: A sequence of virtual Kind children, with each Kind's children merged by the kind.name """ def __init__(self, name, parent, description=None, kinds=[]): self._name = name self._parent = parent self._description = description self._kinds = kinds[:] self._leafs = [] @property def description(self): return self._description @property def kinds(self): return (i for i in self._kinds) def sort_children(self): self.validate_tree() # order is always controls,static,dynamic find_child = lambda x: [i for i in self._get_children() if i.name == x] new_lst = find_child('controls') \ + find_child('static') \ + find_child('dynamic') self._kinds = new_lst self.validate_tree() def _get_children(self): return (i for i in self.kinds) @property def merged_kinds(self): def aggregate_by_name(acc, el): existing = [i for i in acc if i.name == el.name] if existing: k = existing[0] else: k = Kind(el.name, el.parent) acc.append(k) k._namespaces.extend(el._namespaces) k._entries.extend(el._entries) return acc new_kinds_lst = reduce(aggregate_by_name, self.kinds, []) for k in new_kinds_lst: yield k class Kind(Node): """ A node corresponding to one of: <static>,<dynamic>,<controls> under a <section> element. Attributes (Read-Only): name: A string which is one of 'static', 'dynamic, or 'controls'. parent: An edge to the parent, which is always a Section instance. namespaces: A sequence of InnerNamespace children. entries: A sequence of Entry/Clone children. merged_entries: A sequence of MergedEntry virtual nodes from entries """ def __init__(self, name, parent): self._name = name self._parent = parent self._namespaces = [] self._entries = [] self._leafs = [] @property def namespaces(self): return self._namespaces @property def entries(self): return self._entries @property def merged_entries(self): for i in self.entries: yield i.merge() def sort_children(self): self._namespaces.sort(key=self._get_name()) self._entries.sort(key=self._get_name()) def _get_children(self): for i in self.namespaces: yield i for i in self.entries: yield i class InnerNamespace(Node): """ A node corresponding to a <namespace> which is an ancestor of a Kind. These namespaces may have other namespaces recursively, or entries as leafs. Attributes (Read-Only): name: Name attribute from the element, e.g. <namespace name="foo"> -> 'foo' parent: An edge to the parent, which is an InnerNamespace or a Kind. namespaces: A sequence of InnerNamespace children. entries: A sequence of Entry/Clone children. merged_entries: A sequence of MergedEntry virtual nodes from entries """ def __init__(self, name, parent): self._name = name self._parent = parent self._namespaces = [] self._entries = [] self._leafs = [] @property def namespaces(self): return self._namespaces @property def entries(self): return self._entries @property def merged_entries(self): for i in self.entries: yield i.merge() def sort_children(self): self._namespaces.sort(key=self._get_name()) self._entries.sort(key=self._get_name()) def _get_children(self): for i in self.namespaces: yield i for i in self.entries: yield i class EnumValue(Node): """ A class corresponding to a <value> element within an <enum> within an <entry>. Attributes (Read-Only): name: A string, e.g. 'ON' or 'OFF' id: An optional numeric string, e.g. '0' or '0xFF' optional: A boolean notes: A string describing the notes, or None. parent: An edge to the parent, always an Enum instance. """ def __init__(self, name, parent, id=None, optional=False, notes=None): self._name = name # str, e.g. 'ON' or 'OFF' self._id = id # int, e.g. '0' self._optional = optional # bool self._notes = notes # None or str self._parent = parent @property def id(self): return self._id @property def optional(self): return self._optional @property def notes(self): return self._notes def _get_children(self): return None class Enum(Node): """ A class corresponding to an <enum> element within an <entry>. Attributes (Read-Only): parent: An edge to the parent, always an Entry instance. values: A sequence of EnumValue children. """ def __init__(self, parent, values, ids={}, optionals=[], notes={}): self._values = \ [ EnumValue(val, self, ids.get(val), val in optionals, notes.get(val)) \ for val in values ] self._parent = parent self._name = None @property def values(self): return (i for i in self._values) def _get_children(self): return (i for i in self._values) class Entry(Node): """ A node corresponding to an <entry> element. Attributes (Read-Only): parent: An edge to the parent node, which is an InnerNamespace or Kind. name: The fully qualified name string, e.g. 'android.shading.mode' name_short: The name attribute from <entry name="mode">, e.g. mode type: The type attribute from <entry type="bar"> kind: A string ('static', 'dynamic', 'controls') corresponding to the ancestor Kind#name container: The container attribute from <entry container="array">, or None. container_sizes: A sequence of size strings or None if container is None. enum: An Enum instance if the enum attribute is true, None otherwise. tuple_values: A sequence of strings describing the tuple values, None if container is not 'tuple'. description: A string description, or None. range: A string range, or None. units: A string units, or None. tags: A sequence of Tag nodes associated with this Entry. type_notes: A string describing notes for the type, or None. Remarks: Subclass Clone can be used interchangeable with an Entry, for when we don't care about the underlying type. parent and tags edges are invalid until after Metadata#construct_graph has been invoked. """ def __init__(self, **kwargs): """ Instantiate a new Entry node. Args: name: A string with the fully qualified name, e.g. 'android.shading.mode' type: A string describing the type, e.g. 'int32' kind: A string describing the kind, e.g. 'static' Args (if container): container: A string describing the container, e.g. 'array' or 'tuple' container_sizes: A list of string sizes if a container, or None otherwise Args (if container is 'tuple'): tuple_values: A list of tuple values, e.g. ['width', 'height'] Args (if the 'enum' attribute is true): enum: A boolean, True if this is an enum, False otherwise enum_values: A list of value strings, e.g. ['ON', 'OFF'] enum_optionals: A list of optional enum values, e.g. ['OFF'] enum_notes: A dictionary of value->notes strings. enum_ids: A dictionary of value->id strings. Args (optional): description: A string with a description of the entry. range: A string with the range of the values of the entry, e.g. '>= 0' units: A string with the units of the values, e.g. 'inches' notes: A string with the notes for the entry tag_ids: A list of tag ID strings, e.g. ['BC', 'V1'] type_notes: A string with the notes for the type """ if kwargs.get('type') is None: print >> sys.stderr, "ERROR: Missing type for entry '%s' kind '%s'" \ %(kwargs.get('name'), kwargs.get('kind')) # Attributes are Read-Only, but edges may be mutated by # Metadata, particularly during construct_graph self._name = kwargs['name'] self._type = kwargs['type'] self._kind = kwargs['kind'] # static, dynamic, or controls self._init_common(**kwargs) @property def type(self): return self._type @property def kind(self): return self._kind @property def name_short(self): return self.get_name_minimal() @property def container(self): return self._container @property def container_sizes(self): if self._container_sizes is None: return None else: return (i for i in self._container_sizes) @property def tuple_values(self): if self._tuple_values is None: return None else: return (i for i in self._tuple_values) @property def description(self): return self._description @property def range(self): return self._range @property def units(self): return self._units @property def notes(self): return self._notes @property def tags(self): if self._tags is None: return None else: return (i for i in self._tags) @property def type_notes(self): return self._type_notes @property def enum(self): return self._enum def _get_children(self): if self.enum: yield self.enum def sort_children(self): return None def is_clone(self): """ Whether or not this is a Clone instance. Returns: False """ return False def _init_common(self, **kwargs): self._parent = None # filled in by MetadataSet::_construct_entries self._container = kwargs.get('container') self._container_sizes = kwargs.get('container_sizes') # access these via the 'enum' prop enum_values = kwargs.get('enum_values') enum_optionals = kwargs.get('enum_optionals') enum_notes = kwargs.get('enum_notes') # { value => notes } enum_ids = kwargs.get('enum_ids') # { value => notes } self._tuple_values = kwargs.get('tuple_values') self._description = kwargs.get('description') self._range = kwargs.get('range') self._units = kwargs.get('units') self._notes = kwargs.get('notes') self._tag_ids = kwargs.get('tag_ids', []) self._tags = None # Filled in by MetadataSet::_construct_tags self._type_notes = kwargs.get('type_notes') if kwargs.get('enum', False): self._enum = Enum(self, enum_values, enum_ids, enum_optionals, enum_notes) else: self._enum = None self._property_keys = kwargs def merge(self): """ Copy the attributes into a new entry, merging it with the target entry if it's a clone. """ return MergedEntry(self) # Helpers for accessing less than the fully qualified name def get_name_as_list(self): """ Returns the name as a list split by a period. For example: entry.name is 'android.lens.info.shading' entry.get_name_as_list() == ['android', 'lens', 'info', 'shading'] """ return self.name.split(".") def get_inner_namespace_list(self): """ Returns the inner namespace part of the name as a list For example: entry.name is 'android.lens.info.shading' entry.get_inner_namespace_list() == ['info'] """ return self.get_name_as_list()[2:-1] def get_outer_namespace(self): """ Returns the outer namespace as a string. For example: entry.name is 'android.lens.info.shading' entry.get_outer_namespace() == 'android' Remarks: Since outer namespaces are non-recursive, and each entry has one, this does not need to be a list. """ return self.get_name_as_list()[0] def get_section(self): """ Returns the section as a string. For example: entry.name is 'android.lens.info.shading' entry.get_section() == '' Remarks: Since outer namespaces are non-recursive, and each entry has one, this does not need to be a list. """ return self.get_name_as_list()[1] def get_name_minimal(self): """ Returns only the last component of the fully qualified name as a string. For example: entry.name is 'android.lens.info.shading' entry.get_name_minimal() == 'shading' Remarks: entry.name_short it an alias for this """ return self.get_name_as_list()[-1] def get_path_without_name(self): """ Returns a string path to the entry, with the name component excluded. For example: entry.name is 'android.lens.info.shading' entry.get_path_without_name() == 'android.lens.info' """ return ".".join(self.get_name_as_list()[0:-1]) class Clone(Entry): """ A Node corresponding to a <clone> element. It has all the attributes of an <entry> element (Entry) plus the additions specified below. Attributes (Read-Only): entry: an edge to an Entry object that this targets target_kind: A string describing the kind of the target entry. name: a string of the name, same as entry.name kind: a string of the Kind ancestor, one of 'static', 'controls', 'dynamic' for the <clone> element. type: always None, since a clone cannot override the type. """ def __init__(self, entry=None, **kwargs): """ Instantiate a new Clone node. Args: name: A string with the fully qualified name, e.g. 'android.shading.mode' type: A string describing the type, e.g. 'int32' kind: A string describing the kind, e.g. 'static' target_kind: A string for the kind of the target entry, e.g. 'dynamic' Args (if container): container: A string describing the container, e.g. 'array' or 'tuple' container_sizes: A list of string sizes if a container, or None otherwise Args (if container is 'tuple'): tuple_values: A list of tuple values, e.g. ['width', 'height'] Args (if the 'enum' attribute is true): enum: A boolean, True if this is an enum, False otherwise enum_values: A list of value strings, e.g. ['ON', 'OFF'] enum_optionals: A list of optional enum values, e.g. ['OFF'] enum_notes: A dictionary of value->notes strings. enum_ids: A dictionary of value->id strings. Args (optional): entry: An edge to the corresponding target Entry. description: A string with a description of the entry. range: A string with the range of the values of the entry, e.g. '>= 0' units: A string with the units of the values, e.g. 'inches' notes: A string with the notes for the entry tag_ids: A list of tag ID strings, e.g. ['BC', 'V1'] type_notes: A string with the notes for the type Remarks: Note that type is not specified since it has to be the same as the entry.type. """ self._entry = entry # Entry object self._target_kind = kwargs['target_kind'] self._name = kwargs['name'] # same as entry.name self._kind = kwargs['kind'] # illegal to override the type, it should be the same as the entry self._type = None # the rest of the kwargs are optional # can be used to override the regular entry data self._init_common(**kwargs) @property def entry(self): return self._entry @property def target_kind(self): return self._target_kind def is_clone(self): """ Whether or not this is a Clone instance. Returns: True """ return True class MergedEntry(Entry): """ A MergedEntry has all the attributes of a Clone and its target Entry merged together. Remarks: Useful when we want to 'unfold' a clone into a real entry by copying out the target entry data. In this case we don't care about distinguishing a clone vs an entry. """ def __init__(self, entry): """ Create a new instance of MergedEntry. Args: entry: An Entry or Clone instance """ props_distinct = ['description', 'units', 'range', 'notes', 'tags', 'kind'] for p in props_distinct: p = '_' + p if entry.is_clone(): setattr(self, p, getattr(entry, p) or getattr(entry.entry, p)) else: setattr(self, p, getattr(entry, p)) props_common = ['parent', 'name', 'container', 'container_sizes', 'enum', 'tuple_values', 'type', 'type_notes', ] for p in props_common: p = '_' + p if entry.is_clone(): setattr(self, p, getattr(entry.entry, p)) else: setattr(self, p, getattr(entry, p))