Package: property-manager
Theproperty-managerpackage defines several custompropertyvariants for
Python programming including required properties, writable properties, cached
properties, etc. It’s currently tested on Python 2.7, 3.5, 3.6, 3.7, 3.8 and
PyPy. For usage instructions please refer to thedocumentation.StatusInstallationUsageWritable propertiesRequired propertiesCached propertiesProperties based on environment variablesSupport for setters and deletersThePropertyManagerclassSimilar projectsDistinguishing featuresContactLicenseStatusTheproperty-managerpackage came into existence as a submodule of myexecutorpackage where I wanted to define classes with a lot of properties
that had a default value which was computed on demand but also needed to
support assignment to easily override the default value.Since I created that module I’d wanted to re-use it in a couple of other
projects I was working on, but adding anexecutordependency just for theproperty_managersubmodule felt kind of ugly.This is when I decided that it was time for theproperty-managerpackage to
be created. When I extracted the submodule fromexecutorI significantly
changed its implementation (making the code more robust and flexible) and
improved the tests, documentation and coverage in the process.InstallationTheproperty-managerpackage is available onPyPIwhich means installation
should be as simple as:$pipinstallproperty-managerThere’s actually a multitude of ways to install Python packages (e.g. theper
user site-packages directory,virtual environmentsor just installing
system wide) and I have no intention of getting into that discussion here, so
if this intimidates you then read up on your options before returning to these
instructions ;-).UsageThis section shows how to use the most useful property subclasses. Please refer
to thedocumentationfor more detailed information.Writable propertiesRequired propertiesCached propertiesProperties based on environment variablesSupport for setters and deletersThePropertyManagerclassWritable propertiesWritable properties with a computed default value are easy to create using thewritable_propertydecorator:fromrandomimportrandomfromproperty_managerimportwritable_propertyclassWritablePropertyDemo(object):@writable_propertydefchange_me(self):returnrandom()First let’s see how the computed default value behaves:>>> instance = WritablePropertyDemo()
>>> print(instance.change_me)
0.13692489329941815
>>> print(instance.change_me)
0.8664002331885933As you can see the value is recomputed each time. Now we’ll assign it a value:>>> instance.change_me = 42
>>> print(instance.change_me)
42From this point onwardschange_mewill be the number42and it’s impossible
to revert back to the computed value:>>> delattr(instance, 'change_me')
Traceback (most recent call last):
  File "property_manager/__init__.py", line 584, in __delete__
    raise AttributeError(msg % (obj.__class__.__name__, self.__name__))
AttributeError: 'WritablePropertyDemo' object attribute 'change_me' is read-onlyIf you’re looking for a property that supports both assignment and deletion
(clearing the assigned value) you can usemutable_property.Required propertiesTherequired_propertydecorator can be used to create required properties:fromproperty_managerimportPropertyManager,required_propertyclassRequiredPropertyDemo(PropertyManager):@required_propertydefimportant(self):"""A very important attribute."""What does it mean for a property to be required? Let’s create an instance of
the class and find out:>>> instance = RequiredPropertyDemo()
Traceback (most recent call last):
  File "property_manager/__init__.py", line 131, in __init__
    raise TypeError("%s (%s)" % (msg, concatenate(missing_properties)))
TypeError: missing 1 required argument (important)So the constructor of the class raises an exception when the property hasn’t
been given a value. We can give the property a value by providing keyword
arguments to the constructor:>>> instance = RequiredPropertyDemo(important=42)
>>> print(instance)
RequiredPropertyDemo(important=42)We can also assign a new value to the property:>>> instance.important = 13
>>> print(instance)
RequiredPropertyDemo(important=13)Cached propertiesTwo kinds of cached properties are supported, we’ll show both here:fromrandomimportrandomfromproperty_managerimportcached_property,lazy_propertyclassCachedPropertyDemo(object):@cached_propertydefexpensive(self):print("Calculating expensive property ..")returnrandom()@lazy_propertydefnon_idempotent(self):print("Calculating non-idempotent property ..")returnrandom()The properties created by thecached_propertydecorator compute the
property’s value on demand and cache the result:>>> instance = CachedPropertyDemo()
>>> print(instance.expensive)
Calculating expensive property ..
0.763863180683
>>> print(instance.expensive)
0.763863180683The property’s cached value can be invalidated in order to recompute its value:>>> del instance.expensive
>>> print(instance.expensive)
Calculating expensive property ..
0.396322737214
>>> print(instance.expensive)
0.396322737214Now that you understandcached_property, explaininglazy_propertyis very
simple: It simply doesn’t support invalidation of cached values! Here’s how
that works in practice:>>> instance.non_idempotent
Calculating non-idempotent property ..
0.27632566561900895
>>> instance.non_idempotent
0.27632566561900895
>>> del instance.non_idempotent
Traceback (most recent call last):
  File "property_manager/__init__.py", line 499, in __delete__
    raise AttributeError(msg % (obj.__class__.__name__, self.__name__))
