Source code for flexicon.sync.conflict_resolvers
"""
Conflict Resolvers - Strategies for resolving sync conflicts
This module provides conflict resolution policies for sync operations.
Author: FlexTools Development Team
Date: 2025-11-26
"""
import logging
from typing import Any, Optional, List
from abc import ABC, abstractmethod
logger = logging.getLogger(__name__)
[docs]
class ConflictResolver(ABC):
"""
Base class for conflict resolution strategies.
When an object exists in both source and target but differs, a conflict
resolver determines which version to keep.
Custom resolvers can be implemented by subclassing and implementing resolve().
"""
[docs]
@abstractmethod
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Resolve conflict between source and target objects.
Args:
source_obj: Source version of object
target_obj: Target version of object
source_project: Source FLExProject instance
target_project: Target FLExProject instance
Returns:
The object to keep (either source_obj or target_obj)
"""
pass
[docs]
class SourceWinsResolver(ConflictResolver):
"""
Resolve conflicts by always choosing the source version.
This is the most common strategy for one-way sync operations where
the source is considered authoritative.
Use this when:
- Source is the "master" or "stable" project
- Syncing updates from development to production
- You want to overwrite target with source data
Example:
>>> resolver = SourceWinsResolver()
>>> sync.sync(
... object_type="Allomorph",
... conflict_resolver=resolver
... )
"""
[docs]
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Always choose source version.
Args:
source_obj: Source object (will be chosen)
target_obj: Target object (will be overwritten)
source_project: Source project
target_project: Target project
Returns:
source_obj
"""
logger.debug(f"Conflict resolved: source wins (GUID: {source_obj.Guid})")
return source_obj
[docs]
class TargetWinsResolver(ConflictResolver):
"""
Resolve conflicts by always keeping the target version.
This strategy preserves the target project's data when conflicts occur.
Useful for selective merging or when target has priority.
Use this when:
- Target contains more recent data
- You want to preserve local changes
- Syncing metadata but keeping target content
Example:
>>> resolver = TargetWinsResolver()
>>> sync.sync(
... object_type="Allomorph",
... conflict_resolver=resolver
... )
"""
[docs]
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Always keep target version.
Args:
source_obj: Source object (will be ignored)
target_obj: Target object (will be kept)
source_project: Source project
target_project: Target project
Returns:
target_obj
"""
logger.debug(f"Conflict resolved: target wins (GUID: {target_obj.Guid})")
return target_obj
[docs]
class NewestWinsResolver(ConflictResolver):
"""
Resolve conflicts by choosing the most recently modified version.
This strategy compares modification timestamps and chooses the newer version.
Requires objects to have DateModified or similar timestamp properties.
Use this when:
- Both source and target are actively maintained
- You want automatic conflict resolution based on recency
- Bidirectional sync scenarios
Example:
>>> resolver = NewestWinsResolver()
>>> sync.sync(
... object_type="LexEntry",
... conflict_resolver=resolver
... )
Note:
Full implementation in Phase 2. Phase 1 placeholder.
"""
[docs]
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Choose version with most recent modification date.
Args:
source_obj: Source object
target_obj: Target object
source_project: Source project
target_project: Target project
Returns:
Newer object (source or target)
"""
# Phase 1: Basic implementation
# Full implementation with proper date comparison in Phase 2
try:
source_date = getattr(source_obj, "DateModified", None)
target_date = getattr(target_obj, "DateModified", None)
if source_date and target_date:
if source_date > target_date:
logger.debug(f"Conflict resolved: source newer " f"({source_date} > {target_date})")
return source_obj
else:
logger.debug(f"Conflict resolved: target newer " f"({target_date} >= {source_date})")
return target_obj
except Exception as e:
logger.warning(f"Could not compare dates: {e}")
# Fallback to source wins
logger.debug("Conflict resolved: fallback to source wins")
return source_obj
[docs]
class ManualResolver(ConflictResolver):
"""
Resolve conflicts by prompting the user.
This strategy asks the user to manually choose which version to keep
for each conflict. Useful for interactive sync operations.
Use this when:
- Conflicts are rare and important
- You want human review of changes
- Data is critical and needs careful merging
Example:
>>> resolver = ManualResolver()
>>> sync.sync(
... object_type="LexEntry",
... conflict_resolver=resolver
... )
Note:
Full implementation in Phase 2. Phase 1 placeholder.
"""
[docs]
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Prompt user to choose version.
Args:
source_obj: Source object
target_obj: Target object
source_project: Source project
target_project: Target project
Returns:
User-chosen object
Raises:
NotImplementedError: Phase 1 - full implementation in Phase 2
"""
# Phase 1: Not implemented
# Full interactive UI in Phase 2
raise NotImplementedError(
"Manual conflict resolution not yet implemented (Phase 2). "
"Use 'source_wins' or 'target_wins' for automated resolution."
)
[docs]
class FieldMergeResolver(ConflictResolver):
"""
Resolve conflicts by merging specific fields.
This strategy allows fine-grained control, merging some fields from source
and keeping others from target.
Example:
>>> # Keep target's gloss, but update form from source
>>> resolver = FieldMergeResolver(
... source_fields=["form", "morph_type"],
... target_fields=["gloss", "definition"]
... )
Note:
Full implementation in Phase 4. Phase 1 placeholder.
"""
def __init__(self, source_fields: List[str], target_fields: List[str]):
"""
Initialize FieldMergeResolver.
Args:
source_fields: Fields to take from source
target_fields: Fields to keep from target
"""
self.source_fields = source_fields
self.target_fields = target_fields
[docs]
def resolve(self, source_obj: Any, target_obj: Any, source_project: Any, target_project: Any) -> Any:
"""
Merge fields from both versions.
Note:
Phase 1 placeholder - full implementation in Phase 4.
"""
raise NotImplementedError(
"Field-level merge not yet implemented (Phase 4). "
"Use 'source_wins' or 'target_wins' for whole-object resolution."
)