Add more property aliases and do them properly (no more lambdas and
string lookups) which means we can remove `_alias`. This commit also
defines `__slots__` for all classes which should prevent assignation of
invalid attributes with an AttributeError (more friendly than silently
doing the wrong thing). Finally, it cleans up all the property defs to
use Ben's preferred decorator style.
This commit is contained in:
Dave Jones
2015-10-17 20:29:33 +01:00
parent cc79749758
commit 6583223299
3 changed files with 138 additions and 74 deletions

View File

@@ -47,7 +47,11 @@ class GPIODevice(object):
The GPIO pin (in BCM numbering) that the device is connected to. If The GPIO pin (in BCM numbering) that the device is connected to. If
this is `None` a `GPIODeviceError` will be raised. this is `None` a `GPIODeviceError` will be raised.
""" """
__slots__ = ('_pin', '_active_state', '_inactive_state')
def __init__(self, pin=None): def __init__(self, pin=None):
super(GPIODevice, self).__init__()
# self._pin must be set before any possible exceptions can be raised # self._pin must be set before any possible exceptions can be raised
# because it's accessed in __del__. However, it mustn't be given the # because it's accessed in __del__. However, it mustn't be given the
# value of pin until we've verified that it isn't already allocated # value of pin until we've verified that it isn't already allocated

View File

