# Useful property variants for Python programming.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: March 2, 2020
# URL: https://property-manager.readthedocs.io
"""
Useful :class:`property` variants for Python programming.
Introduction
============
The :mod:`property_manager` module defines several :class:`property` variants
that implement Python's `descriptor protocol`_ to provide decorators that turn
methods into computed properties with several additional features.
Custom property types
---------------------
Here's an overview of the predefined property variants and their supported
operations:
========================== ========== ============ ========== =======
Variant Assignment Reassignment Deletion Caching
========================== ========== ============ ========== =======
:class:`custom_property` No No No No
:class:`writable_property` Yes Yes No No
:class:`mutable_property` Yes Yes Yes No
:class:`required_property` Yes Yes No No
:class:`key_property` Yes No No No
:class:`lazy_property` No No No Yes
:class:`cached_property` No No Yes Yes
========================== ========== ============ ========== =======
If you want a different combination of supported options (for example a cached
property that supports assignment) this is also possible, please take a look at
:class:`custom_property.__new__()`.
The following inheritance diagram shows how the predefined :class:`property`
variants relate to each other:
.. inheritance-diagram:: property_manager.custom_property \
property_manager.writable_property \
property_manager.mutable_property \
property_manager.required_property \
property_manager.key_property \
property_manager.lazy_property \
property_manager.cached_property
:parts: 1
The property manager superclass
-------------------------------
In addition to these :class:`property` variants the :mod:`property_manager`
module also defines a :class:`PropertyManager` class which implements several
related enhancements:
- Keyword arguments to the constructor can be used to set writable properties
created using any of the :class:`property` variants defined by the
:mod:`property_manager` module.
- Required properties without an assigned value will cause the constructor
to raise an appropriate exception (:exc:`~exceptions.TypeError`).
- The :func:`repr()` of :class:`PropertyManager` objects shows the names and
values of all properties. Individual properties can be omitted from the
:func:`repr()` output by setting the :attr:`~custom_property.repr` option to
:data:`False`.
Logging
=======
The :mod:`property_manager` module emits log messages at the custom log level
:data:`~verboselogs.SPAM` which is considered *more* verbose than :mod:`DEBUG
<logging>`, so if you want these messages to be logged make sure they're not
being ignored based on their level.
Classes
=======
.. _descriptor protocol: https://docs.python.org/2/howto/descriptor.html
"""
# Standard library modules.
import os
import sys
import textwrap
try:
# Python 3.3 and newer.
from collections.abc import Hashable
except ImportError:
# Python 2.7.
from collections import Hashable
# External dependencies.
from humanfriendly import coerce_boolean
from humanfriendly.text import compact, concatenate, format, pluralize
from verboselogs import VerboseLogger
try:
# Check if `basestring' is defined (Python 2).
basestring = basestring
except NameError:
# Alias basestring to str in Python 3.
basestring = str
__version__ = '3.0'
"""Semi-standard module versioning."""
SPHINX_ACTIVE = 'sphinx' in sys.modules
"""
:data:`True` when Sphinx_ is running, :data:`False` otherwise.
We detect whether Sphinx is running by checking for the presence of the
'sphinx' key in :data:`sys.modules`. The result determines the default
value of :data:`USAGE_NOTES_ENABLED`.
"""
USAGE_NOTES_VARIABLE = 'PROPERTY_MANAGER_USAGE_NOTES'
"""The name of the environment variable that controls whether usage notes are enabled (a string)."""
USAGE_NOTES_ENABLED = (coerce_boolean(os.environ[USAGE_NOTES_VARIABLE])
if USAGE_NOTES_VARIABLE in os.environ
else SPHINX_ACTIVE)
"""
:data:`True` if usage notes are enabled, :data:`False` otherwise.
This defaults to the environment variable :data:`USAGE_NOTES_VARIABLE` (coerced
using :func:`~humanfriendly.coerce_boolean()`) when available, otherwise
:data:`SPHINX_ACTIVE` determines the default value.
Usage notes are only injected when Sphinx is running because of performance.
It's nothing critical of course, but modifying hundreds or thousands of
docstrings that no one is going to look at seems rather pointless :-).
"""
NOTHING = object()
"""A unique object instance used to detect missing attributes."""
CUSTOM_PROPERTY_NOTE = compact("""
The :attr:`{name}` property is a :class:`~{type}`.
""")
DYNAMIC_PROPERTY_NOTE = compact("""
The :attr:`{name}` property is a :class:`~{type}`.
""")
ENVIRONMENT_PROPERTY_NOTE = compact("""
If the environment variable ``${variable}`` is set it overrides the
computed value of this property.
""")
REQUIRED_PROPERTY_NOTE = compact("""
You are required to provide a value for this property by calling the
constructor of the class that defines the property with a keyword argument
named `{name}` (unless a custom constructor is defined, in this case please
refer to the documentation of that constructor).
""")
KEY_PROPERTY_NOTE = compact("""
Once this property has been assigned a value you are not allowed to assign
a new value to the property.
""")
WRITABLE_PROPERTY_NOTE = compact("""
You can change the value of this property using normal attribute assignment
syntax.
""")
CACHED_PROPERTY_NOTE = compact("""
This property's value is computed once (the first time it is accessed) and
the result is cached.
""")
RESETTABLE_CACHED_PROPERTY_NOTE = compact("""
To clear the cached value you can use :keyword:`del` or
:func:`delattr()`.
""")
RESETTABLE_WRITABLE_PROPERTY_NOTE = compact("""
To reset it to its default (computed) value you can use :keyword:`del` or
:func:`delattr()`.
""")
# Initialize a logger for this module.
logger = VerboseLogger(__name__)
[docs]def set_property(obj, name, value):
"""
Set or override the value of a property.
:param obj: The object that owns the property.
:param name: The name of the property (a string).
:param value: The new value for the property.
This function directly modifies the :attr:`~object.__dict__` of the given
object and as such it avoids any interaction with object properties. This
is intentional: :func:`set_property()` is meant to be used by extensions of
the `property-manager` project and by user defined setter methods.
"""
logger.spam("Setting value of %s property to %r ..", format_property(obj, name), value)
obj.__dict__[name] = value
[docs]def clear_property(obj, name):
"""
Clear the assigned or cached value of a property.
:param obj: The object that owns the property.
:param name: The name of the property (a string).
This function directly modifies the :attr:`~object.__dict__` of the given
object and as such it avoids any interaction with object properties. This
is intentional: :func:`clear_property()` is meant to be used by extensions
of the `property-manager` project and by user defined deleter methods.
"""
logger.spam("Clearing value of %s property ..", format_property(obj, name))
obj.__dict__.pop(name, None)
[docs]class PropertyManager(object):
"""
Optional superclass for classes that use the computed properties from this module.
Provides support for required properties, setting of properties in the
constructor and generating a useful textual representation of objects with
properties.
"""
[docs] def __init__(self, **kw):
"""
Initialize a :class:`PropertyManager` object.
:param kw: Any keyword arguments are passed on to :func:`set_properties()`.
"""
self.set_properties(**kw)
missing_properties = self.missing_properties
if missing_properties:
msg = "missing %s" % pluralize(len(missing_properties), "required argument")
raise TypeError("%s (%s)" % (msg, concatenate(missing_properties)))
[docs] def set_properties(self, **kw):
"""
Set instance properties from keyword arguments.
:param kw: Every keyword argument is used to assign a value to the
instance property whose name matches the keyword argument.
:raises: :exc:`~exceptions.TypeError` when a keyword argument doesn't
match a :class:`property` on the given object.
"""
for name, value in kw.items():
if self.have_property(name):
setattr(self, name, value)
else:
msg = "got an unexpected keyword argument %r"
raise TypeError(msg % name)
@property
def key_properties(self):
"""A sorted list of strings with the names of any :attr:`~custom_property.key` properties."""
return self.find_properties(key=True)
@property
def key_values(self):
"""A tuple of tuples with (name, value) pairs for each name in :attr:`key_properties`."""
return tuple((name, getattr(self, name)) for name in self.key_properties)
@property
def missing_properties(self):
"""
The names of key and/or required properties that are missing.
This is a list of strings with the names of key and/or required
properties that either haven't been set or are set to :data:`None`.
"""
names = sorted(set(self.required_properties) | set(self.key_properties))
return [n for n in names if getattr(self, n, None) is None]
@property
def repr_properties(self):
"""
The names of the properties rendered by :func:`__repr__()` (a list of strings).
When :attr:`key_properties` is nonempty the names of the key properties
are returned, otherwise a more complex selection is made (of properties
defined by subclasses of :class:`PropertyManager` whose
:attr:`~custom_property.repr` is :data:`True`).
"""
return self.key_properties or [
name for name in self.find_properties(repr=True)
if not hasattr(PropertyManager, name)
]
@property
def required_properties(self):
"""A sorted list of strings with the names of any :attr:`~custom_property.required` properties."""
return self.find_properties(required=True)
[docs] def find_properties(self, **options):
"""
Find an object's properties (of a certain type).
:param options: Passed on to :func:`have_property()` to enable
filtering properties by the operations they support.
:returns: A sorted list of strings with the names of properties.
"""
# We don't explicitly sort our results here because the dir() function
# is documented to sort its results alphabetically.
return [n for n in dir(self) if self.have_property(n, **options)]
[docs] def have_property(self, name, **options):
"""
Check if the object has a property (of a certain type).
:param name: The name of the property (a string).
:param options: Any keyword arguments give the name of an option
(one of :attr:`~custom_property.writable`,
:attr:`~custom_property.resettable`,
:attr:`~custom_property.cached`,
:attr:`~custom_property.required`,
:attr:`~custom_property.key`,
:attr:`~custom_property.repr`) and an expected value
(:data:`True` or :data:`False`). Filtering on more than
one option is supported.
:returns: :data:`True` if the object has a property with the expected
options enabled/disabled, :data:`False` otherwise.
"""
property_type = getattr(self.__class__, name, None)
if isinstance(property_type, property):
if options:
return all(getattr(property_type, n, None) == v or
n == 'repr' and v is True and getattr(property_type, n, None) is not False
for n, v in options.items())
else:
return True
else:
return False
[docs] def clear_cached_properties(self):
"""Clear cached properties so that their values are recomputed."""
for name in self.find_properties(cached=True, resettable=True):
delattr(self, name)
[docs] def render_properties(self, *names):
"""
Render a human friendly string representation of an object with computed properties.
:param names: Each positional argument gives the name of a property
to include in the rendered object representation.
:returns: The rendered object representation (a string).
This method generates a user friendly textual representation for
objects that use computed properties created using the
:mod:`property_manager` module.
"""
fields = []
for name in names:
value = getattr(self, name, None)
if value is not None or name in self.key_properties:
fields.append("%s=%r" % (name, value))
return "%s(%s)" % (self.__class__.__name__, ", ".join(fields))
[docs] def __eq__(self, other):
"""Enable equality comparison and hashing for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key == other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __ne__(self, other):
"""Enable non-equality comparison for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key != other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __lt__(self, other):
"""Enable "less than" comparison for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key < other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __le__(self, other):
"""Enable "less than or equal" comparison for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key <= other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __gt__(self, other):
"""Enable "greater than" comparison for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key > other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __ge__(self, other):
"""Enable "greater than or equal" comparison for :class:`PropertyManager` subclasses."""
our_key = self.key_values
return (our_key >= other.key_values
if our_key and isinstance(other, PropertyManager)
else NotImplemented)
[docs] def __hash__(self):
"""
Enable hashing for :class:`PropertyManager` subclasses.
This method makes it possible to add :class:`PropertyManager` objects
to sets and use them as dictionary keys. The hashes computed by this
method are based on the values in :attr:`key_values`.
"""
return hash(PropertyManager) ^ hash(self.key_values)
[docs] def __repr__(self):
"""
Render a human friendly string representation of an object with computed properties.
:returns: The rendered object representation (a string).
This method uses :func:`render_properties()` to render the properties
whose names are given by :attr:`repr_properties`. When the object
doesn't have any key properties, :func:`__repr__()` assumes that
all of the object's properties are idempotent and may be evaluated
at any given time without worrying too much about performance (refer
to the :attr:`~custom_property.repr` option for an escape hatch).
"""
return self.render_properties(*self.repr_properties)
[docs]class custom_property(property):
"""
Custom :class:`property` subclass that supports additional features.
The :class:`custom_property` class implements Python's `descriptor
protocol`_ to provide a decorator that turns methods into computed
properties with several additional features.
.. _descriptor protocol: https://docs.python.org/2/howto/descriptor.html
The additional features are controlled by attributes defined on the
:class:`custom_property` class. These attributes (documented below) are
intended to be changed by the constructor (:func:`__new__()`) and/or
classes that inherit from :class:`custom_property`.
"""
cached = False
"""
If this attribute is set to :data:`True` the property's value is computed
only once and then cached in an object's :attr:`~object.__dict__`. The next
time you access the attribute's value the cached value is automatically
returned. By combining the :attr:`cached` and :attr:`resettable` options
you get a cached property whose cached value can be cleared. If the value
should never be recomputed then don't enable the :attr:`resettable`
option.
:see also: :class:`cached_property` and :class:`lazy_property`.
"""
dynamic = False
"""
:data:`True` when the :class:`custom_property` subclass was dynamically
constructed by :func:`__new__()`, :data:`False` otherwise. Used by
:func:`compose_usage_notes()` to decide whether to link to the
documentation of the subclass or not (because it's impossible to link to
anonymous classes).
"""
environment_variable = None
"""
If this attribute is set to the name of an environment variable the
property's value will default to the value of the environment variable. If
the environment variable isn't set the property falls back to its computed
value.
"""
key = False
"""
If this attribute is :data:`True` the property's name is included in the
value of :attr:`~PropertyManager.key_properties` which means that the
property's value becomes part of the "key" that is used to compare, sort
and hash :class:`PropertyManager` objects. There are a few things to be
aware of with regards to key properties and their values:
- The property's value must be set during object initialization (the same
as for :attr:`required` properties) and it cannot be changed after it is
initially assigned a value (because allowing this would "compromise"
the results of the :func:`~PropertyManager.__hash__()` method).
- The property's value must be hashable (otherwise it can't be used by the
:func:`~PropertyManager.__hash__()` method).
:see also: :class:`key_property`.
"""
repr = True
"""
By default :func:`PropertyManager.__repr__()` includes the names and values
of all properties that aren't :data:`None` in :func:`repr()` output. If you
want to omit a certain property you can set :attr:`repr` to :data:`False`.
Examples of why you would want to do this include property values that
contain secrets or are expensive to calculate and data structures with
cycles which cause :func:`repr()` to die a slow and horrible death :-).
"""
required = False
"""
If this attribute is set to :data:`True` the property requires a value to
be set during the initialization of the object that owns the property. For
this to work the class that owns the property needs to inherit from
:class:`PropertyManager`.
:see also: :class:`required_property`.
The constructor of :class:`PropertyManager` will ensure that required
properties are set to values that aren't :data:`None`. Required properties
must be set by providing keyword arguments to the constructor of the class
that inherits from :class:`PropertyManager`. When
:func:`PropertyManager.__init__()` notices that required properties haven't
been set it raises a :exc:`~exceptions.TypeError` similar to the type error
raised by Python when required arguments are missing in a function call.
Here's an example:
.. code-block:: python
from property_manager import PropertyManager, required_property, mutable_property
class Example(PropertyManager):
@required_property
def important(self):
"A very important attribute."
@mutable_property
def optional(self):
"A not so important attribute."
return 13
Let's construct an instance of the class defined above:
>>> Example()
Traceback (most recent call last):
File "property_manager/__init__.py", line 107, in __init__
raise TypeError("%s (%s)" % (msg, concatenate(missing_properties)))
TypeError: missing 1 required argument ('important')
As expected it complains that a required property hasn't been
initialized. Here's how it's supposed to work:
>>> Example(important=42)
Example(important=42, optional=13)
"""
resettable = False
"""
If this attribute is set to :data:`True` the property can be reset to its
default or computed value using :keyword:`del` and :func:`delattr()`. This
works by removing the assigned or cached value from the object's
:attr:`~object.__dict__`.
:see also: :class:`mutable_property` and :class:`cached_property`.
"""
usage_notes = True
"""
If this attribute is :data:`True` :func:`inject_usage_notes()` is used to
inject usage notes into the documentation of the property. You can set this
attribute to :data:`False` to disable :func:`inject_usage_notes()`.
"""
writable = False
"""
If this attribute is set to :data:`True` the property supports assignment.
The assigned value is stored in the :attr:`~object.__dict__` of the object
that owns the property.
:see also: :class:`writable_property`, :class:`mutable_property` and
:class:`required_property`.
A relevant note about how Python looks up attributes: When an attribute is
looked up and exists in an object's :attr:`~object.__dict__` Python ignores
any property (descriptor) by the same name and immediately returns the
value that was found in the object's :attr:`~object.__dict__`.
"""
[docs] def __new__(cls, *args, **options):
"""
Constructor for :class:`custom_property` subclasses and instances.
To construct a subclass:
:param args: The first positional argument is used as the name of the
subclass (defaults to 'customized_property').
:param options: Each keyword argument gives the name of an option
(:attr:`writable`, :attr:`resettable`, :attr:`cached`,
:attr:`required`, :attr:`environment_variable`,
:attr:`repr`) and the value to use for that option
(:data:`True`, :data:`False` or a string).
:returns: A dynamically constructed subclass of
:class:`custom_property` with the given options.
To construct an instance:
:param args: The first positional argument is the function that's
called to compute the value of the property.
:returns: A :class:`custom_property` instance corresponding to the
class whose constructor was called.
Here's an example of how the subclass constructor can be used to
dynamically construct custom properties with specific options:
.. code-block:: python
from property_manager import custom_property
class WritableCachedPropertyDemo(object):
@custom_property(cached=True, writable=True)
def customized_test_property(self):
return 42
The example above defines and uses a property whose computed value is
cached and which supports assignment of new values. The example could
have been made even simpler:
.. code-block:: python
from property_manager import cached_property
class WritableCachedPropertyDemo(object):
@cached_property(writable=True)
def customized_test_property(self):
return 42
Basically you can take any of the custom property classes defined in
the :mod:`property_manager` module and call the class with keyword
arguments corresponding to the options you'd like to change.
"""
if options:
# Keyword arguments construct subclasses.
name = args[0] if args else 'customized_property'
options['dynamic'] = True
return type(name, (cls,), options)
else:
# Positional arguments construct instances.
return super(custom_property, cls).__new__(cls, *args)
[docs] def __init__(self, *args, **kw):
"""
Initialize a :class:`custom_property` object.
:param args: Any positional arguments are passed on to the initializer
of the :class:`property` class.
:param kw: Any keyword arguments are passed on to the initializer of
the :class:`property` class.
Automatically calls :func:`inject_usage_notes()` during initialization
(only if :data:`USAGE_NOTES_ENABLED` is :data:`True`).
"""
# It's not documented so I went to try it out and apparently the
# property class initializer performs absolutely no argument
# validation. The first argument doesn't have to be a callable,
# in fact none of the arguments are even mandatory?! :-P
super(custom_property, self).__init__(*args, **kw)
# Explicit is better than implicit so I'll just go ahead and check
# whether the value(s) given by the user make sense :-).
self.ensure_callable('fget')
# We only check the 'fset' and 'fdel' values when they are not None
# because both of these arguments are supposed to be optional :-).
for name in 'fset', 'fdel':
if getattr(self, name) is not None:
self.ensure_callable(name)
# Copy some important magic members from the decorated method.
for name in '__doc__', '__module__', '__name__':
value = getattr(self.fget, name, None)
if value is not None:
setattr(self, name, value)
# Inject usage notes when running under Sphinx.
if USAGE_NOTES_ENABLED:
self.inject_usage_notes()
[docs] def ensure_callable(self, role):
"""
Ensure that a decorated value is in fact callable.
:param role: The value's role (one of 'fget', 'fset' or 'fdel').
:raises: :exc:`exceptions.ValueError` when the value isn't callable.
"""
value = getattr(self, role)
if not callable(value):
msg = "Invalid '%s' value! (expected callable, got %r instead)"
raise ValueError(msg % (role, value))
[docs] def inject_usage_notes(self):
"""
Inject the property's semantics into its documentation.
Calls :func:`compose_usage_notes()` to get a description of the property's
semantics and appends this to the property's documentation. If the
property doesn't have documentation it will not be added.
"""
if self.usage_notes and self.__doc__ and isinstance(self.__doc__, basestring):
notes = self.compose_usage_notes()
if notes:
self.__doc__ = "\n\n".join([
textwrap.dedent(self.__doc__),
".. note:: %s" % " ".join(notes),
])
[docs] def compose_usage_notes(self):
"""
Get a description of the property's semantics to include in its documentation.
:returns: A list of strings describing the semantics of the
:class:`custom_property` in reStructuredText_ format with
Sphinx_ directives.
.. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText
.. _Sphinx: http://sphinx-doc.org/
"""
template = DYNAMIC_PROPERTY_NOTE if self.dynamic else CUSTOM_PROPERTY_NOTE
cls = custom_property if self.dynamic else self.__class__
dotted_path = "%s.%s" % (cls.__module__, cls.__name__)
notes = [format(template, name=self.__name__, type=dotted_path)]
if self.environment_variable:
notes.append(format(ENVIRONMENT_PROPERTY_NOTE, variable=self.environment_variable))
if self.required:
notes.append(format(REQUIRED_PROPERTY_NOTE, name=self.__name__))
if self.key:
notes.append(KEY_PROPERTY_NOTE)
if self.writable:
notes.append(WRITABLE_PROPERTY_NOTE)
if self.cached:
notes.append(CACHED_PROPERTY_NOTE)
if self.resettable:
if self.cached:
notes.append(RESETTABLE_CACHED_PROPERTY_NOTE)
else:
notes.append(RESETTABLE_WRITABLE_PROPERTY_NOTE)
return notes
[docs] def __get__(self, obj, type=None):
"""
Get the assigned, cached or computed value of the property.
:param obj: The instance that owns the property.
:param type: The class that owns the property.
:returns: The value of the property.
"""
if obj is None:
# Called to get the attribute of the class.
return self
else:
# Called to get the attribute of an instance. We calculate the
# property's dotted name here once to minimize string creation.
dotted_name = format_property(obj, self.__name__)
if self.key or self.writable or self.cached:
# Check if a value has been assigned or cached.
value = obj.__dict__.get(self.__name__, NOTHING)
if value is not NOTHING:
logger.spam("%s reporting assigned or cached value (%r) ..", dotted_name, value)
return value
# Check if the property has an environment variable. We do this
# after checking for an assigned value so that the `writable' and
# `environment_variable' options can be used together.
if self.environment_variable:
value = os.environ.get(self.environment_variable, NOTHING)
if value is not NOTHING:
logger.spam("%s reporting value from environment variable (%r) ..", dotted_name, value)
return value
# Compute the property's value.
value = super(custom_property, self).__get__(obj, type)
logger.spam("%s reporting computed value (%r) ..", dotted_name, value)
if self.cached:
# Cache the computed value.
logger.spam("%s caching computed value ..", dotted_name)
set_property(obj, self.__name__, value)
return value
[docs] def __set__(self, obj, value):
"""
Override the computed value of the property.
:param obj: The instance that owns the property.
:param value: The new value for the property.
:raises: :exc:`~exceptions.AttributeError` if :attr:`writable` is
:data:`False`.
"""
# Calculate the property's dotted name only once.
dotted_name = format_property(obj, self.__name__)
# Evaluate the property's setter (if any).
try:
logger.spam("%s calling setter with value %r ..", dotted_name, value)
super(custom_property, self).__set__(obj, value)
except AttributeError:
logger.spam("%s setter raised attribute error, falling back.", dotted_name)
if self.writable:
# Override a computed or previously assigned value.
logger.spam("%s overriding computed value to %r ..", dotted_name, value)
set_property(obj, self.__name__, value)
else:
# Check if we're setting a key property during initialization.
if self.key and obj.__dict__.get(self.__name__, None) is None:
# Make sure we were given a hashable value.
if not isinstance(value, Hashable):
msg = "Invalid value for key property '%s'! (expected hashable object, got %r instead)"
raise ValueError(msg % (self.__name__, value))
# Set the key property's value.
logger.spam("%s setting initial value to %r ..", dotted_name, value)
set_property(obj, self.__name__, value)
else:
# Refuse to override the computed value.
msg = "%r object attribute %r is read-only"
raise AttributeError(msg % (obj.__class__.__name__, self.__name__))
[docs] def __delete__(self, obj):
"""
Reset the assigned or cached value of the property.
:param obj: The instance that owns the property.
:raises: :exc:`~exceptions.AttributeError` if :attr:`resettable` is
:data:`False`.
Once the property has been deleted the next read will evaluate the
decorated function to compute the value.
"""
# Calculate the property's dotted name only once.
dotted_name = format_property(obj, self.__name__)
# Evaluate the property's deleter (if any).
try:
logger.spam("%s calling deleter ..", dotted_name)
super(custom_property, self).__delete__(obj)
except AttributeError:
logger.spam("%s deleter raised attribute error, falling back.", dotted_name)
if self.resettable:
# Reset the computed or overridden value.
logger.spam("%s clearing assigned or computed value ..", dotted_name)
clear_property(obj, self.__name__)
else:
msg = "%r object attribute %r is read-only"
raise AttributeError(msg % (obj.__class__.__name__, self.__name__))
[docs]class writable_property(custom_property):
"""
A computed property that supports assignment.
This is a variant of :class:`custom_property`
that has the :attr:`~custom_property.writable`
option enabled by default.
"""
writable = True
[docs]class required_property(writable_property):
"""
A property that requires a value to be set.
This is a variant of :class:`writable_property` that has the
:attr:`~custom_property.required` option enabled by default. Refer to the
documentation of the :attr:`~custom_property.required` option for an
example.
"""
required = True
[docs]class key_property(custom_property):
"""
A property whose value is used for comparison and hashing.
This is a variant of :class:`custom_property` that has the
:attr:`~custom_property.key` and :attr:`~custom_property.required`
options enabled by default.
"""
key = True
required = True
[docs]class mutable_property(writable_property):
"""
A computed property that can be assigned and reset.
This is a variant of :class:`writable_property` that
has the :attr:`~custom_property.resettable`
option enabled by default.
"""
resettable = True
[docs]class lazy_property(custom_property):
"""
A computed property whose value is computed once and cached.
This is a variant of :class:`custom_property` that
has the :attr:`~custom_property.cached`
option enabled by default.
"""
cached = True
[docs]class cached_property(lazy_property):
"""
A computed property whose value is computed once and cached, but can be reset.
This is a variant of :class:`lazy_property` that
has the :attr:`~custom_property.resettable`
option enabled by default.
"""
resettable = True