Skip to content

Commit

Permalink
Merge pull request #458 from nucleic/py310-fixes
Browse files Browse the repository at this point in the history
core: prune return None from inserted Python blocks
  • Loading branch information
MatthieuDartiailh committed Nov 11, 2021
2 parents 027e82e + 2ce1347 commit 1045062
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 17 deletions.
19 changes: 12 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,18 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.7', '3.8', '3.9', '3.10.0-rc.2']
python-version: ['3.7', '3.8', '3.9', '3.10.0']
qt-binding: [pyqt5, pyside2]
exclude:
- os: ubuntu-latest
python-version: '3.10.0'
qt-binding: pyside2
- os: windows-latest
python-version: '3.10.0'
qt-binding: pyside2
- os: macos-latest
python-version: '3.10.0'
qt-binding: pyside2
fail-fast: false
steps:
- name: Install linux only test dependency
Expand All @@ -50,7 +60,6 @@ jobs:
pip install https://github.com/nucleic/atom/tarball/main
pip install https://github.com/nucleic/kiwi/tarball/main
- name: Install Qt bindings
if: matrix.python-version != '3.10.0-rc.1'
run: |
pip install numpy qtpy '${{matrix.qt-binding}}'
- name: Install extra dependencies
Expand All @@ -68,11 +77,7 @@ jobs:
python setup.py develop
- name: Install pytest
run: |
pip install pytest pytest-cov
- name: Install pytest-qt
if: matrix.python-version != '3.10.0-rc.2'
run: |
pip install pytest-qt
pip install pytest pytest-cov pytest-qt
- name: Run tests (Windows, Mac)
if: matrix.os != 'ubuntu-latest'
run: python -X dev -m pytest tests --cov enaml --cov-report xml
Expand Down
2 changes: 2 additions & 0 deletions enaml/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

PY39 = sys.version_info >= (3, 9)

PY310 = sys.version_info >= (3, 10)


STRING_ESCAPE_SEQUENCE_RE = re.compile(r'''
( \\U........ # 8-digit hex escapes
Expand Down
65 changes: 60 additions & 5 deletions enaml/core/code_generator.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013-2020, Nucleic Development Team.
# Copyright (c) 2013-2021, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
import ast
from contextlib import contextmanager

import bytecode as bc
from atom.api import Atom, Bool, Int, List, Str

from ..compat import POS_ONLY_ARGS, PY38, PY39
from ..compat import POS_ONLY_ARGS, PY38, PY39, PY310


class _ReturnNoneIdentifier(ast.NodeVisitor):
"""Visit top level nodes looking for return None."""

def __init__(self) -> None:
super().__init__()
# Lines on which an explicit return None or return exist
self.lines = set()

def visit_Return(self, node: ast.Return) -> None:
if not node.value or (
isinstance(node.value, ast.Constant) and node.value.value is None
):
self.lines.add(node.lineno)

# Do not inspect nodes in which a return None won't be relevant.
def visit_AsyncFunctionDef(self, node):
pass

def visit_FunctionDef(self, node):
pass

def visit_ClassDef(self, node):
pass


class CodeGenerator(Atom):
Expand Down Expand Up @@ -433,12 +459,41 @@ def insert_python_block(self, pydata, trim=True):
""" Insert the compiled code for a Python Module ast or string.
"""
if PY310:
_inspector = _ReturnNoneIdentifier()
_inspector.visit(pydata)
code = compile(pydata, self.filename, mode='exec')
bc_code = bc.Bytecode.from_code(code)
# Skip the LOAD_CONST RETURN_VALUE pair if it exists (on Python 3.10+
# if the module ends on a raise, that pair which is unreachable is ommitted)
if trim and bc_code[-1].name == "RETURN_VALUE":
# On python 3.10 with a with or try statement the implicit return None
# can be duplicated. We remove return None from all basic blocks when
# it was not present in the AST
if PY310:
cfg = bc.ControlFlowGraph.from_bytecode(bc_code)
new_end = None
last_block = cfg[-1]
for block in list(cfg):
if (
block[-1].name == "RETURN_VALUE"
and block[-2].name == "LOAD_CONST"
and block[-2].arg is None
and block[-1].lineno not in _inspector.lines
):
del block[-2:]
# If we have multiple block jump to the end of the last block
# to execute the code that may be appended to this block
if block is not last_block:
# We use a NOP to be sure to always have a valid jump target
new_end = new_end or cfg.add_block([bc.Instr("NOP")])
block.append(bc.Instr("JUMP_FORWARD", new_end))

if new_end is not None:
last_block.next_block = new_end

bc_code = cfg.to_bytecode()
# Skip the LOAD_CONST RETURN_VALUE pair if it exists
elif trim and bc_code[-1].name == "RETURN_VALUE":
bc_code = bc_code[:-2]

self.code_ops.extend(bc_code)

def insert_python_expr(self, pydata, trim=True):
Expand Down
8 changes: 4 additions & 4 deletions enaml/core/enaml_compiler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
# Copyright (c) 2013-2021, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
import sys
import ast

from . import compiler_common as cmn
from .enaml_ast import Module
Expand Down Expand Up @@ -183,7 +183,7 @@ def visit_Module(self, node):
# Generate the startup code for the module.
cg.set_lineno(1)
for start in STARTUP:
cg.insert_python_block(start)
cg.insert_python_block(ast.parse(start))

# Create the template map.
cg.build_map()
Expand All @@ -198,7 +198,7 @@ def visit_Module(self, node):

# Generate the cleanup code for the module.
for end in CLEANUP:
cg.insert_python_block(end)
cg.insert_python_block(ast.parse(end))

# Finalize the ops and return the code object.
cg.load_const(None)
Expand Down
4 changes: 3 additions & 1 deletion releasenotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Dates are written as DD/MM/YYYY
- fix operator bindings in template instances PR #445
- fix FlowLayout error with FlowItems that have non-zero stretch or ortho_stretch PR #448
- add support for styling notebook tabs PR #452
- drop official support for Python 3.6
- drop official support for Python 3.6 and add minimal support for Python 3.10
As with earlier Python version, support for 3.10 is currently limited to running on
Python 3.10 excluding any features that were added on Python 3.10

0.13.0 - 19/04/2021
-------------------
Expand Down
27 changes: 27 additions & 0 deletions tests/core/test_code_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#------------------------------------------------------------------------------
# Copyright (c) 2021, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
"""Test for the code generator."""
import ast
import pytest

from enaml.core.code_generator import CodeGenerator


# Since we cannot have a return outside a function I am not sure we can ever
# encounter a non-implicit return opcode.
@pytest.mark.parametrize("source, return_on_lines", [
("pass", []),
("for i in range(10):\n i", []),
("with open(f) as f:\n print(f.readlines())", []),
])
def test_python_block_insertion(source, return_on_lines):
cg = CodeGenerator()
cg.insert_python_block(ast.parse(source))
for i in cg.code_ops:
if getattr(i, "name", "") == "RETURN_VALUE":
assert i.lineno in return_on_lines

0 comments on commit 1045062

Please sign in to comment.