From ef49c55620ee9bbbd4670f802f78b5779776c486 Mon Sep 17 00:00:00 2001
From: Frank Hoffmann <15r10nk-git@polarbit.de>
Date: Mon, 7 Feb 2022 23:12:54 +0100
Subject: [PATCH 1/7] feat: support for python 3.11

* This provides a new implementation, which uses co_positions() to lookup the ast node.
* It has a simpler implementation and better performance.
* Some limitations in the unit tests are removed for 3.11.
    * support for `and` and `or`
    * no ambiguities for generators
---
 executing/executing.py | 41 +++++++++++++++++++++++++++++++++++++++++
 tests/test_main.py     |  5 ++++-
 tests/utils.py         | 12 ++++++++----
 tox.ini                |  2 +-
 4 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index 1e06155..a941f37 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -309,6 +309,47 @@ def executing(cls, frame_or_tb):
             lineno = frame.f_lineno
             lasti = frame.f_lasti
 
+        if sys.version_info >= (3, 11):
+            # we can use co_positions() since 3.11, which has fewer limitations
+
+            positions = list(frame.f_code.co_positions())
+            source = cls.for_frame(frame)
+            stmts = source.statements_at_line(lineno)
+
+            def find_node(lasti):
+                position = positions[lasti // 2]
+                for node in source._nodes_by_line[position[0]]:
+                    if isinstance(node, (ast.expr, ast.stmt)) and position == (
+                        node.lineno,
+                        node.end_lineno,
+                        node.col_offset,
+                        node.end_col_offset,
+                    ):
+                        return node
+
+            node = find_node(lasti)
+            assert_(node != None)
+
+            if isinstance(node, (ast.ClassDef, function_node_types)):
+                # get the decorator by counting all CALL_FUNCTION ops until the next STORE_*
+                for idx, inst in enumerate(
+                    islice(dis.Bytecode(frame.f_code), lasti // 2, None)
+                ):
+                    if inst.opname.startswith("STORE_"):
+                        return Executing(
+                            frame, source, node, stmts, node.decorator_list[idx - 1]
+                        )
+
+                    if inst.opname in ("EXTENDED_ARG", "NOP"):
+                        continue
+
+                    assert_(inst.opname == "CALL_FUNCTION", inst)
+
+            if isinstance(node, ast.Expr):
+                node = node.value
+
+            return Executing(frame, source, node, stmts, None)
+
         code = frame.f_code
         key = (code, id(code), lasti)
         executing_cache = cls._class_local('__executing_cache', {})
diff --git a/tests/test_main.py b/tests/test_main.py
index 91ead33..ad2dbb8 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -122,8 +122,11 @@ def test_comprehensions(self):
         str([{tester(x) for x in [1]}, list(tester(x) for x in [1])])
         # but not if everything is the same
         # noinspection PyTypeChecker
-        with self.assertRaises(NotOneValueFound):
+        if sys.version_info >= (3, 11):
             str([{tester(x) for x in [1]}, {tester(x) for x in [2]}])
+        else:
+            with self.assertRaises(NotOneValueFound):
+                str([{tester(x) for x in [1]}, {tester(x) for x in [2]}])
 
     def test_lambda(self):
         self.assertEqual(
diff --git a/tests/utils.py b/tests/utils.py
index 90804c7..d3986af 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -89,11 +89,15 @@ def __lt__(self, other):
     __ne__ = __ge__ = __lt__
 
     def __bool__(self):
-        try:
-            self.get_node(None)
-        except RuntimeError:
+        if sys.version_info >= (3, 11):
+            self.get_node(ast.BoolOp)
             return False
-        assert 0
+        else:
+            try:
+                self.get_node(None)
+            except RuntimeError:
+                return False
+            assert 0
 
     __nonzero__ = __bool__
 
diff --git a/tox.ini b/tox.ini
index 2e2765a..d06f963 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py34,py35,py36,py37,py38,py39,py310,pypy2,pypy35,pypy36
+envlist = py27,py34,py35,py36,py37,py38,py39,py310,py311,pypy2,pypy35,pypy36
 
 [testenv]
 commands =

From 85707fe8b3df56c4bd42cc89c9a4732660b9511b Mon Sep 17 00:00:00 2001
From: Alex Hall <alex.mojaki@gmail.com>
Date: Tue, 8 Feb 2022 11:21:45 +0200
Subject: [PATCH 2/7] GHA: test 3.11 and PRs

---
 .github/workflows/test.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e8a979a..4769b1a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,13 +1,13 @@
 name: Tests
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   test:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy2, pypy-3.6]
+        python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10', 3.11-dev, pypy2, pypy-3.6]
 
     steps:
     - uses: actions/checkout@v2

From e0524e17e05166d7eab762446fdd88f7eea13301 Mon Sep 17 00:00:00 2001
From: Alex Hall <alex.mojaki@gmail.com>
Date: Tue, 8 Feb 2022 11:22:23 +0200
Subject: [PATCH 3/7] Use `only` instead of `find_node` function

---
 executing/executing.py | 29 +++++++++++++----------------
 1 file changed, 13 insertions(+), 16 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index a941f37..5bf9f44 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -316,19 +316,19 @@ def executing(cls, frame_or_tb):
             source = cls.for_frame(frame)
             stmts = source.statements_at_line(lineno)
 
-            def find_node(lasti):
-                position = positions[lasti // 2]
-                for node in source._nodes_by_line[position[0]]:
-                    if isinstance(node, (ast.expr, ast.stmt)) and position == (
-                        node.lineno,
-                        node.end_lineno,
-                        node.col_offset,
-                        node.end_col_offset,
-                    ):
-                        return node
-
-            node = find_node(lasti)
-            assert_(node != None)
+            position = positions[lasti // 2]
+            node = only(
+                node
+                for node in source._nodes_by_line[position[0]]
+                if isinstance(node, (ast.expr, ast.stmt))
+                if not isinstance(node, ast.Expr)
+                if position == (
+                    node.lineno,
+                    node.end_lineno,
+                    node.col_offset,
+                    node.end_col_offset,
+                )
+            )
 
             if isinstance(node, (ast.ClassDef, function_node_types)):
                 # get the decorator by counting all CALL_FUNCTION ops until the next STORE_*
@@ -345,9 +345,6 @@ def find_node(lasti):
 
                     assert_(inst.opname == "CALL_FUNCTION", inst)
 
-            if isinstance(node, ast.Expr):
-                node = node.value
-
             return Executing(frame, source, node, stmts, None)
 
         code = frame.f_code

From 7831ba9c251b63aadfa5f09f6e26622482dfee5c Mon Sep 17 00:00:00 2001
From: Alex Hall <alex.mojaki@gmail.com>
Date: Tue, 8 Feb 2022 11:23:03 +0200
Subject: [PATCH 4/7] Ignore EXTENDED_ARG/NOP when counting CALL_FUNCTION ops

---
 executing/executing.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index 5bf9f44..9ef3d7d 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -333,16 +333,15 @@ def executing(cls, frame_or_tb):
             if isinstance(node, (ast.ClassDef, function_node_types)):
                 # get the decorator by counting all CALL_FUNCTION ops until the next STORE_*
                 for idx, inst in enumerate(
-                    islice(dis.Bytecode(frame.f_code), lasti // 2, None)
+                    inst
+                    for inst in islice(dis.Bytecode(frame.f_code), lasti // 2, None)
+                    if inst.opname not in ("EXTENDED_ARG", "NOP")
                 ):
                     if inst.opname.startswith("STORE_"):
                         return Executing(
                             frame, source, node, stmts, node.decorator_list[idx - 1]
                         )
 
-                    if inst.opname in ("EXTENDED_ARG", "NOP"):
-                        continue
-
                     assert_(inst.opname == "CALL_FUNCTION", inst)
 
             return Executing(frame, source, node, stmts, None)

From c57a81d907f41e41cb287708e8ca43954a83fbaf Mon Sep 17 00:00:00 2001
From: Frank Hoffmann <15r10nk-git@polarbit.de>
Date: Tue, 8 Feb 2022 23:33:24 +0100
Subject: [PATCH 5/7] fix: fixed decorator detection for 3.11.0a5

---
 executing/executing.py | 78 ++++++++++++++++++++++++++++--------------
 1 file changed, 53 insertions(+), 25 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index 9ef3d7d..5a466b5 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -316,33 +316,61 @@ def executing(cls, frame_or_tb):
             source = cls.for_frame(frame)
             stmts = source.statements_at_line(lineno)
 
-            position = positions[lasti // 2]
-            node = only(
-                node
-                for node in source._nodes_by_line[position[0]]
-                if isinstance(node, (ast.expr, ast.stmt))
-                if not isinstance(node, ast.Expr)
-                if position == (
-                    node.lineno,
-                    node.end_lineno,
-                    node.col_offset,
-                    node.end_col_offset,
+            def find_node(index):
+                position = positions[index // 2]
+                return only(
+                    node
+                    for node in source._nodes_by_line[position[0]]
+                    if isinstance(node, (ast.expr, ast.stmt))
+                    if not isinstance(node, ast.Expr)
+                    if position
+                    == (
+                        node.lineno,
+                        node.end_lineno,
+                        node.col_offset,
+                        node.end_col_offset,
+                    )
                 )
-            )
 
-            if isinstance(node, (ast.ClassDef, function_node_types)):
-                # get the decorator by counting all CALL_FUNCTION ops until the next STORE_*
-                for idx, inst in enumerate(
-                    inst
-                    for inst in islice(dis.Bytecode(frame.f_code), lasti // 2, None)
-                    if inst.opname not in ("EXTENDED_ARG", "NOP")
-                ):
-                    if inst.opname.startswith("STORE_"):
-                        return Executing(
-                            frame, source, node, stmts, node.decorator_list[idx - 1]
-                        )
-
-                    assert_(inst.opname == "CALL_FUNCTION", inst)
+            node = find_node(lasti)
+
+            if (
+                isinstance(node.parent, (ast.ClassDef, function_node_types))
+                and node in node.parent.decorator_list
+            ):
+                node_func = node.parent
+                index = lasti
+                bc_list = list(dis.Bytecode(frame.f_code))
+
+                def opname(i):
+                    return bc_list[i // 2].opname
+
+                while True:
+                    # idx-2: PRECALL_FUNCTION
+                    # idx:   CALL
+                    # idx+2: STORE_* / PRECALL_FUNCTION
+
+                    if not (
+                        opname(index - 2) == "PRECALL_FUNCTION"
+                        and opname(index) == "CALL"
+                    ):
+                        break
+
+                    if (
+                        bc_list[index // 2 - 1].positions
+                        != bc_list[index // 2].positions
+                    ):
+                        break
+
+                    if find_node(index) not in node_func.decorator_list:
+                        break
+
+                    if find_node(index + 2) == node_func and opname(
+                        index + 2
+                    ).startswith("STORE_"):
+                        return Executing(frame, source, node_func, {node_func}, node)
+
+                    index += 4
 
             return Executing(frame, source, node, stmts, None)
 

From 6691eec238d0e8574bdc2b5c8dbeb9303764bf78 Mon Sep 17 00:00:00 2001
From: Frank Hoffmann <15r10nk-git@polarbit.de>
Date: Thu, 31 Mar 2022 15:30:50 +0200
Subject: [PATCH 6/7] fix: detect decorators in 3.11.a6 correctly and ignore
 qualname checks for 3.11

---
 executing/executing.py | 25 ++++++++++++++-----------
 tests/test_main.py     |  2 +-
 2 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index 5a466b5..9ca2bb2 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -340,24 +340,22 @@ def find_node(index):
             ):
                 node_func = node.parent
                 index = lasti
-                bc_list = list(dis.Bytecode(frame.f_code))
+                bc_list = list(dis.Bytecode(frame.f_code, show_caches=True))
 
                 def opname(i):
                     return bc_list[i // 2].opname
 
                 while True:
-                    # idx-2: PRECALL_FUNCTION
+                    # idx-4: PRECALL
+                    # idx-2: CACHE
                     # idx:   CALL
-                    # idx+2: STORE_* / PRECALL_FUNCTION
+                    # idx+2: STORE_* / CACHE
 
-                    if not (
-                        opname(index - 2) == "PRECALL_FUNCTION"
-                        and opname(index) == "CALL"
-                    ):
+                    if not (opname(index - 4) == "PRECALL" and opname(index) == "CALL"):
                         break
 
                     if (
-                        bc_list[index // 2 - 1].positions
+                        bc_list[index // 2 - 2].positions
                         != bc_list[index // 2].positions
                     ):
                         break
@@ -365,9 +363,14 @@ def opname(i):
                     if find_node(index) not in node_func.decorator_list:
                         break
 
-                    if find_node(index + 2) == node_func and opname(
-                        index + 2
-                    ).startswith("STORE_"):
+                    index += 2
+
+                    while opname(index) == "CACHE":
+                        index += 2
+
+                    if find_node(index) == node_func and opname(index).startswith(
+                        "STORE_"
+                    ):
                         return Executing(frame, source, node_func, {node_func}, node)
 
                     index += 4
diff --git a/tests/test_main.py b/tests/test_main.py
index ad2dbb8..cd8ee83 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -470,7 +470,7 @@ def check_filename(self, filename, check_names):
         print(filename)
         source = Source.for_filename(filename)
 
-        if PY3:
+        if PY3 and sys.version_info < (3, 11):
             code = compile(source.text, filename, "exec", dont_inherit=True)
             for subcode, qualname in find_qualnames(code):
                 if not qualname.endswith(">"):

From 1b2b96a23a164a58f512fd35a16690a4d3915e3c Mon Sep 17 00:00:00 2001
From: Frank Hoffmann <15r10nk-git@polarbit.de>
Date: Fri, 1 Apr 2022 01:32:02 +0200
Subject: [PATCH 7/7] wip: fix linenumber problem

---
 executing/executing.py | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/executing/executing.py b/executing/executing.py
index 9ca2bb2..bfc1c2c 100644
--- a/executing/executing.py
+++ b/executing/executing.py
@@ -78,13 +78,21 @@ def wrapper(*args):
     # noinspection PyUnresolvedReferences
     text_type = unicode
 
-try:
-    # noinspection PyUnresolvedReferences
-    _get_instructions = dis.get_instructions
-except AttributeError:
-    class Instruction(namedtuple('Instruction', 'offset argval opname starts_line')):
-        lineno = None
 
+if hasattr(dis, "get_instructions"):
+
+    if sys.version_info >= (3, 11):
+
+        def _get_instructions(co):
+            for inst in dis.get_instructions(co, show_caches=True):
+                if inst.opname != "CACHE":
+                    yield inst
+
+    else:
+        # noinspection PyUnresolvedReferences
+        _get_instructions = dis.get_instructions
+
+else:
 
     from dis import HAVE_ARGUMENT, EXTENDED_ARG, hasconst, opname, findlinestarts, hasname
 
