Repository: rocky/python-xdis (language: python)

Issue:
Python 3.10 support
Most of the changes are simple enough and are in branch 3.10-adjust. 

The is a long-standing design flaw in converting a python version into a floating point number and using that in comparisons to check whether we are running or disassembling particular sets of versions.  With 3.10 this no longer works because 3.10 is treated as the same as 3.1. 

The change to use a tuple instead of a float is pretty straight-forward. However it is pervasive. 


Discussion / hints:

Fix patch:
diff --git a/admin-tools/check-newest-versions.sh b/admin-tools/check-newest-versions.sh
index 6a886771..a973a373 100755
--- a/admin-tools/check-newest-versions.sh
+++ b/admin-tools/check-newest-versions.sh
@@ -11,9 +11,9 @@ cd $(dirname ${BASH_SOURCE[0]})
 if ! source ./pyenv-newest-versions ; then
     exit $?
 fi
-if ! source ./setup-master.sh ; then
-    exit $?
-fi
+# if ! source ./setup-master.sh ; then
+#     exit $?
+# fi
 cd ..
 for version in $PYVERSIONS; do
     echo --- $version ---
diff --git a/admin-tools/pyenv-newest-versions b/admin-tools/pyenv-newest-versions
index 3c3c193b..449dc434 100644
--- a/admin-tools/pyenv-newest-versions
+++ b/admin-tools/pyenv-newest-versions
@@ -5,4 +5,4 @@ if [[ $0 == ${BASH_SOURCE[0]} ]] ; then
     echo "This script should be *sourced* rather than run directly through bash"
     exit 1
 fi
-export PYVERSIONS='3.6.15 pypy3.6-7.3.1 3.7.12 pypy3.7-7.3.5 3.8.11 3.9.7'
+export PYVERSIONS='3.6.15 pypy3.6-7.3.1 3.7.12 pypy3.7-7.3.5 pyston-2.3 3.8.11 3.9.7 3.10.0'
diff --git a/appveyor.yml b/appveyor.yml
index b1787450..2eea8dcf 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -6,26 +6,12 @@ environment:
     CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd"
 
   matrix:
-
     # Pre-installed Python versions, which Appveyor may upgrade to
     # a later point release.
     # See: http://www.appveyor.com/docs/installed-software#python
 
-    # - PYTHON: "C:\\Python27"
-    #   PYTHON_VERSION: "2.7.x"
-    #   PYTHON_ARCH: "32"
-
-    - PYTHON: "C:\\Python27-x64"
-      PYTHON_VERSION: "2.7.x"
-      PYTHON_ARCH: "64"
-
-    # - PYTHON: "C:\\Python26"
-    #   PYTHON_VERSION: "2.6.x"
-    #   PYTHON_ARCH: "32"
-
-    # - PYTHON: "C:\\Python26-x64"
-    #   PYTHON_VERSION: "2.6.x"
-    #   PYTHON_ARCH: "64"
+    - TOXENV: py36
+    - TOXENV: py37
 
 install:
   # We need wheel installed to build wheels
diff --git a/xdis/.gitignore b/xdis/.gitignore
index 4d3cbae7..44879ca7 100644
--- a/xdis/.gitignore
+++ b/xdis/.gitignore
@@ -1,2 +1,3 @@
 /.coverage
 /.mypy_cache
+/.python-version
diff --git a/xdis/__init__.py b/xdis/__init__.py
index a8787a0d..3b1b637f 100644
--- a/xdis/__init__.py
+++ b/xdis/__init__.py
@@ -1,5 +1,5 @@
 """
-  Copyright (c) 2015-2017, 2020 by Rocky Bernstein
+  Copyright (c) 2015-2017, 2020-2021 by Rocky Bernstein
   Copyright (c) 2000 by hartmut Goebel <h.goebel@crazy-compilers.com>
 
   This program is free software; you can redistribute it and/or
@@ -30,6 +30,7 @@
     PYTHON3,
     PYTHON_VERSION,
     PYTHON_VERSION_STR,
+    PYTHON_VERSION_TRIPLE,
 )
 
 from xdis.bytecode import (
diff --git a/xdis/bin/pydisasm.py b/xdis/bin/pydisasm.py
index acc915df..6326e793 100644
--- a/xdis/bin/pydisasm.py
+++ b/xdis/bin/pydisasm.py
@@ -11,7 +11,7 @@
 import os.path as osp
 
 from xdis.version import __version__
-from xdis import PYTHON_VERSION
+from xdis.version_info import PYTHON_VERSION_STR, PYTHON_VERSION_TRIPLE
 from xdis import disassemble_file
 
 program, ext = os.path.splitext(os.path.basename(__file__))
@@ -40,15 +40,15 @@ def main(format, files):
     the Python interpreter used to run this program. For example, you can disassemble Python 3.6.9
     bytecode from Python 2.7.15 and vice versa.
     """
-    if not (2.7 <= PYTHON_VERSION <= 3.9):
-        if 2.4 <= PYTHON_VERSION <= 2.6:
+    if not ((2, 7) <= PYTHON_VERSION_TRIPLE < (3, 11)):
+        if (2, 4) <= PYTHON_VERSION_TRIPLE <= (2, 6):
             sys.stderr.write(
-                "This code works on 2.7..3.8. code that works for %s can be found in the python-2.4 branch\n"
-                % PYTHON_VERSION
+                "This code works on 2.7 to 3.10. code that works for %s can be found in the python-2.4 branch\n"
+                % PYTHON_VERSION_STR
             )
             sys.exit(1)
         sys.stderr.write(
-            "This works on Python version 2.7..3.9; have %s.\n" % PYTHON_VERSION
+            "This works on Python version 2.7 to 3.10; have %s.\n" % PYTHON_VERSION_STR
         )
         sys.exit(2)
 
diff --git a/xdis/bytecode.py b/xdis/bytecode.py
index 79f5b4d2..d5a42ce0 100644
--- a/xdis/bytecode.py
+++ b/xdis/bytecode.py
@@ -75,9 +75,9 @@ def offset2line(offset, linestarts):
 def _get_const_info(const_index, const_list):
     """Helper to get optional details about const references
 
-       Returns the dereferenced constant and its repr if the constant
-       list is defined.
-       Otherwise returns the constant index and its repr().
+    Returns the dereferenced constant and its repr if the constant
+    list is defined.
+    Otherwise returns the constant index and its repr().
     """
     argval = const_index
     if const_list is not None:
