From 55a4ac71e10ad79f35f20455869d51a2d7e9e8eb Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Wed, 10 Jun 2026 20:43:19 +0530 Subject: [PATCH 1/2] feat!: rename project from pylings to pythonlings The PyPI package (previously python-learnings), the CLI command (previously pylings), and the Python package are all now pythonlings. Legacy .pylings/ workspace state directories are migrated automatically on the next run, preserving progress and reset snapshots. Historical design docs under docs/superpowers/ keep the old name as a record of past work. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/publish.yml | 12 +-- .gitignore | 14 +-- AGENTS.md | 14 +-- CHANGELOG.md | 14 ++- CLAUDE.md | 61 ++++++++++++ CNAME | 2 +- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 12 +-- RELEASE.md | 24 ++--- Readme.md | 87 +++++++++--------- checks/pathlib/pathlib2.py | 2 +- docs-site/CNAME | 2 +- docs-site/contributing.md | 6 +- docs-site/curriculum.md | 4 +- docs-site/index.md | 20 ++-- docs-site/interface.md | 6 +- docs-site/local-docs.md | 8 +- docs-site/quick-start.md | 36 ++++---- docs-site/roadmap.md | 6 +- docs-site/robots.txt | 2 +- docs/DEMO_GIF.md | 16 ++-- docs/RELEASE_PROCESS.md | 6 +- ...{pylings-demo.gif => pythonlings-demo.gif} | Bin docs/demo.tape | 12 +-- docs/roadmap/0.3.0.md | 44 ++++----- exercises/json/json5.py | 2 +- exercises/json/json8.py | 2 +- exercises/pathlib/pathlib2.py | 2 +- info.toml | 2 +- mkdocs.yml | 10 +- pyproject.toml | 28 +++--- {pylings => pythonlings}/__init__.py | 0 {pylings => pythonlings}/__main__.py | 2 +- {pylings => pythonlings}/app.py | 16 ++-- {pylings => pythonlings}/cli.py | 81 ++++++++-------- {pylings => pythonlings}/core/__init__.py | 0 {pylings => pythonlings}/core/curriculum.py | 22 ++++- {pylings => pythonlings}/core/docs.py | 6 +- {pylings => pythonlings}/core/exercise.py | 2 +- {pylings => pythonlings}/core/manifest.py | 6 +- {pylings => pythonlings}/core/reset.py | 12 +-- {pylings => pythonlings}/core/runner.py | 8 +- {pylings => pythonlings}/core/solutions.py | 2 +- {pylings => pythonlings}/core/state.py | 8 +- {pylings => pythonlings}/core/watcher.py | 0 {pylings => pythonlings}/docs/NOTICE.md | 2 +- {pylings => pythonlings}/docs/__init__.py | 0 {pylings => pythonlings}/docs/index.json | 0 .../docs/topics/__init__.py | 0 {pylings => pythonlings}/docs/topics/async.md | 2 +- .../docs/topics/classes.md | 2 +- .../docs/topics/collections.md | 2 +- .../docs/topics/comprehensions.md | 2 +- .../docs/topics/conditionals.md | 2 +- .../docs/topics/context_managers.md | 2 +- .../docs/topics/dataclasses.md | 2 +- .../docs/topics/datetime.md | 2 +- .../docs/topics/decorators.md | 2 +- .../docs/topics/dictionaries.md | 2 +- {pylings => pythonlings}/docs/topics/enums.md | 2 +- .../docs/topics/exceptions.md | 2 +- .../docs/topics/file_io.md | 2 +- .../docs/topics/functional.md | 2 +- .../docs/topics/functions.md | 2 +- .../docs/topics/generators.md | 2 +- .../docs/topics/itertools.md | 2 +- {pylings => pythonlings}/docs/topics/json.md | 2 +- {pylings => pythonlings}/docs/topics/lists.md | 2 +- {pylings => pythonlings}/docs/topics/loops.md | 2 +- .../docs/topics/modules.md | 2 +- .../docs/topics/oop_advanced.md | 2 +- .../docs/topics/pathlib.md | 2 +- .../docs/topics/recursion.md | 2 +- {pylings => pythonlings}/docs/topics/regex.md | 2 +- {pylings => pythonlings}/docs/topics/sets.md | 2 +- .../docs/topics/strings.md | 2 +- .../docs/topics/testing.md | 2 +- .../docs/topics/tuples.md | 2 +- .../docs/topics/type_hints.md | 2 +- .../docs/topics/variables.md | 2 +- .../pythonlings.tcss | 0 {pylings => pythonlings}/screens/__init__.py | 0 {pylings => pythonlings}/screens/docs.py | 6 +- .../screens/topic_picker.py | 8 +- {pylings => pythonlings}/screens/track.py | 20 ++-- {pylings => pythonlings}/widgets/__init__.py | 0 .../widgets/editor_pane.py | 4 +- .../widgets/exercise_tree.py | 8 +- .../widgets/output_panel.py | 4 +- {pylings => pythonlings}/widgets/progress.py | 2 +- scripts/fetch_python_docs.py | 10 +- scripts/generate_demo_gif.sh | 2 +- solutions/_answers.py | 8 +- tests/integration/test_cli_cold_start.py | 4 +- tests/integration/test_cli_dry_run.py | 2 +- tests/integration/test_cli_hint.py | 2 +- tests/integration/test_cli_reset.py | 8 +- tests/integration/test_cli_run.py | 2 +- tests/integration/test_cli_solution.py | 4 +- tests/integration/test_cli_topics.py | 4 +- tests/integration/test_cli_verify.py | 2 +- tests/integration/test_cli_workspace.py | 4 +- tests/integration/test_installed_package.py | 2 +- tests/integration/test_solution_verify.py | 6 +- tests/tui/test_app_pilot.py | 56 +++++------ tests/tui/test_editor_pane.py | 4 +- tests/tui/test_output_panel.py | 4 +- tests/unit/test_curriculum.py | 10 +- tests/unit/test_docs.py | 6 +- tests/unit/test_exercise.py | 2 +- tests/unit/test_manifest.py | 4 +- tests/unit/test_reset.py | 18 ++-- tests/unit/test_runner.py | 8 +- tests/unit/test_state.py | 10 +- tests/unit/test_watcher.py | 2 +- 117 files changed, 509 insertions(+), 422 deletions(-) create mode 100644 CLAUDE.md rename docs/assets/demos/{pylings-demo.gif => pythonlings-demo.gif} (100%) rename {pylings => pythonlings}/__init__.py (100%) rename {pylings => pythonlings}/__main__.py (63%) rename {pylings => pythonlings}/app.py (86%) rename {pylings => pythonlings}/cli.py (75%) rename {pylings => pythonlings}/core/__init__.py (100%) rename {pylings => pythonlings}/core/curriculum.py (80%) rename {pylings => pythonlings}/core/docs.py (93%) rename {pylings => pythonlings}/core/exercise.py (95%) rename {pylings => pythonlings}/core/manifest.py (97%) rename {pylings => pythonlings}/core/reset.py (70%) rename {pylings => pythonlings}/core/runner.py (94%) rename {pylings => pythonlings}/core/solutions.py (90%) rename {pylings => pythonlings}/core/state.py (91%) rename {pylings => pythonlings}/core/watcher.py (100%) rename {pylings => pythonlings}/docs/NOTICE.md (85%) rename {pylings => pythonlings}/docs/__init__.py (100%) rename {pylings => pythonlings}/docs/index.json (100%) rename {pylings => pythonlings}/docs/topics/__init__.py (100%) rename {pylings => pythonlings}/docs/topics/async.md (91%) rename {pylings => pythonlings}/docs/topics/classes.md (96%) rename {pylings => pythonlings}/docs/topics/collections.md (91%) rename {pylings => pythonlings}/docs/topics/comprehensions.md (96%) rename {pylings => pythonlings}/docs/topics/conditionals.md (95%) rename {pylings => pythonlings}/docs/topics/context_managers.md (96%) rename {pylings => pythonlings}/docs/topics/dataclasses.md (91%) rename {pylings => pythonlings}/docs/topics/datetime.md (91%) rename {pylings => pythonlings}/docs/topics/decorators.md (96%) rename {pylings => pythonlings}/docs/topics/dictionaries.md (97%) rename {pylings => pythonlings}/docs/topics/enums.md (91%) rename {pylings => pythonlings}/docs/topics/exceptions.md (96%) rename {pylings => pythonlings}/docs/topics/file_io.md (97%) rename {pylings => pythonlings}/docs/topics/functional.md (91%) rename {pylings => pythonlings}/docs/topics/functions.md (96%) rename {pylings => pythonlings}/docs/topics/generators.md (95%) rename {pylings => pythonlings}/docs/topics/itertools.md (92%) rename {pylings => pythonlings}/docs/topics/json.md (90%) rename {pylings => pythonlings}/docs/topics/lists.md (96%) rename {pylings => pythonlings}/docs/topics/loops.md (96%) rename {pylings => pythonlings}/docs/topics/modules.md (97%) rename {pylings => pythonlings}/docs/topics/oop_advanced.md (91%) rename {pylings => pythonlings}/docs/topics/pathlib.md (91%) rename {pylings => pythonlings}/docs/topics/recursion.md (96%) rename {pylings => pythonlings}/docs/topics/regex.md (91%) rename {pylings => pythonlings}/docs/topics/sets.md (96%) rename {pylings => pythonlings}/docs/topics/strings.md (96%) rename {pylings => pythonlings}/docs/topics/testing.md (91%) rename {pylings => pythonlings}/docs/topics/tuples.md (96%) rename {pylings => pythonlings}/docs/topics/type_hints.md (91%) rename {pylings => pythonlings}/docs/topics/variables.md (96%) rename pylings/pylings.tcss => pythonlings/pythonlings.tcss (100%) rename {pylings => pythonlings}/screens/__init__.py (100%) rename {pylings => pythonlings}/screens/docs.py (95%) rename {pylings => pythonlings}/screens/topic_picker.py (94%) rename {pylings => pythonlings}/screens/track.py (93%) rename {pylings => pythonlings}/widgets/__init__.py (100%) rename {pylings => pythonlings}/widgets/editor_pane.py (92%) rename {pylings => pythonlings}/widgets/exercise_tree.py (88%) rename {pylings => pythonlings}/widgets/output_panel.py (98%) rename {pylings => pythonlings}/widgets/progress.py (92%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b713fa0..b2553be 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,4 +15,4 @@ labels: bug - OS: - Python: - Terminal: -- Pylings version: +- Pythonlings version: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d870f43..c954c0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: python-version: ${{ matrix.python-version }} - run: pip install -e ".[dev]" - run: pytest -v - - run: pylings --root tests/fixtures/passing_curriculum verify + - run: pythonlings --root tests/fixtures/passing_curriculum verify - run: python -m pip install build - run: python -m build - run: python -m pip install --force-reinstall dist/*.whl - - run: pylings init --path /tmp/pylings-workspace - - run: pylings --root /tmp/pylings-workspace list + - run: pythonlings init --path /tmp/pythonlings-workspace + - run: pythonlings --root /tmp/pythonlings-workspace list diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 898c867..d962899 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,10 +30,10 @@ jobs: - run: python -m pip install build - run: python -m build - run: python -m pip install --force-reinstall dist/*.whl - - run: pylings --version - - run: pylings init --path /tmp/pylings-workspace - - run: pylings --root /tmp/pylings-workspace list - - run: pylings --root /tmp/pylings-workspace solution variables1 - - run: pylings --root /tmp/pylings-workspace reset variables1 --yes - - run: pylings --root tests/fixtures/passing_curriculum verify + - run: pythonlings --version + - run: pythonlings init --path /tmp/pythonlings-workspace + - run: pythonlings --root /tmp/pythonlings-workspace list + - run: pythonlings --root /tmp/pythonlings-workspace solution variables1 + - run: pythonlings --root /tmp/pythonlings-workspace reset variables1 --yes + - run: pythonlings --root tests/fixtures/passing_curriculum verify - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index c258dde..4ab9dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -128,13 +128,13 @@ dmypy.json # Pyre type checker .pyre/ -# Pylings runtime state (root install and any nested fixture roots) -.pylings/ +# Pythonlings runtime state (root install and any nested fixture roots) +.pythonlings/ .superpowers/ .worktrees -# Local artifacts from older pylings workspaces; not part of this package. -/pylings/.gitignore -/pylings/.git/ -/pylings/.pylings.toml -/pylings/exercises/ +# Local artifacts from older pythonlings workspaces; not part of this package. +/pythonlings/.gitignore +/pythonlings/.git/ +/pythonlings/.pythonlings.toml +/pythonlings/exercises/ diff --git a/AGENTS.md b/AGENTS.md index fde0978..71e5eb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,17 +2,17 @@ ## Project Structure & Module Organization -`pylings/` contains the installable application package. Core exercise loading, workspace setup, state, reset, solutions, and runner logic live in `pylings/core/`; CLI entry points are in `pylings/cli.py` and `pylings/__main__.py`; Textual screens/widgets live in `pylings/screens/` and `pylings/widgets/`; `pylings/pylings.tcss` holds TUI styles. +`pythonlings/` contains the installable application package. Core exercise loading, workspace setup, state, reset, solutions, and runner logic live in `pythonlings/core/`; CLI entry points are in `pythonlings/cli.py` and `pythonlings/__main__.py`; Textual screens/widgets live in `pythonlings/screens/` and `pythonlings/widgets/`; `pythonlings/pythonlings.tcss` holds TUI styles. Curriculum files are split between `exercises//.py` for learner code, `checks//.py` for hidden assertions, and `solutions/.py` for reference answers. Keep these trees aligned with `info.toml`, which defines order, hints, and docs URLs. Tests live in `tests/unit/`, `tests/integration/`, and `tests/tui/`, with fixtures in `tests/fixtures/`. ## Build, Test, and Development Commands -- `pip install -e ".[dev]"`: install pylings locally with pytest dependencies. -- `pylings init --path ./learn-python`: create a self-contained learner workspace. -- `pylings`, `pylings topics`, `pylings list`: launch the TUI or inspect progress. -- `pylings run variables1`, `pylings dry-run variables1`, `pylings solution variables1`: test exercise and solution flows. -- `pylings --root tests/fixtures/passing_curriculum verify`: smoke-test a known passing fixture. +- `pip install -e ".[dev]"`: install pythonlings locally with pytest dependencies. +- `pythonlings init --path ./learn-python`: create a self-contained learner workspace. +- `pythonlings`, `pythonlings topics`, `pythonlings list`: launch the TUI or inspect progress. +- `pythonlings run variables1`, `pythonlings dry-run variables1`, `pythonlings solution variables1`: test exercise and solution flows. +- `pythonlings --root tests/fixtures/passing_curriculum verify`: smoke-test a known passing fixture. - `python -m pytest -q`: run the full suite configured in `pyproject.toml`. - `python -m build`: build source and wheel distributions. @@ -22,7 +22,7 @@ Use Python 3.11+ idioms and 4-space indentation. Prefer small, typed functions w ## Testing Guidelines -Use pytest for all tests; async tests are supported by `pytest-asyncio` in auto mode. Add unit tests for core behavior, integration tests for CLI/workspace flows, and TUI tests for Textual interactions. When changing curriculum, update `exercises/`, `checks/`, `solutions/`, and `info.toml`, then run relevant pytest files plus `pylings --root tests/fixtures/passing_curriculum verify`. +Use pytest for all tests; async tests are supported by `pytest-asyncio` in auto mode. Add unit tests for core behavior, integration tests for CLI/workspace flows, and TUI tests for Textual interactions. When changing curriculum, update `exercises/`, `checks/`, `solutions/`, and `info.toml`, then run relevant pytest files plus `pythonlings --root tests/fixtures/passing_curriculum verify`. ## Commit & Pull Request Guidelines diff --git a/CHANGELOG.md b/CHANGELOG.md index d83463c..f7e162e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,18 @@ # Changelog -All notable changes to this project are documented here. Pylings follows +All notable changes to this project are documented here. Pythonlings follows Semantic Versioning. +## [Unreleased] + +### Changed + +- The project is renamed from pylings to **pythonlings**. The PyPI package + (previously `python-learnings`), the CLI command (previously `pylings`), + and the Python package are all now `pythonlings`. Existing workspaces are + migrated automatically: a legacy `.pylings/` state directory is renamed to + `.pythonlings/` on the next run, preserving progress and reset snapshots. + ## [0.2.0] - 2026-05-30 ### Added @@ -34,7 +44,7 @@ Semantic Versioning. - 292 Python exercises across 31 topics with mirrored hidden checks. - Bundled local Python documentation snippets generated from official docs. - In-app documentation modal with `F5`, `Esc`, and `O` keyboard flow. -- PyPI distribution name `python-learnings`, which installs the `pylings` command. +- PyPI distribution name `pythonlings`, which installs the `pythonlings` command. - Contributor guide, screenshots, release flow notes, and MIT license. ### Verified diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5e59a24 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +Pythonlings is "Rustlings for Python": a terminal TUI (Textual) where learners fix small broken exercises and checks rerun on save. Published on PyPI as `pythonlings` (formerly `python-learnings`; the unrelated `pylings` PyPI name belongs to a different project). + +## Commands + +```bash +pip install -e ".[dev]" # local install with pytest deps + +python -m pytest -q # full suite +python -m pytest tests/unit/test_runner.py -q # one file +python -m pytest tests/unit/test_state.py::test_name -q # one test + +pylings --root tests/fixtures/passing_curriculum verify # smoke-test: all solutions pass their checks + +python -m build # sdist + wheel +``` + +Manual testing of flows: `pylings init --path ./learn-python` (create a learner workspace), `pylings` (TUI), `pylings run variables1`, `pylings dry-run variables1`, `pylings solution variables1`, `pylings hint`, `pylings list`, `pylings topics`, `pylings reset`. `--root` points any command at an arbitrary workspace (used heavily by tests against `tests/fixtures/`). + +## Architecture + +Two distinct trees in this repo: + +1. **The application** — `pylings/` (installable package) +2. **The curriculum** — repo-root `exercises/`, `checks/`, `solutions/`, and `info.toml` + +At build time, hatch `force-include` maps the curriculum into the wheel as `pylings/curriculum/` (see `pyproject.toml`). `pylings init` copies that bundled curriculum into a self-contained learner workspace; progress lives in `/.pylings/state.json` (written atomically, with `.bak` recovery on corruption). + +### Curriculum model + +Each exercise is a triple that must stay in sync, plus a manifest entry: + +- `exercises//.py` — the broken code the learner edits, containing the `# I AM NOT DONE` marker (defined in `core/exercise.py`) +- `checks//.py` — hidden bare `assert` statements (not pytest) +- `solutions/.py` — reference answer +- `info.toml` — ordered `[[exercises]]` entries with `name`, `path`, `hint`, `docs` URL; this file is the source of truth for exercise order and topics + +When changing curriculum, update all four, then run the relevant tests plus `pylings --root tests/fixtures/passing_curriculum verify`. Exercise names are topic + ordinal (`variables1`, `collections10`). + +### How checks run (`core/runner.py`) + +An exercise passes when a generated runner script `exec()`s the exercise source and then the check source in a shared namespace, in a fresh subprocess with a 5s timeout. The check sees the exercise's variables directly — that's why checks are bare asserts. Passing checks alone aren't enough: the learner must also remove `# I AM NOT DONE` to advance. + +### Layering + +- `pylings/core/` — all filesystem, manifest, state, reset, solutions, runner, and watcher logic. No UI imports. +- `pylings/screens/` and `pylings/widgets/` — Textual UI only; `pylings/app.py` wires them up; `pylings.tcss` holds styles. +- `pylings/cli.py` — argparse subcommands; entry point `pylings = "pylings.cli:main"`. + +Keep UI behavior in screens/widgets and behavior logic in core — tests depend on this split (`tests/unit/` for core, `tests/integration/` for CLI/workspace flows, `tests/tui/` for Textual pilot tests, fixtures in `tests/fixtures/`). + +## Conventions + +- `requires-python = ">=3.9"`: guard newer-stdlib usage (e.g. `tomllib` falls back to `tomli` in `core/manifest.py`); `from __future__ import annotations` at the top of modules. +- Async tests run under `pytest-asyncio` in auto mode (configured in `pyproject.toml`). +- Tests are named `test_.py` / `test_`. +- Commits use conventional prefixes (`feat:`, `fix:`, `docs:`, `chore:`); work flows through `feature/*` → `dev` → `main`. +- `AGENTS.md` holds the same contributor guidelines in long form. diff --git a/CNAME b/CNAME index 4d8f91d..de05cbd 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -pylings.abhik.ai \ No newline at end of file +pythonlings.abhik.ai \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1811537..a621414 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,6 +1,6 @@ # Code of Conduct -Pylings follows the Contributor Covenant Code of Conduct, version 2.1. +Pythonlings follows the Contributor Covenant Code of Conduct, version 2.1. ## Our Standards diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37e5550..27dbd9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,15 @@ # Contributing -Pylings is actively developed and **open to contributors** — beginners welcome. -The fastest way in is a [`good first issue`](https://github.com/abhiksark/pylings/issues?q=is%3Aopen+label%3A%22good+first+issue%22). +Pythonlings is actively developed and **open to contributors** — beginners welcome. +The fastest way in is a [`good first issue`](https://github.com/abhiksark/pythonlings/issues?q=is%3Aopen+label%3A%22good+first+issue%22). ## Where the work is - **[0.3.0 roadmap](docs/roadmap/0.3.0.md)** — the current focus (wider adoption for beginners). Each roadmap issue is written to be picked up cold: it has context, scope, the exact files to touch, and how to verify. -- Browse open issues by label: [`good first issue`](https://github.com/abhiksark/pylings/issues?q=is%3Aopen+label%3A%22good+first+issue%22), - [`help wanted`](https://github.com/abhiksark/pylings/issues?q=is%3Aopen+label%3A%22help+wanted%22). +- Browse open issues by label: [`good first issue`](https://github.com/abhiksark/pythonlings/issues?q=is%3Aopen+label%3A%22good+first+issue%22), + [`help wanted`](https://github.com/abhiksark/pythonlings/issues?q=is%3Aopen+label%3A%22help+wanted%22). ## Claiming an issue @@ -23,8 +23,8 @@ No need to wait for a formal assignment; claiming by comment is enough. ## Development Setup ```bash -git clone git@github.com:abhiksark/pylings.git -cd pylings +git clone git@github.com:abhiksark/pythonlings.git +cd pythonlings pip install -e ".[dev]" # or: uv pip install -e ".[dev]" python -m pytest -q ``` diff --git a/RELEASE.md b/RELEASE.md index 23e00c2..6c0e440 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,13 +1,13 @@ # Release Checklist -Pylings follows Semantic Versioning. Use full `MAJOR.MINOR.PATCH` versions in +Pythonlings follows Semantic Versioning. Use full `MAJOR.MINOR.PATCH` versions in package metadata and annotated git tags such as `v0.1.0`. ## Package Name -The PyPI name `pylings` is already owned by another project. This repository's -distribution name is `python-learnings`; installing it provides the `pylings` -console command. Do not document `pip install pylings` for this project unless +The PyPI name `pythonlings` is already owned by another project. This repository's +distribution name is `pythonlings`; installing it provides the `pythonlings` +console command. Do not document `pip install pythonlings` for this project unless the package name is transferred. ## Pre-Release Verification @@ -16,21 +16,21 @@ Run these checks from a clean working tree before tagging: ```bash python -m pytest -q -pylings --root tests/fixtures/passing_curriculum verify +pythonlings --root tests/fixtures/passing_curriculum verify python -m build python -m pip install --force-reinstall dist/python_learnings-*.whl -pylings --version -tmp=$(mktemp -d /tmp/pylings-release.XXXXXX) -pylings init --path "$tmp" -pylings --root "$tmp" list -pylings --root "$tmp" solution variables1 -pylings --root "$tmp" reset variables1 --yes +pythonlings --version +tmp=$(mktemp -d /tmp/pythonlings-release.XXXXXX) +pythonlings init --path "$tmp" +pythonlings --root "$tmp" list +pythonlings --root "$tmp" solution variables1 +pythonlings --root "$tmp" reset variables1 --yes ``` Expected release version for `v0.1.0`: ```text -pylings 0.1.0 +pythonlings 0.1.0 ``` ## Tag And Publish diff --git a/Readme.md b/Readme.md index f0c4465..da54251 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,16 @@ -# Pylings +# Pythonlings -[![PyPI](https://img.shields.io/pypi/v/python-learnings)](https://pypi.org/project/python-learnings/) -[![Python](https://img.shields.io/pypi/pyversions/python-learnings)](https://pypi.org/project/python-learnings/) -[![CI](https://github.com/abhiksark/pylings/actions/workflows/ci.yml/badge.svg)](https://github.com/abhiksark/pylings/actions/workflows/ci.yml) -[![Downloads](https://img.shields.io/pypi/dm/python-learnings)](https://pypi.org/project/python-learnings/) +[![PyPI](https://img.shields.io/pypi/v/pythonlings)](https://pypi.org/project/pythonlings/) +[![Python](https://img.shields.io/pypi/pyversions/pythonlings)](https://pypi.org/project/pythonlings/) +[![CI](https://github.com/abhiksark/pythonlings/actions/workflows/ci.yml/badge.svg)](https://github.com/abhiksark/pythonlings/actions/workflows/ci.yml) +[![Downloads](https://img.shields.io/pypi/dm/pythonlings)](https://pypi.org/project/pythonlings/) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow)](LICENSE) -Documentation: [pylings.abhik.ai](https://pylings.abhik.ai/) +Documentation: [pythonlings.abhik.ai](https://pythonlings.abhik.ai/) **Rustlings for Python — learn by fixing 292 tiny broken programs in your terminal.** -Pylings helps you learn Python by fixing small broken programs and watching +Pythonlings helps you learn Python by fixing small broken programs and watching checks rerun as you type. It is built for beginner Python practice, coding practice, and self-paced Python tutorial workflows: 292 exercises, hidden pytest-style checks, a live Textual editor, progressive hints, and bundled @@ -20,14 +20,14 @@ Python documentation snippets so learners can work without leaving the terminal. and [uv](https://docs.astral.sh/uv/): ```bash -uvx --from python-learnings pylings init --path ./learn-python -cd learn-python && uvx --from python-learnings pylings +uvx pythonlings init --path ./learn-python +cd learn-python && uvx pythonlings ``` How it works: **edit** the broken exercise in your own editor → **save** → checks rerun and advance you to the next one. That's the whole loop. -Status: `v0.2.0`, alpha — published on PyPI as `python-learnings`. +Status: `v0.2.0`, alpha — published on PyPI as `pythonlings`. ![Coding screen](docs/assets/screenshots/coding-screen.png) @@ -49,9 +49,9 @@ Status: `v0.2.0`, alpha — published on PyPI as `python-learnings`. ## What Happens When You Run It -1. `pylings init` creates a self-contained learner workspace with exercises, +1. `pythonlings init` creates a self-contained learner workspace with exercises, hidden checks, local docs, and original snapshots for reset. -2. `pylings` opens the first pending exercise in the terminal UI. +2. `pythonlings` opens the first pending exercise in the terminal UI. 3. You edit the broken code, remove `# I AM NOT DONE`, and checks rerun as you work. 4. Passing checks mark the exercise complete and move progress forward. `F4` @@ -59,68 +59,67 @@ Status: `v0.2.0`, alpha — published on PyPI as `python-learnings`. ## Install -Pylings is on PyPI as **`python-learnings`** (it installs the `pylings` +Pythonlings is on PyPI as **`pythonlings`** (it installs the `pythonlings` command). You need [Python 3.9+](https://www.python.org/downloads/). Recommended — install it isolated, like any CLI app: ```bash -pipx install python-learnings +pipx install pythonlings # or -uv tool install python-learnings +uv tool install pythonlings ``` Or run it with no install at all: ```bash -uvx --from python-learnings pylings +uvx pythonlings ``` Or plain pip (use a virtual environment): ```bash -pip install python-learnings +pip install pythonlings ``` -> The PyPI **name** is `python-learnings`; the **command** is `pylings`. The -> name `pylings` on PyPI belongs to a different project, so do **not** run -> `pip install pylings`. +> Pythonlings was previously published as `python-learnings`. The package +> `pylings` on PyPI is an unrelated project — install `pythonlings`. For the latest development build from `main`: ```bash -pipx install --force "git+https://github.com/abhiksark/pylings.git" +pipx install --force "git+https://github.com/abhiksark/pythonlings.git" ``` Create a learner workspace before starting: ```bash -pylings init --path ~/pylings-workspace -cd ~/pylings-workspace -pylings +pythonlings init --path ~/pythonlings-workspace +cd ~/pythonlings-workspace +pythonlings ``` For local development: ```bash -git clone git@github.com:abhiksark/pylings.git -cd pylings +git clone git@github.com:abhiksark/pythonlings.git +cd pythonlings pip install -e ".[dev]" ``` ## Quick Start ```bash -pylings init --path ./learn-python # create a self-contained workspace +pythonlings init --path ./learn-python # create a self-contained workspace cd learn-python -pylings # open the TUI on the first pending exercise -pylings topics # open the topic picker -pylings list # show topic progress -pylings hint variables1 # print a hint and docs link -pylings run variables1 # run one exercise check -pylings dry-run variables1 # run one exercise non-interactively -pylings reset variables1 --yes # restore an exercise from its original -pylings update # refresh checks/docs after upgrading pylings +pythonlings # open the TUI on the first pending exercise +pythonlings topics # open the topic picker +pythonlings list # show topic progress +pythonlings hint variables1 # print a hint and docs link +pythonlings run variables1 # run one exercise check +pythonlings dry-run variables1 # run one exercise non-interactively +pythonlings reset variables1 --yes # restore an exercise from its original +pythonlings update # refresh checks/docs after upgrading pythonlings ``` Each exercise contains a `# I AM NOT DONE` marker. Fix the code, remove the @@ -128,7 +127,7 @@ marker, and let the live check advance you to the next exercise. ## Demo -![Pylings terminal demo](docs/assets/demos/pylings-demo.gif) +![Pythonlings terminal demo](docs/assets/demos/pythonlings-demo.gif) The demo is generated from [docs/demo.tape](docs/demo.tape). See [docs/DEMO_GIF.md](docs/DEMO_GIF.md) to regenerate it with @@ -156,12 +155,12 @@ path. ## Project Layout ```text -pylings/ # application package +pythonlings/ # application package core/ # manifest, state, runner, reset, docs loading screens/ # Textual screens widgets/ # reusable TUI widgets docs/ # bundled Python documentation snippets -pylings/curriculum/ # packaged copy in built wheels +pythonlings/curriculum/ # packaged copy in built wheels exercises// # learner-editable exercise files checks// # hidden assertions for each exercise tests/ # unit, integration, and TUI tests @@ -180,7 +179,7 @@ python scripts/fetch_python_docs.py ```bash pip install -e ".[dev]" python -m pytest -q -pylings --root tests/fixtures/passing_curriculum verify +pythonlings --root tests/fixtures/passing_curriculum verify ``` When adding curriculum, update `exercises/`, `checks/`, and `info.toml` @@ -189,15 +188,15 @@ together. Keep exercise and check filenames mirrored, for example ## Contributing -Pylings is actively developed and welcomes contributors — beginners included. +Pythonlings is actively developed and welcomes contributors — beginners included. The current focus is the [0.3.0 roadmap](docs/roadmap/0.3.0.md) (wider adoption), and every roadmap issue is written to be picked up cold. Start with a -[`good first issue`](https://github.com/abhiksark/pylings/issues?q=is%3Aopen+label%3A%22good+first+issue%22), +[`good first issue`](https://github.com/abhiksark/pythonlings/issues?q=is%3Aopen+label%3A%22good+first+issue%22), comment to claim it, and see [CONTRIBUTING.md](CONTRIBUTING.md). ## Release Flow -Pylings uses Semantic Versioning: +Pythonlings uses Semantic Versioning: - `MAJOR`: incompatible curriculum or CLI changes. - `MINOR`: new topics, exercises, TUI features, or docs workflows. @@ -215,9 +214,9 @@ See [RELEASE.md](RELEASE.md) for the release checklist. ## Attribution -Pylings is inspired by [rustlings](https://github.com/rust-lang/rustlings). +Pythonlings is inspired by [rustlings](https://github.com/rust-lang/rustlings). Bundled reference snippets are generated from the official Python documentation. -See [pylings/docs/NOTICE.md](pylings/docs/NOTICE.md) for Python documentation +See [pythonlings/docs/NOTICE.md](pythonlings/docs/NOTICE.md) for Python documentation licensing details. ## License diff --git a/checks/pathlib/pathlib2.py b/checks/pathlib/pathlib2.py index f519eb1..87b94a3 100644 --- a/checks/pathlib/pathlib2.py +++ b/checks/pathlib/pathlib2.py @@ -1,4 +1,4 @@ from pathlib import Path -assert source == Path("pylings/src/main.py") +assert source == Path("pythonlings/src/main.py") print("pathlib2 ok") diff --git a/docs-site/CNAME b/docs-site/CNAME index 5c95dda..5f0cbb2 100644 --- a/docs-site/CNAME +++ b/docs-site/CNAME @@ -1 +1 @@ -pylings.abhik.ai +pythonlings.abhik.ai diff --git a/docs-site/contributing.md b/docs-site/contributing.md index 52a540b..65e8c98 100644 --- a/docs-site/contributing.md +++ b/docs-site/contributing.md @@ -6,8 +6,8 @@ contributor workflow. ## Development Setup ```bash -git clone git@github.com:abhiksark/pylings.git -cd pylings +git clone git@github.com:abhiksark/pythonlings.git +cd pythonlings pip install -e ".[dev]" python -m pytest -q ``` @@ -16,7 +16,7 @@ python -m pytest -q ```bash python -m pytest -q -pylings --root tests/fixtures/passing_curriculum verify +pythonlings --root tests/fixtures/passing_curriculum verify python -m build ``` diff --git a/docs-site/curriculum.md b/docs-site/curriculum.md index 196fa79..f238667 100644 --- a/docs-site/curriculum.md +++ b/docs-site/curriculum.md @@ -1,6 +1,6 @@ # Curriculum -Pylings ships 292 exercises across 31 topics. Each exercise has a learner file, +Pythonlings ships 292 exercises across 31 topics. Each exercise has a learner file, a hidden check file, a hint, and a Python documentation link in `info.toml`. ## Topic Coverage @@ -55,4 +55,4 @@ Exercise and check filenames stay mirrored. For example, Progress is stored in the learner workspace, not in the package repository. Passing checks mark exercises complete. Reset restores a learner file from the -original snapshot created during `pylings init`. +original snapshot created during `pythonlings init`. diff --git a/docs-site/index.md b/docs-site/index.md index 996ccd8..706a9a0 100644 --- a/docs-site/index.md +++ b/docs-site/index.md @@ -1,10 +1,10 @@ -# Pylings +# Pythonlings Python learnings, Rustlings-style, in a live terminal TUI. -![Pylings terminal demo](https://raw.githubusercontent.com/abhiksark/pylings/main/docs/assets/demos/pylings-demo.gif) +![Pythonlings terminal demo](https://raw.githubusercontent.com/abhiksark/pythonlings/main/docs/assets/demos/pythonlings-demo.gif) -Pylings helps learners practice Python by fixing small broken programs and +Pythonlings helps learners practice Python by fixing small broken programs and watching checks rerun as they work. The project includes 292 exercises across 31 topics, hidden pytest-style checks, a Textual editor, progressive hints, and bundled Python documentation snippets for offline-friendly practice. @@ -12,13 +12,13 @@ bundled Python documentation snippets for offline-friendly practice. ## Start Here ```bash -pipx install "git+https://github.com/abhiksark/pylings.git@v0.1.0" -pylings init --path ~/pylings-workspace -cd ~/pylings-workspace -pylings +pipx install "git+https://github.com/abhiksark/pythonlings.git@v0.1.0" +pythonlings init --path ~/pythonlings-workspace +cd ~/pythonlings-workspace +pythonlings ``` -The package name reserved for PyPI publishing is `python-learnings`. Until the +The package name reserved for PyPI publishing is `pythonlings`. Until the PyPI release is live, install the stable v0.1.0 release from the GitHub tag. ## What You Get @@ -27,10 +27,10 @@ PyPI release is live, install the stable v0.1.0 release from the GitHub tag. - Live exercise verification while editing. - Topic picker, progress tracking, reset, hints, and one-shot CLI commands. - Local docs window with links back to the official Python documentation. -- A self-contained learner workspace created by `pylings init`. +- A self-contained learner workspace created by `pythonlings init`. ## Project Status -Pylings is currently `v0.1.0` alpha. The public API, CLI, and curriculum are +Pythonlings is currently `v0.1.0` alpha. The public API, CLI, and curriculum are usable, but the project is still hardening packaging, docs, first-run flow, and release automation. diff --git a/docs-site/interface.md b/docs-site/interface.md index 9c3e2b6..6f774ba 100644 --- a/docs-site/interface.md +++ b/docs-site/interface.md @@ -2,21 +2,21 @@ ## Coding Screen -![Coding screen](https://raw.githubusercontent.com/abhiksark/pylings/main/docs/assets/screenshots/coding-screen.png) +![Coding screen](https://raw.githubusercontent.com/abhiksark/pythonlings/main/docs/assets/screenshots/coding-screen.png) The coding screen keeps the exercise, editor, output, progress, and exercise list in one terminal view. Checks rerun as files change. ## Topic Picker -![Topic picker](https://raw.githubusercontent.com/abhiksark/pylings/main/docs/assets/screenshots/topic-picker.png) +![Topic picker](https://raw.githubusercontent.com/abhiksark/pythonlings/main/docs/assets/screenshots/topic-picker.png) The topic picker shows curriculum progress by topic and lets learners jump back to a topic without leaving the terminal. ## Local Docs Window -![Local docs reference](https://raw.githubusercontent.com/abhiksark/pylings/main/docs/assets/screenshots/docs-reference.png) +![Local docs reference](https://raw.githubusercontent.com/abhiksark/pythonlings/main/docs/assets/screenshots/docs-reference.png) The docs window opens bundled Python reference snippets for the current exercise. Press `O` from that window to open the official Python docs page. diff --git a/docs-site/local-docs.md b/docs-site/local-docs.md index 6367216..33d85ed 100644 --- a/docs-site/local-docs.md +++ b/docs-site/local-docs.md @@ -1,6 +1,6 @@ # Local Docs -Pylings bundles short Python reference snippets so learners can stay in the +Pythonlings bundles short Python reference snippets so learners can stay in the terminal while solving exercises. ## In The TUI @@ -12,7 +12,7 @@ terminal while solving exercises. ## Source Material Bundled snippets are generated from the official Python documentation. Licensing -details live in `pylings/docs/NOTICE.md`. +details live in `pythonlings/docs/NOTICE.md`. ## Refresh Snippets @@ -25,8 +25,8 @@ python scripts/fetch_python_docs.py The generated files live under: ```text -pylings/docs/index.json -pylings/docs/topics/ +pythonlings/docs/index.json +pythonlings/docs/topics/ ``` When adding or changing exercises, keep `info.toml` docs links aligned with the diff --git a/docs-site/quick-start.md b/docs-site/quick-start.md index 6f4172b..28c27ce 100644 --- a/docs-site/quick-start.md +++ b/docs-site/quick-start.md @@ -5,47 +5,47 @@ Install the current release from GitHub: ```bash -pipx install "git+https://github.com/abhiksark/pylings.git@v0.1.0" +pipx install "git+https://github.com/abhiksark/pythonlings.git@v0.1.0" ``` After PyPI publishing is enabled, the package install path will be: ```bash -pipx install python-learnings +pipx install pythonlings ``` -The command installed by the package is `pylings`. +The command installed by the package is `pythonlings`. ## Create A Workspace ```bash -pylings init --path ~/pylings-workspace -cd ~/pylings-workspace -pylings +pythonlings init --path ~/pythonlings-workspace +cd ~/pythonlings-workspace +pythonlings ``` -`pylings init` copies exercises, hidden checks, reference solutions, and reset -snapshots into a learner workspace. Run `pylings` from that workspace to open +`pythonlings init` copies exercises, hidden checks, reference solutions, and reset +snapshots into a learner workspace. Run `pythonlings` from that workspace to open the first pending exercise. ## Useful Commands ```bash -pylings list -pylings topics -pylings hint variables1 -pylings run variables1 -pylings dry-run variables1 -pylings reset variables1 --yes -pylings update +pythonlings list +pythonlings topics +pythonlings hint variables1 +pythonlings run variables1 +pythonlings dry-run variables1 +pythonlings reset variables1 --yes +pythonlings update ``` -Use `pylings list` to inspect progress, `pylings hint ` for help, and -`pylings reset --yes` to restore an exercise to its original broken +Use `pythonlings list` to inspect progress, `pythonlings hint ` for help, and +`pythonlings reset --yes` to restore an exercise to its original broken state. ## Exercise Loop Each exercise contains a `# I AM NOT DONE` marker. Fix the code, remove the -marker, and let Pylings run the hidden check. Passing checks mark the exercise +marker, and let Pythonlings run the hidden check. Passing checks mark the exercise complete and move the progress state forward. diff --git a/docs-site/roadmap.md b/docs-site/roadmap.md index 30c7175..3a48e01 100644 --- a/docs-site/roadmap.md +++ b/docs-site/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -Pylings is currently `v0.1.0` alpha. The core learner loop works, but the +Pythonlings is currently `v0.1.0` alpha. The core learner loop works, but the project still needs product hardening before calling it stable. ## Current Release @@ -13,7 +13,7 @@ project still needs product hardening before calling it stable. ## Next Work -- Finish PyPI publishing for the `python-learnings` project name. +- Finish PyPI publishing for the `pythonlings` project name. - Improve first-run onboarding and empty-state copy. - Harden keyboard flow around `Enter`, `Esc`, `F4`, and `F5`. - Add more TUI tests for the coding screen, docs window, and topic picker. @@ -22,7 +22,7 @@ project still needs product hardening before calling it stable. ## Release Policy -Pylings follows Semantic Versioning. +Pythonlings follows Semantic Versioning. - `MAJOR`: incompatible curriculum or CLI changes. - `MINOR`: new topics, exercises, TUI features, or docs workflows. diff --git a/docs-site/robots.txt b/docs-site/robots.txt index 5ee96ac..4712002 100644 --- a/docs-site/robots.txt +++ b/docs-site/robots.txt @@ -1,4 +1,4 @@ User-agent: * Allow: / -Sitemap: https://pylings.abhik.ai/sitemap.xml +Sitemap: https://pythonlings.abhik.ai/sitemap.xml diff --git a/docs/DEMO_GIF.md b/docs/DEMO_GIF.md index 891736f..dbff5d7 100644 --- a/docs/DEMO_GIF.md +++ b/docs/DEMO_GIF.md @@ -1,7 +1,7 @@ # Demo GIF Workflow Use this workflow to generate a repeatable GIF for the README, GitHub release, -and PyPI project page. The goal is to show the real `pylings` first-run flow, +and PyPI project page. The goal is to show the real `pythonlings` first-run flow, not a mocked terminal. ## Recommended Tool @@ -30,12 +30,12 @@ VHS_BIN=/path/to/vhs scripts/generate_demo_gif.sh If Homebrew VHS fails on macOS with `could not open ttyd` and `ERR_CONNECTION_REFUSED`, use a newer VHS build or run VHS through Docker. That -failure happens before Pylings starts. +failure happens before Pythonlings starts. The tape writes: ```text -docs/assets/demos/pylings-demo.gif +docs/assets/demos/pythonlings-demo.gif ``` Review the GIF before committing it. Keep the recording under 20 seconds so it @@ -46,9 +46,9 @@ loads quickly on GitHub and PyPI. The scripted flow should stay focused on the first-time user path: 1. Initialize a clean workspace. -2. Show topic progress with `pylings list`. -3. Show a hint and official docs link with `pylings hint variables1`. -4. Open the Pylings TUI. +2. Show topic progress with `pythonlings list`. +3. Show a hint and official docs link with `pythonlings hint variables1`. +4. Open the Pythonlings TUI. 5. Show the first pending exercise in the coding screen. 6. Open and close the local docs window with `F5` and `Esc`. 7. Open the topic picker with `F4`. @@ -61,8 +61,8 @@ and render it with `agg`: ```bash brew install asciinema agg -asciinema rec docs/assets/demos/pylings.cast -agg docs/assets/demos/pylings.cast docs/assets/demos/pylings-demo.gif +asciinema rec docs/assets/demos/pythonlings.cast +agg docs/assets/demos/pythonlings.cast docs/assets/demos/pythonlings-demo.gif ``` The VHS path is preferred because `docs/demo.tape` keeps the public demo diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 41d9f25..60beeb3 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -1,6 +1,6 @@ # Release Process -Pylings uses a feature-branch workflow and Semantic Versioning. +Pythonlings uses a feature-branch workflow and Semantic Versioning. ## Branches @@ -20,8 +20,8 @@ Use `MAJOR.MINOR` release tags. 1. Merge feature branches into `dev` with reviewed, focused commits. 2. Run `python -m pytest -q`. -3. Run `pylings --root tests/fixtures/passing_curriculum verify`. -4. Update `CHANGELOG.md` and the version in `pylings/cli.py` and `pyproject.toml`. +3. Run `pythonlings --root tests/fixtures/passing_curriculum verify`. +4. Update `CHANGELOG.md` and the version in `pythonlings/cli.py` and `pyproject.toml`. 5. Merge `dev` into `main`. 6. Create an annotated tag, for example `git tag -a v0.1 -m "Release v0.1"`. 7. Push `main`, `dev`, and tags. diff --git a/docs/assets/demos/pylings-demo.gif b/docs/assets/demos/pythonlings-demo.gif similarity index 100% rename from docs/assets/demos/pylings-demo.gif rename to docs/assets/demos/pythonlings-demo.gif diff --git a/docs/demo.tape b/docs/demo.tape index 3107380..7ddb0bb 100644 --- a/docs/demo.tape +++ b/docs/demo.tape @@ -1,4 +1,4 @@ -Output docs/assets/demos/pylings-demo.gif +Output docs/assets/demos/pythonlings-demo.gif Set Shell "bash" Set FontSize 18 @@ -8,23 +8,23 @@ Set Padding 16 Set Framerate 16 Set TypingSpeed 35ms -Type "pylings init --path /tmp/pylings-demo" +Type "pythonlings init --path /tmp/pythonlings-demo" Enter Sleep 1s -Type "cd /tmp/pylings-demo" +Type "cd /tmp/pythonlings-demo" Enter Sleep 300ms -Type "pylings list" +Type "pythonlings list" Enter Sleep 1.5s -Type "pylings hint variables1" +Type "pythonlings hint variables1" Enter Sleep 1.5s -Type "pylings" +Type "pythonlings" Enter Sleep 2s diff --git a/docs/roadmap/0.3.0.md b/docs/roadmap/0.3.0.md index 92c9f58..68cb65c 100644 --- a/docs/roadmap/0.3.0.md +++ b/docs/roadmap/0.3.0.md @@ -1,58 +1,58 @@ -# Pylings 0.3.0 Roadmap — Wider Adoption +# Pythonlings 0.3.0 Roadmap — Wider Adoption -**Theme:** make pylings easy to *find*, *install*, and *start* — for beginners +**Theme:** make pythonlings easy to *find*, *install*, and *start* — for beginners learning Python. Terminal-native, focused release (no web/no-install bet this cycle — that's a possible 0.4.0). -**North star:** real first-time users get from "discovered pylings" to "doing +**North star:** real first-time users get from "discovered pythonlings" to "doing exercise 1" with as little friction as possible. Everything below is tracked under the -[0.3.0 milestone](https://github.com/abhiksark/pylings/milestone/1). Issues are +[0.3.0 milestone](https://github.com/abhiksark/pythonlings/milestone/1). Issues are written to be picked up cold — each has context, scope, exact files, and how to verify. **Want to help? Comment to claim an issue** (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). Many are labelled -[`good first issue`](https://github.com/abhiksark/pylings/issues?q=is%3Aopen+label%3A%22good+first+issue%22). +[`good first issue`](https://github.com/abhiksark/pythonlings/issues?q=is%3Aopen+label%3A%22good+first+issue%22). ## ① Discovery — be found | Issue | What | Labels | |---|---|---| -| [#5](https://github.com/abhiksark/pylings/issues/5) | Update README install & status for the live PyPI release | docs · good first issue | -| [#6](https://github.com/abhiksark/pylings/issues/6) | Modernize & fix README badges (dynamic PyPI, Python 3.9+, CI) | docs · good first issue | -| [#7](https://github.com/abhiksark/pylings/issues/7) | Strengthen README hero / above-the-fold "try it now" | docs · good first issue | -| [#8](https://github.com/abhiksark/pylings/issues/8) | Submit to awesome-python | docs · good first issue · help wanted | -| [#9](https://github.com/abhiksark/pylings/issues/9) | Submit to awesome-cli-apps / awesome-tuis | docs · good first issue · help wanted | -| [#10](https://github.com/abhiksark/pylings/issues/10) | Add to the Rustlings third-party exercises list | docs · good first issue · help wanted | -| [#11](https://github.com/abhiksark/pylings/issues/11) | Draft a launch announcement (Show HN / r/learnpython) | docs · help wanted | -| [#12](https://github.com/abhiksark/pylings/issues/12) | Refresh the demo GIF for the 0.2.0 flow | docs · good first issue | +| [#5](https://github.com/abhiksark/pythonlings/issues/5) | Update README install & status for the live PyPI release | docs · good first issue | +| [#6](https://github.com/abhiksark/pythonlings/issues/6) | Modernize & fix README badges (dynamic PyPI, Python 3.9+, CI) | docs · good first issue | +| [#7](https://github.com/abhiksark/pythonlings/issues/7) | Strengthen README hero / above-the-fold "try it now" | docs · good first issue | +| [#8](https://github.com/abhiksark/pythonlings/issues/8) | Submit to awesome-python | docs · good first issue · help wanted | +| [#9](https://github.com/abhiksark/pythonlings/issues/9) | Submit to awesome-cli-apps / awesome-tuis | docs · good first issue · help wanted | +| [#10](https://github.com/abhiksark/pythonlings/issues/10) | Add to the Rustlings third-party exercises list | docs · good first issue · help wanted | +| [#11](https://github.com/abhiksark/pythonlings/issues/11) | Draft a launch announcement (Show HN / r/learnpython) | docs · help wanted | +| [#12](https://github.com/abhiksark/pythonlings/issues/12) | Refresh the demo GIF for the 0.2.0 flow | docs · good first issue | ## ② First-run UX — flatten the cliff | Issue | What | Labels | |---|---|---| -| [#13](https://github.com/abhiksark/pylings/issues/13) | `pylings` with no workspace should auto-init instead of erroring | enhancement · good first issue | -| [#14](https://github.com/abhiksark/pylings/issues/14) | Add a first-launch welcome/onboarding screen | enhancement · help wanted | +| [#13](https://github.com/abhiksark/pythonlings/issues/13) | `pythonlings` with no workspace should auto-init instead of erroring | enhancement · good first issue | +| [#14](https://github.com/abhiksark/pythonlings/issues/14) | Add a first-launch welcome/onboarding screen | enhancement · help wanted | ## ③ Motivation & retention | Issue | What | Labels | |---|---|---| -| [#15](https://github.com/abhiksark/pylings/issues/15) | Show overall curriculum progress in the TUI | enhancement · good first issue | -| [#16](https://github.com/abhiksark/pylings/issues/16) | Add an all-exercises-complete celebration screen | enhancement · good first issue | +| [#15](https://github.com/abhiksark/pythonlings/issues/15) | Show overall curriculum progress in the TUI | enhancement · good first issue | +| [#16](https://github.com/abhiksark/pythonlings/issues/16) | Add an all-exercises-complete celebration screen | enhancement · good first issue | ## ④ Curriculum (beginner ramp) | Issue | What | Labels | |---|---|---| -| [#17](https://github.com/abhiksark/pylings/issues/17) | Gentle-ramp the first exercises of each topic | docs · help wanted | +| [#17](https://github.com/abhiksark/pythonlings/issues/17) | Gentle-ramp the first exercises of each topic | docs · help wanted | ## Out of scope for 0.3.0 - Web / no-install (Pyodide) playground — candidate for 0.4.0. -- Reliability/hardening work (`pylings doctor`, real file-watching) — tracked - separately in [#4](https://github.com/abhiksark/pylings/issues/4). +- Reliability/hardening work (`pythonlings doctor`, real file-watching) — tracked + separately in [#4](https://github.com/abhiksark/pythonlings/issues/4). - Full curriculum coverage-gap analysis — see the methodology spec at `docs/superpowers/specs/2026-06-02-gap-audit-design.md`. Issue #17 only smooths the *early ramp* of existing topics. ## Definition of done for the release - Install paths in the README all work and point at PyPI. -- A newcomer can run `pylings` and reach exercise 1 without reading docs. +- A newcomer can run `pythonlings` and reach exercise 1 without reading docs. - Overall progress is visible; finishing the curriculum has a payoff moment. -- pylings is listed in at least one high-traffic discovery source. +- pythonlings is listed in at least one high-traffic discovery source. diff --git a/exercises/json/json5.py b/exercises/json/json5.py index 21c7647..4752d2d 100644 --- a/exercises/json/json5.py +++ b/exercises/json/json5.py @@ -5,5 +5,5 @@ import json -data = json.loads('{"name": "Pylings"}') +data = json.loads('{"name": "Pythonlings"}') timezone = ??? diff --git a/exercises/json/json8.py b/exercises/json/json8.py index 944a85f..93f739d 100644 --- a/exercises/json/json8.py +++ b/exercises/json/json8.py @@ -5,7 +5,7 @@ import json -original = {"project": "pylings", "tags": ["python", "practice"], "meta": {"level": 3}} +original = {"project": "pythonlings", "tags": ["python", "practice"], "meta": {"level": 3}} encoded = json.dumps(original, sort_keys=True) decoded = ??? tag_count = len(decoded["tags"]) diff --git a/exercises/pathlib/pathlib2.py b/exercises/pathlib/pathlib2.py index 7ded49c..e5a2ea7 100644 --- a/exercises/pathlib/pathlib2.py +++ b/exercises/pathlib/pathlib2.py @@ -5,5 +5,5 @@ from pathlib import Path -project = Path("pylings") +project = Path("pythonlings") source = ??? diff --git a/info.toml b/info.toml index afad0f5..1356895 100644 --- a/info.toml +++ b/info.toml @@ -1,5 +1,5 @@ format_version = 1 -welcome_message = "Welcome to pylings! Save a file to re-run its checks." +welcome_message = "Welcome to pythonlings! Save a file to re-run its checks." final_message = "All exercises complete. 🐍 Nice work." [[exercises]] diff --git a/mkdocs.yml b/mkdocs.yml index 1bbf4f7..614fd74 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ -site_name: Pylings -site_url: https://pylings.abhik.ai/ +site_name: Pythonlings +site_url: https://pythonlings.abhik.ai/ site_description: Python learnings, Rustlings-style, in a terminal TUI. -repo_url: https://github.com/abhiksark/pylings -repo_name: abhiksark/pylings +repo_url: https://github.com/abhiksark/pythonlings +repo_name: abhiksark/pythonlings edit_uri: edit/main/docs-site/ docs_dir: docs-site @@ -43,4 +43,4 @@ nav: extra: social: - icon: fontawesome/brands/github - link: https://github.com/abhiksark/pylings + link: https://github.com/abhiksark/pythonlings diff --git a/pyproject.toml b/pyproject.toml index e1df819..16a9644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "python-learnings" +name = "pythonlings" version = "0.2.0" description = "Python learnings, Rustlings-style, in a terminal TUI." readme = "Readme.md" @@ -13,7 +13,7 @@ keywords = [ "python", "python-exercises", "learn-python", - "python-learnings", + "pythonlings", "beginner-python", "coding-practice", "rustlings", @@ -46,11 +46,11 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/abhiksark/pylings" -Repository = "https://github.com/abhiksark/pylings" -Issues = "https://github.com/abhiksark/pylings/issues" -Changelog = "https://github.com/abhiksark/pylings/blob/main/CHANGELOG.md" -Documentation = "https://pylings.abhik.ai/" +Homepage = "https://github.com/abhiksark/pythonlings" +Repository = "https://github.com/abhiksark/pythonlings" +Issues = "https://github.com/abhiksark/pythonlings/issues" +Changelog = "https://github.com/abhiksark/pythonlings/blob/main/CHANGELOG.md" +Documentation = "https://pythonlings.abhik.ai/" [project.optional-dependencies] dev = [ @@ -59,17 +59,17 @@ dev = [ ] [project.scripts] -pylings = "pylings.cli:main" +pythonlings = "pythonlings.cli:main" [tool.hatch.build.targets.wheel] -packages = ["pylings"] +packages = ["pythonlings"] [tool.hatch.build.targets.wheel.force-include] -"pylings/pylings.tcss" = "pylings/pylings.tcss" -"info.toml" = "pylings/curriculum/info.toml" -"exercises" = "pylings/curriculum/exercises" -"checks" = "pylings/curriculum/checks" -"solutions" = "pylings/curriculum/solutions" +"pythonlings/pythonlings.tcss" = "pythonlings/pythonlings.tcss" +"info.toml" = "pythonlings/curriculum/info.toml" +"exercises" = "pythonlings/curriculum/exercises" +"checks" = "pythonlings/curriculum/checks" +"solutions" = "pythonlings/curriculum/solutions" [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/pylings/__init__.py b/pythonlings/__init__.py similarity index 100% rename from pylings/__init__.py rename to pythonlings/__init__.py diff --git a/pylings/__main__.py b/pythonlings/__main__.py similarity index 63% rename from pylings/__main__.py rename to pythonlings/__main__.py index 5c9808c..3315c80 100644 --- a/pylings/__main__.py +++ b/pythonlings/__main__.py @@ -1,4 +1,4 @@ -from pylings.cli import main +from pythonlings.cli import main if __name__ == "__main__": raise SystemExit(main()) diff --git a/pylings/app.py b/pythonlings/app.py similarity index 86% rename from pylings/app.py rename to pythonlings/app.py index 2895cf1..b06f2a9 100644 --- a/pylings/app.py +++ b/pythonlings/app.py @@ -1,18 +1,18 @@ -# pylings/app.py +# pythonlings/app.py from __future__ import annotations from pathlib import Path from textual.app import App -from pylings.core.manifest import Manifest, load as load_manifest -from pylings.core.state import State, load as load_state, next_pending -from pylings.screens.topic_picker import TopicPickerScreen -from pylings.screens.track import TrackScreen +from pythonlings.core.manifest import Manifest, load as load_manifest +from pythonlings.core.state import State, load as load_state, next_pending +from pythonlings.screens.topic_picker import TopicPickerScreen +from pythonlings.screens.track import TrackScreen -class PylingsApp(App[int]): - CSS_PATH = "pylings.tcss" +class PythonlingsApp(App[int]): + CSS_PATH = "pythonlings.tcss" def __init__( self, @@ -78,7 +78,7 @@ def run_tui( watch_files: bool = False, ) -> int: return ( - PylingsApp( + PythonlingsApp( root, start_topic, force_picker=force_picker, diff --git a/pylings/cli.py b/pythonlings/cli.py similarity index 75% rename from pylings/cli.py rename to pythonlings/cli.py index 817bafe..0976e27 100644 --- a/pylings/cli.py +++ b/pythonlings/cli.py @@ -1,4 +1,4 @@ -# pylings/cli.py +# pythonlings/cli.py from __future__ import annotations import argparse @@ -9,10 +9,10 @@ def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="pylings") - parser.add_argument("--version", action="version", version=f"pylings {__version__}") + parser = argparse.ArgumentParser(prog="pythonlings") + parser.add_argument("--version", action="version", version=f"pythonlings {__version__}") parser.add_argument( - "--debug", action="store_true", help="Write debug output to .pylings_debug.log." + "--debug", action="store_true", help="Write debug output to .pythonlings_debug.log." ) parser.add_argument( "--watch-files", @@ -27,13 +27,13 @@ def _build_parser() -> argparse.ArgumentParser: ) sub = parser.add_subparsers(dest="command") - p_init = sub.add_parser("init", help="Create a pylings workspace.") + p_init = sub.add_parser("init", help="Create a pythonlings workspace.") p_init.add_argument("--path", type=Path, default=Path.cwd()) p_init.add_argument( "--force", action="store_true", help="Overwrite managed workspace files." ) - p_update = sub.add_parser("update", help="Update an existing pylings workspace.") + p_update = sub.add_parser("update", help="Update an existing pythonlings workspace.") p_update.add_argument("--path", type=Path, default=Path.cwd()) sub.add_parser("watch", help="Launch the TUI in watch mode (default).") @@ -76,39 +76,39 @@ def _resolve_topic(manifest, topic: str): if topic in manifest.topics(): return topic sys.stderr.write( - f"pylings: no topic named {topic!r}. " + f"pythonlings: no topic named {topic!r}. " f"Topics: {', '.join(manifest.topics())}\n" ) return None def _cmd_init(path: Path, force: bool) -> int: - from pylings.core.curriculum import WorkspaceError, init_workspace + from pythonlings.core.curriculum import WorkspaceError, init_workspace try: root = init_workspace(path, force=force) except WorkspaceError as e: - sys.stderr.write(f"pylings: {e}\n") + sys.stderr.write(f"pythonlings: {e}\n") return 1 print(f"initialized: {root}") return 0 def _cmd_update(path: Path) -> int: - from pylings.core.curriculum import WorkspaceError, update_workspace + from pythonlings.core.curriculum import WorkspaceError, update_workspace try: root = update_workspace(path) except WorkspaceError as e: - sys.stderr.write(f"pylings: {e}\n") + sys.stderr.write(f"pythonlings: {e}\n") return 1 print(f"updated: {root}") return 0 def _cmd_verify(root: Path, topic: str | None) -> int: - from pylings.core.manifest import load as load_manifest - from pylings.core.runner import run_verify + from pythonlings.core.manifest import load as load_manifest + from pythonlings.core.runner import run_verify manifest = load_manifest(root) if topic is not None: @@ -128,8 +128,8 @@ def _cmd_verify(root: Path, topic: str | None) -> int: def _cmd_list(root: Path, topic: str | None) -> int: - from pylings.core.manifest import load as load_manifest - from pylings.core.state import load as load_state, next_pending + from pythonlings.core.manifest import load as load_manifest + from pythonlings.core.state import load as load_state, next_pending manifest = load_manifest(root) state = load_state(root) @@ -158,13 +158,13 @@ def _cmd_list(root: Path, topic: str | None) -> int: def _cmd_hint(root: Path, name: str) -> int: - from pylings.core.manifest import load as load_manifest + from pythonlings.core.manifest import load as load_manifest manifest = load_manifest(root) try: ex = manifest.by_name(name) except KeyError: - sys.stderr.write(f"pylings: no exercise named {name!r}\n") + sys.stderr.write(f"pythonlings: no exercise named {name!r}\n") return 1 print(ex.hint.strip() or "(no hint provided)") if ex.docs: @@ -173,14 +173,14 @@ def _cmd_hint(root: Path, name: str) -> int: def _cmd_run(root: Path, name: str) -> int: - from pylings.core.manifest import load as load_manifest - from pylings.core.runner import run as run_exercise + from pythonlings.core.manifest import load as load_manifest + from pythonlings.core.runner import run as run_exercise manifest = load_manifest(root) try: ex = manifest.by_name(name) except KeyError: - sys.stderr.write(f"pylings: no exercise named {name!r}\n") + sys.stderr.write(f"pythonlings: no exercise named {name!r}\n") return 1 result = run_exercise(ex) @@ -189,31 +189,31 @@ def _cmd_run(root: Path, name: str) -> int: if result.stderr: sys.stderr.write(result.stderr) if result.timed_out: - sys.stderr.write(f"pylings: {name} timed out after {result.duration_s:.1f}s\n") + sys.stderr.write(f"pythonlings: {name} timed out after {result.duration_s:.1f}s\n") return 1 if result.exit_code != 0: return 1 if ex.is_pending(): sys.stderr.write( - f"pylings: tests pass but the '# I AM NOT DONE' marker is still in {name}.\n" + f"pythonlings: tests pass but the '# I AM NOT DONE' marker is still in {name}.\n" ) return 1 return 0 def _cmd_solution(root: Path, name: str) -> int: - from pylings.core.manifest import load as load_manifest - from pylings.core.runner import run_verify - from pylings.core.solutions import SolutionError, solution_exercise + from pythonlings.core.manifest import load as load_manifest + from pythonlings.core.runner import run_verify + from pythonlings.core.solutions import SolutionError, solution_exercise manifest = load_manifest(root) try: ex = solution_exercise(root, manifest.by_name(name)) except KeyError: - sys.stderr.write(f"pylings: no exercise named {name!r}\n") + sys.stderr.write(f"pythonlings: no exercise named {name!r}\n") return 1 except SolutionError as e: - sys.stderr.write(f"pylings: {e}\n") + sys.stderr.write(f"pythonlings: {e}\n") return 1 result = run_verify(ex) @@ -225,15 +225,15 @@ def _cmd_solution(root: Path, name: str) -> int: def _cmd_reset(root: Path, name: str, yes: bool) -> int: - from pylings.core.manifest import load as load_manifest - from pylings.core.reset import ResetError, restore - from pylings.core.state import load as load_state, save as save_state + from pythonlings.core.manifest import load as load_manifest + from pythonlings.core.reset import ResetError, restore + from pythonlings.core.state import load as load_state, save as save_state manifest = load_manifest(root) try: ex = manifest.by_name(name) except KeyError: - sys.stderr.write(f"pylings: no exercise named {name!r}\n") + sys.stderr.write(f"pythonlings: no exercise named {name!r}\n") return 1 if not yes: @@ -245,7 +245,7 @@ def _cmd_reset(root: Path, name: str, yes: bool) -> int: try: restore(root, ex) except ResetError as e: - sys.stderr.write(f"pylings: {e}\n") + sys.stderr.write(f"pythonlings: {e}\n") return 1 state = load_state(root) @@ -259,10 +259,15 @@ def main(argv: list[str] | None = None) -> int: parser = _build_parser() args = parser.parse_args(argv if argv is not None else sys.argv[1:]) + workspace = getattr(args, "root", None) or getattr(args, "path", None) + if workspace is not None: + from pythonlings.core.curriculum import migrate_legacy_state_dir + migrate_legacy_state_dir(Path(workspace)) + try: if getattr(args, "debug", False): try: - (args.root / ".pylings_debug.log").write_text( + (args.root / ".pythonlings_debug.log").write_text( f"argv={argv if argv is not None else sys.argv[1:]!r}\n", encoding="utf-8", ) @@ -292,10 +297,10 @@ def main(argv: list[str] | None = None) -> int: if args.command in (None, "watch", "start", "topics"): start_topic = getattr(args, "topic", None) if start_topic is not None: - from pylings.core.manifest import load as load_manifest + from pythonlings.core.manifest import load as load_manifest if _resolve_topic(load_manifest(args.root), start_topic) is None: return 2 - from pylings.app import run_tui # lazy: Textual is heavy + from pythonlings.app import run_tui # lazy: Textual is heavy return run_tui( args.root, start_topic, @@ -304,12 +309,12 @@ def main(argv: list[str] | None = None) -> int: ) # Other subcommands wired in later tasks. - sys.stderr.write(f"pylings: '{args.command}' not implemented yet\n") + sys.stderr.write(f"pythonlings: '{args.command}' not implemented yet\n") return 1 except Exception as e: # Manifest errors and other startup failures use exit code 2. - from pylings.core.manifest import ManifestError + from pythonlings.core.manifest import ManifestError if isinstance(e, ManifestError): - sys.stderr.write(f"pylings: {e}\n") + sys.stderr.write(f"pythonlings: {e}\n") return 2 raise diff --git a/pylings/core/__init__.py b/pythonlings/core/__init__.py similarity index 100% rename from pylings/core/__init__.py rename to pythonlings/core/__init__.py diff --git a/pylings/core/curriculum.py b/pythonlings/core/curriculum.py similarity index 80% rename from pylings/core/curriculum.py rename to pythonlings/core/curriculum.py index d59e493..d9a7e58 100644 --- a/pylings/core/curriculum.py +++ b/pythonlings/core/curriculum.py @@ -11,16 +11,28 @@ class WorkspaceError(RuntimeError): CURRICULUM_DIRS = ("exercises", "checks", "solutions") GITIGNORE_LINES = [ - ".pylings/state.json", - ".pylings_debug.log", + ".pythonlings/state.json", + ".pythonlings_debug.log", "__pycache__/", "*.pyc", ] +def migrate_legacy_state_dir(root: Path) -> None: + """Rename a pre-rename `.pylings/` state dir to `.pythonlings/`. + + Workspaces created before the project was renamed from pylings to + pythonlings keep their progress and reset snapshots this way. + """ + legacy = root / ".pylings" + current = root / ".pythonlings" + if legacy.is_dir() and not current.exists(): + legacy.rename(current) + + def source_root() -> Path: """Return the curriculum source root for editable and wheel installs.""" - packaged = resources.files("pylings").joinpath("curriculum") + packaged = resources.files("pythonlings").joinpath("curriculum") if packaged.joinpath("info.toml").is_file(): return Path(str(packaged)) @@ -54,7 +66,7 @@ def _write_workspace_gitignore(root: Path) -> None: def _sync_originals(root: Path, src_root: Path) -> None: - originals = root / ".pylings" / "originals" + originals = root / ".pythonlings" / "originals" if originals.exists(): shutil.rmtree(originals) for exercise in (src_root / "exercises").rglob("*.py"): @@ -82,7 +94,7 @@ def init_workspace(path: Path, *, force: bool = False) -> Path: def update_workspace(path: Path) -> Path: path = path.expanduser().resolve() if not (path / "info.toml").exists(): - raise WorkspaceError(f"{path} is not a pylings workspace") + raise WorkspaceError(f"{path} is not a pythonlings workspace") src_root = source_root() _copy_path(src_root / "info.toml", path / "info.toml", overwrite=True) diff --git a/pylings/core/docs.py b/pythonlings/core/docs.py similarity index 93% rename from pylings/core/docs.py rename to pythonlings/core/docs.py index 038b402..d447f6f 100644 --- a/pylings/core/docs.py +++ b/pythonlings/core/docs.py @@ -1,4 +1,4 @@ -# pylings/core/docs.py +# pythonlings/core/docs.py from __future__ import annotations import json @@ -56,7 +56,7 @@ def load_snippet(topic: str, source_url: str = "") -> DocSnippet | None: def _load_index() -> dict[str, Any] | None: try: - docs_root = resources.files("pylings.docs") + docs_root = resources.files("pythonlings.docs") return json.loads((docs_root / "index.json").read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError, ModuleNotFoundError): return None @@ -64,7 +64,7 @@ def _load_index() -> dict[str, Any] | None: def _load_entry(topic: str, entry: dict[str, Any]) -> DocSnippet | None: try: - docs_root = resources.files("pylings.docs") + docs_root = resources.files("pythonlings.docs") text = (docs_root / entry["file"]).read_text(encoding="utf-8").strip() except (FileNotFoundError, KeyError, ModuleNotFoundError): return None diff --git a/pylings/core/exercise.py b/pythonlings/core/exercise.py similarity index 95% rename from pylings/core/exercise.py rename to pythonlings/core/exercise.py index 4eecfd9..419d41c 100644 --- a/pylings/core/exercise.py +++ b/pythonlings/core/exercise.py @@ -1,4 +1,4 @@ -# pylings/core/exercise.py +# pythonlings/core/exercise.py from __future__ import annotations from dataclasses import dataclass diff --git a/pylings/core/manifest.py b/pythonlings/core/manifest.py similarity index 97% rename from pylings/core/manifest.py rename to pythonlings/core/manifest.py index cb1b515..d1b9f3a 100644 --- a/pylings/core/manifest.py +++ b/pythonlings/core/manifest.py @@ -1,4 +1,4 @@ -# pylings/core/manifest.py +# pythonlings/core/manifest.py from __future__ import annotations try: @@ -8,7 +8,7 @@ from dataclasses import dataclass from pathlib import Path -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise class ManifestError(ValueError): @@ -102,6 +102,6 @@ def load(root: Path) -> Manifest: return Manifest( exercises=exercises, - welcome_message=data.get("welcome_message", "Welcome to pylings!"), + welcome_message=data.get("welcome_message", "Welcome to pythonlings!"), final_message=data.get("final_message", "All exercises complete."), ) diff --git a/pylings/core/reset.py b/pythonlings/core/reset.py similarity index 70% rename from pylings/core/reset.py rename to pythonlings/core/reset.py index 8676a10..a9eccc2 100644 --- a/pylings/core/reset.py +++ b/pythonlings/core/reset.py @@ -1,11 +1,11 @@ -# pylings/core/reset.py +# pythonlings/core/reset.py from __future__ import annotations import shutil from pathlib import Path -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise class ResetError(RuntimeError): @@ -15,17 +15,17 @@ class ResetError(RuntimeError): def _snapshot_path(root: Path, exercise: Exercise) -> Path: # Key on Exercise.name (unique per the manifest validator) so two # exercises in different topics with the same filename don't collide. - return root / ".pylings" / "originals" / f"{exercise.name}.py" + return root / ".pythonlings" / "originals" / f"{exercise.name}.py" def _original_path(root: Path, exercise: Exercise) -> Path: if exercise.rel_path is None: return _snapshot_path(root, exercise) - return root / ".pylings" / "originals" / exercise.rel_path.relative_to("exercises") + return root / ".pythonlings" / "originals" / exercise.rel_path.relative_to("exercises") def snapshot(root: Path, exercise: Exercise) -> None: - """Copy the pristine source into .pylings/originals/ if not already snapshotted.""" + """Copy the pristine source into .pythonlings/originals/ if not already snapshotted.""" snap = _original_path(root, exercise) if snap.exists(): return @@ -38,6 +38,6 @@ def restore(root: Path, exercise: Exercise) -> None: snap = _original_path(root, exercise) if not snap.exists(): raise ResetError( - f"no snapshot for {exercise.name!r}. Run 'pylings update' first." + f"no snapshot for {exercise.name!r}. Run 'pythonlings update' first." ) shutil.copy2(snap, exercise.path) diff --git a/pylings/core/runner.py b/pythonlings/core/runner.py similarity index 94% rename from pylings/core/runner.py rename to pythonlings/core/runner.py index 7bf7999..a39881d 100644 --- a/pylings/core/runner.py +++ b/pythonlings/core/runner.py @@ -1,4 +1,4 @@ -# pylings/core/runner.py +# pythonlings/core/runner.py from __future__ import annotations @@ -8,8 +8,8 @@ import tempfile import time -from pylings.core.exercise import Exercise -from pylings.core.exercise import RunResult +from pythonlings.core.exercise import Exercise +from pythonlings.core.exercise import RunResult DEFAULT_TIMEOUT_S = 5.0 @@ -86,7 +86,7 @@ def run(exercise: Exercise, timeout_s: float = DEFAULT_TIMEOUT_S) -> RunResult: duration = time.monotonic() - start exit_code = -1 stdout = "" - stderr = f"pylings: failed to run exercise: {e}" + stderr = f"pythonlings: failed to run exercise: {e}" timed_out = False finally: os.unlink(tmp.name) diff --git a/pylings/core/solutions.py b/pythonlings/core/solutions.py similarity index 90% rename from pylings/core/solutions.py rename to pythonlings/core/solutions.py index 1eea088..6d7543e 100644 --- a/pylings/core/solutions.py +++ b/pythonlings/core/solutions.py @@ -3,7 +3,7 @@ from dataclasses import replace from pathlib import Path -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise class SolutionError(RuntimeError): diff --git a/pylings/core/state.py b/pythonlings/core/state.py similarity index 91% rename from pylings/core/state.py rename to pythonlings/core/state.py index 772fed7..83732a0 100644 --- a/pylings/core/state.py +++ b/pythonlings/core/state.py @@ -1,4 +1,4 @@ -# pylings/core/state.py +# pythonlings/core/state.py from __future__ import annotations @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from pathlib import Path -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise FORMAT_VERSION = 2 @@ -33,7 +33,7 @@ def clear_resume(self) -> None: def _state_path(root: Path) -> Path: - return root / ".pylings" / "state.json" + return root / ".pythonlings" / "state.json" def load(root: Path) -> State: @@ -56,7 +56,7 @@ def load(root: Path) -> State: backup = path.with_suffix(".json.bak") path.rename(backup) print( - f"pylings: state file unreadable ({e}); backed up to {backup} " + f"pythonlings: state file unreadable ({e}); backed up to {backup} " f"and starting fresh", file=sys.stderr, ) diff --git a/pylings/core/watcher.py b/pythonlings/core/watcher.py similarity index 100% rename from pylings/core/watcher.py rename to pythonlings/core/watcher.py diff --git a/pylings/docs/NOTICE.md b/pythonlings/docs/NOTICE.md similarity index 85% rename from pylings/docs/NOTICE.md rename to pythonlings/docs/NOTICE.md index cc50d71..9128b65 100644 --- a/pylings/docs/NOTICE.md +++ b/pythonlings/docs/NOTICE.md @@ -1,7 +1,7 @@ # Bundled Python Documentation Snippets These snippets are generated from the official Python documentation at -https://docs.python.org/3/ and trimmed for use inside pylings. +https://docs.python.org/3/ and trimmed for use inside pythonlings. Python documentation pages are licensed under the Python Software Foundation License Version 2. Examples, recipes, and other code in the diff --git a/pylings/docs/__init__.py b/pythonlings/docs/__init__.py similarity index 100% rename from pylings/docs/__init__.py rename to pythonlings/docs/__init__.py diff --git a/pylings/docs/index.json b/pythonlings/docs/index.json similarity index 100% rename from pylings/docs/index.json rename to pythonlings/docs/index.json diff --git a/pylings/docs/topics/__init__.py b/pythonlings/docs/topics/__init__.py similarity index 100% rename from pylings/docs/topics/__init__.py rename to pythonlings/docs/topics/__init__.py diff --git a/pylings/docs/topics/async.md b/pythonlings/docs/topics/async.md similarity index 91% rename from pylings/docs/topics/async.md rename to pythonlings/docs/topics/async.md index d9a5e70..b854dd8 100644 --- a/pylings/docs/topics/async.md +++ b/pythonlings/docs/topics/async.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/asyncio.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `asyncio` runs concurrent I/O-bound work with coroutines and an event loop. diff --git a/pylings/docs/topics/classes.md b/pythonlings/docs/topics/classes.md similarity index 96% rename from pylings/docs/topics/classes.md rename to pythonlings/docs/topics/classes.md index 0f45464..7aee045 100644 --- a/pylings/docs/topics/classes.md +++ b/pythonlings/docs/topics/classes.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/classes.html#a-first-look-at-classes -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Classes combine data and behavior into reusable types. diff --git a/pylings/docs/topics/collections.md b/pythonlings/docs/topics/collections.md similarity index 91% rename from pylings/docs/topics/collections.md rename to pythonlings/docs/topics/collections.md index 1514490..fd7bf6e 100644 --- a/pylings/docs/topics/collections.md +++ b/pythonlings/docs/topics/collections.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/collections.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `collections` provides specialized containers beyond the built-in list, dict, set, and tuple. diff --git a/pylings/docs/topics/comprehensions.md b/pythonlings/docs/topics/comprehensions.md similarity index 96% rename from pylings/docs/topics/comprehensions.md rename to pythonlings/docs/topics/comprehensions.md index e12576c..d60da1e 100644 --- a/pylings/docs/topics/comprehensions.md +++ b/pythonlings/docs/topics/comprehensions.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Comprehensions build new collections by combining an expression with one or more loops. diff --git a/pylings/docs/topics/conditionals.md b/pythonlings/docs/topics/conditionals.md similarity index 95% rename from pylings/docs/topics/conditionals.md rename to pythonlings/docs/topics/conditionals.md index e07898b..c293c67 100644 --- a/pylings/docs/topics/conditionals.md +++ b/pythonlings/docs/topics/conditionals.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/controlflow.html#if-statements -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `if`, `elif`, and `else` choose which block runs based on boolean conditions. diff --git a/pylings/docs/topics/context_managers.md b/pythonlings/docs/topics/context_managers.md similarity index 96% rename from pylings/docs/topics/context_managers.md rename to pythonlings/docs/topics/context_managers.md index 23ea506..7ccfeb6 100644 --- a/pylings/docs/topics/context_managers.md +++ b/pythonlings/docs/topics/context_managers.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/reference/compound_stmts.html#the-with-statement -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Context managers run setup and cleanup around a block using `with`. diff --git a/pylings/docs/topics/dataclasses.md b/pythonlings/docs/topics/dataclasses.md similarity index 91% rename from pylings/docs/topics/dataclasses.md rename to pythonlings/docs/topics/dataclasses.md index 07bbd76..fb27c8e 100644 --- a/pylings/docs/topics/dataclasses.md +++ b/pythonlings/docs/topics/dataclasses.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/dataclasses.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Dataclasses generate common class methods for data-focused classes. diff --git a/pylings/docs/topics/datetime.md b/pythonlings/docs/topics/datetime.md similarity index 91% rename from pylings/docs/topics/datetime.md rename to pythonlings/docs/topics/datetime.md index 8001c55..db0afe5 100644 --- a/pylings/docs/topics/datetime.md +++ b/pythonlings/docs/topics/datetime.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/datetime.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `datetime` represents dates, times, durations, and time zones. diff --git a/pylings/docs/topics/decorators.md b/pythonlings/docs/topics/decorators.md similarity index 96% rename from pylings/docs/topics/decorators.md rename to pythonlings/docs/topics/decorators.md index 9c0d6a2..6c85fb4 100644 --- a/pylings/docs/topics/decorators.md +++ b/pythonlings/docs/topics/decorators.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/reference/compound_stmts.html#function-definitions -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Decorators wrap or transform functions and classes with `@decorator` syntax. diff --git a/pylings/docs/topics/dictionaries.md b/pythonlings/docs/topics/dictionaries.md similarity index 97% rename from pylings/docs/topics/dictionaries.md rename to pythonlings/docs/topics/dictionaries.md index b7ac0d7..9e66a42 100644 --- a/pylings/docs/topics/dictionaries.md +++ b/pythonlings/docs/topics/dictionaries.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/datastructures.html#dictionaries -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Dictionaries map keys to values and are the standard way to represent lookup tables. diff --git a/pylings/docs/topics/enums.md b/pythonlings/docs/topics/enums.md similarity index 91% rename from pylings/docs/topics/enums.md rename to pythonlings/docs/topics/enums.md index 56f307c..41308c6 100644 --- a/pylings/docs/topics/enums.md +++ b/pythonlings/docs/topics/enums.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/enum.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Enums define named constant values that are easier to read than raw literals. diff --git a/pylings/docs/topics/exceptions.md b/pythonlings/docs/topics/exceptions.md similarity index 96% rename from pylings/docs/topics/exceptions.md rename to pythonlings/docs/topics/exceptions.md index 8bb8b3f..f2be000 100644 --- a/pylings/docs/topics/exceptions.md +++ b/pythonlings/docs/topics/exceptions.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/errors.html#handling-exceptions -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `try` and `except` handle errors without crashing the whole program. diff --git a/pylings/docs/topics/file_io.md b/pythonlings/docs/topics/file_io.md similarity index 97% rename from pylings/docs/topics/file_io.md rename to pythonlings/docs/topics/file_io.md index 0c02db1..5c20b6d 100644 --- a/pylings/docs/topics/file_io.md +++ b/pythonlings/docs/topics/file_io.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `open` and `with` are the usual tools for reading and writing files safely. diff --git a/pylings/docs/topics/functional.md b/pythonlings/docs/topics/functional.md similarity index 91% rename from pylings/docs/topics/functional.md rename to pythonlings/docs/topics/functional.md index fee01f5..16a0f67 100644 --- a/pylings/docs/topics/functional.md +++ b/pythonlings/docs/topics/functional.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/howto/functional.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Functional style uses functions, iterators, and transformations as building blocks. diff --git a/pylings/docs/topics/functions.md b/pythonlings/docs/topics/functions.md similarity index 96% rename from pylings/docs/topics/functions.md rename to pythonlings/docs/topics/functions.md index c642977..47c161a 100644 --- a/pylings/docs/topics/functions.md +++ b/pythonlings/docs/topics/functions.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/controlflow.html#defining-functions -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `def` creates reusable behavior with parameters, return values, and optional defaults. diff --git a/pylings/docs/topics/generators.md b/pythonlings/docs/topics/generators.md similarity index 95% rename from pylings/docs/topics/generators.md rename to pythonlings/docs/topics/generators.md index 997e190..8b1995f 100644 --- a/pylings/docs/topics/generators.md +++ b/pythonlings/docs/topics/generators.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/classes.html#generators -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Generators produce values lazily with `yield` instead of returning a whole collection. diff --git a/pylings/docs/topics/itertools.md b/pythonlings/docs/topics/itertools.md similarity index 92% rename from pylings/docs/topics/itertools.md rename to pythonlings/docs/topics/itertools.md index 5c432c6..4ebaba3 100644 --- a/pylings/docs/topics/itertools.md +++ b/pythonlings/docs/topics/itertools.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/itertools.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `itertools` provides fast iterator building blocks for loops and data pipelines. diff --git a/pylings/docs/topics/json.md b/pythonlings/docs/topics/json.md similarity index 90% rename from pylings/docs/topics/json.md rename to pythonlings/docs/topics/json.md index 6388449..7077e1b 100644 --- a/pylings/docs/topics/json.md +++ b/pythonlings/docs/topics/json.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/json.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `json` converts between Python values and JSON text. diff --git a/pylings/docs/topics/lists.md b/pythonlings/docs/topics/lists.md similarity index 96% rename from pylings/docs/topics/lists.md rename to pythonlings/docs/topics/lists.md index a2ce9af..bf0e470 100644 --- a/pylings/docs/topics/lists.md +++ b/pythonlings/docs/topics/lists.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Lists are mutable ordered collections with methods such as `append`, `pop`, and `sort`. diff --git a/pylings/docs/topics/loops.md b/pythonlings/docs/topics/loops.md similarity index 96% rename from pylings/docs/topics/loops.md rename to pythonlings/docs/topics/loops.md index 14422cd..cab96d4 100644 --- a/pylings/docs/topics/loops.md +++ b/pythonlings/docs/topics/loops.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/controlflow.html#for-statements -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `for` loops iterate over items; `while` loops continue while a condition is true. diff --git a/pylings/docs/topics/modules.md b/pythonlings/docs/topics/modules.md similarity index 97% rename from pylings/docs/topics/modules.md rename to pythonlings/docs/topics/modules.md index cd7a31b..01581bc 100644 --- a/pylings/docs/topics/modules.md +++ b/pythonlings/docs/topics/modules.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/modules.html#modules -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Modules organize Python code into importable files and packages. diff --git a/pylings/docs/topics/oop_advanced.md b/pythonlings/docs/topics/oop_advanced.md similarity index 91% rename from pylings/docs/topics/oop_advanced.md rename to pythonlings/docs/topics/oop_advanced.md index 28e578c..907844b 100644 --- a/pylings/docs/topics/oop_advanced.md +++ b/pythonlings/docs/topics/oop_advanced.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/classes.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Advanced class patterns build on attributes, methods, inheritance, and special methods. diff --git a/pylings/docs/topics/pathlib.md b/pythonlings/docs/topics/pathlib.md similarity index 91% rename from pylings/docs/topics/pathlib.md rename to pythonlings/docs/topics/pathlib.md index 2d236c0..1328c4f 100644 --- a/pylings/docs/topics/pathlib.md +++ b/pythonlings/docs/topics/pathlib.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/pathlib.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. `pathlib.Path` represents filesystem paths with object-oriented helpers. diff --git a/pylings/docs/topics/recursion.md b/pythonlings/docs/topics/recursion.md similarity index 96% rename from pylings/docs/topics/recursion.md rename to pythonlings/docs/topics/recursion.md index 97f72bb..a668840 100644 --- a/pylings/docs/topics/recursion.md +++ b/pythonlings/docs/topics/recursion.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/controlflow.html#defining-functions -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Recursive functions solve a problem by calling themselves on smaller inputs. diff --git a/pylings/docs/topics/regex.md b/pythonlings/docs/topics/regex.md similarity index 91% rename from pylings/docs/topics/regex.md rename to pythonlings/docs/topics/regex.md index 7c050ae..1bae38f 100644 --- a/pylings/docs/topics/regex.md +++ b/pythonlings/docs/topics/regex.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/re.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. The `re` module searches, matches, and transforms text using regular expressions. diff --git a/pylings/docs/topics/sets.md b/pythonlings/docs/topics/sets.md similarity index 96% rename from pylings/docs/topics/sets.md rename to pythonlings/docs/topics/sets.md index 0b9fa6a..587de90 100644 --- a/pylings/docs/topics/sets.md +++ b/pythonlings/docs/topics/sets.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/datastructures.html#sets -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Sets store unique values and support membership tests and set operations. diff --git a/pylings/docs/topics/strings.md b/pythonlings/docs/topics/strings.md similarity index 96% rename from pylings/docs/topics/strings.md rename to pythonlings/docs/topics/strings.md index 389006e..8782b36 100644 --- a/pylings/docs/topics/strings.md +++ b/pythonlings/docs/topics/strings.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Strings are immutable text sequences with indexing, slicing, and many useful methods. diff --git a/pylings/docs/topics/testing.md b/pythonlings/docs/topics/testing.md similarity index 91% rename from pylings/docs/topics/testing.md rename to pythonlings/docs/topics/testing.md index 74c56da..9508ac7 100644 --- a/pylings/docs/topics/testing.md +++ b/pythonlings/docs/topics/testing.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/unittest.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Tests encode expected behavior so code can be checked repeatedly. diff --git a/pylings/docs/topics/tuples.md b/pythonlings/docs/topics/tuples.md similarity index 96% rename from pylings/docs/topics/tuples.md rename to pythonlings/docs/topics/tuples.md index f61371c..b24a696 100644 --- a/pylings/docs/topics/tuples.md +++ b/pythonlings/docs/topics/tuples.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Tuples group ordered values and are commonly unpacked into multiple names. diff --git a/pylings/docs/topics/type_hints.md b/pythonlings/docs/topics/type_hints.md similarity index 91% rename from pylings/docs/topics/type_hints.md rename to pythonlings/docs/topics/type_hints.md index 9a5d844..f5bc06e 100644 --- a/pylings/docs/topics/type_hints.md +++ b/pythonlings/docs/topics/type_hints.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/library/typing.html -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Type hints annotate expected values and help tools reason about your code. diff --git a/pylings/docs/topics/variables.md b/pythonlings/docs/topics/variables.md similarity index 96% rename from pylings/docs/topics/variables.md rename to pythonlings/docs/topics/variables.md index c8a9c64..96fab9f 100644 --- a/pylings/docs/topics/variables.md +++ b/pythonlings/docs/topics/variables.md @@ -2,7 +2,7 @@ Source: https://docs.python.org/3/tutorial/introduction.html#using-python-as-a-calculator -This local reference is generated from the official Python documentation and trimmed for pylings. +This local reference is generated from the official Python documentation and trimmed for pythonlings. Assigning names with `=` lets you keep values for later expressions. diff --git a/pylings/pylings.tcss b/pythonlings/pythonlings.tcss similarity index 100% rename from pylings/pylings.tcss rename to pythonlings/pythonlings.tcss diff --git a/pylings/screens/__init__.py b/pythonlings/screens/__init__.py similarity index 100% rename from pylings/screens/__init__.py rename to pythonlings/screens/__init__.py diff --git a/pylings/screens/docs.py b/pythonlings/screens/docs.py similarity index 95% rename from pylings/screens/docs.py rename to pythonlings/screens/docs.py index ba0eb44..d5002b5 100644 --- a/pylings/screens/docs.py +++ b/pythonlings/screens/docs.py @@ -1,4 +1,4 @@ -# pylings/screens/docs.py +# pythonlings/screens/docs.py from __future__ import annotations import webbrowser @@ -11,8 +11,8 @@ from textual.screen import ModalScreen from textual.widgets import Markdown, Static -from pylings.core.docs import load_snippet -from pylings.core.exercise import Exercise +from pythonlings.core.docs import load_snippet +from pythonlings.core.exercise import Exercise class DocsScreen(ModalScreen[None]): diff --git a/pylings/screens/topic_picker.py b/pythonlings/screens/topic_picker.py similarity index 94% rename from pylings/screens/topic_picker.py rename to pythonlings/screens/topic_picker.py index 56ccb67..c88f780 100644 --- a/pylings/screens/topic_picker.py +++ b/pythonlings/screens/topic_picker.py @@ -1,4 +1,4 @@ -# pylings/screens/topic_picker.py +# pythonlings/screens/topic_picker.py from __future__ import annotations from textual.app import ComposeResult @@ -7,7 +7,7 @@ from textual.screen import Screen from textual.widgets import Footer, Header, ListItem, ListView, Static -from pylings.core.state import save as save_state +from pythonlings.core.state import save as save_state class TopicPickerScreen(Screen[None]): @@ -25,7 +25,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.app.title = "pylings" + self.app.title = "pythonlings" self.app.sub_title = "choose a topic" self._populate() @@ -97,7 +97,7 @@ def _banner_text(self) -> str: def on_list_view_selected(self, event: ListView.Selected) -> None: topic = event.item.name if topic: - from pylings.screens.track import TrackScreen + from pythonlings.screens.track import TrackScreen self.app.state.record_resume(topic, None) save_state(self.app.root, self.app.state) diff --git a/pylings/screens/track.py b/pythonlings/screens/track.py similarity index 93% rename from pylings/screens/track.py rename to pythonlings/screens/track.py index ac66dc8..bb0332f 100644 --- a/pylings/screens/track.py +++ b/pythonlings/screens/track.py @@ -1,4 +1,4 @@ -# pylings/screens/track.py +# pythonlings/screens/track.py from __future__ import annotations from textual.app import ComposeResult @@ -8,13 +8,13 @@ from textual.timer import Timer from textual.widgets import Footer, Header, TextArea -from pylings.core.exercise import Exercise, RunResult -from pylings.core.runner import run as run_exercise -from pylings.core.state import next_pending, save as save_state -from pylings.widgets.editor_pane import EditorPane -from pylings.widgets.exercise_tree import ExerciseTree -from pylings.widgets.output_panel import OutputPanel -from pylings.widgets.progress import ProgressBar +from pythonlings.core.exercise import Exercise, RunResult +from pythonlings.core.runner import run as run_exercise +from pythonlings.core.state import next_pending, save as save_state +from pythonlings.widgets.editor_pane import EditorPane +from pythonlings.widgets.exercise_tree import ExerciseTree +from pythonlings.widgets.output_panel import OutputPanel +from pythonlings.widgets.progress import ProgressBar _DEBOUNCE_SECONDS = 0.6 @@ -190,7 +190,7 @@ def action_toggle_hint(self) -> None: self.query_one(OutputPanel).toggle_hint(self._exercise(self.current).hint) def action_reset(self) -> None: - from pylings.core.reset import restore + from pythonlings.core.reset import restore if self.current is None: return @@ -208,7 +208,7 @@ def action_toggle_list(self) -> None: def action_docs(self) -> None: if self.current is None: return - from pylings.screens.docs import DocsScreen + from pythonlings.screens.docs import DocsScreen self.app.push_screen(DocsScreen(self._exercise(self.current))) diff --git a/pylings/widgets/__init__.py b/pythonlings/widgets/__init__.py similarity index 100% rename from pylings/widgets/__init__.py rename to pythonlings/widgets/__init__.py diff --git a/pylings/widgets/editor_pane.py b/pythonlings/widgets/editor_pane.py similarity index 92% rename from pylings/widgets/editor_pane.py rename to pythonlings/widgets/editor_pane.py index ca7b53b..8308664 100644 --- a/pylings/widgets/editor_pane.py +++ b/pythonlings/widgets/editor_pane.py @@ -1,11 +1,11 @@ -# pylings/widgets/editor_pane.py +# pythonlings/widgets/editor_pane.py from __future__ import annotations from textual.app import ComposeResult from textual.containers import Vertical from textual.widgets import TextArea -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise class EditorPane(Vertical): diff --git a/pylings/widgets/exercise_tree.py b/pythonlings/widgets/exercise_tree.py similarity index 88% rename from pylings/widgets/exercise_tree.py rename to pythonlings/widgets/exercise_tree.py index 9fe6b55..57d494a 100644 --- a/pylings/widgets/exercise_tree.py +++ b/pythonlings/widgets/exercise_tree.py @@ -1,4 +1,4 @@ -# pylings/widgets/exercise_tree.py +# pythonlings/widgets/exercise_tree.py from __future__ import annotations from typing import TYPE_CHECKING @@ -6,9 +6,9 @@ from textual.widgets import Tree if TYPE_CHECKING: - from pylings.core.exercise import Exercise - from pylings.core.manifest import Manifest - from pylings.core.state import State + from pythonlings.core.exercise import Exercise + from pythonlings.core.manifest import Manifest + from pythonlings.core.state import State class ExerciseTree(Tree[str]): diff --git a/pylings/widgets/output_panel.py b/pythonlings/widgets/output_panel.py similarity index 98% rename from pylings/widgets/output_panel.py rename to pythonlings/widgets/output_panel.py index ff727bc..b12b5df 100644 --- a/pylings/widgets/output_panel.py +++ b/pythonlings/widgets/output_panel.py @@ -1,4 +1,4 @@ -# pylings/widgets/output_panel.py +# pythonlings/widgets/output_panel.py from __future__ import annotations import os @@ -7,7 +7,7 @@ from textual.containers import Vertical from textual.widgets import Static -from pylings.core.exercise import Exercise, RunResult +from pythonlings.core.exercise import Exercise, RunResult _INSTRUCTION = "Edit the code on the left. Checks update automatically." diff --git a/pylings/widgets/progress.py b/pythonlings/widgets/progress.py similarity index 92% rename from pylings/widgets/progress.py rename to pythonlings/widgets/progress.py index 76d2a8e..0e6bac5 100644 --- a/pylings/widgets/progress.py +++ b/pythonlings/widgets/progress.py @@ -1,4 +1,4 @@ -# pylings/widgets/progress.py +# pythonlings/widgets/progress.py from __future__ import annotations from textual.widgets import Static diff --git a/scripts/fetch_python_docs.py b/scripts/fetch_python_docs.py index f1daf7c..422a54b 100644 --- a/scripts/fetch_python_docs.py +++ b/scripts/fetch_python_docs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Fetch small local reference snippets from the official Python docs. -The generated snippets are committed so learners can use pylings offline. +The generated snippets are committed so learners can use pythonlings offline. Run from the repository root: python scripts/fetch_python_docs.py @@ -21,7 +21,7 @@ BASE_URL = "https://docs.python.org/3/" -OUTPUT = Path("pylings/docs") +OUTPUT = Path("pythonlings/docs") MAX_LINES = 26 @@ -141,7 +141,7 @@ def _collecting(self) -> bool: def fetch(url: str) -> str: request = urllib.request.Request( url, - headers={"User-Agent": "pylings-doc-fetcher/0.1"}, + headers={"User-Agent": "pythonlings-doc-fetcher/0.1"}, ) with urllib.request.urlopen(request, timeout=20) as response: return response.read().decode("utf-8", errors="replace") @@ -180,7 +180,7 @@ def render_markdown(source: Source, extracted: str) -> str: f"# {source.title}\n\n" f"Source: {source.url}\n\n" "This local reference is generated from the official Python documentation " - "and trimmed for pylings.\n\n" + "and trimmed for pythonlings.\n\n" f"{source.summary}\n\n" "## Extracted reference\n\n" f"{extracted}\n" @@ -215,7 +215,7 @@ def write_outputs(sources: tuple[Source, ...], output: Path) -> None: # Bundled Python Documentation Snippets These snippets are generated from the official Python documentation at - https://docs.python.org/3/ and trimmed for use inside pylings. + https://docs.python.org/3/ and trimmed for use inside pythonlings. Python documentation pages are licensed under the Python Software Foundation License Version 2. Examples, recipes, and other code in the diff --git a/scripts/generate_demo_gif.sh b/scripts/generate_demo_gif.sh index dcf6af4..13d3cbd 100755 --- a/scripts/generate_demo_gif.sh +++ b/scripts/generate_demo_gif.sh @@ -22,6 +22,6 @@ EOF fi mkdir -p "$output_dir" -rm -rf /tmp/pylings-demo +rm -rf /tmp/pythonlings-demo cd "$repo_root" "$vhs_bin" docs/demo.tape diff --git a/solutions/_answers.py b/solutions/_answers.py index 2682d80..86660a0 100644 --- a/solutions/_answers.py +++ b/solutions/_answers.py @@ -1038,7 +1038,7 @@ 'buffer.seek(0)\n' 'restored = json.load(buffer)\n', 'json5': 'import json\n' - 'data = json.loads(\'{"name": "Pylings"}\')\n' + 'data = json.loads(\'{"name": "Pythonlings"}\')\n' 'timezone = data.get("timezone", "UTC")\n', 'json6': 'import json\n' 'raw = \'[{"name": "Ada", "active": true}, {"name": "Lin", "active": false}, {"name": ' @@ -1050,7 +1050,7 @@ 'settings["theme"] = "dark"\n' 'encoded = json.dumps(settings, sort_keys=True)\n', 'json8': 'import json\n' - 'original = {"project": "pylings", "tags": ["python", "practice"], "meta": {"level": ' + 'original = {"project": "pythonlings", "tags": ["python", "practice"], "meta": {"level": ' '3}}\n' 'encoded = json.dumps(original, sort_keys=True)\n' 'decoded = json.loads(encoded)\n' @@ -1269,7 +1269,7 @@ ' return 0 <= percent <= 100\n', 'pathlib1': 'from pathlib import Path\npath = Path("notes/today.txt")\nfilename = path.name\n', 'pathlib2': 'from pathlib import Path\n' - 'project = Path("pylings")\n' + 'project = Path("pythonlings")\n' 'source = project / "src" / "main.py"\n', 'pathlib3': 'from pathlib import Path\n' 'path = Path("docs/guide.md")\n' @@ -1476,7 +1476,7 @@ '\n' 'def test_title_case():\n' ' assert to_title_case("hello world") == "Hello World"\n' - ' assert to_title_case("pylings") == "Pylings"\n' + ' assert to_title_case("pythonlings") == "Pythonlings"\n' '\n' 'def test_truncate():\n' ' assert truncate("short", 10) == "short"\n' diff --git a/tests/integration/test_cli_cold_start.py b/tests/integration/test_cli_cold_start.py index 5776916..6833d83 100644 --- a/tests/integration/test_cli_cold_start.py +++ b/tests/integration/test_cli_cold_start.py @@ -12,7 +12,7 @@ def _cold_start_ms(*args: str) -> str: # We don't measure wall-clock for the test (CI flake); we assert that # textual was never imported in the subcommand path. proc = subprocess.run( - [sys.executable, "-X", "importtime", "-m", "pylings", "--root", str(FIXTURES), *args], + [sys.executable, "-X", "importtime", "-m", "pythonlings", "--root", str(FIXTURES), *args], capture_output=True, text=True, ) @@ -21,7 +21,7 @@ def _cold_start_ms(*args: str) -> str: def test_hint_does_not_import_textual() -> None: out = _cold_start_ms("hint", "passing") - assert "import 'textual'" not in out, f"textual loaded for `pylings hint`:\n{out}" + assert "import 'textual'" not in out, f"textual loaded for `pythonlings hint`:\n{out}" def test_list_does_not_import_textual() -> None: diff --git a/tests/integration/test_cli_dry_run.py b/tests/integration/test_cli_dry_run.py index f94c1ff..ff848f3 100644 --- a/tests/integration/test_cli_dry_run.py +++ b/tests/integration/test_cli_dry_run.py @@ -1,4 +1,4 @@ -from pylings.cli import main +from pythonlings.cli import main def test_dry_run_alias_runs_one_exercise() -> None: diff --git a/tests/integration/test_cli_hint.py b/tests/integration/test_cli_hint.py index 43311ea..617e97a 100644 --- a/tests/integration/test_cli_hint.py +++ b/tests/integration/test_cli_hint.py @@ -8,7 +8,7 @@ def _run(*args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, "-m", "pylings", *args], + [sys.executable, "-m", "pythonlings", *args], capture_output=True, text=True, ) diff --git a/tests/integration/test_cli_reset.py b/tests/integration/test_cli_reset.py index a06a6ec..922b1d5 100644 --- a/tests/integration/test_cli_reset.py +++ b/tests/integration/test_cli_reset.py @@ -6,14 +6,14 @@ import sys from pathlib import Path -from pylings.core.curriculum import init_workspace +from pythonlings.core.curriculum import init_workspace FIXTURES = Path(__file__).parent.parent / "fixtures" / "tiny_curriculum" def _run(*args: str, input: str | None = None) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, "-m", "pylings", *args], + [sys.executable, "-m", "pythonlings", *args], capture_output=True, text=True, input=input, @@ -43,14 +43,14 @@ def test_reset_without_yes_aborts_on_no(tmp_path: Path) -> None: def test_reset_does_not_create_original_from_current_file(tmp_path: Path) -> None: work = tmp_path / "work" - shutil.copytree(FIXTURES, work, ignore=shutil.ignore_patterns(".pylings")) + shutil.copytree(FIXTURES, work, ignore=shutil.ignore_patterns(".pythonlings")) target = work / "exercises" / "passing.py" target.write_text("# learner edit before reset\n", encoding="utf-8") result = _run("--root", str(work), "reset", "passing", "--yes") assert result.returncode != 0 - assert "Run 'pylings update' first" in result.stderr + assert "Run 'pythonlings update' first" in result.stderr assert target.read_text() == "# learner edit before reset\n" diff --git a/tests/integration/test_cli_run.py b/tests/integration/test_cli_run.py index c4f2f5d..4a7438d 100644 --- a/tests/integration/test_cli_run.py +++ b/tests/integration/test_cli_run.py @@ -8,7 +8,7 @@ def _run(*args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, "-m", "pylings", *args], + [sys.executable, "-m", "pythonlings", *args], capture_output=True, text=True, ) diff --git a/tests/integration/test_cli_solution.py b/tests/integration/test_cli_solution.py index 8c64d8f..953e5bf 100644 --- a/tests/integration/test_cli_solution.py +++ b/tests/integration/test_cli_solution.py @@ -1,6 +1,6 @@ from pathlib import Path -from pylings.cli import main +from pythonlings.cli import main def test_solution_command_runs_workspace_solution(tmp_path: Path) -> None: @@ -51,4 +51,4 @@ def test_debug_writes_workspace_log(tmp_path: Path) -> None: code = main(["--debug", "--root", str(root), "run", "passing1"]) assert code == 0 - assert "run" in (root / ".pylings_debug.log").read_text(encoding="utf-8") + assert "run" in (root / ".pythonlings_debug.log").read_text(encoding="utf-8") diff --git a/tests/integration/test_cli_topics.py b/tests/integration/test_cli_topics.py index 34a6a7b..e7ece28 100644 --- a/tests/integration/test_cli_topics.py +++ b/tests/integration/test_cli_topics.py @@ -3,14 +3,14 @@ import sys from pathlib import Path -from pylings.cli import _build_parser +from pythonlings.cli import _build_parser FIXTURES = Path(__file__).parent.parent / "fixtures" / "tiny_curriculum" def _run(*args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, "-m", "pylings", *args], capture_output=True, text=True + [sys.executable, "-m", "pythonlings", *args], capture_output=True, text=True ) diff --git a/tests/integration/test_cli_verify.py b/tests/integration/test_cli_verify.py index 534644a..0e3fc07 100644 --- a/tests/integration/test_cli_verify.py +++ b/tests/integration/test_cli_verify.py @@ -8,7 +8,7 @@ def _run(*args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, "-m", "pylings", *args], + [sys.executable, "-m", "pythonlings", *args], capture_output=True, text=True, ) diff --git a/tests/integration/test_cli_workspace.py b/tests/integration/test_cli_workspace.py index 2eb8765..632a5be 100644 --- a/tests/integration/test_cli_workspace.py +++ b/tests/integration/test_cli_workspace.py @@ -1,6 +1,6 @@ from pathlib import Path -from pylings.cli import main +from pythonlings.cli import main def test_init_command_creates_workspace(tmp_path: Path) -> None: @@ -37,4 +37,4 @@ def test_update_command_preserves_user_exercises(tmp_path: Path) -> None: assert code == 0 assert exercise.read_text(encoding="utf-8") == "# edited\n" - assert (target / ".pylings" / "originals").is_dir() + assert (target / ".pythonlings" / "originals").is_dir() diff --git a/tests/integration/test_installed_package.py b/tests/integration/test_installed_package.py index 1a7f2dc..91e5628 100644 --- a/tests/integration/test_installed_package.py +++ b/tests/integration/test_installed_package.py @@ -1,6 +1,6 @@ from pathlib import Path -from pylings.cli import main +from pythonlings.cli import main def test_package_can_initialize_and_list_workspace(tmp_path: Path) -> None: diff --git a/tests/integration/test_solution_verify.py b/tests/integration/test_solution_verify.py index c0edda3..62668a2 100644 --- a/tests/integration/test_solution_verify.py +++ b/tests/integration/test_solution_verify.py @@ -1,8 +1,8 @@ from pathlib import Path -from pylings.core.manifest import load -from pylings.core.runner import run_verify -from pylings.core.solutions import solution_exercise +from pythonlings.core.manifest import load +from pythonlings.core.runner import run_verify +from pythonlings.core.solutions import solution_exercise def test_every_reference_solution_passes() -> None: diff --git a/tests/tui/test_app_pilot.py b/tests/tui/test_app_pilot.py index f213cab..3452193 100644 --- a/tests/tui/test_app_pilot.py +++ b/tests/tui/test_app_pilot.py @@ -7,19 +7,19 @@ from textual.widgets import Markdown, Static, TextArea from textual.worker import WorkerCancelled -from pylings.app import PylingsApp -from pylings.core.state import State, save as save_state -from pylings.screens.docs import DocsScreen -from pylings.screens.topic_picker import TopicPickerScreen -from pylings.screens.track import TrackScreen -from pylings.widgets.output_panel import OutputPanel +from pythonlings.app import PythonlingsApp +from pythonlings.core.state import State, save as save_state +from pythonlings.screens.docs import DocsScreen +from pythonlings.screens.topic_picker import TopicPickerScreen +from pythonlings.screens.track import TrackScreen +from pythonlings.widgets.output_panel import OutputPanel MULTI = Path(__file__).parent.parent / "fixtures" / "multi_topic" def _work_copy(tmp_path: Path) -> Path: work = tmp_path / "work" - shutil.copytree(MULTI, work, ignore=shutil.ignore_patterns(".pylings")) + shutil.copytree(MULTI, work, ignore=shutil.ignore_patterns(".pythonlings")) return work @@ -34,7 +34,7 @@ async def _settle(pilot) -> None: @pytest.mark.asyncio async def test_default_launch_opens_first_pending_exercise(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path)) + app = PythonlingsApp(root=_work_copy(tmp_path)) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -44,7 +44,7 @@ async def test_default_launch_opens_first_pending_exercise(tmp_path: Path) -> No @pytest.mark.asyncio async def test_picker_lists_topics_with_progress(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), force_picker=True) + app = PythonlingsApp(root=_work_copy(tmp_path), force_picker=True) async with app.run_test() as pilot: await _settle(pilot) rendered = " ".join( @@ -56,7 +56,7 @@ async def test_picker_lists_topics_with_progress(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_enter_on_picker_opens_selected_topic(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), force_picker=True) + app = PythonlingsApp(root=_work_copy(tmp_path), force_picker=True) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TopicPickerScreen) @@ -69,7 +69,7 @@ async def test_enter_on_picker_opens_selected_topic(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_picker_shows_first_run_start_banner(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), force_picker=True) + app = PythonlingsApp(root=_work_copy(tmp_path), force_picker=True) async with app.run_test() as pilot: await _settle(pilot) banner = str(app.screen.query_one("#topic-banner").content) @@ -81,7 +81,7 @@ async def test_picker_shows_first_run_start_banner(tmp_path: Path) -> None: async def test_picker_rows_show_status_labels(tmp_path: Path) -> None: work = _work_copy(tmp_path) save_state(work, State(completed={"a1"}, seen_intro=True, last_topic="alpha")) - app = PylingsApp(root=work, force_picker=True) + app = PythonlingsApp(root=work, force_picker=True) async with app.run_test() as pilot: await _settle(pilot) rendered = " ".join( @@ -94,7 +94,7 @@ async def test_picker_rows_show_status_labels(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_start_topic_opens_track(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -108,7 +108,7 @@ async def test_default_launch_resumes_last_incomplete_exercise(tmp_path: Path) - work, State(seen_intro=True, last_topic="alpha", last_exercise="a2"), ) - app = PylingsApp(root=work) + app = PythonlingsApp(root=work) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -123,7 +123,7 @@ async def test_default_launch_falls_back_from_invalid_resume_state(tmp_path: Pat work, State(seen_intro=True, last_topic="missing", last_exercise="ghost"), ) - app = PylingsApp(root=work) + app = PythonlingsApp(root=work) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -138,7 +138,7 @@ async def test_force_picker_launch_ignores_resume_state(tmp_path: Path) -> None: work, State(seen_intro=True, last_topic="alpha", last_exercise="a2"), ) - app = PylingsApp(root=work, force_picker=True) + app = PythonlingsApp(root=work, force_picker=True) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TopicPickerScreen) @@ -146,7 +146,7 @@ async def test_force_picker_launch_ignores_resume_state(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_f4_returns_to_picker(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -157,7 +157,7 @@ async def test_f4_returns_to_picker(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_escape_quits_from_track_screen(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -168,7 +168,7 @@ async def test_escape_quits_from_track_screen(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_escape_quits_from_topic_picker(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), force_picker=True) + app = PythonlingsApp(root=_work_copy(tmp_path), force_picker=True) async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TopicPickerScreen) @@ -179,7 +179,7 @@ async def test_escape_quits_from_topic_picker(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_f5_opens_docs_popup_for_current_exercise(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) assert isinstance(app.screen, TrackScreen) @@ -199,7 +199,7 @@ async def test_f5_opens_docs_popup_for_current_exercise(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_escape_closes_docs_popup_without_quitting(tmp_path: Path) -> None: - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) await pilot.press("f5") @@ -217,7 +217,7 @@ async def test_docs_popup_can_open_browser( ) -> None: opened: list[str] = [] monkeypatch.setattr(webbrowser, "open", lambda url: opened.append(url)) - app = PylingsApp(root=_work_copy(tmp_path), start_topic="alpha") + app = PythonlingsApp(root=_work_copy(tmp_path), start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) await pilot.press("f5") @@ -231,7 +231,7 @@ async def test_docs_popup_can_open_browser( @pytest.mark.asyncio async def test_track_records_resume_when_loaded(tmp_path: Path) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="alpha") + app = PythonlingsApp(root=work, start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) assert app.state.seen_intro is True @@ -244,7 +244,7 @@ async def test_instant_advance_updates_resume_to_next_exercise( tmp_path: Path, ) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="alpha") + app = PythonlingsApp(root=work, start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) track = app.screen @@ -261,7 +261,7 @@ async def test_instant_advance_updates_resume_to_next_exercise( @pytest.mark.asyncio async def test_failed_run_shows_progressive_nudge(tmp_path: Path) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="alpha") + app = PythonlingsApp(root=work, start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) track = app.screen @@ -275,7 +275,7 @@ async def test_failed_run_shows_progressive_nudge(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_hint_visibility_resets_after_advance(tmp_path: Path) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="alpha") + app = PythonlingsApp(root=work, start_topic="alpha") async with app.run_test() as pilot: await _settle(pilot) track = app.screen @@ -294,7 +294,7 @@ async def test_hint_visibility_resets_after_advance(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_solving_a_topic_marks_progress(tmp_path: Path) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="beta") + app = PythonlingsApp(root=work, start_topic="beta") async with app.run_test() as pilot: await _settle(pilot) track = app.screen @@ -309,7 +309,7 @@ async def test_solving_a_topic_marks_progress(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_picker_refreshes_progress_after_returning(tmp_path: Path) -> None: work = _work_copy(tmp_path) - app = PylingsApp(root=work, start_topic="beta") + app = PythonlingsApp(root=work, start_topic="beta") async with app.run_test() as pilot: await _settle(pilot) track = app.screen diff --git a/tests/tui/test_editor_pane.py b/tests/tui/test_editor_pane.py index 777e5b9..e763652 100644 --- a/tests/tui/test_editor_pane.py +++ b/tests/tui/test_editor_pane.py @@ -5,8 +5,8 @@ from textual.app import App, ComposeResult from textual.widgets import TextArea -from pylings.core.exercise import Exercise -from pylings.widgets.editor_pane import EditorPane +from pythonlings.core.exercise import Exercise +from pythonlings.widgets.editor_pane import EditorPane class _Harness(App[None]): diff --git a/tests/tui/test_output_panel.py b/tests/tui/test_output_panel.py index 65ee47e..4dcfb83 100644 --- a/tests/tui/test_output_panel.py +++ b/tests/tui/test_output_panel.py @@ -4,8 +4,8 @@ from textual.app import App, ComposeResult from textual.widgets import Static -from pylings.core.exercise import Exercise, RunResult -from pylings.widgets.output_panel import OutputPanel +from pythonlings.core.exercise import Exercise, RunResult +from pythonlings.widgets.output_panel import OutputPanel class _Harness(App[None]): diff --git a/tests/unit/test_curriculum.py b/tests/unit/test_curriculum.py index df243a6..948cb45 100644 --- a/tests/unit/test_curriculum.py +++ b/tests/unit/test_curriculum.py @@ -1,6 +1,6 @@ from pathlib import Path -from pylings.core import curriculum +from pythonlings.core import curriculum def test_source_root_finds_curriculum_files() -> None: @@ -21,10 +21,10 @@ def test_init_workspace_copies_curriculum(tmp_path: Path) -> None: assert (target / "exercises").is_dir() assert (target / "checks").is_dir() assert (target / "solutions").is_dir() - assert (target / ".pylings" / "originals").is_dir() + assert (target / ".pythonlings" / "originals").is_dir() assert (target / ".gitignore").read_text(encoding="utf-8").splitlines() == [ - ".pylings/state.json", - ".pylings_debug.log", + ".pythonlings/state.json", + ".pythonlings_debug.log", "__pycache__/", "*.pyc", ] @@ -51,6 +51,6 @@ def test_update_workspace_preserves_user_exercise_edit(tmp_path: Path) -> None: curriculum.update_workspace(target) assert exercise.read_text(encoding="utf-8") == "# user edit\n" - original = target / ".pylings" / "originals" / exercise.relative_to(target / "exercises") + original = target / ".pythonlings" / "originals" / exercise.relative_to(target / "exercises") assert original.exists() assert (target / "solutions" / "_answers.py").exists() diff --git a/tests/unit/test_docs.py b/tests/unit/test_docs.py index b5673d8..bb5831e 100644 --- a/tests/unit/test_docs.py +++ b/tests/unit/test_docs.py @@ -1,8 +1,8 @@ from pathlib import Path -from pylings.core.docs import load_snippet, load_topic_snippet -from pylings.core.manifest import load -from pylings.screens.docs import DocsScreen +from pythonlings.core.docs import load_snippet, load_topic_snippet +from pythonlings.core.manifest import load +from pythonlings.screens.docs import DocsScreen def test_load_topic_snippet_returns_bundled_reference() -> None: diff --git a/tests/unit/test_exercise.py b/tests/unit/test_exercise.py index 90ed9cc..346745b 100644 --- a/tests/unit/test_exercise.py +++ b/tests/unit/test_exercise.py @@ -5,7 +5,7 @@ import pytest -from pylings.core.exercise import Exercise +from pythonlings.core.exercise import Exercise def _ex(path: Path) -> Exercise: diff --git a/tests/unit/test_manifest.py b/tests/unit/test_manifest.py index bd97085..43823bf 100644 --- a/tests/unit/test_manifest.py +++ b/tests/unit/test_manifest.py @@ -3,7 +3,7 @@ import pytest -from pylings.core.manifest import Manifest, ManifestError, load +from pythonlings.core.manifest import Manifest, ManifestError, load FIXTURES = Path(__file__).parent.parent / "fixtures" / "tiny_curriculum" @@ -50,7 +50,7 @@ def test_check_path_is_derived() -> None: def test_load_defaults_messages_when_omitted(tmp_path: Path) -> None: _write_curriculum(tmp_path, "a") manifest = load(tmp_path) - assert manifest.welcome_message == "Welcome to pylings!" + assert manifest.welcome_message == "Welcome to pythonlings!" assert manifest.final_message == "All exercises complete." diff --git a/tests/unit/test_reset.py b/tests/unit/test_reset.py index bc33511..7d4f39d 100644 --- a/tests/unit/test_reset.py +++ b/tests/unit/test_reset.py @@ -4,10 +4,10 @@ import pytest -from pylings.core.curriculum import init_workspace -from pylings.core.exercise import Exercise -from pylings.core.manifest import load -from pylings.core.reset import ResetError, restore, snapshot +from pythonlings.core.curriculum import init_workspace +from pythonlings.core.exercise import Exercise +from pythonlings.core.manifest import load +from pythonlings.core.reset import ResetError, restore, snapshot def _ex(tmp_path: Path, contents: str) -> Exercise: @@ -22,10 +22,10 @@ def _ex(tmp_path: Path, contents: str) -> Exercise: ) -def test_snapshot_copies_file_to_pylings_originals(tmp_path: Path) -> None: +def test_snapshot_copies_file_to_pythonlings_originals(tmp_path: Path) -> None: ex = _ex(tmp_path, "original content\n") snapshot(tmp_path, ex) - snap = tmp_path / ".pylings" / "originals" / "ex.py" + snap = tmp_path / ".pythonlings" / "originals" / "ex.py" assert snap.exists() assert snap.read_text() == "original content\n" @@ -35,7 +35,7 @@ def test_snapshot_does_not_overwrite_existing(tmp_path: Path) -> None: snapshot(tmp_path, ex) ex.path.write_text("modified\n", encoding="utf-8") snapshot(tmp_path, ex) # second call should be a no-op - snap = tmp_path / ".pylings" / "originals" / "ex.py" + snap = tmp_path / ".pythonlings" / "originals" / "ex.py" assert snap.read_text() == "first\n" @@ -84,7 +84,7 @@ def test_snapshot_keys_on_exercise_name_not_filename(tmp_path: Path) -> None: snapshot(tmp_path, a) snapshot(tmp_path, b) - originals = tmp_path / ".pylings" / "originals" + originals = tmp_path / ".pythonlings" / "originals" assert (originals / "variables_utils.py").read_text() == "variables-version\n" assert (originals / "functions_utils.py").read_text() == "functions-version\n" @@ -93,7 +93,7 @@ def test_restore_uses_pristine_originals_not_current_file(tmp_path: Path) -> Non root = init_workspace(tmp_path / "workspace") manifest = load(root) exercise = manifest.exercises[0] - original = root / ".pylings" / "originals" / exercise.rel_path.relative_to( + original = root / ".pythonlings" / "originals" / exercise.rel_path.relative_to( "exercises" ) pristine = original.read_text(encoding="utf-8") diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index d9bad59..47f9ec8 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -2,8 +2,8 @@ from pathlib import Path -from pylings.core.exercise import Exercise -from pylings.core.runner import run +from pythonlings.core.exercise import Exercise +from pythonlings.core.runner import run CURRICULUM = Path(__file__).parent.parent / "fixtures" / "tiny_curriculum" EXERCISES = CURRICULUM / "exercises" @@ -84,14 +84,14 @@ def test_utf8_output(tmp_path: Path) -> None: def test_runner_uses_workspace_for_relative_files(tmp_path: Path) -> None: data_path = tmp_path / "data.txt" - data_path.write_text("pylings\n", encoding="utf-8") + data_path.write_text("pythonlings\n", encoding="utf-8") ex_path = tmp_path / "exercise.py" check_path = tmp_path / "check.py" ex_path.write_text( "value = open('data.txt', encoding='utf-8').read().strip()\n", encoding="utf-8", ) - check_path.write_text("assert value == 'pylings'\n", encoding="utf-8") + check_path.write_text("assert value == 'pythonlings'\n", encoding="utf-8") result = run( Exercise( diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py index 818a956..b490e25 100644 --- a/tests/unit/test_state.py +++ b/tests/unit/test_state.py @@ -1,7 +1,7 @@ # tests/unit/test_state.py from pathlib import Path -from pylings.core.state import State, load, save +from pythonlings.core.state import State, load, save def test_load_creates_fresh_state_when_missing(tmp_path: Path) -> None: @@ -35,7 +35,7 @@ def test_save_then_load_ux_fields(tmp_path: Path) -> None: def test_missing_ux_fields_default_for_existing_v2_state(tmp_path: Path) -> None: import json - pdir = tmp_path / ".pylings" + pdir = tmp_path / ".pythonlings" pdir.mkdir() (pdir / "state.json").write_text( json.dumps({"format_version": 2, "completed": ["x"]}), @@ -51,14 +51,14 @@ def test_missing_ux_fields_default_for_existing_v2_state(tmp_path: Path) -> None def test_state_file_is_format_version_2(tmp_path: Path) -> None: import json save(tmp_path, State(completed={"a"})) - data = json.loads((tmp_path / ".pylings" / "state.json").read_text()) + data = json.loads((tmp_path / ".pythonlings" / "state.json").read_text()) assert data["format_version"] == 2 assert data["completed"] == ["a"] def test_old_v1_state_is_discarded(tmp_path: Path) -> None: import json - pdir = tmp_path / ".pylings" + pdir = tmp_path / ".pythonlings" pdir.mkdir() (pdir / "state.json").write_text( json.dumps({"format_version": 1, "completed": ["x"], "current": "y"}), @@ -70,7 +70,7 @@ def test_old_v1_state_is_discarded(tmp_path: Path) -> None: def test_corrupt_state_is_recovered(tmp_path: Path) -> None: - pdir = tmp_path / ".pylings" + pdir = tmp_path / ".pythonlings" pdir.mkdir() (pdir / "state.json").write_text("not json {{", encoding="utf-8") state = load(tmp_path) diff --git a/tests/unit/test_watcher.py b/tests/unit/test_watcher.py index 73a18d2..a48a1c5 100644 --- a/tests/unit/test_watcher.py +++ b/tests/unit/test_watcher.py @@ -1,4 +1,4 @@ -from pylings.core.watcher import changed_python_file +from pythonlings.core.watcher import changed_python_file def test_changed_python_file_accepts_exercise_file() -> None: From 6ae92a45f4646e0aa90231645e3df3e6050dcd4a Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Wed, 10 Jun 2026 20:57:30 +0530 Subject: [PATCH 2/2] chore: bump version to 0.3.0 for the pythonlings rename release --- CHANGELOG.md | 2 +- Readme.md | 2 +- pyproject.toml | 2 +- pythonlings/cli.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e162e..da0772d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project are documented here. Pythonlings follows Semantic Versioning. -## [Unreleased] +## [0.3.0] - 2026-06-10 ### Changed diff --git a/Readme.md b/Readme.md index da54251..c328504 100644 --- a/Readme.md +++ b/Readme.md @@ -27,7 +27,7 @@ cd learn-python && uvx pythonlings How it works: **edit** the broken exercise in your own editor → **save** → checks rerun and advance you to the next one. That's the whole loop. -Status: `v0.2.0`, alpha — published on PyPI as `pythonlings`. +Status: `v0.3.0`, alpha — published on PyPI as `pythonlings`. ![Coding screen](docs/assets/screenshots/coding-screen.png) diff --git a/pyproject.toml b/pyproject.toml index 16a9644..8b11857 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pythonlings" -version = "0.2.0" +version = "0.3.0" description = "Python learnings, Rustlings-style, in a terminal TUI." readme = "Readme.md" requires-python = ">=3.9" diff --git a/pythonlings/cli.py b/pythonlings/cli.py index 0976e27..891b9d1 100644 --- a/pythonlings/cli.py +++ b/pythonlings/cli.py @@ -5,7 +5,7 @@ import sys from pathlib import Path -__version__ = "0.2.0" +__version__ = "0.3.0" def _build_parser() -> argparse.ArgumentParser: