Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions MUTABILITY_FIX_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Mutability Consistency Fix Summary

## Problem Description

There was an inconsistency in the mutability of objects returned by `vcon.dialog` and `vcon.parties`:

- **`vcon.dialog`** returned a list of dictionaries from the internal data structure, allowing users to modify dialog entries in place.
- **`vcon.parties`** returned newly constructed Party objects from the underlying data, making in-place modifications ineffective since changes were not reflected in the internal vcon_dict.

This inconsistency was confusing for users who expected both properties to behave the same way.

## Solution Implemented

**Approach**: Return references to mutable objects in both cases.

### Changes Made

1. **Modified `vcon.parties` property** (lines 1152-1169 in `src/vcon/vcon.py`):
- Changed return type from `List[Party]` to `List[Dict[str, Any]]`
- Changed implementation from `[Party(**party) for party in self.vcon_dict.get("parties", [])]` to `self.vcon_dict.get("parties", [])`
- Updated documentation to reflect the new behavior

2. **Added `get_party_objects()` method** (lines 1171-1187 in `src/vcon/vcon.py`):
- Provides backward compatibility for code that needs Party objects
- Returns `[Party(**party) for party in self.vcon_dict.get("parties", [])]`
- Includes comprehensive documentation and examples

3. **Updated `vcon.dialog` property documentation** (lines 1189-1206 in `src/vcon/vcon.py`):
- Made the mutability behavior explicit in the documentation
- Added examples showing in-place modifications

4. **Fixed existing tests** (lines 286-297 in `tests/test_vcon.py`):
- Updated `test_properties()` to work with the new dictionary-based approach
- Changed from `vcon.parties[0].to_dict()` to `vcon.parties[0]`

## Benefits

1. **Consistency**: Both `vcon.dialog` and `vcon.parties` now return mutable references
2. **Performance**: No longer creates new objects on every access
3. **User expectations**: Users can modify objects in place as expected
4. **Backward compatibility**: Existing code can use `get_party_objects()` when Party objects are needed

## Usage Examples

### Before (Inconsistent)
```python
# This worked - dialog was mutable
vcon.dialog[0]["body"] = "Modified content"

# This didn't work - parties were not mutable
vcon.parties[0].name = "Modified name" # Changes lost
```

### After (Consistent)
```python
# Both work - both are mutable
vcon.dialog[0]["body"] = "Modified content"
vcon.parties[0]["name"] = "Modified name"

# Both changes are reflected in the internal data
assert vcon.dialog[0]["body"] == "Modified content"
assert vcon.parties[0]["name"] == "Modified name"
```

### Backward Compatibility
```python
# For code that needs Party objects
party_objects = vcon.get_party_objects()
party_objects[0].name = "Modified name" # This creates new objects
```

## Testing

- Created comprehensive tests in `tests/test_mutability_consistency.py`
- Updated existing tests to work with the new behavior
- Verified core mutability logic works correctly
- All tests pass with the new implementation

## Breaking Changes

- **`vcon.parties`** now returns `List[Dict[str, Any]]` instead of `List[Party]`
- Code that accessed `vcon.parties[0].name` needs to be updated to `vcon.parties[0]["name"]`
- Code that needs Party objects should use `vcon.get_party_objects()`

## Migration Guide

For existing code that uses Party objects:

```python
# Old way (no longer works)
parties = vcon.parties
for party in parties:
print(party.name)

# New way 1: Use dictionary access
parties = vcon.parties
for party in parties:
print(party["name"])

# New way 2: Use get_party_objects() for backward compatibility
party_objects = vcon.get_party_objects()
for party in party_objects:
print(party.name)
```

## Files Modified

1. `src/vcon/vcon.py` - Main implementation changes
2. `tests/test_vcon.py` - Updated existing tests
3. `tests/test_mutability_consistency.py` - New comprehensive tests
4. `MUTABILITY_FIX_SUMMARY.md` - This documentation

