Repository: ManiacalLabs/BiblioPixel (python)

Issue:
Race condition in websocket code.
I wasn't able to reproduce this a second time, but reading the code makes it obvious that this exception is quite possible because there is no lock protecting the clients...

    DEBUG - websocket - Connected:('127.0.0.1', 50114)
    Traceback (most recent call last):
      File "/development/env/bp/bin/bp", line 6, in <module>
        exec(compile(open(__file__).read(), __file__, 'exec'))
      File "/development/BiblioPixel/scripts/bp", line 15, in <module>
        main.main()
      File "/development/BiblioPixel/bibliopixel/main/main.py", line 75, in main
        return run(args) or 0
      File "/development/BiblioPixel/bibliopixel/main/run.py", line 141, in run
        _run_projects(projects, args)
      File "/development/BiblioPixel/bibliopixel/main/run.py", line 111, in _run_projects
        project.run()
      File "/development/BiblioPixel/bibliopixel/project/project.py", line 85, in run
        self.start()
      File "/development/BiblioPixel/bibliopixel/project/project.py", line 75, in start
        self.animation.start()
      File "/development/BiblioPixel/bibliopixel/animation/animation.py", line 87, in start
        self.threading.start()
      File "/development/BiblioPixel/bibliopixel/util/threads/animation_threading.py", line 72, in start
        self.target()
      File "/development/BiblioPixel/bibliopixel/util/threads/animation_threading.py", line 32, in target
        self.run()
      File "/development/BiblioPixel/bibliopixel/animation/animation.py", line 90, in run_all_frames
        for i in self.generate_frames():
      File "/development/BiblioPixel/bibliopixel/animation/animation.py", line 96, in generate_frames
        self._run_one_frame()
      File "/development/BiblioPixel/bibliopixel/animation/animation.py", line 123, in _run_one_frame
        self.layout.push_to_driver()
      File "/development/BiblioPixel/bibliopixel/layout/layout.py", line 127, in push_to_driver
        self.threading.push_to_driver()
      File "/development/BiblioPixel/bibliopixel/util/threads/update_threading.py", line 93, in push_to_driver
        self.update_colors()
      File "/development/BiblioPixel/bibliopixel/util/threads/update_threading.py", line 83, in update_colors
        d.update_colors()
      File "/development/BiblioPixel/bibliopixel/drivers/driver_base.py", line 155, in update_colors
        self._send_packet()
      File "/development/BiblioPixel/bibliopixel/drivers/SimPixel/driver.py", line 41, in _send_packet
        self.server.update(pixels=self._buf)
      File "/development/BiblioPixel/bibliopixel/drivers/SimPixel/websocket.py", line 72, in update
        for client in self.clients:
    RuntimeError: Set changed size during iteration


