I'm trying to make a quick and dirty caching system for Python, using the trick that a context-manager can be made to conditionally skip the code in its context — see Skipping execution of -with- block. I've stumbled upon a weird failure case of this and I was wondering if someone can help understand and fix this.
Before anyone says this, I know what I'm doing is terrible and I shouldn't do it, etc, etc.
Anyway, here is the code for the tricky context manager:
import sys
import inspect
class SkippableContext(object):
def __init__(self,mode=0):
"""
if mode = 0, proceed as normal
if mode = 1, do not execute block
"""
self.mode=mode
def __enter__(self):
if self.mode==1:
print(' ... Skipping Context')
# Do some magic
sys.settrace(lambda *args, **keys: None)
frame = inspect.currentframe(1)
frame.f_trace = self.trace
return 'SET BY TRICKY CONTEXT MANAGER!!'
def trace(self, frame, event, arg):
raise
def __exit__(self, type, value, traceback):
return True
And here is the test code:
print('==== First Pass with skipping disabled ====')
c='not set'
with SkippableContext(mode=0) as c:
print('Should Get into here')
c = 'set in context'
print('c: {}'.format(c))
print('==== Second Pass with skipping enabled ====')
c='not set'
with SkippableContext(mode=1) as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
c='not set'
with SkippableContext(mode=1) as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
print('==== Third Pass: Same as second pass but in a loop ====')
for i in range(2):
c='not set'
with SkippableContext(mode=1) as c: # For some reason, assinging c fails on the second iteration!
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
The output generated by the test code is as expected, except for the very last line, where c is not set:
==== First Pass with skipping disabled ====
Should Get into here
c: set in context
==== Second Pass with skipping enabled ====
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
==== Third Pass: Same as second pass but in a loop ====
... Skipping Context
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
c: not set
Why is c not set in the second run of the loop? Is there some hack to fix the bug in this hack?
The awful hack you're using does a lot of things with nasty, subtle consequences. I doubt the author fully understood it (if they did, they wouldn't have used a bare raise, and they wouldn't have tried to pass inspect.currentframe an argument it doesn't take). Incidentally, the incorrect usage of inspect.currentframe causes the code to fail with a TypeError instead of doing what you describe, so for the rest of this answer, I'll assume that call is replaced with sys._getframe(1), which produces the described behavior.
One of the things the hack relies on is setting a local trace function with frame.f_trace = self.trace. This local trace function will raise an exception on the first line inside the with block... or at least, that's what it normally does.
Python calls trace functions when certain trace events happen. One of those trace events is the start of a new source line. Python determines that a new source line has started by checking whether the current bytecode instruction index corresponds to either the first instruction of a line, or an instruction at an index prior to the last instruction executed. You can see that in maybe_call_line_trace in Python/ceval.c.
Python only updates instr_prev, the variable used to determine the last instruction executed, when tracing is active. However, once the local trace function raises an exception, it is automatically deactivated, and instr_prev stops receiving updates.
When the local trace function is set, the next two instructions it could activate on are the STORE_NAME to set c (or STORE_FAST if you put the code in a function), and the LOAD_NAME to load the print function for the next line (or LOAD_GLOBAL if you put the code in a function).
The first time through the loop, it activates on LOAD_NAME, and instr_prev is set to that instruction's index. The local trace function is then disabled, because it raised an exception.
The second time through the loop, instr_prev is still set to the index of the LOAD_NAME, so Python thinks the STORE_NAME marks the beginning of a new line. The local trace function activates on STORE_NAME, and the exception prevents the assignment to c.
You can see the instructions where the local trace function activates by inspecting frame.f_lasti in trace, and comparing the results to the instruction indices in the output of dis.dis. For example, the following variant of your code:
import sys
import inspect
import dis
class SkippableContext(object):
def __enter__(self):
print(' ... Skipping Context')
sys.settrace(lambda *args, **keys: None)
frame = sys._getframe(1)
frame.f_trace = self.trace
return 'SET BY TRICKY CONTEXT MANAGER!!'
def trace(self, frame, event, arg):
print(frame.f_lasti)
raise Exception
def __exit__(self, type, value, traceback):
return True
def f():
for i in range(2):
c='not set'
with SkippableContext() as c:
print('This code is not printed')
c = 'set in context'
print('c: {}'.format(c))
f()
dis.dis(f)
produces the following output:
... Skipping Context
26
c: SET BY TRICKY CONTEXT MANAGER!!
... Skipping Context
24
c: not set
21 0 SETUP_LOOP 64 (to 66)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (2)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 52 (to 64)
12 STORE_FAST 0 (i)
22 14 LOAD_CONST 2 ('not set')
16 STORE_FAST 1 (c)
23 18 LOAD_GLOBAL 1 (SkippableContext)
20 CALL_FUNCTION 0
22 SETUP_WITH 18 (to 42)
24 STORE_FAST 1 (c)
24 26 LOAD_GLOBAL 2 (print)
28 LOAD_CONST 3 ('This code is not printed')
30 CALL_FUNCTION 1
32 POP_TOP
25 34 LOAD_CONST 4 ('set in context')
36 STORE_FAST 1 (c)
38 POP_BLOCK
40 LOAD_CONST 0 (None)
>> 42 WITH_CLEANUP_START
44 WITH_CLEANUP_FINISH
46 END_FINALLY
26 48 LOAD_GLOBAL 2 (print)
50 LOAD_CONST 5 ('c: {}')
52 LOAD_METHOD 3 (format)
54 LOAD_FAST 1 (c)
56 CALL_METHOD 1
58 CALL_FUNCTION 1
60 POP_TOP
62 JUMP_ABSOLUTE 10
>> 64 POP_BLOCK
>> 66 LOAD_CONST 0 (None)
68 RETURN_VALUE
The 26 printed the first time corresponds to the index of the LOAD_GLOBAL, and the 24 printed the second time corresponds to the index of the STORE_FAST.
Related
How can one use type comments in Python to change or narrow the type of an already declared variable, in such a way as to make pycharm or other type-aware systems understand the new type.
For instance, I might have two classes:
class A:
is_b = False
...
class B(A):
is_b = True
def flummox(self):
return '?'
and another function elsewhere:
def do_something_to_A(a_in: A):
...
if a_in.is_b:
assert isinstance(a_in, B) # THIS IS THE LINE...
a_in.flummox()
As long as I have the assert statement, PyCharm will understand that I've narrowed a_in to be of class B, and not complain about .flummox(). Without it, errors/warnings such as a_in has no method flummox will appear.
The question I have is, is there a PEP 484 (or successor) way of showing that a_in (which might have originally been of type A or B or something else) is now of type B without having the assert statement. The statement b_in : B = a_in also gives type errors.
In TypeScript I could do something like this:
if a_in.is_b:
const b_in = <B><any> a_in;
b_in.flummox()
// or
if a_in.is_b:
(a_in as B).flummox()
There are two main reasons I don't want to use the assert line is (1) speed is very important to this part of code, and having an extra is_instance call for every time the line is run slows it down too much, and (2) a project code style that forbids bare assert statements.
So long as you are using Python 3.6+, you can "re-annotate" the type of a variable arbitrarily using the same syntax as you would use to "declare" the type of a variable without initializing it (PEP 526).
In the example you have provided, the following snippet has the behavior you expect:
def do_something_to_A(a_in: A):
...
if a_in.is_b:
a_in: B
a_in.flummox()
I have tested that this technique is properly detected by PyCharm 2019.2.
It is worth noting that this incurs no runtime cost since the same bytecode is generated with or without this added annotation statement. Given the following defintions,
def do_something_with_annotation(a_in: A):
if a_in.is_b:
a_in: B
a_in.flummox()
def do_something_without_annotation(a_in: A):
if a_in.is_b:
a_in.flummox()
dis produce the following bytecode:
>>> dis.dis(do_something_with_annotation)
3 0 LOAD_FAST 0 (a_in)
2 LOAD_ATTR 0 (is_b)
4 POP_JUMP_IF_FALSE 14
5 6 LOAD_FAST 0 (a_in)
8 LOAD_ATTR 1 (flummox)
10 CALL_FUNCTION 0
12 POP_TOP
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
>>> dis.dis(do_something_without_annotation)
3 0 LOAD_FAST 0 (a_in)
2 LOAD_ATTR 0 (is_b)
4 POP_JUMP_IF_FALSE 14
4 6 LOAD_FAST 0 (a_in)
8 LOAD_ATTR 1 (flummox)
10 CALL_FUNCTION 0
12 POP_TOP
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
As a side note, you could also keep the assertion statements and disable assertions in your production environment by invoking the interpreter with the -O flag. This may or may not be considered more readable by your colleagues, depending on their familiarity with type hinting in Python.
At work, I stumbled upon an except clause with an or operator:
try:
# Do something.
except IndexError or KeyError:
# ErrorHandling
I know the exception classes should be passed as a tuple, but it bugged me that it wouldn't even cause a SyntaxError.
So first I wanted to investigate whether it actually works. And it doesn't.
>>> def with_or_raise(exc):
... try:
... raise exc()
... except IndexError or KeyError:
... print('Got ya!')
...
>>> with_or_raise(IndexError)
Got ya!
>>> with_or_raise(KeyError)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in with_or_raise
KeyError
So it did not catch the second exception, and looking at the bytecode, it becomes clearer why:
>>> import dis
>>> dis.dis(with_or_raise)
2 0 SETUP_EXCEPT 10 (to 12)
3 2 LOAD_FAST 0 (exc)
4 CALL_FUNCTION 0
6 RAISE_VARARGS 1
8 POP_BLOCK
10 JUMP_FORWARD 32 (to 44)
4 >> 12 DUP_TOP
14 LOAD_GLOBAL 0 (IndexError)
16 JUMP_IF_TRUE_OR_POP 20
18 LOAD_GLOBAL 1 (KeyError)
>> 20 COMPARE_OP 10 (exception match)
22 POP_JUMP_IF_FALSE 42
24 POP_TOP
26 POP_TOP
28 POP_TOP
5 30 LOAD_GLOBAL 2 (print)
32 LOAD_CONST 1 ('Got ya!')
34 CALL_FUNCTION 1
36 POP_TOP
38 POP_EXCEPT
40 JUMP_FORWARD 2 (to 44)
>> 42 END_FINALLY
>> 44 LOAD_CONST 0 (None)
46 RETURN_VALUE
So we can see, instruction 14 first loads the IndexError class onto the stack. Then it checks whether that value is True, which it is because of Python truthiness and finally jumps directly to instruction 20 where the exception match is done. Since instruction 18 was skipped, KeyError was never loaded onto the stack and therefore doesn't match.
I tried with Python 2.7 and 3.6, same result.
But then, why is it valid syntax? I imagine it being one of the following:
It's an artifact from a really old version of Python.
There is actually a valid use case for using or within an except clause.
It's simply a limitation of the Python parser which might have to accept any expression after the except keyword.
My vote is on 3 (given I saw some discussion about a new parser for Python) but I'm hoping someone can confirm that hypothesis. Because if it was 2 for example, I want to know that use case!
Also, I'm a bit clueless on how I'd continue that exploration. I imagine I would have to dig into CPython parser's source code but idk where to find it and maybe there's an easier way?
In except e, e can be any valid Python expression:
try1_stmt ::= "try" ":" suite
("except" [expression ["as" identifier]] ":" suite)+
...
[..] For an except clause with an expression, that expression is evaluated, and the clause matches the exception if the resulting object is “compatible” with the exception. An object is compatible with an exception if it is the class or a base class of the exception object or a tuple containing an item compatible with the exception.
https://docs.python.org/3/reference/compound_stmts.html#the-try-statement
The expression IndexError or KeyError yields the value IndexError. So this is equivalent to:
except IndexError:
...
You should use a n-tuple of types instead of a logical expression (which just returns the first non-false element):
def with_or_raise(exc):
try:
raise exc()
except (IndexError,KeyError):
print('Got ya!')
Imagine this simple function creating a modified value of a variable default, modified:
default = 0
def modify():
modified = default + 1
print(modified) # replace with OS call, I can't see the output
modify() # 1
default # 0
disassembled:
import dis
dis.dis(modify)
2 0 LOAD_GLOBAL 0 (default)
3 LOAD_CONST 1 (1)
6 BINARY_ADD
7 STORE_FAST 0 (modified)
3 10 LOAD_GLOBAL 1 (print)
13 LOAD_FAST 0 (modified)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE
I can't change the function modify(), but I know what's in it either directly (I can see the code) or indirectly (disassembly). What I need it is to get a value of the modified variable, so I though maybe there is a way how to remove specific parts (print(modified)) of the function through dis module, but I didn't find anything.
Is there any way how to remove probably everything except return_value after 16 CALL_FUNCTION and replace it with e.g. return modified? Or is there any other way how to pull a local variable out without actually executing the last line(s)?
As a possible solution I see 3 ways:
pulling disassembled codes and creating my own function (or inplace) according to them with removing the code I don't want (everything after 16 ...)
modifying the function's return value, so that it returns modified (that unfortunately calls the OS function)
manually recreating the function according to the source code
I'd like to avoid the second way, which is probably easier than the first one, but I must avoid the third way, so... is there any way how to solve my problem?
There is a 4th option: replace the print() global:
printed = []
print = lambda *args: printed.extend(args)
modify()
del print
modified = printed[0]
It is otherwise possible to produce modified bytecode, but this can easily lead to bugs that blow up the interpreter (there is zero protection from invalid bytecode), so be warned.
You can create a new function object with a new code object with updated bytecode; based on the offsets in the dis you showed, I manually created new bytecode that would return the local variable at index 0:
>>> altered_bytecode = modify.__code__.co_code[:8] + bytes(
... [dis.opmap['LOAD_FAST'], 0, # load local variable 0 onto the stack
... dis.opmap['RETURN_VALUE']])) # and return it.
>>> dis.dis(altered_bytecode)
0 LOAD_GLOBAL 0 (0)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 STORE_FAST 0 (0)
8 LOAD_FAST 0 (0)
10 RETURN_VALUE
RETURN_VALUE returns the object at the top of the stack; all I did was inject a LOAD_FAST opcode to load what modified references onto the stack.
You'd have to create a new code object, then a new function object wrapping the code object, to make this callable:
>>> code = type(modify.__code__)
>>> function = type(modify)
>>> ocode = modify.__code__
>>> new_modify = function(
... code(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals, ocode.co_stacksize,
... ocode.co_flags, altered_bytecode,
... ocode.co_consts, ocode.co_names, ocode.co_varnames, ocode.co_filename,
... 'new_modify', ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
... ocode.co_cellvars),
... modify.__globals__, 'new_modify', modify.__defaults__, modify.__closure__)
>>> new_modify()
1
This does, obviously, require some understanding of how Python bytecode works in the first place; the dis module does contain descriptions of the various codes, and the dis.opmap dictionary lets you map back to byte values.
There are a few modules out there that try to make this easier; take a look at byteplay, the bytecode module of the pwnypack project or several others, if you want to explore this further.
I can also heartily recommend you watch the Playing with Python Bytecode presentation given by Scott Sanderson, Joe Jevnik at PyCon 2016, and play with their codetransformer module. Highly entertaining and very informative.
I was wondering how to modify byte code, then recompile that code so I can use it in python as a function? I've been trying:
a = """
def fact():
a = 8
a = 0
"""
c = compile(a, '<string>', 'exec')
w = c.co_consts[0].co_code
dis(w)
which decompiles to:
0 LOAD_CONST 1 (1)
3 STORE_FAST 1 (1)
6 LOAD_CONST 2 (2)
9 STORE_FAST 1 (1)
12 LOAD_CONST 0 (0)
15 RETURN_VALUE
supposing I want to get rid of lines 0 and 3, I call:
x = c.co_consts[0].co_code[6:16]
dis(x)
which results in :
0 LOAD_CONST 2 (2)
3 STORE_FAST 1 (1)
6 LOAD_CONST 0 (0)
9 RETURN_VALUE
my problem is what to do with x, if I try exec x I get an 'expected string without nullbytes and I get the same for exec w,
trying to compile x results in: compile() expected string without null bytes.
I'm not sure what the best way to proceed, except maybe I need to create some kind of code-object, but I'm not sure how, but I'm assuming it must be
possible aka byteplay, python assemblers et al
I'm using python 2.7.10, but I'd like it to be future compatible (Eg python 3) if it's possible.
Update: For sundry reasons I have started writing a Cross-Python-version assembler. See https://github.com/rocky/python-xasm. It is still in very early beta. See also bytecode.
As far as I know there is no other currently-maintained Python assembler. PEAK's Bytecode Disassembler was developed for Python 2.6, and later modified to support early Python 2.7.
It is pretty cool from the documentation. But it relies on other PEAK libraries which might be problematic.
I'll go through the whole example to give you a feel for what you'd have to do. It is not pretty, but then you should expect that.
Basically after modifying the bytecode, you need to create a new types.CodeType object. You need a new one because many of the objects in the code type, for good reason, you can't change. For example the interpreter may have some of these object values cached.
After creating code, you can use this in functions that use a code type which can be used in exec or eval.
Or you can write this to a bytecode file. Alas the code format has changed between Python versions 1.3, 1,5, 2.0, 3.0, 3.8, and 3.10. And by the way so has the optimization and bytecodes. In fact, in Python 3.6 they will be word codes not bytecodes.
So here is what you'd have to do for your example:
a = """
def fact():
a = 8
a = 0
return a
"""
c = compile(a, '<string>', 'exec')
fn_code = c.co_consts[0] # Pick up the function code from the main code
from dis import dis
dis(fn_code)
print("=" * 30)
x = fn_code.co_code[6:16] # modify bytecode
import types
opt_fn_code = types.CodeType(fn_code.co_argcount,
# c.co_kwonlyargcount, Add this in Python3
# c.co_posonlyargcount, Add this in Python 3.8+
fn_code.co_nlocals,
fn_code.co_stacksize,
fn_code.co_flags,
x, # fn_code.co_code: this you changed
fn_code.co_consts,
fn_code.co_names,
fn_code.co_varnames,
fn_code.co_filename,
fn_code.co_name,
fn_code.co_firstlineno,
fn_code.co_lnotab, # In general, You should adjust this
fn_code.co_freevars,
fn_code.co_cellvars)
dis(opt_fn_code)
print("=" * 30)
print("Result is", eval(opt_fn_code))
# Now let's change the value of what's returned
co_consts = list(opt_fn_code.co_consts)
co_consts[-1] = 10
opt_fn_code = types.CodeType(fn_code.co_argcount,
# c.co_kwonlyargcount, Add this in Python3
# c.co_posonlyargcount, Add this in Python 3.8+
fn_code.co_nlocals,
fn_code.co_stacksize,
fn_code.co_flags,
x, # fn_code.co_code: this you changed
tuple(co_consts), # this is now changed too
fn_code.co_names,
fn_code.co_varnames,
fn_code.co_filename,
fn_code.co_name,
fn_code.co_firstlineno,
fn_code.co_lnotab, # In general, You should adjust this
fn_code.co_freevars,
fn_code.co_cellvars)
dis(opt_fn_code)
print("=" * 30)
print("Result is now", eval(opt_fn_code))
When I ran this here is what I got:
3 0 LOAD_CONST 1 (8)
3 STORE_FAST 0 (a)
4 6 LOAD_CONST 2 (0)
9 STORE_FAST 0 (a)
5 12 LOAD_FAST 0 (a)
15 RETURN_VALUE
==============================
3 0 LOAD_CONST 2 (0)
3 STORE_FAST 0 (a)
4 6 LOAD_FAST 0 (a)
9 RETURN_VALUE
==============================
('Result is', 0)
3 0 LOAD_CONST 2 (10)
3 STORE_FAST 0 (a)
4 6 LOAD_FAST 0 (a)
9 RETURN_VALUE
==============================
('Result is now', 10)
Notice that the line numbers haven't changed even though I removed in code a couple of lines. That is because I didn't update fn_code.co_lnotab.
If you want to now write a Python bytecode file from this. Here is what you'd do:
co_consts = list(c.co_consts)
co_consts[0] = opt_fn_code
c1 = types.CodeType(c.co_argcount,
# c.co_posonlyargcount, Add this in Python 3.8+
# c.co_kwonlyargcount, Add this in Python3
c.co_nlocals,
c.co_stacksize,
c.co_flags,
c.co_code,
tuple(co_consts),
c.co_names,
c.co_varnames,
c.co_filename,
c.co_name,
c.co_firstlineno,
c.co_lnotab, # In general, You should adjust this
c.co_freevars,
c.co_cellvars)
from struct import pack
with open('/tmp/testing.pyc', 'w') as fp:
fp.write(pack('Hcc', 62211, '\r', '\n')) # Python 2.7 magic number
import time
fp.write(pack('I', int(time.time())))
# In Python 3.7+ you need to PEP 552 bits
# In Python 3 you need to write out the size mod 2**32 here
import marshal
fp.write(marshal.dumps(c1))
To simplify writing the boilerplate bytecode above, I've added a routine to xasm called write_pycfile().
Now to check the results:
$ uncompyle6 /tmp/testing.pyc
# uncompyle6 version 2.9.2
# Python bytecode 2.7 (62211)
# Disassembled from: Python 2.7.12 (default, Jul 26 2016, 22:53:31)
# [GCC 5.4.0 20160609]
# Embedded file name: <string>
# Compiled at: 2016-10-18 05:52:13
def fact():
a = 0
# okay decompiling /tmp/testing.pyc
$ pydisasm /tmp/testing.pyc
# pydisasm version 3.1.0
# Python bytecode 2.7 (62211) disassembled from Python 2.7
# Timestamp in code: 2016-10-18 05:52:13
# Method Name: <module>
# Filename: <string>
# Argument count: 0
# Number of locals: 0
# Stack size: 1
# Flags: 0x00000040 (NOFREE)
# Constants:
# 0: <code object fact at 0x7f815843e4b0, file "<string>", line 2>
# 1: None
# Names:
# 0: fact
2 0 LOAD_CONST 0 (<code object fact at 0x7f815843e4b0, file "<string>", line 2>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (fact)
9 LOAD_CONST 1 (None)
12 RETURN_VALUE
# Method Name: fact
# Filename: <string>
# Argument count: 0
# Number of locals: 1
# Stack size: 1
# Flags: 0x00000043 (NOFREE | NEWLOCALS | OPTIMIZED)
# Constants:
# 0: None
# 1: 8
# 2: 10
# Local variables:
# 0: a
3 0 LOAD_CONST 2 (10)
3 STORE_FAST 0 (a)
4 6 LOAD_CONST 0 (None)
9 RETURN_VALUE
$
An alternate approach for optimization is to optimize at the Abstract Syntax Tree level (AST). The compile, eval and exec functions can start from an AST, or you can dump the AST. You could also write this back out as Python source using the Python module astor
Note however that some kinds of optimization like tail-recursion elimination might leave bytecode in a form that it can't be transformed in a truly faithful way to source code. See my pycon2018 Columbia Lightning Talk for a video I made which eliminates tail recursion in bytecode to get an idea of what I'm talking about here.
If you want to be able to debug and single step bytecode instructions. See my bytecode interpreter and its bytecode debugger.
If I have a class of the following format:
class TestClass:
def __init__(self):
self.value = 0
def getNewObject(self):
return TestClass()
Is there a limitation to the amount of times I can call the function? For example:
obj = TestClass()
obj.getNewObject().getNewObject()
Is there a limitation to how many times I can call getNewObject() on the return value of getNewObject()? If so what factors affect this?
I doubt it. One reason that makes me doubt it is that if we have this function:
def test(obj):
obj.getNewObject().getNewObject().getNewObject()
And we disassemble it:
import dis
dis.dis(test)
We get this:
2 0 LOAD_FAST 0 (obj)
3 LOAD_ATTR 0 (getNewObject)
6 CALL_FUNCTION 0
9 LOAD_ATTR 0 (getNewObject)
12 CALL_FUNCTION 0
15 LOAD_ATTR 0 (getNewObject)
18 CALL_FUNCTION 0
21 POP_TOP
22 LOAD_CONST 0 (None)
25 RETURN_VALUE
That's just repetitions of LOAD_ATTR followed by CALL_FUNCTION. I can't imagine that that would require much memory or other resources to manage. As such, there is probably no limit.
There is a recursion limit in Python (adjustable), but that is unrelated. Each call is made after the previous call has completed, so they're all called from the same level of the stack frame (i.e. from your user code). Now, you might hit a line-length limit or something, especially for an interactive Python shell.