Added ProxyObject + CLAUDE.md
This commit is contained in:
154
CLAUDE.md
Normal file
154
CLAUDE.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
MyUtils is a Python utility library providing dynamic object manipulation capabilities. The library focuses on three core utilities: Observable (attribute change tracking), Expando (dynamic dictionary wrapper), and Dummy (null-safe mock object).
|
||||||
|
|
||||||
|
## Development Workflow & Collaboration Guidelines
|
||||||
|
|
||||||
|
### Developer Profile
|
||||||
|
You are an experienced Python developer producing Python 3.12 code.
|
||||||
|
|
||||||
|
### Development Process
|
||||||
|
- All proposed code must be testable
|
||||||
|
- **Before writing any code**, explain the possible implementation options
|
||||||
|
- Wait for mutual validation and understanding of requirements before producing code
|
||||||
|
|
||||||
|
### Collaboration Style
|
||||||
|
- Ask clarifying questions to refine understanding or suggest alternative approaches
|
||||||
|
- **Ask questions one at a time** - wait for complete understanding of the answer before asking the next
|
||||||
|
- When you have multiple questions, indicate progress (e.g., "Question 1/5")
|
||||||
|
|
||||||
|
### Communication
|
||||||
|
- Conversations can be in French or English
|
||||||
|
- **All code, documentation, and comments must be exclusively in English**
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
- Follow PEP 8 conventions for Python code style
|
||||||
|
- Use Google or NumPy format for docstrings
|
||||||
|
- Variable and function names must be explicit and in snake_case
|
||||||
|
- Never use emojis in code
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
- List all external dependencies required for installation
|
||||||
|
- When possible, propose alternatives using only Python standard library
|
||||||
|
|
||||||
|
### Unit Testing
|
||||||
|
- Use pytest library for unit tests
|
||||||
|
- **Before writing tests**, list the tests you plan to implement with explanations of why they're important
|
||||||
|
- Wait for validation before implementing tests
|
||||||
|
- Unless explicitly needed (e.g., inheritance), write tests as functions, not classes
|
||||||
|
- Test function naming patterns:
|
||||||
|
- `test_i_can_xxx` for tests that should pass
|
||||||
|
- `test_i_cannot_xxx` for edge cases that should raise errors/exceptions
|
||||||
|
|
||||||
|
### File Management
|
||||||
|
- When adding or modifying a file, always provide the complete file path
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- When presented with a code execution error:
|
||||||
|
1. First provide a clear explanation of the problem
|
||||||
|
2. **Do not** immediately propose a new version
|
||||||
|
3. Wait for validation of the bug analysis before proposing solutions
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_observable.py
|
||||||
|
pytest tests/test_expando.py
|
||||||
|
pytest tests/test_dummy.py
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
pytest tests/test_observable.py::test_i_can_make_an_object_observable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Management
|
||||||
|
```bash
|
||||||
|
# Install development dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Install package in development mode
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Install with development extras
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Observable Pattern Implementation
|
||||||
|
|
||||||
|
The Observable module uses **dynamic class substitution** to add observability to any Python object:
|
||||||
|
|
||||||
|
1. `make_observable(obj)` replaces the object's class with a dynamically created subclass
|
||||||
|
2. The subclass overrides `__setattr__` to intercept attribute changes
|
||||||
|
3. Listeners are stored in a `_listeners` dict on the object instance
|
||||||
|
4. Two types of callbacks exist:
|
||||||
|
- **Attribute callbacks**: Triggered on specific attribute changes via `bind()`
|
||||||
|
- **Event listeners**: Triggered via `add_event_listener()` for events like `AFTER_PROPERTY_CHANGE`
|
||||||
|
|
||||||
|
#### Event System
|
||||||
|
|
||||||
|
- `AFTER_PROPERTY_CHANGE` event fires after all attribute callbacks execute
|
||||||
|
- Event listeners receive: `(attr_name, old_value, new_value, callback_results)`
|
||||||
|
- Empty string `""` as attr_name registers listener for ALL attributes
|
||||||
|
- Return values from attribute callbacks are collected and passed to event listeners
|
||||||
|
|
||||||
|
### Expando Pattern
|
||||||
|
|
||||||
|
Expando wraps dictionaries to enable dot-notation access:
|
||||||
|
- Uses `__getattr__` to intercept attribute access and delegate to internal dict
|
||||||
|
- Nested dicts are automatically wrapped in Expando instances
|
||||||
|
- `get(path)` method supports path notation (`"a.b.c"`) and list traversal
|
||||||
|
- When path encounters a list, it collects values from all list items
|
||||||
|
|
||||||
|
### ProxyObject
|
||||||
|
|
||||||
|
ProxyObject (currently in development) maps object attributes to a different structure using a mappings dictionary. Note: Implementation appears incomplete - `_create_props()` is defined but never called.
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
- Test files use pytest fixtures (`data`, `observable_data`)
|
||||||
|
- Test names follow pattern: `test_i_can_<action>` or `test_<behavior>`
|
||||||
|
- Tests are organized by feature with separator comments
|
||||||
|
- Each test is self-contained with its own callbacks/data
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/myutils/ # Main package code
|
||||||
|
├── __init__.py # Package exports (currently empty)
|
||||||
|
├── observable.py # Observable implementation
|
||||||
|
├── Expando.py # Expando wrapper
|
||||||
|
├── Dummy.py # Dummy mock object
|
||||||
|
└── ProxyObject.py # ProxyObject (incomplete)
|
||||||
|
|
||||||
|
tests/ # Test suite
|
||||||
|
├── test_observable.py
|
||||||
|
├── test_expando.py
|
||||||
|
└── test_dummy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Patterns
|
||||||
|
|
||||||
|
### Observable Callback Semantics
|
||||||
|
- Callbacks must be the **same object** to unbind (not just equivalent lambdas)
|
||||||
|
- `unbind()` and `unbind_all()` fail silently if callback/attribute doesn't exist
|
||||||
|
- Multiple instances have independent listeners
|
||||||
|
- `make_observable()` is idempotent (safe to call multiple times)
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
- Use English for persona names (per global CLAUDE.md)
|
||||||
|
- Class names use PascalCase
|
||||||
|
- Functions use snake_case
|
||||||
|
- Private attributes prefixed with `_`
|
||||||
156
README.md
156
README.md
@@ -3,8 +3,7 @@
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The **MyUtils** project is a Python library providing utilities to enhance Python objects, enabling features like
|
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
|
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.
|
||||||
objects in an easy-to-use and modular way, making it more developer-friendly.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -335,6 +334,149 @@ 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
|
## Project Structure
|
||||||
|
|
||||||
To understand the file organization, here's the structure of the project:
|
To understand the file organization, here's the structure of the project:
|
||||||
@@ -344,12 +486,17 @@ To understand the file organization, here's the structure of the project:
|
|||||||
MyUtils
|
MyUtils
|
||||||
├── src
|
├── src
|
||||||
│ └── myutils
|
│ └── myutils
|
||||||
│ └── __init__.py # Main package initialization
|
│ ├── __init__.py # Main package initialization
|
||||||
|
│ ├── Dummy.py #
|
||||||
|
│ ├── Expando.py #
|
||||||
|
│ ├── observable.py #
|
||||||
|
│ └── ProxyObject.py #
|
||||||
├── tests
|
├── tests
|
||||||
│ ├── __init__.py # Test package initialization
|
│ ├── __init__.py # Test package initialization
|
||||||
│ ├── test_dummy.py # Tests for Dummy module
|
│ ├── test_dummy.py # Tests for Dummy module
|
||||||
│ ├── test_expando.py # Tests for Expando module
|
│ ├── test_expando.py # Tests for Expando module
|
||||||
│ └── test_observable.py # Tests for Observable module
|
│ ├── test_observable.py # Tests for Observable module
|
||||||
|
│ └── test_proxyobject.py # Tests for ProxyObject module
|
||||||
├── .gitignore # Git ignore file
|
├── .gitignore # Git ignore file
|
||||||
├── main.py # Application entry point
|
├── main.py # Application entry point
|
||||||
├── requirements.txt # Project dependencies
|
├── requirements.txt # Project dependencies
|
||||||
@@ -397,3 +544,4 @@ Special thanks to the Python and open-source community for their tools, inspirat
|
|||||||
* 0.2.0 : Observable results can be collected using `collect_return_values`
|
* 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.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.4.0 : Added `add_event_listener` and `remove_event_listener`
|
||||||
|
* 0.5.0 : Added `ProxyObject`
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "myutils"
|
name = "myutils"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
description = "Base useful classes."
|
description = "Base useful classes."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
69
src/myutils/ProxyObject.py
Normal file
69
src/myutils/ProxyObject.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
class ProxyObject:
|
||||||
|
def __init__(self, obj, mappings):
|
||||||
|
self._obj = obj
|
||||||
|
self._mappings = mappings
|
||||||
|
self._props = {}
|
||||||
|
self._create_props()
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item not in self._props:
|
||||||
|
raise AttributeError(item)
|
||||||
|
|
||||||
|
return self._props[item]
|
||||||
|
|
||||||
|
def __hasattr__(self, item):
|
||||||
|
return item in self._props
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if "key" in self._props:
|
||||||
|
return f"ProxyObject(key={self._props["key"]})"
|
||||||
|
|
||||||
|
props_as_str = str(self._props)
|
||||||
|
if len(props_as_str) > 50:
|
||||||
|
props_as_str = props_as_str[:50] + "..."
|
||||||
|
|
||||||
|
return f"ProxyObject({props_as_str})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, ProxyObject):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._props == other._props
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(sorted(self._props.items())))
|
||||||
|
|
||||||
|
def _create_props(self):
|
||||||
|
for prop_name, path in self._mappings.items():
|
||||||
|
attrs = path.split(".")
|
||||||
|
current = self._obj
|
||||||
|
|
||||||
|
# Check if path ends with wildcard
|
||||||
|
has_wildcard_in_path = attrs[-1] == "*"
|
||||||
|
|
||||||
|
# Navigate to the target object
|
||||||
|
for attr in attrs:
|
||||||
|
if attr == "*":
|
||||||
|
break
|
||||||
|
if hasattr(current, attr):
|
||||||
|
current = getattr(current, attr)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Handle wildcard cases
|
||||||
|
if prop_name == "*" or has_wildcard_in_path:
|
||||||
|
# Copy all properties from current object
|
||||||
|
if hasattr(current, '__dict__'):
|
||||||
|
for attr_name, attr_value in current.__dict__.items():
|
||||||
|
if not attr_name.startswith('_'):
|
||||||
|
# If attr_value is an object with properties, expand them
|
||||||
|
if has_wildcard_in_path and hasattr(attr_value, '__dict__'):
|
||||||
|
for sub_attr_name, sub_attr_value in attr_value.__dict__.items():
|
||||||
|
if not sub_attr_name.startswith('_'):
|
||||||
|
self._props[sub_attr_name] = sub_attr_value
|
||||||
|
else:
|
||||||
|
# Simple value or prop_name == "*"
|
||||||
|
self._props[attr_name] = attr_value
|
||||||
|
else:
|
||||||
|
# Normal mapping (no wildcard)
|
||||||
|
self._props[prop_name] = current
|
||||||
251
tests/test_proxyobject.py
Normal file
251
tests/test_proxyobject.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from myutils.ProxyObject import ProxyObject
|
||||||
|
|
||||||
|
|
||||||
|
# Test fixtures
|
||||||
|
class Person:
|
||||||
|
def __init__(self, name, age):
|
||||||
|
self.name = name
|
||||||
|
self.age = age
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Employee:
|
||||||
|
def __init__(self, name, age, address):
|
||||||
|
self.name = name
|
||||||
|
self.age = age
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def person():
|
||||||
|
return Person("John", 30)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def employee():
|
||||||
|
address = Address("Paris", "France")
|
||||||
|
return Employee("Alice", 25, address)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Basic tests
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_i_can_create_proxy_object_with_simple_mapping(person):
|
||||||
|
"""
|
||||||
|
ProxyObject should be created successfully with a simple mapping.
|
||||||
|
"""
|
||||||
|
mappings = {"display_name": "name"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
assert isinstance(proxy, ProxyObject)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_access_mapped_property(person):
|
||||||
|
"""
|
||||||
|
Accessing a mapped property should return the value from the source object.
|
||||||
|
"""
|
||||||
|
mappings = {"display_name": "name"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
assert proxy.display_name == "John"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_map_nested_properties(employee):
|
||||||
|
"""
|
||||||
|
Nested properties using dot notation should be mapped correctly.
|
||||||
|
"""
|
||||||
|
mappings = {"city_name": "address.city"}
|
||||||
|
proxy = ProxyObject(employee, mappings)
|
||||||
|
|
||||||
|
assert proxy.city_name == "Paris"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_map_nested_nested_properties():
|
||||||
|
address = Address(City("Paris", "75001"), "France")
|
||||||
|
employee2 = Employee("Alice", 25, address)
|
||||||
|
mappings = {"city_zip_code": "address.city.zip_code"}
|
||||||
|
proxy = ProxyObject(employee2, mappings)
|
||||||
|
|
||||||
|
assert proxy.city_zip_code == "75001"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_map_multiple_properties(employee):
|
||||||
|
"""
|
||||||
|
Multiple mappings should work simultaneously.
|
||||||
|
"""
|
||||||
|
mappings = {
|
||||||
|
"full_name": "name",
|
||||||
|
"years": "age",
|
||||||
|
"city_name": "address.city"
|
||||||
|
}
|
||||||
|
proxy = ProxyObject(employee, mappings)
|
||||||
|
|
||||||
|
assert proxy.full_name == "Alice"
|
||||||
|
assert proxy.years == 25
|
||||||
|
assert proxy.city_name == "Paris"
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_wild_card(employee):
|
||||||
|
mappings = {"*": ""}
|
||||||
|
proxy = ProxyObject(employee, mappings)
|
||||||
|
|
||||||
|
assert proxy.name == employee.name
|
||||||
|
assert proxy.age == employee.age
|
||||||
|
assert proxy.address == employee.address
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_wild_card_2(employee):
|
||||||
|
mappings = {"*": "*"}
|
||||||
|
proxy = ProxyObject(employee, mappings)
|
||||||
|
|
||||||
|
assert proxy.name == employee.name
|
||||||
|
assert proxy.age == employee.age
|
||||||
|
assert proxy.city == employee.address.city
|
||||||
|
assert proxy.country == employee.address.country
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_wild_card_on_nested_properties():
|
||||||
|
address = Address(City("Paris", "75001"), "France")
|
||||||
|
employee2 = Employee("Alice", 25, address)
|
||||||
|
|
||||||
|
mappings = {"name": "name", "*": "address"}
|
||||||
|
proxy = ProxyObject(employee2, mappings)
|
||||||
|
|
||||||
|
assert proxy.name == employee2.name
|
||||||
|
assert proxy.city == employee2.address.city
|
||||||
|
assert proxy.country == employee2.address.country
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_manage_wild_card_on_multiple_properties():
|
||||||
|
address = Address(City("Paris", "75001"), "France")
|
||||||
|
employee2 = Employee("Alice", 25, address)
|
||||||
|
|
||||||
|
mappings = {"*": "address.*"}
|
||||||
|
proxy = ProxyObject(employee2, mappings)
|
||||||
|
|
||||||
|
assert proxy.name == employee2.address.city.name
|
||||||
|
assert proxy.zip_code == employee2.address.city.zip_code
|
||||||
|
assert proxy.country == employee2.address.country
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Edge case tests
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_i_cannot_access_unmapped_property(person):
|
||||||
|
"""
|
||||||
|
Accessing an unmapped property should raise AttributeError.
|
||||||
|
"""
|
||||||
|
mappings = {"display_name": "name"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
_ = proxy.unknown_property
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_handle_missing_source_property(person):
|
||||||
|
"""
|
||||||
|
When the source property doesn't exist, the proxy should handle it gracefully.
|
||||||
|
The current implementation breaks the loop and assigns the last valid value.
|
||||||
|
"""
|
||||||
|
mappings = {"missing": "non_existent_attr"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
# The property exists in _props (because of break behavior)
|
||||||
|
# but contains the original object
|
||||||
|
assert proxy.missing == person
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_handle_none_values():
|
||||||
|
"""
|
||||||
|
None values should be handled correctly.
|
||||||
|
"""
|
||||||
|
person = Person(None, 30)
|
||||||
|
mappings = {"display_name": "name"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
assert proxy.display_name is None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Special methods tests
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_proxy_objects_with_same_props_are_equal(person):
|
||||||
|
"""
|
||||||
|
Two ProxyObject instances with the same _props should be equal.
|
||||||
|
"""
|
||||||
|
mappings = {"display_name": "name", "years": "age"}
|
||||||
|
proxy1 = ProxyObject(person, mappings)
|
||||||
|
proxy2 = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
assert proxy1 == proxy2
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_objects_with_different_props_are_not_equal(person):
|
||||||
|
"""
|
||||||
|
Two ProxyObject instances with different _props should not be equal.
|
||||||
|
"""
|
||||||
|
mappings1 = {"display_name": "name"}
|
||||||
|
mappings2 = {"years": "age"}
|
||||||
|
proxy1 = ProxyObject(person, mappings1)
|
||||||
|
proxy2 = ProxyObject(person, mappings2)
|
||||||
|
|
||||||
|
assert proxy1 != proxy2
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_hash_proxy_object(person):
|
||||||
|
"""
|
||||||
|
ProxyObject should be hashable and usable in sets/dicts.
|
||||||
|
"""
|
||||||
|
mappings = {"display_name": "name", "years": "age"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
# Should not raise an exception
|
||||||
|
proxy_hash = hash(proxy)
|
||||||
|
assert isinstance(proxy_hash, int)
|
||||||
|
|
||||||
|
# Should be usable in a set
|
||||||
|
proxy_set = {proxy}
|
||||||
|
assert proxy in proxy_set
|
||||||
|
|
||||||
|
|
||||||
|
def test_repr_shows_key_when_present():
|
||||||
|
"""
|
||||||
|
When 'key' is present in _props, __repr__ should show it.
|
||||||
|
"""
|
||||||
|
person = Person("key_value", 30)
|
||||||
|
mappings = {"key": "name"}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
repr_str = repr(proxy)
|
||||||
|
assert "key=key_value" in repr_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_repr_truncates_long_content(person):
|
||||||
|
"""
|
||||||
|
When _props content is longer than 50 chars, __repr__ should truncate it.
|
||||||
|
"""
|
||||||
|
mappings = {
|
||||||
|
"very_long_property_name_1": "name",
|
||||||
|
"very_long_property_name_2": "age"
|
||||||
|
}
|
||||||
|
proxy = ProxyObject(person, mappings)
|
||||||
|
|
||||||
|
repr_str = repr(proxy)
|
||||||
|
assert "..." in repr_str
|
||||||
|
assert len(repr_str) < 100 # Should be truncated
|
||||||
Reference in New Issue
Block a user