@@ -96,9 +96,9 @@ def _get_const_info(const_index, const_list):
 def _get_name_info(name_index, name_list):
     """Helper to get optional details about named references
 
-       Returns the dereferenced name as both value and repr if the name
-       list is defined.
-       Otherwise returns the name index and its repr().
+    Returns the dereferenced name as both value and repr if the name
+    list is defined.
+    Otherwise returns the name index and its repr().
     """
     argval = name_index
     if (
@@ -138,7 +138,7 @@ def get_instructions_bytes(
 
     # FIXME: We really need to distinguish 3.6.0a1 from 3.6.a3.
     # See below FIXME
-    python_36 = True if opc.python_version >= 3.6 else False
+    python_36 = True if opc.python_version >= (3, 6) else False
 
     starts_line = None
     # enumerate() is not an option, since we sometimes process
@@ -201,10 +201,14 @@ def get_instructions_bytes(
                 optype = "name"
             elif op in opc.JREL_OPS:
                 argval = i + arg
+                if opc.python_version >= (3, 10):
+                    argval *= 2
                 argrepr = "to " + repr(argval)
                 optype = "jrel"
             elif op in opc.JABS_OPS:
                 argval = arg
+                if opc.python_version >= (3, 10):
+                    argval *= 2
                 argrepr = "to " + repr(argval)
                 optype = "jabs"
             elif op in opc.LOCAL_OPS:
@@ -308,14 +312,14 @@ def __repr__(self):
 
     @classmethod
     def from_traceback(cls, tb):
-        """ Construct a Bytecode from the given traceback """
+        """Construct a Bytecode from the given traceback"""
         while tb.tb_next:
             tb = tb.tb_next
         return cls(tb.tb_frame.f_code, current_offset=tb.tb_lasti)
 
     def info(self):
         """Return formatted information about the code object."""
-        return format_code_info(self.codeobj, self.opc.version)
+        return format_code_info(self.codeobj, self.opc.version_tuple)
 
     def dis(self, asm_format="classic"):
         """Return a formatted view of the bytecode operations."""
@@ -393,8 +397,9 @@ def disassemble_bytes(
                 file.write("\n")
             is_current_instr = instr.offset == lasti
             file.write(
-                instr.disassemble(self.opc, lineno_width, is_current_instr, asm_format,
-                                  instructions)
+                instr.disassemble(
+                    self.opc, lineno_width, is_current_instr, asm_format, instructions
+                )
                 + "\n"
             )
             pass
diff --git a/xdis/codetype/__init__.py b/xdis/codetype/__init__.py
index afd6b2ef..8d918b98 100644
--- a/xdis/codetype/__init__.py
+++ b/xdis/codetype/__init__.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2020 by Rocky Bernstein
+# (C) Copyright 2020-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -25,26 +25,21 @@
 from xdis.codetype.code38 import *
 
 import types
-from xdis.version_info import PYTHON_VERSION
+from xdis.version_info import PYTHON_VERSION_TRIPLE
 
 
-def codeType2Portable(code, version=PYTHON_VERSION):
+def codeType2Portable(code, version_tuple=PYTHON_VERSION_TRIPLE):
     """Converts a native types.CodeType code object into a the
-corresponding more flexible xdis Code type,.
+    corresponding more flexible xdis Code type,.
     """
     if isinstance(code, CodeBase):
         return code
-    if not (
-            isinstance(code, types.CodeType)
-            or isinstance(code, CodeTypeUnion)
-    ):
+    if not (isinstance(code, types.CodeType) or isinstance(code, CodeTypeUnion)):
         raise TypeError(
             "parameter expected to be a types.CodeType type; is %s instead" % type(code)
         )
-    if isinstance(version, str):
-        version = float(version)
-    if version >= 3.0:
-        if version < 3.8:
+    if version_tuple >= (3, 0):
+        if version_tuple < (3, 8):
             return Code3(
                 code.co_argcount,
                 code.co_kwonlyargcount,
@@ -81,7 +76,7 @@ def codeType2Portable(code, version=PYTHON_VERSION):
                 code.co_freevars,
                 code.co_cellvars,
             )
-    elif version > 2.0:
+    elif version_tuple > (2, 0):
         # 2.0 .. 2.7
         return Code2(
             code.co_argcount,
@@ -96,12 +91,12 @@ def codeType2Portable(code, version=PYTHON_VERSION):
             code.co_name,
             code.co_firstlineno,
             code.co_lnotab,
-            code.co_freevars,     # not in 1.x
-            code.co_cellvars,     # not in 1.x
+            code.co_freevars,  # not in 1.x
+            code.co_cellvars,  # not in 1.x
         )
     else:
         # 1.0 .. 1.5
-        if version < 1.5:
+        if version_tuple < (1, 5):
             # 1.0 .. 1.3
             return Code13(
                 code.co_argcount,
@@ -126,34 +121,36 @@ def codeType2Portable(code, version=PYTHON_VERSION):
                 code.co_varnames,
                 code.co_filename,
                 code.co_name,
-                code.co_firstlineno, # Not in 1.0..1.4
-                code.co_lnotab,      # Not in 1.0..1.4
+                code.co_firstlineno,  # Not in 1.0..1.4
+                code.co_lnotab,  # Not in 1.0..1.4
             )
 
-def portableCodeType(version=PYTHON_VERSION):
+
+def portableCodeType(version_tuple=PYTHON_VERSION_TRIPLE):
     """
     Return the portable CodeType version for the supplied Python release version.
     `version` is a floating-point number, like 2.7, or 3.9. If no version
     number is supplied we'll use the current interpreter version.
     """
-    if version >= 3.0:
-        if version < 3.8:
+    if version_tuple >= (3, 0):
+        if version_tuple < (3, 8):
             # 3.0 .. 3.7
             return Code3
         else:
             # 3.8 ..
             return Code38
-    elif version > 2.0:
+    elif version_tuple > (2, 0):
         # 2.0 .. 2.7
         return Code2
     else:
         # 1.0 .. 1.5
-        if version <= 1.3:
+        if version_tuple <= (1, 3):
             return Code13
         else:
             return Code15
     raise RuntimeError("Implementation bug: can't handle version %s" % version)
 
+
 # In contrast to Code3, Code2, etc. you can use CodeTypeUnint for building
 # an incomplete code type, which might be converted to another code type
 # later.
@@ -164,23 +161,23 @@ def portableCodeType(version=PYTHON_VERSION):
 # Note: default values of `None` indicate a required parameter.
 # default values of -1, (None,) or "" indicate an unsupplied parameter.
 def to_portable(
-        co_argcount,
-        co_posonlyargcount = -1,  # 3.8+
-        co_kwonlyargcount = -1,   # 3.0+
-        co_nlocals = None,
-        co_stacksize = -1,        # 1.5+
-        co_flags = None,
-        co_code = None,           # 3.0+ this type changes from <str> to <bytes>
-        co_consts = None,
-        co_names = None,
-        co_varnames = None,
-        co_filename = None,
-        co_name = None,
-        co_firstlineno = -1,
-        co_lnotab = "",           # 1.5+; 3.0+ this type changes from <str> to <bytes>
-        co_freevars = (None,),    # 2.0+
-        co_cellvars = (None,),    # 2.0+
-        version = PYTHON_VERSION,
+    co_argcount,
+    co_posonlyargcount=-1,  # 3.8+
+    co_kwonlyargcount=-1,  # 3.0+
+    co_nlocals=None,
+    co_stacksize=-1,  # 1.5+
+    co_flags=None,
+    co_code=None,  # 3.0+ this type changes from <str> to <bytes>
+    co_consts=None,
+    co_names=None,
+    co_varnames=None,
+    co_filename=None,
+    co_name=None,
+    co_firstlineno=-1,
+    co_lnotab="",  # 1.5+; 3.0+ this type changes from <str> to <bytes>
+    co_freevars=(None,),  # 2.0+
+    co_cellvars=(None,),  # 2.0+
+    version_triple=PYTHON_VERSION_TRIPLE,
 ):
     code = CodeTypeUnion(
         co_argcount,
@@ -200,7 +197,8 @@ def to_portable(
         co_freevars,
         co_cellvars,
     )
-    return codeType2Portable(code, version)
+    return codeType2Portable(code, version_triple)
+
 
 if __name__ == "__main__":
     x = codeType2Portable(to_portable.__code__)
diff --git a/xdis/cross_dis.py b/xdis/cross_dis.py
index 121d1f4e..b8420bda 100644
--- a/xdis/cross_dis.py
+++ b/xdis/cross_dis.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2020 by Rocky Bernstein
+# (C) Copyright 2020-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -18,6 +18,8 @@
 # However it appears that Python names and code has copied a bit heavily from
 # earlier versions of xdis (and without attribution).
 
+from xdis.version_info import PYTHON_VERSION_TRIPLE
+
 from xdis.util import (
     COMPILER_FLAG_NAMES,
     PYPY_COMPILER_FLAG_NAMES,
@@ -28,10 +30,10 @@
 
 def _try_compile(source, name):
     """Attempts to compile the given source, first as an expression and
-       then as a statement if the first approach fails.
+    then as a statement if the first approach fails.
 
-       Utility function to accept strings in functions that otherwise
-       expect code objects
+    Utility function to accept strings in functions that otherwise
+    expect code objects
     """
     try:
         c = compile(source, name, "eval")
@@ -89,9 +91,9 @@ def findlinestarts(code, dup_lines=False):
             yield (offset, lineno)
 
 
-def code_info(x, version, is_pypy=False):
+def code_info(x, version_tuple, is_pypy=False):
     """Formatted details of methods, functions, or code."""
-    return format_code_info(get_code_object(x), version, is_pypy=is_pypy)
+    return format_code_info(get_code_object(x), version_tuple, is_pypy=is_pypy)
 
 
 def get_code_object(x):
@@ -117,7 +119,33 @@ def get_code_object(x):
     raise TypeError("don't know how to disassemble %s objects" % type(x).__name__)
 
 
-def get_jump_targets(code, opc):
+def findlabels(code, opc):
+    if opc.version_tuple < (3, 10):
+        return findlabels_pre_310(code, opc)
+    else:
+        return findlabels_310(code, opc)
+
+
+def findlabels_310(code, opc):
+    """Returns a list of instruction offsets in the supplied bytecode
+    which are the targets of some sort of jump instruction.
+    """
+    labels = []
+    for offset, op, arg in unpack_opargs_bytecode_310(code, opc):
+        if arg is not None:
+            jump_offset = -1
+            if op in opc.JREL_OPS:
+                label = offset + 2 + arg * 2
+            elif op in opc.JABS_OPS:
+                label = arg * 2
+            else:
+                continue
+            if label not in labels:
+                labels.append(label)
+    return labels
+
+
+def findlabels_pre_310(code, opc):
     """Returns a list of instruction offsets in the supplied bytecode
     which are the targets of some sort of jump instruction.
     """
@@ -136,9 +164,6 @@ def get_jump_targets(code, opc):
     return offsets
 
 
-findlabels = get_jump_targets
-
-
 def instruction_size(op, opc):
     """For a given opcode, `op`, in opcode module `opc`,
     return the size, in bytes, of an `op` instruction.
@@ -147,24 +172,24 @@ def instruction_size(op, opc):
     Python before version 3.6 this will be either 1 or 3 bytes.  In
     Python 3.6 or later, it is 2 bytes or a "word"."""
     if op < opc.HAVE_ARGUMENT:
-        return 2 if opc.version >= 3.6 else 1
+        return 2 if opc.version_tuple >= (3, 6) else 1
     else:
-        return 2 if opc.version >= 3.6 else 3
+        return 2 if opc.version_tuple >= (3, 6) else 3
 
 
 # Compatiblity
 op_size = instruction_size
 
 
-def show_code(co, version, file=None, is_pypy=False):
+def show_code(co, version_tuple, file=None, is_pypy=False):
     """Print details of methods, functions, or code to *file*.
 
     If *file* is not provided, the output is printed on stdout.
     """
     if file is None:
-        print(code_info(co, version, is_pypy=is_pypy))
+        print(code_info(co, version_tuple, is_pypy=is_pypy))
     else:
-        file.write(code_info(co, version) + "\n")
+        file.write(code_info(co, version_tuple) + "\n")
 
 
 def op_has_argument(op, opc):
@@ -190,39 +215,39 @@ def pretty_flags(flags, is_pypy=False):
     return "%s (%s)" % (result, " | ".join(names))
 
 
-def format_code_info(co, version, name=None, is_pypy=False):
+def format_code_info(co, version_tuple, name=None, is_pypy=False):
     if not name:
         name = co.co_name
     lines = []
 
-    if not (name == "?" and version <= 2.4):
+    if not (name == "?" and version_tuple <= (2, 4)):
         lines.append("# Method Name:       %s" % name)
 
     # Python before version 2.4 and earlier didn't store a name for the main routine.
     # Later versions use "<module>"
     lines.append("# Filename:          %s" % co.co_filename)
 
-    if version >= 1.3:
+    if version_tuple >= (1, 3):
         lines.append("# Argument count:    %s" % co.co_argcount)
 
-    if version >= 3.8 and hasattr(co, "co_posonlyargcount"):
+    if version_tuple >= (3, 8) and hasattr(co, "co_posonlyargcount"):
         lines.append("# Position-only argument count: %s" % co.co_posonlyargcount)
 
-    if version >= 3.0 and hasattr(co, "co_kwonlyargcount"):
+    if version_tuple >= (3, 0) and hasattr(co, "co_kwonlyargcount"):
         lines.append("# Keyword-only arguments: %s" % co.co_kwonlyargcount)
 
     pos_argc = co.co_argcount
-    if version >= 1.3:
+    if version_tuple >= (1, 3):
         lines.append("# Number of locals:  %s" % co.co_nlocals)
-    if version >= 1.5:
+    if version_tuple >= (1, 5):
         lines.append("# Stack size:        %s" % co.co_stacksize)
 
-    if version >= 1.3:
+    if version_tuple >= (1, 3):
         lines.append(
             "# Flags:             %s" % pretty_flags(co.co_flags, is_pypy=is_pypy)
         )
 
-    if version >= 1.5:
+    if version_tuple >= (1, 5):
         lines.append("# First Line:        %s" % co.co_firstlineno)
     # if co.co_freevars:
     #     lines.append("# Freevars:      %s" % str(co.co_freevars))
@@ -246,7 +271,7 @@ def format_code_info(co, version, name=None, is_pypy=False):
         lines.append("# Local variables:")
         for i, n in enumerate(co.co_varnames[pos_argc:]):
             lines.append("# %4d: %s" % (pos_argc + i, n))
-    if version > 2.0:
+    if version_tuple > (2, 0):
         if co.co_freevars:
             lines.append("# Free variables:")
             for i_n in enumerate(co.co_freevars):
@@ -266,6 +291,23 @@ def extended_arg_val(opc, val):
     return val << opc.EXTENDED_ARG_SHIFT
 
 
+def unpack_opargs_bytecode_310(code, opc):
+    extended_arg = 0
+    try:
+        n = len(code)
+    except TypeError:
+        code = code.co_code
+        n = len(code)
+    for offset in range(0, n, 2):
+        op = code2num(code, offset)
+        if op_has_argument(op, opc):
+            arg = code2num(code, offset + 1) | extended_arg
+            extended_arg = extended_arg_val(opc, arg) if op == opc.EXTENDED_ARG else 0
+        else:
+            arg = None
+        yield (offset, op, arg)
+
+
 # This is modified from Python 3.6's dis
 def unpack_opargs_bytecode(code, opc):
     extended_arg = 0
@@ -336,27 +378,27 @@ def xstack_effect(opcode, opc, oparg=None, jump=None):
     pop, push = opc.oppop[opcode], opc.oppush[opcode]
     opname = opc.opname[opcode]
     if opname in ("BUILD_MAP",):
-        if opc.version >= 3.5:
+        if opc.version_tuple >= (3, 5):
             return 1 - (2 * oparg)
-    elif opname in ("UNPACK_SEQUENCE", "UNPACK_EX") and opc.version >= 3.0:
+    elif opname in ("UNPACK_SEQUENCE", "UNPACK_EX") and opc.version_tuple >= (3, 0):
         return push + oparg
-    elif opname in ("BUILD_SLICE") and opc.version <= 2.7:
+    elif opname in ("BUILD_SLICE") and opc.version_tuple <= (2, 7):
         if oparg == 3:
             return -2
         else:
             return -1
         pass
     elif opname == "MAKE_FUNCTION":
-        if opc.version >= 3.5:
+        if opc.version_tuple >= (3, 5):
             if 0 <= oparg <= 10:
-                if opc.version == 3.5:
+                if opc.version_tuple == (3, 5):
                     return [-1, -2, -3, -3, -2, -3, -3, -4, -2, -3, -3, -4][oparg]
-                elif opc.version >= 3.6:
+                elif opc.version_tuple >= (3, 6):
                     return [-1, -2, -2, -3, -2, -3, -3, -4, -2, -3, -3, -4][oparg]
             else:
                 return None
     elif opname == "CALL_FUNCTION_EX":
-        if opc.version >= 3.5:
+        if opc.version_tuple >= (3, 5):
             if 0 <= oparg <= 10:
                 return [-1, -2, -1][oparg]
             else:
@@ -382,7 +424,10 @@ def check_stack_effect():
     else:
         variant = ""
     opc = get_opcode_module(None, variant)
-    for opname, opcode, in opc.opmap.items():
+    for (
+        opname,
+        opcode,
+    ) in opc.opmap.items():
         if opname in ("EXTENDED_ARG", "NOP"):
             continue
         xdis_args = [opcode, opc]
@@ -391,7 +436,7 @@ def check_stack_effect():
             xdis_args.append(0)
             dis_args.append(0)
         if (
-            PYTHON_VERSION > 3.7
+            PYTHON_VERSION_TRIPLE > (3, 7)
             and opcode in opc.CONDITION_OPS
             and opname
             not in (
@@ -426,7 +471,12 @@ def check_stack_effect():
 
 
 if __name__ == "__main__":
-    from xdis import PYTHON_VERSION
+    from dis import findlabels as findlabels_std
+
+    code = findlabels.__code__.co_code
+    from xdis.op_imports import get_opcode_module
 
-    if PYTHON_VERSION >= 3.4:
+    opc = get_opcode_module()
+    assert findlabels(code, opc) == findlabels_std(code)
+    if PYTHON_VERSION_TRIPLE >= (3, 4):
         check_stack_effect()
diff --git a/xdis/disasm.py b/xdis/disasm.py
index 553e8cc7..3f8b5db1 100644
--- a/xdis/disasm.py
+++ b/xdis/disasm.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016-2018, 2020 by Rocky Bernstein
+# Copyright (c) 2016-2018, 2020-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -26,21 +26,19 @@
 
 import xdis
 
-from xdis import IS_PYPY, PYTHON_VERSION
 from xdis.bytecode import Bytecode
 from xdis.codetype import iscode, codeType2Portable
 from xdis.load import check_object_path, load_module
 from xdis.magics import PYTHON_MAGIC_INT
 from xdis.cross_dis import format_code_info
 from xdis.version import __version__
+from xdis.version_info import IS_PYPY, PYTHON_VERSION_TRIPLE
 from xdis.op_imports import op_imports, remap_opcodes
 
 
-def get_opcode(version, is_pypy, alternate_opmap=None):
+def get_opcode(version_tuple, is_pypy, alternate_opmap=None):
     # Set up disassembler with the right opcodes
-    if type(version) in (list, tuple):
-        version = ".".join([str(x) for x in version])
-    lookup = str(version)
+    lookup = ".".join((str(i) for i in version_tuple))
     if is_pypy:
         lookup += "pypy"
     if lookup in op_imports.keys():
@@ -56,7 +54,7 @@ def get_opcode(version, is_pypy, alternate_opmap=None):
 
 
 def show_module_header(
-    bytecode_version,
+    version_tuple,
     co,
     timestamp,
     out=sys.stdout,
@@ -68,6 +66,7 @@ def show_module_header(
     show_filename=True,
 ):
 
+    bytecode_version = ".".join((str(i) for i in version_tuple))
     real_out = out or sys.stdout
     if is_pypy:
         co_pypy_str = "PyPy "
@@ -97,8 +96,10 @@ def show_module_header(
                 "\n# ".join(sys.version.split("\n")),
             )
         )
-    if PYTHON_VERSION < 3.0 and bytecode_version >= 3.0:
-        real_out.write("\n## **Warning** bytecode strings will be converted to strings.\n")
+    if PYTHON_VERSION_TRIPLE < (3, 0) and bytecode_version >= 3.0:
+        real_out.write(
+            "\n## **Warning** bytecode strings will be converted to strings.\n"
+        )
         real_out.write("## To avoid loss, run this from Python 3.0 or greater\n\n")
 
     if timestamp is not None:
@@ -114,7 +115,7 @@ def show_module_header(
 
 
 def disco(
-    bytecode_version,
+    version_tuple,
     co,
     timestamp,
     out=sys.stdout,
@@ -133,8 +134,10 @@ def disco(
 
     assert iscode(co)
 
+    bytecode_version = ".".join((str(i) for i in version_tuple))
+
     show_module_header(
-        bytecode_version,
+        version_tuple,
         co,
         timestamp,
         out,
@@ -150,20 +153,23 @@ def disco(
     real_out = out or sys.stdout
 
     if co.co_filename and asm_format != "xasm":
-        real_out.write(format_code_info(co, bytecode_version) + "\n")
+        real_out.write(format_code_info(co, version_tuple) + "\n")
         pass
 
-    opc = get_opcode(bytecode_version, is_pypy, alternate_opmap)
+    opc = get_opcode(version_tuple, is_pypy, alternate_opmap)
 
     if asm_format == "xasm":
-        disco_loop_asm_format(opc, bytecode_version, co, real_out, {}, set([]))
+        disco_loop_asm_format(opc, version_tuple, co, real_out, {}, set([]))
     else:
         queue = deque([co])
-        disco_loop(opc, bytecode_version, queue, real_out, asm_format=asm_format,
-                   dup_lines=True)
+        disco_loop(
+            opc, version_tuple, queue, real_out, asm_format=asm_format, dup_lines=True
+        )
 
 
-def disco_loop(opc, version, queue, real_out, dup_lines=False, asm_format="classic"):
+def disco_loop(
+    opc, version_tuple, queue, real_out, dup_lines=False, asm_format="classic"
+):
     """Disassembles a queue of code objects. If we discover
     another code object which will be found in co_consts, we add
     the new code to the list. Note that the order of code discovery
@@ -177,7 +183,7 @@ def disco_loop(opc, version, queue, real_out, dup_lines=False, asm_format="class
     while len(queue) > 0:
         co = queue.popleft()
         if co.co_name not in ("<module>", "?"):
-            real_out.write("\n" + format_code_info(co, version) + "\n")
+            real_out.write("\n" + format_code_info(co, version_tuple) + "\n")
 
         bytecode = Bytecode(co, opc, dup_lines=dup_lines)
         real_out.write(bytecode.dis(asm_format=asm_format) + "\n")
@@ -194,7 +200,7 @@ def code_uniquify(basename, co_code):
     return "%s_0x%x" % (basename, id(co_code))
 
 
-def disco_loop_asm_format(opc, version, co, real_out, fn_name_map, all_fns):
+def disco_loop_asm_format(opc, version_tuple, co, real_out, fn_name_map, all_fns):
     """Produces disassembly in a format more conducive to
     automatic assembly by producing inner modules before they are
     used by outer ones. Since this is recusive, we'll
@@ -214,7 +220,7 @@ def disco_loop_asm_format(opc, version, co, real_out, fn_name_map, all_fns):
                 c_compat = c
 
             disco_loop_asm_format(
-                opc, version, c_compat, real_out, fn_name_map, all_fns
+                opc, version_tuple, c_compat, real_out, fn_name_map, all_fns
             )
 
             m = re.match(".* object <(.+)> at", str(c))
@@ -253,7 +259,7 @@ def disco_loop_asm_format(opc, version, co, real_out, fn_name_map, all_fns):
     co = co.freeze()
     all_fns.add(co_name)
     if co.co_name != "<module>" or co.co_filename:
-        real_out.write("\n" + format_code_info(co, version, mapped_name) + "\n")
+        real_out.write("\n" + format_code_info(co, version_tuple, mapped_name) + "\n")
 
     bytecode = Bytecode(co, opc, dup_lines=True)
     real_out.write(bytecode.dis(asm_format="asm") + "\n")
@@ -279,7 +285,15 @@ def disassemble_file(
     try:
         # FIXME: add whether we want PyPy
         pyc_filename = check_object_path(filename)
-        version, timestamp, magic_int, co, is_pypy, source_size, sip_hash = load_module(pyc_filename)
+        (
+            version_tuple,
+            timestamp,
+            magic_int,
+            co,
+            is_pypy,
+            source_size,
+            sip_hash,
+        ) = load_module(pyc_filename)
     except:
 
         # Hack alert: we're using pyc_filename set as a proxy for whether the filename exists.
@@ -294,13 +308,13 @@ def disassemble_file(
         sip_hash = 0
         source_size = stat.st_size
         timestamp = stat.st_mtime
-        version = PYTHON_VERSION
+        version_tuple = PYTHON_VERSION_TRIPLE
     else:
         filename = pyc_filename
 
     if asm_format == "header":
         show_module_header(
-            version,
+            version_tuple,
             co,
             timestamp,
             outstream,
@@ -312,7 +326,7 @@ def disassemble_file(
         )
     else:
         disco(
-            bytecode_version=version,
+            version_tuple=version_tuple,
             co=co,
             timestamp=timestamp,
             out=outstream,
@@ -325,7 +339,16 @@ def disassemble_file(
             alternate_opmap=alternate_opmap,
         )
     # print co.co_filename
-    return filename, co, version, timestamp, magic_int, is_pypy, source_size, sip_hash
+    return (
+        filename,
+        co,
+        version_tuple,
+        timestamp,
+        magic_int,
+        is_pypy,
+        source_size,
+        sip_hash,
+    )
 
 
 def _test():
diff --git a/xdis/dropbox/decrypt25.py b/xdis/dropbox/decrypt25.py
index 0da20aab..65cbf02b 100644
--- a/xdis/dropbox/decrypt25.py
+++ b/xdis/dropbox/decrypt25.py
@@ -294,7 +294,7 @@ def fix_dropbox_pyc(fp, fixed_pyc="/tmp/test.pyc"):
     timestamp = struct.unpack("I", ts)[0]
     b = fp.read()
     co = loads(b)
-    return 2.5, timestamp, 62131, co, False, source_size, None
+    return (2, 5), timestamp, 62131, co, False, source_size, None
 
 
 def fix_dir(path):
diff --git a/xdis/instruction.py b/xdis/instruction.py
index b9a7d383..d881b72f 100644
--- a/xdis/instruction.py
+++ b/xdis/instruction.py
@@ -55,30 +55,30 @@
 class Instruction(_Instruction):
     """Details for a bytecode operation
 
-       Defined fields:
-         opname - human readable name for operation
-         opcode - numeric code for operation
-         optype - opcode classification. One of
-            compare, const, free, jabs, jrel, local, name, nargs
-         inst_size - number of bytes the instruction occupies
-         arg - numeric argument to operation (if any), otherwise None
-         argval - resolved arg value (if known), otherwise same as arg
-         argrepr - human readable description of operation argument
-         has_arg - True opcode takes an argument. In that case,
-                   argval and argepr will have that value. False
-                   if this opcode doesn't take an argument. In that case,
-                   don't look at argval or argrepr.
-         offset - start index of operation within bytecode sequence
-         starts_line - line started by this opcode (if any), otherwise None
-         is_jump_target - True if other code jumps to here,
-                          'loop' if this is a loop beginning, which
-                          in Python can be determined jump to an earlier offset.
-                          Otherwise False
-         has_extended_arg - True if the instruction was built from EXTENDED_ARG
-                            opcodes
-         fallthrough - True if the instruction can (not must) fall through to the next
-                       instruction. Note conditionals are in this category, but
-                       returns, raise, and unconditional jumps are not
+    Defined fields:
+      opname - human readable name for operation
+      opcode - numeric code for operation
+      optype - opcode classification. One of
+         compare, const, free, jabs, jrel, local, name, nargs
+      inst_size - number of bytes the instruction occupies
+      arg - numeric argument to operation (if any), otherwise None
+      argval - resolved arg value (if known), otherwise same as arg
+      argrepr - human readable description of operation argument
+      has_arg - True opcode takes an argument. In that case,
+                argval and argepr will have that value. False
+                if this opcode doesn't take an argument. In that case,
+                don't look at argval or argrepr.
+      offset - start index of operation within bytecode sequence
+      starts_line - line started by this opcode (if any), otherwise None
+      is_jump_target - True if other code jumps to here,
+                       'loop' if this is a loop beginning, which
+                       in Python can be determined jump to an earlier offset.
+                       Otherwise False
+      has_extended_arg - True if the instruction was built from EXTENDED_ARG
+                         opcodes
+      fallthrough - True if the instruction can (not must) fall through to the next
+                    instruction. Note conditionals are in this category, but
+                    returns, raise, and unconditional jumps are not
     """
 
     # FIXME: remove has_arg from initialization but keep it as a field.
@@ -184,7 +184,9 @@ def disassemble(
                     hasattr(opc, "opcode_extended_fmt")
                     and opc.opname[op] in opc.opcode_extended_fmt
                 ):
-                    new_repr = opc.opcode_extended_fmt[opc.opname[op]](opc, list(reversed(instructions)))
+                    new_repr = opc.opcode_extended_fmt[opc.opname[op]](
+                        opc, list(reversed(instructions))
+                    )
                     if new_repr:
                         argrepr = new_repr
                 pass
@@ -202,7 +204,9 @@ def disassemble(
                 hasattr(opc, "opcode_extended_fmt")
                 and opc.opname[op] in opc.opcode_extended_fmt
             ):
-                new_repr = opc.opcode_extended_fmt[opc.opname[op]](opc, list(reversed(instructions)))
+                new_repr = opc.opcode_extended_fmt[opc.opname[op]](
+                    opc, list(reversed(instructions))
+                )
                 if new_repr:
                     fields.append("(%s)" % new_repr)
             pass
diff --git a/xdis/lineoffsets.py b/xdis/lineoffsets.py
index 1db27eff..337afd49 100644
--- a/xdis/lineoffsets.py
+++ b/xdis/lineoffsets.py
@@ -86,13 +86,10 @@ def _populate_lines(self):
             pass
         self.code_map = code_map
 
-
     def __str__(self):
         return str(self.line_numbers())
 
-    def line_numbers(
-        self, include_dups=True, include_offsets=False
-    ):
+    def line_numbers(self, include_dups=True, include_offsets=False):
         """Return all of the valid lines for a given piece of code"""
         if include_offsets:
             lines = {}
@@ -135,7 +132,8 @@ def lineoffsets_in_module(module, toplevel_only=False):
 
     def multi_line():
         # We have two statements on the same line
-        x = 1; y = 2;
+        x = 1
+        y = 2
         return x, y
 
     def foo():
@@ -155,15 +153,20 @@ def print_code_info(code_info):
         else:
             print("%s has no children" % (code_info.name))
 
-        print("\tlines with children and dups:\n\t%s" %
-              code_info.line_numbers(include_dups=True))
         print(
-            "\tlines without children and without dups:\n\t%s" %
-            code_info.line_numbers(include_dups=False))
+            "\tlines with children and dups:\n\t%s"
+            % code_info.line_numbers(include_dups=True)
+        )
+        print(
+            "\tlines without children and without dups:\n\t%s"
+            % code_info.line_numbers(include_dups=False)
+        )
         print("Offsets in %s" % code_info.name, code_info.offsets)
         lines = code_info.line_numbers(include_offsets=True)
         for line_num, li in lines.items():
-            print("\tline: %4d: %s" % (line_num, ", ".join([str(i.offsets) for i in li])))
+            print(
+                "\tline: %4d: %s" % (line_num, ", ".join([str(i.offsets) for i in li]))
+            )
         print("=" * 30)
         for mod, code in code_info.code_map.items():
             print(mod, ":", code)
diff --git a/xdis/load.py b/xdis/load.py
index efac3969..2db30ebf 100644
--- a/xdis/load.py
+++ b/xdis/load.py
@@ -20,12 +20,13 @@
 
 import xdis.marsh
 import xdis.unmarshal
-from xdis.version_info import PYTHON3, PYTHON_VERSION
+from xdis.version_info import PYTHON3, PYTHON_VERSION_TRIPLE
 from xdis.magics import (
     IS_PYPY3,
     PYTHON_MAGIC_INT,
     int2magic,
     magic_int2float,
+    magic_int2tuple,
     magic2int,
     magicint2version,
     versions,
@@ -107,7 +108,7 @@ def load_file(filename, out=sys.stdout):
     try:
         source = fp.read()
         try:
-            if PYTHON_VERSION < 2.6:
+            if PYTHON_VERSION_TRIPLE < (2, 6):
                 co = compile(source, filename, "exec")
             else:
                 co = compile(source, filename, "exec", dont_inherit=True)
@@ -137,8 +138,8 @@ def load_module(filename, code_objects=None, fast_load=False, get_code=True):
                      `False`.
 
     Return values are as follows:
-        float_version: float; the floating-point version number for the given magic_int,
-                       e.g. 2.7 or 3.4
+        float_tuple    a tuple version number for the given magic_int,
+                       e.g. (2, 7) or (3, 4)
         timestamp: int; the seconds since EPOCH of the time of the bytecode creation, or None
                         if no timestamp was stored
         magic_int: int, a more specific than version number. The actual byte code version of the
@@ -197,6 +198,7 @@ def load_module_from_file_object(
         try:
             # FIXME: use the internal routine below
             float_version = magic_int2float(magic_int)
+            tuple_version = magic_int2tuple(magic_int)
         except KeyError:
             if magic_int in (2657, 22138):
                 raise ImportError("This smells like Pyston which is not supported.")
@@ -253,20 +255,19 @@ def load_module_from_file_object(
             )
 
         try:
-            # print version
             my_magic_int = PYTHON_MAGIC_INT
             magic_int = magic2int(magic)
-            version = magic_int2float(magic_int)
+            version = magic_int2tuple(magic_int)
 
             timestamp = None
             source_size = None
             sip_hash = None
 
             ts = fp.read(4)
-            if version >= 3.7:
+            if magic_int in (3439,) or version >= (3, 7):
                 # PEP 552. https://www.python.org/dev/peps/pep-0552/
                 pep_bits = ts[-1]
-                if PYTHON_VERSION <= 2.7:
+                if PYTHON_VERSION_TRIPLE <= (2, 7):
                     pep_bits = ord(pep_bits)
                 if (pep_bits & 1) or magic_int == 3393:  # 3393 is 3.7.0beta3
                     # SipHash
@@ -286,7 +287,7 @@ def load_module_from_file_object(
                 # PYTHON_VERSION, although PYTHON_VERSION would probably work.
                 if (
                     (3200 <= magic_int < 20121)
-                    and version >= 1.5
+                    and version >= (1, 5)
                     or magic_int in IS_PYPY3
                 ):
                     source_size = unpack("<I", fp.read(4))[0]  # size mod 2**32
@@ -315,7 +316,7 @@ def load_module_from_file_object(
         fp.close()
 
     return (
-        float_version,
+        tuple_version,
         timestamp,
         magic_int,
         co,
@@ -332,10 +333,10 @@ def write_bytecode_file(
     magic_int (i.e. bytecode associated with some version of Python)
     """
     fp = open(bytecode_path, "wb")
-    version = float(magicint2version[magic_int][:3])
-    if version >= 3.0:
+    version = magicint2tuple(magic_int)
+    if version >= (3, 0):
         fp.write(pack("<Hcc", magic_int, b"\r", b"\n"))
-        if version >= 3.7:  # pep552 bytes
+        if version >= (3, 7):  # pep552 bytes
             fp.write(pack("<I", 0))  # pep552 bytes
     else:
         fp.write(pack("<Hcc", magic_int, b"\r", b"\n"))
@@ -348,7 +349,7 @@ def write_bytecode_file(
     else:
         fp.write(pack("<I", int(datetime.now().timestamp())))
 
-    if version >= 3.3:
+    if version >= (3, 3):
         # In Python 3.3+, these 4 bytes are the size of the source code_obj file (mod 2^32)
         fp.write(pack("<I", filesize))
     if isinstance(code_obj, types.CodeType):
@@ -366,7 +367,10 @@ def write_bytecode_file(
     )
     print("version", version, "magic int", magic_int, "is_pypy", pypy)
     if timestamp is not None:
-        print(datetime.datetime.fromtimestamp(timestamp))
+        if PYTHON_VERSION_TRIPLE < (3, 10):
+            print(datetime.datetime.fromtimestamp(timestamp))
+        else:
+            print(datetime.fromtimestamp(timestamp))
     if source_size is not None:
         print("source size mod 2**32: %d" % source_size)
     if sip_hash is not None:
diff --git a/xdis/magics.py b/xdis/magics.py
index 4f901cd3..2c3459a9 100755
--- a/xdis/magics.py
+++ b/xdis/magics.py
@@ -304,11 +304,9 @@ def __by_version(magics):
 # add IS_OP, CONTAINS_OP and JUMP_IF_NOT_EXC_MATCH bytecodes #39156
 add_magic_from_int(3423, "3.9.0a0")
 
-# simplify bytecodes for *value unpacking
 add_magic_from_int(3424, "3.9.0a2")
-
-# simplify bytecodes for **value unpacking
 add_magic_from_int(3425, "3.9.0beta5")
+add_magic_from_int(3439, "3.10.0rc2")
 
 # Weird ones
 # WTF? Python 3.2.5 and PyPy have weird magic numbers
@@ -408,6 +406,8 @@ def add_canonic_versions(versions, canonic):
     "3.9 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.0b5+", "3.9.0beta5"
 )
 
+add_canonic_versions("3.10 3.10.0", "3.10.0rc2")
+
 # The canonic version for a canonic version is itself
 for v in versions.values():
     canonic_python_version[v] = v
@@ -419,7 +419,7 @@ def __show(text, magic):
     print(text, struct.unpack("BBBB", magic), struct.unpack("<HBB", magic))
 
 
-def magic_int2float(magic_int):
+def magic_int2float(magic_int: int):
     """Convert a Python magic int into a 'canonic' floating-point number,
     e.g. 2.7, 3.7. runtime error is raised if "version" is not found.
 
@@ -430,7 +430,18 @@ def magic_int2float(magic_int):
     return py_str2float(magicint2version[magic_int])
 
 
-def py_str2float(orig_version):
+def magic_int2tuple(magic_int: int) -> tuple:
+    """Convert a Python magic int into a 'canonic' tuple
+    e.g. (2, 7), (3, 7). runtime error is raised if "version" is not found.
+
+    Note that there can be several magic_int's that map to a single floating-
+    point number. For example 3320 (3.5.a0), 3340 (3.5b1)
+    all map to 3.5.
+    """
+    return py_str2tuple(magicint2version[magic_int])
+
+
+def py_str2float(orig_version: str) -> float:
     """Convert a Python version into a two-digit 'canonic' floating-point number,
     e.g. 2.5, 3.6.
 
@@ -469,6 +480,35 @@ def py_str2float(orig_version):
     return
 
 
+def py_str2tuple(orig_version):
+    """Convert a Python version into a tuple number,
+    e.g. (2, 5), (3, 6).
+
+    A runtime error is raised if "version" is not found.
+
+    Note that there can be several strings that map to a single
+    tuple. For example 3.2a1, 3.2.0, 3.2.2, 3.2.6 among others all map
+    to (3, 2).
+    """
+    version = re.sub(r"(pypy|dropbox)$", "", orig_version)
+    if version in magics:
+        magic = magics[version]
+        m = re.match(r"^(\d)\.(\d+)\.(\d+)", version)
+        if m:
+            return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
+        else:
+            # Match things like 3.5a0, 3.5b2, 3.6a1+1, 3.6rc1, 3.7.0beta3
+            m = re.match(r"^(\d)\.(\d)(\d+)?[abr]?", version)
+            if m:
+                return (int(m.group(1)), int(m.group(2)))
+            pass
+        pass
+    raise RuntimeError(
+        "Can't find a valid Python version for version %s" % orig_version
+    )
+    return
+
+
 def sysinfo2float(version_info=sys.version_info):
     """Convert a sys.versions_info-compatible list into a 'canonic'
     floating-point number which that can then be used to look up a
diff --git a/xdis/op_imports.py b/xdis/op_imports.py
index ca7f44a2..bbed1ad4 100644
--- a/xdis/op_imports.py
+++ b/xdis/op_imports.py
@@ -44,6 +44,7 @@
 from xdis.opcodes import opcode_37 as opcode_37
 from xdis.opcodes import opcode_38 as opcode_38
 from xdis.opcodes import opcode_39 as opcode_39
+from xdis.opcodes import opcode_310 as opcode_310
 
 from xdis.opcodes import opcode_26pypy as opcode_26pypy
 from xdis.opcodes import opcode_27pypy as opcode_27pypy
@@ -126,6 +127,9 @@
     "3.9.0beta5": opcode_39,
     "3.9": opcode_39,
     3.9: opcode_39,
+    "3.10.0rc2": opcode_310,
+    "3.10": opcode_310,
+    3.10: opcode_310,
     "2.6pypy": opcode_26pypy,
     "2.7pypy": opcode_27pypy,
     "3.2pypy": opcode_32pypy,
diff --git a/xdis/opcodes/base.py b/xdis/opcodes/base.py
index 203c39e2..27c00962 100644
--- a/xdis/opcodes/base.py
+++ b/xdis/opcodes/base.py
@@ -24,10 +24,9 @@
     findlinestarts,
     findlabels,
     get_jump_target_maps,
-    get_jump_targets,
 )
 from xdis import wordcode
-from xdis import IS_PYPY, PYTHON_VERSION
+from xdis.version_info import IS_PYPY, PYTHON_VERSION, PYTHON_VERSION_TRIPLE
 
 cmp_op = (
     "<",
@@ -56,26 +55,26 @@
 """.split()
 
 
-def init_opdata(l, from_mod, version=None, is_pypy=False):
+def init_opdata(l, from_mod, version_tuple=None, is_pypy=False):
     """Sets up a number of the structures found in Python's
     opcode.py. Python opcode.py routines assign attributes to modules.
     In order to do this in a modular way here, the local dictionary
     for the module is passed.
     """
 
-    if version:
-        l["python_version"] = version
+    if version_tuple:
+        l["python_version"] = version_tuple
     l["is_pypy"] = is_pypy
     l["cmp_op"] = cmp_op
     l["HAVE_ARGUMENT"] = HAVE_ARGUMENT
     l["findlinestarts"] = findlinestarts
-    if version <= 3.5:
+    if version_tuple <= (3, 5):
         l["findlabels"] = findlabels
-        l["get_jump_targets"] = get_jump_targets
+        l["get_jump_targets"] = findlabels
         l["get_jump_target_maps"] = get_jump_target_maps
     else:
         l["findlabels"] = wordcode.findlabels
-        l["get_jump_targets"] = wordcode.get_jump_targets
+        l["get_jump_targets"] = wordcode.findlabels
         l["get_jump_target_maps"] = wordcode.get_jump_target_maps
 
     l["opmap"] = deepcopy(from_mod.opmap)
@@ -262,7 +261,7 @@ def update_sets(l):
     l["JUMP_UNCONDITONAL"] = frozenset(
         [l["opmap"]["JUMP_ABSOLUTE"], l["opmap"]["JUMP_FORWARD"]]
     )
-    if l["python_version"] < 3.8:
+    if PYTHON_VERSION_TRIPLE < (3,8,0) and l["python_version"] < (3, 8):
         l["LOOP_OPS"] = frozenset([l["opmap"]["SETUP_LOOP"]])
     else:
         l["LOOP_OPS"] = frozenset()
@@ -447,8 +446,7 @@ def opcode_check(l):
     importing, we perform checks to make sure our opcode
     set matches exactly.
     """
-    # Python 2.6 reports 2.6000000000000001
-    if abs(PYTHON_VERSION - l["python_version"]) <= 0.01 and IS_PYPY == l["is_pypy"]:
+    if PYTHON_VERSION_TRIPLE[:2] == l["python_version"][:2] and IS_PYPY == l["is_pypy"]:
         try:
             import dis
 
diff --git a/xdis/opcodes/opcode_10.py b/xdis/opcodes/opcode_10.py
index fe6b83b8..367526b5 100644
--- a/xdis/opcodes/opcode_10.py
+++ b/xdis/opcodes/opcode_10.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2019-2020 by Rocky Bernstein
+# (C) Copyright 2019-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -38,10 +38,11 @@
 )
 
 version = 1.0
+version_tuple = (1, 0)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_11, version)
+init_opdata(l, opcode_11, version_tuple)
 
 # fmt: off
 # 1.0 - 1.1 bytecodes differences
diff --git a/xdis/opcodes/opcode_11.py b/xdis/opcodes/opcode_11.py
index 25f6203c..454f20b5 100644
--- a/xdis/opcodes/opcode_11.py
+++ b/xdis/opcodes/opcode_11.py
@@ -37,10 +37,11 @@
 )
 
 version = 1.1  # 1.2 is the same
+version_tuple = (1, 2)  # 1.2 is the same
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_13, version)
+init_opdata(l, opcode_13, version_tuple)
 
 update_pj2(globals(), l)
 
diff --git a/xdis/opcodes/opcode_13.py b/xdis/opcodes/opcode_13.py
index de7e347e..793b4308 100644
--- a/xdis/opcodes/opcode_13.py
+++ b/xdis/opcodes/opcode_13.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2018-2020 by Rocky Bernstein
+# (C) Copyright 2018-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -38,10 +38,11 @@
 )
 
 version = 1.3
+version_tuple = (1, 3)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_14, version)
+init_opdata(l, opcode_14, version_tuple)
 
 # 1.3 - 1.4 bytecodes differences
 rm_op(l, "BINARY_POWER", 19)
diff --git a/xdis/opcodes/opcode_14.py b/xdis/opcodes/opcode_14.py
index b75aca98..185bbde1 100644
--- a/xdis/opcodes/opcode_14.py
+++ b/xdis/opcodes/opcode_14.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2018-2020 by Rocky Bernstein
+# (C) Copyright 2018-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -37,21 +37,24 @@
 )
 
 version = 1.4
+version_tuple = (1, 4)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_15, version)
+init_opdata(l, opcode_15, version_tuple)
 
+# fmt: off
 # 1.4 Bytecodes not in 1.5
-def_op(l, "UNARY_CALL", 14)
-def_op(l, "BINARY_CALL", 26)
-def_op(l, "RAISE_EXCEPTION", 81)
-def_op(l, "BUILD_FUNCTION", 86)
-varargs_op(l, "UNPACK_ARG", 94)  # Number of arguments expected
-varargs_op(l, "UNPACK_VARARG", 99)  # Minimal number of arguments
-name_op(l, "LOAD_LOCAL", 115)
+def_op(l, "UNARY_CALL",         14)
+def_op(l, "BINARY_CALL",        26)
+def_op(l, "RAISE_EXCEPTION",    81)
+def_op(l, "BUILD_FUNCTION",     86)
+varargs_op(l, "UNPACK_ARG",     94)  # Number of arguments expected
+varargs_op(l, "UNPACK_VARARG",  99)  # Minimal number of arguments
+name_op(l, "LOAD_LOCAL",       115)
 varargs_op(l, "SET_FUNC_ARGS", 117)  # Argcount
-varargs_op(l, "RESERVE_FAST", 123)  # Number of local variables
+varargs_op(l, "RESERVE_FAST",  123)  # Number of local variables
+# fmt: on
 
 update_pj2(globals(), l)
 
diff --git a/xdis/opcodes/opcode_15.py b/xdis/opcodes/opcode_15.py
index 1f14a696..74752614 100644
--- a/xdis/opcodes/opcode_15.py
+++ b/xdis/opcodes/opcode_15.py
@@ -46,6 +46,7 @@
 )
 
 version = 1.5
+version_tuple = (1, 5)
 python_implementation = "CPython"
 
 cmp_op = (
@@ -68,7 +69,7 @@
 HAVE_ARGUMENT = 90
 
 l = locals()
-l["python_version"] = version
+l["python_version"] = version_tuple
 l["cmp_op"] = cmp_op
 l["HAVE_ARGUMENT"] = HAVE_ARGUMENT
 
diff --git a/xdis/opcodes/opcode_16.py b/xdis/opcodes/opcode_16.py
index 37c7a5d3..b965116b 100644
--- a/xdis/opcodes/opcode_16.py
+++ b/xdis/opcodes/opcode_16.py
@@ -38,10 +38,11 @@
 )
 
 version = 1.6
+version_tuple = (1, 6)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_15, version)
+init_opdata(l, opcode_15, version_tuple)
 
 # fmt: off
 # 1.6 Bytecodes not in 1.5
diff --git a/xdis/opcodes/opcode_20.py b/xdis/opcodes/opcode_20.py
index bc6fec51..1fccdb7a 100644
--- a/xdis/opcodes/opcode_20.py
+++ b/xdis/opcodes/opcode_20.py
@@ -34,10 +34,11 @@
 )
 
 version = 2.0
+version_tuple = (2, 0)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_21, version)
+init_opdata(l, opcode_21, version_tuple)
 
 # fmt: off
 # 2.1 Bytecodes not in 2.0
diff --git a/xdis/opcodes/opcode_21.py b/xdis/opcodes/opcode_21.py
index 46e99372..07b8222c 100644
--- a/xdis/opcodes/opcode_21.py
+++ b/xdis/opcodes/opcode_21.py
@@ -32,10 +32,11 @@
 )
 
 version = 2.1
+version_tuple = (2, 1)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_22, version)
+init_opdata(l, opcode_22, version_tuple)
 
 # 2.1 bytecodes changes from 2.2
 rm_op(l, "BINARY_FLOOR_DIVIDE", 26)
diff --git a/xdis/opcodes/opcode_22.py b/xdis/opcodes/opcode_22.py
index 44df7221..e84b3ca8 100644
--- a/xdis/opcodes/opcode_22.py
+++ b/xdis/opcodes/opcode_22.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2019 by Rocky Bernstein
+# (C) Copyright 2017, 2019, 2021 by Rocky Bernstein
 """
 CPython 2.2 bytecode opcodes
 
@@ -18,10 +18,11 @@
 )
 
 version = 2.2
+version_tuple = (2, 2)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_2x, version)
+init_opdata(l, opcode_2x, version_tuple)
 
 # 2.2 Bytecodes not in 2.3
 def_op(l, "FOR_LOOP", 114)
diff --git a/xdis/opcodes/opcode_23.py b/xdis/opcodes/opcode_23.py
index 7157cbbd..f5e42599 100644
--- a/xdis/opcodes/opcode_23.py
+++ b/xdis/opcodes/opcode_23.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2019-2020 by Rocky Bernstein
+# (C) Copyright 2017, 2019-2021 by Rocky Bernstein
 """
 CPython 2.3 bytecode opcodes
 
@@ -23,10 +23,11 @@
 )
 
 version = 2.3
+version_tuple = (2, 3)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_2x, version)
+init_opdata(l, opcode_2x, version_tuple)
 
 update_pj2(globals(), l)
 
diff --git a/xdis/opcodes/opcode_24.py b/xdis/opcodes/opcode_24.py
index cb3d2f7e..5749e7c1 100644
--- a/xdis/opcodes/opcode_24.py
+++ b/xdis/opcodes/opcode_24.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2020 by Rocky Bernstein
+# (C) Copyright 2017, 2020-2021 by Rocky Bernstein
 """
 CPython 2.4 bytecode opcodes
 
@@ -20,22 +20,26 @@
     format_MAKE_FUNCTION_default_argc,
     format_RAISE_VARARGS_older,
     format_extended_arg,
-    update_pj2
+    update_pj2,
 )
 
 version = 2.4
+version_tuple = (2, 4)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_2x, version)
+init_opdata(l, opcode_2x, version_tuple)
 
+# fmt: off
 # Bytecodes added since 2.3
 #          OP NAME            OPCODE POP PUSH
-#--------------------------------------------
-def_op(l, 'NOP',                   9,  0,  0)
-def_op(l, 'LIST_APPEND',          18,  2,  0)  # Calls list.append(TOS[-i], TOS).
-                                               # Used to implement list comprehensions.
+ # Used to implement list comprehensions.
 def_op(l, 'YIELD_VALUE',          86,  1,  1)
+# --------------------------------------------
+def_op(l, "NOP", 9, 0, 0)
+def_op(l, "LIST_APPEND", 18, 2, 0)  # Calls list.append(TOS[-i], TOS).
+# Used to implement list comprehensions.
+def_op(l, "YIELD_VALUE", 86, 1, 1)
 
 # FIXME remove (fix uncompyle6)
 update_pj2(globals(), l)
@@ -59,3 +63,4 @@
     "RETURN_VALUE": extended_format_RETURN_VALUE,
     "STORE_ATTR": extended_format_ATTR,
 }
+# fmt: on
diff --git a/xdis/opcodes/opcode_25.py b/xdis/opcodes/opcode_25.py
index c03700cb..eb836bec 100644
--- a/xdis/opcodes/opcode_25.py
+++ b/xdis/opcodes/opcode_25.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2021 by Rocky Bernstein
+# (C) Copyright 2017, 2020-2021 by Rocky Bernstein
 """
 CPython 2.5 bytecode opcodes
 
@@ -24,10 +24,11 @@
 )
 
 version = 2.5
+version_tuple = (2, 5)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_24, version)
+init_opdata(l, opcode_24, version_tuple)
 
 # fmt: off
 # Bytecodes added in 2.5 from 2.4
diff --git a/xdis/opcodes/opcode_26.py b/xdis/opcodes/opcode_26.py
index ad9f7dc8..c7e0ae7c 100644
--- a/xdis/opcodes/opcode_26.py
+++ b/xdis/opcodes/opcode_26.py
@@ -42,9 +42,10 @@
 python_implementation = "CPython"
 
 version = 2.6
+version_tuple = (2, 6)
 
 l = locals()
-init_opdata(l, opcode_25, version)
+init_opdata(l, opcode_25, version_tuple)
 
 # Below are opcode changes since Python 2.5
 
diff --git a/xdis/opcodes/opcode_26pypy.py b/xdis/opcodes/opcode_26pypy.py
index 272b128f..06dde045 100644
--- a/xdis/opcodes/opcode_26pypy.py
+++ b/xdis/opcodes/opcode_26pypy.py
@@ -34,10 +34,11 @@
 )
 
 version = 2.6
+version_tuple = (2, 6)
 python_implementation = "PyPy"
 
 l = locals()
-init_opdata(l, opcode_26, version, is_pypy=True)
+init_opdata(l, opcode_26, version_tuple, is_pypy=True)
 
 # FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_27.py b/xdis/opcodes/opcode_27.py
index fa49cd47..5256f608 100644
--- a/xdis/opcodes/opcode_27.py
+++ b/xdis/opcodes/opcode_27.py
@@ -45,10 +45,11 @@
 import xdis.opcodes.opcode_26 as opcode_26
 
 version = 2.7
+version_tuple = (2, 7)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_26, version)
+init_opdata(l, opcode_26, version_tuple)
 
 # fmt: off
 # Below are opcode changes since Python 2.6
diff --git a/xdis/opcodes/opcode_27pypy.py b/xdis/opcodes/opcode_27pypy.py
index ccb61ef2..fe6c68f7 100644
--- a/xdis/opcodes/opcode_27pypy.py
+++ b/xdis/opcodes/opcode_27pypy.py
@@ -27,11 +27,12 @@
 )
 
 version = 2.7
+version_tuple = (2, 7)
 python_implementation = "PyPy"
 
 l = locals()
 
-init_opdata(l, opcode_27, version, is_pypy=True)
+init_opdata(l, opcode_27, version_tuple, is_pypy=True)
 
 # FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_30.py b/xdis/opcodes/opcode_30.py
index a7457a58..edda9616 100644
--- a/xdis/opcodes/opcode_30.py
+++ b/xdis/opcodes/opcode_30.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2019-2020 by Rocky Bernstein
+# (C) Copyright 2017, 2019-2021 by Rocky Bernstein
 """
 CPython 3.0 bytecode opcodes
 
@@ -22,14 +22,16 @@
 import xdis.opcodes.opcode_31 as opcode_31
 
 version = 3.0
+version_tuple = (3, 0)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_31, version)
+init_opdata(l, opcode_31, version_tuple)
 
 # These are in Python 3.x but not in Python 3.0
 
+# fmt: off
 rm_op(l, 'JUMP_IF_FALSE_OR_POP', 111)
 rm_op(l, 'JUMP_IF_TRUE_OR_POP',  112)
 rm_op(l, 'POP_JUMP_IF_FALSE',    114)
@@ -45,12 +47,13 @@
 #          OP NAME            OPCODE POP PUSH
 #--------------------------------------------
 
-def_op(l, 'SET_ADD',              17, 2, 0)  # Calls set.add(TOS1[-i], TOS).
+def_op(l, 'SET_ADD',              17,  2, 0)  # Calls set.add(TOS1[-i], TOS).
                                              # Used to implement set comprehensions.
-def_op(l, 'LIST_APPEND',          18, 2, 0)  # Calls list.append(TOS1, TOS).
+def_op(l, 'LIST_APPEND',          18,  2, 0)  # Calls list.append(TOS1, TOS).
                                              # Used to implement list comprehensions.
-jrel_op(l, 'JUMP_IF_FALSE',      111, 1, 1)
-jrel_op(l, 'JUMP_IF_TRUE',       112, 1, 1)
+jrel_op(l, 'JUMP_IF_FALSE',      111,  1, 1)
+jrel_op(l, 'JUMP_IF_TRUE',       112,  1, 1)
+# fmt: on
 
 # Yes, pj2 not pj3 - Python 3.0 is more like 2.7 here with its
 # JUMP_IF rather than POP_JUMP_IF.
diff --git a/xdis/opcodes/opcode_31.py b/xdis/opcodes/opcode_31.py
index febc1130..7e1813c4 100644
--- a/xdis/opcodes/opcode_31.py
+++ b/xdis/opcodes/opcode_31.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2020 by Rocky Bernstein
+# (C) Copyright 2017, 2020-2021 by Rocky Bernstein
 """
 CPython 3.1 bytecode opcodes
 
@@ -24,14 +24,16 @@
 l = locals()
 
 version = 3.1
+version_tuple = (3, 1)
 python_implementation = "CPython"
 
-init_opdata(l, opcode_32, version)
+init_opdata(l, opcode_32, version_tuple)
 
+# fmt: off
 # These are in Python 3.2 but not in Python 3.1
-rm_op(l, "DUP_TOP_TWO", 5)
-rm_op(l, "DELETE_DEREF", 138)
-rm_op(l, "SETUP_WITH", 143)
+rm_op(l, "DUP_TOP_TWO",     5)
+rm_op(l, "DELETE_DEREF",  138)
+rm_op(l, "SETUP_WITH",    143)
 
 # These are in Python 3.1 but not Python 3.2
 name_op(l, "IMPORT_NAME", 108,  1, 1)  # Imports TOS and TOS1; module pushed
@@ -40,6 +42,7 @@
 
 # This op is in 3.2 but its opcode is a 144 instead
 def_op(l, "EXTENDED_ARG", 143)
+# fmt: on
 
 update_pj3(globals(), l)
 
diff --git a/xdis/opcodes/opcode_310.py b/xdis/opcodes/opcode_310.py
new file mode 100644
index 00000000..2c18f9f6
--- /dev/null
+++ b/xdis/opcodes/opcode_310.py
@@ -0,0 +1,108 @@
+# (C) Copyright 2021 by Rocky Bernstein
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+"""
+CPython 3.10 bytecode opcodes
+
+This is a like Python 3.10's opcode.py
+"""
+
+from xdis.opcodes.base import (
+    def_op,
+    extended_format_ATTR,
+    extended_format_RETURN_VALUE,
+    finalize_opcodes,
+    init_opdata,
+    jabs_op,
+    rm_op,
+    update_pj3,
+)
+
+from xdis.opcodes.opcode_36 import (
+    extended_format_CALL_FUNCTION,
+    extended_format_CALL_METHOD,
+    extended_format_MAKE_FUNCTION,
+    format_BUILD_MAP_UNPACK_WITH_CALL,
+    format_CALL_FUNCTION_EX,
+    format_CALL_FUNCTION_KW,
+    format_extended_arg36,
+)
+
+from xdis.opcodes.opcode_36 import format_MAKE_FUNCTION_flags
+from xdis.opcodes.opcode_37 import extended_format_RAISE_VARARGS, format_RAISE_VARARGS
+import xdis.opcodes.opcode_39 as opcode_39
+
+version = 3.10
+version_tuple = (3, 10)
+python_implementation = "CPython"
+
+l = locals()
+
+init_opdata(l, opcode_39, version_tuple)
+
+# fmt: off
+format_value_flags = opcode_39.format_value_flags
+# These are removed since 3.9...
+rm_op(l,  "RERAISE",                  48)
+
+# These are added since 3.9...
+#         OP NAME                 OPCODE  POP PUSH
+#------------------------------------------------
+def_op(l, "GET_LEN",                  30,   0, 1)
+def_op(l, "MATCH_MAPPING",            31,   0, 1)
+def_op(l, "MATCH_SEQUENCE",           32,   0, 1)
+def_op(l, "MATCH_KEYS",               33,   0, 2)
+def_op(l, "COPY_DICT_WITHOUT_KEYS",   34,   2, 2)
+def_op(l, "ROT_N",                    99,   0, 0)
+def_op(l, "RERAISE",                 119,   3, 0)
+def_op(l, "GEN_START",               129,   1, 0)
+def_op(l, "MATCH_CLASS",             152,   2, 1)
+# fmt: on
+
+
+def format_extended_is_op(arg):
+    return "is" if arg == 0 else "is not"
+
+
+def format_extended_contains_op(arg):
+    return "in" if arg == 0 else "not in"
+
+
+opcode_arg_fmt = {
+    "BUILD_MAP_UNPACK_WITH_CALL": format_BUILD_MAP_UNPACK_WITH_CALL,
+    "CALL_FUNCTION_EX": format_CALL_FUNCTION_EX,
+    "CALL_FUNCTION_KW": format_CALL_FUNCTION_KW,
+    "CONTAINS_OP": format_extended_contains_op,
+    "EXTENDED_ARG": format_extended_arg36,
+    "FORMAT_VALUE": format_value_flags,
+    "IS_OP": format_extended_is_op,
+    "MAKE_FUNCTION": format_MAKE_FUNCTION_flags,
+    "RAISE_VARARGS": format_RAISE_VARARGS,
+}
+
+opcode_extended_fmt = {
+    "CALL_FUNCTION": extended_format_CALL_FUNCTION,
+    "CALL_METHOD": extended_format_CALL_METHOD,
+    "LOAD_ATTR": extended_format_ATTR,
+    "MAKE_FUNCTION": extended_format_MAKE_FUNCTION,
+    "RAISE_VARARGS": extended_format_RAISE_VARARGS,
+    "RETURN_VALUE": extended_format_RETURN_VALUE,
+    "STORE_ATTR": extended_format_ATTR,
+}
+# fmt: on
+
+update_pj3(globals(), l)
+
+finalize_opcodes(l)
diff --git a/xdis/opcodes/opcode_32.py b/xdis/opcodes/opcode_32.py
index 49216efc..00fc16fb 100644
--- a/xdis/opcodes/opcode_32.py
+++ b/xdis/opcodes/opcode_32.py
@@ -25,11 +25,12 @@
 # FIXME: can we DRY this even more?
 
 version = 3.2
+version_tuple = (3, 2)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_3x, version)
+init_opdata(l, opcode_3x, version_tuple)
 
 # There are no opcodes to add or change.
 # If there were, they'd be listed below.
