From 159a39e117a1572f2f3fbed82cd26b16ee3d63b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:54:25 +0300 Subject: [PATCH 01/21] Improve semicolon interpolation --- tests/test_yabi.py | 11 +++++++++++ yabi/parser.py | 25 ++++++++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/test_yabi.py b/tests/test_yabi.py index dcd915c..299c2b4 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -167,6 +167,17 @@ def test_comment_with_semicolon(): def test_semicolon_parse(): assert to_pure_python("; while True {}") == "while True:\n pass\n" + assert ( + to_pure_python("if True: if True: pass; while True: pass") + == """ +if True: + if True: + pass + while True: + pass + """.strip() + + "\n\n\n" + ) def test_inline_comment(): diff --git a/yabi/parser.py b/yabi/parser.py index bb9634d..6e1fb1e 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -309,7 +309,7 @@ def __init__(self, tokens: Iterable[str]): self.result = None self.in_head = False self.after_colon = False - self.finish_on_nl = False + self.finish_on_nl = 0 self.capture_indent = False self.after_indent = False self.after_nl = True @@ -318,7 +318,6 @@ def __init__(self, tokens: Iterable[str]): self.seen_lambdas = 0 self.block_started = False self.skip = False - self.next_indent = None self.head_term = None self.tokens = [tok for tok in tokens if tok] self.i = 0 @@ -333,11 +332,6 @@ def parse(self): def _parse(self): while self.i < len(self.tokens): tok = self.tokens[self.i] - if self.next_indent is not None: - if not tok.isspace(): - self.i -= 1 - tok = self.next_indent - self.next_indent = None self.block_started = False if tok == "#": while ( @@ -347,19 +341,20 @@ def _parse(self): self.i += 1 continue if self.after_colon: + self.accept_keyword = True self._parse_after_colon(tok) elif self.after_indent: self._parse_after_indent(tok) elif self.after_nl: self._parse_after_nl(tok) - if tok == ";" and not self.finish_on_nl: + if tok == ";": tok = "\n" - self.next_indent = self.indent_stack[-1] or None - if tok == "\n": self.after_nl = True - if self.finish_on_nl: - self.finish_on_nl = False + elif tok == "\n": + self.after_nl = True + for _ in range(self.finish_on_nl): self.result.finish() + self.finish_on_nl = 0 elif tok == "async" and self._next_nonspace(self.i) == "lambda": self.async_lambda = True self.i += 1 @@ -405,7 +400,7 @@ def _parse_after_colon(self, tok: str): self.capture_indent = True elif not tok.isspace(): self.after_colon = False - self.finish_on_nl = True + self.finish_on_nl += 1 def _parse_after_indent(self, tok: str): self.after_indent = False @@ -553,9 +548,9 @@ def _parse_tok_in_braces_values(self, tok: str): f" opening parenthesis '{brace}'" ) if block_finished: - if self.finish_on_nl: - self.finish_on_nl = False + for _ in range(self.finish_on_nl): self.result.finish() + self.finish_on_nl = 0 self.result.finish() self.skip = True if self.indent_stack.pop() is not None: From 084f33aafd270d09546788de281e6683eda38f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:54:35 +0300 Subject: [PATCH 02/21] Do not append extra newline to input code --- yabi/parser.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/yabi/parser.py b/yabi/parser.py index 6e1fb1e..bb2bb23 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -377,12 +377,10 @@ def _parse(self): self.result.append(tok) self.i += 1 - for indent in self.indent_stack: - if indent is None: - raise SyntaxError(UNCLOSED_BLOCK_ERROR) - self.result.finish() - if not self.result.finished: + if any(indent is None for indent in self.indent_stack): raise SyntaxError(UNCLOSED_BLOCK_ERROR) + while not self.result.finished: + self.result.finish() def _next_nonspace(self, i: int) -> str | None: return next( @@ -563,7 +561,7 @@ def parse(tokens: Iterable[str]) -> Block: def _transform(code: str, python: bool) -> str: - result = parse(tokenize(code + "\n")) + result = parse(tokenize(code)) return result.unparse(python) From 18f8ce4d8a09dcfec31d90e2833ade5606e00cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:01:43 +0300 Subject: [PATCH 03/21] Do not add newlines after each block --- tests/test_yabi.py | 4 +--- yabi/parser.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_yabi.py b/tests/test_yabi.py index 299c2b4..563443c 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -157,7 +157,6 @@ def test_to_bython(): """.strip().replace( "!", "" ) - + "\n" ) @@ -166,7 +165,7 @@ def test_comment_with_semicolon(): def test_semicolon_parse(): - assert to_pure_python("; while True {}") == "while True:\n pass\n" + assert to_pure_python("; while True {}\n") == "while True:\n pass\n" assert ( to_pure_python("if True: if True: pass; while True: pass") == """ @@ -176,7 +175,6 @@ def test_semicolon_parse(): while True: pass """.strip() - + "\n\n\n" ) diff --git a/yabi/parser.py b/yabi/parser.py index bb2bb23..d27ef0d 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -135,7 +135,7 @@ def reindent(self, indent: str): ): del self.body[i - 1] i -= 1 - if i + 1 == len(self.body) or self.body[i + 1] != "\n": + if i + 1 < len(self.body) and self.body[i + 1] != "\n": self.body.insert(i + 1, "\n") elif self.body[i] == "\n": after_nl = True From bd41b639b441787c09474c69d2870af9a2c468f8 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:16:33 +0300 Subject: [PATCH 04/21] Update to dev PWCP --- yabi/console.py | 13 ++++--------- yabi/hooks.py | 20 ++++++++++++++++++++ yabi/runner.py | 18 ++++-------------- yabi/version.py | 3 +++ 4 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 yabi/hooks.py create mode 100644 yabi/version.py diff --git a/yabi/console.py b/yabi/console.py index 51d31a4..7b77050 100644 --- a/yabi/console.py +++ b/yabi/console.py @@ -2,24 +2,19 @@ import ast from code import InteractiveConsole -from pwcp.preprocessor import PyPreprocessor, PreprocessorError, preprocess +from pwcp.preprocessor import PreprocessorError, preprocess -from . import config -from .parser import UNCLOSED_BLOCK_ERROR, to_pure_python +from .parser import UNCLOSED_BLOCK_ERROR class YabiConsole(InteractiveConsole): def __init__(self): super().__init__() - self.preprocessor = PyPreprocessor( - disabled=not config.ENABLE_PREPROCESSING - ) + self.pwcp_data = {} def runsource(self, source, filename="", symbol="single") -> bool: try: - parsed = to_pure_python( - preprocess(source, filename, self.preprocessor)[0] - ) + parsed = preprocess(source, filename, self.pwcp_data)[0] except PreprocessorError: self.showtraceback() return False diff --git a/yabi/hooks.py b/yabi/hooks.py new file mode 100644 index 0000000..c4ff6cc --- /dev/null +++ b/yabi/hooks.py @@ -0,0 +1,20 @@ +from pwcp import PreprocessorHooks, PycType + +from .version import __version__ +from .parser import to_pure_python + + +class YabiHooks(PreprocessorHooks): + def __init__(self): + super().__init__("yabi") + + def process_source( + self, source: str, filename: str, state: None + ) -> tuple[str, None]: + return to_pure_python(source), None + + def create_pyc_data(self, data: None, pyc_type: PycType) -> dict: + return {"version": __version__} + + def validate_pyc_data(self, pyc: dict, pyc_type: PycType) -> bool: + return pyc["version"] == __version__ diff --git a/yabi/runner.py b/yabi/runner.py index ecf11e8..822260f 100644 --- a/yabi/runner.py +++ b/yabi/runner.py @@ -1,16 +1,14 @@ import os import sys import argparse -from importlib import metadata import pwcp from . import config -from .parser import to_pure_python +from .hooks import YabiHooks +from .version import __version__ -__version__ = metadata.version("yabi-bython") - parser = argparse.ArgumentParser( ( "python -m " + __package__ @@ -52,15 +50,7 @@ def main(args=sys.argv[1:]): args = parser.parse_args(args) pwcp.add_file_extension(config.EXTENSION) - - def preprocess(src, filename, preprocessor): - if not config.ENABLE_PREPROCESSING and filename.endswith( - config.EXTENSION - ): - preprocessor.disabled = True - return to_pure_python(orig_preprocess(src, filename, preprocessor)) - - orig_preprocess = pwcp.set_preprocessing_function(preprocess) + pwcp.add_hook(YabiHooks()) config.SAVE_FILES = args.save_files config.ENABLE_PREPROCESSING = args.enable_preprocessing @@ -71,5 +61,5 @@ def preprocess(src, filename, preprocessor): args.target = f"{__package__}.console" pwcp.main_with_params( - **vars(args), preprocess_unknown_sources=config.ENABLE_PREPROCESSING + **vars(args), skip_unknown_sources=not config.ENABLE_PREPROCESSING ) diff --git a/yabi/version.py b/yabi/version.py new file mode 100644 index 0000000..6aeeeac --- /dev/null +++ b/yabi/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("yabi-bython") From ef6f1dd93b4748630b83dd616510af4199f40c05 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:13:11 +0300 Subject: [PATCH 05/21] Add console preprocessing test --- tests/test_yabi.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_yabi.py b/tests/test_yabi.py index 563443c..a108582 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -126,6 +126,19 @@ def test_console_linecont(): check_console(code, expexted_output) +def test_console_preprocessing(): + code = ( + """ +#pragma pypp on +#define a 1 +a + """.strip() + + "\n" + ) + expexted_output = ">>> >>> >>> 1\n>>> " + check_console(code, expexted_output) + + def test_to_bython(): assert ( to_bython( From 4cfe5c5f21dfc3ba11fd72515d08e94468fb60c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:24:42 +0300 Subject: [PATCH 06/21] Rename --prefer-py to --prefer-python --- yabi/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yabi/runner.py b/yabi/runner.py index 822260f..5ee9aa2 100644 --- a/yabi/runner.py +++ b/yabi/runner.py @@ -25,7 +25,7 @@ "-c", action="store_true", help="run target as command line" ) parser.add_argument( - "--prefer-py", + "--prefer-python", dest="prefer_python", action="store_true", help="prefer .py files over .by when importing", From b092062cbb2a93bc52fe689c9b648ae568749a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:13:34 +0300 Subject: [PATCH 07/21] Properly set default preprocessing mode --- yabi/runner.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/yabi/runner.py b/yabi/runner.py index 5ee9aa2..9d6b0f9 100644 --- a/yabi/runner.py +++ b/yabi/runner.py @@ -49,13 +49,15 @@ def main(args=sys.argv[1:]): args = parser.parse_args(args) - pwcp.add_file_extension(config.EXTENSION) - pwcp.add_hook(YabiHooks()) - config.SAVE_FILES = args.save_files config.ENABLE_PREPROCESSING = args.enable_preprocessing del args.enable_preprocessing + pwcp.add_file_extension( + config.EXTENSION, preprocess=config.ENABLE_PREPROCESSING + ) + pwcp.add_hook(YabiHooks()) + if not args.target: args.m = True args.target = f"{__package__}.console" From 5e98af234a6200b55115e19d0a0759e51374e2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:21:09 +0300 Subject: [PATCH 08/21] Save preprocessing mode into `pyc`s --- yabi/hooks.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/yabi/hooks.py b/yabi/hooks.py index c4ff6cc..af0eb2e 100644 --- a/yabi/hooks.py +++ b/yabi/hooks.py @@ -1,5 +1,6 @@ from pwcp import PreprocessorHooks, PycType +from . import config from .version import __version__ from .parser import to_pure_python @@ -14,7 +15,13 @@ def process_source( return to_pure_python(source), None def create_pyc_data(self, data: None, pyc_type: PycType) -> dict: - return {"version": __version__} + return { + "version": __version__, + "preprocessing": config.ENABLE_PREPROCESSING, + } def validate_pyc_data(self, pyc: dict, pyc_type: PycType) -> bool: - return pyc["version"] == __version__ + return ( + pyc["version"] == __version__ + and pyc["preprocessing"] == config.ENABLE_PREPROCESSING + ) From 0a38eb8c8ba82c6c542f28c04a27a4d3b1a5ac18 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:22:02 +0300 Subject: [PATCH 09/21] Use ast.parse directly --- yabi/parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/yabi/parser.py b/yabi/parser.py index d27ef0d..d15f2ec 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -2,7 +2,6 @@ import ast import random from io import StringIO -from builtins import compile from string import ascii_letters from typing import Generator, Iterable from tokenize import NAME, OP, generate_tokens @@ -279,8 +278,7 @@ def _insert_into_line(line: str, index: int, part: str): def _add_return(code: str) -> str: - # same as `ast.parse`, but with non-overriden compile - tree = compile(code, "", "exec", ast.PyCF_ONLY_AST) + tree = ast.parse(code) last_node = tree.body[0].body[-1] if not isinstance(last_node, ast.Expr): return code From 3c83e6329e501e5ec89910c9168b90b429650b4c Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:39:25 +0300 Subject: [PATCH 10/21] Generate lambda names deterministically --- yabi/parser.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/yabi/parser.py b/yabi/parser.py index d15f2ec..9d1d262 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -1,8 +1,6 @@ from __future__ import annotations import ast -import random from io import StringIO -from string import ascii_letters from typing import Generator, Iterable from tokenize import NAME, OP, generate_tokens @@ -296,8 +294,14 @@ def _add_return(code: str) -> str: return "\n".join(code) +_lambda_count = 0 + + def _gen_lambda_name() -> str: - return "_yabi_lambda_" + "".join(random.choices(ascii_letters, k=16)) + global _lambda_count + + _lambda_count += 1 + return f"_yabi_lambda_{_lambda_count:016x}" class Parser: From 2d0b75439514a05f8b3e4f6e3ebeb264295aea6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:01:03 +0300 Subject: [PATCH 11/21] Rework yabi-convert invocation --- pyproject.toml | 2 +- yabi/__init__.py | 2 -- yabi/convert.py | 6 +++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5929306..d92b218 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ license = "MIT" license-files = ["LICENSE.md"] dependencies = ["pwcp>=0.12.2,<0.13"] optional-dependencies = {tests = ["pytest"]} -scripts = {yabi = "yabi:main", yabi-convert = "yabi:convert_main"} +scripts = {yabi = "yabi:main", yabi-convert = "yabi.convert:main"} [project.urls] Repository = "https://github.com/solaluset/yabi" diff --git a/yabi/__init__.py b/yabi/__init__.py index 2d4298b..bf3f33d 100644 --- a/yabi/__init__.py +++ b/yabi/__init__.py @@ -1,12 +1,10 @@ __all__ = ( "__version__", "main", - "convert_main", "to_bython", "to_pure_python", ) from .runner import __version__, main -from .convert import convert_main from .parser import to_bython, to_pure_python diff --git a/yabi/convert.py b/yabi/convert.py index 4fd21b0..662f7c1 100644 --- a/yabi/convert.py +++ b/yabi/convert.py @@ -16,8 +16,12 @@ parser.add_argument("target") -def convert_main(args=sys.argv[1:]): +def main(args=sys.argv[1:]): args = parser.parse_args(args) converter = to_pure_python if args.to_python else to_bython with open(args.target) as source: print(converter(source.read()).rstrip("\n")) + + +if __name__ == "__main__": + main() From 1f72da854924d92208ae0323b8a2a38710673706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:00:04 +0300 Subject: [PATCH 12/21] Rewrite C comments as Python comments when converting --- tests/test_yabi.py | 19 +++++++++++++++++++ yabi/parser.py | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_yabi.py b/tests/test_yabi.py index a108582..176886c 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -199,6 +199,25 @@ def test_inline_comment(): assert to_pure_python(code).strip() == code +def test_c_comment(): + assert to_pure_python("/* test */") == "# test" + assert ( + to_pure_python( + """ +/* one + more + test */ + """ + ) + == """ +# one +# more +# test + """.strip() + + "\n" + ) + + def test_preprocessing(): with open("tests/preprocess.by") as f: code = f.read() diff --git a/yabi/parser.py b/yabi/parser.py index 9d1d262..73b4cdc 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -65,6 +65,13 @@ def strip_spaces(tokens: list[str]) -> None: del tokens[-1] +def to_python_comment(part: str) -> str: + if not part.startswith("/*") or not part.endswith("*/"): + return part + part = part.replace("/*", "#", 1).removesuffix("*/").rstrip() + return part.replace("\n", "\n#") + + class Block: def __init__(self): self.head = [] @@ -180,7 +187,7 @@ def unparse(self, pure_python=True, depth=0) -> str: ( child.unparse(pure_python, depth + 1) if isinstance(child, Block) - else child + else (to_python_comment(child) if pure_python else child) ) for child in self.body ) From 78bf8ddc80e88833c46a4b81a2ccc59292f227fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:16:32 +0300 Subject: [PATCH 13/21] Add additional AST equality testing --- tests/test_yabi.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_yabi.py b/tests/test_yabi.py index 176886c..134ac1b 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -1,14 +1,16 @@ import os import sys +import ast from io import StringIO from unittest.mock import patch -from pytest import mark +from pytest import mark, fixture sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from yabi import config, main, to_bython, to_pure_python # noqa: E402 from yabi.console import YabiConsole # noqa: E402 +from yabi.parser import _transform # noqa: E402 sys.dont_write_bytecode = True @@ -16,6 +18,35 @@ sys.ps2 = getattr(sys, "ps2", "... ") +def _ok_err(func, *args): + try: + return func(*args), None + except Exception as e: + return None, e + + +@fixture(scope="session", autouse=True) +def checked_transform(): + def _transform_test(code: str, python: bool) -> str: + ret = _transform(code, python) + + result1 = _transform(ret, True) + result2 = ret if python else _transform(result1, True) + + ast1, err1 = _ok_err(ast.parse, result1) + ast2, err2 = _ok_err(ast.parse, result2) + # check if ASTs or their errors are equivalent + if ast1: + assert ast2 and ast.dump(ast1) == ast.dump(ast2) + else: + assert type(err1) is type(err2) and err1.args == err2.args + + return ret + + with patch("yabi.parser._transform", new=_transform_test): + yield + + def check_file(file, expexted_output, *args): with patch("sys.stdout", new=StringIO()): main(args + (file,)) From 1a155bb4dad352d2385661bf14775fe4f42e9808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:23:33 +0300 Subject: [PATCH 14/21] Move version constant to version.py --- pyproject.toml | 5 ++++- yabi/version.py | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d92b218..4919e23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "yabi-bython" -version = "0.9.3" +dynamic = ["version"] description = "Yet Another Bython (braced Python) Implementation" authors = [{name = "Sóla Łuset"}] readme = "README.md" @@ -19,3 +19,6 @@ Repository = "https://github.com/solaluset/yabi" [tool.hatch.build.targets.wheel] packages = ["yabi"] + +[tool.hatch.version] +path = "yabi/version.py" diff --git a/yabi/version.py b/yabi/version.py index 6aeeeac..c598173 100644 --- a/yabi/version.py +++ b/yabi/version.py @@ -1,3 +1 @@ -from importlib import metadata - -__version__ = metadata.version("yabi-bython") +__version__ = "0.9.3" From a7351801c45f9b61a44687cd810a1b6771b386c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:37:15 +0300 Subject: [PATCH 15/21] Improve console exception handling --- yabi/console.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/yabi/console.py b/yabi/console.py index 7b77050..9b3f557 100644 --- a/yabi/console.py +++ b/yabi/console.py @@ -15,15 +15,12 @@ def __init__(self): def runsource(self, source, filename="", symbol="single") -> bool: try: parsed = preprocess(source, filename, self.pwcp_data)[0] - except PreprocessorError: - self.showtraceback() - return False - except SyntaxError as e: + except (PreprocessorError, SyntaxError) as e: if e.args[0] == UNCLOSED_BLOCK_ERROR or e.args[0].startswith( "Unterminated" ): return True - self.showtraceback() + self.showsyntaxerror(filename) return False if not source.endswith("\n"): parsed = parsed.rstrip("\n") @@ -64,9 +61,6 @@ def _compiler(self, source, filename, symbol, original_source): return None try: return self.compile(source, filename, symbol) - except PreprocessorError: - self.showtraceback() - return self.compile("", filename, symbol) except SyntaxError: if "\n" in original_source and not source.endswith("\n"): return None From 7ecd41f1c99ef9f7ee89bde404e08e6b2f9c9bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B3la=20=C5=81uset?= <60041069+solaluset@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:07:26 +0300 Subject: [PATCH 16/21] Raise YabiSyntaxError instead of SyntaxError --- yabi/parser.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/yabi/parser.py b/yabi/parser.py index 73b4cdc..74fdc15 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -34,6 +34,10 @@ UNCLOSED_BLOCK_ERROR = "there is an unclosed block" +class YabiSyntaxError(SyntaxError): + pass + + def tokenize(text: str) -> Generator[str, None, None]: lexer = default_lexer() lexer.input(text) @@ -122,7 +126,7 @@ def finish(self): self.body[-1].finish() else: if self.finished: - raise SyntaxError("the block was already closed") + raise YabiSyntaxError("the block was already closed") self.finished = True def reindent(self, indent: str): @@ -387,7 +391,7 @@ def _parse(self): self.i += 1 if any(indent is None for indent in self.indent_stack): - raise SyntaxError(UNCLOSED_BLOCK_ERROR) + raise YabiSyntaxError(UNCLOSED_BLOCK_ERROR) while not self.result.finished: self.result.finish() @@ -460,7 +464,7 @@ def _parse_long_lambda(self, tok: str) -> bool: self.i += 1 return True if terminator != "{": - raise SyntaxError("async lambda must use braces") + raise YabiSyntaxError("async lambda must use braces") lambda_body = self._fully_parse_long_lambda() brace_stack_copy = [] for brace, is_block in reversed(self.brace_stack): @@ -495,7 +499,7 @@ def _fully_parse_long_lambda(self) -> Block: head.append(tok) self.i += 1 if brace_stack: - raise SyntaxError(UNCLOSED_BLOCK_ERROR) + raise YabiSyntaxError(UNCLOSED_BLOCK_ERROR) name = _gen_lambda_name() if self.in_head: @@ -547,10 +551,10 @@ def _parse_tok_in_braces_values(self, tok: str): try: brace, block_finished = self.brace_stack.pop() except IndexError as e: - raise SyntaxError(f"unmatched '{tok}'") from e + raise YabiSyntaxError(f"unmatched '{tok}'") from e self.accept_keyword = block_finished if BRACES[brace] != tok: - raise SyntaxError( + raise YabiSyntaxError( f"closing parenthesis '{tok}' does not match" f" opening parenthesis '{brace}'" ) @@ -561,7 +565,7 @@ def _parse_tok_in_braces_values(self, tok: str): self.result.finish() self.skip = True if self.indent_stack.pop() is not None: - raise SyntaxError("indented block was not properly closed") + raise YabiSyntaxError("indented block was not properly closed") def parse(tokens: Iterable[str]) -> Block: From fbdc6f0b83101bf7ec192653cda19140ec156525 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:49:50 +0300 Subject: [PATCH 17/21] Bump PWCP version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4919e23..d5d121d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [{name = "Sóla Łuset"}] readme = "README.md" license = "MIT" license-files = ["LICENSE.md"] -dependencies = ["pwcp>=0.12.2,<0.13"] +dependencies = ["pwcp~=0.13.0"] optional-dependencies = {tests = ["pytest"]} scripts = {yabi = "yabi:main", yabi-convert = "yabi.convert:main"} From 597348c60830fdeefc1c22f9e69c5735d6db98be Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:50:42 +0300 Subject: [PATCH 18/21] Use ruff instead of black --- .pre-commit-config.yaml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9905a2..c02d0b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,15 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files -- repo: https://github.com/psf/black - rev: 25.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.2 hooks: - - id: black - args: ["-l", "79"] - -- repo: https://github.com/pycqa/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - args: ["--ignore", "E203,W503"] + - id: ruff-check + args: [ --fix ] + - id: ruff-format From a8f4a16444a8f0ded183f70d74fac82b79b2e44a Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:55:41 +0300 Subject: [PATCH 19/21] Reformat with ruff --- ruff.toml | 1 + tests/test_yabi.py | 26 +++++++++++--------------- yabi/__init__.py | 2 +- yabi/__main__.py | 1 - yabi/console.py | 6 +++--- yabi/convert.py | 5 ++--- yabi/hooks.py | 2 +- yabi/parser.py | 6 +++--- yabi/runner.py | 3 +-- 9 files changed, 23 insertions(+), 29 deletions(-) create mode 100644 ruff.toml diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..28d6185 --- /dev/null +++ b/ruff.toml @@ -0,0 +1 @@ +line-length = 79 diff --git a/tests/test_yabi.py b/tests/test_yabi.py index 134ac1b..84933d2 100644 --- a/tests/test_yabi.py +++ b/tests/test_yabi.py @@ -1,17 +1,16 @@ +import ast import os import sys -import ast from io import StringIO from unittest.mock import patch -from pytest import mark, fixture +from pytest import fixture, mark sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from yabi import config, main, to_bython, to_pure_python # noqa: E402 -from yabi.console import YabiConsole # noqa: E402 -from yabi.parser import _transform # noqa: E402 - +from yabi import config, main, to_bython, to_pure_python +from yabi.console import YabiConsole +from yabi.parser import _transform sys.dont_write_bytecode = True sys.ps1 = getattr(sys, "ps1", ">>> ") @@ -21,7 +20,7 @@ def _ok_err(func, *args): try: return func(*args), None - except Exception as e: + except Exception as e: # noqa: BLE001 return None, e @@ -54,8 +53,9 @@ def check_file(file, expexted_output, *args): def check_console(code, expexted_output): - with patch("sys.stdout", new=StringIO()), patch( - "sys.stdin", new=StringIO(code) + with ( + patch("sys.stdout", new=StringIO()), + patch("sys.stdin", new=StringIO(code)), ): YabiConsole().interact() assert sys.stdout.getvalue() == expexted_output @@ -182,9 +182,7 @@ def test_to_bython(): ! /* these comments too */ print("No") - """.strip().replace( - "!", "" - ) + """.strip().replace("!", "") ) == """ for (i in {1, 2, 3}) { @@ -198,9 +196,7 @@ def test_to_bython(): print("No") } } - """.strip().replace( - "!", "" - ) + """.strip().replace("!", "") ) diff --git a/yabi/__init__.py b/yabi/__init__.py index bf3f33d..a90f896 100644 --- a/yabi/__init__.py +++ b/yabi/__init__.py @@ -6,5 +6,5 @@ ) -from .runner import __version__, main from .parser import to_bython, to_pure_python +from .runner import __version__, main diff --git a/yabi/__main__.py b/yabi/__main__.py index e7d3e76..8273c4f 100644 --- a/yabi/__main__.py +++ b/yabi/__main__.py @@ -1,4 +1,3 @@ from . import main - main() diff --git a/yabi/console.py b/yabi/console.py index 9b3f557..ac6eb96 100644 --- a/yabi/console.py +++ b/yabi/console.py @@ -1,5 +1,5 @@ -import sys import ast +import sys from code import InteractiveConsole from pwcp.preprocessor import PreprocessorError, preprocess @@ -48,11 +48,11 @@ def runsource(self, source, filename="", symbol="single") -> bool: def runcode(self, code) -> bool: try: - exec(code, self.locals) + exec(code, self.locals) # noqa: S102 return True except SystemExit: raise - except BaseException: + except BaseException: # noqa: BLE001 self.showtraceback() return False diff --git a/yabi/convert.py b/yabi/convert.py index 662f7c1..6180b53 100644 --- a/yabi/convert.py +++ b/yabi/convert.py @@ -1,8 +1,7 @@ -import sys import argparse +import sys -from .parser import to_pure_python, to_bython - +from .parser import to_bython, to_pure_python parser = argparse.ArgumentParser( description="Python <==> Bython converter", diff --git a/yabi/hooks.py b/yabi/hooks.py index af0eb2e..daa67ee 100644 --- a/yabi/hooks.py +++ b/yabi/hooks.py @@ -1,8 +1,8 @@ from pwcp import PreprocessorHooks, PycType from . import config -from .version import __version__ from .parser import to_pure_python +from .version import __version__ class YabiHooks(PreprocessorHooks): diff --git a/yabi/parser.py b/yabi/parser.py index 74fdc15..8166c37 100644 --- a/yabi/parser.py +++ b/yabi/parser.py @@ -1,12 +1,12 @@ from __future__ import annotations + import ast +from collections.abc import Generator, Iterable from io import StringIO -from typing import Generator, Iterable from tokenize import NAME, OP, generate_tokens from pypp.parser import default_lexer - KEYWORDS = { "async", "class", @@ -508,7 +508,7 @@ def _fully_parse_long_lambda(self) -> Block: self.result.append(name) strip_spaces(head) - if not head or not head[0] == "(" or not head[-1] == ")": + if not head or head[0] != "(" or head[-1] != ")": head.insert(0, "(") head.append(")") head = ["def", " ", name] + head diff --git a/yabi/runner.py b/yabi/runner.py index 9d6b0f9..b835d7d 100644 --- a/yabi/runner.py +++ b/yabi/runner.py @@ -1,6 +1,6 @@ +import argparse import os import sys -import argparse import pwcp @@ -8,7 +8,6 @@ from .hooks import YabiHooks from .version import __version__ - parser = argparse.ArgumentParser( ( "python -m " + __package__ From 9efe520adb478417ba2f5397b8e1e0adc16cf549 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:14:52 +0300 Subject: [PATCH 20/21] Bump minimal Python version to 3.9 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4376256..17563af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runners: '[ "ubuntu-24.04" ]' - min-version: "3.8" + min-version: "3.9" include-pre-releases: true implementations: '["CPython", "PyPy"]' From b304e9c903b0f51f49714bcc6657a304e339b7e9 Mon Sep 17 00:00:00 2001 From: solaluset <60041069+solaluset@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:47:08 +0300 Subject: [PATCH 21/21] Bump version to 0.10.0 --- yabi/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yabi/version.py b/yabi/version.py index c598173..61fb31c 100644 --- a/yabi/version.py +++ b/yabi/version.py @@ -1 +1 @@ -__version__ = "0.9.3" +__version__ = "0.10.0"