xref: /third_party/python/Lib/test/test_code.py (revision 7db96d56)
1"""This module includes tests of the code object representation.
2
3>>> def f(x):
4...     def g(y):
5...         return x + y
6...     return g
7...
8
9>>> dump(f.__code__)
10name: f
11argcount: 1
12posonlyargcount: 0
13kwonlyargcount: 0
14names: ()
15varnames: ('x', 'g')
16cellvars: ('x',)
17freevars: ()
18nlocals: 2
19flags: 3
20consts: ('None', '<code object g>')
21
22>>> dump(f(4).__code__)
23name: g
24argcount: 1
25posonlyargcount: 0
26kwonlyargcount: 0
27names: ()
28varnames: ('y',)
29cellvars: ()
30freevars: ('x',)
31nlocals: 1
32flags: 19
33consts: ('None',)
34
35>>> def h(x, y):
36...     a = x + y
37...     b = x - y
38...     c = a * b
39...     return c
40...
41
42>>> dump(h.__code__)
43name: h
44argcount: 2
45posonlyargcount: 0
46kwonlyargcount: 0
47names: ()
48varnames: ('x', 'y', 'a', 'b', 'c')
49cellvars: ()
50freevars: ()
51nlocals: 5
52flags: 3
53consts: ('None',)
54
55>>> def attrs(obj):
56...     print(obj.attr1)
57...     print(obj.attr2)
58...     print(obj.attr3)
59
60>>> dump(attrs.__code__)
61name: attrs
62argcount: 1
63posonlyargcount: 0
64kwonlyargcount: 0
65names: ('print', 'attr1', 'attr2', 'attr3')
66varnames: ('obj',)
67cellvars: ()
68freevars: ()
69nlocals: 1
70flags: 3
71consts: ('None',)
72
73>>> def optimize_away():
74...     'doc string'
75...     'not a docstring'
76...     53
77...     0x53
78
79>>> dump(optimize_away.__code__)
80name: optimize_away
81argcount: 0
82posonlyargcount: 0
83kwonlyargcount: 0
84names: ()
85varnames: ()
86cellvars: ()
87freevars: ()
88nlocals: 0
89flags: 3
90consts: ("'doc string'", 'None')
91
92>>> def keywordonly_args(a,b,*,k1):
93...     return a,b,k1
94...
95
96>>> dump(keywordonly_args.__code__)
97name: keywordonly_args
98argcount: 2
99posonlyargcount: 0
100kwonlyargcount: 1
101names: ()
102varnames: ('a', 'b', 'k1')
103cellvars: ()
104freevars: ()
105nlocals: 3
106flags: 3
107consts: ('None',)
108
109>>> def posonly_args(a,b,/,c):
110...     return a,b,c
111...
112
113>>> dump(posonly_args.__code__)
114name: posonly_args
115argcount: 3
116posonlyargcount: 2
117kwonlyargcount: 0
118names: ()
119varnames: ('a', 'b', 'c')
120cellvars: ()
121freevars: ()
122nlocals: 3
123flags: 3
124consts: ('None',)
125
126"""
127
128import inspect
129import sys
130import threading
131import doctest
132import unittest
133import textwrap
134import weakref
135import dis
136
137try:
138    import ctypes
139except ImportError:
140    ctypes = None
141from test.support import (cpython_only,
142                          check_impl_detail, requires_debug_ranges,
143                          gc_collect)
144from test.support.script_helper import assert_python_ok
145from test.support import threading_helper
146from opcode import opmap
147COPY_FREE_VARS = opmap['COPY_FREE_VARS']
148
149
150def consts(t):
151    """Yield a doctest-safe sequence of object reprs."""
152    for elt in t:
153        r = repr(elt)
154        if r.startswith("<code object"):
155            yield "<code object %s>" % elt.co_name
156        else:
157            yield r
158
159def dump(co):
160    """Print out a text representation of a code object."""
161    for attr in ["name", "argcount", "posonlyargcount",
162                 "kwonlyargcount", "names", "varnames",
163                 "cellvars", "freevars", "nlocals", "flags"]:
164        print("%s: %s" % (attr, getattr(co, "co_" + attr)))
165    print("consts:", tuple(consts(co.co_consts)))
166
167# Needed for test_closure_injection below
168# Defined at global scope to avoid implicitly closing over __class__
169def external_getitem(self, i):
170    return f"Foreign getitem: {super().__getitem__(i)}"
171
172class CodeTest(unittest.TestCase):
173
174    @cpython_only
175    def test_newempty(self):
176        import _testcapi
177        co = _testcapi.code_newempty("filename", "funcname", 15)
178        self.assertEqual(co.co_filename, "filename")
179        self.assertEqual(co.co_name, "funcname")
180        self.assertEqual(co.co_firstlineno, 15)
181        #Empty code object should raise, but not crash the VM
182        with self.assertRaises(Exception):
183            exec(co)
184
185    @cpython_only
186    def test_closure_injection(self):
187        # From https://bugs.python.org/issue32176
188        from types import FunctionType
189
190        def create_closure(__class__):
191            return (lambda: __class__).__closure__
192
193        def new_code(c):
194            '''A new code object with a __class__ cell added to freevars'''
195            return c.replace(co_freevars=c.co_freevars + ('__class__',), co_code=bytes([COPY_FREE_VARS, 1])+c.co_code)
196
197        def add_foreign_method(cls, name, f):
198            code = new_code(f.__code__)
199            assert not f.__closure__
200            closure = create_closure(cls)
201            defaults = f.__defaults__
202            setattr(cls, name, FunctionType(code, globals(), name, defaults, closure))
203
204        class List(list):
205            pass
206
207        add_foreign_method(List, "__getitem__", external_getitem)
208
209        # Ensure the closure injection actually worked
210        function = List.__getitem__
211        class_ref = function.__closure__[0].cell_contents
212        self.assertIs(class_ref, List)
213
214        # Ensure the zero-arg super() call in the injected method works
215        obj = List([1, 2, 3])
216        self.assertEqual(obj[0], "Foreign getitem: 1")
217
218    def test_constructor(self):
219        def func(): pass
220        co = func.__code__
221        CodeType = type(co)
222
223        # test code constructor
224        CodeType(co.co_argcount,
225                        co.co_posonlyargcount,
226                        co.co_kwonlyargcount,
227                        co.co_nlocals,
228                        co.co_stacksize,
229                        co.co_flags,
230                        co.co_code,
231                        co.co_consts,
232                        co.co_names,
233                        co.co_varnames,
234                        co.co_filename,
235                        co.co_name,
236                        co.co_qualname,
237                        co.co_firstlineno,
238                        co.co_linetable,
239                        co.co_exceptiontable,
240                        co.co_freevars,
241                        co.co_cellvars)
242
243    def test_qualname(self):
244        self.assertEqual(
245            CodeTest.test_qualname.__code__.co_qualname,
246            CodeTest.test_qualname.__qualname__
247        )
248
249    def test_replace(self):
250        def func():
251            x = 1
252            return x
253        code = func.__code__
254
255        # different co_name, co_varnames, co_consts
256        def func2():
257            y = 2
258            z = 3
259            return y
260        code2 = func2.__code__
261
262        for attr, value in (
263            ("co_argcount", 0),
264            ("co_posonlyargcount", 0),
265            ("co_kwonlyargcount", 0),
266            ("co_nlocals", 1),
267            ("co_stacksize", 0),
268            ("co_flags", code.co_flags | inspect.CO_COROUTINE),
269            ("co_firstlineno", 100),
270            ("co_code", code2.co_code),
271            ("co_consts", code2.co_consts),
272            ("co_names", ("myname",)),
273            ("co_varnames", ('spam',)),
274            ("co_freevars", ("freevar",)),
275            ("co_cellvars", ("cellvar",)),
276            ("co_filename", "newfilename"),
277            ("co_name", "newname"),
278            ("co_linetable", code2.co_linetable),
279        ):
280            with self.subTest(attr=attr, value=value):
281                new_code = code.replace(**{attr: value})
282                self.assertEqual(getattr(new_code, attr), value)
283
284        new_code = code.replace(co_varnames=code2.co_varnames,
285                                co_nlocals=code2.co_nlocals)
286        self.assertEqual(new_code.co_varnames, code2.co_varnames)
287        self.assertEqual(new_code.co_nlocals, code2.co_nlocals)
288
289    def test_nlocals_mismatch(self):
290        def func():
291            x = 1
292            return x
293        co = func.__code__
294        assert co.co_nlocals > 0;
295
296        # First we try the constructor.
297        CodeType = type(co)
298        for diff in (-1, 1):
299            with self.assertRaises(ValueError):
300                CodeType(co.co_argcount,
301                         co.co_posonlyargcount,
302                         co.co_kwonlyargcount,
303                         # This is the only change.
304                         co.co_nlocals + diff,
305                         co.co_stacksize,
306                         co.co_flags,
307                         co.co_code,
308                         co.co_consts,
309                         co.co_names,
310                         co.co_varnames,
311                         co.co_filename,
312                         co.co_name,
313                         co.co_qualname,
314                         co.co_firstlineno,
315                         co.co_linetable,
316                         co.co_exceptiontable,
317                         co.co_freevars,
318                         co.co_cellvars,
319                         )
320        # Then we try the replace method.
321        with self.assertRaises(ValueError):
322            co.replace(co_nlocals=co.co_nlocals - 1)
323        with self.assertRaises(ValueError):
324            co.replace(co_nlocals=co.co_nlocals + 1)
325
326    def test_shrinking_localsplus(self):
327        # Check that PyCode_NewWithPosOnlyArgs resizes both
328        # localsplusnames and localspluskinds, if an argument is a cell.
329        def func(arg):
330            return lambda: arg
331        code = func.__code__
332        newcode = code.replace(co_name="func")  # Should not raise SystemError
333        self.assertEqual(code, newcode)
334
335    def test_empty_linetable(self):
336        def func():
337            pass
338        new_code = code = func.__code__.replace(co_linetable=b'')
339        self.assertEqual(list(new_code.co_lines()), [])
340
341    @requires_debug_ranges()
342    def test_co_positions_artificial_instructions(self):
343        import dis
344
345        namespace = {}
346        exec(textwrap.dedent("""\
347        try:
348            1/0
349        except Exception as e:
350            exc = e
351        """), namespace)
352
353        exc = namespace['exc']
354        traceback = exc.__traceback__
355        code = traceback.tb_frame.f_code
356
357        artificial_instructions = []
358        for instr, positions in zip(
359            dis.get_instructions(code, show_caches=True),
360            code.co_positions(),
361            strict=True
362        ):
363            # If any of the positions is None, then all have to
364            # be None as well for the case above. There are still
365            # some places in the compiler, where the artificial instructions
366            # get assigned the first_lineno but they don't have other positions.
367            # There is no easy way of inferring them at that stage, so for now
368            # we don't support it.
369            self.assertIn(positions.count(None), [0, 3, 4])
370
371            if not any(positions):
372                artificial_instructions.append(instr)
373
374        self.assertEqual(
375            [
376                (instruction.opname, instruction.argval)
377                for instruction in artificial_instructions
378            ],
379            [
380                ("PUSH_EXC_INFO", None),
381                ("LOAD_CONST", None), # artificial 'None'
382                ("STORE_NAME", "e"),  # XX: we know the location for this
383                ("DELETE_NAME", "e"),
384                ("RERAISE", 1),
385                ("COPY", 3),
386                ("POP_EXCEPT", None),
387                ("RERAISE", 1)
388            ]
389        )
390
391    def test_endline_and_columntable_none_when_no_debug_ranges(self):
392        # Make sure that if `-X no_debug_ranges` is used, there is
393        # minimal debug info
394        code = textwrap.dedent("""
395            def f():
396                pass
397
398            positions = f.__code__.co_positions()
399            for line, end_line, column, end_column in positions:
400                assert line == end_line
401                assert column is None
402                assert end_column is None
403            """)
404        assert_python_ok('-X', 'no_debug_ranges', '-c', code)
405
406    def test_endline_and_columntable_none_when_no_debug_ranges_env(self):
407        # Same as above but using the environment variable opt out.
408        code = textwrap.dedent("""
409            def f():
410                pass
411
412            positions = f.__code__.co_positions()
413            for line, end_line, column, end_column in positions:
414                assert line == end_line
415                assert column is None
416                assert end_column is None
417            """)
418        assert_python_ok('-c', code, PYTHONNODEBUGRANGES='1')
419
420    # co_positions behavior when info is missing.
421
422    @requires_debug_ranges()
423    def test_co_positions_empty_linetable(self):
424        def func():
425            x = 1
426        new_code = func.__code__.replace(co_linetable=b'')
427        positions = new_code.co_positions()
428        for line, end_line, column, end_column in positions:
429            self.assertIsNone(line)
430            self.assertEqual(end_line, new_code.co_firstlineno + 1)
431
432    def test_code_equality(self):
433        def f():
434            try:
435                a()
436            except:
437                b()
438            else:
439                c()
440            finally:
441                d()
442        code_a = f.__code__
443        code_b = code_a.replace(co_linetable=b"")
444        code_c = code_a.replace(co_exceptiontable=b"")
445        code_d = code_b.replace(co_exceptiontable=b"")
446        self.assertNotEqual(code_a, code_b)
447        self.assertNotEqual(code_a, code_c)
448        self.assertNotEqual(code_a, code_d)
449        self.assertNotEqual(code_b, code_c)
450        self.assertNotEqual(code_b, code_d)
451        self.assertNotEqual(code_c, code_d)
452
453
454def isinterned(s):
455    return s is sys.intern(('_' + s + '_')[1:-1])
456
457class CodeConstsTest(unittest.TestCase):
458
459    def find_const(self, consts, value):
460        for v in consts:
461            if v == value:
462                return v
463        self.assertIn(value, consts)  # raises an exception
464        self.fail('Should never be reached')
465
466    def assertIsInterned(self, s):
467        if not isinterned(s):
468            self.fail('String %r is not interned' % (s,))
469
470    def assertIsNotInterned(self, s):
471        if isinterned(s):
472            self.fail('String %r is interned' % (s,))
473
474    @cpython_only
475    def test_interned_string(self):
476        co = compile('res = "str_value"', '?', 'exec')
477        v = self.find_const(co.co_consts, 'str_value')
478        self.assertIsInterned(v)
479
480    @cpython_only
481    def test_interned_string_in_tuple(self):
482        co = compile('res = ("str_value",)', '?', 'exec')
483        v = self.find_const(co.co_consts, ('str_value',))
484        self.assertIsInterned(v[0])
485
486    @cpython_only
487    def test_interned_string_in_frozenset(self):
488        co = compile('res = a in {"str_value"}', '?', 'exec')
489        v = self.find_const(co.co_consts, frozenset(('str_value',)))
490        self.assertIsInterned(tuple(v)[0])
491
492    @cpython_only
493    def test_interned_string_default(self):
494        def f(a='str_value'):
495            return a
496        self.assertIsInterned(f())
497
498    @cpython_only
499    def test_interned_string_with_null(self):
500        co = compile(r'res = "str\0value!"', '?', 'exec')
501        v = self.find_const(co.co_consts, 'str\0value!')
502        self.assertIsNotInterned(v)
503
504
505class CodeWeakRefTest(unittest.TestCase):
506
507    def test_basic(self):
508        # Create a code object in a clean environment so that we know we have
509        # the only reference to it left.
510        namespace = {}
511        exec("def f(): pass", globals(), namespace)
512        f = namespace["f"]
513        del namespace
514
515        self.called = False
516        def callback(code):
517            self.called = True
518
519        # f is now the last reference to the function, and through it, the code
520        # object.  While we hold it, check that we can create a weakref and
521        # deref it.  Then delete it, and check that the callback gets called and
522        # the reference dies.
523        coderef = weakref.ref(f.__code__, callback)
524        self.assertTrue(bool(coderef()))
525        del f
526        gc_collect()  # For PyPy or other GCs.
527        self.assertFalse(bool(coderef()))
528        self.assertTrue(self.called)
529
530# Python implementation of location table parsing algorithm
531def read(it):
532    return next(it)
533
534def read_varint(it):
535    b = read(it)
536    val = b & 63;
537    shift = 0;
538    while b & 64:
539        b = read(it)
540        shift += 6
541        val |= (b&63) << shift
542    return val
543
544def read_signed_varint(it):
545    uval = read_varint(it)
546    if uval & 1:
547        return -(uval >> 1)
548    else:
549        return uval >> 1
550
551def parse_location_table(code):
552    line = code.co_firstlineno
553    it = iter(code.co_linetable)
554    while True:
555        try:
556            first_byte = read(it)
557        except StopIteration:
558            return
559        code = (first_byte >> 3) & 15
560        length = (first_byte & 7) + 1
561        if code == 15:
562            yield (code, length, None, None, None, None)
563        elif code == 14:
564            line_delta = read_signed_varint(it)
565            line += line_delta
566            end_line = line + read_varint(it)
567            col = read_varint(it)
568            if col == 0:
569                col = None
570            else:
571                col -= 1
572            end_col = read_varint(it)
573            if end_col == 0:
574                end_col = None
575            else:
576                end_col -= 1
577            yield (code, length, line, end_line, col, end_col)
578        elif code == 13: # No column
579            line_delta = read_signed_varint(it)
580            line += line_delta
581            yield (code, length, line, line, None, None)
582        elif code in (10, 11, 12): # new line
583            line_delta = code - 10
584            line += line_delta
585            column = read(it)
586            end_column = read(it)
587            yield (code, length, line, line, column, end_column)
588        else:
589            assert (0 <= code < 10)
590            second_byte = read(it)
591            column = code << 3 | (second_byte >> 4)
592            yield (code, length, line, line, column, column + (second_byte & 15))
593
594def positions_from_location_table(code):
595    for _, length, line, end_line, col, end_col in parse_location_table(code):
596        for _ in range(length):
597            yield (line, end_line, col, end_col)
598
599def dedup(lst, prev=object()):
600    for item in lst:
601        if item != prev:
602            yield item
603            prev = item
604
605def lines_from_postions(positions):
606    return dedup(l for (l, _, _, _) in positions)
607
608def misshappen():
609    """
610
611
612
613
614
615    """
616    x = (
617
618
619        4
620
621        +
622
623        y
624
625    )
626    y = (
627        a
628        +
629            b
630                +
631
632                d
633        )
634    return q if (
635
636        x
637
638        ) else p
639
640def bug93662():
641    example_report_generation_message= (
642            """
643            """
644    ).strip()
645    raise ValueError()
646
647
648class CodeLocationTest(unittest.TestCase):
649
650    def check_positions(self, func):
651        pos1 = list(func.__code__.co_positions())
652        pos2 = list(positions_from_location_table(func.__code__))
653        for l1, l2 in zip(pos1, pos2):
654            self.assertEqual(l1, l2)
655        self.assertEqual(len(pos1), len(pos2))
656
657    def test_positions(self):
658        self.check_positions(parse_location_table)
659        self.check_positions(misshappen)
660        self.check_positions(bug93662)
661
662    def check_lines(self, func):
663        co = func.__code__
664        lines1 = list(dedup(l for (_, _, l) in co.co_lines()))
665        lines2 = list(lines_from_postions(positions_from_location_table(co)))
666        for l1, l2 in zip(lines1, lines2):
667            self.assertEqual(l1, l2)
668        self.assertEqual(len(lines1), len(lines2))
669
670    def test_lines(self):
671        self.check_lines(parse_location_table)
672        self.check_lines(misshappen)
673        self.check_lines(bug93662)
674
675    @cpython_only
676    def test_code_new_empty(self):
677        # If this test fails, it means that the construction of PyCode_NewEmpty
678        # needs to be modified! Please update this test *and* PyCode_NewEmpty,
679        # so that they both stay in sync.
680        def f():
681            pass
682        PY_CODE_LOCATION_INFO_NO_COLUMNS = 13
683        f.__code__ = f.__code__.replace(
684            co_firstlineno=42,
685            co_code=bytes(
686                [
687                    dis.opmap["RESUME"], 0,
688                    dis.opmap["LOAD_ASSERTION_ERROR"], 0,
689                    dis.opmap["RAISE_VARARGS"], 1,
690                ]
691            ),
692            co_linetable=bytes(
693                [
694                    (1 << 7)
695                    | (PY_CODE_LOCATION_INFO_NO_COLUMNS << 3)
696                    | (3 - 1),
697                    0,
698                ]
699            ),
700        )
701        self.assertRaises(AssertionError, f)
702        self.assertEqual(
703            list(f.__code__.co_positions()),
704            3 * [(42, 42, None, None)],
705        )
706
707
708if check_impl_detail(cpython=True) and ctypes is not None:
709    py = ctypes.pythonapi
710    freefunc = ctypes.CFUNCTYPE(None,ctypes.c_voidp)
711
712    RequestCodeExtraIndex = py._PyEval_RequestCodeExtraIndex
713    RequestCodeExtraIndex.argtypes = (freefunc,)
714    RequestCodeExtraIndex.restype = ctypes.c_ssize_t
715
716    SetExtra = py._PyCode_SetExtra
717    SetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, ctypes.c_voidp)
718    SetExtra.restype = ctypes.c_int
719
720    GetExtra = py._PyCode_GetExtra
721    GetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t,
722                         ctypes.POINTER(ctypes.c_voidp))
723    GetExtra.restype = ctypes.c_int
724
725    LAST_FREED = None
726    def myfree(ptr):
727        global LAST_FREED
728        LAST_FREED = ptr
729
730    FREE_FUNC = freefunc(myfree)
731    FREE_INDEX = RequestCodeExtraIndex(FREE_FUNC)
732
733    class CoExtra(unittest.TestCase):
734        def get_func(self):
735            # Defining a function causes the containing function to have a
736            # reference to the code object.  We need the code objects to go
737            # away, so we eval a lambda.
738            return eval('lambda:42')
739
740        def test_get_non_code(self):
741            f = self.get_func()
742
743            self.assertRaises(SystemError, SetExtra, 42, FREE_INDEX,
744                              ctypes.c_voidp(100))
745            self.assertRaises(SystemError, GetExtra, 42, FREE_INDEX,
746                              ctypes.c_voidp(100))
747
748        def test_bad_index(self):
749            f = self.get_func()
750            self.assertRaises(SystemError, SetExtra, f.__code__,
751                              FREE_INDEX+100, ctypes.c_voidp(100))
752            self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
753                              ctypes.c_voidp(100)), 0)
754
755        def test_free_called(self):
756            # Verify that the provided free function gets invoked
757            # when the code object is cleaned up.
758            f = self.get_func()
759
760            SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100))
761            del f
762            self.assertEqual(LAST_FREED, 100)
763
764        def test_get_set(self):
765            # Test basic get/set round tripping.
766            f = self.get_func()
767
768            extra = ctypes.c_voidp()
769
770            SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(200))
771            # reset should free...
772            SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(300))
773            self.assertEqual(LAST_FREED, 200)
774
775            extra = ctypes.c_voidp()
776            GetExtra(f.__code__, FREE_INDEX, extra)
777            self.assertEqual(extra.value, 300)
778            del f
779
780        @threading_helper.requires_working_threading()
781        def test_free_different_thread(self):
782            # Freeing a code object on a different thread then
783            # where the co_extra was set should be safe.
784            f = self.get_func()
785            class ThreadTest(threading.Thread):
786                def __init__(self, f, test):
787                    super().__init__()
788                    self.f = f
789                    self.test = test
790                def run(self):
791                    del self.f
792                    self.test.assertEqual(LAST_FREED, 500)
793
794            SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(500))
795            tt = ThreadTest(f, self)
796            del f
797            tt.start()
798            tt.join()
799            self.assertEqual(LAST_FREED, 500)
800
801
802def load_tests(loader, tests, pattern):
803    tests.addTest(doctest.DocTestSuite())
804    return tests
805
806
807if __name__ == "__main__":
808    unittest.main()
809