diff --git a/xdis/opcodes/opcode_32pypy.py b/xdis/opcodes/opcode_32pypy.py
index e9a2fd75..31e13c6e 100644
--- a/xdis/opcodes/opcode_32pypy.py
+++ b/xdis/opcodes/opcode_32pypy.py
@@ -19,12 +19,13 @@
 )
 
 version = 3.2
+version_tuple = (3, 2)
 python_implementation = "PyPy"
 
 import xdis.opcodes.opcode_32 as opcode_32
 
 l = locals()
-init_opdata(l, opcode_32, version, is_pypy=True)
+init_opdata(l, opcode_32, version_tuple, is_pypy=True)
 
 ## FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_33.py b/xdis/opcodes/opcode_33.py
index 9e0d18b4..c9249cb9 100644
--- a/xdis/opcodes/opcode_33.py
+++ b/xdis/opcodes/opcode_33.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2020 by Rocky Bernstein
+# (C) Copyright 2017, 2020-2021 by Rocky Bernstein
 """
 CPython 3.3 bytecode opcodes
 
@@ -23,15 +23,19 @@
 import xdis.opcodes.opcode_3x as opcode_3x
 
 version = 3.3
+version_tuple = (3, 3)
 python_implementation = "CPython"
 
 l = locals()
-init_opdata(l, opcode_3x, version)
+init_opdata(l, opcode_3x, version_tuple)
 
 # Below are opcode changes since Python 3.2
 
-rm_op(l, "STOP_CODE", 0)
+# fmt: off
+rm_op(l,  "STOP_CODE",   0)
 def_op(l, "YIELD_FROM", 72, 1, 0)
+# fmt: on
+
 
 update_pj3(globals(), l)
 
diff --git a/xdis/opcodes/opcode_33pypy.py b/xdis/opcodes/opcode_33pypy.py
index f7ee3a9a..816833ee 100644
--- a/xdis/opcodes/opcode_33pypy.py
+++ b/xdis/opcodes/opcode_33pypy.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2019-2020 by Rocky Bernstein
+# (C) Copyright 2019-2021 by Rocky Bernstein
 """
 PYPY 3.3 opcodes
 
