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
117 changes: 108 additions & 9 deletions dbzero/dbzero/atomic.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,122 @@
from __future__ import annotations

from typing import Any, Dict
from .interfaces import Memo
from .dbzero import begin_atomic, assign


class AtomicManager:
def __enter__(self):
self.__ctx = begin_atomic()
return self.__ctx
"""Context manager that provides atomic context functionality for dbzero operations.

It is intended for use in a 'with' statement.
"""

def __init__(self):
self.__ctx = None

def __enter__(self) -> AtomicManager:
self.begin()
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.__ctx.close()
self.close()
else:
self.__ctx.cancel()
self.__ctx = None
self.cancel()

def begin(self):
"""Begin the atomic context"""
if self.__ctx is None:
self.__ctx = begin_atomic()

def close(self):
"""Close the atomic context, staging the changes for commit"""
if self.__ctx is None:
return

self.__ctx.close()
self.__ctx = None

def cancel(self):
"""Cancel the atomic context, reverting all changes"""
if self.__ctx is None:
return

self.__ctx.cancel()
self.__ctx = None


def atomic() -> AtomicManager:
"""Create a context manager to group multiple mutating operations into a single indivisible transaction.

This function ensures that all modifications within the `with` block are applied together, or none are applied at all.
If the block completes successfully, all changes are merged into the current
transaction. If an exception occurs inside the block, or if the transaction is
manually canceled, all changes are reverted, leaving the data in its previous state.

Returns
-------
AtomicManager
A context manager that provides atomic context functionality.

Examples
--------
Grouping successful operations:

>>> obj1 = MyMemoClass("initial value")
>>> with dbzero.atomic():
... obj1.value = "updated value"
... obj2 = MyMemoClass("new object")
... dbzero.tags(obj2).add("new")
>>> # Both changes are now visible
>>> assert obj1.value == "updated value"

Automatic rollback on exception:

def atomic():
>>> obj = MyMemoClass(value=100)
>>> try:
... with dbzero.atomic():
... obj.value = 200 # This change will be reverted
... raise ValueError("Something went wrong")
... except ValueError:
... print("Caught expected error.")
>>> # The object's value is unchanged
>>> assert obj.value == 100

Manual rollback with cancel():

>>> obj = MyMemoClass(value=100)
>>> with dbzero.atomic() as atomic:
... obj.value = 200
... if obj.value > 150:
... print("Value too high, canceling.")
... atomic.cancel()
>>> # The changes were discarded
>>> assert obj.value == 100

Notes
-----
An atomic() block does not immediately create a new, committed transaction or
increment the global state number. Instead, the changes are staged
and applied as part of the surrounding transaction, which is then committed
either manually via dbzero.commit() or by the autocommit mechanism.
"""
return AtomicManager()


def atomic_assign(*args, **kwargs):
def atomic_assign(*objects: Memo, **attributes: Dict[str, Any]) -> None:
"""Perform bulk attribute updates on one or more Memo objects within an atomic transaction.

This is a helper function that performs `dbzero.assign` operation in an atomic context.

Parameters
----------
*objects : Any
A variable number of Memo objects to modify.
**attributes : Dict[str, Any]
The attributes to update, provided as name=value pairs where each key is
the name of an attribute to update and the corresponding value is the new
value to assign.
"""
with atomic():
assign(*args, **kwargs)
assign(*objects, **attributes)
62 changes: 61 additions & 1 deletion dbzero/dbzero/compare.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,68 @@
from typing import Optional, List
from .interfaces import Memo, Tag
import dbzero as db0
from .dbzero import _compare


def compare(obj_1, obj_2, tags=None):
def compare(obj_1: Memo, obj_2: Memo, tags: Optional[List[Tag]] = None) -> bool:
"""Perform a deep, content-based comparison of two Memo objects to check if they are identical.

By default, it only compares the objects' data fields.
When optional `tags` parameter is provided, their tag assignments are included in the comparison.

Parameters
----------
obj_1 : Memo
The first Memo object for comparison.
obj_2 : Memo
The second Memo object for comparison.
tags : list of Tag, optional
A list of tags to include in the comparison. If provided, the method
will check if both objects have an identical assignment (or lack) of each
tag in the list.

Returns
-------
bool
True if the objects are of the same type and their data content (and
specified tags, if checked) are identical. False if the objects are of
different types, their data differs, or their specified tag assignments differ.

Examples
--------
Basic content comparison:

>>> obj_1 = MemoTestClass(100)
>>> obj_2 = MemoTestClass(100)
>>> dbzero.compare(obj_1, obj_2) # Returns True
>>>
>>> # Change the content of one object
>>> obj_2.value = 200
>>> dbzero.compare(obj_1, obj_2) # Returns False

Comparing with tags:

>>> obj_A = MemoTestClass(100)
>>> obj_B = MemoTestClass(100)
>>>
>>> dbzero.tags(obj_B).add("featured")
>>> dbzero.commit()
>>>
>>> # Default comparison ignores tags and returns True
>>> # because their content is the same.
>>> assert dbzero.compare(obj_A, obj_B) == True
>>>
>>> # Including the 'featured' tag in the comparison
>>> # returns False because obj_A lacks the tag.
>>> assert dbzero.compare(obj_A, obj_B, tags=['featured']) == False
>>>
>>> # Now add the tag to obj_A as well
>>> dbzero.tags(obj_A).add("featured")
>>> dbzero.commit()
>>>
>>> # The comparison now returns True
>>> assert dbzero.compare(obj_A, obj_B, tags=['featured']) == True
"""
if _compare(obj_1, obj_2):
# if objects are identical then also compare tags
if tags is not None:
Expand Down
Loading