-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Python learns to detect conflicts and rebase
#420
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,10 @@ class PyIcechunkStore: | |
async def async_reset(self) -> bytes: ... | ||
def merge(self, changes: bytes) -> None: ... | ||
async def async_merge(self, changes: bytes) -> None: ... | ||
def rebase(self, solver: ConflictDetector | BasicConflictSolver) -> None: ... | ||
async def async_rebase( | ||
self, solver: ConflictDetector | BasicConflictSolver | ||
) -> None: ... | ||
def new_branch(self, branch_name: str) -> str: ... | ||
async def async_new_branch(self, branch_name: str) -> str: ... | ||
def reset_branch(self, snapshot_id: str) -> None: ... | ||
|
@@ -275,6 +279,56 @@ class StoreConfig: | |
""" | ||
... | ||
|
||
class VersionSelection: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this better than an enum? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how Rust enums map unfortunately (i think, I'll confirm) |
||
"""Configuration for selecting a version when performing conflict resolution""" | ||
|
||
@classmethod | ||
def use_ours(cls) -> VersionSelection: | ||
"""Select the local version when performing conflict resolution""" | ||
... | ||
|
||
@classmethod | ||
def use_theirs(cls) -> VersionSelection: | ||
"""Select the remote version when performing conflict resolution""" | ||
... | ||
|
||
@classmethod | ||
def fail(cls) -> VersionSelection: | ||
"""Fail if a conflict is encountered when performing conflict resolution""" | ||
... | ||
|
||
class BasicConflictSolver: | ||
"""A basic conflict solver that allows for simple configuration of resolution behavior""" | ||
|
||
def __init__( | ||
self, | ||
*, | ||
on_user_attributes_conflict: VersionSelection = VersionSelection.use_ours(), | ||
on_chunk_conflict: VersionSelection = VersionSelection.use_ours(), | ||
fail_on_delete_of_updated_array: bool = False, | ||
fail_on_delete_of_updated_group: bool = False, | ||
) -> BasicConflictSolver: | ||
"""Create a BasicConflictSolver object with the given configuration options | ||
Parameters: | ||
on_user_attributes_conflict: VersionSelection | ||
The behavior to use when a user attribute conflict is encountered, by default VersionSelection.use_ours() | ||
on_chunk_conflict: VersionSelection | ||
The behavior to use when a chunk conflict is encountered, by default VersionSelection.use_theirs() | ||
fail_on_delete_of_updated_array: bool | ||
Whether to fail when a chunk is deleted that has been updated, by default False | ||
fail_on_delete_of_updated_group: bool | ||
Whether to fail when a group is deleted that has been updated, by default False | ||
""" | ||
... | ||
|
||
class ConflictDetector: | ||
"""A conflict detector that can be used to detect conflicts between two stores and | ||
report if resolution is possible | ||
""" | ||
|
||
def __init__(self) -> ConflictDetector: ... | ||
|
||
async def async_pyicechunk_store_exists(storage: StorageConfig) -> bool: ... | ||
def pyicechunk_store_exists(storage: StorageConfig) -> bool: ... | ||
async def async_pyicechunk_store_create( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
use icechunk::conflicts::{basic_solver::{BasicConflictSolver, VersionSelection}, detector::ConflictDetector, ConflictSolver}; | ||
use pyo3::{prelude::*, types::PyType}; | ||
|
||
#[pyclass(name = "VersionSelection")] | ||
#[derive(Clone, Debug)] | ||
pub struct PyVersionSelection(VersionSelection); | ||
|
||
impl Default for PyVersionSelection { | ||
fn default() -> Self { | ||
PyVersionSelection(VersionSelection::UseOurs) | ||
} | ||
} | ||
|
||
#[pymethods] | ||
impl PyVersionSelection { | ||
#[classmethod] | ||
fn use_ours(_cls: &Bound<'_, PyType>) -> Self { | ||
PyVersionSelection(VersionSelection::UseOurs) | ||
} | ||
|
||
#[classmethod] | ||
fn use_theirs(_cls: &Bound<'_, PyType>) -> Self { | ||
PyVersionSelection(VersionSelection::UseTheirs) | ||
} | ||
|
||
#[classmethod] | ||
fn fail(_cls: &Bound<'_, PyType>) -> Self { | ||
PyVersionSelection(VersionSelection::Fail) | ||
} | ||
} | ||
|
||
impl From<PyVersionSelection> for VersionSelection { | ||
fn from(value: PyVersionSelection) -> Self { | ||
value.0 | ||
} | ||
} | ||
|
||
impl From<&PyVersionSelection> for VersionSelection { | ||
fn from(value: &PyVersionSelection) -> Self { | ||
value.0.clone() | ||
} | ||
} | ||
|
||
impl AsRef<VersionSelection> for PyVersionSelection { | ||
fn as_ref(&self) -> &VersionSelection { | ||
&self.0 | ||
} | ||
} | ||
|
||
#[pyclass(name = "ConflictDetector")] | ||
#[derive(Clone, Debug)] | ||
pub struct PyConflictDetector(ConflictDetector); | ||
|
||
#[pymethods] | ||
impl PyConflictDetector { | ||
#[new] | ||
fn new() -> Self { | ||
PyConflictDetector(ConflictDetector) | ||
} | ||
} | ||
|
||
impl From<PyConflictDetector> for ConflictDetector { | ||
fn from(value: PyConflictDetector) -> Self { | ||
value.0 | ||
} | ||
} | ||
|
||
impl From<&PyConflictDetector> for ConflictDetector { | ||
fn from(value: &PyConflictDetector) -> Self { | ||
value.0.clone() | ||
} | ||
} | ||
|
||
impl AsRef<ConflictDetector> for PyConflictDetector { | ||
fn as_ref(&self) -> &ConflictDetector { | ||
&self.0 | ||
} | ||
} | ||
|
||
#[pyclass(name = "BasicConflictSolver")] | ||
#[derive(Clone, Debug)] | ||
pub struct PyBasicConflictSolver(BasicConflictSolver); | ||
|
||
#[pymethods] | ||
impl PyBasicConflictSolver { | ||
#[new] | ||
#[pyo3(signature = (*, on_user_attributes_conflict=PyVersionSelection::default(), on_chunk_conflict=PyVersionSelection::default(), fail_on_delete_of_updated_array = false, fail_on_delete_of_updated_group = false))] | ||
fn new( | ||
on_user_attributes_conflict: PyVersionSelection, | ||
on_chunk_conflict: PyVersionSelection, | ||
fail_on_delete_of_updated_array: bool, | ||
fail_on_delete_of_updated_group: bool, | ||
) -> Self { | ||
PyBasicConflictSolver(BasicConflictSolver { | ||
on_user_attributes_conflict: on_user_attributes_conflict.into(), | ||
on_chunk_conflict: on_chunk_conflict.into(), | ||
fail_on_delete_of_updated_array, | ||
fail_on_delete_of_updated_group, | ||
}) | ||
} | ||
} | ||
|
||
impl From<PyBasicConflictSolver> for BasicConflictSolver { | ||
fn from(value: PyBasicConflictSolver) -> Self { | ||
value.0 | ||
} | ||
} | ||
|
||
impl From<&PyBasicConflictSolver> for BasicConflictSolver { | ||
fn from(value: &PyBasicConflictSolver) -> Self { | ||
value.0.clone() | ||
} | ||
} | ||
|
||
impl AsRef<BasicConflictSolver> for PyBasicConflictSolver { | ||
fn as_ref(&self) -> &BasicConflictSolver { | ||
&self.0 | ||
} | ||
} | ||
|
||
|
||
#[derive(FromPyObject)] | ||
pub enum PyConflictSolver { | ||
#[pyo3(transparent)] | ||
Detect(PyConflictDetector), | ||
#[pyo3(transparent)] | ||
Basic(PyBasicConflictSolver), | ||
} | ||
|
||
impl AsRef<dyn ConflictSolver + 'static> for PyConflictSolver { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks weird. I wonder why you need it. If you do, I think this probably should have a generic timeline instead of impl<'a> AsRef<dyn ConflictSolver + 'a> for ..... |
||
fn as_ref(&self) -> &(dyn ConflictSolver + 'static) { | ||
match self { | ||
PyConflictSolver::Detect(detector) => detector.as_ref(), | ||
PyConflictSolver::Basic(solver) => solver.as_ref(), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should write a few paragraphs of design document on this. Two topics would be:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should. For 1: I think we can used subclassing/inheritance to solve this in Python probably.
I wanted to start this PR as a jumping off point, cuz I agree there is a lot to figure out here in terms of the API