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"