Repository: ManiacalLabs/BiblioPixel
Pull request #650 (2018-03-08 16:12:14)

Rename BaseAnimation to Animation (#645)
I was going to charge ahead and finish #645 but I wanted to run it past you more first.

In particular, names like `BaseCircleAnim` would just go away, to be replaced by `animation.circle.Circle`. 

I think it's really neat and sparse - and I'd leave links back to the old names of course - but wanted to run the whole idea past you.

rec: I pushed an example of one such commit to the end to see what you think...
rec: The input needed here isn't really the pull request but on the overall agenda of canonicalizing names (while being backwards compatible).

Patch:
diff --git a/bibliopixel/animation/__init__.py b/bibliopixel/animation/__init__.py
index 78df619..700c208 100644
--- a/bibliopixel/animation/__init__.py
+++ b/bibliopixel/animation/__init__.py
@@ -1,5 +1,5 @@
-from . animation import BaseAnimation
-from . circle import BaseCircleAnim
+from . animation import Animation
+from . circle import Circle
 from . game import BaseGameAnim
 from . matrix import BaseMatrixAnim
 from . cube import BaseCubeAnim
@@ -15,3 +15,5 @@ strip_test = StripChannelTest
 matrix_calibration = MatrixCalibrationTest
 matrix_test = MatrixChannelTest
 from . off import OffAnim
+from . animation import BaseAnimation
+from . circle import BaseCircleAnim
diff --git a/bibliopixel/animation/animation.py b/bibliopixel/animation/animation.py
index e7d13f5..44cd1b8 100644
--- a/bibliopixel/animation/animation.py
+++ b/bibliopixel/animation/animation.py
@@ -1,11 +1,11 @@
 import contextlib, threading, time
 from . runner import Runner, STATE
-from .. util import log
+from .. util import deprecate, log
 from .. util.threads.animation_threading import AnimationThreading
 from .. project import attributes, fields
 
 
-class BaseAnimation(object):
+class Animation(object):
     free_run = False
     pre_recursion = fields.default_converter
 
@@ -97,7 +97,7 @@ class BaseAnimation(object):
                 yield
 
     def run(self, **kwds):
-        # DEPRECATED
+        deprecate.deprecate('BaseAnimation.run')
         self._set_runner(kwds)
         self.start()
 
@@ -164,6 +164,10 @@ class BaseAnimation(object):
         self.threading = AnimationThreading(self.runner, self.run_all_frames)
 
 
+# DEPRECATED
+BaseAnimation = Animation
+
+
 def _report_framerate(timestamps):
     total_time = timestamps[-1] - timestamps[0]
     fps = int(1.0 / max(total_time, 0.001))
diff --git a/bibliopixel/animation/circle.py b/bibliopixel/animation/circle.py
index ba924b3..62c1caf 100644
--- a/bibliopixel/animation/circle.py
+++ b/bibliopixel/animation/circle.py
@@ -1,8 +1,8 @@
-from . animation import BaseAnimation
+from . animation import Animation
 from .. layout import Circle
 
 
-class BaseCircleAnim(BaseAnimation):
+class Circle(Animation):
 
     def __init__(self, layout, **kwds):
         super().__init__(layout, **kwds)
@@ -15,3 +15,7 @@ class BaseCircleAnim(BaseAnimation):
         self.ringCount = layout.ringCount
         self.lastRing = layout.lastRing
         self.ringSteps = layout.ringSteps
+
+
+# DEPRECATED
+BaseCircleAnim = Circle
diff --git a/bibliopixel/animation/collection.py b/bibliopixel/animation/collection.py
index 611625d..b1aef01 100644
--- a/bibliopixel/animation/collection.py
+++ b/bibliopixel/animation/collection.py
@@ -1,12 +1,12 @@
 import os, traceback
 from .. project import aliases, construct, load, project
 from . import animation
-from .. util import json, log
+from .. util import deprecate, json, log
 
 
-class Collection(animation.BaseAnimation):
+class Collection(animation.Animation):
     """
-    A ``Collection`` is a list of ``BaseAnimation``s
+    A ``Collection`` is a list of ``Animation``s
     """
     FAIL_ON_EXCEPTION = False
 
@@ -61,7 +61,7 @@ class Collection(animation.BaseAnimation):
         super().cleanup(clean_layout)
 
     def add_animation(self, anim, **kwds):
-        # DEPRECATED.
+        deprecate.deprecate('Collection.add_animation')
         anim.set_runner(kwds)
         self.animations.append(anim)
 
diff --git a/bibliopixel/animation/cube.py b/bibliopixel/animation/cube.py
index 40cd98e..454ee7a 100644
--- a/bibliopixel/animation/cube.py
+++ b/bibliopixel/animation/cube.py
@@ -1,8 +1,8 @@
-from . animation import BaseAnimation
+from . animation import Animation
 from .. layout import Cube
 
 
-class BaseCubeAnim(BaseAnimation):
+class BaseCubeAnim(Animation):
     LAYOUT_CLASS = Cube
     LAYOUT_ARGS = 'x', 'y', 'z'
 
diff --git a/bibliopixel/animation/failed.py b/bibliopixel/animation/failed.py
index 74aceb8..c7c0ca6 100644
--- a/bibliopixel/animation/failed.py
+++ b/bibliopixel/animation/failed.py
@@ -3,7 +3,7 @@ from .. util import log
 from . import animation
 
 
-class Failed(animation.BaseAnimation):
+class Failed(animation.Animation):
     """
     An animation that's created when we fail to load or construct the
     animation that was originally specified
diff --git a/bibliopixel/animation/matrix.py b/bibliopixel/animation/matrix.py
index d5dcf12..55c9993 100644
--- a/bibliopixel/animation/matrix.py
+++ b/bibliopixel/animation/matrix.py
@@ -1,8 +1,8 @@
-from . animation import BaseAnimation
+from . animation import Animation
 from .. layout import Matrix
 
 
-class BaseMatrixAnim(BaseAnimation):
+class BaseMatrixAnim(Animation):
     LAYOUT_CLASS = Matrix
     LAYOUT_ARGS = 'width', 'height'
 
diff --git a/bibliopixel/animation/off.py b/bibliopixel/animation/off.py
index 95764c9..30e325f 100644
--- a/bibliopixel/animation/off.py
+++ b/bibliopixel/animation/off.py
@@ -1,7 +1,7 @@
-from . animation import BaseAnimation
+from . animation import Animation
 
 
-class Off(BaseAnimation):
+class Off(Animation):
     """A trivial animation that turns all pixels in a layout off."""
 
     def __init__(self, layout, timeout=1, **kwds):
diff --git a/bibliopixel/animation/receiver.py b/bibliopixel/animation/receiver.py
index 7029a1f..b265b3c 100644
--- a/bibliopixel/animation/receiver.py
+++ b/bibliopixel/animation/receiver.py
@@ -1,4 +1,4 @@
-from . animation import BaseAnimation
+from . animation import Animation
 from .. util import log
 import threading
 from enum import IntEnum
@@ -19,7 +19,7 @@ HOST_MAP = {
 }
 
 
-class Receiver(BaseAnimation):
+class Receiver(Animation):
     free_run = True
 
     def __init__(self, layout, **kwds):
diff --git a/bibliopixel/animation/strip.py b/bibliopixel/animation/strip.py
index 4fd03a2..c98a1fb 100644
--- a/bibliopixel/animation/strip.py
+++ b/bibliopixel/animation/strip.py
@@ -1,8 +1,8 @@
-from . animation import BaseAnimation
+from . animation import Animation
 from .. layout import Strip
 
 
-class BaseStripAnim(BaseAnimation):
+class BaseStripAnim(Animation):
     LAYOUT_CLASS = Strip
     LAYOUT_ARGS = 'num',
 
diff --git a/bibliopixel/control/address.py b/bibliopixel/control/address.py
index dfe5edc..fd30239 100644
--- a/bibliopixel/control/address.py
+++ b/bibliopixel/control/address.py
@@ -63,7 +63,11 @@ class Address:
         def set(self, target, *value):
             target(*value)
 
-    def __init__(self, name):
+    def __init__(self, name=None):
+        if not name:
+            self.address = self.assignment = ()
+            return
+
         self.name, *assignment = name.split('=', 1)
         self.name = self.name.strip()
 
@@ -76,6 +80,8 @@ class Address:
             raise ValueError('Empty Addresses are not allowed')
 
         if assignment:
+            if not self.address:
+                raise ValueError('Cannot assign to an empty address')
             if isinstance(self.address[-1], Address.Call):
                 raise ValueError('Cannot assign to a call operation')
             assignment = [s.strip() for s in assignment[0].split(',')]
@@ -84,6 +90,9 @@ class Address:
         else:
             self.assignment = ()
 
+    def __bool__(self):
+        return bool(self.address)
+
     def __str__(self):
         return self.name
 
diff --git a/bibliopixel/control/serial_gamepad.py b/bibliopixel/control/serial_gamepad.py
index 6143884..23a5532 100644
--- a/bibliopixel/control/serial_gamepad.py
+++ b/bibliopixel/control/serial_gamepad.py
@@ -1,5 +1,5 @@
 from distutils.version import LooseVersion
-from .. util import AttributeDict, generate_header, log
+from .. util import AttributeDict, deprecate, generate_header, log
 from .. drivers.return_codes import RETURN_CODES, print_error
 from . gamepad import BaseGamePad
 
@@ -49,7 +49,7 @@ class SerialGamePad(BaseGamePad):
             self._com.close()
 
     def close(self):
-        # DEPRECATED.
+        deprecate.deprecate('SerialGamePad.close')
         self.cleanup()
 
     @staticmethod
diff --git a/bibliopixel/drivers/SPI/base.py b/bibliopixel/drivers/SPI/base.py
index c4d9e42..77633cf 100644
--- a/bibliopixel/drivers/SPI/base.py
+++ b/bibliopixel/drivers/SPI/base.py
@@ -1,6 +1,7 @@
 from .. channel_order import ChannelOrder
 from .. driver_base import DriverBase
 from ... util.colors import gamma as _gamma
+from ... util import deprecate
 from . import interfaces
 
 
@@ -20,11 +21,14 @@ class SPIBase(DriverBase):
     """
     def __init__(self, num, dev='/dev/spidev0.0',
                  spi_interface=None, spi_speed=1,
-                 interface='FILE',  # DEPRECATED
-                 **kwargs):
+                 interface=None, **kwargs):
         super().__init__(num, **kwargs)
         from ...project.types.spi_interface import make
         # See https://github.com/ManiacalLabs/BiblioPixel/issues/419