@@ -13,16 +13,6 @@ from spidev import SpiDev
from .devices import GPIODeviceError, GPIODeviceClosed, GPIODevice, GPIOQueue from .devices import GPIODeviceError, GPIODeviceClosed, GPIODevice, GPIOQueue
def _alias(key, doc=None):
if doc is None:
doc = 'Alias for %s' % key
return property(
lambda self: getattr(self, key),
lambda self, val: setattr(self, key, val),
doc=doc
)
class InputDeviceError(GPIODeviceError): class InputDeviceError(GPIODeviceError):
pass pass
@@ -45,6 +35,13 @@ class InputDevice(GPIODevice):
If `True`, the pin will be pulled high with an internal resistor. If If `True`, the pin will be pulled high with an internal resistor. If
`False` (the default), the pin will be pulled low. `False` (the default), the pin will be pulled low.
""" """
__slots__ = (
'_pull_up',
'_active_edge',
'_inactive_edge',
)
def __init__(self, pin=None, pull_up=False): def __init__(self, pin=None, pull_up=False):
if pin in (2, 3) and not pull_up: if pin in (2, 3) and not pull_up:
raise InputDeviceError( raise InputDeviceError(
@@ -105,6 +102,15 @@ class WaitableInputDevice(InputDevice):
Note that this class provides no means of actually firing its events; it's Note that this class provides no means of actually firing its events; it's
effectively an abstract base class. effectively an abstract base class.
""" """
__slots__ = (
'_active_event',
'_inactive_event',
'_when_activated',
'_when_deactivated',
'_last_state',
)
def __init__(self, pin=None, pull_up=False): def __init__(self, pin=None, pull_up=False):
super(WaitableInputDevice, self).__init__(pin, pull_up) super(WaitableInputDevice, self).__init__(pin, pull_up)
self._active_event = Event() self._active_event = Event()
@@ -135,13 +141,9 @@ class WaitableInputDevice(InputDevice):
""" """
return self._inactive_event.wait(timeout) return self._inactive_event.wait(timeout)
def _get_when_activated(self): @property
return self._when_activated def when_activated(self):
"""
def _set_when_activated(self, value):
self._when_activated = self._wrap_callback(value)
when_activated = property(_get_when_activated, _set_when_activated, doc="""\
The function to run when the device changes state from inactive to The function to run when the device changes state from inactive to
active. active.
@@ -154,15 +156,16 @@ class WaitableInputDevice(InputDevice):
Set this property to `None` (the default) to disable the event. Set this property to `None` (the default) to disable the event.
See also: when_deactivated. See also: when_deactivated.
""") """
return self._when_activated
def _get_when_deactivated(self): @when_activated.setter
return self._when_deactivated def when_activated(self, value):
self._when_activated = self._wrap_callback(value)
def _set_when_deactivated(self, value): @property
self._when_deactivated = self._wrap_callback(value) def when_deactivated(self):
"""
when_deactivated = property(_get_when_deactivated, _set_when_deactivated, doc="""\
The function to run when the device changes state from active to The function to run when the device changes state from active to
inactive. inactive.
@@ -175,7 +178,12 @@ class WaitableInputDevice(InputDevice):
Set this property to `None` (the default) to disable the event. Set this property to `None` (the default) to disable the event.
See also: when_activated. See also: when_activated.
""") """
return self._when_deactivated
@when_deactivated.setter
def when_deactivated(self, value):
self._when_deactivated = self._wrap_callback(value)
def _wrap_callback(self, fn): def _wrap_callback(self, fn):
if fn is None: if fn is None:
@@ -240,6 +248,9 @@ class DigitalInputDevice(WaitableInputDevice):
ignore changes in state after an initial change. This defaults to ignore changes in state after an initial change. This defaults to
`None` which indicates that no bounce compensation will be performed. `None` which indicates that no bounce compensation will be performed.
""" """
__slots__ = ()
def __init__(self, pin=None, pull_up=False, bounce_time=None): def __init__(self, pin=None, pull_up=False, bounce_time=None):
super(DigitalInputDevice, self).__init__(pin, pull_up) super(DigitalInputDevice, self).__init__(pin, pull_up)
try: try:
@@ -290,6 +301,9 @@ class SmoothedInputDevice(WaitableInputDevice):
If `True`, a value will be returned immediately, but be aware that this If `True`, a value will be returned immediately, but be aware that this
value is likely to fluctuate excessively. value is likely to fluctuate excessively.
""" """
__slots__ = ('_queue', '_threshold', '__weakref__')
def __init__( def __init__(
self, pin=None, pull_up=False, threshold=0.5, self, pin=None, pull_up=False, threshold=0.5,
queue_len=5, sample_wait=0.0, partial=False): queue_len=5, sample_wait=0.0, partial=False):
@@ -357,20 +371,21 @@ class SmoothedInputDevice(WaitableInputDevice):
self._check_open() self._check_open()
return self._queue.value return self._queue.value
def _get_threshold(self): @property
def threshold(self):
"""
If `value` exceeds this amount, then `is_active` will return `True`.
"""
return self._threshold return self._threshold
def _set_threshold(self, value): @threshold.setter
def threshold(self, value):
if not (0.0 < value < 1.0): if not (0.0 < value < 1.0):
raise InputDeviceError( raise InputDeviceError(
'threshold must be between zero and one exclusive' 'threshold must be between zero and one exclusive'
) )
self._threshold = float(value) self._threshold = float(value)
threshold = property(_get_threshold, _set_threshold, doc="""\
If `value` exceeds this amount, then `is_active` will return `True`.
""")
@property @property
def is_active(self): def is_active(self):
return self.value > self.threshold return self.value > self.threshold
@@ -384,16 +399,17 @@ class Button(DigitalInputDevice):
side of the switch, and ground to the other (the default `pull_up` value side of the switch, and ground to the other (the default `pull_up` value
is `True`). is `True`).
""" """
__slots__ = ()
def __init__(self, pin=None, pull_up=True, bouncetime=None): def __init__(self, pin=None, pull_up=True, bouncetime=None):
super(Button, self).__init__(pin, pull_up, bouncetime) super(Button, self).__init__(pin, pull_up, bouncetime)
is_pressed = _alias('is_active') Button.is_pressed = Button.is_active
Button.when_pressed = Button.when_activated
when_pressed = _alias('when_activated') Button.when_released = Button.when_deactivated
when_released = _alias('when_deactivated') Button.wait_for_press = Button.wait_for_active
Button.wait_for_release = Button.wait_for_inactive
wait_for_press = _alias('wait_for_active')
wait_for_release = _alias('wait_for_inactive')
class MotionSensor(SmoothedInputDevice): class MotionSensor(SmoothedInputDevice):
@@ -410,6 +426,9 @@ class MotionSensor(SmoothedInputDevice):
particularly "jittery" you may wish to set this to a higher value (e.g. 5) particularly "jittery" you may wish to set this to a higher value (e.g. 5)
to mitigate this. to mitigate this.
""" """
__slots__ = ()
def __init__( def __init__(
self, pin=None, queue_len=1, sample_rate=10, threshold=0.5, self, pin=None, queue_len=1, sample_rate=10, threshold=0.5,
partial=False): partial=False):
@@ -423,13 +442,11 @@ class MotionSensor(SmoothedInputDevice):
self.close() self.close()
raise raise
motion_detected = _alias('is_active') MotionSensor.motion_detected = MotionSensor.is_active
MotionSensor.when_motion = MotionSensor.when_activated
when_motion = _alias('when_activated') MotionSensor.when_no_motion = MotionSensor.when_deactivated
when_no_motion = _alias('when_deactivated') MotionSensor.wait_for_motion = MotionSensor.wait_for_active
MotionSensor.wait_for_no_motion = MotionSensor.wait_for_inactive
wait_for_motion = _alias('wait_for_active')
wait_for_no_motion = _alias('wait_for_inactive')
class LightSensor(SmoothedInputDevice): class LightSensor(SmoothedInputDevice):
@@ -441,6 +458,12 @@ class LightSensor(SmoothedInputDevice):
class repeatedly discharges the capacitor, then times the duration it takes class repeatedly discharges the capacitor, then times the duration it takes
to charge (which will vary according to the light falling on the LDR). to charge (which will vary according to the light falling on the LDR).
""" """
__slots__ = (
'_charge_time_limit',
'_charged',
)
def __init__( def __init__(
self, pin=None, queue_len=5, charge_time_limit=0.01, self, pin=None, queue_len=5, charge_time_limit=0.01,
threshold=0.1, partial=False): threshold=0.1, partial=False):
@@ -478,13 +501,11 @@ class LightSensor(SmoothedInputDevice):
self.charge_time_limit self.charge_time_limit
) )
light_detected = _alias('is_active') LightSensor.light_detected = LightSensor.is_active
LightSensor.when_light = LightSensor.when_activated
when_light = _alias('when_activated') LightSensor.when_dark = LightSensor.when_deactivated
when_dark = _alias('when_deactivated') LightSensor.wait_for_light = LightSensor.wait_for_active
LightSensor.wait_for_dark = LightSensor.wait_for_inactive
wait_for_light = _alias('wait_for_active')
wait_for_dark = _alias('wait_for_inactive')
class TemperatureSensor(W1ThermSensor): class TemperatureSensor(W1ThermSensor):
@@ -500,6 +521,13 @@ class AnalogInputDevice(object):
""" """
Represents an analog input device connected to SPI (serial interface). Represents an analog input device connected to SPI (serial interface).
""" """
__slots__ = (
'_device',
'_bits',
'_spi',
)
def __init__(self, device=0, bits=None): def __init__(self, device=0, bits=None):
if bits is None: if bits is None:
raise InputDeviceError('you must specify the bit resolution of the device') raise InputDeviceError('you must specify the bit resolution of the device')
@@ -539,6 +567,9 @@ class AnalogInputDevice(object):
class MCP3008(AnalogInputDevice): class MCP3008(AnalogInputDevice):
__slots__ = ('_channel')
def __init__(self, device=0, channel=0): def __init__(self, device=0, channel=0):
if not 0 <= channel < 8: if not 0 <= channel < 8:
raise InputDeviceError('channel must be between 0 and 7') raise InputDeviceError('channel must be between 0 and 7')
@@ -571,6 +602,9 @@ class MCP3008(AnalogInputDevice):
class MCP3004(MCP3008): class MCP3004(MCP3008):
__slots__ = ()
def __init__(self, device=0, channel=0): def __init__(self, device=0, channel=0):
# MCP3004 protocol is identical to MCP3008 but the top bit of the # MCP3004 protocol is identical to MCP3008 but the top bit of the
# channel number must be 0 (effectively restricting it to 4 channels) # channel number must be 0 (effectively restricting it to 4 channels)

