From 0fbbb30e5b40318c795fa4c8a81d87a791e5e229 Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Tue, 19 May 2026 17:52:03 -0500 Subject: [PATCH] added tests, files, workflow, toml, etc --- .gitea/workflows/ci.yml | 28 ++++++ .gitea/workflows/publish.yml | 80 +++++++++++++++ .gitignore | 44 +++++++++ pyproject.toml | 30 ++++++ src/foreignthon_template/__init__.py | 29 ++++++ src/foreignthon_template/template.json | 132 +++++++++++++++++++++++++ tests/test_pack.py | 114 +++++++++++++++++++++ 7 files changed, 457 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 src/foreignthon_template/__init__.py create mode 100644 src/foreignthon_template/template.json create mode 100644 tests/test_pack.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..6a57af9 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,28 @@ +name: Pack Validation CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Test Dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Run Pack Verification Suite + run: python -m pytest tests/ -v diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..6a1bc2d --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,80 @@ +name: Publish Language Pack + +on: + push: + tags: + - "v*" # Fires directly on v0.1.0, v0.2.0 etc. + +jobs: + # Enforce that tests MUST pass before release can execute + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install and Verify + run: | + pip install pytest + pytest tests/ -v + + publish: + needs: verify # Blocks execution if verify job fails + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Release Tools + run: pip install build twine + + - name: Build Wheel and Source Distribution + # Automatically detects pyproject.toml and builds the correct pack + run: python -m build . + + - name: Publish Package to PyPI + env: + TWINE_USERNAME: __token__ + # Inherits your clean Organization level secret + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload --skip-existing dist/* + + - name: Build Gitea Release with Assets + env: + # Pulls your clean Organization level Gitea Token + GIT_RELEASE_TOKEN: ${{ secrets.GIT_RELEASE_TOKEN }} + run: | + TAG=${GITHUB_REF#refs/tags/} + REPO_NAME=${{ github.event.repository.name }} + + # Delete existing release block if present + EXISTING=$(curl -s -H "Authorization: token $GIT_RELEASE_TOKEN" "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/$TAG") + EXISTING_ID=$(echo $EXISTING | python -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "") + if [ -n "$EXISTING_ID" ]; then + curl -s -X DELETE -H "Authorization: token $GIT_RELEASE_TOKEN" "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$EXISTING_ID" + fi + + # Create fresh production release container + RELEASE=$(curl -s -X POST \ + -H "Authorization: token $GIT_RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$TAG\", + \"name\": \"$REPO_NAME $TAG\", + \"body\": \"Language pack release version $TAG for the foreignthon transpiler framework.\", + \"draft\": false, + \"prerelease\": false + }" "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases") + + RELEASE_ID=$(echo $RELEASE | python -c "import sys,json; print(json.load(sys.stdin)['id'])") + + # Upload wheels directly into Gitea Assets tab + for FILE in dist/*; do + curl -s -X POST -H "Authorization: token $GIT_RELEASE_TOKEN" -F "attachment=@$FILE" "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets" + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f973e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +*.egg +dist/ +build/ +wheels/ +sdist/ + +# Virtual envs +.venv/ +venv/ +env/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Ruff / linting +.ruff_cache/ + +# Mypy +.mypy_cache/ + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# ForeignThon compiled output +*.compiled.py + +# Temporary Tests +tmp/** diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..553f31b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "foreignthon-template" # TODO: Modify to language code +version = "0.0.0" +description = "[Template] language pack for ForeignThon." # TODO: Replace [Template] +license = { text = "GPL v3" } +requires-python = ">=3.9" +authors = [ + { name = "John Doe", email = "loremipsum@example.com" } # TODO: Swap with real information +] +keywords = ["foreignthon", "template", "template"] # TODO: Swap with real information +dependencies = ["foreignthon>=0.5.3"] # TODO: Update Version to Latest + +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://git.keshavanand.net/foreign-thon/foreignthon-template" # TODO: Update URL + +[project.entry-points."foreignthon.langs"] +template = "foreignthon_template" # TODO: Swap in format "code = foreignthon_code" + +[tool.hatch.build.targets.wheel] +packages = ["src/foreignthon_template"] # TODO: Change to src/foreignthon_code \ No newline at end of file diff --git a/src/foreignthon_template/__init__.py b/src/foreignthon_template/__init__.py new file mode 100644 index 0000000..105e737 --- /dev/null +++ b/src/foreignthon_template/__init__.py @@ -0,0 +1,29 @@ +from importlib.resources import files +from importlib.metadata import version, metadata, PackageNotFoundError + +try: + package_name = (__package__ or "").replace("_", "-") + __version__ = version(package_name) + + pkg_metadata = metadata(package_name) + + # 1. Grab names from BOTH possible fields (Author and Author-email) + raw_authors = pkg_metadata.get_all("Author") or [] + raw_emails = pkg_metadata.get_all("Author-email") or [] + + # 2. Combine them into one single, clean list of unique names + combined = [] + for item in (raw_authors + raw_emails): + clean_name = item.split("<")[0].strip() + if clean_name and clean_name not in combined: + combined.append(clean_name) + + __authors__ = combined + +except PackageNotFoundError: + __version__ = "0.0.0" + __authors__ = [] + +def get_pack_path(): + # TODO: Modify this path + return files(__name__) / "template.json" \ No newline at end of file diff --git a/src/foreignthon_template/template.json b/src/foreignthon_template/template.json new file mode 100644 index 0000000..efa8316 --- /dev/null +++ b/src/foreignthon_template/template.json @@ -0,0 +1,132 @@ +{ + "meta": { + "name": "Template", + "native_name": "Template", + "code": "template" + }, + "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", + "from": "from", + "as": "as", + "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", + "sum": "sum", + "min": "min", + "max": "max", + "abs": "abs", + "round": "round", + "all": "all", + "any": "any", + "next": "next", + "id": "id", + "chr": "chr", + "reversed": "reversed" + }, + "exceptions": { + "Exception": "Exception", + "BaseException": "BaseException", + "ValueError": "ValueError", + "TypeError": "TypeError", + "KeyError": "KeyError", + "IndexError": "IndexError", + "AttributeError": "AttributeError", + "NameError": "NameError", + "ImportError": "ImportError", + "FileNotFoundError": "FileNotFoundError", + "RuntimeError": "RuntimeError", + "ZeroDivisionError": "ZeroDivisionError", + "SyntaxError": "SyntaxError", + "AssertionError": "AssertionError", + "MemoryError": "MemoryError", + "OverflowError": "OverflowError", + "RecursionError": "RecursionError", + "PermissionError": "PermissionError", + "TimeoutError": "TimeoutError", + "SystemExit": "SystemExit", + "KeyboardInterrupt": "KeyboardInterrupt" + }, + "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", + "PermissionError": "PermissionError", + "TimeoutError": "TimeoutError", + "KeyboardInterrupt": "KeyboardInterrupt" + }, + "stdlib": { + "math": "math", + "sys": "sys", + "datetime": "datetime", + "time": "time", + "random": "random", + "collections": "collections", + "pathlib": "pathlib", + "re": "re" + }, + + "postfix_keywords": [ + + ] +} \ No newline at end of file diff --git a/tests/test_pack.py b/tests/test_pack.py new file mode 100644 index 0000000..e656588 --- /dev/null +++ b/tests/test_pack.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import builtins +import json +import keyword +from pathlib import Path +import pytest + +# --------------------------------------------------------------------------- +# Dynamic Discovery: Find the pack file in this specific repository +# --------------------------------------------------------------------------- + + +def find_pack_file() -> Path: + """Scans the repository root for a Foreignthon language pack JSON.""" + repo_root = Path(__file__).parent.parent + + for path in repo_root.rglob("*.json"): + # Ignore virtual environments, cache files, and CI directories + ignored_dirs = {".venv", "venv", ".git", ".pytest_cache", ".gitea", ".github"} + if any(part in path.parts for part in ignored_dirs): + continue + + # Peek inside the JSON to see if it's a Foreignthon pack + try: + with open(path, encoding="utf-8") as f: + content = json.load(f) + if "meta" in content and "keywords" in content: + return path + except (json.JSONDecodeError, KeyError, TypeError): + continue + + raise FileNotFoundError( + "Could not find a valid Foreignthon language pack JSON file in this repository." + ) + + +# --------------------------------------------------------------------------- +# Global Validation Sets +# --------------------------------------------------------------------------- +VALID_KEYWORDS = set(keyword.kwlist) | {"True", "False", "None"} +VALID_BUILTINS = set(dir(builtins)) + +# --------------------------------------------------------------------------- +# The Universal Test Suite +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def pack_data() -> tuple[Path, dict]: + """Loads the dynamically discovered pack file once for the test module.""" + path = find_pack_file() + with open(path, encoding="utf-8") as f: + return path, json.load(f) + + +def test_meta_section_integrity(pack_data): + path, data = pack_data + assert "meta" in data, f"Missing 'meta' section in {path.name}" + meta = data["meta"] + + assert meta.get("name"), "meta.name cannot be empty" + assert meta.get("native_name"), "meta.native_name cannot be empty" + assert meta.get("code"), "meta.code cannot be empty" + + # Enforce that the file name matches the language code (e.g., es.json matches code 'es') + assert ( + path.stem == meta["code"] + ), f"Filename '{path.name}' must exactly match meta.code '{meta['code']}'" + + +def test_keywords_integrity(pack_data): + path, data = pack_data + assert "keywords" in data, f"Missing 'keywords' section in {path.name}" + + for foreign_word, python_keyword in data["keywords"].items(): + assert python_keyword in VALID_KEYWORDS, ( + f"Invalid target '{python_keyword}' for foreign keyword '{foreign_word}' in {path.name}. " + "It is not a recognized Python keyword." + ) + + +def test_builtins_and_exceptions_integrity(pack_data): + path, data = pack_data + + # Validate Builtins + assert "builtins" in data, f"Missing 'builtins' section in {path.name}" + for foreign_word, python_builtin in data["builtins"].items(): + assert ( + python_builtin in VALID_BUILTINS + ), f"Invalid target '{python_builtin}' for builtin function '{foreign_word}' in {path.name}." + + # Validate Exceptions + assert "exceptions" in data, f"Missing 'exceptions' section in {path.name}" + for foreign_word, python_exception in data["exceptions"].items(): + assert ( + python_exception in VALID_BUILTINS + ), f"Invalid target '{python_exception}' for exception '{foreign_word}' in {path.name}." + + +def test_postfix_keywords_integrity(pack_data): + path, data = pack_data + assert "postfix_keywords" in data, f"Missing 'postfix_keywords' section in {path.name}" + assert isinstance(data["postfix_keywords"], list), "'postfix_keywords' must be an array/list" + + # Extract all the English Python keywords that have been mapped in this pack + mapped_python_targets = set(data["keywords"].values()) + + # Ensure any declared English postfix keyword actually has a mapped translation + for post_kw in data["postfix_keywords"]: + assert post_kw in mapped_python_targets, ( + f"English postfix keyword '{post_kw}' is missing from the mapped values " + f"in the main 'keywords' dictionary in {path.name}." + ) \ No newline at end of file