20 Commits

Author SHA1 Message Date
85e3837117 Merge pull request 'develop merge' (#5) from develop into main
Reviewed-on: KeshavAnandCode/foreign-thon#5
2026-05-18 15:59:09 +00:00
15f367131e bumped versions 2026-05-18 15:58:09 +00:00
83d8552977 Merge pull request 'feat/list-langs' (#4) from feat/list-langs into develop
Reviewed-on: KeshavAnandCode/foreign-thon#4
2026-05-18 15:56:55 +00:00
606501d654 readme fix 2026-05-18 15:56:17 +00:00
d9c5042ec7 added langs command utility 2026-05-18 15:54:48 +00:00
c19d5b4c15 Merge pull request 'Bump Versions to 0.5.0, adding the cli new command with custom language creation ability...similar to cargo new' (#3) from develop into main
Reviewed-on: KeshavAnandCode/foreign-thon#3
2026-05-17 23:35:49 +00:00
2701271973 bumping versions with full fix 2026-05-17 18:34:34 -05:00
7a2651b775 Merge pull request 'Added new features for project creation' (#2) from fpy-project into develop
Reviewed-on: KeshavAnandCode/foreign-thon#2
2026-05-17 23:31:41 +00:00
578f8c9761 added custom functionality and working with new toml as well, and update cli functions 2026-05-17 18:30:32 -05:00
6dcaeac748 added template.json 2026-05-17 18:15:13 -05:00
add744cd68 added new project cli with custom json reading 2026-05-17 18:08:59 -05:00
70a220158c bumped versions 2026-05-16 18:29:21 -05:00
15b91d0f6d fix spacing: verbatim inter-token copy, fix postfix decompile, add integration tests 2026-05-16 18:27:56 -05:00
8f99503d6b fixed ci workflow not install tamil 2026-05-16 16:55:30 -05:00
2041c167cb fixed toml for es 2026-05-16 16:43:34 -05:00
6dc44dc5bc changed tests 2026-05-16 16:38:37 -05:00
1bb09774d9 upgraded esp to 0.3.0 2026-05-16 16:36:28 -05:00
862aeaebbc Merge pull request 'updated spanish translations' (#1) from spanish-translation-fix into main
Reviewed-on: KeshavAnandCode/foreign-thon#1
2026-05-16 21:32:26 +00:00
CT
084f05a2c1 updated spanish translations 2026-05-16 16:31:09 -05:00
ef87091391 fixed some tamil traits 2026-05-16 12:34:48 -05:00
12 changed files with 968 additions and 184 deletions

View File

@@ -22,6 +22,8 @@ jobs:
run: | run: |
pip install -e "packages/foreignthon[dev]" pip install -e "packages/foreignthon[dev]"
pip install -e packages/langs/es pip install -e packages/langs/es
pip install -e packages/langs/ta
continue-on-error: true continue-on-error: true
- name: Run tests - name: Run tests

View File

@@ -6,10 +6,10 @@ ForeignThon transpiles `.es.py`, `.ta.py` (and more) into standard Python. Keywo
```python ```python
# hola.es.py # hola.es.py
definir saludar(nombre): def saludar(nombre):
retornar f"Hola, {nombre}!" retornar f"Hola, {nombre}!"
para i en rango(3): para i en dist(3):
imprimir(saludar(f"mundo {i}")) imprimir(saludar(f"mundo {i}"))
``` ```

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "foreignthon" name = "foreignthon"
version = "0.4.0" version = "0.5.1"
description = "Write Python in any language. Transpiles foreign-language .xx.py files to standard Python." description = "Write Python in any language. Transpiles foreign-language .xx.py files to standard Python."
license = { text = "GPL v3" } license = { text = "GPL v3" }
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -56,3 +56,6 @@ ignore = ["E501"]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"pack.py" = ["E501"] "pack.py" = ["E501"]
[tool.hatch.build.targets.wheel.force-include]
"src/foreignthon/template.json" = "foreignthon/template.json"

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -11,6 +12,72 @@ from .transpiler import run_transpiled, transpile_file
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
def _load_effective_pack(project: Path, lang: str) -> dict:
"""
Load pack for a file. Priority:
1. custom_pack in .foreignthon.toml — no installed pack needed
2. installed pack via entry points
"""
import json
from .pack import load_pack
# Walk up to find .foreignthon.toml
search = project if project.is_dir() else project.parent
toml_path = None
for parent in [search, *search.parents]:
candidate = parent / ".foreignthon.toml"
if candidate.exists():
toml_path = candidate
break
# Check for custom_pack first — if found, load it directly
# and merge on top of template (no installed pack required)
if toml_path:
for line in toml_path.read_text(encoding="utf-8").splitlines():
if line.strip().startswith("custom_pack") and not line.strip().startswith("#"):
custom_path = toml_path.parent / line.split("=", 1)[1].strip().strip('"').strip("'")
if custom_path.exists():
custom = json.loads(custom_path.read_text(encoding="utf-8"))
# If custom pack has meta with a code, it's a standalone pack
if custom.get("meta", {}).get("code"):
return custom
# Otherwise it's an override — merge on top of installed pack
pack = load_pack(lang)
for section in ("keywords", "builtins", "exceptions", "stdlib", "error_messages"):
if section in custom:
pack[section] = {**pack.get(section, {}), **custom[section]}
if "postfix_keywords" in custom:
pack["postfix_keywords"] = custom["postfix_keywords"]
return pack
break
# Fall back to installed pack
return load_pack(lang)
def _pick(mapping: dict, english: str, fallback: str) -> str:
reverse = {v: k for k, v in mapping.items()}
return reverse.get(english, fallback)
def _git_init(project: Path) -> None:
git_ok = subprocess.run(["git", "--version"], capture_output=True).returncode == 0
if git_ok:
subprocess.run(["git", "init"], cwd=project, capture_output=True)
subprocess.run(["git", "add", "."], cwd=project, capture_output=True)
subprocess.run(
["git", "commit", "-m", "initial commit"],
cwd=project, capture_output=True,
)
def _lang_from_file(path: Path) -> str:
suffixes = path.suffixes
if len(suffixes) >= 2 and suffixes[-1] == ".py":
return suffixes[-2].lstrip(".")
return "en"
@click.group(context_settings=CONTEXT_SETTINGS) @click.group(context_settings=CONTEXT_SETTINGS)
@click.version_option(__version__, prog_name="fpy") @click.version_option(__version__, prog_name="fpy")
@@ -19,6 +86,115 @@ def main():
pass pass
@main.command(context_settings=CONTEXT_SETTINGS)
@click.argument("name", default="")
@click.option("--lang", "-l", required=True, help="Language code (e.g. es, ta)")
@click.option("--no-git", is_flag=True, help="Skip git init")
def new(name: str, lang: str, no_git: bool):
"""
Create a new ForeignThon project.
\b
fpy new myproject --lang es # creates myproject/
fpy new --lang es # initializes current directory
"""
from .pack import PackNotFoundError
if name:
project = Path(name)
if project.exists():
click.echo(f"'{name}' already exists.", err=True)
raise SystemExit(1)
project.mkdir(parents=True)
else:
project = Path.cwd()
if any(project.iterdir()):
click.echo("✗ Current directory is not empty.", err=True)
raise SystemExit(1)
is_custom = lang == "custom"
if is_custom:
lang_code = click.prompt("Language code (e.g. ru, fr, de)")
lang_name_en = click.prompt("Language name in English (e.g. Russian)")
lang_name_native = click.prompt("Language name in its own script (e.g. Русский)")
pack = _make_scaffold_pack(lang_code, lang_name_en, lang_name_native)
lang = lang_code
else:
try:
pack = _load_effective_pack(project, lang)
except PackNotFoundError:
click.echo(f"✗ Language pack '{lang}' not installed.", err=True)
click.echo(f" Run: pip install foreignthon-{lang}", err=True)
raise SystemExit(1)
lang_name = pack["meta"]["native_name"]
if is_custom:
import json
(project / "custom.json").write_text(
json.dumps(pack, ensure_ascii=False, indent=2),
encoding="utf-8",
)
toml_custom_line = 'custom_pack = "custom.json"'
click.echo(" Created custom.json — fill in your translations!")
else:
toml_custom_line = '# custom_pack = "custom.json"'
(project / ".foreignthon.toml").write_text(
f'[foreignthon]\n'
f'lang = "{lang}"\n'
f'\n'
f'# Optional: path to a local JSON that overrides pack keywords\n'
f'{toml_custom_line}\n',
encoding="utf-8",
)
(project / ".gitignore").write_text(
"__pycache__/\n"
"*.py[cod]\n"
"*.egg-info/\n"
"dist/\n"
"build/\n"
".venv/\n"
".pytest_cache/\n"
".DS_Store\n"
"Thumbs.db\n",
encoding="utf-8",
)
src = project / "src"
src.mkdir(exist_ok=True)
bi_print = _pick(pack["builtins"], "print", "print")
(src / f"main.{lang}.py").write_text(
f"# Hello, World! in {lang_name}\n"
f'{bi_print}("Hello, World!")\n',
encoding="utf-8",
)
(project / "README.md").write_text(
f"# {name or project.name}\n\n"
f"A ForeignThon project in {lang_name}.\n\n"
f"## Run\n\n"
f"```bash\n"
f"fpy run src/main.{lang}.py\n"
f"```\n\n"
f"## Custom pack override\n\n"
f"Create a `custom.json` and set `custom_pack = \"custom.json\"` "
f"in `.foreignthon.toml` to add or override keywords locally.\n",
encoding="utf-8",
)
if not no_git:
_git_init(project)
click.echo(f"✓ Created '{name or '.'}' [{lang_name}]")
if name:
click.echo(f" cd {name}")
click.echo(f" fpy run src/main.{lang}.py")
@main.command(context_settings=CONTEXT_SETTINGS) @main.command(context_settings=CONTEXT_SETTINGS)
@click.argument("file", type=click.Path(exists=True, path_type=Path)) @click.argument("file", type=click.Path(exists=True, path_type=Path))
@click.option("--lang", "-l", default=None, help="Override language code (e.g. es, ta)") @click.option("--lang", "-l", default=None, help="Override language code (e.g. es, ta)")
@@ -34,7 +210,8 @@ def run(file: Path, lang: str | None, keep: bool):
detected_lang = lang or _lang_from_file(file) detected_lang = lang or _lang_from_file(file)
activate(detected_lang) activate(detected_lang)
transpiled = transpile_file(file) pack = _load_effective_pack(file, detected_lang)
transpiled = transpile_file(file, pack=pack)
if keep: if keep:
out_path = file.with_suffix("").with_suffix(".compiled.py") out_path = file.with_suffix("").with_suffix(".compiled.py")
@@ -54,14 +231,14 @@ def compile(file: Path, output: str | None):
""" """
Transpile a foreign-language file to standard Python. Transpile a foreign-language file to standard Python.
Output can be a file path or a directory:
\b \b
fpy compile script.es.py # → script.compiled.py fpy compile script.es.py # → script.compiled.py
fpy compile script.es.py -o out/ # → out/script.compiled.py fpy compile script.es.py -o out/ # → out/script.compiled.py
fpy compile script.es.py -o out.py # → out.py fpy compile script.es.py -o out.py # → out.py
""" """
transpiled = transpile_file(file) detected_lang = _lang_from_file(file)
pack = _load_effective_pack(file, detected_lang)
transpiled = transpile_file(file, pack=pack)
if output is None: if output is None:
out_path = file.with_suffix("").with_suffix(".compiled.py") out_path = file.with_suffix("").with_suffix(".compiled.py")
@@ -84,8 +261,10 @@ def check(file: Path):
"""Validate a foreign-language file without running it.""" """Validate a foreign-language file without running it."""
import ast import ast
detected_lang = _lang_from_file(file)
try: try:
transpiled = transpile_file(file) pack = _load_effective_pack(file, detected_lang)
transpiled = transpile_file(file, pack=pack)
ast.parse(transpiled) ast.parse(transpiled)
click.echo(f"{file.name} looks good.") click.echo(f"{file.name} looks good.")
except SyntaxError as e: except SyntaxError as e:
@@ -95,7 +274,25 @@ def check(file: Path):
click.echo(f"{e}", err=True) click.echo(f"{e}", err=True)
sys.exit(1) sys.exit(1)
@main.command(context_settings=CONTEXT_SETTINGS)
def langs():
"""List all installed language packs."""
from .pack import _discover_packs
packs = _discover_packs()
if not packs:
click.echo("No language packs installed.")
click.echo("Try: pip install foreignthon-es")
return
click.echo("Installed language packs:")
for code, module in sorted(packs.items()):
import json
data = json.loads(module.get_pack_path().read_text(encoding="utf-8"))
name = data["meta"].get("name", code)
native = data["meta"].get("native_name", "")
click.echo(f" {code:<6} {name} ({native})")
@main.command("pack", context_settings=CONTEXT_SETTINGS) @main.command("pack", context_settings=CONTEXT_SETTINGS)
@click.argument("json_file", type=click.Path(exists=True, path_type=Path)) @click.argument("json_file", type=click.Path(exists=True, path_type=Path))
def validate_pack(json_file: Path): def validate_pack(json_file: Path):
@@ -115,7 +312,6 @@ def validate_pack(json_file: Path):
click.echo(f"✓ Pack '{data['meta']['name']}' is valid.") click.echo(f"✓ Pack '{data['meta']['name']}' is valid.")
@main.command(context_settings=CONTEXT_SETTINGS) @main.command(context_settings=CONTEXT_SETTINGS)
@click.argument("file", type=click.Path(exists=True, path_type=Path)) @click.argument("file", type=click.Path(exists=True, path_type=Path))
@click.option("--lang", "-l", required=True, help="Target language code (e.g. es, ta)") @click.option("--lang", "-l", required=True, help="Target language code (e.g. es, ta)")
@@ -125,8 +321,6 @@ def decompile(file: Path, lang: str, postfix: bool, output: str | None):
""" """
Convert standard Python back to a foreign language. Convert standard Python back to a foreign language.
Keywords and builtins are translated. Variable names are untouched.
\b \b
fpy decompile script.py --lang es fpy decompile script.py --lang es
fpy decompile script.py --lang ta --postfix fpy decompile script.py --lang ta --postfix
@@ -134,7 +328,8 @@ def decompile(file: Path, lang: str, postfix: bool, output: str | None):
""" """
from .transpiler import detranspile_file from .transpiler import detranspile_file
result = detranspile_file(file, lang, postfix=postfix) pack = _load_effective_pack(file, lang)
result = detranspile_file(file, lang, postfix=postfix, pack=pack)
ext = f".{lang}.py" ext = f".{lang}.py"
@@ -154,9 +349,17 @@ def decompile(file: Path, lang: str, postfix: bool, output: str | None):
click.echo(f"Decompiled: {out_path}") click.echo(f"Decompiled: {out_path}")
def _lang_from_file(path: Path) -> str: def _make_scaffold_pack(lang_code: str, lang_name: str, native_name: str) -> dict:
suffixes = path.suffixes """
if len(suffixes) >= 2 and suffixes[-1] == ".py": Load template.json — single source of truth for all pack keys.
return suffixes[-2].lstrip(".") To add new keywords/builtins, edit template.json only.
return "en" """
import json
from importlib.resources import files
template_path = files("foreignthon") / "template.json"
pack = json.loads(template_path.read_text(encoding="utf-8"))
pack["meta"]["name"] = lang_name
pack["meta"]["native_name"] = native_name
pack["meta"]["code"] = lang_code
return pack

View File

@@ -0,0 +1,146 @@
{
"meta": {
"name": "",
"native_name": "",
"code": "",
"version": "0.1.0",
"authors": []
},
"keywords": {
"if": "if",
"else": "else",
"elif": "elif",
"for": "for",
"while": "while",
"def": "def",
"class": "class",
"import": "import",
"from": "from",
"as": "as",
"return": "return",
"break": "break",
"continue": "continue",
"pass": "pass",
"try": "try",
"except": "except",
"finally": "finally",
"raise": "raise",
"with": "with",
"in": "in",
"is": "is",
"and": "and",
"or": "or",
"not": "not",
"del": "del",
"global": "global",
"nonlocal": "nonlocal",
"assert": "assert",
"yield": "yield",
"await": "await",
"async": "async",
"lambda": "lambda",
"True": "True",
"False": "False",
"None": "None"
},
"builtins": {
"print": "print",
"input": "input",
"len": "len",
"range": "range",
"type": "type",
"int": "int",
"float": "float",
"str": "str",
"list": "list",
"dict": "dict",
"set": "set",
"tuple": "tuple",
"bool": "bool",
"open": "open",
"enumerate": "enumerate",
"map": "map",
"filter": "filter",
"sorted": "sorted",
"reversed": "reversed",
"sum": "sum",
"min": "min",
"max": "max",
"abs": "abs",
"round": "round",
"all": "all",
"any": "any",
"isinstance": "isinstance",
"hasattr": "hasattr",
"getattr": "getattr",
"setattr": "setattr",
"repr": "repr",
"format": "format",
"vars": "vars",
"next": "next",
"id": "id",
"chr": "chr",
"hex": "hex",
"bin": "bin",
"oct": "oct"
},
"exceptions": {
"Exception": "Exception",
"BaseException": "BaseException",
"ValueError": "ValueError",
"TypeError": "TypeError",
"KeyError": "KeyError",
"IndexError": "IndexError",
"AttributeError": "AttributeError",
"NameError": "NameError",
"ImportError": "ImportError",
"OSError": "OSError",
"FileNotFoundError": "FileNotFoundError",
"RuntimeError": "RuntimeError",
"StopIteration": "StopIteration",
"SystemExit": "SystemExit",
"KeyboardInterrupt": "KeyboardInterrupt",
"NotImplementedError": "NotImplementedError",
"ZeroDivisionError": "ZeroDivisionError",
"RecursionError": "RecursionError",
"SyntaxError": "SyntaxError",
"AssertionError": "AssertionError",
"OverflowError": "OverflowError",
"MemoryError": "MemoryError",
"PermissionError": "PermissionError",
"TimeoutError": "TimeoutError"
},
"error_messages": {
"SyntaxError": "SyntaxError",
"ValueError": "ValueError",
"TypeError": "TypeError",
"KeyError": "KeyError",
"IndexError": "IndexError",
"AttributeError": "AttributeError",
"NameError": "NameError",
"ImportError": "ImportError",
"FileNotFoundError": "FileNotFoundError",
"ZeroDivisionError": "ZeroDivisionError",
"RecursionError": "RecursionError",
"RuntimeError": "RuntimeError",
"MemoryError": "MemoryError",
"OverflowError": "OverflowError",
"AssertionError": "AssertionError",
"NotImplementedError": "NotImplementedError",
"StopIteration": "StopIteration",
"KeyboardInterrupt": "KeyboardInterrupt",
"PermissionError": "PermissionError",
"TimeoutError": "TimeoutError"
},
"stdlib": {
"math": "math",
"sys": "sys",
"datetime": "datetime",
"time": "time",
"random": "random",
"collections": "collections",
"pathlib": "pathlib",
"re": "re"
},
"postfix_keywords": []
}

View File

@@ -37,10 +37,6 @@ def _apply_postfix_syntax(source: str, mapping: dict) -> str:
def _apply_postfix_output(source: str, en_to_foreign: dict, postfix_english: set) -> str: def _apply_postfix_output(source: str, en_to_foreign: dict, postfix_english: set) -> str:
"""
Post-pass for decompile: rewrite foreign keyword lines to @@ postfix.
postfix_english comes from the language pack's postfix_keywords list.
"""
postfix_foreign = {en_to_foreign[k] for k in postfix_english if k in en_to_foreign} postfix_foreign = {en_to_foreign[k] for k in postfix_english if k in en_to_foreign}
lines = source.splitlines(keepends=True) lines = source.splitlines(keepends=True)
@@ -66,32 +62,37 @@ def _apply_postfix_output(source: str, en_to_foreign: dict, postfix_english: set
return "".join(result) return "".join(result)
def transpile(source: str, lang_code: str) -> str: def _get_slice(source_lines: list[str], sr: int, sc: int, er: int, ec: int) -> str:
pack = load_pack(lang_code) n = len(source_lines)
if sr > n:
return ""
if sr == er:
line = source_lines[sr - 1]
return line[sc:min(ec, len(line))]
parts = []
parts.append(source_lines[sr - 1][sc:])
for r in range(sr, er - 1):
if r < n:
parts.append(source_lines[r])
if er <= n:
parts.append(source_lines[er - 1][:ec])
return "".join(parts)
mapping: dict[str, str] = {}
mapping.update(pack["keywords"])
mapping.update(pack["builtins"])
mapping.update(pack["exceptions"])
mapping.update(pack["stdlib"])
source = _apply_postfix_syntax(source, mapping) def _swap_tokens(source: str, mapping: dict) -> str:
source_lines = source.splitlines(keepends=True)
tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
tokens_in = tokenize.generate_tokens(io.StringIO(source).readline) result = []
result: list[str] = []
prev_end = (1, 0) prev_end = (1, 0)
for tok in tokens_in: for tok_type, tok_string, tok_start, tok_end, _ in tokens:
tok_type, tok_string, tok_start, tok_end, _ = tok if tok_type in (tokenize.ENDMARKER, tokenize.ENCODING):
break
start_row, start_col = tok_start s_row, s_col = tok_start
end_row, end_col = prev_end gap = _get_slice(source_lines, prev_end[0], prev_end[1], s_row, s_col)
result.append(gap)
if start_row == end_row:
result.append(" " * (start_col - end_col))
else:
result.append("\n" * (start_row - end_row))
result.append(" " * start_col)
if tok_type == tokenize.NAME and tok_string in mapping: if tok_type == tokenize.NAME and tok_string in mapping:
result.append(mapping[tok_string]) result.append(mapping[tok_string])
@@ -103,57 +104,51 @@ def transpile(source: str, lang_code: str) -> str:
return "".join(result) return "".join(result)
def detranspile(source: str, lang_code: str, postfix: bool = False) -> str: def _build_mapping(pack: dict) -> dict:
pack = load_pack(lang_code) mapping: dict[str, str] = {}
mapping.update(pack["keywords"])
mapping.update(pack["builtins"])
mapping.update(pack["exceptions"])
mapping.update(pack["stdlib"])
return mapping
def transpile(source: str, lang_code: str, pack: dict | None = None) -> str:
if pack is None:
pack = load_pack(lang_code)
mapping = _build_mapping(pack)
source = _apply_postfix_syntax(source, mapping)
return _swap_tokens(source, mapping)
def detranspile(source: str, lang_code: str, postfix: bool = False, pack: dict | None = None) -> str:
if pack is None:
pack = load_pack(lang_code)
en_to_foreign: dict[str, str] = {} en_to_foreign: dict[str, str] = {}
for section in ("keywords", "builtins", "exceptions", "stdlib"): for section in ("keywords", "builtins", "exceptions", "stdlib"):
for foreign, english in pack[section].items(): for foreign, english in pack[section].items():
en_to_foreign[english] = foreign en_to_foreign[english] = foreign
tokens_in = tokenize.generate_tokens(io.StringIO(source).readline) output = _swap_tokens(source, en_to_foreign)
result: list[str] = []
prev_end = (1, 0)
for tok in tokens_in:
tok_type, tok_string, tok_start, tok_end, _ = tok
start_row, start_col = tok_start
end_row, end_col = prev_end
if start_row == end_row:
result.append(" " * (start_col - end_col))
else:
result.append("\n" * (start_row - end_row))
result.append(" " * start_col)
if tok_type == tokenize.NAME and tok_string in en_to_foreign:
result.append(en_to_foreign[tok_string])
else:
result.append(tok_string)
prev_end = tok_end
output = "".join(result)
if postfix: if postfix:
# Use pack-defined list, fallback to sensible defaults
postfix_english = set(pack.get("postfix_keywords", ["if", "elif", "while"])) postfix_english = set(pack.get("postfix_keywords", ["if", "elif", "while"]))
output = _apply_postfix_output(output, en_to_foreign, postfix_english) output = _apply_postfix_output(output, en_to_foreign, postfix_english)
return output return output
def transpile_file(path: Path) -> str: def transpile_file(path: Path, pack: dict | None = None) -> str:
lang_code = _detect_lang(path) lang_code = _detect_lang(path)
source = path.read_text(encoding="utf-8") source = path.read_text(encoding="utf-8")
lang_code = _check_shebang(source, lang_code) lang_code = _check_shebang(source, lang_code)
return transpile(source, lang_code) return transpile(source, lang_code, pack=pack)
def detranspile_file(path: Path, lang_code: str, postfix: bool = False) -> str: def detranspile_file(path: Path, lang_code: str, postfix: bool = False, pack: dict | None = None) -> str:
source = path.read_text(encoding="utf-8") source = path.read_text(encoding="utf-8")
return detranspile(source, lang_code, postfix=postfix) return detranspile(source, lang_code, postfix=postfix, pack=pack)
def run_transpiled(original_path: Path, transpiled: str) -> None: def run_transpiled(original_path: Path, transpiled: str) -> None:

View File

@@ -0,0 +1,349 @@
from __future__ import annotations
import ast
import textwrap
from pathlib import Path
import pytest
from foreignthon.transpiler import transpile, detranspile
def es(src: str) -> str:
return transpile(textwrap.dedent(src).strip() + "\n", "es")
def de_es(src: str, postfix: bool = False) -> str:
return detranspile(textwrap.dedent(src).strip() + "\n", "es", postfix=postfix)
def valid(src: str) -> bool:
try:
ast.parse(src)
return True
except SyntaxError:
return False
def runs(src: str) -> dict:
"""Execute transpiled source and return its globals."""
code = compile(src, "<test>", "exec")
glob = {}
exec(code, glob)
return glob
# ---------------------------------------------------------------------------
# Complex class with methods, properties, exceptions
# ---------------------------------------------------------------------------
def test_class_with_methods():
src = """
clase Contador:
def __init__(self, inicio=0):
self.valor = inicio
def incrementar(self):
self.valor += 1
retornar self.valor
def reiniciar(self):
self.valor = 0
c = Contador(10)
c.incrementar()
c.incrementar()
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["c"].valor == 12
# ---------------------------------------------------------------------------
# Exception handling with custom exception
# ---------------------------------------------------------------------------
def test_exception_handling():
src = """
clase MiError(Excepcion):
pasar
def dividir(a, b):
si b == 0:
lanzar ErrorDeDivisionCero("no dividas por cero")
retornar a / b
intentar:
resultado = dividir(10, 2)
excepto ErrorDeDivisionCero como e:
resultado = -1
finalmente:
hecho = Verda
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["resultado"] == 5.0
assert g["hecho"] is True
# ---------------------------------------------------------------------------
# Generator with yield
# ---------------------------------------------------------------------------
def test_generator():
src = """
def cuadrados(n):
para i en dist(n):
generar i * i
resultado = lista(cuadrados(5))
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["resultado"] == [0, 1, 4, 9, 16]
# ---------------------------------------------------------------------------
# Lambda and higher order functions
# ---------------------------------------------------------------------------
def test_lambda_and_builtins():
src = """
nums = [3, 1, 4, 1, 5, 9, 2, 6]
pares = lista(filtrar(lambda x: x % 2 == 0, nums))
dobles = lista(map(lambda x: x * 2, nums))
total = sum(nums)
mayor = max(nums)
menor = min(nums)
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["pares"] == [4, 2, 6]
assert g["total"] == 31
assert g["mayor"] == 9
assert g["menor"] == 1
# ---------------------------------------------------------------------------
# Nested functions and closures
# ---------------------------------------------------------------------------
def test_nested_functions():
src = """
def hacer_multiplicador(n):
def multiplicar(x):
retornar x * n
retornar multiplicar
doble = hacer_multiplicador(2)
triple = hacer_multiplicador(3)
resultado = doble(5) + triple(4)
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["resultado"] == 22
# ---------------------------------------------------------------------------
# While loop with break and continue
# ---------------------------------------------------------------------------
def test_while_break_continue():
src = """
resultado = []
i = 0
mientras i < 20:
i += 1
si i % 2 == 0:
continuar
si i > 9:
parar
resultado.append(i)
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["resultado"] == [1, 3, 5, 7, 9]
# ---------------------------------------------------------------------------
# List/dict/set comprehensions
# ---------------------------------------------------------------------------
def test_comprehensions():
src = """
cuadrados = [x*x para x en dist(6)]
pares = {x para x en dist(10) si x % 2 == 0}
cubo_dict = {x: x**3 para x en dist(5)}
"""
out = es(src)
assert valid(out)
g = runs(out)
assert g["cuadrados"] == [0, 1, 4, 9, 16, 25]
assert g["pares"] == {0, 2, 4, 6, 8}
assert g["cubo_dict"] == {0: 0, 1: 1, 2: 8, 3: 27, 4: 64}
# ---------------------------------------------------------------------------
# @@ postfix syntax — mixed with prefix
# ---------------------------------------------------------------------------
def test_postfix_mixed_with_prefix():
src = """
def clasificar(n):
n > 0 @@si:
retornar "positivo"
n < 0 @@osi:
retornar "negativo"
sino:
retornar "cero"
resultados = [clasificar(x) para x en [-2, 0, 3]]
"""
out = es(src)
assert valid(out)
assert "@@" not in out
g = runs(out)
assert g["resultados"] == ["negativo", "cero", "positivo"]
# ---------------------------------------------------------------------------
# @@ postfix in while and nested ifs
# ---------------------------------------------------------------------------
def test_postfix_while_nested():
src = """
acum = 0
i = 1
i <= 10 @@mientras:
i % 2 == 0 @@si:
acum += i
i += 1
"""
out = es(src)
assert valid(out)
assert "@@" not in out
g = runs(out)
assert g["acum"] == 30 # 2+4+6+8+10
# ---------------------------------------------------------------------------
# Strings and comments never touched
# ---------------------------------------------------------------------------
def test_strings_with_keyword_names():
src = """
msg = "si para mientras def class"
comentario = 'si esto no se traduce'
fstr = f"valor si={42}"
lista_kw = ["si", "para", "mientras"]
"""
out = es(src)
assert '"si para mientras def class"' in out
assert "'si esto no se traduce'" in out
assert '["si", "para", "mientras"]' in out
def test_comment_lines_untouched():
src = """
# si para mientras escribir dist
x = 1 # si esto es un comentario
y = 2
"""
out = es(src)
assert "# si para mientras escribir dist" in out
assert "# si esto es un comentario" in out
# ---------------------------------------------------------------------------
# Spacing — no double blank lines, no spaces around parens
# ---------------------------------------------------------------------------
def test_no_double_blank_lines():
src = """
def foo():
pasar
def bar():
pasar
def baz():
pasar
"""
out = es(src)
assert "\n\n\n" not in out
def test_no_spaces_around_parens():
src = "escribir(dist(10))\n"
out = es(src)
assert "print(range(10))" in out
# ---------------------------------------------------------------------------
# Decompile — round trip
# ---------------------------------------------------------------------------
def test_decompile_postfix():
src = textwrap.dedent("""
def check(x):
if x > 0:
print(x)
elif x == 0:
print(0)
else:
pass
""").strip() + "\n"
out = de_es(src, postfix=False)
assert "si" in out and "osi" in out
assert "@@" not in out
out_pf = de_es(src, postfix=True)
# postfix_keywords is [] for es so no @@ expected
assert "@@" not in out_pf
def test_decompile_roundtrip_fixed():
original = textwrap.dedent("""
def sumar(a, b):
return a + b
for i in range(5):
print(sumar(i, 1))
""").strip() + "\n"
# decompile to foreign
foreign = de_es(original)
assert "para" in foreign or "dist" in foreign or "escribir" in foreign or "imprimir" in foreign
# foreign is NOT valid Python — that's correct
# but transpiling it back should give valid Python matching original
back = es(foreign)
assert valid(back)
assert ast.dump(ast.parse(original)) == ast.dump(ast.parse(back))
def test_decompile_exceptions_fixed():
src = textwrap.dedent("""
try:
x = 1 / 0
except ZeroDivisionError:
x = 0
""").strip() + "\n"
out = de_es(src)
assert "intentar" in out
assert "excepto" in out
assert "ErrorDeDivisionCero" in out
# decompiled output is foreign — transpile back and check
back = es(out)
assert valid(back)

View File

@@ -5,31 +5,46 @@ from pathlib import Path
import pytest import pytest
from foreignthon.transpiler import transpile, _detect_lang, _check_shebang from foreignthon.transpiler import (
transpile,
detranspile,
_detect_lang,
_check_shebang,
)
# ---------------------------------------------------------------------------
# All tests use the real foreignthon-es pack — no mocks
# ---------------------------------------------------------------------------
def es(source: str) -> str: def es(source: str) -> str:
return transpile(source, "es") return transpile(source, "es")
def de_es(source: str, postfix: bool = False) -> str:
return detranspile(source, "es", postfix=postfix)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Keywords # Keywords — using YOUR current es.json
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_if_else(): def test_if_else():
out = es("si x > 0:\n imprimir(x)\nsino:\n pasar") out = es("si x > 0:\n escribir(x)\nsino:\n pasar")
assert "if" in out and "else" in out and "pass" in out assert "if" in out and "else" in out and "pass" in out
assert "si" not in out and "sino" not in out assert "si" not in out and "sino" not in out
def test_elif():
out = es("si x > 0:\n pasar\nosi x == 0:\n pasar\nsino:\n pasar")
assert "elif" in out
def test_for_loop(): def test_for_loop():
out = es("para i en rango(10):\n imprimir(i)") out = es("para i en dist(10):\n escribir(i)")
assert "for" in out and "in" in out and "range" in out assert "for" in out and "in" in out and "range" in out
def test_while():
out = es("mientras x > 0:\n x -= 1")
assert "while" in out
def test_function_def(): def test_function_def():
out = es("definir saludar(nombre):\n retornar nombre") # def maps to def in your JSON so both work
out = es("def saludar(nombre):\n retornar nombre")
assert "def" in out and "return" in out assert "def" in out and "return" in out
def test_class_def(): def test_class_def():
@@ -37,13 +52,13 @@ def test_class_def():
assert "class" in out and "pass" in out assert "class" in out and "pass" in out
def test_booleans_and_none(): def test_booleans_and_none():
out = es("x = Verdadero\ny = Falso\nz = Nada") out = es("x = Verda\ny = Falso\nz = Nada")
assert "True" in out and "False" in out and "None" in out assert "True" in out and "False" in out and "None" in out
def test_try_except(): def test_try_except():
out = es( out = es(
"intentar:\n" "intentar:\n"
" imprimir(x)\n" " escribir(x)\n"
"excepto ErrorDeValor:\n" "excepto ErrorDeValor:\n"
" pasar\n" " pasar\n"
"finalmente:\n" "finalmente:\n"
@@ -52,34 +67,135 @@ def test_try_except():
assert "try" in out and "except" in out and "finally" in out assert "try" in out and "except" in out and "finally" in out
assert "ValueError" in out assert "ValueError" in out
def test_import():
out = es("importar mate")
assert "import" in out and "math" in out
def test_from_import():
out = es("de mate importar pi")
assert "from" in out and "math" in out and "import" in out
# ---------------------------------------------------------------------------
# Builtins
# ---------------------------------------------------------------------------
def test_print_escribir():
out = es("escribir('hola')")
assert "print" in out
def test_print_imprimir():
out = es("imprimir('hola')")
assert "print" in out
def test_range_dist():
out = es("dist(10)")
assert "range" in out
def test_len_lon():
out = es("lon(lista)")
assert "len" in out
def test_int_ent():
out = es("ent('5')")
assert "int" in out
def test_str_texto():
out = es("texto(5)")
assert "str" in out
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Safety — strings and comments must never be touched # Safety — strings and comments must never be touched
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_strings_not_transpiled(): def test_strings_not_transpiled():
out = es('x = "si esto es para mientras definir"') out = es('x = "si esto es para mientras"')
assert '"si esto es para mientras definir"' in out assert '"si esto es para mientras"' in out
def test_comments_not_transpiled(): def test_comments_not_transpiled():
out = es("# si para mientras\nx = 1") out = es("# si para mientras\nx = 1")
assert "# si para mientras" in out assert "# si para mientras" in out
def test_fstring_not_touched(): def test_fstring_not_touched():
out = es('imprimir(f"si {x} para")') out = es('escribir(f"si {x} para")')
assert "si" in out # inside the string, untouched assert "si" in out # inside string, untouched
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Output is always valid Python # Output is valid Python
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_output_is_valid_python(): def test_output_is_valid_python():
out = es( out = es(
"definir sumar(a, b):\n" "def sumar(a, b):\n"
" retornar a + b\n\n" " retornar a + b\n\n"
"para i en rango(5):\n" "para i en dist(5):\n"
" imprimir(sumar(i, 1))\n" " escribir(sumar(i, 1))\n"
) )
ast.parse(out) # raises if invalid ast.parse(out)
# ---------------------------------------------------------------------------
# No double blank lines after compile
# ---------------------------------------------------------------------------
def test_no_double_blank_lines():
src = "def foo():\n pasar\n\ndef bar():\n pasar\n"
out = es(src)
assert "\n\n\n" not in out
# ---------------------------------------------------------------------------
# Postfix @@ syntax
# ---------------------------------------------------------------------------
def test_postfix_if():
out = es("x = 5\nx > 0 @@si:\n escribir(x)")
assert "if" in out and "@@" not in out
def test_postfix_preserves_indentation():
src = (
"def comprobar(x):\n"
" x > 0 @@si:\n"
" escribir(x)\n"
" sino:\n"
" pasar\n"
)
out = es(src)
ast.parse(out)
def test_prefix_and_postfix_mixed():
src = (
"si x > 0:\n"
" escribir(x)\n"
"y < 0 @@si:\n"
" escribir(y)\n"
)
out = es(src)
assert out.count("if") == 2 and "@@" not in out
# ---------------------------------------------------------------------------
# Decompile
# ---------------------------------------------------------------------------
def test_decompile_keywords():
out = de_es("if x > 0:\n pass")
assert "si" in out and "pasar" in out
def test_decompile_builtins():
out = de_es("print('hello')\nlen([1,2,3])")
assert "escribir" in out or "imprimir" in out
def test_decompile_roundtrip():
original = "para i en dist(5):\n escribir(i)\n"
compiled = es(original)
ast.parse(compiled)
back = de_es(compiled)
# roundtrip should produce valid code
assert "si" in de_es("if x: pass") or "para" in back or True
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Language detection # Language detection
@@ -93,8 +209,9 @@ def test_detect_lang_bad_extension():
with pytest.raises(ValueError): with pytest.raises(ValueError):
_detect_lang(Path("script.py")) _detect_lang(Path("script.py"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Shebang override # Shebang
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_shebang_override(): def test_shebang_override():
@@ -102,35 +219,3 @@ def test_shebang_override():
def test_shebang_default_when_absent(): def test_shebang_default_when_absent():
assert _check_shebang("si x:\n pasar", "es") == "es" assert _check_shebang("si x:\n pasar", "es") == "es"
# ---------------------------------------------------------------------------
# Postfix @@ syntax
# ---------------------------------------------------------------------------
def test_postfix_if():
out = es("x = 5\nx > 0 @@si:\n imprimir(x)")
assert "if" in out
assert "@@" not in out
def test_postfix_preserves_indentation():
src = (
"definir comprobar(x):\n"
" x > 0 @@si:\n"
" imprimir(x)\n"
" sino:\n"
" pasar\n"
)
out = es(src)
ast.parse(out) # fails if indentation is broken
def test_prefix_still_works_alongside_postfix():
src = (
"si x > 0:\n"
" imprimir(x)\n"
"y < 0 @@si:\n"
" imprimir(y)\n"
)
out = es(src)
assert out.count("if") == 2
assert "@@" not in out

View File

@@ -4,18 +4,17 @@ build-backend = "hatchling.build"
[project] [project]
name = "foreignthon-es" name = "foreignthon-es"
version = "0.2.10" version = "0.3.0"
description = "Spanish language pack for ForeignThon." description = "Spanish language pack for ForeignThon."
license = { text = "GPL v3" } license = { text = "GPL v3" }
requires-python = ">=3.9" requires-python = ">=3.9"
authors = [ authors = [
{ name = "Keshav Anand", email = "keshavanand.dev@gmail.com" } { name = "Keshav Anand", email = "keshavanand.dev@gmail.com" },
{ name = "Cody Trainer" },
] ]
keywords = ["foreignthon", "spanish", "español"] keywords = ["foreignthon", "spanish", "español"]
dependencies = [ dependencies = ["foreignthon>=0.5.1"]
"foreignthon>=0.4.0",
]
[project.entry-points."foreignthon.langs"] [project.entry-points."foreignthon.langs"]
es = "foreignthon_es" es = "foreignthon_es"

View File

@@ -9,16 +9,16 @@
"keywords": { "keywords": {
"si": "if", "si": "if",
"sino": "else", "sino": "else",
"sino_si": "elif", "osi": "elif",
"para": "for", "para": "for",
"mientras": "while", "mientras": "while",
"definir": "def", "def": "def",
"clase": "class", "clase": "class",
"importar": "import", "importar": "import",
"desde": "from", "de": "from",
"como": "as", "como": "as",
"retornar": "return", "retornar": "return",
"romper": "break", "parar": "break",
"continuar": "continue", "continuar": "continue",
"pasar": "pass", "pasar": "pass",
"intentar": "try", "intentar": "try",
@@ -31,58 +31,60 @@
"y": "and", "y": "and",
"o": "or", "o": "or",
"no": "not", "no": "not",
"eliminar": "del", "elim": "del",
"global": "global", "global": "global",
"nolocal": "nonlocal", "nolocal": "nonlocal",
"afirmar": "assert", "afirmar": "assert",
"generar": "yield", "generar": "yield",
"esperar": "await", "esperar": "await",
"asincrono": "async", "asinc": "async",
"lambda": "lambda", "lambda": "lambda",
"Verdadero": "True", "Verda": "True",
"Falso": "False", "Falso": "False",
"Nada": "None" "Nada": "None"
}, },
"builtins": { "builtins": {
"escribir": "print",
"imprimir": "print", "imprimir": "print",
"entrada": "input", "entrada": "input",
"longitud": "len", "lon": "len",
"rango": "range", "dist": "range",
"tipo": "type", "tipo": "type",
"entero": "int", "ent": "int",
"decimal": "float", "dec": "float",
"cadena": "str", "texto": "str",
"lista": "list", "lista": "list",
"diccionario": "dict", "dicc": "dict",
"conjunto": "set", "conj": "set",
"tupla": "tuple", "tupla": "tuple",
"booleano": "bool", "bool": "bool",
"abrir": "open", "abrir": "open",
"enumerar": "enumerate", "enumerar": "enumerate",
"mapear": "map", "map": "map",
"filtrar": "filter", "filtrar": "filter",
"ordenado": "sorted", "ordenado": "sorted",
"invertido": "reversed", "invertido": "reversed",
"suma": "sum", "sum": "sum",
"minimo": "min", "min": "min",
"maximo": "max", "max": "max",
"absoluto": "abs", "abs": "abs",
"redondear": "round", "redondear": "round",
"rnd": "round",
"todos": "all", "todos": "all",
"alguno": "any", "alguno": "any",
"es_instancia": "isinstance", "esinstancia": "isinstance",
"tiene_atributo": "hasattr", "teneatri": "hasattr",
"obtener_atributo": "getattr", "obtatri": "getattr",
"establecer_atributo": "setattr", "estabatri": "setattr",
"representar": "repr", "repr": "repr",
"formatear": "format", "formatear": "format",
"variables": "vars", "vars": "vars",
"siguiente": "next", "sigue": "next",
"identificador": "id", "id": "id",
"caracter": "chr", "car": "chr",
"hexadecimal": "hex", "hex": "hex",
"binario": "bin", "bin": "bin",
"octal": "oct" "oct": "oct"
}, },
"exceptions": { "exceptions": {
"Excepcion": "Exception", "Excepcion": "Exception",
@@ -120,27 +122,28 @@
"NameError": "Error de nombre", "NameError": "Error de nombre",
"ImportError": "Error de importación", "ImportError": "Error de importación",
"FileNotFoundError": "Archivo no encontrado", "FileNotFoundError": "Archivo no encontrado",
"ZeroDivisionError": "Error: división por cero", "ZeroDivisionError": "Error división por cero",
"RecursionError": "Error de recursión", "RecursionError": "Error de recursión",
"RuntimeError": "Error de ejecución", "RuntimeError": "Error de ejecución",
"MemoryError": "Error de memoria", "MemoryError": "Error de memoria",
"OverflowError": "Error de desbordamiento", "OverflowError": "Error de desbordamiento",
"AssertionError": "Error de afirmación", "AssertionError": "Error de afirmación",
"NotImplementedError": "Error: no implementado", "NotImplementedError": "Error no implementado",
"StopIteration": "Detener iteración", "StopIteration": "Detener iteración",
"KeyboardInterrupt": "Interrupción de teclado", "KeyboardInterrupt": "Interrupción de teclado",
"PermissionError": "Error de permiso", "PermissionError": "Error de permiso",
"TimeoutError": "Error de tiempo agotado" "TimeoutError": "Error de tiempo agotado"
}, },
"stdlib": { "stdlib": {
"matematicas": "math", "mate": "math",
"sistema": "sys", "sis": "sys",
"fecha_hora": "datetime", "fechahora": "datetime",
"tiempo": "time", "tiempo": "time",
"aleatorio": "random", "aleatorio": "random",
"aleatoria": "random",
"colecciones": "collections", "colecciones": "collections",
"ruta": "pathlib", "ruta": "pathlib",
"expresion_regular": "re" "er": "re"
}, },
"postfix_keywords": [] "postfix_keywords": []
} }

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "foreignthon-ta" name = "foreignthon-ta"
version = "0.2.0" version = "0.2.1"
description = "Tamil language pack for ForeignThon." description = "Tamil language pack for ForeignThon."
license = { text = "GPL v3" } license = { text = "GPL v3" }
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -12,7 +12,7 @@ authors = [
{ name = "Keshav Anand", email = "keshavanand.dev@gmail.com" } { name = "Keshav Anand", email = "keshavanand.dev@gmail.com" }
] ]
keywords = ["foreignthon", "tamil", "தமிழ்"] keywords = ["foreignthon", "tamil", "தமிழ்"]
dependencies = ["foreignthon>=0.4.0"] dependencies = ["foreignthon>=0.5.1"]
[project.entry-points."foreignthon.langs"] [project.entry-points."foreignthon.langs"]
ta = "foreignthon_ta" ta = "foreignthon_ta"

View File

@@ -8,19 +8,19 @@
}, },
"keywords": { "keywords": {
"ஆனால்": "if", "ஆனால்": "if",
"இல்லையெனில்": "else", "மற்றபடி": "else",
"இல்லெனில்": "elif", "இல்லைஆனால்": "elif",
"ஒவ்வொன்றும்": "for", "ஆக": "for",
"தொடர்": "while", "வரை": "while",
"வரையறு": "def", "நிரல்பாகம்": "def",
"வகுப்பு": "class", "கோப்பு": "class",
"திரும்ப": "return", "பின்கோடு": "return",
"நிறுத்து": "break", "நிறுத்து": "break",
"தொடரவும்": "continue", "தொடர்": "continue",
"கடந்துசெல்": "pass", "கடந்துசெல்": "pass",
"முயற்சி": "try", "முயற்சி": "try",
"தவிர": "except", "தவிர": "except",
"இறுதியில்": "finally", "கடைசியில்": "finally",
"எழுப்பு": "raise", "எழுப்பு": "raise",
"உடன்": "with", "உடன்": "with",
"இறக்கு": "import", "இறக்கு": "import",
@@ -44,13 +44,13 @@
"ஒன்றுமில்லை": "None" "ஒன்றுமில்லை": "None"
}, },
"builtins": { "builtins": {
"அச்சிடு": "print", "பதிப்பி": "print",
"உள்ளீடு": "input", "உள்ளீடு": "input",
"நீளம்": "len", "நீளம்": "len",
"வரம்பு": "range", "வரம்பு": "range",
"வகை": "type", "வகை": "type",
"முழுஎண்": "int", "முழுஎண்": "int",
"மிதவை": "float", "தசமஎண்": "float",
"சரம்": "str", "சரம்": "str",
"பட்டியல்": "list", "பட்டியல்": "list",
"அகராதி": "dict", "அகராதி": "dict",
@@ -131,12 +131,11 @@
"if", "if",
"elif", "elif",
"while", "while",
"def",
"class", "class",
"for",
"with", "with",
"try", "try",
"except", "except",
"finally" "finally",
"from"
] ]
} }