@@ -19,12 +19,13 @@
 )
 
 version = 3.3
+version_tuple = (3, 3)
 python_implementation = "PyPy"
 
 import xdis.opcodes.opcode_33 as opcode_33
 
 l = locals()
-init_opdata(l, opcode_33, version, is_pypy=True)
+init_opdata(l, opcode_33, version_tuple, is_pypy=True)
 
 ## FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_34.py b/xdis/opcodes/opcode_34.py
index cd5a6388..a88015b7 100644
--- a/xdis/opcodes/opcode_34.py
+++ b/xdis/opcodes/opcode_34.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2017, 2020 by Rocky Bernstein
+# (C) Copyright 2017, 2020-2021 by Rocky Bernstein
 """
 CPython 3.4 bytecode opcodes
 
@@ -23,17 +23,20 @@
 import xdis.opcodes.opcode_33 as opcode_33
 
 version = 3.4
+version_tuple = (3, 4)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_33, version)
+init_opdata(l, opcode_33, version_tuple)
 
+# fmt: off
 # These are removed since Python 3.3
-rm_op(l, "STORE_LOCALS", 69)
+rm_op(l, "STORE_LOCALS",       69)
 
 # These are new since Python 3.3
 free_op(l, "LOAD_CLASSDEREF", 148)
+# fmt: on
 
 update_pj3(globals(), l)
 
diff --git a/xdis/opcodes/opcode_35.py b/xdis/opcodes/opcode_35.py
index 8d47382f..021e8fdc 100644
--- a/xdis/opcodes/opcode_35.py
+++ b/xdis/opcodes/opcode_35.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2016-2017, 2020 by Rocky Bernstein
+# (C) Copyright 2016-2017, 2020-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -44,12 +44,14 @@
 )
 
 version = 3.5
+version_tuple = (3, 5)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_34, version)
+init_opdata(l, opcode_34, version_tuple)
 
+# fmt: off
 # These are removed since Python 3.5.
 # Removals happen before adds since
 # some opcodes are reused
@@ -79,6 +81,7 @@
 varargs_op(l, "BUILD_SET_UNPACK",           153, -1,  1)
 
 jrel_op(l, "SETUP_ASYNC_WITH",          154,  0,  6)
+# fmt: on
 
 update_pj3(globals(), l)
 
diff --git a/xdis/opcodes/opcode_35pypy.py b/xdis/opcodes/opcode_35pypy.py
index b1fc24b5..ce5616ad 100644
--- a/xdis/opcodes/opcode_35pypy.py
+++ b/xdis/opcodes/opcode_35pypy.py
@@ -20,12 +20,13 @@
 )
 
 version = 3.5
+version_tuple = (3, 5)
 python_implementation = "PyPy"
 
 import xdis.opcodes.opcode_35 as opcode_35
 
 l = locals()
-init_opdata(l, opcode_35, version, is_pypy=True)
+init_opdata(l, opcode_35, version_tuple, is_pypy=True)
 
 ## FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_36.py b/xdis/opcodes/opcode_36.py
index e0c752c5..cb30e979 100644
--- a/xdis/opcodes/opcode_36.py
+++ b/xdis/opcodes/opcode_36.py
@@ -49,11 +49,12 @@
 EXTENDED_ARG_SHIFT = 8
 
 version = 3.6
+version_tuple = (3, 6)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_35, version)
+init_opdata(l, opcode_35, version_tuple)
 
 # fmt: off
 # These are removed since Python 3.6
@@ -103,6 +104,7 @@
 # qualified name of the function.
 
 
+# fmt: off
 # These are new since Python 3.6
 #          OP NAME                OPCODE POP PUSH
 #---------------------------------------------------
diff --git a/xdis/opcodes/opcode_36pypy.py b/xdis/opcodes/opcode_36pypy.py
index 3cec10a7..879f5bec 100644
--- a/xdis/opcodes/opcode_36pypy.py
+++ b/xdis/opcodes/opcode_36pypy.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2019-2020 by Rocky Bernstein
+# (C) Copyright 2019-2021 by Rocky Bernstein
 """
 PYPY 3.6 opcodes
 
@@ -25,6 +25,7 @@
 )
 
 version = 3.6
+version_tuple = (3, 6)
 python_implementation = "PyPy"
 
 from xdis.opcodes.opcode_33 import extended_format_MAKE_FUNCTION
@@ -34,7 +35,7 @@
 )
 
 l = locals()
-init_opdata(l, opcode_36, version, is_pypy=True)
+init_opdata(l, opcode_36, version_tuple, is_pypy=True)
 
 ## FIXME: DRY common PYPY opcode additions
 
diff --git a/xdis/opcodes/opcode_37.py b/xdis/opcodes/opcode_37.py
index a97fbc84..bea4189d 100644
--- a/xdis/opcodes/opcode_37.py
+++ b/xdis/opcodes/opcode_37.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2016-2017, 2019-2020 by Rocky Bernstein
+# (C) Copyright 2016-2017, 2019-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -37,11 +37,12 @@
 import xdis.opcodes.opcode_36 as opcode_36
 
 version = 3.7
+version_tuple = (3, 7)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_36, version)
+init_opdata(l, opcode_36, version_tuple)
 
 ### Opcodes that have changed drastically ####
 
diff --git a/xdis/opcodes/opcode_37pypy.py b/xdis/opcodes/opcode_37pypy.py
index 28de77b2..6c4d6644 100644
--- a/xdis/opcodes/opcode_37pypy.py
+++ b/xdis/opcodes/opcode_37pypy.py
@@ -25,6 +25,7 @@
 )
 
 version = 3.7
+version_tuple = (3, 7)
 python_implementation = "PyPy"
 
 from xdis.opcodes.opcode_33 import extended_format_MAKE_FUNCTION
@@ -34,7 +35,7 @@
 )
 
 l = locals()
-init_opdata(l, opcode_37, version, is_pypy=True)
+init_opdata(l, opcode_37, version_tuple, is_pypy=True)
 
 
 # FIXME: DRY common PYPY opcode additions
diff --git a/xdis/opcodes/opcode_38.py b/xdis/opcodes/opcode_38.py
index c9adec54..77a4c589 100644
--- a/xdis/opcodes/opcode_38.py
+++ b/xdis/opcodes/opcode_38.py
@@ -43,11 +43,12 @@
 )
 
 version = 3.8
+version_tuple = (3, 8)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_37, version)
+init_opdata(l, opcode_37, version_tuple)
 
 # fmt: off
 # These are removed since 3.7...
diff --git a/xdis/opcodes/opcode_39.py b/xdis/opcodes/opcode_39.py
index 85b50e5d..622e9cf2 100644
--- a/xdis/opcodes/opcode_39.py
+++ b/xdis/opcodes/opcode_39.py
@@ -45,11 +45,12 @@
 import xdis.opcodes.opcode_38 as opcode_38
 
 version = 3.9
+version_tuple = (3, 9)
 python_implementation = "CPython"
 
 l = locals()
 
-init_opdata(l, opcode_38, version)
+init_opdata(l, opcode_38, version_tuple)
 
 # fmt: off
 # These are removed since 3.8...
diff --git a/xdis/std.py b/xdis/std.py
index 04f8dcff..0e304be0 100644
--- a/xdis/std.py
+++ b/xdis/std.py
@@ -47,21 +47,26 @@
 
 # std
 import sys
+
 # xdis
 from xdis import IS_PYPY
 from xdis.bytecode import Bytecode as _Bytecode
 from xdis.instruction import _Instruction
 from xdis.disasm import disco as _disco
 from xdis.op_imports import get_opcode_module
-from xdis.cross_dis import code_info as _code_info, pretty_flags as _pretty_flags, show_code as _show_code, xstack_effect as _stack_effect
+from xdis.cross_dis import (
+    code_info as _code_info,
+    pretty_flags as _pretty_flags,
+    show_code as _show_code,
+    xstack_effect as _stack_effect,
+)
 
 
-PYPY = 'pypy'
+PYPY = "pypy"
 VARIANT = PYPY if IS_PYPY else None
 
 
 class _StdApi:
-
     def __init__(self, python_version=sys.version_info, variant=VARIANT):
 
         if python_version >= (3, 6):
@@ -72,6 +77,7 @@ def __init__(self, python_version=sys.version_info, variant=VARIANT):
 
         self.opc = opc = get_opcode_module(python_version, variant)
         self.python_version = opc.version
+        self.python_version_tuple = opc.version_tuple
         self.is_pypy = variant == PYPY
         self.hasconst = opc.hasconst
         self.hasname = opc.hasname
@@ -88,50 +94,53 @@ class Bytecode(_Bytecode):
 
             Iterating over this yields the bytecode operations as Instruction instances.
             """
+
             def __init__(self, x, first_line=None, current_offset=None):
-                super(Bytecode, self).__init__(x, opc=opc, first_line=first_line,
-                                               current_offset=current_offset)
+                super(Bytecode, self).__init__(
+                    x, opc=opc, first_line=first_line, current_offset=current_offset
+                )
+
         self.Bytecode = Bytecode
 
         class Instruction(_Instruction):
             """Details for a bytecode operation
 