The fix successfully resolves the mutability inconsistency while maintaining backward compatibility through the new `get_party_objects()` method.
8 changes: 8 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ To read an existing vCon container:
# Get participants with enhanced information
parties = vcon.parties
for party in parties:
print(f"Name: {party['name']}")
print(f"SIP: {party.get('sip', 'Not set')}")
print(f"DID: {party.get('did', 'Not set')}")
print(f"Timezone: {party.get('timezone', 'Not set')}")

# Alternative: Use get_party_objects() for Party object access
party_objects = vcon.get_party_objects()
for party in party_objects:
print(f"Name: {party.name}")
print(f"SIP: {getattr(party, 'sip', 'Not set')}")
print(f"DID: {getattr(party, 'did', 'Not set')}")
Expand Down
32 changes: 28 additions & 4 deletions src/vcon/vcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,18 +1150,39 @@ def dumps(self) -> str:
return self.to_json()

@property
def parties(self) -> List[Party]:
def parties(self) -> List[Dict[str, Any]]:
"""
Get the list of parties in the vCon.

Returns:
A list of Party objects representing all participants in the conversation
A list of party dictionaries representing all participants in the conversation.
These are mutable references to the internal data structure, allowing in-place modifications.

Example:
>>> vcon = Vcon.build_new()
>>> vcon.add_party(Party(type="person", name="John Doe"))
>>> parties = vcon.parties
>>> print(parties[0].name) # Prints "John Doe"
>>> print(parties[0]["name"]) # Prints "John Doe"
>>> parties[0]["name"] = "Jane Doe" # In-place modification works
>>> print(vcon.parties[0]["name"]) # Prints "Jane Doe"
"""
return self.vcon_dict.get("parties", [])

def get_party_objects(self) -> List[Party]:
"""
Get the list of parties as Party objects.

This method creates Party objects from the underlying dictionary data.
Use this when you need Party objects instead of raw dictionaries.

Returns:
A list of Party objects representing all participants in the conversation

Example:
>>> vcon = Vcon.build_new()
>>> vcon.add_party(Party(type="person", name="John Doe"))
>>> party_objects = vcon.get_party_objects()
>>> print(party_objects[0].name) # Prints "John Doe"
"""
return [Party(**party) for party in self.vcon_dict.get("parties", [])]

Expand All @@ -1171,13 +1192,16 @@ def dialog(self) -> List[Dict[str, Any]]:
Get the list of dialog entries in the vCon.

Returns:
A list of dialog entries representing the conversation content
A list of dialog dictionaries representing the conversation content.
These are mutable references to the internal data structure, allowing in-place modifications.

Example:
>>> vcon = Vcon.build_new()
>>> vcon.add_dialog(Dialog(type="text", start="2023-01-01T00:00:00Z", parties=[0]))
>>> dialog = vcon.dialog
>>> print(dialog[0]["type"]) # Prints "text"
>>> dialog[0]["body"] = "Modified content" # In-place modification works
>>> print(vcon.dialog[0]["body"]) # Prints "Modified content"
"""
return self.vcon_dict.get("dialog", [])

Expand Down
148 changes: 148 additions & 0 deletions tests/test_mutability_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""
Test to demonstrate and verify the mutability consistency between vcon.dialog and vcon.parties properties.
"""
import pytest
from vcon import Vcon
from vcon.party import Party
from vcon.dialog import Dialog
from datetime import datetime, timezone


def test_mutability_consistency_demonstration():
"""
Demonstrate that both vcon.dialog and vcon.parties now work consistently.

This test shows that:
- vcon.dialog returns mutable references (in-place modifications work)
- vcon.parties now also returns mutable references (in-place modifications work)
"""
# Create a vCon with parties and dialogs
vcon = Vcon.build_new()

# Add parties
party1 = Party(name="Alice", tel="+1234567890")
party2 = Party(name="Bob", tel="+1987654321")
vcon.add_party(party1)
vcon.add_party(party2)

# Add dialogs
dialog1 = Dialog(
type="text",
start=datetime.now(timezone.utc).isoformat(),
parties=[0, 1],
body="Hello, this is a test message"
)
dialog2 = Dialog(
type="text",
start=datetime.now(timezone.utc).isoformat(),
parties=[0],
body="This is another message"
)
vcon.add_dialog(dialog1)
vcon.add_dialog(dialog2)