Pull request description:
Copy websocket.Server.clients before iterating over it (fix #656)
`set.copy` is thread-safe - see https://stackoverflow.com/a/46335759/43839 - so this should fix the problem though I wasn't able to reproduce the issue again.

Patch:
diff --git a/bibliopixel/__init__.py b/bibliopixel/__init__.py
index ee0a2dd..bab3491 100644
--- a/bibliopixel/__init__.py
+++ b/bibliopixel/__init__.py
@@ -9,10 +9,11 @@ from . util.colors import gamma
 from . drivers import return_codes
 from . project import data_maker
 
-# These are DEPRECATED
-from . layout import LEDCircle, LEDMatrix, LEDPOV, LEDStrip, LEDCube
-from . layout import matrix_drawing as matrix
-from . layout.geometry.rotation import Rotation
+from . util import deprecated
+if deprecated.allowed():
+    from . layout import LEDCircle, LEDMatrix, LEDPOV, LEDStrip, LEDCube
+    from . layout import matrix_drawing as matrix
+    from . layout.geometry.rotation import Rotation
 
 
 def _get_version():
diff --git a/bibliopixel/animation/__init__.py b/bibliopixel/animation/__init__.py
index 700c208..7d57005 100644
--- a/bibliopixel/animation/__init__.py
+++ b/bibliopixel/animation/__init__.py
@@ -9,11 +9,11 @@ from . strip import BaseStripAnim
 from . receiver import BaseReceiver
 from . tests import StripChannelTest, MatrixChannelTest, MatrixCalibrationTest
 
-
-# DEPRECATED
-strip_test = StripChannelTest
-matrix_calibration = MatrixCalibrationTest
-matrix_test = MatrixChannelTest
-from . off import OffAnim
-from . animation import BaseAnimation
-from . circle import BaseCircleAnim
+from .. util import deprecated
+if deprecated.allowed():
+    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 44cd1b8..23cf931 100644
--- a/bibliopixel/animation/animation.py
+++ b/bibliopixel/animation/animation.py
@@ -1,6 +1,6 @@
 import contextlib, threading, time
 from . runner import Runner, STATE
-from .. util import deprecate, log
+from .. util import deprecated, log
 from .. util.threads.animation_threading import AnimationThreading
 from .. project import attributes, fields
 
@@ -97,7 +97,7 @@ class Animation(object):
                 yield
 
     def run(self, **kwds):
-        deprecate.deprecate('BaseAnimation.run')
+        deprecated.deprecated('BaseAnimation.run')
         self._set_runner(kwds)
         self.start()
 
@@ -143,7 +143,8 @@ class Animation(object):
         self.runner.run_start_time = time.time()
         self.threading.stop_event.clear()
 
-        self._step = 0  # DEPRECATED
+        if deprecated.allowed():
+            self._step = 0
         self.cur_step = 0
         self.cycle_count = 0
 
@@ -164,8 +165,8 @@ class Animation(object):
         self.threading = AnimationThreading(self.runner, self.run_all_frames)
 
 
-# DEPRECATED
-BaseAnimation = Animation
+if deprecated.allowed():
+    BaseAnimation = Animation
 
 
 def _report_framerate(timestamps):
diff --git a/bibliopixel/animation/circle.py b/bibliopixel/animation/circle.py
index 62c1caf..2300a2f 100644
--- a/bibliopixel/animation/circle.py
+++ b/bibliopixel/animation/circle.py
@@ -1,5 +1,5 @@
 from . animation import Animation
-from .. layout import Circle
+from .. layout import Circle as CircleLayout
 
 
 class Circle(Animation):
@@ -7,9 +7,10 @@ class Circle(Animation):
     def __init__(self, layout, **kwds):
         super().__init__(layout, **kwds)
 
-        if not isinstance(layout, Circle):
-            raise RuntimeError('Must use bibliopixel.layout.Circle with ' +
-                               'Circle Animations!')
+        if not isinstance(layout, CircleLayout):
+            raise RuntimeError(
+                'Must use %s with Circle animations, not %s' %
+                CircleLayout, type(layout))
 
         self.rings = layout.rings
         self.ringCount = layout.ringCount
@@ -17,5 +18,6 @@ class Circle(Animation):
         self.ringSteps = layout.ringSteps
 
 
-# DEPRECATED
-BaseCircleAnim = Circle
+from .. util import deprecated
+if deprecated.allowed():
+    BaseCircleAnim = Circle
diff --git a/bibliopixel/animation/collection.py b/bibliopixel/animation/collection.py
index b1aef01..74f930c 100644
--- a/bibliopixel/animation/collection.py
+++ b/bibliopixel/animation/collection.py
@@ -1,7 +1,7 @@
 import os, traceback
 from .. project import aliases, construct, load, project
 from . import animation
-from .. util import deprecate, json, log
+from .. util import json, log
 
 
 class Collection(animation.Animation):
@@ -61,7 +61,9 @@ class Collection(animation.Animation):
         super().cleanup(clean_layout)
 
     def add_animation(self, anim, **kwds):
-        deprecate.deprecate('Collection.add_animation')
+        from .. util import deprecated
+        deprecated.deprecated('Collection.add_animation')
+
         anim.set_runner(kwds)
         self.animations.append(anim)
 
diff --git a/bibliopixel/animation/off.py b/bibliopixel/animation/off.py
index 30e325f..8393470 100644
--- a/bibliopixel/animation/off.py
+++ b/bibliopixel/animation/off.py
@@ -12,5 +12,6 @@ class Off(Animation):
         self.layout.all_off()
 
 
-# DEPRECATED
-OffAnim = Off
+from .. util import deprecated
+if deprecated.allowed():
+    OffAnim = Off
diff --git a/bibliopixel/animation/receiver.py b/bibliopixel/animation/receiver.py
index b265b3c..84f2dbe 100644
--- a/bibliopixel/animation/receiver.py
+++ b/bibliopixel/animation/receiver.py
@@ -67,5 +67,6 @@ class Receiver(Animation):
             self._hold_for_data.clear()
 
 
-# DEPRECATED
-BaseReceiver = Receiver
+from .. util import deprecated
+if deprecated.allowed():
+    BaseReceiver = Receiver
diff --git a/bibliopixel/colors/__init__.py b/bibliopixel/colors/__init__.py
index fc2ac4e..c48ba11 100644
--- a/bibliopixel/colors/__init__.py
+++ b/bibliopixel/colors/__init__.py
@@ -1,3 +1,3 @@
-# This is DEPRECATED
-
-from .. util.colors import *  # noqa: F403
+from .. util import deprecated
+if deprecated.allowed():
+    from .. util.colors import *  # noqa: F403
diff --git a/bibliopixel/control/serial_gamepad.py b/bibliopixel/control/serial_gamepad.py
index 23a5532..77eb195 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, deprecate, generate_header, log
+from .. util import AttributeDict, generate_header, log
 from .. drivers.return_codes import RETURN_CODES, print_error
 from . gamepad import BaseGamePad
 
@@ -49,7 +49,8 @@ class SerialGamePad(BaseGamePad):
             self._com.close()
 
     def close(self):
-        deprecate.deprecate('SerialGamePad.close')
+        from .. util import deprecated
+        deprecated.deprecated('SerialGamePad.close')
         self.cleanup()
 
     @staticmethod
diff --git a/bibliopixel/drivers/SPI/base.py b/bibliopixel/drivers/SPI/base.py
index 77633cf..6dd53df 100644
--- a/bibliopixel/drivers/SPI/base.py
+++ b/bibliopixel/drivers/SPI/base.py
@@ -1,7 +1,6 @@
 from .. channel_order import ChannelOrder
 from .. driver_base import DriverBase
 from ... util.colors import gamma as _gamma
-from ... util import deprecate
 from . import interfaces
 
 
@@ -26,7 +25,8 @@ class SPIBase(DriverBase):
         from ...project.types.spi_interface import make
         # See https://github.com/ManiacalLabs/BiblioPixel/issues/419
         if interface:
-            deprecate.deprecate('SPIBase.interface')
+            from ... util import deprecated
+            deprecated.deprecated('SPIBase.interface')
         else:
             interface = 'FILE'
 
diff --git a/bibliopixel/drivers/SimPixel/driver.py b/bibliopixel/drivers/SimPixel/driver.py
index 807538d..0a77191 100644
--- a/bibliopixel/drivers/SimPixel/driver.py
+++ b/bibliopixel/drivers/SimPixel/driver.py
@@ -41,5 +41,6 @@ class SimPixel(ServerDriver):
         self.server.update(pixels=self._buf)
 
 
-# This is DEPRECATED.
-DriverSimPixel = SimPixel
+from ... util import deprecated
+if deprecated.allowed():
+    DriverSimPixel = SimPixel
diff --git a/bibliopixel/drivers/SimPixel/websocket.py b/bibliopixel/drivers/SimPixel/websocket.py
index 9cf758c..4a9c463 100644
--- a/bibliopixel/drivers/SimPixel/websocket.py
+++ b/bibliopixel/drivers/SimPixel/websocket.py
@@ -69,7 +69,7 @@ class Server:
 
     def update(self, **state):
         self.state.update(state)
-        for client in self.clients:
+        for client in self.clients.copy():
             client.update(**state)
 
     def add_client(self, client):
diff --git a/bibliopixel/drivers/dummy.py b/bibliopixel/drivers/dummy.py
index eabb117..36d478b 100644
--- a/bibliopixel/drivers/dummy.py
+++ b/bibliopixel/drivers/dummy.py
@@ -23,5 +23,6 @@ class Dummy(DriverBase):
             pass
 
 
-# This is DEPRECATED.
-DriverDummy = Dummy
+from .. util import deprecated
+if deprecated.allowed():
+    DriverDummy = Dummy
diff --git a/bibliopixel/drivers/dummy_driver.py b/bibliopixel/drivers/dummy_driver.py
index 868cf49..abd68f8 100644
--- a/bibliopixel/drivers/dummy_driver.py
+++ b/bibliopixel/drivers/dummy_driver.py
@@ -1,4 +1,4 @@
-# This whole file is DEPRECATED.
-
-from . dummy import Dummy
-DriverDummy = Dummy
+from .. util import deprecated
+if deprecated.allowed():
+    from . dummy import Dummy
+    DriverDummy = Dummy
diff --git a/bibliopixel/drivers/hue.py b/bibliopixel/drivers/hue.py
index f8cbfe8..92c2e0d 100644
--- a/bibliopixel/drivers/hue.py
+++ b/bibliopixel/drivers/hue.py
@@ -79,5 +79,6 @@ class Hue(DriverBase):
             self._bridge.set_light(self._ids[i], cmd)
 
 
-# This is DEPRECATED.
-DriverHue = Hue
+from .. util import deprecated
+if deprecated.allowed():
+    DriverHue = Hue
diff --git a/bibliopixel/drivers/mirror.py b/bibliopixel/drivers/mirror.py
index 023b8b9..6b4c753 100644
--- a/bibliopixel/drivers/mirror.py
+++ b/bibliopixel/drivers/mirror.py
@@ -44,5 +44,6 @@ class Mirror(DriverBase):
         self.update_colors = forward('update_colors')
 
 
-# DEPRECATED
-MirrorDriver = Mirror
+from .. util import deprecated
+if deprecated.allowed():
+    MirrorDriver = Mirror
diff --git a/bibliopixel/drivers/network.py b/bibliopixel/drivers/network.py
index 75d7cab..bc2472a 100644
--- a/bibliopixel/drivers/network.py
+++ b/bibliopixel/drivers/network.py
@@ -85,5 +85,6 @@ class Network(DriverBase):
         return resp == RETURN_CODES.SUCCESS
 
 
-# This is DEPRECATED.
-DriverNetwork = Network
+from .. util import deprecated
+if deprecated.allowed():
+    DriverNetwork = Network
diff --git a/bibliopixel/drivers/serial/driver.py b/bibliopixel/drivers/serial/driver.py
index af75230..8f76352 100644
--- a/bibliopixel/drivers/serial/driver.py
+++ b/bibliopixel/drivers/serial/driver.py
@@ -192,10 +192,6 @@ class Serial(DriverBase):
             log.error('Serial exception %s in write', e)
 
 
-# This is DEPRECATED.
-DriverSerial = Serial
-
-
 class TeensySmartMatrix(Serial):
     """Variant of :py:class:`Serial` for use with the Teensy and
     SmartMatrix library. The following provides compatible firmware:
@@ -214,5 +210,7 @@ class TeensySmartMatrix(Serial):
         self.sync = self._send_sync
 
 
-# This is DEPRECATED.
-DriverTeensySmartMatrix = TeensySmartMatrix
+from ... util import deprecated
+if deprecated.allowed():
+    DriverSerial = Serial
+    DriverTeensySmartMatrix = TeensySmartMatrix
diff --git a/bibliopixel/layout/__init__.py b/bibliopixel/layout/__init__.py
index 4a2cf08..be00aba 100644
--- a/bibliopixel/layout/__init__.py
+++ b/bibliopixel/layout/__init__.py
@@ -7,10 +7,11 @@ from . geometry.matrix import make_matrix_coord_map
 from . geometry.circle import make_circle_coord_map
 from . geometry.cube import make_cube_coord_map
 
-# These are DEPRECATED
-from . circle import LEDCircle
-from . cube import LEDCube
-from . matrix import LEDMatrix
-from . pov import LEDPOV
-from . strip import LEDStrip
-from . geometry.rotation import Rotation
+from .. util import deprecated
+if deprecated.allowed():
+    from . circle import LEDCircle
+    from . cube import LEDCube
+    from . matrix import LEDMatrix
+    from . pov import LEDPOV
+    from . strip import LEDStrip
+    from . geometry.rotation import Rotation
diff --git a/bibliopixel/layout/circle.py b/bibliopixel/layout/circle.py
index 20c5bba..1574126 100644
--- a/bibliopixel/layout/circle.py
+++ b/bibliopixel/layout/circle.py
@@ -2,7 +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
+from .. util import deprecated
 
 
 class Circle(Layout):
@@ -37,7 +37,7 @@ class Circle(Layout):
 
     @property
     def lastRing(self):
-        deprecate.deprecate('Circle.lastRing')
+        deprecated.deprecated('Circle.lastRing')
         return self.ringCount - 1
 
     @property
@@ -128,5 +128,5 @@ class Circle(Layout):
             self._set_base(i, color)
 
 
-# This is DEPRECATED
-LEDCircle = Circle
+if deprecated.allowed():
+    LEDCircle = Circle
diff --git a/bibliopixel/layout/cube.py b/bibliopixel/layout/cube.py
index 62fc87c..c3fccca 100644
--- a/bibliopixel/layout/cube.py
+++ b/bibliopixel/layout/cube.py
@@ -60,5 +60,6 @@ class Cube(Layout):
         self._set(x, y, z, color)
 
 
-# This is DEPRECATED
-LEDCube = Cube
+from .. util import deprecated
+if deprecated.allowed():
+    LEDCube = Cube
diff --git a/bibliopixel/layout/geometry/__init__.py b/bibliopixel/layout/geometry/__init__.py
index 0344fdc..bcce4e1 100644
--- a/bibliopixel/layout/geometry/__init__.py
+++ b/bibliopixel/layout/geometry/__init__.py
@@ -1,8 +1,17 @@
-from . circle import (calc_ring_pixel_count, calc_ring_steps, make_circle_coord_map,
-                      make_circle_coord_map_positions)
+from . circle import (
+    calc_ring_pixel_count, calc_ring_steps, make_circle_coord_map,
+    make_circle_coord_map_positions)
+
 from . cube import make_cube_coord_map, make_cube_coord_map_positions
-from . matrix import make_matrix_coord_map, make_matrix_coord_map_multi, make_matrix_coord_map_positions
-from . strip import make_strip_coord_map, make_strip_coord_map_multi, make_strip_coord_map_positions
 
-# DEPRECATED
-from . rotation import Rotation, rotate_and_flip
+from . matrix import (
+    make_matrix_coord_map, make_matrix_coord_map_multi,
+    make_matrix_coord_map_positions)
+
+from . strip import (
+    make_strip_coord_map, make_strip_coord_map_multi,
+    make_strip_coord_map_positions)
+
+from ... util import deprecated
+if deprecated.allowed():
+    from . rotation import Rotation, rotate_and_flip
diff --git a/bibliopixel/layout/geometry/rotation.py b/bibliopixel/layout/geometry/rotation.py
index 4a24709..9558f4b 100644
--- a/bibliopixel/layout/geometry/rotation.py
+++ b/bibliopixel/layout/geometry/rotation.py
@@ -12,9 +12,10 @@ def rotate_and_flip(coord_map, rotation, flip):
     return coord_map
 
 
-# This is DEPRECATED
-class Rotation(IntEnum):
-    ROTATE_0 = 0  # no rotation
-    ROTATE_90 = 90  # rotate 90 degrees
-    ROTATE_180 = 180  # rotate 180 degrees
-    ROTATE_270 = 270  # rotate 270 degrees
+from ... util import deprecated
+if deprecated.allowed():
+    class Rotation(IntEnum):
+        ROTATE_0 = 0  # no rotation
+        ROTATE_90 = 90  # rotate 90 degrees
+        ROTATE_180 = 180  # rotate 180 degrees
+        ROTATE_270 = 270  # rotate 270 degrees
diff --git a/bibliopixel/layout/layout.py b/bibliopixel/layout/layout.py
index fff16bf..da7d1a6 100644
--- a/bibliopixel/layout/layout.py
+++ b/bibliopixel/layout/layout.py
@@ -64,7 +64,7 @@ class Layout(object):
 
     def update(self):
         """DEPRECATED: Use :py:func:`push_to_driver` instead"""
-        util.deprecate.deprecate('Layout.update')
+        util.deprecated.deprecated('Layout.update')
         return self.push_to_driver()
 
     def start(self):
@@ -91,7 +91,7 @@ class Layout(object):
 
     @property
     def dimensions(self):
-        util.deprecate.deprecate('Layout.dimensions')
+        util.deprecated.deprecated('Layout.dimensions')
         return self.shape
 
     @property
@@ -139,7 +139,7 @@ class Layout(object):
         self._colors[:] = buf
 
     def setBuffer(self, buf):
-        util.deprecate.deprecate('layout.setBuffer')
+        util.deprecated.deprecated('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 7e421b5..2e37f0c 100644
--- a/bibliopixel/layout/matrix.py
+++ b/bibliopixel/layout/matrix.py
@@ -1,6 +1,6 @@
 import math, threading, time
 
-from .. util import colors, log
+from .. util import colors, deprecated, log
 from . import matrix_drawing as md
 from . import font
 from . layout import MultiLayout
@@ -321,7 +321,8 @@ class Matrix(MultiLayout):
         """
         md.fill_triangle(self.set, x0, y0, x1, y1, x2, y2, color, aa)
 
-    fillTrangle = fillTriangle  # DEPRECATED!
+    if deprecated.allowed():
+        fillTrangle = fillTriangle
 
     def drawChar(self, x, y, c, color, bg,
                  aa=False, font=font.default_font, font_scale=1):
@@ -346,5 +347,5 @@ class Matrix(MultiLayout):
                      x, y, color, bg, aa, font, font_scale)
 
 
