# 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`