+        if interface:
+            deprecate.deprecate('SPIBase.interface')
+        else:
+            interface = 'FILE'
 
         maker = interfaces._SPI_INTERFACES[make(spi_interface or interface)]
         self._interface = maker(dev=dev, spi_speed=spi_speed)
diff --git a/bibliopixel/layout/circle.py b/bibliopixel/layout/circle.py
index 34c8842..20c5bba 100644
--- a/bibliopixel/layout/circle.py
+++ b/bibliopixel/layout/circle.py
@@ -2,6 +2,7 @@ from . layout import Layout
 from . geometry.circle import (
     make_circle_coord_map, calc_ring_steps, calc_ring_pixel_count,
     make_circle_coord_map_positions)
+from .. util import deprecate
 
 
 class Circle(Layout):
@@ -23,9 +24,6 @@ class Circle(Layout):
             self.ringSteps = calc_ring_steps(self.rings)
 
         self.ringCount = len(self.rings)
-        # DEPRECATED
-        self.lastRing = self.ringCount - 1
-
         self.set_pixel_positions(make_circle_coord_map_positions(self.rings))
 
         num = calc_ring_pixel_count(self.rings)
@@ -38,7 +36,12 @@ class Circle(Layout):
                 "Total ring LED count does not equal driver LED count!")
 
     @property