-# This is DEPRECATED
-LEDMatrix = Matrix
+if deprecated.allowed():
+    LEDMatrix = Matrix
diff --git a/bibliopixel/layout/pov.py b/bibliopixel/layout/pov.py
index 49998d5..aeabe9e 100644
--- a/bibliopixel/layout/pov.py
+++ b/bibliopixel/layout/pov.py
@@ -40,5 +40,6 @@ class POV(Matrix):
                 time.sleep(max(0, sleep - sendTime))
 
 
-# This is DEPRECATED
-LEDPOV = POV
+from .. util import deprecated
+if deprecated.allowed():
+    LEDPOV = POV
diff --git a/bibliopixel/layout/strip.py b/bibliopixel/layout/strip.py
index e635f77..60e86e1 100644
--- a/bibliopixel/layout/strip.py
+++ b/bibliopixel/layout/strip.py
@@ -82,5 +82,6 @@ class Strip(MultiLayout):
         self.set(pixel, (0, 0, 0))
 
 
-# This is DEPRECATED
-LEDStrip = Strip
+from .. util import deprecated
+if deprecated.allowed():
+    LEDStrip = Strip
diff --git a/bibliopixel/main/common_flags.py b/bibliopixel/main/common_flags.py
index 6d1cb03..693f1ae 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 deprecate, json, log
+from .. util import deprecated, json, log
 
 """Common command line arguments for run and demo."""
 
