Skip to main content

A Python module for impartial computation. Implements the Functional Core, Stateful Shell pattern.

Project description

🐚 pureshell

A Python design pattern for creating Stateful Entities powered by a pure Functional Core. This library provides a simple, enforceable, and testable architecture for separating state management from business logic.

PyPI version

🏛️ The "Stateful Shell" Pattern

At its core, pureshell helps you implement the "Functional Core, Stateful Shell" pattern. This means:

  • 🧠 Functional Core: All your complex business logic (e.g., game rules, business calculations) is written as pure, stateless functions. These functions are grouped into Ruleset classes and are incredibly easy to unit test.
  • 🤖 Stateful Shell: Your objects (StatefulEntity classes) act as simple "shells" that hold state. They delegate all their logic and calculations to the functional core, keeping the classes themselves clean, declarative, and free of implementation details.

This separation makes your system more robust, highly testable, and easier to reason about as it grows in complexity.

✨ Core Concepts

The framework is built around a few key components:

  • StatefulEntity: A base class for your objects (ShoppingCart, GameCharacter, etc.). It enforces the pattern by ensuring no business logic is accidentally added to the class itself.
  • Ruleset: A base class for your collections of pure functions (e.g., CartRules, CharacterRules). It enforces that all rules are defined as @staticmethod.
  • @ruleset_provider(RulesetClass): A class decorator that links a StatefulEntity to its corresponding Ruleset.
  • @shell_method(...): A method decorator that declaratively links a method on a StatefulEntity to a pure function in its Ruleset.
  • @side_effect_method: A decorator to explicitly mark methods that perform I/O (like printing to the console or rendering graphics) as being exempt from the "pure logic" enforcement.

🚀 Installation

pip install pureshell

🚀 Usage

Here's a practical example of defining a ShoppingCart using the pureshell pattern. This example is available in examples/shopping_cart_example.py.

1. Define your data structures and pure functions:

First, we define our data classes and a Ruleset containing all the business logic as pure, static methods.

# In examples/shopping_cart_example.py
from dataclasses import dataclass
from typing import List
from pureshell import Ruleset

@dataclass(frozen=True)
class CartItem:
    """Represents an item in the shopping cart."""
    name: str
    price: float
    requires_age_check: bool = False

@dataclass(frozen=True)
class UserProfile:
    """Represents basic user profile data."""
    user_id: str
    age: int

class CartRules(Ruleset):
    """A collection of pure functions for cart logic."""
    @staticmethod
    def add_item(items: List[CartItem], new_item: CartItem) -> List[CartItem]:
        """Pure function to add an item to a list of cart items."""
        return items + [new_item]

    @staticmethod
    def calculate_total(items: List[CartItem]) -> float:
        """Pure function to calculate the total price of all items in a list."""
        return sum(item.price for item in items)

    @staticmethod
    def is_valid(items: List[CartItem], profile: UserProfile) -> bool:
        """Checks if a cart is valid based on items and user profile."""
        if any(item.requires_age_check for item in items):
            return profile.age >= 21
        return True

2. Define your stateful entity:

Next, create the ShoppingCart class. It inherits from StatefulEntity, holds the state, and uses @shell_method to declaratively link its methods to the pure functions in CartRules.

# In examples/shopping_cart_example.py
from pureshell import StatefulEntity, shell_method, ruleset_provider, side_effect_method

@ruleset_provider(CartRules)
class ShoppingCart(StatefulEntity):
    """Manages a shopping cart by delegating logic to pure functions."""
    def __init__(self, user_id: str, age: int):
        self._items: List[CartItem] = []
        self._profile: UserProfile = UserProfile(user_id=user_id, age=age)

    @shell_method('_items', pure_func='calculate_total')
    def get_total(self) -> float:
        """Calculates the current total price of all items."""
        raise NotImplementedError()

    @shell_method('_items', mutates=True) # Infers pure_func='add_item'
    def add_item(self, item: CartItem) -> None:
        """Adds a CartItem to the cart."""
        raise NotImplementedError()

    @shell_method(('_items', '_profile'), pure_func='is_valid')
    def is_valid_for_checkout(self) -> bool:
        """Checks if the cart is valid for the user."""
        raise NotImplementedError()

    @side_effect_method
    def display(self):
        """Prints the current state of the cart (a permitted side-effect)."""
        # ... implementation for printing state ...