-    def dimensions(self):
+    def lastRing(self):
+        deprecate.deprecate('Circle.lastRing')
+        return self.ringCount - 1
+
+    @property
+    def shape(self):
         """Returns ``ringCount, ringSteps``."""
         return self.ringCount, self.ringSteps
 
diff --git a/bibliopixel/layout/cube.py b/bibliopixel/layout/cube.py
index 824e93e..62fc87c 100644
--- a/bibliopixel/layout/cube.py
+++ b/bibliopixel/layout/cube.py
@@ -33,7 +33,7 @@ class Cube(Layout):
         return make_cube_coord_map_positions(self.coord_map)
 
     @property
-    def dimensions(self):
+    def shape(self):
         """Returns ``x, y, z``"""
         return self.x, self.y, self.z
 
diff --git a/bibliopixel/layout/layout.py b/bibliopixel/layout/layout.py
index 39130a8..fff16bf 100644
--- a/bibliopixel/layout/layout.py
+++ b/bibliopixel/layout/layout.py
@@ -63,7 +63,8 @@ class Layout(object):
             d.set_pixel_positions(pixel_positions)
 
     def update(self):
-        """DEPRECATED - use :py:func:`push_to_driver`"""
+        """DEPRECATED: Use :py:func:`push_to_driver` instead"""
+        util.deprecate.deprecate('Layout.update')
         return self.push_to_driver()
 
     def start(self):
