diff --git a/.github/workflows/run-docs-build.yml b/.github/workflows/run-docs-build.yml index 73bac781..06bfa8fb 100644 --- a/.github/workflows/run-docs-build.yml +++ b/.github/workflows/run-docs-build.yml @@ -16,7 +16,7 @@ jobs: matrix: include: - os: ubuntu-20.04 - python: 3.7 + python: "3.8" steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/run-tests-workflow.yml b/.github/workflows/run-tests-workflow.yml index 6310a77c..e39b2224 100644 --- a/.github/workflows/run-tests-workflow.yml +++ b/.github/workflows/run-tests-workflow.yml @@ -21,10 +21,6 @@ jobs: python-version: 3.8 test-env: "PyQt5~=5.12.0" - - os: ubuntu-20.04 - python-version: 3.7 - test-env: "PyQt5~=5.15.0" - - os: ubuntu-20.04 python-version: 3.8 test-env: "PyQt5~=5.15.0" @@ -56,31 +52,31 @@ jobs: extra-system-packages: "glibc-tools" # macOS - - os: macos-11 + - os: macos-12 python-version: 3.8 test-env: "PyQt5~=5.12.0" - - os: macos-11 + - os: macos-12 python-version: 3.9 test-env: "PyQt5~=5.14.0" - - os: macos-11 + - os: macos-12 python-version: "3.10" test-env: "PyQt5~=5.15.0" - - os: macos-12 + - os: macos-13 python-version: "3.11" test-env: "PyQt5~=5.15.0" - - os: macos-12 + - os: macos-14 python-version: "3.11" test-env: "PyQt6~=6.2.3 PyQt6-Qt6~=6.2.3" - - os: macos-12 + - os: macos-latest python-version: "3.11" test-env: "PyQt6~=6.5.0 PyQt6-Qt6~=6.5.0" - - os: macos-12 + - os: macos-latest python-version: "3.12" test-env: "PyQt6~=6.5.0 PyQt6-Qt6~=6.5.0" diff --git a/.readthedocs.yml b/.readthedocs.yml index 0042c121..3917de26 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.7" + python: "3.8" python: install: diff --git a/docs/requirements-rtd.txt b/docs/requirements-rtd.txt index 5b1cc484..53aff798 100644 --- a/docs/requirements-rtd.txt +++ b/docs/requirements-rtd.txt @@ -3,7 +3,7 @@ setuptools sphinx~=4.2.0 sphinx-rtd-theme -PyQt5~=5.9.2 +PyQt5 AnyQt # sphinx pins docutils version, but the installation in the RTD worker/config diff --git a/orangecanvas/scheme/readwrite.py b/orangecanvas/scheme/readwrite.py index 42e5a985..27e70aa3 100644 --- a/orangecanvas/scheme/readwrite.py +++ b/orangecanvas/scheme/readwrite.py @@ -3,9 +3,6 @@ """ import numbers -import sys -import types -import warnings import base64 import binascii import itertools @@ -14,7 +11,7 @@ from xml.etree.ElementTree import TreeBuilder, Element, ElementTree, parse from collections import defaultdict -from itertools import chain, count +from itertools import chain import pickle import json @@ -29,6 +26,8 @@ NamedTuple, Dict, Tuple, List, Union, Any, Optional, AnyStr, IO ) +from typing_extensions import TypeGuard + from . import SchemeNode, SchemeLink from .annotations import SchemeTextAnnotation, SchemeArrowAnnotation from .errors import IncompatibleChannelTypeError @@ -65,9 +64,10 @@ def string_eval(source): """ node = _ast_parse_expr(source) - if not isinstance(node.body, ast.Str): + body = node.body + if not _is_constant(body, (str,)): raise ValueError("%r is not a string literal" % source) - return node.body.s + return body.value def tuple_eval(source): @@ -85,11 +85,11 @@ def tuple_eval(source): if not isinstance(node.body, ast.Tuple): raise ValueError("%r is not a tuple literal" % source) - if not all(isinstance(el, (ast.Str, ast.Num)) or + if not all(_is_constant(el, (str, float, complex, int)) or # allow signed number literals in Python3 (i.e. -1|+1|-1.0) (isinstance(el, ast.UnaryOp) and isinstance(el.op, (ast.UAdd, ast.USub)) and - isinstance(el.operand, ast.Num)) + _is_constant(el.operand, (float, complex, int))) for el in node.body.elts): raise ValueError("Can only contain numbers or strings") @@ -112,18 +112,17 @@ def terminal_eval(source): def _terminal_value(node): # type: (ast.AST) -> Union[str, bytes, int, float, complex, None] - if isinstance(node, ast.Str): - return node.s - elif isinstance(node, ast.Bytes): - return node.s - elif isinstance(node, ast.Num): - return node.n - elif isinstance(node, ast.NameConstant): + if _is_constant(node, (str, bytes, int, float, complex, type(None))): return node.value - raise ValueError("Not a terminal") +def _is_constant( + node: ast.AST, types: Tuple[type, ...] +) -> TypeGuard[ast.Constant]: + return isinstance(node, ast.Constant) and isinstance(node.value, types) + + # Intermediate scheme representation _scheme = NamedTuple( "_scheme", [ diff --git a/orangecanvas/scheme/tests/test_readwrite.py b/orangecanvas/scheme/tests/test_readwrite.py index 4435d687..e300bae4 100644 --- a/orangecanvas/scheme/tests/test_readwrite.py +++ b/orangecanvas/scheme/tests/test_readwrite.py @@ -81,6 +81,9 @@ def test_safe_evals(self): s = readwrite.string_eval(r"'\x00\xff'") self.assertEqual(s, chr(0) + chr(255)) + with self.assertRaises(ValueError): + readwrite.string_eval("3") + with self.assertRaises(ValueError): readwrite.string_eval("[1, 2]") @@ -96,10 +99,14 @@ def test_safe_evals(self): self.assertIs(readwrite.terminal_eval("True"), True) self.assertIs(readwrite.terminal_eval("False"), False) self.assertIs(readwrite.terminal_eval("None"), None) - self.assertEqual(readwrite.terminal_eval("42"), 42) + self.assertEqual(readwrite.terminal_eval("42."), 42.) self.assertEqual(readwrite.terminal_eval("'42'"), '42') self.assertEqual(readwrite.terminal_eval(r"b'\xff\x00'"), b'\xff\x00') + with self.assertRaises(ValueError): + readwrite.terminal_eval("...") + with self.assertRaises(ValueError): + readwrite.terminal_eval("{}") def test_literal_dump(self): struct = {1: [{(1, 2): ""}], diff --git a/setup.py b/setup.py index 4e36319b..cd871afa 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "qasync>=0.10.0", "importlib_metadata>=4.6; python_version<'3.10'", "importlib_resources; python_version<'3.9'", + "typing_extensions", "packaging", "numpy", ) @@ -59,7 +60,7 @@ "Documentation": "https://orange-canvas-core.readthedocs.io/en/latest/", } -PYTHON_REQUIRES = ">=3.6" +PYTHON_REQUIRES = ">=3.8" if __name__ == "__main__": setup(