548 lines
13 KiB
Markdown
548 lines
13 KiB
Markdown
# MyUtils Project
|
|
|
|
## Overview
|
|
|
|
The **MyUtils** project is a Python library providing utilities to enhance Python objects, enabling features like
|
|
dynamic property access, observability of attributes, and flexible property mapping. 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
try:
|
|
unbind(data, "value", non_existent_callback)
|
|
except Exception:
|
|
# This won't execute - unbind fails silently
|
|
pass
|
|
```
|
|
|
|
#### ✅ Correct: Check before unbinding if needed
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
```
|
|
|
|
---
|
|
|
|
### 3. **ProxyObject**
|
|
|
|
The `ProxyObject` module provides a way to create proxy objects that map properties from a source object to new property names, with support for nested properties and wildcards.
|
|
|
|
#### Key Features:
|
|
|
|
- Map object properties to different names using a mapping dictionary
|
|
- Access nested properties using dot notation (e.g., `"address.city"`)
|
|
- Support for wildcard patterns to copy multiple properties at once
|
|
- Automatically resolve property paths during initialization
|
|
|
|
#### Key Classes/Functions:
|
|
|
|
- **`ProxyObject`**:
|
|
A class that wraps an object and exposes its properties through a customizable mapping.
|
|
|
|
#### Example Usage:
|
|
|
|
Here are examples using the `ProxyObject` module:
|
|
|
|
##### Basic Property Mapping
|
|
|
|
```python
|
|
from myutils.ProxyObject import ProxyObject
|
|
|
|
class Person:
|
|
def __init__(self, name, age):
|
|
self.name = name
|
|
self.age = age
|
|
|
|
person = Person("John", 30)
|
|
|
|
# Map 'name' to 'display_name' and 'age' to 'years'
|
|
mappings = {
|
|
"display_name": "name",
|
|
"years": "age"
|
|
}
|
|
|
|
proxy = ProxyObject(person, mappings)
|
|
|
|
print(proxy.display_name) # Output: John
|
|
print(proxy.years) # Output: 30
|
|
```
|
|
|
|
##### Nested Property Mapping
|
|
|
|
```python
|
|
from myutils.ProxyObject import ProxyObject
|
|
|
|
class Address:
|
|
def __init__(self, city, country):
|
|
self.city = city
|
|
self.country = country
|
|
|
|
class Employee:
|
|
def __init__(self, name, address):
|
|
self.name = name
|
|
self.address = address
|
|
|
|
address = Address("Paris", "France")
|
|
employee = Employee("Alice", address)
|
|
|
|
# Map nested properties using dot notation
|
|
mappings = {
|
|
"employee_name": "name",
|
|
"city_name": "address.city"
|
|
}
|
|
|
|
proxy = ProxyObject(employee, mappings)
|
|
|
|
print(proxy.employee_name) # Output: Alice
|
|
print(proxy.city_name) # Output: Paris
|
|
```
|
|
|
|
##### Wildcard Patterns
|
|
|
|
```python
|
|
from myutils.ProxyObject import ProxyObject
|
|
|
|
class Person:
|
|
def __init__(self, name, age):
|
|
self.name = name
|
|
self.age = age
|
|
|
|
person = Person("John", 30)
|
|
|
|
# Copy all properties from the source object
|
|
mappings = {"*": ""}
|
|
proxy = ProxyObject(person, mappings)
|
|
|
|
print(proxy.name) # Output: John
|
|
print(proxy.age) # Output: 30
|
|
```
|
|
|
|
```python
|
|
# Copy all properties from a nested object
|
|
class Employee:
|
|
def __init__(self, name, address):
|
|
self.name = name
|
|
self.address = address
|
|
|
|
class Address:
|
|
def __init__(self, city, country):
|
|
self.city = city
|
|
self.country = country
|
|
|
|
address = Address("Paris", "France")
|
|
employee = Employee("Alice", address)
|
|
|
|
# Copy all properties from 'address' to the proxy
|
|
mappings = {"*": "address"}
|
|
proxy = ProxyObject(employee, mappings)
|
|
|
|
print(proxy.city) # Output: Paris
|
|
print(proxy.country) # Output: France
|
|
```
|
|
|
|
```python
|
|
# Flatten nested object properties
|
|
class City:
|
|
def __init__(self, name, zip_code):
|
|
self.name = name
|
|
self.zip_code = zip_code
|
|
|
|
class Address:
|
|
def __init__(self, city, country):
|
|
self.city = city
|
|
self.country = country
|
|
|
|
address = Address(City("Paris", "75001"), "France")
|
|
employee = Employee("Alice", address)
|
|
|
|
# Flatten all properties from address.* (expands city's properties and country)
|
|
mappings = {"*": "address.*"}
|
|
proxy = ProxyObject(employee, mappings)
|
|
|
|
print(proxy.name) # Output: Paris (from address.city.name)
|
|
print(proxy.zip_code) # Output: 75001 (from address.city.zip_code)
|
|
print(proxy.country) # Output: France (from address.country)
|
|
```
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
To understand the file organization, here's the structure of the project:
|
|
|
|
```python
|
|
"""
|
|
MyUtils
|
|
├── src
|
|
│ └── myutils
|
|
│ ├── __init__.py # Main package initialization
|
|
│ ├── Dummy.py #
|
|
│ ├── Expando.py #
|
|
│ ├── observable.py #
|
|
│ └── ProxyObject.py #
|
|
├── 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
|
|
│ └── test_proxyobject.py # Tests for ProxyObject 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](LICENSE) file for details.
|
|
|
|
---
|
|
|
|
## Maintainers
|
|
|
|
For any inquiries or support, feel free to contact the maintainer:
|
|
|
|
- **Name**: [Your Name Here]
|
|
- **Email**: [your-email@example.com]
|
|
- **GitHub**: [Your GitHub Profile](https://github.com/your-profile)
|
|
|
|
---
|
|
|
|
## 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`
|
|
* 0.5.0 : Added `ProxyObject`
|
|
* 0.5.1 : Added `ObservableResultCollector` |