@@ -80,7 +81,7 @@ class Layout(object):
         self.threading.wait_for_update()
 
     @property
-    def dimensions(self):
+    def shape(self):
         """
         Return a tuple indicating the dimensions of the layout - (x,) for a
         strip, (x, y) for an array, (x, y, z) for a cube, and
@@ -88,6 +89,11 @@ class Layout(object):
         """
         raise NotImplementedError
 
+    @property
+    def dimensions(self):
+        util.deprecate.deprecate('Layout.dimensions')
+        return self.shape
+
     @property
     def color_list(self):
         return self._colors
@@ -133,7 +139,8 @@ class Layout(object):
         self._colors[:] = buf
 
     def setBuffer(self, buf):
-        """DEPRECATED!"""
+        util.deprecate.deprecate('layout.setBuffer')
+
         # https://stackoverflow.com/questions/1624883
         self.set_colors(buf=list(zip(*(iter(buf),) * 3)))
 
diff --git a/bibliopixel/layout/matrix.py b/bibliopixel/layout/matrix.py
index 8e8d529..7e421b5 100644
--- a/bibliopixel/layout/matrix.py
+++ b/bibliopixel/layout/matrix.py
@@ -113,7 +113,7 @@ class Matrix(MultiLayout):
         self.fonts = font.fonts
 
     @property
-    def dimensions(self):
+    def shape(self):
         """Returns ``width, height``"""
         return self.width, self.height
 
diff --git a/bibliopixel/layout/strip.py b/bibliopixel/layout/strip.py
index 5c6c539..e635f77 100644
--- a/bibliopixel/layout/strip.py
+++ b/bibliopixel/layout/strip.py
@@ -34,7 +34,7 @@ class Strip(MultiLayout):
         self.set_pixel_positions(make_strip_coord_map_positions(self.numLEDs))
 
     @property
-    def dimensions(self):
+    def shape(self):
         """Returns a 1-tuple with the length of the strip"""
         return self.numLEDs,
 
diff --git a/bibliopixel/main/commands.py b/bibliopixel/main/commands.py
index 9848079..d163273 100644
--- a/bibliopixel/main/commands.py
+++ b/bibliopixel/main/commands.py
@@ -50,8 +50,8 @@ Examples of flags are ``-v``, ``--verbose``, ``--defaults``, ``-d``,
 ``--simpixel``.  Flags either start with ``-``, for one letter flags, or ``--``
 for multi-letter flags.
 
-Sometimes two flags have the same meaning, like ``--simpixel`` and ``-s``,
-``--verbose`` and ``-v``, or ``--dimensions`` and ``--dim``.
+Sometimes two flags have the same meaning, like ``--simpixel`` and ``-s``
+or ``--verbose`` and ``-v``.
 
 Some flags take an argument, like ``--dim=160`` or ``--loglevel=frame``.
 Other flags do not, like ``--verbose`` or ``-v``.
diff --git a/bibliopixel/main/common_flags.py b/bibliopixel/main/common_flags.py
index 373b970..6d1cb03 100644
--- a/bibliopixel/main/common_flags.py
+++ b/bibliopixel/main/common_flags.py
@@ -1,6 +1,6 @@
 import os
 from .. project import data_maker, defaults, project
-from .. util import json, log
+from .. util import deprecate, json, log
 
 """Common command line arguments for run and demo."""
 
@@ -20,6 +20,9 @@ NUMBER_TYPES = ('python',) + data_maker.NUMPY_TYPES
 
 
 def add_common_flags(parser):