AttributeError: 'CachedPropertyDemo' object attribute 'non_idempotent' is read-only
>>> instance.non_idempotent
0.27632566561900895Properties based on environment variablesThe constructor of thecustom_propertyclass (and its subclasses) accepts the
keyword argumentenvironment_variablewhich can be provided to get the
property’s value from the environment:fromrandomimportrandomfromproperty_managerimportmutable_propertyclassEnvironmentPropertyDemo(object):@mutable_property(environment_variable='WHATEVER_YOU_WANT')defenvironment_based(self):return'some-default-value'By default the property’s value is computed as expected:>>> instance = EnvironmentPropertyDemo()
>>> print(instance.environment_based)
some-default-valueWhen the environment variable is set it overrides the computed value:>>> os.environ['WHATEVER_YOU_WANT'] = '42'
>>> print(instance.environment_based)
42Assigning a value to the property overrides the value from the environment:>>> instance.environment_based = '13'
>>> print(instance.environment_based)
13Deleting the property clears the assigned value so that the property falls back
to the environment:>>> delattr(instance, 'environment_based')
>>> print(instance.environment_based)
42If we now clear the environment variable as well then the property falls back
to the computed value:>>> os.environ.pop('WHATEVER_YOU_WANT')
'42'
>>> print(instance.environment_based)
some-default-valueSupport for setters and deletersAll of the custom property classes support setters and deleters just like
Python’spropertydecorator does.ThePropertyManagerclassWhen you define a class that inherits from thePropertyManagerclass the
following behavior is made available to your class:Required properties raise an exception if they’re not set.The values of writable properties can be set by passing
keyword arguments to the constructor of your class.Therepr()of your objects will render the name of the class and the names
and values of all properties. Individual properties can easily be excluded
from therepr()output.Theclear_cached_properties()method can be used to invalidate the cached
values of all cached properties at once.Additionally you can use theproperty_manager.sphinxmodule as a Sphinx
extension to automatically generate boilerplate documentation that provides an
overview of base classes, properties, public methods and special methods.Similar projectsThe Python Package Index contains quite a few packages that provide custom
properties with similar semantics:cached-propertyMy personal favorite until I wrote my own :-). This package provides several
cached property variants. It supports threading and time based cache
invalidation whichproperty-managerdoesn’t support.lazy-propertyThis package provides two cached property variants: a read only property and
a writable property. Both variants cache computed values indefinitely.memoized-propertyThis package provides a single property variant which simply caches computed
values indefinitely.property-cachingThis package provides several cached property variants supporting class
properties, object properties and cache invalidation.propertylibThis package uses metaclasses to implement an alternative syntax for defining
computed properties. It defines several property variants with semantics that
are similar to those defined by theproperty-managerpackage.rwpropertyThis package implements computed, writable properties using an alternative
syntax to define the properties.Distinguishing featuresDespite all of the existing Python packages discussed above I decided to create
and publish theproperty-managerpackage because it was fun to get to know
Python’sdescriptor protocoland I had several features in mind I couldn’t
find anywhere else:A superclass that sets writable properties based on constructor arguments.A superclass that understands required properties and raises a clear
exception if a required property is not properly initialized.Clear disambiguation between lazy properties (whose computed value is cached
but cannot be invalidated because it would compromise internal state) and
cached properties (whose computed value is cached but can be invalidated to
compute a fresh value).An easy way to quickly invalidate all cached properties of an object.An easy way to change the semantics of custom properties, e.g. what if the
user wants a writable cached property? Withproperty-managerit is trivial
to define new property variants by combining existing semantics:fromproperty_managerimportcached_propertyclassWritableCachedPropertyDemo(object):@cached_property(writable=True)defexpensive_overridable_attribute(self):"""Expensive calculations go here."""The example above creates a new anonymous class and then immediately uses
that to decorate the method. We could have given the class a name though:fromproperty_managerimportcached_propertywritable_cached_property=cached_property(writable=True)classWritableCachedPropertyDemo(object):@writable_cached_propertydefexpensive_overridable_attribute(self):"""Expensive calculations go here."""By giving the new property variant a name it can be reused. We can go one
step further and properly document the new property variant:fromproperty_managerimportcached_propertyclasswritable_cached_property(cached_property):"""A cached property that supports assignment."""writable=TrueclassWritableCachedPropertyDemo(object):@writable_cached_propertydefexpensive_overridable_attribute(self):"""Expensive calculations go here."""I’ve used computed properties for years in Python and over those years I’ve
learned that different Python projects have different requirements from
custom property variants. Defining every possible permutation up front is
madness, but I think that the flexibility with which theproperty-managerpackage enables adaptation gets a long way. This was the one thing that
bothered me the most about all of the other Python packages that implement
property variants: They are not easily adapted to unanticipated use cases.ContactThe latest version ofproperty-manageris available onPyPIandGitHub. The
documentation is hosted onRead the Docsand includes achangelog. For bug
reports please create an issue onGitHub. If you have questions, suggestions,
etc. feel free to send me an e-mail atpeter@peterodding.com.LicenseThis software is licensed under theMIT license.© 2020 Peter Odding.