22import dis
33import functools
44import inspect
5+ import sys
56from collections .abc import Mapping
67from types import CodeType , FunctionType
78from tlz import concatv , merge
9+ from . import cfg
810
911try :
1012 # Added in Python 3.8
@@ -350,9 +352,24 @@ class ScopedFunction:
350352 2
351353 """
352354
353- def __init__ (self , func , * mappings , use_closures = True , use_globals = True ):
355+ def __init__ (self , func , * mappings , use_closures = True , use_globals = True , method = "default" ):
354356 self .use_closures = use_closures
355357 self .use_globals = use_globals
358+ if method == "default" :
359+ method = cfg .default_method
360+ if method == "default" :
361+ raise ValueError (
362+ 'Who would set the default method to "default"? That\' s just silly!\n '
363+ 'Please set ``innerscope.cfg.default_method`` back to "bytecode", '
364+ "and then please continue doing what you're doing, because it's probably "
365+ "something awesome :)"
366+ )
367+ if method not in {"bytecode" , "trace" }:
368+ raise ValueError (
369+ 'method= argument to ScopedFunc must be "bytecode", "trace", or "default". '
370+ f"Got { method !r} . Using the default method is recommended."
371+ )
372+ self .method = method
356373 if isinstance (func , ScopedFunction ):
357374 self .func = func .func
358375 code = func .func .__code__
@@ -457,12 +474,42 @@ def __call__(self, *args, **kwargs):
457474 outer_scope ["__builtins__" ] = builtins
458475 outer_scope ["_innerscope_locals_" ] = locals
459476 outer_scope ["_innerscope_secret_" ] = secret = object ()
477+ is_trace = self .method == "trace"
478+ if is_trace :
479+ code = self .func .__code__
480+ prev_trace = sys .gettrace ()
481+ info = {}
482+
483+ # coverage uses sys.settrace, so I don't know how to cover this function
484+ def trace_func (frame , event , arg ): # pragma: no cover
485+ if prev_trace is not None :
486+ prev = prev_trace (frame , event , arg )
487+ sys .settrace (trace_func )
488+ else :
489+ prev = None
490+ if event != "call" or frame .f_code is not func .__code__ :
491+ return prev
492+
493+ def trace_returns (frame , event , arg ):
494+ if event == "return" :
495+ info ["locals" ] = frame .f_locals
496+ if prev is not None :
497+ prev (frame , event , arg )
498+
499+ frame .f_trace = trace_returns
500+ return trace_returns
501+
502+ else :
503+ code = self ._code
504+
460505 func = FunctionType (
461- self . _code , outer_scope , argdefs = self .func .__defaults__ , closure = self ._closure
506+ code , outer_scope , argdefs = self .func .__defaults__ , closure = self ._closure
462507 )
463508 func .__kwdefaults__ = self .func .__kwdefaults__
464509 try :
465- results = func (* args , ** kwargs )
510+ if is_trace :
511+ sys .settrace (trace_func )
512+ return_value = func (* args , ** kwargs )
466513 except UnboundLocalError as exc :
467514 message = exc .args and exc .args [0 ] or ""
468515 if message .startswith ("local variable " ) and message .endswith (
@@ -482,18 +529,25 @@ def __call__(self, *args, **kwargs):
482529 ) from exc
483530 else :
484531 raise
485- try :
486- return_value , inner_scope , expect_secret = results
487- if secret is expect_secret :
488- del outer_scope ["__builtins__" ]
489- del outer_scope ["_innerscope_locals_" ]
490- del outer_scope ["_innerscope_secret_" ]
491- # closures show up in locals, but we want them only in outer_scope
492- for key in self ._code .co_freevars :
493- del inner_scope [key ]
494- return Scope (self , outer_scope , return_value , inner_scope )
495- except Exception :
496- pass
532+ finally :
533+ if is_trace :
534+ sys .settrace (prev_trace )
535+ if is_trace :
536+ inner_scope = info ["locals" ]
537+ else :
538+ try :
539+ return_value , inner_scope , expect_secret = return_value
540+ except Exception :
541+ expect_secret = None
542+ if is_trace or secret is expect_secret :
543+ del outer_scope ["__builtins__" ]
544+ del outer_scope ["_innerscope_locals_" ]
545+ del outer_scope ["_innerscope_secret_" ]
546+ # closures show up in locals, but we want them only in outer_scope
547+ for key in self ._code .co_freevars :
548+ del inner_scope [key ]
549+ return Scope (self , outer_scope , return_value , inner_scope )
550+
497551 return_indices = [
498552 inst .offset for inst in dis .get_instructions (self .func ) if inst .opname == "RETURN_VALUE"
499553 ]
@@ -523,7 +577,10 @@ def __call__(self, *args, **kwargs):
523577 f"\n \n { return_msg } The next closest return statement is { next_closest } bytes away."
524578 "\n \n A cute workaround is to add code such as `if bool(): return`."
525579 "\n \n If it's important to you that this limitation is fixed, then please submit "
526- "an issue (or a pull request!) to:\n https://github.com/eriknw/innerscope"
580+ "an issue (or a pull request!) to:\n https://github.com/eriknw/innerscope\n \n "
581+ 'Another workaround is to use `method="trace"` in ScopedFunction. This will '
582+ "usually work, but it will be slower and may cause havoc if `sys.settrace` is "
583+ "used by anything else. The default method is preferred if possible."
527584 )
528585
529586 def bind (self , * mappings , ** kwargs ):
@@ -592,7 +649,7 @@ def _repr_html_(self):
592649 )
593650
594651
595- def scoped_function (func = None , * mappings , use_closures = True , use_globals = True ):
652+ def scoped_function (func = None , * mappings , use_closures = True , use_globals = True , method = "default" ):
596653 """Use to expose the inner scope of a wrapped function after being called.
597654
598655 The wrapped function should have no return statements. Instead of a return value,
@@ -625,17 +682,32 @@ def scoped_function(func=None, *mappings, use_closures=True, use_globals=True):
625682
626683 def inner_scoped_func (func ):
627684 return ScopedFunction (
628- func , * mappings , use_closures = use_closures , use_globals = use_globals
685+ func ,
686+ * mappings ,
687+ use_closures = use_closures ,
688+ use_globals = use_globals ,
689+ method = method ,
629690 )
630691
631692 return inner_scoped_func
632693
633694 if isinstance (func , Mapping ):
634695 return scoped_function (
635- None , func , * mappings , use_closures = use_closures , use_globals = use_globals
696+ None ,
697+ func ,
698+ * mappings ,
699+ use_closures = use_closures ,
700+ use_globals = use_globals ,
701+ method = method ,
636702 )
637703
638- return ScopedFunction (func , * mappings , use_closures = use_closures , use_globals = use_globals )
704+ return ScopedFunction (
705+ func ,
706+ * mappings ,
707+ use_closures = use_closures ,
708+ use_globals = use_globals ,
709+ method = method ,
710+ )
639711
640712
641713def bindwith (* mappings , ** kwargs ):
0 commit comments