Immutable, hierarchical context management for Grimoire
Project description
Grimoire Context
Immutable, hierarchical context management for the Grimoire tabletop RPG engine.
Grimoire Context provides a robust, thread-safe context management system designed for complex game state management. It combines the power of immutable data structures with intuitive dict-like interfaces and advanced features like hierarchical scoping and parallel execution.
✨ Features
- 🔒 Immutable by Design: All operations return new context instances, ensuring thread safety and preventing accidental state mutations
- 🏗️ Hierarchical Scoping: Create child contexts that inherit from parents with proper variable shadowing
- 🎯 Dot Notation Paths: Access and modify nested data using intuitive dot notation (
character.stats.hp) - 📖 Dict-like Interface: Familiar Python dictionary operations while maintaining immutability
- 🔧 Template Resolution: Pluggable template system for dynamic content generation
- ⚡ Parallel Execution: Thread-safe concurrent operations with intelligent conflict detection
- 🛡️ Type Safe: Full type hints and protocol-based design for better development experience
🚀 Quick Start
Installation
pip install grimoire-context
Basic Usage
from grimoire_context import GrimoireContext
# Create a context with initial data
context = GrimoireContext({
'player': 'Alice',
'character': {
'name': 'Aragorn',
'hp': 100,
'mp': 50
}
})
# Immutable operations - original context unchanged
new_context = context.set('round', 1)
updated_hp = context.set_variable('character.hp', 85)
print(context['character']['hp']) # 100 (original unchanged)
print(updated_hp['character']['hp']) # 85 (new context)
# Dict-like interface
print('player' in context) # True
print(list(context.keys())) # ['player', 'character']
Hierarchical Contexts
# Create parent context (global game state)
game_state = GrimoireContext({
'system': 'grimoire',
'version': '1.0',
'difficulty': 'normal'
})
# Create child context (player-specific)
player_context = game_state.create_child_context({
'player_id': 'alice',
'character': 'warrior'
})
# Child inherits from parent
print(player_context['system']) # 'grimoire' (from parent)
print(player_context['player_id']) # 'alice' (from child)
# Variable shadowing
session = player_context.set('difficulty', 'hard')
print(session['difficulty']) # 'hard' (shadows parent)
print(game_state['difficulty']) # 'normal' (parent unchanged)
Advanced Path Operations
context = GrimoireContext({
'character': {
'stats': {'str': 15, 'dex': 12},
'inventory': ['sword', 'potion']
}
})
# Nested modifications
boosted = context.set_variable('character.stats.str', 18)
new_item = context.set_variable('character.inventory', ['sword', 'potion', 'shield'])
# Path queries
has_dex = context.has_variable('character.stats.dex') # True
missing = context.get_variable('character.stats.con', 10) # 10 (default)
# Delete nested paths
no_inventory = context.delete_variable('character.inventory')
Working with Custom Objects
Grimoire Context supports setting nested paths on custom objects, not just dictionaries. The library intelligently handles different object types:
class Character:
def __init__(self):
self.name = "Aragorn"
self.hp = 100
self.inventory = []
# Store custom object in context
character = Character()
context = GrimoireContext({'player': character})
# Set nested paths on the object - preserves object type and data
updated = context.set_variable('player.hp', 85)
updated = updated.set_variable('player.inventory', ['sword', 'shield'])
# Object is preserved with updates
player = updated.get_variable('player')
print(type(player)) # <class 'Character'>
print(player.name) # "Aragorn" (preserved)
print(player.hp) # 85 (updated)
print(player.inventory) # ['sword', 'shield'] (updated)
How it works:
- For objects with
__setitem__()(dict-like), uses item assignment - For objects with attributes, uses
setattr() - Maintains object type and all existing data
- Creates deep copies to preserve immutability
- Only converts to dict as a last resort with a warning
Parallel Execution
def buff_strength(ctx):
current = ctx.get_variable('character.stats.str', 10)
return ctx.set_variable('character.stats.str', current + 2)
def buff_dexterity(ctx):
current = ctx.get_variable('character.stats.dex', 10)
return ctx.set_variable('character.stats.dex', current + 2)
def heal_character(ctx):
return ctx.set_variable('character.hp', 100)
# Execute multiple operations concurrently
operations = [buff_strength, buff_dexterity, heal_character]
result = context.execute_parallel(operations)
# All changes applied atomically
print(result.get_variable('character.stats.str')) # Original + 2
print(result.get_variable('character.stats.dex')) # Original + 2
print(result.get_variable('character.hp')) # 100
Conflict Resolution and Merge Semantics
When using execute_parallel(), GrimoireContext merges results using these semantics:
- None values: Treated as "no change" - will not overwrite existing values
- Explicit removal: Use
discard()ordelete_variable()to explicitly remove values - Conflicts: Operations modifying the same path will raise
ContextMergeError - Nested objects: Deep merged recursively with the same semantics
Examples:
# ✓ This works - different variables in same object
ctx = GrimoireContext({'stats': {'hp': None, 'mp': None}})
def set_hp(ctx):
return ctx.set_variable('stats.hp', 100)
def set_mp(ctx):
return ctx.set_variable('stats.mp', 50)
result = ctx.execute_parallel([set_hp, set_mp])
# Result: {'stats': {'hp': 100, 'mp': 50}} - Both values preserved
# ✗ This raises error - same variable modified
def set_hp_100(ctx):
return ctx.set_variable('stats.hp', 100)
def set_hp_200(ctx):
return ctx.set_variable('stats.hp', 200)
ctx.execute_parallel([set_hp_100, set_hp_200]) # ContextMergeError
# To explicitly remove a value, use delete methods
def remove_hp(ctx):
return ctx.delete_variable('stats.hp')
Template Resolution
from grimoire_context import GrimoireContext
class GameTemplateResolver:
def resolve_template(self, template: str, context_dict: dict) -> str:
# Simple template replacement
import re
def replace_var(match):
var_name = match.group(1)
return str(context_dict.get(var_name, f'<{var_name}>'))
return re.sub(r'{{(\w+)}}', replace_var, template)
context = GrimoireContext({'player': 'Alice', 'hp': 75})
context = context.set_template_resolver(GameTemplateResolver())
message = context.resolve_template("{{player}} has {{hp}} health remaining")
print(message) # "Alice has 75 health remaining"
📚 Core Concepts
Immutability
Every operation on a GrimoireContext returns a new instance. The original context is never modified:
original = GrimoireContext({'score': 100})
modified = original.set('score', 200)
print(original['score']) # 100 (unchanged)
print(modified['score']) # 200 (new instance)
print(original is modified) # False
Context IDs
Each context has a unique identifier that changes when the context is modified:
context = GrimoireContext({'data': 'value'})
original_id = context.context_id
new_context = context.set('data', 'new_value')
new_id = new_context.context_id
print(original_id != new_id) # True
Error Handling
The package provides specific exceptions for different error conditions:
from grimoire_context import (
InvalidContextOperation,
ContextMergeError,
TemplateError
)
try:
context['key'] = 'value' # Direct assignment forbidden
except InvalidContextOperation:
print("Use .set() method instead")
try:
context.resolve_template("{{missing_var}}")
except TemplateError:
print("Template resolution failed")
🔧 API Reference
GrimoireContext
Constructor
GrimoireContext(data=None, parent=None, template_resolver=None, context_id=None)
Core Methods
set(key, value)- Return new context with key set to valuediscard(key)- Return new context with key removedupdate(mapping)- Return new context with multiple key-value pairs updatedcopy(new_id=None)- Create a copy of the context
Path Operations
set_variable(path, value)- Set value using dot notation pathget_variable(path, default=None)- Get value using dot notation pathhas_variable(path)- Check if path existsdelete_variable(path)- Delete value at path
Hierarchical Operations
create_child_context(data=None)- Create child contextlocal_data()- Get only local (non-inherited) data
Template Operations
set_template_resolver(resolver)- Set template resolverresolve_template(template)- Resolve template string
Parallel Operations
execute_parallel(operations)- Execute operations concurrently
Dict Interface
[key],get(),keys(),values(),items(),len(),iter(),in
🧪 Development
Setup
git clone https://github.com/wyrdbound/grimoire-context.git
cd grimoire-context
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=grimoire_context
# Run specific test file
pytest tests/test_context.py
Code Quality
# Linting and formatting
ruff check .
ruff format .
# Type checking
mypy src/
📋 Requirements
- Python 3.8+
- pyrsistent >= 0.19.0
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2025 The Wyrd One
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate and follow the existing code style.
If you have questions about the project, please contact: wyrdbound@proton.me
🎯 Use Cases
Grimoire Context is particularly well-suited for:
- Game State Management: Track character stats, inventory, and world state
- Rule Engine Contexts: Manage rule evaluation environments with scoping
- Template Systems: Dynamic content generation with variable substitution
- Configuration Management: Hierarchical configuration with inheritance
- Concurrent Processing: Thread-safe operations on shared state
🏗️ Architecture
The package is built on several key components:
- Immutable Data Layer: Uses
pyrsistent.PMapfor structural sharing and efficiency - Hierarchical Chain:
collections.ChainMapprovides parent-child relationships - Path Resolution: Custom dot notation parser for nested access
- Conflict Detection: Sophisticated merge logic for parallel operations
- Protocol Design: Clean interfaces for extensibility
📈 Performance
- Memory Efficient: Structural sharing means copying contexts is fast and memory-light
- Thread Safe: Immutable design eliminates race conditions
- Scalable: Hierarchical design supports deep context chains efficiently
- Optimized Paths: Dot notation operations are optimized for common access patterns
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 grimoire_context-0.3.1.tar.gz.
File metadata
- Download URL: grimoire_context-0.3.1.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f8547c033b8741714164ba2edb3684a8900162e0a2e868d1ee768916efb21b2
|
|
| MD5 |
37a933b5363df2678e02d07d96afa1f9
|
|
| BLAKE2b-256 |
81034a8b2bd1052ca322c1ceef537c743e207676c2537932674109d4aae6fd8c
|
File details
Details for the file grimoire_context-0.3.1-py3-none-any.whl.
File metadata
- Download URL: grimoire_context-0.3.1-py3-none-any.whl
- Upload date:
- Size: 19.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0c2122571319ad5f7072caeee2941a87bd7e7ba884583ae106b4288c3f6b895
|
|
| MD5 |
8179b170472ea990fc25db5f7e564d74
|
|
| BLAKE2b-256 |
4ca8e22b29791562785c02942d2f62e92bb0e02ea3effdb74727568f14d0983c
|