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 `_`
|
||||
158
README.md
158
README.md
@@ -3,8 +3,7 @@
|
||||
## 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
├── src
|
||||
│ └── myutils
|
||||
│ └── __init__.py # Main package initialization
|
||||
│ ├── __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_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
|
||||
@@ -396,4 +543,5 @@ Special thanks to the Python and open-source community for their tools, inspirat
|
||||
* 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.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]
|
||||
name = "myutils"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "Base useful classes."
|
||||
readme = "README.md"
|
||||
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