View File

@@ -25,6 +25,9 @@ class OutputDevice(GPIODevice):
`False`, the `on` method will set the GPIO to LOW (the `off` method `False`, the `on` method will set the GPIO to LOW (the `off` method
always does the opposite). always does the opposite).
""" """
__slots__ = ('_active_high')
def __init__(self, pin=None, active_high=True): def __init__(self, pin=None, active_high=True):
self._active_high = active_high self._active_high = active_high
super(OutputDevice, self).__init__(pin) super(OutputDevice, self).__init__(pin)
@@ -83,6 +86,9 @@ class DigitalOutputDevice(OutputDevice):
optional background thread to handle toggling the device state without optional background thread to handle toggling the device state without
further interaction. further interaction.
""" """
__slots__ = ('_blink_thread', '_lock')
def __init__(self, pin=None, active_high=True): def __init__(self, pin=None, active_high=True):
super(DigitalOutputDevice, self).__init__(pin, active_high) super(DigitalOutputDevice, self).__init__(pin, active_high)
self._blink_thread = None self._blink_thread = None
@@ -165,7 +171,9 @@ class LED(DigitalOutputDevice):
anode (long leg) of the LED, and the cathode (short leg) to ground, with anode (long leg) of the LED, and the cathode (short leg) to ground, with
an optional resistor to prevent the LED from burning out. an optional resistor to prevent the LED from burning out.
""" """
pass __slots__ = ()
LED.is_lit = LED.is_active
class Buzzer(DigitalOutputDevice): class Buzzer(DigitalOutputDevice):
@@ -175,13 +183,16 @@ class Buzzer(DigitalOutputDevice):
A typical configuration of such a device is to connect a GPIO pin to the A typical configuration of such a device is to connect a GPIO pin to the
anode (long leg) of the buzzer, and the cathode (short leg) to ground. anode (long leg) of the buzzer, and the cathode (short leg) to ground.
""" """
pass __slots__ = ()
class PWMOutputDevice(DigitalOutputDevice): class PWMOutputDevice(DigitalOutputDevice):
""" """
Generic Output device configured for PWM (Pulse-Width Modulation). Generic Output device configured for PWM (Pulse-Width Modulation).
""" """
__slots__ = ('_pwm', '_frequency', '_value')
def __init__(self, pin=None, frequency=100): def __init__(self, pin=None, frequency=100):
self._pwm = None self._pwm = None
super(PWMOutputDevice, self).__init__(pin) super(PWMOutputDevice, self).__init__(pin)
@@ -214,35 +225,35 @@ class PWMOutputDevice(DigitalOutputDevice):
self._pwm.ChangeDutyCycle(value * 100) self._pwm.ChangeDutyCycle(value * 100)
self._value = value self._value = value
def _get_value(self): @property
return self._read() def value(self):
"""
def _set_value(self, value):
self._stop_blink()
self._write(value)
value = property(_get_value, _set_value, doc="""\
The duty cycle of the PWM device. 0.0 is off, 1.0 is fully on. Values The duty cycle of the PWM device. 0.0 is off, 1.0 is fully on. Values
in between may be specified for varying levels of power in the device. in between may be specified for varying levels of power in the device.
""" """
) return self._read()
@value.setter
def value(self, value):
self._stop_blink()
self._write(value)
@property @property
def is_active(self): def is_active(self):
return self.value > 0.0 return self.value > 0.0
def _get_frequency(self): @property
return self._frequency def frequency(self):
"""
def _set_frequency(self, value):
self._pwm.ChangeFrequency(value)
self._frequency = value
frequency = property(_get_frequency, _set_frequency, doc="""\
The frequency of the pulses used with the PWM device, in Hz. The The frequency of the pulses used with the PWM device, in Hz. The
default is 100. default is 100.
""" """
) return self._frequency
@frequency.setter
def frequency(self, value):
self._pwm.ChangeFrequency(value)
self._frequency = value
def _led_property(index, doc=None): def _led_property(index, doc=None):
@@ -265,6 +276,9 @@ class RGBLED(object):
blue: `None` blue: `None`
The GPIO pin that controls the blue component of the RGB LED. The GPIO pin that controls the blue component of the RGB LED.
""" """
__slots__ = ('_leds')
def __init__(self, red=None, green=None, blue=None): def __init__(self, red=None, green=None, blue=None):
self._leds = tuple(PWMOutputDevice(pin) for pin in (red, green, blue)) self._leds = tuple(PWMOutputDevice(pin) for pin in (red, green, blue))
@@ -274,6 +288,13 @@ class RGBLED(object):
@property @property
def color(self): def color(self):
"""
Set the color of the LED from an RGB 3-tuple of `(red, green, blue)`
where each value between 0 and 1.
For example, purple would be `(1, 0, 1)` and yellow would be `(1, 1,
0)`, while orange would be `(1, 0.5, 0)`.
"""
return (self.red, self.green, self.blue) return (self.red, self.green, self.blue)
@color.setter @color.setter
@@ -282,13 +303,15 @@ class RGBLED(object):
def on(self): def on(self):
""" """
Turn the device on Turn the device on. This equivalent to setting the device color to
white `(1, 1, 1)`.
""" """
self.color = (1, 1, 1) self.color = (1, 1, 1)
def off(self): def off(self):
""" """
Turn the device off Turn the device off. This is equivalent to setting the device color
to black `(0, 0, 0)`.
""" """
self.color = (0, 0, 0) self.color = (0, 0, 0)
@@ -307,6 +330,9 @@ class Motor(object):
""" """
Generic bi-directional motor. Generic bi-directional motor.
""" """
__slots__ = ('_forward', '_backward')
def __init__(self, forward=None, back=None): def __init__(self, forward=None, back=None):
if not all([forward, back]): if not all([forward, back]):
raise GPIODeviceError('forward and back pins must be provided') raise GPIODeviceError('forward and back pins must be provided')