diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 657fe4481..7a583cc15 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ on: - docs env: - PYTHON_VERSION: 3.12.3 + PYTHON_VERSION: 3.14.3 jobs: build: @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 @@ -34,7 +34,7 @@ jobs: poetry config virtualenvs.in-project true - name: Set up poetry cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .venv key: venv-${{ runner.os }}-py-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42cff9ce8..d69ca1233 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.4 + rev: v0.15.12 hooks: # Run the linter. - id: ruff diff --git a/poetry.lock b/poetry.lock index f8fb948fc..ca40be381 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "bump2version" @@ -1098,5 +1098,5 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.1" -python-versions = ">=3.11" -content-hash = "d4834e154177290ee17b2c82c065ff8a39079065d02e18af40fe4bea4c629fb2" +python-versions = ">=3.12" +content-hash = "aa734f52ed314d0af56c786d5eae67b0964d6d199359f642a10418b5cafd1866" diff --git a/pyproject.toml b/pyproject.toml index f52bc577d..387408eb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license-files = [ "LICENSE.txt" ] readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" keywords = ["compiler", "zxspectrum", "BASIC", "z80"] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -130,7 +130,7 @@ follow_imports = "skip" [tool.ruff] line-length = 120 -target-version = "py311" +target-version = "py314" exclude = [ ".venv/", "venv/", diff --git a/src/api/config.py b/src/api/config.py index 9923394e9..7321fed46 100644 --- a/src/api/config.py +++ b/src/api/config.py @@ -115,7 +115,7 @@ def load_config_from_file( try: cfg = configparser.ConfigParser() cfg.read(filename, encoding="utf-8") - except (configparser.DuplicateSectionError, configparser.DuplicateOptionError): + except configparser.DuplicateSectionError, configparser.DuplicateOptionError: errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields") if stop_on_error: sys.exit(1) @@ -157,7 +157,7 @@ def save_config_into_file( if os.path.exists(filename): try: cfg.read(filename, encoding="utf-8") - except (configparser.DuplicateSectionError, configparser.DuplicateOptionError): + except configparser.DuplicateSectionError, configparser.DuplicateOptionError: errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields") if stop_on_error: sys.exit(1) diff --git a/src/api/optimize.py b/src/api/optimize.py index c888d8f2f..41284cb6c 100644 --- a/src/api/optimize.py +++ b/src/api/optimize.py @@ -65,7 +65,7 @@ def filter_inorder( node, filter_func: Callable[[Any], bool], child_selector: Callable[[Ast], bool] = lambda x: True, - ) -> Generator[Ast, None, None]: + ) -> Generator[Ast]: """Visit the tree inorder, but only those that return true for filter_func and visiting children which return true for child_selector. """ diff --git a/src/api/symboltable/scope.py b/src/api/symboltable/scope.py index 9e938c120..621d98903 100644 --- a/src/api/symboltable/scope.py +++ b/src/api/symboltable/scope.py @@ -6,7 +6,6 @@ # -------------------------------------------------------------------- from collections import OrderedDict -from typing import Optional from src.api.config import OPTIONS from src.symbols.id_ import SymbolID @@ -37,7 +36,7 @@ class Scope: myFunct will be output as _myFunct_a. """ - def __init__(self, namespace: str = "", parent_scope: Optional["Scope"] = None): + def __init__(self, namespace: str = "", parent_scope: Scope | None = None): from src.symbols.funcdecl import SymbolFUNCDECL self.symbols: dict[str, SymbolID] = OrderedDict() diff --git a/src/api/utils.py b/src/api/utils.py index cf6173ebc..ec44f24b1 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -36,13 +36,13 @@ T = TypeVar("T") -def first(iter_: Iterable[T], default: T | None = None) -> T | None: +def first[T](iter_: Iterable[T], default: T | None = None) -> T | None: """Return the first element of an Iterable, or None if it's empty or there are no more elements to return.""" return next(iter(iter_), default) -def sfirst(iter_: Iterable[T]) -> T: +def sfirst[T](iter_: Iterable[T]) -> T: """Return the first element of an Iterable, or fails if it's empty""" return next(iter(iter_)) @@ -175,7 +175,7 @@ def eval_to_num(expr: str) -> int | float | None: if it was non-numeric.""" try: result = eval(expr, {}, {}) - except (NameError, SyntaxError, ValueError): + except NameError, SyntaxError, ValueError: return None if isinstance(result, int | float): diff --git a/src/arch/z80/optimizer/helpers.py b/src/arch/z80/optimizer/helpers.py index 9c041d2c2..ececec619 100644 --- a/src/arch/z80/optimizer/helpers.py +++ b/src/arch/z80/optimizer/helpers.py @@ -6,7 +6,7 @@ # -------------------------------------------------------------------- from collections.abc import Iterable, Mapping -from typing import Any, Final, TypeVar, cast +from typing import Any, Final, cast from . import patterns @@ -48,10 +48,6 @@ ) -T = TypeVar("T") -K = TypeVar("K") - - # All 'single' registers (even f FLAG one). SP is not decomposable so it's 'single' already ALL_REGS: Final[frozenset[str]] = frozenset( [ @@ -493,7 +489,7 @@ def HI16_val(x: int | str | None) -> str: return f"0{HL_SEP}{x}".split(HL_SEP)[-2] -def dict_intersection(dict_a: Mapping[K, T], dict_b: Mapping[K, T]) -> dict[K, T]: +def dict_intersection[K, T](dict_a: Mapping[K, T], dict_b: Mapping[K, T]) -> dict[K, T]: """Given 2 dictionaries a, b, returns a new one which contains the common key/pair values. e.g. for {'a': 1, 'b': 'x'}, {'a': 'q', 'b': 'x', 'c': 2} returns {'b': 'x'} diff --git a/src/arch/z80/peephole/engine.py b/src/arch/z80/peephole/engine.py index d76ff8e4c..484c6a2b5 100644 --- a/src/arch/z80/peephole/engine.py +++ b/src/arch/z80/peephole/engine.py @@ -77,7 +77,7 @@ def read_opt(opt_path: str) -> OptPattern | None: errmsg.warning(define_.lineno, "this template will be ignored", fpath) return None - except (ValueError, KeyError, TypeError): + except ValueError, KeyError, TypeError: errmsg.warning(1, "There is an error in this template and it will be ignored", fpath) else: MAXLEN = max(len(pattern_.patt), MAXLEN or 0) @@ -95,7 +95,7 @@ def read_opts(folder_path: str, result: list[OptPattern] | None = None) -> list[ try: files_to_read = [f for f in os.listdir(folder_path) if f.endswith(".opt")] - except (FileNotFoundError, NotADirectoryError, PermissionError): + except FileNotFoundError, NotADirectoryError, PermissionError: return result for fname in files_to_read: diff --git a/src/arch/z80/peephole/evaluator.py b/src/arch/z80/peephole/evaluator.py index 0182c5580..c01ebf02f 100644 --- a/src/arch/z80/peephole/evaluator.py +++ b/src/arch/z80/peephole/evaluator.py @@ -238,7 +238,7 @@ def eval(self, vars_: dict[str, Any] | None = None) -> str | Evaluator | list[An try: oper = FN(self.expression[0]) assert oper in UNARY - except (AssertionError, ValueError): + except AssertionError, ValueError: raise ValueError(f"Invalid unary operator '{self.expression[0]}'") operand = self.expression[1].eval(vars_) @@ -248,7 +248,7 @@ def eval(self, vars_: dict[str, Any] | None = None) -> str | Evaluator | list[An try: oper = FN(self.expression[1]) assert oper in BINARY - except (AssertionError, ValueError): + except AssertionError, ValueError: raise ValueError(f"Invalid binary operator '{self.expression[1]}'") # Do lazy evaluation diff --git a/src/ast_/ast.py b/src/ast_/ast.py index 4c1094afd..69da2bb68 100644 --- a/src/ast_/ast.py +++ b/src/ast_/ast.py @@ -28,14 +28,14 @@ def token(self): class NodeVisitor(GenericNodeVisitor[Ast]): def _visit(self, node: Ast): - meth: Callable[[Ast], Generator[Ast | Any, Any, None]] = getattr( + meth: Callable[[Ast], Generator[Ast | Any, Any]] = getattr( self, f"visit_{node.token}", self.generic_visit, ) return meth(node) - def generic_visit(self, node: Ast) -> Generator[Ast | Any, Any, None]: + def generic_visit(self, node: Ast) -> Generator[Ast | Any, Any]: for i, child in enumerate(node.children): node.children[i] = yield self.visit(child) diff --git a/src/ast_/visitor.py b/src/ast_/visitor.py index 106adbf56..e6aea3ffd 100644 --- a/src/ast_/visitor.py +++ b/src/ast_/visitor.py @@ -3,21 +3,19 @@ from abc import abstractmethod from collections.abc import Generator from types import GeneratorType -from typing import Final, Generic, NamedTuple, TypeVar +from typing import Final, NamedTuple __all__: Final[tuple[str, ...]] = ("GenericNodeVisitor",) -_T = TypeVar("_T") +class ToVisit[T](NamedTuple): + obj: T -class ToVisit(NamedTuple, Generic[_T]): - obj: _T - -class GenericNodeVisitor(Generic[_T]): - def visit(self, node: _T | None) -> _T | Generator[_T | None, None, None] | None: - stack: list[_T | GeneratorType] = [ToVisit[_T](node) if node is not None else None] - last_result: _T | None = None +class GenericNodeVisitor[T]: + def visit(self, node: T | None) -> T | Generator[T | None] | None: + stack: list[T | GeneratorType] = [ToVisit[T](node) if node is not None else None] + last_result: T | None = None while stack: try: @@ -36,7 +34,7 @@ def visit(self, node: _T | None) -> _T | Generator[_T | None, None, None] | None return last_result @abstractmethod - def _visit(self, node: _T): ... + def _visit(self, node: T): ... @abstractmethod - def generic_visit(self, node: _T) -> Generator[_T | None, None, None]: ... + def generic_visit(self, node: T) -> Generator[T | None]: ... diff --git a/src/zxbpp/prepro/id_.py b/src/zxbpp/prepro/id_.py index 3c397b749..322b40e2f 100644 --- a/src/zxbpp/prepro/id_.py +++ b/src/zxbpp/prepro/id_.py @@ -53,7 +53,7 @@ def __str__(self): return self.name @staticmethod - def __dumptable(table: "prepro.DefinesTable") -> None: + def __dumptable(table: prepro.DefinesTable) -> None: """Dumps table on screen for debugging purposes""" for k, v in table.table.items(): sys.stdout.write(f"{k}\t<--- {v} {type(v)}") diff --git a/src/zxbpp/prepro/macrocall.py b/src/zxbpp/prepro/macrocall.py index 17a302573..880dda9d6 100644 --- a/src/zxbpp/prepro/macrocall.py +++ b/src/zxbpp/prepro/macrocall.py @@ -7,7 +7,6 @@ import copy import re -from typing import Union from src.api.debug import __DEBUG__ from src.zxbpp import prepro @@ -26,7 +25,7 @@ class MacroCall: __slots__ = "callargs", "fname", "id_", "lineno", "table" - def __init__(self, fname: str, lineno: int, table: "prepro.DefinesTable", id_: Union["MacroCall", str], args=None): + def __init__(self, fname: str, lineno: int, table: prepro.DefinesTable, id_: MacroCall | str, args=None): """Initializes the object with the ID table, the ID name and optionally, the passed args. """ @@ -44,7 +43,7 @@ def eval(arg) -> str: """ return str(arg()) # Evaluate the arg (could be a macrocall) - def __call__(self, symbolTable: "prepro.DefinesTable" = None) -> str: + def __call__(self, symbolTable: prepro.DefinesTable | None = None) -> str: """Execute the macro call using LAZY evaluation""" if isinstance(self.id_, MacroCall): self.id_ = self.id_() @@ -102,7 +101,7 @@ def __call__(self, symbolTable: "prepro.DefinesTable" = None) -> str: tmp = id_(table, self) return tmp - def is_defined(self, symbolTable: "prepro.DefinesTable" = None) -> bool: + def is_defined(self, symbolTable: prepro.DefinesTable | None = None) -> bool: """True if this macro has been defined""" if symbolTable is None: symbolTable = self.table diff --git a/src/zxbpp/prepro/operators.py b/src/zxbpp/prepro/operators.py index 727b13c94..2589b5fd9 100644 --- a/src/zxbpp/prepro/operators.py +++ b/src/zxbpp/prepro/operators.py @@ -22,7 +22,7 @@ def __init__(self, fname: str, lineno: int, table: DefinesTable, left: MacroCall self.left = left self.right = right - def __call__(self, symbolTable: DefinesTable = None) -> str: + def __call__(self, symbolTable: DefinesTable | None = None) -> str: return self.left(symbolTable).rstrip() + self.right(symbolTable).lstrip() @@ -41,5 +41,5 @@ def stringize(s: str) -> str: s = s.replace('"', '""') return f'"{s}"' - def __call__(self, symbolTable: DefinesTable = None) -> str: + def __call__(self, symbolTable: DefinesTable | None = None) -> str: return self.stringize(self.macro_call(symbolTable)) diff --git a/tests/functional/cmdline/test_cmdline.txt b/tests/functional/cmdline/test_cmdline.txt index 3933a993d..3fcf536b1 100644 --- a/tests/functional/cmdline/test_cmdline.txt +++ b/tests/functional/cmdline/test_cmdline.txt @@ -3,9 +3,9 @@ >>> os.environ['COLUMNS'] = '80' >>> process_file('arch/zx48k/arrbase1.bas', ['-q', '-S', '-O --mmap arrbase1.map']) -usage: zxbc.py [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE] - [-T | -t | -A | -E | --parse-only | -f {asm,bin,ir,sna,tap,tzx,z80}] - [-B] [-a] [-S ORG] [-e STDERR] [--array-base ARRAY_BASE] +usage: zxbc.py [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE] [-T | -t | -A | -E | + --parse-only | -f {asm,bin,ir,sna,tap,tzx,z80}] [-B] [-a] + [-S ORG] [-e STDERR] [--array-base ARRAY_BASE] [--string-base STRING_BASE] [-Z] [-H HEAP_SIZE] [--heap-address HEAP_ADDRESS] [--debug-memory] [--debug-array] [--strict-bool] [--enable-break] [--explicit] [-D DEFINES] diff --git a/tests/functional/test.py b/tests/functional/test.py index 91a330149..930f29041 100755 --- a/tests/functional/test.py +++ b/tests/functional/test.py @@ -100,7 +100,7 @@ def __exit__(self, type_, value, traceback): if self.error_level or not self.keep_file: # command failure or remove file? try: os.unlink(self.fname) - except (OSError, FileNotFoundError): + except OSError, FileNotFoundError: pass # Ok. It might be that it wasn't created