diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e18e147 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Default owner for everything in the repo. +* @alphacrack + +# Security-sensitive paths: sandbox, transpiler string handling, CI/publish. +/maithili_dsl/cli.py @alphacrack +/maithili_dsl/transpiler/ @alphacrack +/.github/workflows/ @alphacrack +/SECURITY.md @alphacrack diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..30474fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,69 @@ +name: ЁЯРЫ Bug report +description: Something in the transpiler, linter, sandbox, or CLI doesn't work as documented. +title: "[BUG] " +labels: ["bug", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! рдзрдиреНрдпрд╡рд╛рдж ЁЯЩП + + тЪая╕П **Security issues** (sandbox escapes, exec bypasses) must NOT be reported here. + Use [private vulnerability reporting](https://github.com/alphacrack/python-maithili-dsl/security/advisories/new) + or the process in [SECURITY.md](https://github.com/alphacrack/python-maithili-dsl/blob/main/SECURITY.md). + - type: textarea + id: description + attributes: + label: What happened? + description: A clear description of the bug. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal .dmai reproduction + description: The smallest Maithili snippet that triggers the bug. + placeholder: | + рдЫрдкрд╛рдЙ("рдпрд╣ рдореЗрдВ рд╣реИ") + render: text + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What you expected to happen (e.g. the transpiled Python, or output). + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What actually happened. Paste the full output / traceback. + render: shell + validations: + required: true + - type: input + id: version + attributes: + label: python_maithili version + description: Output of `python_maithili --version` + placeholder: "0.3.0" + validations: + required: true + - type: input + id: environment + attributes: + label: Python version and OS + placeholder: "Python 3.12, macOS 15" + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched existing issues and this is not a duplicate. + required: true + - label: This is not a security vulnerability (those go through SECURITY.md). + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..408d7d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: ЁЯФР Report a security vulnerability + url: https://github.com/alphacrack/python-maithili-dsl/security/advisories/new + about: Sandbox escapes and exec bypasses must be reported privately тАФ never as a public issue. See SECURITY.md. + - name: ЁЯТм Questions & general discussion + url: https://github.com/alphacrack/python-maithili-dsl/blob/main/README.md + about: Check the README and docs first; for usage questions, open a blank issue with the "question" label. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..32d94be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: тЬи Feature request +description: Propose a new capability for the DSL, CLI, or tooling. +title: "[FEATURE] " +labels: ["enhancement", "needs-triage"] +body: + - type: markdown + attributes: + value: | + For a **new Maithili keyword or module mapping**, please use the + dedicated "ЁЯФд Keyword / mapping proposal" template instead. + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? Who benefits? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What you'd like to see, ideally with an example `.dmai` snippet. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you thought about, and why they're worse. + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched existing issues and BACKLOG.md and this is not already tracked. + required: true diff --git a/.github/ISSUE_TEMPLATE/keyword_proposal.yml b/.github/ISSUE_TEMPLATE/keyword_proposal.yml new file mode 100644 index 0000000..87cc6bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/keyword_proposal.yml @@ -0,0 +1,57 @@ +name: ЁЯФд Keyword / mapping proposal +description: Propose a new Maithili keyword, builtin, or module-name mapping. +title: "[KEYWORD] " +labels: ["enhancement", "keyword-mapping", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Keyword choices shape the language for every Maithili speaker, so + native-speaker input on word choice is especially valuable here. + - type: input + id: maithili + attributes: + label: Proposed Maithili word (Devanagari) + placeholder: "рдЬрдмрддрдХ" + validations: + required: true + - type: input + id: python + attributes: + label: Python equivalent + placeholder: "while" + validations: + required: true + - type: textarea + id: rationale + attributes: + label: Why this word? + description: | + Is this the natural Maithili term? Are there regional variants or + alternative spellings a reader might expect? Could it collide with + an existing keyword or with common identifier names? + validations: + required: true + - type: textarea + id: example + attributes: + label: Example usage + description: A short `.dmai` snippet showing the keyword in use, with the expected transpiled Python. + render: text + placeholder: | + рдЧрд┐рдирддреА = реж + рдЬрдмрддрдХ рдЧрд┐рдирддреА < рел: + рдЫрдкрд╛рдЙ(рдЧрд┐рдирддреА) + рдЧрд┐рдирддреА = рдЧрд┐рдирддреА + рез + validations: + required: true + - type: dropdown + id: speaker + attributes: + label: Are you a Maithili speaker? + options: + - "Yes тАФ native" + - "Yes тАФ non-native / learner" + - "No тАФ proposing on technical grounds only" + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b3d7aae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + # Keep GitHub Actions pinned versions current (checkout, setup-python, etc.) + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "development" + labels: ["infra", "needs-triage"] + + # Dev dependencies (pytest, coverage tooling). The package itself has + # zero runtime deps, so this mostly tracks requirements-dev.txt. + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + target-branch: "development" + labels: ["infra", "needs-triage"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41e9f9c..d0c97ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,14 +20,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip @@ -70,8 +70,24 @@ jobs: - name: Upload coverage artifact if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-xml path: coverage.xml if-no-files-found: warn + + # Aggregate gate: branch protection requires this single check instead of + # every matrix leg, so the matrix can change without touching repo settings. + ci-ok: + name: ci-ok + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Fail if any test job failed + run: | + if [ "${{ needs.test.result }}" != "success" ]; then + echo "test matrix result: ${{ needs.test.result }}" + exit 1 + fi + echo "all test jobs green" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fb6f9d7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,192 @@ +name: Publish to PyPI + +# Trusted Publishing (OIDC) тАФ no long-lived API tokens stored. +# +# Triggers: +# - release.published on a non-prerelease GitHub Release +# тЖТ publishes to PyPI (environment: pypi) +# - release.published on a pre-release GitHub Release +# тЖТ publishes to TestPyPI (environment: testpypi) +# - workflow_dispatch with target=testpypi +# тЖТ publishes to TestPyPI (staging verification) +# - workflow_dispatch with target=build-only +# тЖТ builds + twine-checks, no network publish (dry run) +# +# One-time setup required before the publish jobs succeed: +# See docs/RELEASE.md for Trusted Publisher configuration on +# pypi.org and test.pypi.org. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + target: + description: "Publish target" + required: true + default: "build-only" + type: choice + options: + - build-only + - testpypi + +concurrency: + group: publish-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + # ------------------------------------------------------------------ + # Build sdist + wheel, run twine check, run the full test suite + # against the built wheel so we never ship a wheel that fails tests. + # ------------------------------------------------------------------ + build: + name: Build distributions + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + version: ${{ steps.version.outputs.value }} + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: requirements-dev.txt + + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Extract version from package + id: version + run: | + v=$(python -c "import re, pathlib; print(re.search(r'^__version__\s*=\s*[\"\']([^\"\']+)', pathlib.Path('maithili_dsl/__init__.py').read_text(), re.M).group(1))") + echo "value=$v" >> "$GITHUB_OUTPUT" + echo "Package version: $v" + + - name: Build sdist + wheel + run: python -m build + + - name: twine check + run: twine check --strict dist/* + + - name: Install built wheel in clean venv + run: | + python -m venv /tmp/smoketest + /tmp/smoketest/bin/pip install dist/*.whl + /tmp/smoketest/bin/python -m maithili_dsl --version + /tmp/smoketest/bin/python_maithili examples/hello.dmai + + - name: Run tests against the installed wheel + run: | + /tmp/smoketest/bin/pip install -r requirements-dev.txt + /tmp/smoketest/bin/pytest -q --no-cov + + - name: Upload distributions artifact + uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/ + if-no-files-found: error + retention-days: 7 + + # ------------------------------------------------------------------ + # TestPyPI тАФ fires on workflow_dispatch (target=testpypi) or on + # a pre-release GitHub Release. Safe to re-run with a different + # version number. + # ------------------------------------------------------------------ + publish-testpypi: + name: Publish to TestPyPI + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 10 + if: | + (github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi') || + (github.event_name == 'release' && github.event.release.prerelease == true) + environment: + name: testpypi + url: https://test.pypi.org/p/python-maithili + permissions: + id-token: write # OIDC token for Trusted Publishing + steps: + - name: Download distributions + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: false + verbose: true + + - name: Summary + run: | + echo "### Published to TestPyPI :rocket:" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Version: **${{ needs.build.outputs.version }}**" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY" + echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ python-maithili==${{ needs.build.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" + + # ------------------------------------------------------------------ + # Production PyPI тАФ fires only on a non-prerelease GitHub Release. + # This is the single authoritative way to publish a real version. + # ------------------------------------------------------------------ + publish-pypi: + name: Publish to PyPI + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 10 + if: | + github.event_name == 'release' && github.event.release.prerelease == false + environment: + name: pypi + url: https://pypi.org/p/python-maithili + permissions: + id-token: write # OIDC token for Trusted Publishing + steps: + - name: Download distributions + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - name: Verify release tag matches package version + env: + TAG: ${{ github.event.release.tag_name }} + VERSION: ${{ needs.build.outputs.version }} + run: | + expected="v${VERSION}" + if [ "$TAG" != "$expected" ]; then + echo "::error::Release tag '$TAG' does not match package version '$expected'. Refusing to publish." + exit 1 + fi + echo "Tag $TAG matches package version $VERSION тАФ proceeding." + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: false + verbose: true + + - name: Summary + run: | + echo "### Published to PyPI :tada:" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Version: **${{ needs.build.outputs.version }}**" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY" + echo "pip install python-maithili==${{ needs.build.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/BACKLOG.md b/BACKLOG.md index 19de5e7..3b1e420 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1,7 +1,9 @@ # Maithili DSL тАФ Project Backlog -> Generated: 2026-04-11 | Status: Active -> Tracking: GitHub Issues (use `scripts/create_issues.sh` to push) +> Generated: 2026-04-11 | Status: **Historical snapshot** +> Live tracking has moved to [GitHub Issues](https://github.com/alphacrack/python-maithili-dsl/issues). +> All P0 and P1 items below (plus items 11, 13тАУ15, 19) have been resolved as of v0.3.0; +> the remaining open items are tracked as individual issues. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c4cc3..f310d88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -_Nothing yet. Open a PR to add your change here._ +## [0.4.0] тАФ 2026-07-23 + +First release published to PyPI since 0.2.0 (0.3.0 was an internal +release that was never pushed to the index). This release bundles the +0.3.0 hardening work plus the fixes and tooling below. + +### Added +- **PyPI publish pipeline** (`.github/workflows/publish.yml`) using + Trusted Publishing (OIDC). Triggers: GitHub Release for production + PyPI, pre-release or `workflow_dispatch` for TestPyPI, `build-only` + dispatch for a credential-free dry run. The workflow builds sdist + + wheel, runs `twine check --strict`, installs the wheel in a clean + venv, runs the full pytest suite against the installed wheel, and + verifies the release tag matches `__version__` before publishing. +- **`docs/RELEASE.md`** тАФ step-by-step release process including + one-time Trusted Publisher setup on PyPI + TestPyPI, routine + release flow, version-bump convention, and recovery procedure for + bad releases. +- **`build` and `twine`** added to `requirements-dev.txt`. +- **`examples/README.md`** documenting every bundled example program, + the keywords each demonstrates, and expected output. Closes #41. +- **Enforced coverage gate.** `fail_under = 85` in `pyproject.toml`'s + `[tool.coverage.report]` so a coverage regression fails CI. Closes #42. + +### Fixed +- **Augmented assignments flagged as invalid variable names.** `+=`, + `-=`, `*=`, `/=`, `//=`, `%=`, `**=` no longer produce a spurious + "invalid variable name" linter error. Closes #29. +- **Unused-function checks accepted identifier substrings as calls.** + Function names now require a real Devanagari-aware call site outside + strings and comments. Closes #31. + - Known trade-off: because a call now requires `name(`, passing a + function as a value (`x = рдЬреЛрдбрд╝`, `map(рдЬреЛрдбрд╝, тАж)`) counts as *unused* + and warns. This is a deliberate choice favoring clarity for + learner-oriented code over higher-order usage. + +### Removed +- **Dropped support for Python 3.9** (end-of-life since October 2025). + Minimum supported version is now Python 3.10. This unblocks dev-tool + upgrades (pytest 9, build 1.5+) that no longer support 3.9. Closes #44. ## [0.3.0] тАФ 2026-04-17 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..25ef0ac --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances + of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**jha.bishwas@gmail.com**. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a9c50a..612a80a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,9 @@ Welcome! Your contributions help make the Maithili DSL stronger and more accessible. This guide covers local setup, the test workflow, and how to submit a change for review. +This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). +By participating, you are expected to uphold it. + --- ## ЁЯЫа Development setup @@ -74,6 +77,27 @@ import whitelist (`MAITHILI_MODULES`), or the safe-builtins list - тЬНя╕П Translate documentation into Maithili - ЁЯФР Report a security vulnerability via the process in `SECURITY.md` +### Finding something to work on + +Issues labeled [`good first issue`](https://github.com/alphacrack/python-maithili-dsl/labels/good%20first%20issue) +are scoped for newcomers; [`help wanted`](https://github.com/alphacrack/python-maithili-dsl/labels/help%20wanted) +issues are open to anyone. Labels follow a simple scheme: + +| Label group | Meaning | +|-------------|---------| +| `P0`тАУ`P3` | Priority, from critical to nice-to-have | +| `bug`, `enhancement`, `documentation`, `question` | Issue type | +| `security`, `testing`, `infra`, `packaging`, `quality`, `compliance` | Category | +| `area:transpiler`, `area:linter`, `area:cli` | Part of the codebase affected | +| `keyword-mapping` | Proposals for new Maithili keywords or module mappings | +| `translation` | Translating docs or messages into Maithili | +| `needs-triage` | New issue awaiting a maintainer's look | + +Comment on an issue before starting significant work so it can be assigned +to you and effort isn't duplicated. New keyword ideas should go through the +**ЁЯФд Keyword / mapping proposal** issue template тАФ native-speaker input on +word choice is especially welcome. + --- ## ЁЯУм Submitting changes @@ -87,4 +111,12 @@ import whitelist (`MAITHILI_MODULES`), or the safe-builtins list 4. Open a Pull Request targeting `development`. The PR template will prompt you for test evidence and any security considerations. +## ЁЯУж Releasing + +Releases are cut by a project maintainer following the checklist in +[`docs/RELEASE.md`](docs/RELEASE.md). The short version: +publishing is triggered by creating a **GitHub Release** and uses +**Trusted Publishing (OIDC)** тАФ no long-lived PyPI tokens are stored +in the repo. + Thanks for your interest! тЭдя╕П diff --git a/README.md b/README.md index f6e1b84..6ac346d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ python_maithili examples/hello.dmai # or, without installing the console script: python -m maithili_dsl examples/hello.dmai ``` -тЬЕ Works on Mac, Windows, and Linux, on Python 3.9 тАУ 3.12. +тЬЕ Works on Mac, Windows, and Linux, on Python 3.10 тАУ 3.12. + +See [`examples/README.md`](examples/README.md) for a description of every bundled example program. --- @@ -127,6 +129,9 @@ workflow. ## ЁЯдЭ Contributing See [`CONTRIBUTING.md`](CONTRIBUTING.md) to learn how you can help build and improve this DSL. +Good entry points are issues labeled +[`good first issue`](https://github.com/alphacrack/python-maithili-dsl/labels/good%20first%20issue). +This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). --- diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..a790e6d --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,27 @@ +# Getting Help + +## ЁЯУЪ Documentation + +- [README](README.md) тАФ installation, keyword table, module mappings, examples +- [CONTRIBUTING](CONTRIBUTING.md) тАФ development setup and PR workflow +- [`examples/`](examples/) тАФ runnable `.dmai` programs + +## ЁЯРЫ Found a bug? + +Open a [bug report](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=bug_report.yml) +with a minimal `.dmai` reproduction. + +## тЬи Want a feature or a new keyword? + +- New Maithili keyword or module mapping тЖТ [keyword proposal](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=keyword_proposal.yml) +- Anything else тЖТ [feature request](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=feature_request.yml) + +## тЭУ Usage questions + +Open a blank issue and add the `question` label. Please include your +`python_maithili --version` output and a code snippet. + +## ЁЯФР Security issues + +**Never open a public issue.** Follow [SECURITY.md](SECURITY.md) тАФ +email or use [private vulnerability reporting](https://github.com/alphacrack/python-maithili-dsl/security/advisories/new). diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..3c15485 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,163 @@ +# Release Process + +This document describes how to cut a new release of `python_maithili` +to PyPI. The workflow is **`.github/workflows/publish.yml`** and uses +**Trusted Publishing (OIDC)** тАФ no API tokens are stored in GitHub. + +--- + +## One-time setup (required before the first publish succeeds) + +### 1. Create GitHub Environments + +In the repo, go to **Settings тЖТ Environments** and create two +environments тАФ they give us a place to pin the OIDC trust and (for +production) an optional manual-approval gate. + +#### `testpypi` +- No required reviewers. +- Deployment branches: all branches (we publish to TestPyPI from + feature branches too). + +#### `pypi` +- **Required reviewers**: add yourself. Every production publish will + wait for explicit approval. This is the final safety gate. +- Deployment branches: protected tags matching `v*` only + (`v[0-9]+.[0-9]+.[0-9]+` and `v[0-9]+.[0-9]+.[0-9]+-*`). + +### 2. Register the Trusted Publisher on PyPI + +Go to +(owner only) and add a new "Trusted publisher": + +| Field | Value | +|---------------------|--------------------------------| +| Owner | `alphacrack` | +| Repository | `python-maithili-dsl` | +| Workflow filename | `publish.yml` | +| Environment name | `pypi` | + +### 3. Register the Trusted Publisher on TestPyPI + +TestPyPI doesn't have the project yet, so use a **pending publisher**. +Go to and under +"Pending trusted publishers" add: + +| Field | Value | +|---------------------|--------------------------------| +| PyPI Project Name | `python-maithili` | +| Owner | `alphacrack` | +| Repository | `python-maithili-dsl` | +| Workflow filename | `publish.yml` | +| Environment name | `testpypi` | + +The pending publisher converts to a real one the first time a workflow +run successfully creates the project on TestPyPI. + +--- + +## Routine release workflow + +### A. Dry-run build (no publish) + +Push a PR or run **Actions тЖТ Publish to PyPI тЖТ Run workflow тЖТ `build-only`**. +This executes the full pipeline except the publish step: + +- Build sdist + wheel. +- `twine check --strict` metadata validation. +- Install the wheel in a clean venv. +- Run the full pytest suite against the installed wheel. +- Upload the `dist/` artifact for inspection. + +No credentials are required and nothing touches the network package +indexes. Use this whenever you want to confirm the package will build. + +### B. Publish to TestPyPI (staging verification) + +Two ways to trigger: + +1. **Manual**: Actions тЖТ Publish to PyPI тЖТ Run workflow тЖТ `testpypi`. +2. **Automatic**: create a GitHub **pre-release** (check "This is a + pre-release" when making the Release). Tag format: `v0.X.Y-rc1`, + `v0.X.Y-dev1`, etc. + +Version numbers you publish to TestPyPI are **separate** from +production PyPI тАФ TestPyPI has its own index. Verify the install: + +```bash +pip install \ + --index-url https://test.pypi.org/simple/ \ + --extra-index-url https://pypi.org/simple/ \ + python-maithili==0.X.Y +python_maithili --version +``` + +Any version on TestPyPI cannot be re-used. If you need to iterate, +bump the version (`0.3.0.dev0`, `0.3.0.dev1`, ...) in +`maithili_dsl/__init__.py` before re-triggering. + +### C. Publish to production PyPI + +**Pre-flight checklist:** + +- [ ] `CHANGELOG.md` has a section for the new version with date filled in. +- [ ] `maithili_dsl/__init__.py` has `__version__` set to the new version. +- [ ] `pyproject.toml` has matching `version = "..."`. +- [ ] `pytest` runs green on `main` CI. +- [ ] The same version was successfully published to TestPyPI and installed + cleanly (step B). + +**Cut the release:** + +1. Ensure `main` has the final release commit on it (all three version + strings updated, CHANGELOG finalized). +2. Create a new **GitHub Release** (not a pre-release): + - Tag: `v0.X.Y` (must match `__version__` exactly; the workflow + enforces this and refuses to publish on mismatch). + - Target: `main`. + - Title: `v0.X.Y`. + - Body: the corresponding CHANGELOG section. + - Leave "This is a pre-release" **unchecked**. +3. Click **Publish release**. The workflow fires. +4. If the `pypi` environment has required reviewers, approve the + deployment in the Actions run. +5. After the run finishes, verify: + + ```bash + pip install python-maithili==0.X.Y + python_maithili --version + ``` + +--- + +## Version bump convention + +This project uses [Semantic Versioning](https://semver.org/): + +| Change | Bump | +|------------------------------------------------|-------| +| Breaking API change (exit code semantics, etc) | MAJOR | +| New keyword / module / feature | MINOR | +| Bugfix, doc change, internal refactor | PATCH | + +Version is tracked in three places that must stay in sync: + +1. `maithili_dsl/__init__.py` тАФ `__version__` (single source of truth). +2. `pyproject.toml` тАФ `version`. +3. `CHANGELOG.md` тАФ section header and date. + +The publish workflow's "Verify release tag matches package version" +step will fail the production publish if these drift. + +--- + +## Recovery: what if a bad version reaches PyPI? + +PyPI does not allow re-uploading the same filename. If you published +a broken release: + +1. **Yank** (do not delete) the broken version on PyPI: + тЖТ + pick the version тЖТ "Yank release". This hides it from `pip install` + without breaking already-pinned consumers. +2. Bump PATCH and release the fix via the normal flow above. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..880248c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,85 @@ +# Examples + +This directory contains runnable Maithili DSL (`.dmai`) programs that demonstrate +the language. Run any of them with either entry point: + +```bash +python_maithili examples/hello.dmai +# or, without installing the console script: +python -m maithili_dsl examples/hello.dmai +``` + +> **Note:** `error.dmai` is intentionally broken тАФ it is used by CI to verify +> that the linter/transpiler correctly rejects invalid programs. It is expected +> to exit with a non-zero status and should not be run as a normal example. + +## `hello.dmai` + +Prints a greeting in Maithili. + +- **Keywords/modules shown:** `рдХрд╛рд░реНрдп` (function definition), `рдЫрдкрд╛рдЙ` (print), + top-level call. +- **Expected output:** + + ``` + рд╣рдо рдореИрдерд┐рд▓реА рдореЗрдВ рдХреЛрдб рдХрд╜ рд░рд╣рд▓ рдЫреАред + ``` + +## `person.dmai` + +Defines a `рд╡реНрдпрдХреНрддрд┐` (person) class, instantiates it, and calls a method. + +- **Keywords/modules shown:** `рд╡рд░реНрдЧ` (class), `рдирд╡` (constructor), `рд╕реНрд╡рдпрдВ` (self), + method definition and invocation. +- **Expected output:** + + ``` + рд╣рдорд░ рдирд╛рдо рд╕реБрдорди рдЕрдЫрд┐ред + ``` + +## `calculator.dmai` + +Implements basic arithmetic (`рдЬреЛрдбрд╝`/`рдШрдЯрд╛рдЙ`/`рдЧреБрдгрд╛`/`рднрд╛рдЧ`) and prints a +multiplication table. + +- **Keywords/modules shown:** functions returning values, `str()`, `range()`, + string concatenation, loops (`рдкреНрд░рддреНрдпреЗрдХ`). +- **Expected output (abridged):** + + ``` + === рдореИрдерд┐рд▓реА рдХреИрд▓рдХреБрд▓реЗрдЯрд░ === + рдЬреЛрдбрд╝ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 15 + рдШрдЯрд╛рдЙ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 5 + рдЧреБрдгрд╛ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 50 + рднрд╛рдЧ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 2.0 + ``` + +## `shopping_list.dmai` + +Builds a list, appends/removes items, iterates, and computes a sum and average. + +- **Keywords/modules shown:** lists (`[]`), `append`/`remove`, `len()`, + `рдпрджрд┐` (if), `рдкреНрд░рддреНрдпреЗрдХ` (for), arithmetic and averages. +- **Expected output (abridged):** + + ``` + === рдЦрд░реАрджрд╛рд░реА рд╕реВрдЪреА === + - рджреВрдз + - рд░реЛрдЯреА + - рдЕрдВрдбрд╛ + - рдЪрд╛рд╡рд▓ + рдХреБрд▓ рд╡рд╕реНрддреБ: 4 + рджреВрдз рд╕реВрдЪреА рдореЗрдВ рдЕрдЫрд┐! + рдЕрдВрдбрд╛ рд╣рдЯрд╛рдПрд▓ рдЧреЗрд▓ред рдирдИ рд╕реВрдЪреА: + - рджреВрдз + - рд░реЛрдЯреА + - рдЪрд╛рд╡рд▓ + ``` + +## `error.dmai` + +Intentionally invalid program (uses non-Maithili tokens and broken syntax) used +to confirm the linter rejects bad input. + +- **Keywords/modules shown:** n/a (negative test case). +- **Expected output:** none тАФ the run exits with a non-zero status. diff --git a/maithili_dsl/__init__.py b/maithili_dsl/__init__.py index a564c5e..64837b5 100644 --- a/maithili_dsl/__init__.py +++ b/maithili_dsl/__init__.py @@ -3,7 +3,7 @@ from maithili_dsl.transpiler.linter import lint_maithili_code, translate_exception_to_maithili from maithili_dsl.transpiler.numeral import convert_devanagari_numerals -__version__ = "0.3.0" +__version__ = "0.4.0" __all__ = [ "__version__", diff --git a/maithili_dsl/transpiler/linter.py b/maithili_dsl/transpiler/linter.py index 0459e93..75510cc 100644 --- a/maithili_dsl/transpiler/linter.py +++ b/maithili_dsl/transpiler/linter.py @@ -2,6 +2,8 @@ import re +from .transpile import _make_keyword_pattern, _tokenize_preserving_strings + LINT_CONFIG = { "enforce_snake_case": False, "max_line_length": 80, @@ -66,6 +68,9 @@ def lint_maithili_code(code, config=LINT_CONFIG): pass # == comparison else: left_side = stripped.split("=")[0].strip() + left_side = re.sub( + r"(?:\*\*|//|[+\-*/%])$", "", left_side + ).rstrip() # Skip print statements, function calls, and attribute assignments (e.g., рд╕реНрд╡рдпрдВ.рдирд╛рдо) if not (stripped.startswith("рдЫрдкрд╛рдЙ(") or "(" in left_side or left_side.startswith("рдЫрдкрд╛рдЙ") or "." in left_side): @@ -100,7 +105,14 @@ def lint_maithili_code(code, config=LINT_CONFIG): for f in declared_functions: if f == "рдирд╡": continue # constructor is called implicitly - used = any(f in line for line in lines if not line.strip().startswith("рдХрд╛рд░реНрдп")) + call_pattern = re.compile(_make_keyword_pattern(f).pattern + r"\s*\(") + used = any( + call_pattern.search(text) + for line in lines + if not line.strip().startswith("рдХрд╛рд░реНрдп") + for is_string, text in _tokenize_preserving_strings(line) + if not is_string + ) if not used: errors.append(f"рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп '{f}' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐") diff --git a/pyproject.toml b/pyproject.toml index 77745a3..08b6158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,17 +4,16 @@ build-backend = "setuptools.build_meta" [project] name = "python_maithili" -version = "0.3.0" +version = "0.4.0" description = "Run Python code written in Maithili using Devanagari script" readme = "README.md" license = { file = "LICENSE" } authors = [ { name = "Bishwas Jha", email = "jha.bishwas@gmail.com" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -56,6 +55,7 @@ omit = [ ] [tool.coverage.report] +fail_under = 85 exclude_lines = [ "pragma: no cover", "if __name__ == .__main__.:", diff --git a/requirements-dev.txt b/requirements-dev.txt index 3a4df92..7065e76 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,9 @@ # Development dependencies for python_maithili. # Runtime has zero dependencies тАФ everything here is for tests and tooling. -pytest>=7.4,<9 -pytest-cov>=4.1,<6 -pytest-timeout>=2.2,<3 +pytest>=9.1.1,<10 +pytest-cov>=7.1.0,<8 +pytest-timeout>=2.4.0,<3 + +# Build + release tooling (used by docs/RELEASE.md workflow). +build>=1.5.0,<2 +twine>=6.2.0,<7 diff --git a/setup.py b/setup.py index f6ea8c9..077aea9 100644 --- a/setup.py +++ b/setup.py @@ -35,12 +35,11 @@ def _read_version() -> str: }, classifiers=[ 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], - python_requires='>=3.9', + python_requires='>=3.10', ) diff --git a/tests/test_linter.py b/tests/test_linter.py index e13bf93..e15f557 100644 --- a/tests/test_linter.py +++ b/tests/test_linter.py @@ -32,6 +32,32 @@ def test_simple_function_with_usage_passes(): assert lint_maithili_code(code) == [] +def test_function_name_inside_longer_identifier_is_unused(): + code = ( + "рдХрд╛рд░реНрдп рдЬреЛрдбрд╝():\n" + " рдлреЗрд░ рдХрд░реВ рез\n" + "\n" + "рдЬреЛрдбрд╝рд▓ = рел\n" + ) + + errors = lint_maithili_code(code) + + assert "рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп 'рдЬреЛрдбрд╝' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐" in errors + + +def test_function_name_inside_string_is_unused(): + code = ( + "рдХрд╛рд░реНрдп рдЬреЛрдбрд╝():\n" + " рдлреЗрд░ рдХрд░реВ рез\n" + "\n" + "рдЫрдкрд╛рдЙ(\"рдЬреЛрдбрд╝()\")\n" + ) + + errors = lint_maithili_code(code) + + assert "рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп 'рдЬреЛрдбрд╝' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐" in errors + + def test_class_with_constructor_passes(): code = ( "рд╡рд░реНрдЧ рд╡реНрдпрдХреНрддрд┐:\n" @@ -78,6 +104,13 @@ def test_gte_operator(self): assert not any("рдЪрд░ рдирд╛рдо" in e for e in errors) +@pytest.mark.parametrize("operator", ["+=", "-=", "*=", "/=", "//=", "%=", "**="]) +def test_augmented_assignment_not_flagged_as_invalid_name(operator): + code = f"рдХ = реж\nрдХ {operator} рез\n" + errors = lint_maithili_code(code) + assert not any("рдЪрд░ рдирд╛рдо" in error for error in errors) + + # --------------------------------------------------------------------------- # Structural errors still fire # ---------------------------------------------------------------------------