11from __future__ import annotations
22
33from functools import partial , wraps
4- from operator import xor
54from typing import TYPE_CHECKING , Any
65
76from .dep import Dep
1918 # given container type
2019 TrapFactory = Callable [[str , type ], Trap ]
2120
21+ # Performance notes that apply to all trap factories below. Traps are
22+ # the per-read/per-write entry points of observ, so their call overhead
23+ # is felt in every interaction with a proxy:
24+ # - The Dep.stack list (a ClassVar that is only ever mutated, never
25+ # reassigned) is bound to a closure variable, which is cheaper to
26+ # load than a global plus an attribute
27+ # - Dep.depend() is inlined as dep_stack[-1].add_dep(dep), which saves
28+ # a method call plus a second check of the stack per tracked read
29+ # - proxy() is called with positional arguments; keyword arguments
30+ # make a call measurably slower
31+ # - Trap signatures only take **kwargs when a wrapped method actually
32+ # accepts keyword arguments (only list.sort does; dict.update is
33+ # handled explicitly), because **kwargs allocates a dict on every
34+ # call. The wrapped plain containers raise TypeError for unexpected
35+ # keyword arguments, and so do the traps
36+
2237
2338class ReadonlyError (Exception ):
2439 """
@@ -30,54 +45,63 @@ class ReadonlyError(Exception):
3045
3146def read_trap (method : str , obj_cls : type ) -> Trap :
3247 fn = getattr (obj_cls , method )
48+ dep_stack = Dep .stack
3349
3450 @wraps (fn )
35- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
36- if Dep . stack :
37- self .__dep__ . depend ( )
38- value = fn (self .__target__ , * args , ** kwargs )
51+ def trap (self : Proxy [Any ], * args : Any ) -> Any :
52+ if dep_stack :
53+ dep_stack [ - 1 ]. add_dep ( self .__dep__ )
54+ value = fn (self .__target__ , * args )
3955 if self .__shallow__ :
4056 return value
41- return proxy (value , readonly = self .__readonly__ )
57+ return proxy (value , self .__readonly__ )
4258
4359 return trap
4460
4561
62+ # The proxy function with the readonly flag pre-bound, for both flag
63+ # values, so that iterate_trap doesn't construct a partial per call
64+ _PROXY_PARTIAL = partial (proxy , readonly = False )
65+ _PROXY_PARTIAL_READONLY = partial (proxy , readonly = True )
66+
67+
4668def iterate_trap (method : str , obj_cls : type ) -> Trap :
4769 fn = getattr (obj_cls , method )
4870 # Hoist the method check out of the trap
4971 is_items = method == "items"
72+ dep_stack = Dep .stack
5073
74+ # The wrapped iterator methods (items, values, keys, __iter__,
75+ # __reversed__) take no arguments at all
5176 @wraps (fn )
52- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
53- if Dep . stack :
54- self .__dep__ . depend ( )
55- iterator = fn (self .__target__ , * args , ** kwargs )
77+ def trap (self : Proxy [Any ]) -> Any :
78+ if dep_stack :
79+ dep_stack [ - 1 ]. add_dep ( self .__dep__ )
80+ iterator = fn (self .__target__ )
5681 if self .__shallow__ :
5782 return iterator
83+ readonly = self .__readonly__
5884 if is_items :
59- return (
60- (key , proxy (value , readonly = self .__readonly__ ))
61- for key , value in iterator
62- )
85+ return ((key , proxy (value , readonly )) for key , value in iterator )
6386 else :
64- proxied = partial ( proxy , readonly = self . __readonly__ )
87+ proxied = _PROXY_PARTIAL_READONLY if readonly else _PROXY_PARTIAL
6588 return map (proxied , iterator )
6689
6790 return trap
6891
6992
7093def read_key_trap (method : str , obj_cls : type ) -> Trap :
7194 fn = getattr (obj_cls , method )
95+ dep_stack = Dep .stack
7296
7397 @wraps (fn )
74- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
75- if Dep . stack :
76- self .__dep__ .keydep (args [ 0 ]). depend ( )
77- value = fn (self .__target__ , * args , ** kwargs )
98+ def trap (self : Proxy [Any ], key : Any , * args : Any ) -> Any :
99+ if dep_stack :
100+ dep_stack [ - 1 ]. add_dep ( self .__dep__ .keydep (key ) )
101+ value = fn (self .__target__ , key , * args )
78102 if self .__shallow__ :
79103 return value
80- return proxy (value , readonly = self .__readonly__ )
104+ return proxy (value , self .__readonly__ )
81105
82106 return trap
83107
@@ -115,27 +139,29 @@ def write_trap(method: str, obj_cls: type) -> Trap:
115139def write_dict_trap (method : str , obj_cls : type ) -> Trap :
116140 fn = getattr (obj_cls , method )
117141
142+ # The signature mirrors dict.update: at most one positional-only
143+ # argument (a mapping or an iterable of key/value pairs), plus
144+ # arbitrary keyword arguments
118145 @wraps (fn )
119- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
146+ def trap (self : Proxy [Any ], positional : Any = _MISSING , / , ** kwargs : Any ) -> Any :
120147 target = self .__target__
121- # Normalize the arguments (an optional mapping or iterable of
122- # key/value pairs, plus optional keyword arguments) into a
123- # single dict, so that only the incoming keys have to be
124- # diffed for changes. This also makes sure that an iterable
125- # argument is not consumed twice
126- if args :
127- incoming = dict (args [0 ])
148+ target_get = target .get
149+ # Normalize the arguments into a single dict, so that only the
150+ # incoming keys have to be diffed for changes. This also makes
151+ # sure that an iterable argument is not consumed twice
152+ if positional is not _MISSING :
153+ incoming = dict (positional )
128154 if kwargs :
129155 incoming .update (kwargs )
130156 else :
131157 incoming = kwargs
132- old_values = {key : target . get (key , _MISSING ) for key in incoming }
158+ old_values = {key : target_get (key , _MISSING ) for key in incoming }
133159 retval = fn (target , incoming )
134160 dep = self .__dep__
135161 keydeps = dep .keydeps if dep .keydeps is not None else _NO_KEYDEPS
136162 change_detected = False
137163 for key , old_value in old_values .items ():
138- if old_value is not target . get (key , _MISSING ):
164+ if old_value is not target_get (key , _MISSING ):
139165 keydep = keydeps .get (key )
140166 if keydep is not None :
141167 keydep .notify ()
@@ -151,10 +177,10 @@ def write_len_compare_trap(method: str, obj_cls: type) -> Trap:
151177 fn = getattr (obj_cls , method )
152178
153179 @wraps (fn )
154- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
180+ def trap (self : Proxy [Any ], * args : Any ) -> Any :
155181 target = self .__target__
156182 old_len = len (target )
157- retval = fn (target , * args , ** kwargs )
183+ retval = fn (target , * args )
158184 if len (target ) != old_len :
159185 self .__dep__ .notify ()
160186 return retval
@@ -165,6 +191,8 @@ def trap(self: Proxy[Any], *args: Any, **kwargs: Any) -> Any:
165191def write_copy_compare_trap (method : str , obj_cls : type ) -> Trap :
166192 fn = getattr (obj_cls , method )
167193
194+ # list.sort takes keyword arguments (key and reverse), so this is
195+ # the one write trap that must accept **kwargs
168196 @wraps (fn )
169197 def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
170198 target = self .__target__
@@ -212,11 +240,10 @@ def write_key_trap(method: str, obj_cls: type) -> Trap:
212240 is_setdefault = method == "setdefault"
213241
214242 @wraps (fn )
215- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
243+ def trap (self : Proxy [Any ], key : Any , * args : Any ) -> Any :
216244 target = self .__target__
217- key = args [0 ]
218245 old_value = getitem_fn (target , key , _MISSING )
219- retval = fn (target , * args , ** kwargs )
246+ retval = fn (target , key , * args )
220247 if is_setdefault and not self .__shallow__ :
221248 # This method is only available when readonly is false
222249 retval = proxy (retval )
@@ -227,7 +254,7 @@ def trap(self: Proxy[Any], *args: Any, **kwargs: Any) -> Any:
227254 # (e.g. PySide6's ItemFlags), see test_use_weird_types_as_value
228255 if old_value is not new_value and (
229256 old_value is _MISSING
230- or xor (old_value is None , new_value is None )
257+ or (old_value is None ) != ( new_value is None )
231258 or old_value != new_value
232259 ):
233260 dep = self .__dep__
@@ -245,9 +272,10 @@ def trap(self: Proxy[Any], *args: Any, **kwargs: Any) -> Any:
245272def delete_trap (method : str , obj_cls : type ) -> Trap :
246273 fn = getattr (obj_cls , method )
247274
275+ # The wrapped deleter methods (clear, popitem) take no arguments
248276 @wraps (fn )
249- def trap (self : DictProxyBase , * args : Any , ** kwargs : Any ) -> Any :
250- retval = fn (self .__target__ , * args , ** kwargs )
277+ def trap (self : DictProxyBase ) -> Any :
278+ retval = fn (self .__target__ )
251279 dep = self .__dep__
252280 dep .notify ()
253281 keydeps = dep .keydeps if dep .keydeps is not None else _NO_KEYDEPS
@@ -266,10 +294,9 @@ def delete_key_trap(method: str, obj_cls: type) -> Trap:
266294 fn = getattr (obj_cls , method )
267295
268296 @wraps (fn )
269- def trap (self : Proxy [Any ], * args : Any , ** kwargs : Any ) -> Any :
270- key = args [0 ]
297+ def trap (self : Proxy [Any ], key : Any , * args : Any ) -> Any :
271298 key_existed = key in self .__target__
272- retval = fn (self .__target__ , * args , ** kwargs )
299+ retval = fn (self .__target__ , key , * args )
273300 if key_existed :
274301 dep = self .__dep__
275302 dep .notify ()
0 commit comments