Skip to content

Instantly share code, notes, and snippets.

@mnesarco
Created July 23, 2020 16:54
Show Gist options
  • Save mnesarco/e9440a196824af4bae439e4aeb4b6dcc to your computer and use it in GitHub Desktop.
Save mnesarco/e9440a196824af4bae439e4aeb4b6dcc to your computer and use it in GitHub Desktop.
Enhanced python properties with some metaprog.
# file: objects.py
# Copyright 2020 Frank David Martínez Muñoz (mnesarco)
# License: MIT
from typing import Union
__all__ = ('self_properties', 'properties')
def self_properties(self, scope: dict, exclude=(), save_args: bool = False):
"""Copies all items from `scope` to self as attributes with single underscore prefix.
:param self: instance ref.
:param scope: dictionary with attributes.
:param exclude: tuple with names to exclude from `scope`.
:param save_args: if True, sets self._args with a tuple with `(scope - exclude).values`
"""
if save_args:
args = []
for (k, v) in scope.items():
if k != 'self' and k not in exclude:
setattr(self, '_' + k, v)
args.append(v)
self._args = tuple(args)
else:
for (k, v) in scope.items():
if k != 'self' and k not in exclude:
setattr(self, '_' + k, v)
class properties:
"""
Utilities for building properties with extended features.
"""
__slots__ = ['_slots', '_scope', '_var', '_auto_dirty']
def __init__(self, scope: dict, var_name: str, auto_dirty: bool = False):
self._slots = []
self._scope = scope
self._var = var_name
self._auto_dirty = auto_dirty
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Export slots to scope
slots = self._scope.get('__slots__', None)
if slots is None:
self._scope['__slots__'] = tuple(self._slots)
elif isinstance(slots, list):
self._scope['__slots__'] += self._slots
elif isinstance(slots, tuple):
self._scope['__slots__'] = (*self._scope['__slots__'], *self._slots)
# Clean scope
if self._var in self._scope:
del self._scope[self._var]
# Clean references
self._scope = None
self._slots = None
self._var = None
def prop(self, read_only: bool = False, listener: Union[bool, str] = None, auto_dirty=False):
"""Decorator: Generates a property with additional features.
:param read_only: if True, only a getter is generated.
:param listener: if str, changes will fire `self.[listener]`, if bool, changes will fire `self._changed`
:param auto_dirty: if True, changes will set `self._is_dirty`
:return: property.
"""
auto_dirty = self._auto_dirty or auto_dirty
if auto_dirty and '_is_dirty' not in self._slots:
self._slots.append('_is_dirty')
def decorator(f):
field = '_' + f.__name__
if read_only and listener:
raise ValueError(f"property {field} cannot be read_only and observable at the same time.")
self._slots.append(field)
if read_only:
setter = None
else:
if listener:
listener_name = listener if isinstance(listener, str) else '_changed'
def setter(inst, new):
old = getattr(inst, field, None)
if old != new:
setattr(inst, field, new)
if auto_dirty:
inst._is_dirty = True
(getattr(inst, listener_name))(field, old, new)
else:
def setter(inst, new):
if getattr(inst, field, None) != new:
setattr(inst, field, new)
if auto_dirty:
inst._is_dirty = True
return property(
lambda inst: getattr(inst, field, f(inst)),
setter,
None,
f.__doc__
)
return decorator
# Example usage:
#
# from objects import *
#
# class Car:
# with properties(locals(), 'meta') as meta:
#
# @meta.prop(read_only=True)
# def brand(self) -> str:
# """Brand"""
#
# @meta.prop(read_only=True)
# def max_speed(self) -> float:
# """Maximum car speed"""
#
# @meta.prop(listener='_on_acceleration')
# def speed(self) -> float:
# "Speed of the car"""
# return 0 # Default stopped
#
# @meta.prop(listener='_on_off_listener')
# def on(self) -> bool:
# """Engine state"""
# return False
#
# def __init__(self, brand: str, max_speed: float = 200):
# self_properties(self, locals())
#
# def _on_off_listener(self, prop, old, on):
# if on:
# print(f"{self.brand} Turned on, Runnnnnn")
# else:
# self._speed = 0
# print(f"{self.brand} Turned off.")
#
# def _on_acceleration(self, prop, old, speed):
# if self.on:
# if speed > self.max_speed:
# print(f"{self.brand} {speed}km/h Bang! Engine exploded!")
# self.on = False
# else:
# print(f"{self.brand} New speed: {speed}km/h")
# else:
# print(f"{self.brand} Car is off, no speed change")
#
#
# mycar = Car('Ford')
#
# # Car is turned off
# for speed in range(0, 300, 50):
# mycar.speed = speed
#
# # Car is turned on
# mycar.on = True
# for speed in range(0, 350, 50):
# mycar.speed = speed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment