Files
MyUtils/README.md

9.4 KiB

MyUtils Project

Overview

The MyUtils project is a Python library providing utilities to enhance Python objects, enabling features like dynamic property access and observability of attributes. The library is designed to modify and extend the behavior of objects in an easy-to-use and modular way, making it more developer-friendly.


Modules

1. Observable

The Observable module provides functionality to track changes to object attributes and notify observers when a change occurs.

Key Features:

  • Transform ordinary objects into observable objects using make_observable.
  • Attach one or more callbacks to specific attributes using bind.
  • Callbacks receive both the old and new values of the attribute.
  • Supports multiple observers on the same attribute and independent observers on different instances.

Key Classes/Functions:

  • make_observable(object): Makes an object observable by internally managing attribute listeners.
  • bind(object, attribute, callback): Adds a callback function to an observable object's attribute. The callback is triggered when the attribute value changes.
  • unbind(object, attribute, callback): Removes a callback from an observable object's attribute.
  • unbind_all(object, attribute): Removes all callbacks from an observable object's attribute.
  • has_listeners(object, attribute): Checks if an observable object has any listeners for a specific attribute.
  • get_listener_count(object, attribute): Returns the number of listeners for a specific attribute on an observable object.
  • Exception: NotObservableError is raised if you attempt to bind a callback to a non-observable object.

Example Usage:

Basic Usage

Here is an example using the Observable module:

from myutils.observable import make_observable, bind, unbind


class Demo:
  def __init__(self):
    self.number = 1


demo = Demo()
make_observable(demo)

l = lambda old, new: print(f"Changed from {old} to {new}")

# Bind a callback to 'number'
bind(demo, 'number', l)

# Updating an attribute triggers the callback
demo.number = 42  # Output: Changed from 1 to 42

# Unbind the callback
unbind(demo, 'number', l)
demo.number = 43  # No output

Multiple Callbacks

from dataclasses import dataclass
from myutils.observable import make_observable, bind, unbind


@dataclass
class Data:
  value: str = "hello"


data = Data()


def callback1(old, new):
  print(f"Callback1: {old} -> {new}")


def callback2(old, new):
  print(f"Callback2: {old} -> {new}")


# Bind both
bind(data, "value", callback1)
bind(data, "value", callback2)

data.value = "test"
# Prints:
# Callback1: hello -> test
# Callback2: hello -> test

# Remove only one
unbind(data, "value", callback1)

data.value = "final"
# Prints:
# Callback2: test -> final

Multiple Callbacks

from dataclasses import dataclass
from myutils.observable import make_observable, bind, unbind


@dataclass
class Data:
  value: str = "hello"


data = Data()


def callback1(old, new):
  print(f"Callback1: {old} -> {new}")


def callback2(old, new):
  print(f"Callback2: {old} -> {new}")


# Bind both
bind(data, "value", callback1)
bind(data, "value", callback2)

data.value = "test"
# Prints:
# Callback1: hello -> test
# Callback2: hello -> test

# Remove only one
unbind(data, "value", callback1)

data.value = "final"
# Prints:
# Callback2: test -> final

Event listener

I can register a listener for the ObservableEvent.AFTER_PROPERTY_CHANGE event.

from dataclasses import dataclass
from myutils.observable import make_observable, bind, add_event_listener, ObservableEvent

@dataclass
class Data:
  number: int = 1
  
data = Data()
make_observable(data)

on_change_results = []
on_after_change_results = []

def on_change(old, new):
  on_change_results.append((old, new))
  return new + 1

def on_after_change(attr, old, new, results):
  on_after_change_results.append((attr, old, new, results))

bind(data, 'number', on_change)
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)

data.number = 5
data.number = 10

assert on_change_results == [(1, 5), (5, 10)]
assert on_after_change_results == [("number", 1, 5, [6]), ("number", 5, 10, [11])]

I can register for all attributes change events.


from myutils.observable import make_observable, bind, add_event_listener, ObservableEvent


class Data:
  number: int = 1
  value: str = "initial"


data = Data()
make_observable(data)


def on_change_1(old, new):
  return new


def on_change_2(old, new):
  return str(new) + "_1"


on_after_change_results = []


def on_after_change(attr, old, new, results):
  on_after_change_results.append((attr, old, new, results))


add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", on_after_change)
bind(data, 'number', on_change_1)
bind(data, 'number', on_change_2)
bind(data, 'value', on_change_1)

data.number = 5
data.value = "new value"

assert on_after_change_results == [("number", 1, 5, [5, "5_1"]), ("value", "initial", "new value", ["new value"])]

Using Helper Functions

from dataclasses import dataclass
from myutils.observable import has_listeners, get_listener_count, unbind_all


@dataclass
class Data:
  value: str = "hello"


data = Data()

# Check if attribute has listeners
if has_listeners(data, "value"):
  print("Value has listeners")

# Get count
count = get_listener_count(data, "value")
print(f"Value has {count} listeners")

# Remove all listeners from one attribute
unbind_all(data, "value")

# Remove all listeners from all attributes
unbind_all(data)

Common Pitfalls

Wrong: Using different callback instance

bind(data, "value", lambda old, new: print(old))
unbind(data, "value", lambda old, new: print(old))  # Different lambda!
# The callback is NOT removed because lambdas are different objects

Correct: Using same callback reference

callback = lambda old, new: print(old)
bind(data, "value", callback)
unbind(data, "value", callback)  # Same reference
# Callback is properly removed

Wrong: Assuming unbind raises on missing callback

try:
  unbind(data, "value", non_existent_callback)
except Exception:
  # This won't execute - unbind fails silently
  pass

Correct: Check before unbinding if needed

if has_listeners(data, "value"):
  unbind(data, "value", callback)

2. Expando

The Expando module provides a dynamic wrapper for dictionaries, enabling access to dictionary values as object-style properties.

Key Features:

  • Access nested dictionary keys using dot notation (.).
  • Dynamically add new properties.
  • Handle None values seamlessly without breaking functionality.
  • Gather lists of values from arrays in nested data structures.

Key Classes/Functions:

  • Expando: A class-based wrapper for dictionaries allowing dynamic access and property management.

Example Usage:

Here is an example using the Expando module:

from myutils.Expando import Expando

data = {
    "key1": 10,
    "key2": {"subkey": "value"}
}

exp = Expando(data)

# Access properties dynamically
print(exp.key1)  # Output: 10
print(exp.key2.subkey)  # Output: value

# Dynamically add a new key
exp.new_key = "new_value"
print(exp.new_key)  # Output: new_value

Project Structure

To understand the file organization, here's the structure of the project:

"""
MyUtils
├── src
│   └── myutils
│       └── __init__.py         # Main package initialization
├── tests
│   ├── __init__.py             # Test package initialization
│   ├── test_dummy.py           # Tests for Dummy module
│   ├── test_expando.py         # Tests for Expando module
│   └── test_observable.py      # Tests for Observable module
├── .gitignore                  # Git ignore file
├── main.py                     # Application entry point
├── requirements.txt            # Project dependencies
└── README.md                   # Project description (this file)
"""

Contributing

If you'd like to contribute:

  1. Fork the repository.
  2. Create a feature branch (git checkout -b feature/your-feature).
  3. Commit your changes (git commit -m "Add some feature").
  4. Push to the branch (git push origin feature/your-feature).
  5. Open a Pull Request.

License

This project is licensed under the MIT License. See the LICENSE file for details.


Maintainers

For any inquiries or support, feel free to contact the maintainer:


Acknowledgments

Special thanks to the Python and open-source community for their tools, inspiration, and support.

Release History

  • 0.1.0 : Initial release
  • 0.2.0 : Observable results can be collected using collect_return_values
  • 0.3.0 : Added unbind, unbind_all, has_listeners get_listener_count to Observable
  • 0.4.0 : Added add_event_listener and remove_event_listener