Skip to content

Commit 3be69c4

Browse files
Korijnclaude
andauthored
Construct proxies with positional flags (#193)
proxy() passed the readonly and shallow flags as keyword arguments to proxy_db.get_proxy, the proxy classes and its own tuple recursion; keyword calls are measurably slower than positional ones, and this is the hottest code path in observ (it runs on the result of every read from a proxied container). The readonly proxy classes' __init__ shims now line up with Proxy.__init__ ((target, readonly, shallow)) so that proxy() can construct any proxy type positionally. The readonly argument is accepted and ignored, so a directly constructed Readonly* proxy is still always readonly (previously enforced with two dict merges per construction, which are gone now). New tests pin down that positional and keyword flags resolve to the same cached proxies, and that direct Readonly* construction still forces readonly and honors shallow. Measured on bench/test_creation.py: proxy creation ~6-12% faster. Claude-Session: https://claude.ai/code/session_01UyALuqgF1ZZ3Lj88FzwGVc Co-authored-by: Claude <noreply@anthropic.com>
1 parent bcf9adf commit 3be69c4

5 files changed

Lines changed: 78 additions & 19 deletions

File tree

observ/dict_proxy.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ def _orphaned_keydeps(self) -> set[Any]:
6464

6565

6666
def readonly_dict_proxy_init(
67-
self: DictProxyBase, target: dict, shallow: bool = False, **kwargs: Any
67+
self: DictProxyBase, target: dict, readonly: bool = True, shallow: bool = False
6868
) -> None:
69-
super(ReadonlyDictProxy, self).__init__(
70-
target, shallow=shallow, **{**kwargs, "readonly": True}
71-
)
69+
# The signature lines up with Proxy.__init__ so that proxy() can
70+
# construct any proxy type positionally; the readonly argument is
71+
# ignored, a ReadonlyDictProxy is always readonly
72+
super(ReadonlyDictProxy, self).__init__(target, True, shallow)
7273

7374

7475
# The proxy classes are assembled dynamically from the trap functions,

observ/list_proxy.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, cast
3+
from typing import cast
44

55
from .proxy import TYPE_LOOKUP, Proxy
66
from .traps import construct_methods_traps_dict, trap_map, trap_map_readonly
@@ -53,11 +53,12 @@ class ListProxyBase(Proxy[list]):
5353

5454

5555
def readonly_list_proxy_init(
56-
self: ListProxyBase, target: list, shallow: bool = False, **kwargs: Any
56+
self: ListProxyBase, target: list, readonly: bool = True, shallow: bool = False
5757
) -> None:
58-
super(ReadonlyListProxy, self).__init__(
59-
target, shallow=shallow, **{**kwargs, "readonly": True}
60-
)
58+
# The signature lines up with Proxy.__init__ so that proxy() can
59+
# construct any proxy type positionally; the readonly argument is
60+
# ignored, a ReadonlyListProxy is always readonly
61+
super(ReadonlyListProxy, self).__init__(target, True, shallow)
6162

6263

6364
# The proxy classes are assembled dynamically from the trap functions,

observ/proxy.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,21 @@ def proxy(target: T, readonly: bool = False, shallow: bool = False) -> T:
102102

103103
# Note that at this point, target is always a non-proxy object
104104
# Check the proxy_db to see if there's already a proxy for the target object
105-
existing_proxy: Any = proxy_db.get_proxy(target, readonly=readonly, shallow=shallow)
105+
# NB: the calls below pass the flags positionally, since keyword
106+
# arguments make a call measurably slower
107+
existing_proxy: Any = proxy_db.get_proxy(target, readonly, shallow)
106108
if existing_proxy is not None:
107109
return existing_proxy
108110

109111
# Create a new proxy
110112
proxy_types = TYPE_LOOKUP.get(type(target))
111113
if proxy_types is not None:
112114
proxy_type = proxy_types[1] if readonly else proxy_types[0]
113-
new_proxy: Any = proxy_type(target, readonly=readonly, shallow=shallow)
115+
new_proxy: Any = proxy_type(target, readonly, shallow)
114116
return new_proxy
115117

116118
if isinstance(target, tuple):
117-
return cast(
118-
T, tuple(proxy(x, readonly=readonly, shallow=shallow) for x in target)
119-
)
119+
return cast(T, tuple(proxy(x, readonly, shallow) for x in target))
120120

121121
# We can't proxy a plain value
122122
return target

observ/set_proxy.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, cast
3+
from typing import cast
44

55
from .proxy import TYPE_LOOKUP, Proxy
66
from .traps import construct_methods_traps_dict, trap_map, trap_map_readonly
@@ -62,11 +62,12 @@ class SetProxyBase(Proxy[set]):
6262

6363

6464
def readonly_set_proxy_init(
65-
self: SetProxyBase, target: set, shallow: bool = False, **kwargs: Any
65+
self: SetProxyBase, target: set, readonly: bool = True, shallow: bool = False
6666
) -> None:
67-
super(ReadonlySetProxy, self).__init__(
68-
target, shallow=shallow, **{**kwargs, "readonly": True}
69-
)
67+
# The signature lines up with Proxy.__init__ so that proxy() can
68+
# construct any proxy type positionally; the readonly argument is
69+
# ignored, a ReadonlySetProxy is always readonly
70+
super(ReadonlySetProxy, self).__init__(target, True, shallow)
7071

7172

7273
# The proxy classes are assembled dynamically from the trap functions,

tests/test_proxy.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,59 @@ def test_proxy_types_registered_without_extra_imports():
356356
],
357357
check=True,
358358
)
359+
360+
361+
def test_proxy_flag_call_conventions():
362+
"""
363+
The readonly and shallow flags resolve identically whether they
364+
are passed positionally or as keywords: every combination maps to
365+
the same (cached) proxy for the same target.
366+
"""
367+
data = {"foo": "bar"}
368+
writable = proxy(data, readonly=False, shallow=False)
369+
readonly_proxy = proxy(data, readonly=True)
370+
shallow = proxy(data, shallow=True)
371+
readonly_shallow = proxy(data, readonly=True, shallow=True)
372+
373+
# All four configurations are distinct proxies
374+
configs = (writable, readonly_proxy, shallow, readonly_shallow)
375+
assert len({id(p) for p in configs}) == 4
376+
377+
# Positional calls resolve to the same proxies as keyword calls
378+
assert proxy(data, False, False) is writable
379+
assert proxy(data, True) is readonly_proxy
380+
assert proxy(data, False, True) is shallow
381+
assert proxy(data, True, True) is readonly_shallow
382+
383+
assert writable.__readonly__ is False and writable.__shallow__ is False
384+
assert readonly_proxy.__readonly__ is True
385+
assert readonly_proxy.__shallow__ is False
386+
assert shallow.__readonly__ is False and shallow.__shallow__ is True
387+
assert readonly_shallow.__readonly__ is True
388+
assert readonly_shallow.__shallow__ is True
389+
390+
391+
def test_readonly_proxy_construction_forces_readonly():
392+
"""
393+
Constructing a Readonly* proxy class directly always yields a
394+
readonly proxy, whatever flags are passed, and the shallow flag
395+
still comes through.
396+
"""
397+
for proxy_type, target in (
398+
(ReadonlyDictProxy, {"foo": "bar"}),
399+
(ReadonlyListProxy, ["foo"]),
400+
(ReadonlySetProxy, {"foo"}),
401+
):
402+
readonly_proxy = proxy_type(target)
403+
assert readonly_proxy.__readonly__ is True
404+
assert readonly_proxy.__shallow__ is False
405+
406+
# A passed readonly flag is ignored
407+
del readonly_proxy
408+
overridden = proxy_type(target, readonly=False)
409+
assert overridden.__readonly__ is True
410+
411+
del overridden
412+
shallow = proxy_type(target, shallow=True)
413+
assert shallow.__readonly__ is True
414+
assert shallow.__shallow__ is True

0 commit comments

Comments
 (0)