-               Defined fields:
-                 opname - human readable name for operation
-                 opcode - numeric code for operation
-                 arg - numeric argument to operation (if any), otherwise None
-                 argval - resolved arg value (if known), otherwise same as arg
-                 argrepr - human readable description of operation argument
-                 offset - start index of operation within bytecode sequence
-                 starts_line - line started by this opcode (if any), otherwise None
-                 is_jump_target - True if other code jumps to here, otherwise False
+            Defined fields:
+              opname - human readable name for operation
+              opcode - numeric code for operation
+              arg - numeric argument to operation (if any), otherwise None
+              argval - resolved arg value (if known), otherwise same as arg
+              argrepr - human readable description of operation argument
+              offset - start index of operation within bytecode sequence
+              starts_line - line started by this opcode (if any), otherwise None
+              is_jump_target - True if other code jumps to here, otherwise False
             """
 
             def __init__(self, *args, **kwargs):
                 _Instruction(*args, **kwargs)
                 self.opc = opc
+
         self.Instruction = Instruction
 
     def _print(self, x, file=None):
         if file is None:
             print(x)
         else:
-            file.write(str(x) + '\n')
+            file.write(str(x) + "\n")
 
     def code_info(self, x):
         """Formatted details of methods, functions, or code."""
-        return _code_info(x, self.python_version)
+        return _code_info(x, self.python_version_tuple)
 
     def show_code(self, x, file=None):
         """Print details of methods, functions, or code to *file*.
 
         If *file* is not provided, the output is printed on stdout.
         """
-        return _show_code(x, self.opc.version, file, is_pypy=self.is_pypy)
+        return _show_code(x, self.opc.version_tuple, file, is_pypy=self.is_pypy)
 
     def stack_effect(self, oparg=None, jump=None):
-        """Compute the stack effect of *opcode* with argument *oparg*.
-        """
+        """Compute the stack effect of *opcode* with argument *oparg*."""
         return _stack_effect(x, self.opc, oparg, jump)
 
     def pretty_flags(self, flags):
@@ -153,7 +162,8 @@ def distb(self, tb=None, file=None):
                 tb = sys.last_traceback
             except AttributeError:
                 raise RuntimeError("no last traceback to disassemble")
-            while tb.tb_next: tb = tb.tb_next
+            while tb.tb_next:
+                tb = tb.tb_next
         self.disassemble(tb.tb_frame.f_code, tb.tb_lasti, file=file)
 
     def disassemble(self, code, lasti=-1, file=None):
@@ -162,8 +172,13 @@ def disassemble(self, code, lasti=-1, file=None):
 
     def disco(self, code, lasti=-1, file=None):
         """Disassemble a code object."""
-        return _disco(self.python_version, code, timestamp=None,
-                      out=file, is_pypy=self.is_pypy)
+        return _disco(
+            self.python_version_tuple,
+            code,
+            timestamp=None,
+            out=file,
+            is_pypy=self.is_pypy,
+        )
 
     def get_instructions(self, x, first_line=None):
         """Iterator for the opcodes in methods, functions or code
@@ -217,6 +232,7 @@ def make_std_api(python_version=sys.version_info, variant=VARIANT):
         python_version = (major, minor)
     return _StdApi(python_version, variant)
 
+
 _std_api = make_std_api()
 
 hasconst = _std_api.hasconst
diff --git a/xdis/unmarshal.py b/xdis/unmarshal.py
index 9994b352..2ff17d58 100644
--- a/xdis/unmarshal.py
+++ b/xdis/unmarshal.py
@@ -30,9 +30,9 @@
 import sys
 from struct import unpack
 
-from xdis.magics import magic_int2float
+from xdis.magics import magic_int2tuple
 from xdis.codetype import to_portable
-from xdis.version_info import PYTHON3, PYTHON_VERSION, IS_PYPY
+from xdis.version_info import PYTHON3, PYTHON_VERSION, PYTHON_VERSION_TRIPLE, IS_PYPY
 
 if PYTHON3:
 
@@ -42,6 +42,7 @@ def long(n):
 
 else:
     import unicodedata
+
     # FIXME: we should write a bytes() class with a repr
     # that prints the b'' prefix so that Python2 can
     # print out Python3 code correctly
@@ -144,15 +145,15 @@ def __init__(self, fp, magic_int, bytes_for_s, code_objects={}):
         self.code_objects = code_objects
 
         self.bytes_for_s = bytes_for_s
-        version_float = magic_int2float(self.magic_int)
-        if version_float >= 3.4:
+        version = magic_int2tuple(self.magic_int)
+        if version >= (3, 4):
             if self.magic_int in (3250, 3260, 3270):
                 self.marshal_version = 3
             else:
                 self.marshal_version = 4
-        elif version_float < 3.4 and version_float >= 2.5:
+        elif version < (3, 4) and version >= (2, 5):
             self.marshal_version = 2
-        elif version_float < 2.5 and version_float >= 2.4:
+        elif version < (2, 5) and version >= (2, 4):
             self.marshal_version = 1
         else:
             self.marshal_version = 0
@@ -358,7 +359,7 @@ def t_interned(self, save_ref, bytes_for_s=False):
     def t_unicode(self, save_ref, bytes_for_s=False):
         strsize = unpack("<i", self.fp.read(4))[0]
         unicodestring = self.fp.read(strsize)
-        if PYTHON_VERSION == 3.2 and IS_PYPY:
+        if PYTHON_VERSION_TRIPLE == (3, 2) and IS_PYPY:
             # FIXME: this isn't quite right. See
             # pypy3-2.4.0/lib-python/3/email/message.py
             # '([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)')
@@ -438,47 +439,46 @@ def t_code(self, save_ref, bytes_for_s=False):
         # FIXME: Python 1.0 .. 1.3 isn't well known
 
         ret, i = self.r_ref_reserve(None, save_ref)
-        version = magic_int2float(self.magic_int)
+        version_tuple = magic_int2tuple(self.magic_int)
 
-        if version >= 2.3:
+        if version_tuple >= (2, 3):
             co_argcount = unpack("<i", self.fp.read(4))[0]
-        elif version >= 1.3:
+        elif version_tuple >= (1, 3):
             co_argcount = unpack("<h", self.fp.read(2))[0]
         else:
             co_argcount = 0
 
-        # FIXME:
-        # Note we do this by magic_int, not version which is *not*
-        # 3.8
-        if self.magic_int in (3412, 3413, 3422, 3425):
-            co_posonlyargcount = unpack("<i", self.fp.read(4))[0]
-        if version >= 3.8:
-            co_posonlyargcount = 0
+        if version_tuple >= (3, 8):
+            co_posonlyargcount = (
+                0
+                if self.magic_int in (3400, 3401, 3410, 3411)
+                else unpack("<i", self.fp.read(4))[0]
+            )
         else:
             co_posonlyargcount = None
 
-        if version >= 3.0:
+        if version_tuple >= (3, 0):
             kwonlyargcount = unpack("<i", self.fp.read(4))[0]
         else:
             kwonlyargcount = 0
 
-        if version >= 2.3:
+        if version_tuple >= (2, 3):
             co_nlocals = unpack("<i", self.fp.read(4))[0]
-        elif version >= 1.3:
+        elif version_tuple >= (1, 3):
             co_nlocals = unpack("<h", self.fp.read(2))[0]
         else:
             co_nlocals = 0
 
-        if version >= 2.3:
+        if version_tuple >= (2, 3):
             co_stacksize = unpack("<i", self.fp.read(4))[0]
-        elif version >= 1.5:
+        elif version_tuple >= (1, 5):
             co_stacksize = unpack("<h", self.fp.read(2))[0]
         else:
             co_stacksize = 0
 
-        if version >= 2.3:
+        if version_tuple >= (2, 3):
             co_flags = unpack("<i", self.fp.read(4))[0]
-        elif version >= 1.3:
+        elif version_tuple >= (1, 3):
             co_flags = unpack("<h", self.fp.read(2))[0]
         else:
             co_flags = 0
@@ -486,16 +486,16 @@ def t_code(self, save_ref, bytes_for_s=False):
         co_code = self.r_object(bytes_for_s=True)
 
         # FIXME: Check/verify that is true:
-        bytes_for_s = PYTHON_VERSION >= 3.0 and version > 3.0
+        bytes_for_s = PYTHON_VERSION_TRIPLE >= (3, 0) and (version_tuple > (3, 0))
         co_consts = self.r_object(bytes_for_s=bytes_for_s)
         co_names = self.r_object(bytes_for_s=bytes_for_s)
 
-        if version >= 1.3:
+        if version_tuple >= (1, 3):
             co_varnames = self.r_object(bytes_for_s=False)
         else:
             co_varnames = []
 
-        if version >= 2.0:
+        if version_tuple >= (2, 0):
             co_freevars = self.r_object(bytes_for_s=bytes_for_s)
             co_cellvars = self.r_object(bytes_for_s=bytes_for_s)
         else:
@@ -505,8 +505,8 @@ def t_code(self, save_ref, bytes_for_s=False):
         co_filename = self.r_object(bytes_for_s=bytes_for_s)
         co_name = self.r_object(bytes_for_s=bytes_for_s)
 
-        if version >= 1.5:
-            if version >= 2.3:
+        if version_tuple >= (1, 5):
+            if version_tuple >= (2, 3):
                 co_firstlineno = unpack("<i", self.fp.read(4))[0]
             else:
                 co_firstlineno = unpack("<h", self.fp.read(2))[0]
@@ -534,7 +534,7 @@ def t_code(self, save_ref, bytes_for_s=False):
             co_lnotab,
             co_freevars,
             co_cellvars,
-            version,
+            version_tuple,
         )
 
         self.code_objects[str(code)] = code
diff --git a/xdis/util.py b/xdis/util.py
index c3455ace..3ea611d4 100644
--- a/xdis/util.py
+++ b/xdis/util.py
@@ -69,6 +69,7 @@ def co_flags_is_async(co_flags):
         | COMPILER_FLAG_BIT["ASYNC_GENERATOR"]
     )
 
+
 def code_has_star_arg(code):
     """Return True iff
     the code object has a variable positional parameter (*args-like)"""
diff --git a/xdis/verify.py b/xdis/verify.py
index d79fedd9..72afd0c3 100644
--- a/xdis/verify.py
+++ b/xdis/verify.py
@@ -72,7 +72,7 @@ def dump_compile(codeobject, filename, timestamp, magic):
 
 
 def compare_code(c1, c2):
-    assert c1.co_code == c2.co_code, ("code %s vs. %s" % (c1.co_code, c2.co_code))
+    assert c1.co_code == c2.co_code, "code %s vs. %s" % (c1.co_code, c2.co_code)
     assert c1.co_argcount == c2.co_argcount
     assert c1.co_consts == c1.co_consts
     # assert len(c1.co_consts) == len(c2.co_consts), "consts:\n%s\nvs.\n%s" % (
diff --git a/xdis/version.py b/xdis/version.py
index b4e8ec58..d7fa6b68 100644
--- a/xdis/version.py
+++ b/xdis/version.py
@@ -4,4 +4,4 @@
 # well as importing into Python. That's why there is no
 # space around "=" below.
 # fmt: off
-__version__="5.0.14.dev0"  # noqa
+__version__="6.0.0.dev0"  # noqa
diff --git a/xdis/version_info.py b/xdis/version_info.py
index c9452f9d..dc7ba1c7 100644
--- a/xdis/version_info.py
+++ b/xdis/version_info.py
@@ -1,5 +1,5 @@
 """
-  Copyright (c) 2020 by Rocky Bernstein
+  Copyright (c) 2020-2021 by Rocky Bernstein
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
@@ -21,13 +21,14 @@
 
 import sys
 
-PYTHON3 = (sys.version_info >= (3, 0))
+PYTHON3 = sys.version_info >= (3, 0)
 
 # We do this crazy way to support Python 2.6 which
 # doesn't support version_major, and has a bug in
 # floating point so we can't divide 26 by 10 and get
 # 2.6
 PYTHON_VERSION = sys.version_info[0] + (sys.version_info[1] / 10.0)
-PYTHON_VERSION_STR  = "%s.%s" % (sys.version_info[0], sys.version_info[1])
+PYTHON_VERSION_TRIPLE = tuple(sys.version_info[:3])
+PYTHON_VERSION_STR = "%s.%s" % (sys.version_info[0], sys.version_info[1])
 
-IS_PYPY = '__pypy__' in sys.builtin_module_names
+IS_PYPY = "__pypy__" in sys.builtin_module_names
diff --git a/xdis/wordcode.py b/xdis/wordcode.py
index df39496d..4f7c44aa 100644
--- a/xdis/wordcode.py
+++ b/xdis/wordcode.py
@@ -1,4 +1,4 @@
-# (C) Copyright 2018, 2020 by Rocky Bernstein
+# (C) Copyright 2018, 2020-2021 by Rocky Bernstein
 #
 #  This program is free software; you can redistribute it and/or
 #  modify it under the terms of the GNU General Public License
@@ -50,17 +50,18 @@ def unpack_opargs_wordcode(code, opc):
             yield (i, op, arg)
 
 
-def get_jump_targets(code, opc):
+def findlabels(code, opc):
     """Returns a list of instruction offsets in the supplied bytecode
     which are the targets of some sort of jump instruction.
     """
     offsets = []
     for offset, op, arg in unpack_opargs_wordcode(code, opc):
         if arg is not None:
+            arg2 = arg * 2 if opc.version_tuple >= (3, 10) else arg
             if op in opc.JREL_OPS:
-                jump_offset = offset + 2 + arg
+                jump_offset = offset + 2 + arg2
             elif op in opc.JABS_OPS:
-                jump_offset = arg
+                jump_offset = arg2
             else:
                 continue
             if jump_offset not in offsets:
@@ -97,6 +98,3 @@ def get_jump_target_maps(code, opc):
                 prev_list.append(offset)
                 offset2prev[jump_offset] = prev_list
     return offset2prev
-
-
-findlabels = get_jump_targets


Test patch:
diff --git a/pytest/test_bytecode.py b/pytest/test_bytecode.py
index 2e0b597d..5e1c3b45 100644
--- a/pytest/test_bytecode.py
+++ b/pytest/test_bytecode.py
@@ -90,7 +90,7 @@ def bug708901():
                      10):
         pass
 
-def test_get_jump_targets():
+def findlabels():
     my_dir = osp.dirname(osp.abspath(__file__))
 
     # Python 2.7 code
diff --git a/pytest/test_cross_dis.py b/pytest/test_cross_dis.py
new file mode 100644
index 00000000..eccb9db5
--- /dev/null
+++ b/pytest/test_cross_dis.py
@@ -0,0 +1,12 @@
+import types
+from dis import findlabels as findlabels_std
+from xdis.cross_dis import findlabels
+from xdis.op_imports import get_opcode_module
+
+def test_findlabels():
+    code = findlabels.__code__.co_code
+    opc = get_opcode_module()
+    assert findlabels(code, opc) == findlabels_std(code)
+
+if __name__ == "__main__":
+    test_findlabels()
diff --git a/pytest/test_disasm.py b/pytest/test_disasm.py
index bc8eb762..d0b4301c 100644
--- a/pytest/test_disasm.py
+++ b/pytest/test_disasm.py
@@ -3,7 +3,7 @@
 import re
 
 from xdis import disassemble_file
-from xdis import PYTHON3, PYTHON_VERSION
+from xdis.version_info import PYTHON3, PYTHON_VERSION_TRIPLE
 
 if PYTHON3:
     from io import StringIO
@@ -20,7 +20,7 @@ def get_srcdir():
     return os.path.realpath(filename)
 
 
-if PYTHON_VERSION >= 3.2:
+if PYTHON_VERSION_TRIPLE >= (3, 2):
     @pytest.mark.parametrize(
         ("test_tuple", "function_to_test"),
         [
@@ -63,7 +63,15 @@ def test_funcoutput(capfd, test_tuple, function_to_test):
             )
             for line in got_lines
         ]
-        got = "\n".join(got_lines[5:])
+
+        # In Python before 3.10 lines decribing Python versions were
+        # two lines, e.g.:
+        #  Python 3.7.11 (default, Jul  3 2021, 19:46:46)
+        #    [GCC 9.3.0]
+        # In Python 3.10 they are a single line, e.g:
+        #    Python 3.10.0 (default, Oct  4 2021, 23:36:04) [GCC 9.3.0]
+        skip_lines = 4 if PYTHON_VERSION_TRIPLE >= (3, 10) else 5
+        got = "\n".join(got_lines[skip_lines:])
 
         if "XDIS_DONT_WRITE_DOT_GOT_FILES" not in os.environ:
             if got != expected:
diff --git a/pytest/test_instructions.py b/pytest/test_instructions.py
index 307687df..e02874d7 100644
--- a/pytest/test_instructions.py
+++ b/pytest/test_instructions.py
@@ -2,6 +2,7 @@
 import sys
 from xdis.op_imports import get_opcode_module
 from xdis import IS_PYPY
+from xdis.version_info import PYTHON_VERSION_TRIPLE
 import pytest
 
 def extended_arg_fn36():
@@ -18,12 +19,11 @@ def loop():
 
 from xdis.bytecode import Bytecode
 
-pytest.mark.skipif(sys.version_info == (3,6),
+pytest.mark.skipif(PYTHON_VERSION_TRIPLE < (3, 6),
                     reason="asssume Python 3.6 or greater wordsize instructions")
 def test_inst_size():
-    if (sys.version_info == (3,6)):
-        variant = 'pypy' if IS_PYPY else None
-        opc = get_opcode_module(sys.version_info, variant)
+    if (PYTHON_VERSION_TRIPLE[:2] == (3,6)) and not IS_PYPY:
+        opc = get_opcode_module(sys.version_info)
         bytecode_obj = Bytecode(extended_arg_fn36, opc)
         instructions = list(bytecode_obj.get_instructions(extended_arg_fn36))
 
@@ -41,7 +41,7 @@ def test_inst_size():
     else:
         assert True
 
-pytest.mark.skipif(sys.version_info < (2,7),
+pytest.mark.skipif(PYTHON_VERSION_TRIPLE < (2, 7),
                     reason="asssume Python 2.7 or greater")
 def test_inst_jumps():
     if (sys.version_info >= (2,7)):
@@ -62,7 +62,10 @@ def test_inst_jumps():
                 pass
             pass
         assert seen_pjif
-        assert seen_ja
+        # Python 3.10 code generation is more efficient and doesn't
+        # and removes a JUMP_ABSOLUTE.
+        if PYTHON_VERSION_TRIPLE < (3, 10):
+            assert seen_ja
 
 if __name__ == '__main__':
     test_inst_jumps()
diff --git a/pytest/test_load_file.py b/pytest/test_load_file.py
index 60f546a8..2c5f3237 100644
--- a/pytest/test_load_file.py
+++ b/pytest/test_load_file.py
@@ -1,5 +1,5 @@
-import os, sys
-from xdis import IS_PYPY, PYTHON_VERSION
+import os
+from xdis import IS_PYPY
 from xdis.load import load_file, check_object_path, load_module
 from xdis.codetype import CodeTypeUnionFields
 
@@ -10,19 +10,27 @@ def get_srcdir():
     filename = osp.normcase(osp.dirname(osp.abspath(__file__)))
     return osp.realpath(filename)
 
+
 def test_load_file():
     srcdir = get_srcdir()
     load_py = osp.realpath(osp.join(srcdir, "..", "xdis", "load.py"))
 
     co_file = load_file(load_py)
     obj_path = check_object_path(load_py)
-    (version, timestamp, magic_int, co_module, pypy,
-     source_size, sip_hash) = load_module(obj_path)
-    if 3.3 <= version <= 3.7:
+    (
+        version_tuple,
+        timestamp,
+        magic_int,
+        co_module,
+        pypy,
+        source_size,
+        sip_hash,
+    ) = load_module(obj_path)
+    if (3, 3) <= version_tuple <= (3, 7):
         statinfo = os.stat(load_py)
         assert statinfo.st_size == source_size
         assert sip_hash is None
-    elif version < 3.3:
+    elif version_tuple < (3, 3):
         assert source_size is None, source_size
         assert sip_hash is None
 
@@ -32,10 +40,15 @@ def test_load_file():
                 continue
             load_file_field = getattr(co_file, field)
             load_module_field = getattr(co_module, field)
-            assert load_module_field == load_file_field, (
-                "field %s\nmodule:\n\t%s\nfile:\n\t%s" % (field, load_module_field, load_file_field)
-                )
+            assert (
+                load_module_field == load_file_field
+            ), "field %s\nmodule:\n\t%s\nfile:\n\t%s" % (
+                field,
+                load_module_field,
+                load_file_field,
+            )
             print("ok %s" % field)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     test_load_file()
diff --git a/pytest/test_opcode.py b/pytest/test_opcode.py
index 9395875a..63c35a40 100644
--- a/pytest/test_opcode.py
+++ b/pytest/test_opcode.py
@@ -1,10 +1,10 @@
-from xdis import IS_PYPY, PYTHON_VERSION
+from xdis import IS_PYPY, PYTHON_VERSION_TRIPLE
 from xdis import get_opcode
 import dis
 
 
 def test_opcode():
-    opc = get_opcode(PYTHON_VERSION, IS_PYPY)
+    opc = get_opcode(PYTHON_VERSION_TRIPLE, IS_PYPY)
     opmap = dict([(k.replace("+", "_"), v) for (k, v) in dis.opmap.items()])
 
     print("Extra in dis:", set(opmap.items()) - set(opc.opmap.items()))
@@ -17,9 +17,9 @@ def test_opcode():
     # PyPy 2.7.13 changes opcodes mid-version. It is too complicated
     # to figure out where the change actually occurred
     # Pypy 3.6.9 may or may not have JUMP_IF_NOT_DEBUG
-    if not (IS_PYPY and PYTHON_VERSION in (2.7, 3.6, 3.7)):
+    if not (IS_PYPY and PYTHON_VERSION_TRIPLE[:2] in ((2, 7), (3, 6), (3, 7))):
         assert all(item in opmap.items() for item in opc.opmap.items())
-    elif IS_PYPY and PYTHON_VERSION == 3.6:
+    elif IS_PYPY and PYTHON_VERSION_TRIPLE[:2] == (3, 6):
         # Don't count JUMP_IF_NOT_DEBUG mismatch
         fields_str = "hascompare hasconst hasfree hasjabs haslocal"
 
diff --git a/pytest/test_std.py b/pytest/test_std.py
index 9aa165b1..1b233207 100644
--- a/pytest/test_std.py
+++ b/pytest/test_std.py
@@ -1,15 +1,18 @@
 # std
 import sys
 from contextlib import closing
+
 # compat
 import six
+
 # 3rd party
 import pytest
+
 # local
 import xdis.std as dis
-from xdis import PYTHON3, IS_PYPY, PYTHON_VERSION
+from xdis import PYTHON3, IS_PYPY, PYTHON_VERSION_TRIPLE
 
-if PYTHON_VERSION >= 3.2:
+if PYTHON_VERSION_TRIPLE >= (3, 2):
     if pytest.__version__ >= "3.2.0":
         yield_fixture = pytest.fixture
     else:
@@ -33,7 +36,7 @@
 # Filename:          <disassembly>
 # Argument count:    0
 """