# Test dialog mutability (should work - returns direct references)
dialog_list = vcon.dialog
original_body = dialog_list[0]["body"]
dialog_list[0]["body"] = "Modified dialog body"

# Verify the change is reflected in the internal data
assert vcon.dialog[0]["body"] == "Modified dialog body"
assert vcon.vcon_dict["dialog"][0]["body"] == "Modified dialog body"

# Test parties mutability (now works - returns mutable references)
parties_list = vcon.parties
original_name = parties_list[0]["name"]

# This modification now affects the internal data
parties_list[0]["name"] = "Modified Alice"

# Verify the change IS reflected in the internal data (new consistent behavior)
assert vcon.parties[0]["name"] == "Modified Alice" # Modified name
assert vcon.vcon_dict["parties"][0]["name"] == "Modified Alice" # Internal data changed


def test_mutability_consistency_after_fix():
"""
Test that after the fix, both vcon.dialog and vcon.parties behave consistently.

This test verifies that both properties return mutable references.
"""
# Create a vCon with parties and dialogs
vcon = Vcon.build_new()

# Add parties
party1 = Party(name="Alice", tel="+1234567890")
party2 = Party(name="Bob", tel="+1987654321")
vcon.add_party(party1)
vcon.add_party(party2)

# Add dialogs
dialog1 = Dialog(
type="text",
start=datetime.now(timezone.utc).isoformat(),
parties=[0, 1],
body="Hello, this is a test message"
)
vcon.add_dialog(dialog1)

# Test dialog mutability (should work)
dialog_list = vcon.dialog
dialog_list[0]["body"] = "Modified dialog body"
assert vcon.dialog[0]["body"] == "Modified dialog body"
assert vcon.vcon_dict["dialog"][0]["body"] == "Modified dialog body"

# Test parties mutability (should work after fix)
parties_list = vcon.parties
parties_list[0]["name"] = "Modified Alice"

# Verify the change IS reflected in the internal data (after fix)
assert vcon.parties[0]["name"] == "Modified Alice"
assert vcon.vcon_dict["parties"][0]["name"] == "Modified Alice"

# Test that we can still access party data as dictionaries
assert isinstance(vcon.parties[0], dict)
assert "name" in vcon.parties[0]
assert "tel" in vcon.parties[0]


def test_party_object_creation_still_works():
"""
Test that we can still create Party objects from the dictionary data when needed.
"""
vcon = Vcon.build_new()

# Add a party
party = Party(name="Alice", tel="+1234567890")
vcon.add_party(party)

# Get the party data as dictionary
party_dict = vcon.parties[0]

# Create a Party object from the dictionary data
party_obj = Party(**party_dict)

# Verify the Party object works correctly
assert party_obj.name == "Alice"
assert party_obj.tel == "+1234567890"
assert isinstance(party_obj, Party)


def test_backward_compatibility():
"""
Test that existing code that expects Party objects still works.
"""
vcon = Vcon.build_new()

# Add a party
party = Party(name="Alice", tel="+1234567890")
vcon.add_party(party)

# Get parties and create Party objects (common pattern)
parties = [Party(**party_dict) for party_dict in vcon.parties]

# Verify the Party objects work correctly
assert len(parties) == 1
assert parties[0].name == "Alice"
assert parties[0].tel == "+1234567890"
assert isinstance(parties[0], Party)
4 changes: 2 additions & 2 deletions tests/test_vcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,13 @@ def test_properties() -> None:
assert vcon.created_at == "2024-10-20T15:02:55.490850+00:00"
assert len(vcon.parties) == 2

assert vcon.parties[0].to_dict() == {
assert vcon.parties[0] == {
"tel": "+14513886516",
"mailto": "david.scott@pickrandombusinesstype.com",
"name": "David Scott",
"meta": {"role": "agent"},
}
assert vcon.parties[1].to_dict() == {
assert vcon.parties[1] == {
"tel": "+16171557264",
"mailto": "diane.allen@gmail.com",
"name": "Diane Allen",
Expand Down