3. Use your object:

Now you can use the ShoppingCart object. Its methods are simple to call, while the complex logic is handled by the functional core.

# --- In your main script ---
adult_cart = ShoppingCart(user_id="user-123", age=30)
adult_cart.add_item(CartItem("Organic Apples", 4.99))
adult_cart.add_item(CartItem("Craft Beer", 12.50, requires_age_check=True))

print(f"Total: ${adult_cart.get_total():.2f}")
# Output: Total: $17.49

print(f"Is valid for checkout: {adult_cart.is_valid_for_checkout()}")
# Output: Is valid for checkout: True

🛑 Why raise NotImplementedError()?

You'll notice that the decorated methods in the StatefulEntity have raise NotImplementedError() as their implementation. This is because the @shell_method decorator replaces the method with its own logic. This practice ensures that if the decorator is ever misconfigured or accidentally removed, the program will fail immediately and loudly, preventing logic from being silently ignored.

In addition, this addresses linter warnings that would be raised if pass or ellipsis (...) were used instead.

🎮 Running the Examples

This repository includes complete, runnable examples to demonstrate the pattern. A helper script is provided to easily run them.

python run_examples.py

This will present a menu where you can choose between:

  • Shopping Cart: The simple e-commerce example detailed above.

  • Pygame Space Shooter: A more advanced example showing how pureshell can be used to completely separate game logic from the Pygame rendering engine, making the logic highly testable.

✅ Running Tests

The project includes a comprehensive test suite. To run the tests, navigate to the project root directory and run the test discovery command:

python -m unittest discover tests

🗺️ Roadmap / Future Iterations

The following features are planned for future versions of pureshell to enhance its flexibility and power for more advanced use cases:

  • Dynamic Ruleset Injection: Allow a Ruleset to be injected at instantiation time (__init__) rather than just at the class level. This would enable different instances of the same entity to use different logic (e.g., "Easy" vs. "Hard" mode AI).

  • Advanced Error Handling: Add a mechanism to the @shell_method wrapper to gracefully catch and handle exceptions raised by pure functions, preventing crashes and allowing for more resilient entities.

  • Asynchronous Operations: Introduce support for async/await within the framework, allowing shell_method to call and await asynchronous pure functions. This is essential for I/O-bound operations.

  • Memoization for Performance: Add an optional caching (memoization) feature to read-only @shell_method calls. This would store and reuse the results of expensive calculations, boosting performance in read-heavy scenarios.

🤝 Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue for any bugs, feature requests, or suggestions.

📄 License

This project is licensed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pureshell-0.1.0.tar.gz (7.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pureshell-0.1.0-py3-none-any.whl (6.0 kB view details)

Uploaded Python 3

File details

Details for the file pureshell-0.1.0.tar.gz.

File metadata

  • Download URL: pureshell-0.1.0.tar.gz
  • Upload date:
  • Size: 7.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.4

File hashes

Hashes for pureshell-0.1.0.tar.gz
Algorithm Hash digest
SHA256 33820ef1104de1bce002d4251cb3844aa7a688024e79ea49ec11aab307558df6
MD5 8fc5d4769c2d6158841f124bb94156a3
BLAKE2b-256 840a24e5175a6a86e1bf52badfaf20c12fdb67df6db139c197e4184e0dfc7776

See more details on using hashes here.

File details

Details for the file pureshell-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pureshell-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 6.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.4

File hashes

Hashes for pureshell-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cb1a453dcfac7e311b07658dbc0c5717808078807c9bd555de846231eaf151c7
MD5 4b8c63004e38f1f394cab77d6e1a8997
BLAKE2b-256 2ee923741559f91ec1f344d2f3a81877b69c76484a5ac255b6423118ef6e4651

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page