Source code for aiatools.aia

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

"""

.. testsetup ::

    from aiatools.aia import AIAFile

The :py:mod:`aiatools.aia` package provides the :py:class:`AIAFile` class for reading App Inventor (.aia) projects.
"""

import logging
import os
from io import StringIO
from os.path import isdir, join
from zipfile import ZipFile

import jprops

from .component_types import Screen
from .selectors import Selector, NamedCollection, UnionSelector

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

log = logging.getLogger(__name__)


[docs]class AIAAsset(object): """ :py:class:`AIAAsset` provides an interface for reading the contents of assets from an App Inventor project. """ def __init__(self, zipfile, name): """ Constructs a new reference to an asset within an AIA file. :param zipfile: The zipped project file that is the source of the asset. :param name: The path of the asset within the project file hierarchy. """ self.zipfile = zipfile self.name = name
[docs] def open(self, mode='r'): """ Opens the asset. ``mode`` is an optional file access mode. :param mode: The access mode to use when accessing the asset. Must be one of 'r', 'rU', or 'U'. :type mode: basestring :return: A file-like for accessing the contents of the asset. :rtype: zipfile.ZipExtFile """ return self.zipfile.open(self.name, mode)
[docs]class AIAFile(object): """ :py:class:`AIAFile` encapsulates an App Inventor project (AIA) file. Opens an App Inventor project (AIA) with the given filename. ``filename`` can be any file-like object that is acceptable to :py:class:`ZipFile`'s constructor, or a path to a directory containing an unzipped project. Parameters ---------- filename : basestring | file A string or file-like containing the contents of an App Inventor project. strict : bool, optional Process the AIAFile in strict mode, i.e., if a blocks file is missing then it is an error. Default: false """ def __init__(self, filename, strict=False): if not isinstance(filename, str) or (not isdir(filename) and filename[-4:] == '.aia'): self.zipfile = ZipFile(filename) else: self.zipfile = None self.assets = [] """ A list of assets contained in the project. :type: list[aiatools.aia.AIAAsset] """ self.filename = filename """ The filename or file-like that is the source of the project. :type: basestring or file """ self.properties = {} """ The contents of the project.properties file. :type: Properties """ self._screens = NamedCollection() self.screens = Selector(self._screens) """ A :py:class:`~aiatools.selectors.Selector` over the components of type :py:class:`~aiatools.component_types.Screen` in the project. For example, if you want to know how many screens are in a project run: .. doctest:: >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: ... len(aia.screens) 1 :type: aiatools.selectors.Selector[aiatools.component_types.Screen] """ self.components = UnionSelector(self._screens, 'components') """ A :py:class:`~aiatools.selectors.Selector` over the component instances of all screens defined in the project. For example, if you want to know how many component instances are in a project run: .. doctest:: >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: ... len(aia.components()) # Form, Map, Marker, Button 4 :type: aiatools.selectors.Selector[aiatools.common.Component] """ self.blocks = UnionSelector(self._screens, 'blocks') """ A :py:class:`~aiatoools.selectors.Selector` over the blocks of all screen defined in the project. For example, if you want to know how many blocks are in a project run: .. doctest:: >>> with AIAFile('test_aias/LondonCholeraMap.aia') as aia: ... len(aia.blocks()) 23 :type: aiatools.selectors.Selector[aiatools.common.Block] """ if self.zipfile: self._process_zip(strict) else: self._process_dir(strict)
[docs] def close(self): if self.zipfile: self.zipfile.close() self.zipfile = None
def __enter__(self): if self.zipfile: self.zipfile.__enter__() return self def __exit__(self, exc_type, exc_val, exc_tb): if self.zipfile: self.zipfile.__exit__(exc_type, exc_val, exc_tb) def _listfiles(self): names = [] for dirname, dirs, files in os.walk(self.filename): names.extend([join(dirname, f) for f in files]) return names def _process_zip(self, strict): """ Processes the contents of an AIA file into Python objects for further operation. """ self.assets = [] for name in self.zipfile.namelist(): if name.startswith('assets/'): self.assets.append(AIAAsset(self, name)) # TODO(ewpatton): Need to load extension JSON to extend language model elif name.startswith('src/'): if name.endswith('.scm'): name = name[:-4] form = self.zipfile.open('%s.scm' % name, 'r') try: blocks = self.zipfile.open('%s.bky' % name, 'r') except KeyError as e: if strict: raise e else: blocks = None # older aia without a bky file screen = Screen(form=form, blocks=blocks) self._screens[screen.name] = screen elif name.endswith('project.properties'): with self.zipfile.open(name) as prop_file: self.properties = jprops.load_properties(prop_file) else: log.warning('Ignoring file in AIA: %s' % name) def _process_dir(self, strict): """ Processes the contents of a directory as if it were an AIA file and converts the content into Python objects for further operation. """ self.assets = [] asset_path = join(self.filename, 'assets') src_path = join(self.filename, 'src') for name in self._listfiles(): if name.startswith(asset_path): self.assets.append(AIAAsset(None, name)) # TODO(ewpatton): Need to load extension JSON to extend language model elif name.startswith(src_path) or name.endswith('.scm') or name.endswith('.bky'): if name.endswith('.scm'): name = name[:-4] if strict and not os.path.exists('%s.bky' % name): raise IOError('Did not find expected blocks file %s.bky' % name) bky_handle = open('%s.bky' % name) if os.path.exists('%s.bky' % name) else StringIO('<xml/>') with open('%s.scm' % name, 'r') as form, bky_handle as blocks: screen = Screen(form=form, blocks=blocks) self._screens[screen.name] = screen elif name.endswith('project.properties'): with open(name, 'r') as prop_file: self.properties = jprops.load_properties(prop_file) else: log.warning('Ignoring file in directory: %s' % name)