+    parser.add_argument(
+        '--deprecate', choices=deprecate.CHOICES, default=deprecate.DEFAULT,
+        help=deprecate.HELP)
     parser.add_argument(
         '--loglevel', choices=log.SORTED_NAMES, default='info',
         help=LOGLEVEL_HELP)
@@ -29,6 +32,15 @@ def add_common_flags(parser):
         '--version', action='store_true', help=VERSION_HELP)
 
 
+def execute_args(args):
+    if args.verbose and args.loglevel != 'frame':
+        log.set_log_level('debug')
+    else:
+        log.set_log_level(args.loglevel)
+
+    deprecate.ACTION = args.deprecate
+
+
 def add_project_flags(parser):
     parser.add_argument(
         '-a', '--animation', default=None,
@@ -44,6 +56,10 @@ def add_project_flags(parser):
 
     parser.add_argument(
         '--dimensions', '--dim', default=None,
+        help='DEPRECATED: x, (x, y) or (x, y, z) dimensions for project')
+
+    parser.add_argument(
+        '--shape', default=None,
         help='x, (x, y) or (x, y, z) dimensions for project')
 
     parser.add_argument(
@@ -134,11 +150,15 @@ def _make_project_flags(args):
         animation['length'] = length
 
     if args.dimensions is not None:
-        dimensions = args.dimensions.split(',')
+        deprecate.deprecate('Use --shape: --dimensions')
+
+    shape = args.shape or args.dimensions
+    if shape is not None:
+        shape = shape.split(',')
         try:
-            project_flags['dimensions'] = [int(i) for i in dimensions]
+            project_flags['shape'] = [int(i) for i in shape]
         except:
-            raise ValueError('--dimensions must be one to three numbers '
+            raise ValueError('--shape must be one to three numbers '
                              'separated by commas.')
 
     return project_flags
diff --git a/bibliopixel/main/main.py b/bibliopixel/main/main.py
index b024b0e..61e22f9 100644
--- a/bibliopixel/main/main.py
+++ b/bibliopixel/main/main.py
@@ -52,6 +52,7 @@ def get_args(argv=sys.argv):
 
     if argv == ['-h']:
         log.printer(commands.HELP)
+
     return parser.parse_args(argv)
 
 
@@ -68,10 +69,7 @@ def main():
         log.printer('    bp --help')
         sys.exit(-1)
 
-    if args.verbose and args.loglevel != 'frame':
-        log.set_log_level('debug')
-    else:
-        log.set_log_level(args.loglevel)
+    common_flags.execute_args(args)
 
     try:
         return run(args) or 0
diff --git a/bibliopixel/main/set.py b/bibliopixel/main/set.py
index b7d109b..6403518 100644
--- a/bibliopixel/main/set.py
+++ b/bibliopixel/main/set.py
@@ -12,7 +12,7 @@ section=value
 Example:
 
 ``````
-bp set numbers=float dimensions=80
+bp set numbers=float shape=80
 ``````
 """
 
diff --git a/bibliopixel/project/cleanup.py b/bibliopixel/project/cleanup.py
index eb3ca1a..7c2fa14 100644
--- a/bibliopixel/project/cleanup.py
+++ b/bibliopixel/project/cleanup.py
@@ -1,6 +1,6 @@
 import copy
 from . import alias_lists, construct, merge
-from .. import layout
+from .. import layout, util
 
 DEFAULT_DRIVERS = [construct.to_type('simpixel')]
 
@@ -71,19 +71,23 @@ def cleanup_drivers(desc):
     return desc
 
 
-def cleanup_dimensions(desc):
+def cleanup_shape(desc):
     desc = copy.deepcopy(desc)
     dimensions = desc.pop('dimensions', None)
-    if not dimensions:
+    if dimensions:
+        util.deprecate.deprecate('Project section "dimensions"')
+
+    shape = desc.pop('shape', dimensions)
+    if not shape:
         return desc
 
     if len(desc['drivers']) != 1:
         raise ValueError('Cannot use dimensions with more than one driver')
 
     try:
-        d = [int(dimensions)]
+        d = [int(shape)]
     except:
-        d = list(dimensions)
+        d = list(shape)
 
     ldesc = construct.to_type_constructor(desc.get('layout') or {})
     driver = desc['drivers'][0]
@@ -113,7 +117,7 @@ def cleanup(desc):
     desc = cleanup_aliases(desc)
     desc = cleanup_animation(desc)
     desc = cleanup_drivers(desc)
-    desc = cleanup_dimensions(desc)  # Must come after cleanup_drivers
+    desc = cleanup_shape(desc)  # Must come after cleanup_drivers
     desc = cleanup_numbers(desc)
 
     return desc
diff --git a/bibliopixel/project/event_queue.py b/bibliopixel/project/event_queue.py
index daff434..f114434 100644
--- a/bibliopixel/project/event_queue.py
+++ b/bibliopixel/project/event_queue.py
@@ -5,7 +5,7 @@ class EventQueue(queue.Queue):
     """
     ``Event``s are thread-unsafe operations to be executed later at a
     thread-safe time.  In the animation loop, this happens in
-    :py:method:`bibliopixel.animation.BaseAnimation.preframe_callback`.
+    :py:method:`bibliopixel.animation.Animation.preframe_callback`.
 
     The typical `Event` is a change to some parameter of an animation,
     but in general is represented by a call to a function with fixed parameters.
diff --git a/bibliopixel/project/merge.py b/bibliopixel/project/merge.py
index bdba41b..a2c9c6e 100644
--- a/bibliopixel/project/merge.py
+++ b/bibliopixel/project/merge.py
@@ -13,7 +13,7 @@ DEFAULT_PROJECT = collections.OrderedDict((
     ('maker', 'bibliopixel.project.data_maker.Maker'),
     ('driver', {}),
     ('drivers', []),
-    ('dimensions', ()),
+    ('shape', ()),
     ('layout', {}),
     ('run', {}),
     ('animation', {}),
@@ -22,7 +22,7 @@ DEFAULT_PROJECT = collections.OrderedDict((
 PROJECT_SECTIONS = tuple(DEFAULT_PROJECT.keys())
 
 NOT_MERGEABLE = (
-    'controls', 'datatype', 'dimensions', 'drivers', 'numbers', 'path',
+    'controls', 'datatype', 'shape', 'drivers', 'numbers', 'path',
     'typename')
 
 SECTION_ISNT_DICT_ERROR = 'Project section "%s" is %s, should be dictionary'
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index 5191f49..6e37cd2 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -2,7 +2,7 @@ import queue
 from . import (
     attributes, construct, cleanup, defaults, event_queue, load, recurse)
 from .. util import exception, json
-from .. animation import BaseAnimation, failed
+from .. animation import Animation, failed
 
 EVENT_QUEUE_MAXSIZE = 1000
 
diff --git a/bibliopixel/util/attribute_dict.py b/bibliopixel/util/attribute_dict.py
index 96824a9..e9c58bf 100644
--- a/bibliopixel/util/attribute_dict.py
+++ b/bibliopixel/util/attribute_dict.py
@@ -1,3 +1,6 @@
+from . import deprecate
+
+
 class AttributeDict(object):
     """
     A dict that exposes its values as attributes.
@@ -6,6 +9,7 @@ class AttributeDict(object):
     """
 
     def __init__(self, *args, **kwds):
+        deprecate.deprecate('util.AttributeDict')
         for k, v in dict(*args, **kwds).items():
             setattr(self, k, v)
 
diff --git a/bibliopixel/util/deprecate.py b/bibliopixel/util/deprecate.py
new file mode 100644
index 0000000..af8d2b1
--- /dev/null
+++ b/bibliopixel/util/deprecate.py
@@ -0,0 +1,30 @@
+from . import log
+
+CHOICES = 'ignore', 'fail', 'warn', 'warn_once'
+DEFAULT = 'warn'
+
+# This gets changed by --deprecate
+ACTION = DEFAULT
+
+HELP = """
+Specify what to do when a project uses deprecated features:
+
+  ignore: do nothing
+  warn: print warning messages for each feature
+  warn_once: print a warning message, but only once for each type of feature
+  fail: throw an exception
+"""
+
+DEPRECATED = set()
+
+
+def deprecate(msg, *args, **kwds):
+    if ACTION == 'ignore' or (ACTION == 'warn_once' and msg in DEPRECATED):
+        return
+
+    formatted = (msg + ' is DEPRECATED').format(*args, **kwds)
+    if ACTION == 'fail':
+        raise ValueError(formatted)
+
+    DEPRECATED.add(msg)
+    log.warning(formatted)
diff --git a/bibliopixel/util/image/gif.py b/bibliopixel/util/image/gif.py
index bf5d6cd..70a4a9d 100644
--- a/bibliopixel/util/image/gif.py
+++ b/bibliopixel/util/image/gif.py
@@ -1,5 +1,5 @@
 import os, pathlib
-from .. import log
+from .. import log, deprecate
 
 
 # PIL has a limit on how many image files can be open at once - you get an
@@ -79,16 +79,22 @@ in BiblioPixelAnimations.matrix.ImageAnim
 
 def convert_mode(image, mode='RGB'):
     """Return an image in the given mode."""
+    deprecate.deprecate('util.gif.convert_model')
+
     return image if (image.mode == mode) else image.convert(mode=mode)
 
 
 def image_to_colorlist(image, container=list):
     """Given a PIL.Image, returns a ColorList of its pixels."""
+    deprecate.deprecate('util.gif.image_to_colorlist')
+
     return container(convert_mode(image).getdata())
 
 
 def animated_gif_to_colorlists(image, container=list):
     """Given an animated GIF, return a list with a colorlist for each frame."""
+    deprecate.deprecate('util.gif.animated_gif_to_colorlists')
+
     from PIL import ImageSequence
 
     it = ImageSequence.Iterator(image)
diff --git a/bibliopixel/util/image/renderer.py b/bibliopixel/util/image/renderer.py
index afce68f..0c5dff6 100644
--- a/bibliopixel/util/image/renderer.py
+++ b/bibliopixel/util/image/renderer.py
@@ -9,17 +9,17 @@ def renderer(layout, color, pixel_width, pixel_height,
 
     from PIL import Image, ImageDraw
 
-    dims = layout.dimensions
-    if len(dims) == 1:
+    shape = layout.shape
+    if len(shape) == 1:
         if vertical:
-            dims, getter = (1, dims[0]), lambda x, y: layout.get(y)
+            shape, getter = (1, shape[0]), lambda x, y: layout.get(y)
         else:
-            dims, getter = (dims[0], 1), lambda x, y: layout.get(x)
+            shape, getter = (shape[0], 1), lambda x, y: layout.get(x)
     else:
         getter = layout.get
 
     # LED width and height
-    width, height = dims
+    width, height = shape
 
     # width and height for individual pixels
     pw, ph = pixel_width, pixel_width
diff --git a/bibliopixel/util/log.py b/bibliopixel/util/log.py
index 7c18912..da39075 100644
--- a/bibliopixel/util/log.py
+++ b/bibliopixel/util/log.py
@@ -124,5 +124,8 @@ frame, debug, info, warning, error = (
 # re-assign this variable if you need to redirect your printing.
 printer = print
 
-# DEPRECATED
-setLogLevel = set_log_level
+
+def setLogLevel(level):
+    from . import deprecate
+    deprecate.deprecate('util.setLogLevel')
+    set_log_level(level)
diff --git a/bibliopixel/util/threads/animation_threading.py b/bibliopixel/util/threads/animation_threading.py
index 203200f..3e43930 100644
--- a/bibliopixel/util/threads/animation_threading.py
+++ b/bibliopixel/util/threads/animation_threading.py
@@ -5,7 +5,7 @@ from .. import log
 class AnimationThreading(object):
     """
     AnimationThreading handles threading - and eventually multiprocessing - for
-    BaseAnimation.
+    Animation.
     """
 
     def __init__(self, runner, run):


Test patch:
diff --git a/test/bibliopixel/control/address_test.py b/test/bibliopixel/control/address_test.py
index fe7dbef..181faa4 100644
--- a/test/bibliopixel/control/address_test.py
+++ b/test/bibliopixel/control/address_test.py
@@ -4,9 +4,14 @@ from bibliopixel.control.address import Address
 
 class AddressTest(unittest.TestCase):
 
-    def test_empty_error(self):
+    def test_empty(self):
+        address = Address('')
+        self.assertFalse(address)
+        self.assertFalse(address.address)
+        self.assertFalse(address.assignment)
+        self.assertEqual(address.get(23), 23)
         with self.assertRaises(ValueError):
-            Address('')
+            address.set(self, 23)
 
     def test_attrib(self):
         address = Address('attr')
diff --git a/test/bibliopixel/project/cleanup_test.py b/test/bibliopixel/project/cleanup_test.py
index 816af52..1c45a6c 100644
--- a/test/bibliopixel/project/cleanup_test.py
+++ b/test/bibliopixel/project/cleanup_test.py
@@ -58,7 +58,7 @@ class CleanupTest(unittest.TestCase):
         actual = cleanup_after(copy.deepcopy(source))
         expected = {
             'aliases': {},
-            'dimensions': (),
+            'shape': (),
             'driver': {},
             'drivers': [],
             'layout': {
@@ -73,7 +73,7 @@ class CleanupTest(unittest.TestCase):
         self.assertEqual(actual, dict(BASE, **expected))
 
     def test_1_dimension(self):
-        source = {'dimensions': 3}
+        source = {'shape': 3}
         actual = cleanup_before(source)
         expected = {
             'drivers': [{'num': 3, 'typename': 'simpixel'}],
@@ -81,12 +81,12 @@ class CleanupTest(unittest.TestCase):
         }
         self.assertEqual(actual, dict(BASE, **expected))
 
-        source = {'dimensions': [3]}
+        source = {'shape': [3]}
         actual = cleanup_before(source)
         self.assertEqual(actual, dict(BASE, **expected))
 
-    def test_2_dimensions(self):
-        source = {'dimensions': [2, 5]}
+    def test_2_shape(self):
+        source = {'shape': [2, 5]}
         actual = cleanup_before(source)
         expected = {
             'drivers': [{'num': 10, 'typename': 'simpixel'}],
@@ -94,8 +94,8 @@ class CleanupTest(unittest.TestCase):
         }
         self.assertEqual(actual, dict(BASE, **expected))
 
-    def test_3_dimensions(self):
-        source = {'dimensions': [2, 3, 7]}
+    def test_3_shape(self):
+        source = {'shape': [2, 3, 7]}
         actual = cleanup_before(source)
         expected = {
             'drivers': [{'num': 42, 'typename': 'simpixel'}],
diff --git a/test/bibliopixel/project/defaults_test.py b/test/bibliopixel/project/defaults_test.py
index 5509ead..8396107 100644
--- a/test/bibliopixel/project/defaults_test.py
+++ b/test/bibliopixel/project/defaults_test.py
@@ -16,12 +16,12 @@ class DefaultsTest(unittest.TestCase):
         self.run_failure()
 
     def test_json(self):
-        self.run_test({"dimensions": 5}, '{"dimensions": 5}')
-        self.run_failure('{"dimensionX": 5}')
+        self.run_test({"shape": 5}, '{"shape": 5}')
+        self.run_failure('{"shapeX": 5}')
 
     def test_sections1(self):
-        self.run_test({"dimensions": 5}, 'dimensions=5')
-        self.run_failure('dimensionX=5')
+        self.run_test({"shape": 5}, 'shape=5')
+        self.run_failure('shapeX=5')
 
     def test_sections2(self):
         data = 'animation={"typename": "feedback", "master": 2}'
diff --git a/test/bibliopixel/project/merge_test.py b/test/bibliopixel/project/merge_test.py
index 10b0404..2e4195a 100644
--- a/test/bibliopixel/project/merge_test.py
+++ b/test/bibliopixel/project/merge_test.py
@@ -32,7 +32,7 @@ class MergeTest(unittest.TestCase):
             'aliases': {},
             'animation': {'typename': 'bfoo'},
             'controls': [],
-            'dimensions': (),
+            'shape': (),
             'driver': {'typename': 'bar'},
             'drivers': ['bang', 'bop'],
             'layout': {},
diff --git a/test/bibliopixel/project/project_test.py b/test/bibliopixel/project/project_test.py
index c8e6867..2bda4b2 100644
--- a/test/bibliopixel/project/project_test.py
+++ b/test/bibliopixel/project/project_test.py
@@ -290,7 +290,7 @@ PROJECT_ANIMATION = """
 """
 
 
-class AnimationTest(animation.BaseAnimation):
+class AnimationTest(animation.Animation):
     PROJECT = {
         "driver": "dummy",
         "layout": {