-+ ("# Position-only argument count: 0\n" if PYTHON_VERSION >= 3.8 else "")
++ ("# Position-only argument count: 0\n" if PYTHON_VERSION_TRIPLE >= (3, 8) else "")
 + ("# Keyword-only arguments: 0\n" if PYTHON3 else "") +
 """# Number of locals:  0
 # Stack size:        1
@@ -44,7 +47,7 @@
 #    1: None
 # Names:
 #    0: a""").format(flags='0x00000000 (0x0)' if (
-    IS_PYPY and PYTHON_VERSION < 3.5) else '0x00000040 (NOFREE)')
+    IS_PYPY and PYTHON_VERSION_TRIPLE < (3, 5)) else '0x00000040 (NOFREE)')
 
 EXPECTED_DIS = """\
   1:           0 LOAD_CONST           (10)
@@ -60,13 +63,13 @@
                6 RETURN_VALUE
 """
 
-if PYTHON_VERSION < 3.6:
+if PYTHON_VERSION_TRIPLE < (3, 6):
     expected_dis = EXPECTED_DIS
 else:
     expected_dis = EXPECTED_DIS_36
 
 
-if PYTHON_VERSION >= 3.2:
+if PYTHON_VERSION_TRIPLE >= (3, 2):
     @pytest.fixture
     def bytecode_fixture():
         return dis.Bytecode(TEST_SOURCE_CODE)
@@ -166,16 +169,18 @@ def test_findlinestarts():
         actual_len = len(actual)
         assert actual_len > 0
 
+    @pytest.mark.skipif(PYTHON_VERSION_TRIPLE >= (3, 10),
+                        reason="Python 3.10 and above doesn't have branches in this code")
     def test_findlabels():
-        if PYTHON_VERSION < 3.6:
+        if PYTHON_VERSION_TRIPLE < (3, 6):
             test_code = TEST_BRANCH_CODE
         else:
             test_code = TEST_BRANCH_CODE.co_code
+
         actual = list(dis.findlabels(test_code))
         actual_len = len(actual)
         assert actual_len > 0
 
 if __name__ == "__main__":
-    # test_disassemble(six.StringIO())
     test_findlabels()
     # test_find_linestarts()
diff --git a/test/Makefile b/test/Makefile
index a108b63a..5e815876 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -21,6 +21,7 @@ check: check-short
                   --bytecode-3.2 --bytecode-3.3 \
                   --bytecode-3.4 --bytecode-3.5 \
 	          --bytecode-3.6 --bytecode-3.7 --bytecode-3.8 \
+	          --bytecode-3.9 --bytecode-3.10 \
 	          --bytecode-2.7pypy --bytecode-3.2pypy \
                   --bytecode-3.5pypy --bytecode-3.6pypy $(COMPILE)
 check-short:
diff --git a/test/add-test.py b/test/add-test.py
index af87a6d2..90b793a1 100755
--- a/test/add-test.py
+++ b/test/add-test.py
@@ -1,11 +1,12 @@
 #!/usr/bin/env python
 """ Trivial helper program to bytecompile
 """
+from xdis.version_info import PYTHON_VERSION_TRIPLE
 import os, sys, py_compile
 assert len(sys.argv) == 2
 path = sys.argv[1]
 short = os.path.basename(path)
-version = sys.version[0:3]
+version = ".".join([str(v) for v in PYTHON_VERSION_TRIPLE[:2]])
 if hasattr(sys, 'pypy_version_info'):
     cfile =  "bytecode_pypy%s/%s" % (version, short) + 'c'
 else:
diff --git a/test/bug3.1/03_extendedarg.py b/test/bug3.1/03_extendedarg.py
deleted file mode 100644
index 89535cc3..00000000
--- a/test/bug3.1/03_extendedarg.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# An example of Python 3.1 funkiness
-# Disassembling this gives the "EXTENDED_ARG" op
-
-def open(file, mode = "r", buffering = None,
-         encoding = None, errors = None,
-         newline = None, closefd = True) -> "IOBase":
-    return text
diff --git a/test/bytecode_3.10/00_chained-compare.pyc b/test/bytecode_3.10/00_chained-compare.pyc
new file mode 100644
index 00000000..90791998
Binary files /dev/null and b/test/bytecode_3.10/00_chained-compare.pyc differ
diff --git a/test/bytecode_3.10/00_docstring.pyc b/test/bytecode_3.10/00_docstring.pyc
new file mode 100644
index 00000000..a6f1cc91
Binary files /dev/null and b/test/bytecode_3.10/00_docstring.pyc differ
diff --git a/test/bytecode_3.10/01_chained_compare.pyc b/test/bytecode_3.10/01_chained_compare.pyc
new file mode 100644
index 00000000..2ad615e5
Binary files /dev/null and b/test/bytecode_3.10/01_chained_compare.pyc differ
diff --git a/test/bytecode_3.10/01_conditional.pyc b/test/bytecode_3.10/01_conditional.pyc
new file mode 100644
index 00000000..22db29d4
Binary files /dev/null and b/test/bytecode_3.10/01_conditional.pyc differ
diff --git a/test/bytecode_3.10/01_extra_iter.pyc b/test/bytecode_3.10/01_extra_iter.pyc
new file mode 100644
index 00000000..52dd2c1b
Binary files /dev/null and b/test/bytecode_3.10/01_extra_iter.pyc differ
diff --git a/test/bytecode_3.10/04_and_del.pyc b/test/bytecode_3.10/04_and_del.pyc
new file mode 100644
index 00000000..30298715
Binary files /dev/null and b/test/bytecode_3.10/04_and_del.pyc differ
diff --git a/test/bytecode_3.10/04_def_annotate.pyc b/test/bytecode_3.10/04_def_annotate.pyc
new file mode 100644
index 00000000..59a1ddc4
Binary files /dev/null and b/test/bytecode_3.10/04_def_annotate.pyc differ
diff --git a/test/bytecode_3.10/04_importlist.pyc b/test/bytecode_3.10/04_importlist.pyc
new file mode 100644
index 00000000..d9fca2f7
Binary files /dev/null and b/test/bytecode_3.10/04_importlist.pyc differ
diff --git a/test/bytecode_3.9/00_chained-compare.pyc b/test/bytecode_3.9/00_chained-compare.pyc
new file mode 100644
index 00000000..ec82a913
Binary files /dev/null and b/test/bytecode_3.9/00_chained-compare.pyc differ
diff --git a/test/bytecode_3.9/00_docstring.pyc b/test/bytecode_3.9/00_docstring.pyc
new file mode 100644
index 00000000..616d6018
Binary files /dev/null and b/test/bytecode_3.9/00_docstring.pyc differ
diff --git a/test/bytecode_3.9/01_assert2.pyc b/test/bytecode_3.9/01_assert2.pyc
new file mode 100644
index 00000000..07b61186
Binary files /dev/null and b/test/bytecode_3.9/01_assert2.pyc differ
diff --git a/test/bytecode_3.9/01_chained_compare.pyc b/test/bytecode_3.9/01_chained_compare.pyc
new file mode 100644
index 00000000..3cbcefe2
Binary files /dev/null and b/test/bytecode_3.9/01_chained_compare.pyc differ
diff --git a/test/bytecode_3.9/01_class.pyc b/test/bytecode_3.9/01_class.pyc
new file mode 100644
index 00000000..ea6ba471
Binary files /dev/null and b/test/bytecode_3.9/01_class.pyc differ
diff --git a/test/bytecode_3.9/01_conditional.pyc b/test/bytecode_3.9/01_conditional.pyc
new file mode 100644
index 00000000..7003771b
Binary files /dev/null and b/test/bytecode_3.9/01_conditional.pyc differ
diff --git a/test/bytecode_3.9/01_extra_iter.pyc b/test/bytecode_3.9/01_extra_iter.pyc
new file mode 100644
index 00000000..c87f59c2
Binary files /dev/null and b/test/bytecode_3.9/01_extra_iter.pyc differ
diff --git a/test/bytecode_3.9/01_triple_compare.pyc b/test/bytecode_3.9/01_triple_compare.pyc
new file mode 100644
index 00000000..68cd4813
Binary files /dev/null and b/test/bytecode_3.9/01_triple_compare.pyc differ
diff --git a/test/bytecode_3.9/04_and_del.pyc b/test/bytecode_3.9/04_and_del.pyc
new file mode 100644
index 00000000..3e16c8b2
Binary files /dev/null and b/test/bytecode_3.9/04_and_del.pyc differ
diff --git a/test/bytecode_3.9/04_def_annotate.pyc b/test/bytecode_3.9/04_def_annotate.pyc
new file mode 100644
index 00000000..e23ed3cd
Binary files /dev/null and b/test/bytecode_3.9/04_def_annotate.pyc differ
diff --git a/test/bytecode_3.9/04_importlist.pyc b/test/bytecode_3.9/04_importlist.pyc
new file mode 100644
index 00000000..4818e5a9
Binary files /dev/null and b/test/bytecode_3.9/04_importlist.pyc differ
diff --git a/test/simple_source/bug30/00_chained-compare.py b/test/simple_source/bug30/00_chained-compare.py
new file mode 100644
index 00000000..199f6526
--- /dev/null
+++ b/test/simple_source/bug30/00_chained-compare.py
@@ -0,0 +1,6 @@
+# From 3.0 asyncore.py
+# This is RUNNABLE!
+r, w, e = ([], [], [])
+if [] == r == w == e:
+    r = [1]
+assert r == [1]
diff --git a/test/simple_source/bug30/04_and_del.py b/test/simple_source/bug30/04_and_del.py
new file mode 100644
index 00000000..b161026f
--- /dev/null
+++ b/test/simple_source/bug30/04_and_del.py
@@ -0,0 +1,32 @@
+# From 2.5.6 osxemxpath.py
+# Bug is in getting "and" and "del" correct
+
+# This is RUNNABLE!
+def normpath(comps):
+    i = 0
+    while i < len(comps):
+        if comps[i] == '.':
+            del comps[i]
+        elif comps[i] == '..' and i > 0 and comps[i-1] not in ('', '..'):
+            del comps[i-1:i+1]
+            i = i - 1
+        elif comps[i] == '' and i > 0 and comps[i-1] != '':
+            del comps[i]
+        else:
+            i = i + 1
+    return comps
+
+assert normpath(['.']) == []
+assert normpath(['a', 'b', '..']) == ['a']
+assert normpath(['a', 'b', '', 'c']) == ['a', 'b', 'c']
+assert normpath(['a', 'b', '.', '', 'c', '..']) == ['a', 'b']
+
+# Adapted from 3.0.1 cgi.py
+# Bug was in detecting "or" and "and" due to lack of PUSH/POP_IF instructions.
+def handle(format, html, text):
+    formatter = (format and html) or text
+    return formatter
+
+assert handle(False, False, True)
+assert not handle(True, False, False)
+assert handle(True, True, False)
diff --git a/test/simple_source/bug31/04_def_annotate.py b/test/simple_source/bug31/04_def_annotate.py
new file mode 100644
index 00000000..250b0bf4
--- /dev/null
+++ b/test/simple_source/bug31/04_def_annotate.py
@@ -0,0 +1,169 @@
+# Python 3 positional, kwonly, varargs, and annotations. Ick.
+
+# RUNNABLE!
+def test1(args_1, c: int, w=4, *varargs: int, **kwargs: 'annotating kwargs') -> tuple:
+    return (args_1, c, w, kwargs)
+
+def test2(args_1, args_2, c: int, w=4, *varargs: int, **kwargs: 'annotating kwargs'):
+    return (args_1, args_2, c, w, varargs, kwargs)
+
+def test3(c: int, w=4, *varargs: int, **kwargs: 'annotating kwargs') -> float:
+    return 5.4
+
+def test4(a: float, c: int, *varargs: int, **kwargs: 'annotating kwargs') -> float:
+    return 5.4
+
+def test5(a: float, c: int = 5, *varargs: int, **kwargs: 'annotating kwargs') -> float:
+    return 5.4
+
+def test6(a: float, c: int, test=None):
+    return (a, c, test)
+
+def test7(*varargs: int, **kwargs):
+    return (varargs, kwargs)
+
+def test8(x=55, *varargs: int, **kwargs) -> list:
+    return (x, varargs, kwargs)
+
+def test9(arg_1=55, *varargs: int, y=5, **kwargs):
+    return x, varargs, int, y, kwargs
+
+def test10(args_1, b: 'annotating b', c: int) -> float:
+    return 5.4
+
+def test11(*, name):
+    return args, name
+
+def test12(a, *args, name):
+    return a, args
+    pass
+
+def test13(*args, name):
+    return args, name
+
+def test14(*args, name: int=1, qname):
+    return args, name, qname
+
+def test15(*args, name='S', fname, qname=4):
+    return args, name, fname, qname
+
+# From 3.4 /asyncio/streams.py open_connection
+_DEFAULT_LIMIT = 5
+def test16(host=None, port=None, *,
+           loop=None, limit=_DEFAULT_LIMIT, **kwds):
+    return host, port, loop, limit, kwds
+
+# Python 3.1 _pyio.py uses the  -> "IOBase" annotation
+def o(f, mode = "r", buffering = None) -> "IOBase":
+    return (f, mode, buffering)
+
+def foo1(x: 'an argument that defaults to 5' = 5):
+    print(x)
+
+def div(a: dict(type=float, help='the dividend'),
+        b: dict(type=float, help='the divisor (must be different than 0)')
+    ) -> dict(type=float, help='the result of dividing a by b'):
+    """Divide a by b"""
+    return a / b
+
+# From 3.7.6 functools.py
+# Bug is in picking up the annotation.
+def f(a:"This is a new annotation"):
+    """This is a test"""
+    assert f.__annotations__['a'] == "This is a new annotation"
+
+f(5)
+
+class TestSignatureObject1():
+    def test_signature_on_wkwonly(self):
+        def test(*, a:float, b:str, c:str = 'test', **kwargs: int) -> int:
+            pass
+
+class TestSignatureObject2():
+    def test_signature_on_wkwonly(self):
+        def test(*, c='test', a:float, b:str="S", **kwargs: int) -> int:
+            pass
+
+class TestSignatureObject3():
+    def test_signature_on_wkwonly(self):
+        def test(*, c='test', a:float, kwargs:str="S", **b: int) -> int:
+            pass
+
+class TestSignatureObject4():
+    def test_signature_on_wkwonly(self):
+        def test(x=55, *args, c:str='test', a:float, kwargs:str="S", **b: int) -> int:
+            pass
+
+class TestSignatureObject5():
+    def test_signature_on_wkwonly(self):
+        def test(x=55, *args: int, c='test', a:float, kwargs:str="S", **b: int) -> int:
+            pass
+
+class TestSignatureObject5():
+    def test_signature_on_wkwonly(self):
+        def test(x:int=55, *args: (int, str), c='test', a:float, kwargs:str="S", **b: int) -> int:
+            pass
+
+class TestSignatureObject7():
+    def test_signature_on_wkwonly(self):
+        def test(c='test', kwargs:str="S", **b: int) -> int:
+            pass
+
+class TestSignatureObject8():
+    def test_signature_on_wkwonly(self):
+        def test(**b: int) -> int:
+            pass
+
+class TestSignatureObject9():
+    def test_signature_on_wkwonly(self):
+        def test(a, **b: int) -> int:
+            pass
+
+class SupportsInt():
+
+    def __int__(self) -> int:
+        pass
+
+def ann1(args_1, b: 'annotating b', c: int, *varargs: str) -> float:
+    assert ann1.__annotations__['b'] == 'annotating b'
+    assert ann1.__annotations__['c'] == int
+    assert ann1.__annotations__['varargs'] == str
+    assert ann1.__annotations__['return'] == float
+
+def ann2(args_1, b: int = 5, **kwargs: float) -> float:
+    assert ann2.__annotations__['b'] == int
+    assert ann2.__annotations__['kwargs'] == float
+    assert ann2.__annotations__['return'] == float
+    assert b == 5
+
+class TestSignatureObject():
+    def test_signature_on_wkwonly(self):
+        def test(x:int=55, *args: (int, str), c='test', a:float, kwargs:str="S", **b: int) -> int:
+            pass
+
+assert test1(1, 5) == (1, 5, 4, {})
+assert test1(1, 5, 6, foo='bar') == (1, 5, 6, {'foo': 'bar'})
+assert test2(2, 3, 4) == (2, 3, 4, 4, (), {})
+assert test3(10, foo='bar') == 5.4
+assert test4(9.5, 7, 6, 4, bar='baz') == 5.4
+### FIXME: fill in...
+assert test6(1.2, 3) == (1.2, 3, None)
+assert test6(2.3, 4, 5) == (2.3, 4, 5)
+
+ann1(1, 'test', 5)
+ann2(1)
+
+### FIXME: fill in...
+
+assert test12(1, 2, 3, name='hi') == (1, (2, 3)), "a, *args, name"
+assert test13(1, 2, 3, name='hi') == ((1, 2, 3), 'hi'), "*args, name"
+assert test16('localhost', loop=2, limit=3, a='b') == ('localhost', None, 2, 3, {'a': 'b'})
+
+# From test 3.5 test_pydoc.py.
+# Bug was in 3.5 and earlier handling of the return type, typing.Tuple[...]
+try:
+    import typing
+    def foo() -> typing.Iterator[typing.Tuple[int, typing.Any]]:
+        ...
+except:
+    pass
diff --git a/test/simple_source/bug33/01_triple_compare.py b/test/simple_source/bug33/01_triple_compare.py
new file mode 100644
index 00000000..dc28d36c
--- /dev/null
+++ b/test/simple_source/bug33/01_triple_compare.py
@@ -0,0 +1,36 @@
+# In Python 3.3+ this uses grammar rule
+# compare_chained2 ::= expr COMPARE_OP RETURN_VALUE
+# In Python 3.6 uses this uses grammar rule
+# compare_chained2 ::= expr COMPARE_OP come_froms JUMP_FORWARD
+
+# Seen in Python 3.3 ipaddress.py
+
+# RUNNABLE!
+def _is_valid_netmask(netmask):
+    return 0 <= netmask <= 10
+
+# There were also bugs in 2.6- involving the use of "or" twice in its "or"
+# detections
+
+# See in 2.6.9 quopri.py ishex():
+assert not '0' <= __file__ <= '9' or 'a' <= __file__ <= 'f' or 'A' <= __file__ <= 'F'
+
+# From 3.7 bug-grammar.py
+
+# Bug in 3.7 was handling the last line where compare_chained -> compare_chained37 and
+# therefore compare_chained has one child, not two as it normally does.
+
+def test_comparison():
+    ### comparison: expr (comp_op expr)*
+    ### comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'|'is' 'not'
+    if 1: pass
+    x = (1 == 1)
+    if 1 == 1: pass
+    if 1 != 1: pass
+    if 1 < 1: pass
+    if 1 > 1: pass
+    if 1 <= 1: pass
+    if 1 >= 1: pass
+    if 1 in (): pass
+    if 1 not in (): pass
+    if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in 1 is 1 is not 1: pass
diff --git a/test/simple_source/bug36/01_conditional.py b/test/simple_source/bug36/01_conditional.py
new file mode 100644
index 00000000..b643af49
--- /dev/null
+++ b/test/simple_source/bug36/01_conditional.py
@@ -0,0 +1,21 @@
+# From 3.7.6 test_buffer.py
+
+# RUNNABLE!
+def foo(n):
+    zero_stride = True if n >= 95 and n & 1 else False
+    return zero_stride
+
+assert foo(95)
+assert not foo(94)
+assert not foo(96)
+
+# from test_buffer.py
+# Bug was handling "or" inside a conditional
+def rslice(a, b):
+    minlen = 0 if a or b else 1
+    return minlen
+
+assert rslice(False, False) == 1
+assert rslice(False, True) == 0
+assert rslice(True, False) == 0
+assert rslice(True, True) == 0
diff --git a/test/simple_source/bug37/01_chained_compare.py b/test/simple_source/bug37/01_chained_compare.py
new file mode 100644
index 00000000..a2347802
--- /dev/null
+++ b/test/simple_source/bug37/01_chained_compare.py
@@ -0,0 +1,26 @@
+# From Python 3.7 pickle.py
+# Bug was different code generation for chained comparisons than prior Python versions
+
+
+def chained_compare_a(protocol):
+    if not 0 <= protocol <= 7:
+        raise ValueError("pickle protocol must be <= %d" % 7)
+
+def chained_compare_b(a, obj):
+    if a:
+        if -0x80000000 <= obj <= 0x7fffffff:
+            return 5
+
+def chained_compare_c(a, d):
+    for i in len(d):
+        if a == d[i] != 2:
+            return 5
+
+chained_compare_a(3)
+try:
+    chained_compare_a(8)
+except ValueError:
+    pass
+chained_compare_b(True, 0x0)
+
+chained_compare_c(3, [3])
diff --git a/test/simple_source/bug38/01_extra_iter.py b/test/simple_source/bug38/01_extra_iter.py
new file mode 100644
index 00000000..476c2055
--- /dev/null
+++ b/test/simple_source/bug38/01_extra_iter.py
@@ -0,0 +1,7 @@
+# Adapted from From 3.3 urllib/parse.py
+qs = "https://travis-ci.org/rocky/python-uncompyle6/builds/605260823?utm_medium=notification&utm_source=email"
+expect = ['https://travis-ci.org/rocky/python-uncompyle6/builds/605260823?utm_medium=notification', 'utm_source=email']
+
+# Should visually see that we don't add an extra iter() which is not technically wrong, just
+# unnecessary.
+assert expect == [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
diff --git a/test/simple_source/def/01_class.py b/test/simple_source/def/01_class.py
new file mode 100644
index 00000000..8d98daf1
--- /dev/null
+++ b/test/simple_source/def/01_class.py
@@ -0,0 +1,26 @@
+# Tests:
+#
+# For Python3:
+#   classdef ::= LOAD_BUILD_CLASS mkfunc LOAD_CONST CALL_FUNCTION_2 store
+#   mkfunc ::= LOAD_CONST LOAD_CONST MAKE_FUNCTION_0
+
+# For Python2:
+#   classdef ::= LOAD_CONST expr mkfunc CALL_FUNCTION_0 BUILD_CLASS store
+#   mkfunc ::= LOAD_CONST MAKE_FUNCTION_0
+
+# RUNNABLE!
+class A:
+    pass
+
+class B(Exception):
+    pass
+
+# From 3.x test_descr.py
+class MyInt(int):
+    class MyInt(int):
+        __slots__ = ()
+    try:
+        (1).__class__ = MyInt
+        assert False, "builtin types don't support __class__ assignment."
+    except TypeError:
+        pass
diff --git a/test/simple_source/stmts/00_docstring.py b/test/simple_source/stmts/00_docstring.py
new file mode 100644
index 00000000..7b1cad6a
--- /dev/null
+++ b/test/simple_source/stmts/00_docstring.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# uncompyle2 bug was not escaping """ properly
+
+# RUNNABLE!
+r'''func placeholder - with ("""\nstring\n""")'''
+
+def dq0():
+    assert __doc__ == r'''func placeholder - with ("""\nstring\n""")'''
+
+def dq1():
+    """assert that dedent() has no effect on 'text'"""
+    assert dq1.__doc__ == """assert that dedent() has no effect on 'text'"""
+
+def dq2():
+    '''assert that dedent() has no effect on 'text\''''
+    assert dq1.__doc__ == '''assert that dedent() has no effect on 'text\''''
+
+def dq3():
+    """assert that dedent() has no effect on 'text\""""
+    assert dq3.__doc__ == """assert that dedent() has no effect on 'text\""""
+
+def dq4():
+    """assert that dedent() has no effect on 'text'"""
+    assert dq4.__doc__ == """assert that dedent() has no effect on 'text'"""
+
+def dq5():
+    r'''func placeholder - ' and with ("""\nstring\n""")'''
+    assert dq5.__doc__ == r'''func placeholder - ' and with ("""\nstring\n""")'''
+
+def dq6():
+    r"""func placeholder - ' and with ('''\nstring\n''') and \"\"\"\nstring\n\"\"\" """
+    assert dq6.__doc__ == r"""func placeholder - ' and with ('''\nstring\n''') and \"\"\"\nstring\n\"\"\" """
+
+def dq7():
+  u"""        <----- SEE 'u' HERE
+  >>> mylen(u"áéíóú")
+  5
+  """
+  assert dq7.__doc__ ==   u"""        <----- SEE 'u' HERE
+  >>> mylen(u"áéíóú")
+  5
+  """
+
+def dq8():
+  u"""        <----- SEE 'u' HERE
+  >>> mylen(u"تست")
+  5
+  """
+  assert dq8.__doc__ ==   u"""        <----- SEE 'u' HERE
+  >>> mylen(u"تست")
+  5
+  """
+
+def baz():
+    """
+        ...     '''>>> assert 1 == 1
+        ...     '''
+        ... \"""
+        >>> exec test_data in m1.__dict__
+        >>> exec test_data in m2.__dict__
+        >>> m1.__dict__.update({"f2": m2._f, "g2": m2.g, "h2": m2.H})
+
+        Tests that objects outside m1 are excluded:
+        \"""
+        >>> t.rundict(m1.__dict__, 'rundict_test_pvt')  # None are skipped.
+        TestResults(failed=0, attempted=8)
+    """
+    assert baz.__doc__ == \
+    """
+        ...     '''>>> assert 1 == 1
+        ...     '''
+        ... \"""
+        >>> exec test_data in m1.__dict__
+        >>> exec test_data in m2.__dict__
+        >>> m1.__dict__.update({"f2": m2._f, "g2": m2.g, "h2": m2.H})
+
+        Tests that objects outside m1 are excluded:
+        \"""
+        >>> t.rundict(m1.__dict__, 'rundict_test_pvt')  # None are skipped.
+        TestResults(failed=0, attempted=8)
+    """
+
+dq0()
+dq1()
+dq2()
+dq3()
+dq4()
+dq5()
+dq6()
+dq7()
+dq8()
+baz()
diff --git a/test/test_pyenvlib.py b/test/test_pyenvlib.py
index d64cb8c6..42333da2 100755
--- a/test/test_pyenvlib.py
+++ b/test/test_pyenvlib.py
@@ -24,7 +24,6 @@
 
 import xdis.magics as magics
 from xdis.load import check_object_path
-from xdis import IS_PYPY
 
 # ----- configure this for your needs
 
@@ -175,6 +174,8 @@ def file_matches(files, root, basenames, patterns):
     for i, bc_file in enumerate(files):
         if verbose:
             print(os.path.join(src_dir, bc_file))
+        if src_dir.find("simple_source") >= 0:
+            continue
         if sys.version_info >= (3, 4, 0) and bc_file.endswith(".py"):
             check_bc_file = check_object_path(bc_file)
             if not osp.exists(check_bc_file):
diff --git a/test/test_pythonlib.py b/test/test_pythonlib.py
index 5f597f60..3a284de0 100755
--- a/test/test_pythonlib.py
+++ b/test/test_pythonlib.py
@@ -75,7 +75,7 @@ def get_srcdir():
              2.1, 2.2, 2.3, 2.4, 2.5, '2.5dropbox', 2.6, 2.7,
              3.0, 3.1, 3.2, 3.3, 3.4, 3.5, '3.2pypy', '2.7pypy',
              '3.5pypy', '3.6pypy',
-             3.6, 3.7, 3.8):
+             3.6, 3.7, 3.8, '3.9', '3.10'):
     bytecode = "bytecode_%s" % vers
     key = "bytecode-%s" % vers
     test_options[key] = (os.path.join(src_dir, bytecode), PYC, bytecode, vers)