Source code for aiatools.attributes

# -*- mode: python; coding: utf-8; -*-
# Copyright © 2017 Massachusetts Institute of Technology, All rights reserved.

"""
The :py:mod:`aiatools.attributes` module provides :py:class:`~aiatools.algebra.Functor` for querying App Inventor
projects.

.. testsetup::

    from aiatools.aia import AIAFile
    from aiatools.attributes import *
    from aiatools.block_types import *
    from aiatools.component_types import *
    project = AIAFile('test_aias/LondonCholeraMap.aia')
"""

from aiatools.algebra import ComputedAttribute
from .algebra import Functor, NotExpression
from .common import Block, BlockKind
from .selectors import select
import collections


__author__ = 'Evan W. Patton <ewpatton@mit.edu>'


[docs]class NamedAttribute(Functor): """ The :py:class:`NamedAttribute` is a :py:class:`Functor` that retrieves the value of a specific field on entities in an App Inventor project. Example _______ Retrieve any Form entities in the project: >>> project.components(NamedAttribute('type') == Form) [Screen('Screen1')] Parameters __________ name : basestring The name of the attribute to be retrieved. """ # noinspection PyShadowingNames def __init__(self, name): self.name = name def __call__(self, obj, *args, **kwargs): if hasattr(obj, self.name): return getattr(obj, self.name) return None def __hash__(self): return hash(self.name) def __eq__(self, other): if isinstance(other, NamedAttribute): return self.name == other.name else: return super(NamedAttribute, self).__eq__(other) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, repr(self.name))
[docs]class NamedAttributeTuple(Functor): """ The :py:class:`NamedAttributeTuple` is a :py:class:`Functor` that retrieves the value of a field on entities in an App Inventor project. Unlike :py:class:`NamedAttribute`, it can take more than one name as a tuple. The given field names are searched in order until one is found. This is useful for presenting a single :py:class:`Functor` over synonyms, for example, ``'name'`` gives the name of a :py:class:`Component` whereas ``'component_name'`` gives the name of a block's component (if any). Parameters ---------- names : (str, unicode) The name(s) of the attribute(s) to be retrieved. Giving a 1-tuple is less efficient than defining and using the equivalent :py:class:`NamedAttribute` instance. """ def __init__(self, names): super(NamedAttributeTuple, self).__init__() self.names = names def __call__(self, obj, *args, **kwargs): for _name in self.names: if hasattr(obj, _name): return getattr(obj, _name) return None def __hash__(self): return hash(self.names) def __eq__(self, other): if isinstance(other, NamedAttributeTuple): return self.names == other.names else: return super(NamedAttributeTuple, self).__eq__(other) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.names)
[docs]def has_ancestor(target=None): """ Constructs a new ComputedAttribute that accepts an entity if and only if the entity has an ancestor that matches the given ``target`` clause. Example ------- Count the number of :py:data:`~.block_type.text` blocks with an ancestor that is a :py:data:`~.block_type.logic_compare` block. >>> project.blocks((type == text) & has_ancestor(type == logic_compare)).count() 1 Parameters ---------- target : Expression An :py:class:`Expression` to use for testing ancestors. Returns ------- ComputedAttribute A new ComputedAttribute that will walk the entity graph using the ``parent`` field and test whether any ``parent`` matches ``target``. """ def checkAncestor(b): if b is None: return False b = b.parent # Skip b while b is not None: if target is None: return True elif isinstance(target, collections.Callable) and target(b): return True elif b is target: return True b = b.parent return False return ComputedAttribute(checkAncestor)
[docs]def has_descendant(target=None): """ Constructs a new ComputedAttribute that accepts an entity if and only if the entity has a descendant that matches the given ``target`` clause. Example ------- Count the number of top-level blocks that have control_if blocks as descendants. >>> project.blocks(top_level & has_descendant(type == controls_if)).count() 1 Parameters ---------- target : Expression An :py:class:`Expression` to use for testing descendants. Returns ------- ComputedAttribute A new ComputedAttribute that will walk the entity graph using the ``children`` field and test whether any descendant in the subgraph matches ``target``. """ def checkDescendant(b): if b is None: return False if not hasattr(b, 'children'): return False for child in b.children(): if target is None: return True elif isinstance(target, collections.Callable) and target(child): return True elif child is target: return True elif checkDescendant(child): return True return False return ComputedAttribute(checkDescendant)
def _root_block(block): """ Looks up the root of the stack of blocks containing the given ``block``. Example ------- Parameters ---------- block : Block The block of interest for which the root block will be obtained. Returns ------- Block The block at the root of the block stack containing ``block``. """ if not block: return block while block.logical_parent: block = block.logical_parent return block root_block = ComputedAttribute(_root_block)
[docs]class HeightAttribute(Functor): """ :py:class:`HeightAttribute` class is used to memoize the heights of the forest representing an App Inventor project. Use :py:data:`aiatools.attributes.height` to benefit from the memoization feature. Example ------- Get the heights of the block stacks in the project. >>> project.blocks(top_level).map(height) [2, 6] """ def __init__(self): self.precomputed = {} # noinspection PyShadowingNames def __call__(self, *args, **kwargs): block_or_component = args[0] if block_or_component in self.precomputed: return self.precomputed[block_or_component] else: try: height = max(self(x) for x in block_or_component.children()) + 1 except ValueError: height = 0 self.precomputed[block_or_component] = height return height
[docs]class DepthAttribute(Functor): """ :py:class:`DepthAttribute` class is used to memoize the depths of entities in the forest representing an App Inventor project. Use :py:data:`aiatools.attributes.depth` to benefit from the memoization feature. Example ------- Get the depth of all leaves in the project. >>> project.blocks(leaf).map(depth) [2, 2, 4, 4, 4, 6, 5, 5, 5, 5, 5] """ def __init__(self): self.precomputed = {} @staticmethod def _get_parent(block_or_component): if hasattr(block_or_component, 'logical_parent'): return block_or_component.logical_parent else: return block_or_component.parent # noinspection PyShadowingNames def __call__(self, *args, **kwargs): block_or_component = DepthAttribute._get_parent(args[0]) depth = 0 while block_or_component is not None: depth += 1 block_or_component = DepthAttribute._get_parent(block_or_component) return depth
class _MutationHelper(Functor): """ :py:class:`_MutatorHelper` is a helper class used for generating new :py:class:`Functor` for retrieving mutation fields on a block. :py:class:`_MutationHelper` interns the instances so that the returned items are the same instance, that is: >>> mutation.component_type is mutation.component_type True :py:class:`_MutationHelper` is accessed through the :py:data:`mutation` instance. """ _interned = {} def __init__(self, child=None): self.child = child def __call__(self, *args, **kwargs): if hasattr(args[0], 'mutation'): _mutation = args[0].mutation if _mutation is not None: if self.child is None: return _mutation elif self.child in _mutation: return _mutation[self.child] return None def __getattr__(self, item): if item not in _MutationHelper._interned: _MutationHelper._interned[item] = _MutationHelper(item) return _MutationHelper._interned[item] def __repr__(self): return class _FieldHelper(Functor): """ :py:class:`_FieldHelper` is a helper class used for generating new :py:class:`Functor` for retrieving fields of blocks. :py:class:`_FieldHelper` interns the instances so that the returned items are the same instace, that is: >>> fields.OP is fields.OP True :py:class:`_FieldHelper` is accessed through the :py:data:`field` instance. """ _interned = {} def __init__(self, child=None): self.child = child def __call__(self, *args, **kwargs): if hasattr(args[0], 'fields'): _fields = args[0].fields if _fields is not None: if self.child is None: return _fields elif self.child in _fields: return _fields[self.child] return None def __getattr__(self, item): if item not in _FieldHelper._interned: _FieldHelper._interned[item] = _FieldHelper(item) return _FieldHelper._interned[item] type = NamedAttributeTuple(('type', 'component_type')) """Returns the type of the entity.""" name = NamedAttributeTuple(('name', 'instance_name')) """Returns the name of the entity.""" def _kind(block): try: return block.kind except AttributeError: return None kind = NamedAttribute("kind") """Returns the kind of the entity.""" external = NamedAttribute('external') """Returns true if the component is an extension.""" version = NamedAttribute('version') """Returns the version number for the entity.""" category = NamedAttributeTuple(('category', 'category_string')) """Returns the category for the entity.""" help_string = NamedAttribute('help_string') """Returns the help string for the entity.""" show_on_palette = NamedAttribute('show_on_palette') """Returns true if the entity is shown in the palette.""" visible = NamedAttribute('visible') """Returns true if the entity is visible.""" non_visible = NotExpression(visible) """Returns true if the entity is nonvisible.""" icon_name = NamedAttribute('iconName') """Returns the icon for the component.""" return_type = NamedAttribute('return_type') """Gets the return type of the block.""" generic = NamedAttribute('generic') """Tests whether the block is a generic component block.""" disabled = NamedAttribute('disabled') """Tests whether the entity is disabled.""" logically_disabled = NamedAttribute('logically_disabled') """Tests whether the block is logically disabled, either because it is explicitly disabled or is contained within a disabled subtree.""" logically_enabled = ~logically_disabled """Tests whether the block is logically enabled.""" enabled = NamedAttribute('Enabled') | ~disabled """Tests whether the entity is enabled.""" top_level = ComputedAttribute(lambda b: isinstance(b, Block) and b.parent is None) """Tests whether the block is at the top level.""" parent = NamedAttribute('parent') """Gets the parent of the entity.""" mutation = _MutationHelper() """ Tests whether a block has a mutation specified. One can also use :py:data:`mutation` to obtain accessors for specific mutation fields, for example: .. doctest:: >>> project.blocks(mutation.component_type == Button) [...] will retrieve all blocks that have a mutation where the component_type key is the Button type. """ fields = _FieldHelper() """ :py:data:`fields` is a generator for :py:class:`Functor` to retrieve the values of fields in a block. For example: .. doctest:: >>> project.blocks(logic_compare).map(fields.OP) ['EQ'] """ depth = DepthAttribute() """ Computes the depth of the entity with its tree. For components, this will be the number of containers from the Screen. For blocks, this will be the number of logical ancestors to the top-most block of the block stack. .. doctest:: >>> project.components(type == Marker).avg(depth) 2.0 """ height = HeightAttribute() """ Computes the height of the tree from the given entity. This will be the longest path from the node to one of its children. For leaf nodes, the height is 0. .. doctest:: >>> project.blocks(top_level).avg(height) 4.0 """ is_procedure = (type == 'procedures_defreturn') | (type == 'procedures_defnoreturn') """ Returns True if the type of a block is a procedure definition block, either :py:data:`procedures_defreturn` or :py:data:`procedures_defnoreturn` .. doctest:: >>> with AIAFile('test_aias/ProcedureTest.aia') as proc_project: ... proc_project.blocks(is_procedure).count() 7 """ is_called = ComputedAttribute(lambda x: not select(x).callers().empty()) """ Returns True if the entity in question is called by some other block in the code. .. todo:: Add a mechanism for describing the call graph internal to components so that, for example, ``project.blocks(mutation.event_name == 'GotText').callers()`` should be non-empty for any ``Get`` method call blocks in the screen for the same ``instance_name``. """ leaf = ComputedAttribute(lambda x: len(x.children()) == 0) """ Returns True if the entity is a leaf in the tree (i.e., it has no children). .. doctest:: >>> project.blocks(leaf).count(group_by=type) {'text': 3, 'lexical_variable_get': 6, 'color_blue': 2} """ declaration = (type == 'component_event') | (type == 'global_declaration') | is_procedure """ """ statement = kind == BlockKind.STATEMENT """ """ value = kind == BlockKind.VALUE """ .. testcleanup:: project.close() """