@@ -20,9 +20,6 @@ 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)
@@ -38,8 +35,6 @@ def execute_args(args):
     else:
         log.set_log_level(args.loglevel)
 
-    deprecate.ACTION = args.deprecate
-
 
 def add_project_flags(parser):
     parser.add_argument(
@@ -54,9 +49,10 @@ def add_project_flags(parser):
         '-d', '--defaults', default=None, nargs='*',
         action='append', help='Use this default setting')
 
-    parser.add_argument(
-        '--dimensions', '--dim', default=None,
-        help='DEPRECATED: x, (x, y) or (x, y, z) dimensions for project')
+    if deprecated.allowed():
+        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,
@@ -150,7 +146,7 @@ def _make_project_flags(args):
         animation['length'] = length
 
     if args.dimensions is not None:
-        deprecate.deprecate('Use --shape: --dimensions')
+        deprecated.deprecated('Use --shape: --dimensions')
 
     shape = args.shape or args.dimensions
     if shape is not None:
diff --git a/bibliopixel/project/cleanup.py b/bibliopixel/project/cleanup.py
index 7c2fa14..488134e 100644
--- a/bibliopixel/project/cleanup.py
+++ b/bibliopixel/project/cleanup.py
@@ -75,9 +75,9 @@ def cleanup_shape(desc):
     desc = copy.deepcopy(desc)
     dimensions = desc.pop('dimensions', None)
     if dimensions:
-        util.deprecate.deprecate('Project section "dimensions"')
+        util.deprecated.deprecated('Project section "dimensions"')
 
-    shape = desc.pop('shape', dimensions)
+    shape = desc.pop('shape', None) or dimensions
     if not shape:
         return desc
 
@@ -89,7 +89,8 @@ def cleanup_shape(desc):
     except:
         d = list(shape)
 
-    ldesc = construct.to_type_constructor(desc.get('layout') or {})
+    ldesc = construct.to_type_constructor(desc.get('layout') or {},
+                                          python_path='bibliopixel.layout')
     driver = desc['drivers'][0]
 
     if len(d) == 1:
diff --git a/bibliopixel/project/merge.py b/bibliopixel/project/merge.py
index a2c9c6e..fcdf779 100644
--- a/bibliopixel/project/merge.py
+++ b/bibliopixel/project/merge.py
@@ -22,7 +22,7 @@ DEFAULT_PROJECT = collections.OrderedDict((
 PROJECT_SECTIONS = tuple(DEFAULT_PROJECT.keys())
 
 NOT_MERGEABLE = (
-    'controls', 'datatype', 'shape', 'drivers', 'numbers', 'path',
+    'controls', 'datatype', 'dimensions', 'shape', 'drivers', 'numbers', 'path',
     'typename')
 
 SECTION_ISNT_DICT_ERROR = 'Project section "%s" is %s, should be dictionary'
diff --git a/bibliopixel/util/__init__.py b/bibliopixel/util/__init__.py
index cbaea42..d5ca5fc 100644
--- a/bibliopixel/util/__init__.py
+++ b/bibliopixel/util/__init__.py
@@ -1,5 +1,6 @@
 from . util import generate_header, even_dist, pointOnCircle, genVector
-from . attribute_dict import AttributeDict
 
-# DEPRECATED
-d = AttributeDict
+from . import deprecated
+if deprecated.allowed():
+    from . attribute_dict import AttributeDict
+    d = AttributeDict
diff --git a/bibliopixel/util/attribute_dict.py b/bibliopixel/util/attribute_dict.py
index e9c58bf..df6b248 100644
--- a/bibliopixel/util/attribute_dict.py
+++ b/bibliopixel/util/attribute_dict.py
@@ -1,28 +1,28 @@
-from . import deprecate
+from . import deprecated
 
+if deprecated.allowed():
+    class AttributeDict(object):
+        """
+        A dict that exposes its values as attributes.
 
-class AttributeDict(object):
-    """
-    A dict that exposes its values as attributes.
+        DEPRECATED: use argparse.Namespace, or just a dict.
+        """
 
-    DEPRECATED: use argparse.Namespace, or just a dict.
-    """
+        def __init__(self, *args, **kwds):
+            deprecated.deprecated('util.AttributeDict')
+            for k, v in dict(*args, **kwds).items():
+                setattr(self, k, v)
 
-    def __init__(self, *args, **kwds):
-        deprecate.deprecate('util.AttributeDict')
-        for k, v in dict(*args, **kwds).items():
-            setattr(self, k, v)
+        def __setattr__(self, k, v):
+            if isinstance(v, dict):
+                v = AttributeDict(**v)
+            super().__setattr__(k, v)
 
-    def __setattr__(self, k, v):
-        if isinstance(v, dict):
-            v = AttributeDict(**v)
-        super().__setattr__(k, v)
+        def __eq__(self, other):
+            return self.__dict__ == other.__dict__
 
-    def __eq__(self, other):
-        return self.__dict__ == other.__dict__
+        def __ne__(self, other):
+            return self.__dict__ != other.__dict__
 
-    def __ne__(self, other):
-        return self.__dict__ != other.__dict__
-
-    def __bool__(self):
-        return bool(self.__dict__)
+        def __bool__(self):
+            return bool(self.__dict__)
diff --git a/bibliopixel/util/colors/__init__.py b/bibliopixel/util/colors/__init__.py
index 0f31e59..05f05e7 100644
--- a/bibliopixel/util/colors/__init__.py
+++ b/bibliopixel/util/colors/__init__.py
@@ -52,9 +52,11 @@ from . conversions import hue_helper, hue_helper360
 from . wheel import wheel_color, wheel_helper
 from . names import COLORS, COLOR_DICT, toggle
 
-# DEPRECATED methods and module name
-hex2rgb = COLORS.__getitem__
-from . import conversions as hue
+from .. import deprecated
 
-# Legacy color names.  DEPRECATED
-from . classic import *  # noqa: F403
+if deprecated.allowed():
+    hex2rgb = COLORS.__getitem__
+    from . import conversions as hue
+
+    # Legacy color names
+    from . classic import *  # noqa: F403
diff --git a/bibliopixel/util/deprecate.py b/bibliopixel/util/deprecate.py
deleted file mode 100644
index af8d2b1..0000000
--- a/bibliopixel/util/deprecate.py
+++ /dev/null
@@ -1,30 +0,0 @@
-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/deprecated.py b/bibliopixel/util/deprecated.py
new file mode 100644
index 0000000..c19b254
--- /dev/null
+++ b/bibliopixel/util/deprecated.py
@@ -0,0 +1,77 @@
+import os, sys
+
+CHOICES = 'ignore', 'fail', 'warn', 'warn_once'
+DEFAULT = 'warn_once'
+ACTION = None
+
+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()
+FLAG = '--deprecated'
+
+
+def allowed():
+    _compute_action()
+    return ACTION != 'fail'
+
+
+def deprecated(msg, *args, **kwds):
+    _compute_action()
+
+    if ACTION == 'ignore':
+        return
+    if ACTION == 'warn_once' and msg in DEPRECATED:
+        return
+
+    formatted = msg.format(*args, **kwds)
+    if ACTION == 'fail':
+        raise ValueError(formatted)
+
+    DEPRECATED.add(msg)
+
+    from . import log
+    log.warning(formatted)
+
+
+def _compute_action():
+    global ACTION
+    if ACTION:
+        return
+
+    if FLAG in sys.argv:
+        raise ValueError('%s needs an argument (one of %s)' %
+                         (FLAG, ', '.join(CHOICES)))
+
+    d = [i for i, v in enumerate(sys.argv) if v.startswith(FLAG + '=')]
+    if not d:
+        ACTION = DEFAULT
+
+    elif len(d) > 1:
+        raise ValueError('Only one %s argument can be used' % FLAG)
+
+    else:
+        arg = sys.argv.pop(d[0])
+        _, *rest = arg.split('=')
+
+        if len(rest) > 1:
+            raise ValueError('Extra = in flag %s' % arg)
+
+        if not (rest and rest[0].strip()):
+            raise ValueError('%s needs an argument (one of %s)' %
+                             (FLAG, ', '.join(CHOICES)))
+
+        if rest[0] not in CHOICES:
+            raise ValueError('Unknown argument to %s (must be one of %s)' %
+                             (FLAG, ', '.join(CHOICES)))
+            raise ValueError('Bad deprecated value %s' % rest[0])
+
+        ACTION = rest[0]
+
+    return ACTION
diff --git a/bibliopixel/util/image/__init__.py b/bibliopixel/util/image/__init__.py
index be3946d..b96e241 100644
--- a/bibliopixel/util/image/__init__.py
+++ b/bibliopixel/util/image/__init__.py
@@ -1,6 +1,8 @@
 from . load_image import loadImage
 from . reshape import crop, resize
 
-# DEPRECATED
-from . load_image import show_image, showImage, loadImage
-from . gif import animated_gif_to_colorlists, convert_mode, image_to_colorlist
+from .. import deprecated
+if deprecated.allowed():
+    from . load_image import show_image, showImage, loadImage
+    from . gif import (
+        animated_gif_to_colorlists, convert_mode, image_to_colorlist)
diff --git a/bibliopixel/util/image/gif.py b/bibliopixel/util/image/gif.py
index 70a4a9d..d713229 100644
--- a/bibliopixel/util/image/gif.py
+++ b/bibliopixel/util/image/gif.py
@@ -1,5 +1,5 @@
 import os, pathlib
-from .. import log, deprecate
+from .. import log
 
 
 # PIL has a limit on how many image files can be open at once - you get an
@@ -69,33 +69,32 @@ def write_animation_windowed(filename, frames, window_size, **gif_options):
 
 
 """
-DEPRECATED!
-
 This is probably not used and should likely be replaced by the code
 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)
-
+from .. import deprecated
+if deprecated.allowed():
+    def convert_mode(image, mode='RGB'):
+        """Return an image in the given mode."""
+        deprecated.deprecated('util.gif.convert_model')
 
-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 image if (image.mode == mode) else image.convert(mode=mode)
 
-    return container(convert_mode(image).getdata())
+    def image_to_colorlist(image, container=list):
+        """Given a PIL.Image, returns a ColorList of its pixels."""
+        deprecated.deprecated('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')
+    def animated_gif_to_colorlists(image, container=list):
+        """
+        Given an animated GIF, return a list with a colorlist for each frame.
+        """
+        deprecated.deprecated('util.gif.animated_gif_to_colorlists')
 
-    from PIL import ImageSequence
+        from PIL import ImageSequence
 
-    it = ImageSequence.Iterator(image)
-    return [image_to_colorlist(i, container) for i in it]
+        it = ImageSequence.Iterator(image)
+        return [image_to_colorlist(i, container) for i in it]
diff --git a/bibliopixel/util/log.py b/bibliopixel/util/log.py
index da39075..5be92f1 100644
--- a/bibliopixel/util/log.py
+++ b/bibliopixel/util/log.py
@@ -125,7 +125,9 @@ frame, debug, info, warning, error = (
 printer = print
 
 
-def setLogLevel(level):
-    from . import deprecate
-    deprecate.deprecate('util.setLogLevel')
-    set_log_level(level)
+from . import deprecated
+
+if deprecated.allowed():
+    def setLogLevel(level):
+        deprecated.deprecated('util.setLogLevel')
+        set_log_level(level)


Test patch:
diff --git a/test/bibliopixel/project/project_test.py b/test/bibliopixel/project/project_test.py
index 2bda4b2..acd3936 100644
--- a/test/bibliopixel/project/project_test.py
+++ b/test/bibliopixel/project/project_test.py
@@ -72,11 +72,8 @@ class ProjectTest(unittest.TestCase):
 
 PROJECT = """
 {
-    "driver": {
-        "typename": "dummy",
-        "num": 12
-    },
-
+    "driver": "dummy",
+    "dimensions": 12,
     "layout": "strip",
     "animation": "strip_test",
     "run": {
@@ -89,8 +86,6 @@ PROJECT_TYPES = """
 {
     "driver": {
         "typename": "dummy",
-        "num": 12,
-
         "c_order": "GBR",
         "color": "green",
         "duration": "1 hour, 2 minutes",
@@ -99,6 +94,7 @@ PROJECT_TYPES = """
         "ledtype": "GENERIC"
     },
 
+    "shape": 12,
     "layout": "strip",
     "animation": "strip_test",
     "run": {