11from copy import copy
2- from inspect import isclass , signature , Signature
2+ from inspect import isclass , signature , Signature , getmodule
33from typing import (
44 Annotated ,
55 AnyStr ,
@@ -122,14 +122,24 @@ def can_call(self, func):
122122def _get_external (module_name : str , access_path : Sequence [str ]):
123123 """Get value from external module given a dotted access path.
124124
125+ Only gets value if the module is already imported.
126+
125127 Raises:
126128 * `KeyError` if module is removed not found, and
127129 * `AttributeError` if access path does not match an exported object
128130 """
129- member_type = sys .modules [module_name ]
130- for attr in access_path :
131- member_type = getattr (member_type , attr )
132- return member_type
131+ try :
132+ member_type = sys .modules [module_name ]
133+ # standard module
134+ for attr in access_path :
135+ member_type = getattr (member_type , attr )
136+ return member_type
137+ except (KeyError , AttributeError ):
138+ # handle modules in namespace packages
139+ module_path = "." .join ([module_name , * access_path ])
140+ if module_path in sys .modules :
141+ return sys .modules [module_path ]
142+ raise
133143
134144
135145def _has_original_dunder_external (
@@ -139,18 +149,26 @@ def _has_original_dunder_external(
139149 method_name : str ,
140150):
141151 if module_name not in sys .modules :
142- # LBYLB as it is faster
143- return False
152+ full_module_path = "." .join ([module_name , * access_path ])
153+ if full_module_path not in sys .modules :
154+ # LBYLB as it is faster
155+ return False
144156 try :
145157 member_type = _get_external (module_name , access_path )
146158 value_type = type (value )
147159 if type (value ) == member_type :
148160 return True
161+ if isinstance (member_type , ModuleType ):
162+ value_module = getmodule (value_type )
163+ if not value_module or not value_module .__name__ :
164+ return False
165+ if value_module .__name__ .startswith (member_type .__name__ ):
166+ return True
149167 if method_name == "__getattribute__" :
150168 # we have to short-circuit here due to an unresolved issue in
151169 # `isinstance` implementation: https://bugs.python.org/issue32683
152170 return False
153- if isinstance (value , member_type ):
171+ if not isinstance ( member_type , ModuleType ) and isinstance (value , member_type ):
154172 method = getattr (value_type , method_name , None )
155173 member_method = getattr (member_type , method_name , None )
156174 if member_method == method :
@@ -185,35 +203,47 @@ def _has_original_dunder(
185203 return False
186204
187205
206+ def _coerce_path_to_tuples (
207+ allow_list : set [tuple [str , ...] | str ],
208+ ) -> set [tuple [str , ...]]:
209+ """Replace dotted paths on the provided allow-list with tuples."""
210+ return {
211+ path if isinstance (path , tuple ) else tuple (path .split ("." ))
212+ for path in allow_list
213+ }
214+
215+
188216@undoc
189217@dataclass
190218class SelectivePolicy (EvaluationPolicy ):
191219 allowed_getitem : set [InstancesHaveGetItem ] = field (default_factory = set )
192- allowed_getitem_external : set [tuple [str , ...]] = field (default_factory = set )
220+ allowed_getitem_external : set [tuple [str , ...] | str ] = field (default_factory = set )
193221
194222 allowed_getattr : set [MayHaveGetattr ] = field (default_factory = set )
195- allowed_getattr_external : set [tuple [str , ...]] = field (default_factory = set )
223+ allowed_getattr_external : set [tuple [str , ...] | str ] = field (default_factory = set )
196224
197225 allowed_operations : set = field (default_factory = set )
198- allowed_operations_external : set [tuple [str , ...]] = field (default_factory = set )
226+ allowed_operations_external : set [tuple [str , ...] | str ] = field (default_factory = set )
199227
200228 _operation_methods_cache : dict [str , set [Callable ]] = field (
201229 default_factory = dict , init = False
202230 )
203231
204232 def can_get_attr (self , value , attr ):
233+ allowed_getattr_external = _coerce_path_to_tuples (self .allowed_getattr_external )
234+
205235 has_original_attribute = _has_original_dunder (
206236 value ,
207237 allowed_types = self .allowed_getattr ,
208238 allowed_methods = self ._getattribute_methods ,
209- allowed_external = self . allowed_getattr_external ,
239+ allowed_external = allowed_getattr_external ,
210240 method_name = "__getattribute__" ,
211241 )
212242 has_original_attr = _has_original_dunder (
213243 value ,
214244 allowed_types = self .allowed_getattr ,
215245 allowed_methods = self ._getattr_methods ,
216- allowed_external = self . allowed_getattr_external ,
246+ allowed_external = allowed_getattr_external ,
217247 method_name = "__getattr__" ,
218248 )
219249
@@ -245,7 +275,7 @@ def can_get_attr(self, value, attr):
245275 return True # pragma: no cover
246276
247277 # Properties in subclasses of allowed types may be ok if not changed
248- for module_name , * access_path in self . allowed_getattr_external :
278+ for module_name , * access_path in allowed_getattr_external :
249279 try :
250280 external_class = _get_external (module_name , access_path )
251281 external_class_attr_val = getattr (external_class , attr )
@@ -257,15 +287,19 @@ def can_get_attr(self, value, attr):
257287
258288 def can_get_item (self , value , item ):
259289 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
290+ allowed_getitem_external = _coerce_path_to_tuples (self .allowed_getitem_external )
260291 return _has_original_dunder (
261292 value ,
262293 allowed_types = self .allowed_getitem ,
263294 allowed_methods = self ._getitem_methods ,
264- allowed_external = self . allowed_getitem_external ,
295+ allowed_external = allowed_getitem_external ,
265296 method_name = "__getitem__" ,
266297 )
267298
268299 def can_operate (self , dunders : tuple [str , ...], a , b = None ):
300+ allowed_operations_external = _coerce_path_to_tuples (
301+ self .allowed_operations_external
302+ )
269303 objects = [a ]
270304 if b is not None :
271305 objects .append (b )
@@ -275,7 +309,7 @@ def can_operate(self, dunders: tuple[str, ...], a, b=None):
275309 obj ,
276310 allowed_types = self .allowed_operations ,
277311 allowed_methods = self ._operator_dunder_methods (dunder ),
278- allowed_external = self . allowed_operations_external ,
312+ allowed_external = allowed_operations_external ,
279313 method_name = dunder ,
280314 )
281315 for dunder in dunders
@@ -586,7 +620,12 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
586620 dunders = _find_dunder (node .op , UNARY_OP_DUNDERS )
587621 if dunders :
588622 if policy .can_operate (dunders , value ):
589- return getattr (value , dunders [0 ])()
623+ try :
624+ return getattr (value , dunders [0 ])()
625+ except AttributeError :
626+ raise TypeError (
627+ f"bad operand type for unary { node .op } : { type (value )} "
628+ )
590629 else :
591630 raise GuardRejection (
592631 f"Operation (`{ dunders } `) for" ,
0 commit comments