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
30 changes: 30 additions & 0 deletions benchmarks/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,36 @@ def test_apply_list_1000_elements(benchmark):
benchmark(apply, a, ops)


@pytest.mark.benchmark(group="apply")
def test_apply_dict_many_ops(benchmark):
"""Benchmark: apply a 400-op patch (replaces, adds, removes) to a
1000-key dict — the op-loop dominates over the deep copy."""
a = {f"key_{i}": i for i in range(1000)}
b = dict(a)
for i in range(200):
b[f"key_{i}"] = i + 10000
for i in range(100):
b[f"new_key_{i}"] = i
for i in range(100):
del b[f"key_{i + 200}"]
ops, _ = diff(a, b)

benchmark(apply, a, ops)


@pytest.mark.benchmark(group="apply")
def test_apply_nested_paths(benchmark):
"""Benchmark: apply 100 replaces addressed three levels deep, so
pointer evaluation is a significant share of the work."""
base = {"items": [{"id": i, "meta": {"prio": i % 3}} for i in range(100)]}
changed = copy.deepcopy(base)
for item in changed["items"]:
item["meta"]["prio"] = 9
ops, _ = diff(base, changed)

benchmark(apply, base, ops)


# ========================================
# Pointer Evaluate Benchmarks
# ========================================
Expand Down
27 changes: 20 additions & 7 deletions patchdiff/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from .pointer import Pointer
from .types import Diffable, Operation

# Immutable types that can never contain (or be) shared mutable state, so
# patch values of these types can be written without a defensive copy.
_SCALAR_TYPES = frozenset({int, float, complex, bool, str, bytes, type(None)})


def iapply(obj: Diffable, patches: list[Operation]) -> Diffable:
"""Apply a list of patches to an object, in place.
Expand All @@ -24,6 +28,8 @@ def iapply(obj: Diffable, patches: list[Operation]) -> Diffable:
"""
if not patches:
return obj
scalar_types = _SCALAR_TYPES
copy_value = deepcopy
for patch in patches:
# The interpreter below is duck-typed on purpose (dict/list/set
# look-alikes such as observ proxies must work), so the operation
Expand All @@ -36,17 +42,24 @@ def iapply(obj: Diffable, patches: list[Operation]) -> Diffable:
key: Any = target[1]
value: Any = None
if op != "remove":
value = deepcopy(op_dict["value"])
if hasattr(parent, "keys"): # dict
value = op_dict["value"]
if value.__class__ not in scalar_types:
value = copy_value(value)
# Dispatch on the parent's exact class first (the overwhelmingly
# common case), falling back to duck typing for container
# look-alikes such as observ proxies.
parent_cls = parent.__class__
if parent_cls is dict or (parent_cls is not list and hasattr(parent, "keys")):
if op == "remove":
del parent[key]
else: # add/replace
parent[key] = value
elif hasattr(parent, "append"): # list
try:
key = int(key)
except ValueError:
pass
elif parent_cls is list or hasattr(parent, "append"):
if key.__class__ is not int:
try:
key = int(key)
except ValueError:
pass

if op == "replace":
parent[key] = value
Expand Down