Toolkit for dynamic data modeling
Project description
contextualproperties
Toolkit for creating dynamic data layers for python classes
Installation
contextualproperties can be added to any existing python project via pip:
pip install contextualproperties
How-To Guide
The Basics
You can do a lot with contextualproperties, but the most useful feature is being able to change the output value of an attribute by changing the object's context.
from contextualproperties import contextualobject, contextualproperty
@contextualobject
class ExampleClass:
@contextualproperty
def property0(self):
return 'output of property 0, default context'
@property0.getter(context="context1")
def property0(self):
return 'output of property 0, context 1'
if __name__ == '__main__':
obj = ExampleClass()
print(obj.property0)
obj.push_context('context1')
print(obj.property0)
>>> output of property 0, default context
>>> output of property 0, context 1
Intermediate Example
Contextual properties are very useful if what you are working with involves a complex data structure where sometimes you need to interact with an object, and other times you need to get the raw data.
In this example, let's assume we have an object for keeping track of what accounts belong to the same user.
from typing import List
from dataclasses import dataclass
from contextualproperties import contextualobject, contextualproperty
@dataclass
class Account:
"""
Takes an account type, username, and link to profile. Produces markdown
formatted information as a string
"""
type: str
user: str
url: str
@contextualobject
class Accounts:
"""
Returns aliases from an underlying data structure
"""
@contextualproperty
def accounts(self) -> List[Account]:
return [
Account(**account) for account in self._data.get("accounts", [])
]
@accounts.getter(context="raw")
def accounts(self) -> List[dict]:
return self._data.get("accounts", [])
def __init__(self, **data):
self._data = data
if __name__ == '__main__':
example_data = {
"uuid": "9a3d7e79-4e24-467d-8444-eaff377bb56d",
"accessed": "2026-03-31",
"accounts": [
{
"type": "mastodon",
"user": "malogan@mastodon.social",
"url": "https://mastodon.social/@malogan"
},
{
"type": "codeberg",
"user": "malogan",
"url": "https://codeberg.org/malogan"
}
]
}
obj = Accounts(**example_data)
print("Object form:")
for account in obj.accounts:
print(account)
print("Raw form:")
obj.push_context('raw')
for account in obj.accounts:
print(account)
>>> Object form:
>>> Alias(type='mastodon', user='malogan@mastodon.social', url='https://mastodon.social/@malogan')
>>> Alias(type='codeberg', user='malogan', url='https://codeberg.org/malogan')
>>> Raw form:
>>> {'type': 'mastodon', 'user': 'malogan@mastodon.social', 'url': 'https://mastodon.social/@malogan'}
>>> {'type': 'codeberg', 'user': 'malogan', 'url': 'https://codeberg.org/malogan'}
Layering
Contexts work as layers and properties will use the most recent layer they have a recognized context for. For example, if your object has five active context layers and you try to access a property that has two relevant matches, it will return the value of the most recent match for that property.
In this example, property0 has contexts 1 and 3, and property1 has contexts
1 and 2. When the properties are called, the most recent relevant context
will be selected for each.
from typing import List
from dataclasses import dataclass
from contextualproperties import contextualobject, contextualproperty
@contextualobject
class ExampleClass:
"""
Property 0 has contexts 1 and 3, Property 1 has contexts 1 and 2
"""
@contextualproperty
def property0(self):
return "property 0 default context"
@property0.getter(context="context1")
def property0(self):
return "property 0 context 1"
@property0.getter(context="context3")
def property0(self):
return "property 0 context 3"
@contextualproperty
def property1(self):
return "property 1 default context"
@property1.getter(context="context1")
def property1(self):
return "property 1 context 1"
@property1.getter(context="context2")
def property1(self):
return "property 1 context 2"
if __name__ == '__main__':
obj = ExampleClass()
print(f"{obj.property0} | {obj.property1}")
obj.push_context("context1")
print(f"{obj.property0} | {obj.property1}")
obj.push_context("context2")
print(f"{obj.property0} | {obj.property1}")
obj.push_context("context3")
print(f"{obj.property0} | {obj.property1}")
>>> property 0 default context | property 1 default context
>>> property 0 context 1 | property 1 context 1
>>> property 0 context 1 | property 1 context 2
>>> property 0 context 3 | property 1 context 2
Caching
Contextual properties have an optional cache on a per-context basis. You can
set a context to be cached by providing cache=True in the decorator and
the value will be stored.
In this example, we have a getter function that reconstructs a Person object whenever it is called. To prevent this unnecessary work, rather than reconstructing the dataclass every time the property is used, the cache will give us the same object that was previously created.
Notice that the UUID in contact and contact2 is the same. Had the object
been regenerated, the UUID would have changed in the second object.
from dataclasses import dataclass, field
from contextualproperties import contextualobject, contextualproperty
from uuid import uuid4
@dataclass
class Person:
name: str
uuid: uuid4 = field(default_factory = uuid4)
address: str = None
phone: str = None
@contextualobject
class User:
@contextualproperty(cache=True)
def contact_card(self) -> Person:
# in an actual setup you would probably pull this from a db.
# the uuid has been left off intentionally for demo purposes
return Person(name = self._data.get('name'), **self._data.get('contact'))
def __init__(self, **data):
self._data = data
if __name__ == '__main__':
obj = User(
**{
'username': 'malogan',
'name': 'Mason Logan',
'contact': {
'address': '123 Road St, Townplace, NC 12345',
'phone': '+1 123-456-7890'
}
}
)
contact = obj.contact_card
print(contact)
contact2 = obj.contact_card
print(contact)
print(f"Same object: {contact is contact2}")
>>> Person(name='Mason Logan', uuid=UUID('15a1d938-8fff-484c-8291-9966eacfeef9'), address='123 Road St, Townplace, NC 12345', phone='+1 123-456-7890')
>>> Person(name='Mason Logan', uuid=UUID('15a1d938-8fff-484c-8291-9966eacfeef9'), address='123 Road St, Townplace, NC 12345', phone='+1 123-456-7890')
>>> Same object: True
Cache Invalidation
Property setters can be used to invalidate a cache by providing them with one
or more context values in the invalidate parameter
from dataclasses import dataclass, field
from contextualproperties import contextualobject, contextualproperty
from uuid import uuid4
@dataclass
class Person:
name: str
uuid: uuid4 = field(default_factory = uuid4)
address: str = None
phone: str = None
@contextualobject
class User:
@contextualproperty
def name(self) -> str:
return getattr(self, '__User_name', None)
@name.setter
def name(self, name):
setattr(self, '__User_name', name)
@contextualproperty(cache=True)
def contact_card(self) -> Person:
# in an actual setup you would probably pull this from a db.
# the uuid has been left off intentionally for demo purposes
return Person(name=self.name, **self._data.get('contact'))
def __init__(self, name, **data):
self.name = name
self._data = data
if __name__ == '__main__':
obj = User(
**{
'username': 'malogan',
'name': 'Mason Logan',
'contact': {
'address': '123 Road St, Townplace, NC 12345',
'phone': '+1 123-456-7890'
}
}
)
contact = obj.contact_card
print(contact)
contact2 = obj.contact_card
print(contact)
print(f"Same object: {contact is contact2}")
Cache Access
Sometimes your setter may need to interact with objects in your cache, like
if you are modifying an underlying data layer and want to update the objects
representing those parts (i.e avoid full reevaluation of complex objects every
time a single value changes). By passing cache_aware=True into the setter
decorator, you can add a new parameter to the setter function to access the
cache.
NOTE: directly modifying the cache is discouraged for most purposes, but it is a very powerful tool when done right.
More Examples
Check the "examples" folder for a recipe book on how to use contextual properties to put together dynamic classes quickly
Full Docs
As soon as I get MKDocs or a similar static doc generator running, I will make the pages available
TODO: set up a static docs page
Licensing
This project is licensed under the LGPL 3.0. If you need a different license for some specific purpose, please reach out.
Roadmap
Check ROADMAP.md for full details, but the following are features planned in the near term:
- timeouts and maximum uses for cached values
- setting up for passing in user-defined functions for condition-based decache
- ability to pass a dict into
invalidateto clear cached values for other properties PropertyTemplateclass for passing into@contextualobjectclass to avoid messy inheritance trees and jungles of boilerplate copy-paste- definition of base properties that can be expanded on
- proper MRO modification for class to put
ContextualObjectand anything pulled from templates at bottom of heirarchy
- context grouping
- pass multiple context values to duplicate functionality
- setter return value caching
- allow setter functions to return a value that replaces cached values under the same property
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contextualproperties-0.1.4.tar.gz.
File metadata
- Download URL: contextualproperties-0.1.4.tar.gz
- Upload date:
- Size: 31.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80ad9072dfa267f024ed76dfe696848c254777085ccc317419ba6d41f149e130
|
|
| MD5 |
25a0700ec7971401794cf8ca8820257a
|
|
| BLAKE2b-256 |
3939d09f7457c5777af7aabcfee2c908ee9e7f826f3734cda19ceea593eb5826
|
File details
Details for the file contextualproperties-0.1.4-py3-none-any.whl.
File metadata
- Download URL: contextualproperties-0.1.4-py3-none-any.whl
- Upload date:
- Size: 36.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0653db4edab0688e49f8965bd5199548e73ef65c445f9a6753b05e6f1d43236c
|
|
| MD5 |
d1eb723f14dc07269fe559c4faf465b4
|
|
| BLAKE2b-256 |
68d95f749c61fc1630ca6b7d4f02af021caf4eb990ab57753c474dae75586a78
|