| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808 |
- """This module includes tests of the code object representation.
- >>> def f(x):
- ... def g(y):
- ... return x + y
- ... return g
- ...
- >>> dump(f.__code__)
- name: f
- argcount: 1
- posonlyargcount: 0
- kwonlyargcount: 0
- names: ()
- varnames: ('x', 'g')
- cellvars: ('x',)
- freevars: ()
- nlocals: 2
- flags: 3
- consts: ('None', '<code object g>')
- >>> dump(f(4).__code__)
- name: g
- argcount: 1
- posonlyargcount: 0
- kwonlyargcount: 0
- names: ()
- varnames: ('y',)
- cellvars: ()
- freevars: ('x',)
- nlocals: 1
- flags: 19
- consts: ('None',)
- >>> def h(x, y):
- ... a = x + y
- ... b = x - y
- ... c = a * b
- ... return c
- ...
- >>> dump(h.__code__)
- name: h
- argcount: 2
- posonlyargcount: 0
- kwonlyargcount: 0
- names: ()
- varnames: ('x', 'y', 'a', 'b', 'c')
- cellvars: ()
- freevars: ()
- nlocals: 5
- flags: 3
- consts: ('None',)
- >>> def attrs(obj):
- ... print(obj.attr1)
- ... print(obj.attr2)
- ... print(obj.attr3)
- >>> dump(attrs.__code__)
- name: attrs
- argcount: 1
- posonlyargcount: 0
- kwonlyargcount: 0
- names: ('print', 'attr1', 'attr2', 'attr3')
- varnames: ('obj',)
- cellvars: ()
- freevars: ()
- nlocals: 1
- flags: 3
- consts: ('None',)
- >>> def optimize_away():
- ... 'doc string'
- ... 'not a docstring'
- ... 53
- ... 0x53
- >>> dump(optimize_away.__code__)
- name: optimize_away
- argcount: 0
- posonlyargcount: 0
- kwonlyargcount: 0
- names: ()
- varnames: ()
- cellvars: ()
- freevars: ()
- nlocals: 0
- flags: 3
- consts: ("'doc string'", 'None')
- >>> def keywordonly_args(a,b,*,k1):
- ... return a,b,k1
- ...
- >>> dump(keywordonly_args.__code__)
- name: keywordonly_args
- argcount: 2
- posonlyargcount: 0
- kwonlyargcount: 1
- names: ()
- varnames: ('a', 'b', 'k1')
- cellvars: ()
- freevars: ()
- nlocals: 3
- flags: 3
- consts: ('None',)
- >>> def posonly_args(a,b,/,c):
- ... return a,b,c
- ...
- >>> dump(posonly_args.__code__)
- name: posonly_args
- argcount: 3
- posonlyargcount: 2
- kwonlyargcount: 0
- names: ()
- varnames: ('a', 'b', 'c')
- cellvars: ()
- freevars: ()
- nlocals: 3
- flags: 3
- consts: ('None',)
- """
- import inspect
- import sys
- import threading
- import doctest
- import unittest
- import textwrap
- import weakref
- import dis
- try:
- import ctypes
- except ImportError:
- ctypes = None
- from test.support import (cpython_only,
- check_impl_detail, requires_debug_ranges,
- gc_collect)
- from test.support.script_helper import assert_python_ok
- from test.support import threading_helper
- from opcode import opmap
- COPY_FREE_VARS = opmap['COPY_FREE_VARS']
- def consts(t):
- """Yield a doctest-safe sequence of object reprs."""
- for elt in t:
- r = repr(elt)
- if r.startswith("<code object"):
- yield "<code object %s>" % elt.co_name
- else:
- yield r
- def dump(co):
- """Print out a text representation of a code object."""
- for attr in ["name", "argcount", "posonlyargcount",
- "kwonlyargcount", "names", "varnames",
- "cellvars", "freevars", "nlocals", "flags"]:
- print("%s: %s" % (attr, getattr(co, "co_" + attr)))
- print("consts:", tuple(consts(co.co_consts)))
- # Needed for test_closure_injection below
- # Defined at global scope to avoid implicitly closing over __class__
- def external_getitem(self, i):
- return f"Foreign getitem: {super().__getitem__(i)}"
- class CodeTest(unittest.TestCase):
- @cpython_only
- def test_newempty(self):
- import _testcapi
- co = _testcapi.code_newempty("filename", "funcname", 15)
- self.assertEqual(co.co_filename, "filename")
- self.assertEqual(co.co_name, "funcname")
- self.assertEqual(co.co_firstlineno, 15)
- #Empty code object should raise, but not crash the VM
- with self.assertRaises(Exception):
- exec(co)
- @cpython_only
- def test_closure_injection(self):
- # From https://bugs.python.org/issue32176
- from types import FunctionType
- def create_closure(__class__):
- return (lambda: __class__).__closure__
- def new_code(c):
- '''A new code object with a __class__ cell added to freevars'''
- return c.replace(co_freevars=c.co_freevars + ('__class__',), co_code=bytes([COPY_FREE_VARS, 1])+c.co_code)
- def add_foreign_method(cls, name, f):
- code = new_code(f.__code__)
- assert not f.__closure__
- closure = create_closure(cls)
- defaults = f.__defaults__
- setattr(cls, name, FunctionType(code, globals(), name, defaults, closure))
- class List(list):
- pass
- add_foreign_method(List, "__getitem__", external_getitem)
- # Ensure the closure injection actually worked
- function = List.__getitem__
- class_ref = function.__closure__[0].cell_contents
- self.assertIs(class_ref, List)
- # Ensure the zero-arg super() call in the injected method works
- obj = List([1, 2, 3])
- self.assertEqual(obj[0], "Foreign getitem: 1")
- def test_constructor(self):
- def func(): pass
- co = func.__code__
- CodeType = type(co)
- # test code constructor
- CodeType(co.co_argcount,
- co.co_posonlyargcount,
- co.co_kwonlyargcount,
- co.co_nlocals,
- co.co_stacksize,
- co.co_flags,
- co.co_code,
- co.co_consts,
- co.co_names,
- co.co_varnames,
- co.co_filename,
- co.co_name,
- co.co_qualname,
- co.co_firstlineno,
- co.co_linetable,
- co.co_exceptiontable,
- co.co_freevars,
- co.co_cellvars)
- def test_qualname(self):
- self.assertEqual(
- CodeTest.test_qualname.__code__.co_qualname,
- CodeTest.test_qualname.__qualname__
- )
- def test_replace(self):
- def func():
- x = 1
- return x
- code = func.__code__
- # different co_name, co_varnames, co_consts
- def func2():
- y = 2
- z = 3
- return y
- code2 = func2.__code__
- for attr, value in (
- ("co_argcount", 0),
- ("co_posonlyargcount", 0),
- ("co_kwonlyargcount", 0),
- ("co_nlocals", 1),
- ("co_stacksize", 0),
- ("co_flags", code.co_flags | inspect.CO_COROUTINE),
- ("co_firstlineno", 100),
- ("co_code", code2.co_code),
- ("co_consts", code2.co_consts),
- ("co_names", ("myname",)),
- ("co_varnames", ('spam',)),
- ("co_freevars", ("freevar",)),
- ("co_cellvars", ("cellvar",)),
- ("co_filename", "newfilename"),
- ("co_name", "newname"),
- ("co_linetable", code2.co_linetable),
- ):
- with self.subTest(attr=attr, value=value):
- new_code = code.replace(**{attr: value})
- self.assertEqual(getattr(new_code, attr), value)
- new_code = code.replace(co_varnames=code2.co_varnames,
- co_nlocals=code2.co_nlocals)
- self.assertEqual(new_code.co_varnames, code2.co_varnames)
- self.assertEqual(new_code.co_nlocals, code2.co_nlocals)
- def test_nlocals_mismatch(self):
- def func():
- x = 1
- return x
- co = func.__code__
- assert co.co_nlocals > 0;
- # First we try the constructor.
- CodeType = type(co)
- for diff in (-1, 1):
- with self.assertRaises(ValueError):
- CodeType(co.co_argcount,
- co.co_posonlyargcount,
- co.co_kwonlyargcount,
- # This is the only change.
- co.co_nlocals + diff,
- co.co_stacksize,
- co.co_flags,
- co.co_code,
- co.co_consts,
- co.co_names,
- co.co_varnames,
- co.co_filename,
- co.co_name,
- co.co_qualname,
- co.co_firstlineno,
- co.co_linetable,
- co.co_exceptiontable,
- co.co_freevars,
- co.co_cellvars,
- )
- # Then we try the replace method.
- with self.assertRaises(ValueError):
- co.replace(co_nlocals=co.co_nlocals - 1)
- with self.assertRaises(ValueError):
- co.replace(co_nlocals=co.co_nlocals + 1)
- def test_shrinking_localsplus(self):
- # Check that PyCode_NewWithPosOnlyArgs resizes both
- # localsplusnames and localspluskinds, if an argument is a cell.
- def func(arg):
- return lambda: arg
- code = func.__code__
- newcode = code.replace(co_name="func") # Should not raise SystemError
- self.assertEqual(code, newcode)
- def test_empty_linetable(self):
- def func():
- pass
- new_code = code = func.__code__.replace(co_linetable=b'')
- self.assertEqual(list(new_code.co_lines()), [])
- @requires_debug_ranges()
- def test_co_positions_artificial_instructions(self):
- import dis
- namespace = {}
- exec(textwrap.dedent("""\
- try:
- 1/0
- except Exception as e:
- exc = e
- """), namespace)
- exc = namespace['exc']
- traceback = exc.__traceback__
- code = traceback.tb_frame.f_code
- artificial_instructions = []
- for instr, positions in zip(
- dis.get_instructions(code, show_caches=True),
- code.co_positions(),
- strict=True
- ):
- # If any of the positions is None, then all have to
- # be None as well for the case above. There are still
- # some places in the compiler, where the artificial instructions
- # get assigned the first_lineno but they don't have other positions.
- # There is no easy way of inferring them at that stage, so for now
- # we don't support it.
- self.assertIn(positions.count(None), [0, 3, 4])
- if not any(positions):
- artificial_instructions.append(instr)
- self.assertEqual(
- [
- (instruction.opname, instruction.argval)
- for instruction in artificial_instructions
- ],
- [
- ("PUSH_EXC_INFO", None),
- ("LOAD_CONST", None), # artificial 'None'
- ("STORE_NAME", "e"), # XX: we know the location for this
- ("DELETE_NAME", "e"),
- ("RERAISE", 1),
- ("COPY", 3),
- ("POP_EXCEPT", None),
- ("RERAISE", 1)
- ]
- )
- def test_endline_and_columntable_none_when_no_debug_ranges(self):
- # Make sure that if `-X no_debug_ranges` is used, there is
- # minimal debug info
- code = textwrap.dedent("""
- def f():
- pass
- positions = f.__code__.co_positions()
- for line, end_line, column, end_column in positions:
- assert line == end_line
- assert column is None
- assert end_column is None
- """)
- assert_python_ok('-X', 'no_debug_ranges', '-c', code)
- def test_endline_and_columntable_none_when_no_debug_ranges_env(self):
- # Same as above but using the environment variable opt out.
- code = textwrap.dedent("""
- def f():
- pass
- positions = f.__code__.co_positions()
- for line, end_line, column, end_column in positions:
- assert line == end_line
- assert column is None
- assert end_column is None
- """)
- assert_python_ok('-c', code, PYTHONNODEBUGRANGES='1')
- # co_positions behavior when info is missing.
- @requires_debug_ranges()
- def test_co_positions_empty_linetable(self):
- def func():
- x = 1
- new_code = func.__code__.replace(co_linetable=b'')
- positions = new_code.co_positions()
- for line, end_line, column, end_column in positions:
- self.assertIsNone(line)
- self.assertEqual(end_line, new_code.co_firstlineno + 1)
- def test_code_equality(self):
- def f():
- try:
- a()
- except:
- b()
- else:
- c()
- finally:
- d()
- code_a = f.__code__
- code_b = code_a.replace(co_linetable=b"")
- code_c = code_a.replace(co_exceptiontable=b"")
- code_d = code_b.replace(co_exceptiontable=b"")
- self.assertNotEqual(code_a, code_b)
- self.assertNotEqual(code_a, code_c)
- self.assertNotEqual(code_a, code_d)
- self.assertNotEqual(code_b, code_c)
- self.assertNotEqual(code_b, code_d)
- self.assertNotEqual(code_c, code_d)
- def isinterned(s):
- return s is sys.intern(('_' + s + '_')[1:-1])
- class CodeConstsTest(unittest.TestCase):
- def find_const(self, consts, value):
- for v in consts:
- if v == value:
- return v
- self.assertIn(value, consts) # raises an exception
- self.fail('Should never be reached')
- def assertIsInterned(self, s):
- if not isinterned(s):
- self.fail('String %r is not interned' % (s,))
- def assertIsNotInterned(self, s):
- if isinterned(s):
- self.fail('String %r is interned' % (s,))
- @cpython_only
- def test_interned_string(self):
- co = compile('res = "str_value"', '?', 'exec')
- v = self.find_const(co.co_consts, 'str_value')
- self.assertIsInterned(v)
- @cpython_only
- def test_interned_string_in_tuple(self):
- co = compile('res = ("str_value",)', '?', 'exec')
- v = self.find_const(co.co_consts, ('str_value',))
- self.assertIsInterned(v[0])
- @cpython_only
- def test_interned_string_in_frozenset(self):
- co = compile('res = a in {"str_value"}', '?', 'exec')
- v = self.find_const(co.co_consts, frozenset(('str_value',)))
- self.assertIsInterned(tuple(v)[0])
- @cpython_only
- def test_interned_string_default(self):
- def f(a='str_value'):
- return a
- self.assertIsInterned(f())
- @cpython_only
- def test_interned_string_with_null(self):
- co = compile(r'res = "str\0value!"', '?', 'exec')
- v = self.find_const(co.co_consts, 'str\0value!')
- self.assertIsNotInterned(v)
- class CodeWeakRefTest(unittest.TestCase):
- def test_basic(self):
- # Create a code object in a clean environment so that we know we have
- # the only reference to it left.
- namespace = {}
- exec("def f(): pass", globals(), namespace)
- f = namespace["f"]
- del namespace
- self.called = False
- def callback(code):
- self.called = True
- # f is now the last reference to the function, and through it, the code
- # object. While we hold it, check that we can create a weakref and
- # deref it. Then delete it, and check that the callback gets called and
- # the reference dies.
- coderef = weakref.ref(f.__code__, callback)
- self.assertTrue(bool(coderef()))
- del f
- gc_collect() # For PyPy or other GCs.
- self.assertFalse(bool(coderef()))
- self.assertTrue(self.called)
- # Python implementation of location table parsing algorithm
- def read(it):
- return next(it)
- def read_varint(it):
- b = read(it)
- val = b & 63;
- shift = 0;
- while b & 64:
- b = read(it)
- shift += 6
- val |= (b&63) << shift
- return val
- def read_signed_varint(it):
- uval = read_varint(it)
- if uval & 1:
- return -(uval >> 1)
- else:
- return uval >> 1
- def parse_location_table(code):
- line = code.co_firstlineno
- it = iter(code.co_linetable)
- while True:
- try:
- first_byte = read(it)
- except StopIteration:
- return
- code = (first_byte >> 3) & 15
- length = (first_byte & 7) + 1
- if code == 15:
- yield (code, length, None, None, None, None)
- elif code == 14:
- line_delta = read_signed_varint(it)
- line += line_delta
- end_line = line + read_varint(it)
- col = read_varint(it)
- if col == 0:
- col = None
- else:
- col -= 1
- end_col = read_varint(it)
- if end_col == 0:
- end_col = None
- else:
- end_col -= 1
- yield (code, length, line, end_line, col, end_col)
- elif code == 13: # No column
- line_delta = read_signed_varint(it)
- line += line_delta
- yield (code, length, line, line, None, None)
- elif code in (10, 11, 12): # new line
- line_delta = code - 10
- line += line_delta
- column = read(it)
- end_column = read(it)
- yield (code, length, line, line, column, end_column)
- else:
- assert (0 <= code < 10)
- second_byte = read(it)
- column = code << 3 | (second_byte >> 4)
- yield (code, length, line, line, column, column + (second_byte & 15))
- def positions_from_location_table(code):
- for _, length, line, end_line, col, end_col in parse_location_table(code):
- for _ in range(length):
- yield (line, end_line, col, end_col)
- def dedup(lst, prev=object()):
- for item in lst:
- if item != prev:
- yield item
- prev = item
- def lines_from_postions(positions):
- return dedup(l for (l, _, _, _) in positions)
- def misshappen():
- """
- """
- x = (
- 4
- +
- y
- )
- y = (
- a
- +
- b
- +
- d
- )
- return q if (
- x
- ) else p
- def bug93662():
- example_report_generation_message= (
- """
- """
- ).strip()
- raise ValueError()
- class CodeLocationTest(unittest.TestCase):
- def check_positions(self, func):
- pos1 = list(func.__code__.co_positions())
- pos2 = list(positions_from_location_table(func.__code__))
- for l1, l2 in zip(pos1, pos2):
- self.assertEqual(l1, l2)
- self.assertEqual(len(pos1), len(pos2))
- def test_positions(self):
- self.check_positions(parse_location_table)
- self.check_positions(misshappen)
- self.check_positions(bug93662)
- def check_lines(self, func):
- co = func.__code__
- lines1 = list(dedup(l for (_, _, l) in co.co_lines()))
- lines2 = list(lines_from_postions(positions_from_location_table(co)))
- for l1, l2 in zip(lines1, lines2):
- self.assertEqual(l1, l2)
- self.assertEqual(len(lines1), len(lines2))
- def test_lines(self):
- self.check_lines(parse_location_table)
- self.check_lines(misshappen)
- self.check_lines(bug93662)
- @cpython_only
- def test_code_new_empty(self):
- # If this test fails, it means that the construction of PyCode_NewEmpty
- # needs to be modified! Please update this test *and* PyCode_NewEmpty,
- # so that they both stay in sync.
- def f():
- pass
- PY_CODE_LOCATION_INFO_NO_COLUMNS = 13
- f.__code__ = f.__code__.replace(
- co_firstlineno=42,
- co_code=bytes(
- [
- dis.opmap["RESUME"], 0,
- dis.opmap["LOAD_ASSERTION_ERROR"], 0,
- dis.opmap["RAISE_VARARGS"], 1,
- ]
- ),
- co_linetable=bytes(
- [
- (1 << 7)
- | (PY_CODE_LOCATION_INFO_NO_COLUMNS << 3)
- | (3 - 1),
- 0,
- ]
- ),
- )
- self.assertRaises(AssertionError, f)
- self.assertEqual(
- list(f.__code__.co_positions()),
- 3 * [(42, 42, None, None)],
- )
- if check_impl_detail(cpython=True) and ctypes is not None:
- py = ctypes.pythonapi
- freefunc = ctypes.CFUNCTYPE(None,ctypes.c_voidp)
- RequestCodeExtraIndex = py._PyEval_RequestCodeExtraIndex
- RequestCodeExtraIndex.argtypes = (freefunc,)
- RequestCodeExtraIndex.restype = ctypes.c_ssize_t
- SetExtra = py._PyCode_SetExtra
- SetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, ctypes.c_voidp)
- SetExtra.restype = ctypes.c_int
- GetExtra = py._PyCode_GetExtra
- GetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t,
- ctypes.POINTER(ctypes.c_voidp))
- GetExtra.restype = ctypes.c_int
- LAST_FREED = None
- def myfree(ptr):
- global LAST_FREED
- LAST_FREED = ptr
- FREE_FUNC = freefunc(myfree)
- FREE_INDEX = RequestCodeExtraIndex(FREE_FUNC)
- class CoExtra(unittest.TestCase):
- def get_func(self):
- # Defining a function causes the containing function to have a
- # reference to the code object. We need the code objects to go
- # away, so we eval a lambda.
- return eval('lambda:42')
- def test_get_non_code(self):
- f = self.get_func()
- self.assertRaises(SystemError, SetExtra, 42, FREE_INDEX,
- ctypes.c_voidp(100))
- self.assertRaises(SystemError, GetExtra, 42, FREE_INDEX,
- ctypes.c_voidp(100))
- def test_bad_index(self):
- f = self.get_func()
- self.assertRaises(SystemError, SetExtra, f.__code__,
- FREE_INDEX+100, ctypes.c_voidp(100))
- self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
- ctypes.c_voidp(100)), 0)
- def test_free_called(self):
- # Verify that the provided free function gets invoked
- # when the code object is cleaned up.
- f = self.get_func()
- SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100))
- del f
- self.assertEqual(LAST_FREED, 100)
- def test_get_set(self):
- # Test basic get/set round tripping.
- f = self.get_func()
- extra = ctypes.c_voidp()
- SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(200))
- # reset should free...
- SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(300))
- self.assertEqual(LAST_FREED, 200)
- extra = ctypes.c_voidp()
- GetExtra(f.__code__, FREE_INDEX, extra)
- self.assertEqual(extra.value, 300)
- del f
- @threading_helper.requires_working_threading()
- def test_free_different_thread(self):
- # Freeing a code object on a different thread then
- # where the co_extra was set should be safe.
- f = self.get_func()
- class ThreadTest(threading.Thread):
- def __init__(self, f, test):
- super().__init__()
- self.f = f
- self.test = test
- def run(self):
- del self.f
- self.test.assertEqual(LAST_FREED, 500)
- SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(500))
- tt = ThreadTest(f, self)
- del f
- tt.start()
- tt.join()
- self.assertEqual(LAST_FREED, 500)
- def load_tests(loader, tests, pattern):
- tests.addTest(doctest.DocTestSuite())
- return tests
- if __name__ == "__main__":
- unittest.main()
|