From 55a4ac71e10ad79f35f20455869d51a2d7e9e8eb Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Wed, 10 Jun 2026 20:43:19 +0530 Subject: [PATCH 1/9] 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/9] 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: From 74d19bf56c69eab1f0a2f114ae201062b20ed25a Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Wed, 10 Jun 2026 22:02:36 +0530 Subject: [PATCH 3/9] docs: fix RELEASE.md after the pythonlings rename Correct the package-name section (pythonlings is this project's own PyPI name; pylings is the unrelated one), fix the wheel filename in the verification snippet, refresh the version example to 0.3.0, and document the trusted-publisher prerequisite for publishing under a new name. --- RELEASE.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 6c0e440..8ab7f36 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,10 +5,10 @@ package metadata and annotated git tags such as `v0.1.0`. ## Package Name -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. +The distribution name on PyPI is `pythonlings`; installing it provides the +`pythonlings` console command. Releases before 0.3.0 were published as +`python-learnings`. The PyPI name `pylings` belongs to an unrelated project — +never publish or document it for this repository. ## Pre-Release Verification @@ -18,7 +18,7 @@ Run these checks from a clean working tree before tagging: python -m pytest -q pythonlings --root tests/fixtures/passing_curriculum verify python -m build -python -m pip install --force-reinstall dist/python_learnings-*.whl +python -m pip install --force-reinstall dist/pythonlings-*.whl pythonlings --version tmp=$(mktemp -d /tmp/pythonlings-release.XXXXXX) pythonlings init --path "$tmp" @@ -27,14 +27,18 @@ pythonlings --root "$tmp" solution variables1 pythonlings --root "$tmp" reset variables1 --yes ``` -Expected release version for `v0.1.0`: +Expected release version for `v0.3.0`: ```text -pythonlings 0.1.0 +pythonlings 0.3.0 ``` ## Tag And Publish +0. One-time setup: the PyPI project `pythonlings` must have a trusted publisher + configured (repository `abhiksark/pythonlings`, workflow `publish.yml`, + environment `pypi`). For a first release under a new name, add it as a + pending publisher on pypi.org before tagging. 1. Commit the release changes. 2. Create an annotated tag, for example `git tag -a v0.1.0 -m "Release v0.1.0"`. 3. Push the branch and tag. From fa41821ae060cf532dba4a86f4e8dcd90d3cac27 Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 20:33:00 +0530 Subject: [PATCH 4/9] refactor: remove unused file-watching scaffolding watchdog was a declared dependency but never imported; core/watcher.py and its test had no callers; the --watch-files flag was plumbed through to PythonlingsApp but never read. The save->rerun loop runs on a debounce in the TUI editor, so none of this was wired to anything. Drop the dep, the dead module, and the no-op flag. Also remove the unused load_topic_snippet (load_snippet covers it). --- CLAUDE.md | 2 +- pyproject.toml | 1 - pythonlings/app.py | 4 ---- pythonlings/cli.py | 6 ------ pythonlings/core/docs.py | 13 ------------- pythonlings/core/watcher.py | 5 ----- tests/unit/test_docs.py | 12 ++++++------ tests/unit/test_watcher.py | 13 ------------- 8 files changed, 7 insertions(+), 49 deletions(-) delete mode 100644 pythonlings/core/watcher.py delete mode 100644 tests/unit/test_watcher.py diff --git a/CLAUDE.md b/CLAUDE.md index 5e59a24..dbf8214 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ An exercise passes when a generated runner script `exec()`s the exercise source ### Layering -- `pylings/core/` — all filesystem, manifest, state, reset, solutions, runner, and watcher logic. No UI imports. +- `pythonlings/core/` — all filesystem, manifest, state, reset, solutions, and runner logic. No UI imports. (Checks rerun on a debounce in the TUI editor, not a filesystem watcher.) - `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"`. diff --git a/pyproject.toml b/pyproject.toml index 8b11857..1ff9083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ classifiers = [ ] dependencies = [ "textual[syntax]>=8.0.0", - "watchdog>=6.0.0", "tomli>=1.1.0; python_version < '3.11'", ] diff --git a/pythonlings/app.py b/pythonlings/app.py index b06f2a9..8023d0b 100644 --- a/pythonlings/app.py +++ b/pythonlings/app.py @@ -19,7 +19,6 @@ def __init__( root: Path, start_topic: str | None = None, force_picker: bool = False, - watch_files: bool = False, ) -> None: super().__init__() self.root = root @@ -27,7 +26,6 @@ def __init__( self.state: State = load_state(root) self._start_topic = start_topic self._force_picker = force_picker - self.watch_files = watch_files def on_mount(self) -> None: self.push_screen(TopicPickerScreen()) @@ -75,14 +73,12 @@ def run_tui( root: Path, start_topic: str | None = None, force_picker: bool = False, - watch_files: bool = False, ) -> int: return ( PythonlingsApp( root, start_topic, force_picker=force_picker, - watch_files=watch_files, ).run() or 0 ) diff --git a/pythonlings/cli.py b/pythonlings/cli.py index 891b9d1..40d9046 100644 --- a/pythonlings/cli.py +++ b/pythonlings/cli.py @@ -14,11 +14,6 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument( "--debug", action="store_true", help="Write debug output to .pythonlings_debug.log." ) - parser.add_argument( - "--watch-files", - action="store_true", - help="Rerun checks when exercise files change outside the TUI.", - ) parser.add_argument( "--root", type=Path, @@ -305,7 +300,6 @@ def main(argv: list[str] | None = None) -> int: args.root, start_topic, force_picker=args.command == "topics", - watch_files=getattr(args, "watch_files", False), ) # Other subcommands wired in later tasks. diff --git a/pythonlings/core/docs.py b/pythonlings/core/docs.py index d447f6f..34deefc 100644 --- a/pythonlings/core/docs.py +++ b/pythonlings/core/docs.py @@ -16,19 +16,6 @@ class DocSnippet: text: str -def load_topic_snippet(topic: str) -> DocSnippet | None: - """Load the bundled local docs snippet for a curriculum topic.""" - index = _load_index() - if index is None: - return None - - entry = index["topics"].get(topic) - if entry is None: - return None - - return _load_entry(topic, entry) - - def load_snippet(topic: str, source_url: str = "") -> DocSnippet | None: """Load a bundled snippet by topic, falling back to an official docs URL.""" index = _load_index() diff --git a/pythonlings/core/watcher.py b/pythonlings/core/watcher.py deleted file mode 100644 index 4ec9099..0000000 --- a/pythonlings/core/watcher.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - - -def changed_python_file(path: str) -> bool: - return path.endswith(".py") and "__pycache__" not in path diff --git a/tests/unit/test_docs.py b/tests/unit/test_docs.py index bb5831e..0ddc000 100644 --- a/tests/unit/test_docs.py +++ b/tests/unit/test_docs.py @@ -1,12 +1,12 @@ from pathlib import Path -from pythonlings.core.docs import load_snippet, load_topic_snippet +from pythonlings.core.docs import load_snippet from pythonlings.core.manifest import load from pythonlings.screens.docs import DocsScreen -def test_load_topic_snippet_returns_bundled_reference() -> None: - snippet = load_topic_snippet("variables") +def test_load_snippet_returns_bundled_reference() -> None: + snippet = load_snippet("variables") assert snippet is not None assert snippet.topic == "variables" assert snippet.title == "Variables" @@ -14,8 +14,8 @@ def test_load_topic_snippet_returns_bundled_reference() -> None: assert "Assigning names" in snippet.text -def test_load_topic_snippet_returns_none_for_unknown_topic() -> None: - assert load_topic_snippet("missing") is None +def test_load_snippet_returns_none_for_unknown_topic() -> None: + assert load_snippet("missing") is None def test_load_snippet_falls_back_to_official_docs_url() -> None: @@ -32,7 +32,7 @@ def test_real_curriculum_topics_have_bundled_references() -> None: repo = Path(__file__).parents[2] manifest = load(repo) missing = [ - topic for topic in manifest.topics() if load_topic_snippet(topic) is None + topic for topic in manifest.topics() if load_snippet(topic) is None ] assert missing == [] diff --git a/tests/unit/test_watcher.py b/tests/unit/test_watcher.py deleted file mode 100644 index a48a1c5..0000000 --- a/tests/unit/test_watcher.py +++ /dev/null @@ -1,13 +0,0 @@ -from pythonlings.core.watcher import changed_python_file - - -def test_changed_python_file_accepts_exercise_file() -> None: - assert changed_python_file("exercises/variables/variables1.py") is True - - -def test_changed_python_file_ignores_bytecode() -> None: - assert changed_python_file("exercises/variables/__pycache__/variables1.pyc") is False - - -def test_changed_python_file_ignores_non_python_files() -> None: - assert changed_python_file("Readme.md") is False From eaa26cbc4cd0c7032f5b16ff10ca03de0a839bbf Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 20:40:00 +0530 Subject: [PATCH 5/9] fix: correct README editing claim and guide users to init The hero said 'edit in your own editor' but the only rerun loop is the built-in editor's debounce; external-editor saves never reran. The no-workspace error said 'info.toml not found' with no hint to run init. --- Readme.md | 4 ++-- pythonlings/core/manifest.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index c328504..cdbcfca 100644 --- a/Readme.md +++ b/Readme.md @@ -24,8 +24,8 @@ 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. +How it works: **edit** the broken exercise in the built-in editor → checks +rerun as you type and advance you to the next one. That's the whole loop. Status: `v0.3.0`, alpha — published on PyPI as `pythonlings`. diff --git a/pythonlings/core/manifest.py b/pythonlings/core/manifest.py index d1b9f3a..ca6b188 100644 --- a/pythonlings/core/manifest.py +++ b/pythonlings/core/manifest.py @@ -49,7 +49,10 @@ def exercises_in(self, topic: str) -> list[Exercise]: def load(root: Path) -> Manifest: info_path = root / "info.toml" if not info_path.exists(): - raise ManifestError(f"info.toml not found at {info_path}") + raise ManifestError( + f"no pythonlings workspace here ({info_path} not found). " + "Run 'pythonlings init' to create one." + ) with info_path.open("rb") as f: data = tomllib.load(f) From 59aecf862f8ab9e47e6065964f672b980e25bb07 Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 20:50:12 +0530 Subject: [PATCH 6/9] feat: enrich bundled docs with a second reference section per topic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each topic's F5 reference now pulls a complementary docs.python.org section in addition to the primary one, and the per-section line cap goes 26->40. Topics grow from ~30 to ~94 lines on average. The fetcher's section parser now triggers on any element bearing the target id, not just
: a
object/method anchor captures its enclosing
so the signature and description come together. Extra sections are best-effort — a failed fetch is skipped, not fatal. --- pythonlings/docs/topics/async.md | 65 +++++++++- pythonlings/docs/topics/classes.md | 61 +++++++++ pythonlings/docs/topics/collections.md | 64 +++++++++- pythonlings/docs/topics/comprehensions.md | 62 +++++++++ pythonlings/docs/topics/conditionals.md | 52 ++++++++ pythonlings/docs/topics/context_managers.md | 62 +++++++++ pythonlings/docs/topics/dataclasses.md | 65 +++++++++- pythonlings/docs/topics/datetime.md | 65 +++++++++- pythonlings/docs/topics/decorators.md | 56 +++++++++ pythonlings/docs/topics/dictionaries.md | 60 +++++++++ pythonlings/docs/topics/enums.md | 64 +++++++++- pythonlings/docs/topics/exceptions.md | 61 +++++++++ pythonlings/docs/topics/file_io.md | 60 +++++++++ pythonlings/docs/topics/functional.md | 64 +++++++++- pythonlings/docs/topics/functions.md | 61 +++++++++ pythonlings/docs/topics/generators.md | 61 +++++++++ pythonlings/docs/topics/itertools.md | 64 +++++++++- pythonlings/docs/topics/json.md | 65 +++++++++- pythonlings/docs/topics/lists.md | 61 +++++++++ pythonlings/docs/topics/loops.md | 59 +++++++++ pythonlings/docs/topics/modules.md | 62 +++++++++ pythonlings/docs/topics/oop_advanced.md | 39 +++++- pythonlings/docs/topics/pathlib.md | 47 ++++++- pythonlings/docs/topics/recursion.md | 37 ++++++ pythonlings/docs/topics/regex.md | 64 +++++++++- pythonlings/docs/topics/sets.md | 62 +++++++++ pythonlings/docs/topics/strings.md | 61 +++++++++ pythonlings/docs/topics/testing.md | 65 +++++++++- pythonlings/docs/topics/tuples.md | 61 +++++++++ pythonlings/docs/topics/type_hints.md | 65 +++++++++- pythonlings/docs/topics/variables.md | 61 +++++++++ scripts/fetch_python_docs.py | 132 +++++++++++++------- 32 files changed, 1914 insertions(+), 74 deletions(-) diff --git a/pythonlings/docs/topics/async.md b/pythonlings/docs/topics/async.md index b854dd8..0385355 100644 --- a/pythonlings/docs/topics/async.md +++ b/pythonlings/docs/topics/async.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -asyncio — Asynchronous I/O — Python 3.14.5 documentation +asyncio — Asynchronous I/O — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Networking and Interprocess Communication » + +- `asyncio` — Asynchronous I/O + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/asyncio-task.html#coroutines + +Coroutines and `async`/`await` with execution examples. + +Coroutines + +Source code: Lib/asyncio/coroutines.py + +Coroutines declared with the async/await syntax is the +preferred way of writing asyncio applications. For example, the following +snippet of code prints “hello”, waits 1 second, +and then prints “world”: + +```python +>>> import asyncio + +>>> async def main(): +... print('hello') +... await asyncio.sleep(1) +... print('world') + +>>> asyncio.run(main()) +hello +world + +``` + +Note that simply calling a coroutine will not schedule it to +be executed: + +```python +>>> main() + + +``` + +To actually run a coroutine, asyncio provides the following mechanisms: + +- + +The `asyncio.run()` function to run the top-level +entry point “main()” function (see the above example.) + +- diff --git a/pythonlings/docs/topics/classes.md b/pythonlings/docs/topics/classes.md index 7aee045..eba7982 100644 --- a/pythonlings/docs/topics/classes.md +++ b/pythonlings/docs/topics/classes.md @@ -34,3 +34,64 @@ definition in a branch of an `if` statement, or inside a function.) In practice, the statements inside a class definition will usually be function definitions, but other statements are allowed, and sometimes useful — we’ll come back to this later. The function definitions inside a class normally have +a peculiar form of argument list, dictated by the calling conventions for +methods — again, this is explained later. + +When a class definition is entered, a new namespace is created, and used as the +local scope — thus, all assignments to local variables go into this new +namespace. In particular, function definitions bind the name of the new +function here. + +When a class definition is left normally (via the end), a class object is +created. This is basically a wrapper around the contents of the namespace +created by the class definition; we’ll learn more about class objects in the +next section. The original local scope (the one in effect just before the class +definition was entered) is reinstated, and the class object is bound here to the +class name given in the class definition header (`ClassName` in the + +## More reference + +Source: https://docs.python.org/3/reference/datamodel.html#special-method-names + +Special methods like `__init__`, `__str__`, and `__repr__`. + +3.3. Special method names + +A class can implement certain operations that are invoked by special syntax +(such as arithmetic operations or subscripting and slicing) by defining methods +with special names. This is Python’s approach to operator overloading, +allowing classes to define their own behavior with respect to language +operators. For instance, if a class defines a method named +`__getitem__()`, +and `x` is an instance of this class, then `x[i]` is roughly equivalent +to `type(x).__getitem__(x, i)`. Except where mentioned, attempts to execute an +operation raise an exception when no appropriate method is defined (typically +`AttributeError` or `TypeError`). + +Setting a special method to `None` indicates that the corresponding +operation is not available. For example, if a class sets +`__iter__()` to `None`, the class is not iterable, so calling +`iter()` on its instances will raise a `TypeError` (without +falling back to `__getitem__()`). [2] + +When implementing a class that emulates any built-in type, it is important that +the emulation only be implemented to the degree that it makes sense for the +object being modelled. For example, some sequences may work well with retrieval +of individual elements, but extracting a slice may not make sense. +(One example of this is the NodeList interface +in the W3C’s Document Object Model.) + +3.3.1. Basic customization + +object.__new__(cls[, ...]) + +Called to create a new instance of class cls. `__new__()` is a static +method (special-cased so you need not declare it as such) that takes the class +of which an instance was requested as its first argument. The remaining +arguments are those passed to the object constructor expression (the call to the +class). The return value of `__new__()` should be the new object instance +(usually an instance of cls). + +Typical implementations create a new instance of the class by invoking the +superclass’s `__new__()` method using `super().__new__(cls[, ...])` +with appropriate arguments and then modifying the newly created instance diff --git a/pythonlings/docs/topics/collections.md b/pythonlings/docs/topics/collections.md index fd7bf6e..dfc0865 100644 --- a/pythonlings/docs/topics/collections.md +++ b/pythonlings/docs/topics/collections.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -collections — Container datatypes — Python 3.14.5 documentation +collections — Container datatypes — Python 3.14.6 documentation Navigation @@ -33,4 +33,64 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Data Types » + +- `collections` — Container datatypes + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/collections.html#collections.Counter + +`Counter` tallies hashable objects and finds the most common ones. + +class collections.Counter(**kwargs) + +class collections.Counter(iterable, /, **kwargs) + +class collections.Counter(mapping, /, **kwargs) + +A `Counter` is a `dict` subclass for counting hashable objects. +It is a collection where elements are stored as dictionary keys +and their counts are stored as dictionary values. Counts are allowed to be +any integer value including zero or negative counts. The `Counter` +class is similar to bags or multisets in other languages. + +Elements are counted from an iterable or initialized from another +mapping (or counter): + +```python +>>> c = Counter() # a new, empty counter +>>> c = Counter('gallahad') # a new counter from an iterable +>>> c = Counter({'red': 4, 'blue': 2}) # a new counter from a mapping +>>> c = Counter(cats=4, dogs=8) # a new counter from keyword args + +``` + +Counter objects have a dictionary interface except that they return a zero +count for missing items instead of raising a `KeyError`: + +```python +>>> c = Counter(['eggs', 'ham']) +>>> c['bacon'] # count of a missing element is zero +0 + +``` + +Setting a count to zero does not remove an element from a counter. +Use `del` to remove it entirely: + +```python +>>> c['sausage'] = 0 # counter entry with a zero count +>>> del c['sausage'] # del actually removes the entry diff --git a/pythonlings/docs/topics/comprehensions.md b/pythonlings/docs/topics/comprehensions.md index d60da1e..d6b9715 100644 --- a/pythonlings/docs/topics/comprehensions.md +++ b/pythonlings/docs/topics/comprehensions.md @@ -33,3 +33,65 @@ side effects using: ```python squares = list(map(lambda x: x**2, range(10))) + +``` + +or, equivalently: + +```python +squares = [x**2 for x in range(10)] + +``` + +which is more concise and readable. + +A list comprehension consists of brackets containing an expression followed +by a `for` clause, then zero or more `for` or `if` +clauses. The result will be a new list resulting from evaluating the expression + +## More reference + +Source: https://docs.python.org/3/tutorial/datastructures.html#nested-list-comprehensions + +Nested comprehensions that build lists of lists from multiple loops. + +5.1.4. Nested List Comprehensions + +The initial expression in a list comprehension can be any arbitrary expression, +including another list comprehension. + +Consider the following example of a 3x4 matrix implemented as a list of +3 lists of length 4: + +```python +>>> matrix = [ +... [1, 2, 3, 4], +... [5, 6, 7, 8], +... [9, 10, 11, 12], +... ] + +``` + +The following list comprehension will transpose rows and columns: + +```python +>>> [[row[i] for row in matrix] for i in range(4)] +[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] + +``` + +As we saw in the previous section, the inner list comprehension is evaluated in +the context of the `for` that follows it, so this example is +equivalent to: + +```python +>>> transposed = [] +>>> for i in range(4): +... transposed.append([row[i] for row in matrix]) +... +>>> transposed +[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] + +``` + +which, in turn, is the same as: diff --git a/pythonlings/docs/topics/conditionals.md b/pythonlings/docs/topics/conditionals.md index c293c67..1f3aee6 100644 --- a/pythonlings/docs/topics/conditionals.md +++ b/pythonlings/docs/topics/conditionals.md @@ -34,3 +34,55 @@ There can be zero or more `elif` parts, and the `else` part is optional. The keyword ‘`elif`’ is short for ‘else if’, and is useful to avoid excessive indentation. An `if` … `elif` … `elif` … sequence is a substitute for the `switch` or +`case` statements found in other languages. + +If you’re comparing the same value to several constants, or checking for specific types or +attributes, you may also find the `match` statement useful. For more +details see match Statements. + +## More reference + +Source: https://docs.python.org/3/tutorial/controlflow.html#match-statements + +`match` pattern matching with examples and syntax. + +4.7. `match` Statements + +A `match` statement takes an expression and compares its value to successive +patterns given as one or more case blocks. This is superficially +similar to a switch statement in C, Java or JavaScript (and many +other languages), but it’s more similar to pattern matching in +languages like Rust or Haskell. Only the first pattern that matches +gets executed and it can also extract components (sequence elements +or object attributes) from the value into variables. If no case matches, +none of the branches is executed. + +The simplest form compares a subject value against one or more literals: + +```python +def http_error(status): +match status: +case 400: +return "Bad request" +case 404: +return "Not found" +case 418: +return "I'm a teapot" +case _: +return "Something's wrong with the internet" + +``` + +Note the last block: the “variable name” `_` acts as a wildcard and +never fails to match. + +You can combine several literals in a single pattern using `|` (“or”): + +```python +case 401 | 403 | 404: +return "Not allowed" + +``` + +Patterns can look like unpacking assignments, and can be used to bind +variables: diff --git a/pythonlings/docs/topics/context_managers.md b/pythonlings/docs/topics/context_managers.md index 7ccfeb6..28c3545 100644 --- a/pythonlings/docs/topics/context_managers.md +++ b/pythonlings/docs/topics/context_managers.md @@ -33,3 +33,65 @@ The context expression (the expression given in the - The context manager’s `__enter__()` is loaded for later use. + +- + +The context manager’s `__exit__()` is loaded for later use. + +- + +The context manager’s `__enter__()` method is invoked. + +- + +If a target was included in the `with` statement, the return value +from `__enter__()` is assigned to it. + +Note + +## More reference + +Source: https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager + +`contextlib.contextmanager` builds a context manager from a generator. + +@contextlib.contextmanager + +This function is a decorator that can be used to define a factory +function for `with` statement context managers, without needing to +create a class or separate `__enter__()` and `__exit__()` methods. + +While many objects natively support use in with statements, sometimes a +resource needs to be managed that isn’t a context manager in its own right, +and doesn’t implement a `close()` method for use with `contextlib.closing`. + +An abstract example would be the following to ensure correct resource +management: + +```python +from contextlib import contextmanager + +@contextmanager +def managed_resource(*args, **kwds): +# Code to acquire resource, e.g.: +resource = acquire_resource(*args, **kwds) +try: +yield resource +finally: +# Code to release resource, e.g.: +release_resource(resource) + +``` + +The function can then be used like this: + +```python +>>> with managed_resource(timeout=3600) as resource: +... # Resource is released at the end of this block, +... # even if code in the block raises an exception + +``` + +The function being decorated must return a generator-iterator when +called. This iterator must yield exactly one value, which will be bound to +the targets in the `with` statement’s `as` clause, if any. diff --git a/pythonlings/docs/topics/dataclasses.md b/pythonlings/docs/topics/dataclasses.md index fb27c8e..cbdf985 100644 --- a/pythonlings/docs/topics/dataclasses.md +++ b/pythonlings/docs/topics/dataclasses.md @@ -8,7 +8,7 @@ Dataclasses generate common class methods for data-focused classes. ## Extracted reference -dataclasses — Data Classes — Python 3.14.5 documentation +dataclasses — Data Classes — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Python Runtime Services » + +- `dataclasses` — Data Classes + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables + +Class vs instance variables — the distinction behind dataclass fields. + +9.3.5. Class and Instance Variables + +Generally speaking, instance variables are for data unique to each instance +and class variables are for attributes and methods shared by all instances +of the class: + +```python +class Dog: + +kind = 'canine' # class variable shared by all instances + +def __init__(self, name): +self.name = name # instance variable unique to each instance + +>>> d = Dog('Fido') +>>> e = Dog('Buddy') +>>> d.kind # shared by all dogs +'canine' +>>> e.kind # shared by all dogs +'canine' +>>> d.name # unique to d +'Fido' +>>> e.name # unique to e +'Buddy' + +``` + +As discussed in A Word About Names and Objects, shared data can have possibly surprising +effects involving mutable objects such as lists and dictionaries. +For example, the tricks list in the following code should not be used as a +class variable because just a single list would be shared by all Dog +instances: + +```python +class Dog: + +tricks = [] # mistaken use of a class variable + +def __init__(self, name): +self.name = name diff --git a/pythonlings/docs/topics/datetime.md b/pythonlings/docs/topics/datetime.md index db0afe5..5cf8252 100644 --- a/pythonlings/docs/topics/datetime.md +++ b/pythonlings/docs/topics/datetime.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -datetime — Basic date and time types — Python 3.14.5 documentation +datetime — Basic date and time types — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Data Types » + +- `datetime` — Basic date and time types + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/datetime.html#examples-of-usage-datetime + +Worked `datetime` examples beyond the API reference. + +Examples of usage: `datetime` + +Examples of working with `datetime` objects: + +```python +>>> import datetime as dt + +>>> # Using datetime.combine() +>>> d = dt.date(2005, 7, 14) +>>> t = dt.time(12, 30) +>>> dt.datetime.combine(d, t) +datetime.datetime(2005, 7, 14, 12, 30) + +>>> # Using datetime.now() +>>> dt.datetime.now() +datetime.datetime(2007, 12, 6, 16, 29, 43, 79043) # GMT +1 +>>> dt.datetime.now(dt.timezone.utc) +datetime.datetime(2007, 12, 6, 15, 29, 43, 79060, tzinfo=datetime.timezone.utc) + +>>> # Using datetime.strptime() +>>> my_datetime = dt.datetime.strptime("21/11/06 16:30", "%d/%m/%y %H:%M") +>>> my_datetime +datetime.datetime(2006, 11, 21, 16, 30) + +>>> # Using datetime.timetuple() to get tuple of all attributes +>>> tt = my_datetime.timetuple() +>>> for it in tt: +... print(it) +... +2006 # year +11 # month +21 # day +16 # hour +30 # minute +0 # second +1 # weekday (0 = Monday) +325 # number of days since 1st January +-1 # dst - method tzinfo.dst() returned None + +>>> # Date in ISO format diff --git a/pythonlings/docs/topics/decorators.md b/pythonlings/docs/topics/decorators.md index 6c85fb4..31001d9 100644 --- a/pythonlings/docs/topics/decorators.md +++ b/pythonlings/docs/topics/decorators.md @@ -33,3 +33,59 @@ defparameter: parameter ["=" expression] funcname: identifier ``` + +A function definition is an executable statement. Its execution binds the +function name in the current local namespace to a function object (a wrapper +around the executable code for the function). This function object contains a +reference to the current global namespace as the global namespace to be used +when the function is called. + +The function definition does not execute the function body; this gets executed +only when the function is called. [4] + +A function definition may be wrapped by one or more decorator expressions. +Decorator expressions are evaluated when the function is defined, in the scope +that contains the function definition. The result must be a callable, which is +invoked with the function object as the only argument. The returned value is +bound to the function name instead of the function object. Multiple decorators + +## More reference + +Source: https://docs.python.org/3/library/functools.html#functools.wraps + +`functools.wraps` preserves the wrapped function's metadata. + +@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) + +This is a convenience function for invoking `update_wrapper()` as a +function decorator when defining a wrapper function. It is equivalent to +`partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)`. +For example: + +```python +>>> from functools import wraps +>>> def my_decorator(f): +... @wraps(f) +... def wrapper(*args, **kwds): +... print('Calling decorated function') +... return f(*args, **kwds) +... return wrapper +... +>>> @my_decorator +... def example(): +... """Docstring""" +... print('Called example function') +... +>>> example() +Calling decorated function +Called example function +>>> example.__name__ +'example' +>>> example.__doc__ +'Docstring' + +``` + +Without the use of this decorator factory, the name of the example function +would have been `'wrapper'`, and the docstring of the original `example()` +would have been lost. diff --git a/pythonlings/docs/topics/dictionaries.md b/pythonlings/docs/topics/dictionaries.md index 9e66a42..b68328a 100644 --- a/pythonlings/docs/topics/dictionaries.md +++ b/pythonlings/docs/topics/dictionaries.md @@ -34,3 +34,63 @@ value associated with that key is forgotten. Extracting a value for a non-existent key by subscripting (`d[key]`) raises a `KeyError`. To avoid getting this error when trying to access a possibly +non-existent key, use the `get()` method instead, which returns +`None` (or a specified default value) if the key is not in the dictionary. + +Performing `list(d)` on a dictionary returns a list of all the keys +used in the dictionary, in insertion order (if you want it sorted, just use +`sorted(d)` instead). To check whether a single key is in the +dictionary, use the `in` keyword. + +Here is a small example using a dictionary: + +```python +>>> tel = {'jack': 4098, 'sape': 4139} +>>> tel['guido'] = 4127 +>>> tel + +## More reference + +Source: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict + +The dict type with constructor forms, methods, and creation examples. + +Mapping Types — `dict` + +A mapping object maps hashable values to arbitrary objects. +Mappings are mutable objects. There is currently only one standard mapping +type, the dictionary. (For other containers see the built-in +`list`, `set`, and `tuple` classes, and the +`collections` module.) + +A dictionary’s keys are almost arbitrary values. Values that are not +hashable, that is, values containing lists, dictionaries or other +mutable types (that are compared by value rather than by object identity) may +not be used as keys. +Values that compare equal (such as `1`, `1.0`, and `True`) +can be used interchangeably to index the same dictionary entry. + +class dict(**kwargs) + +class dict(mapping, /, **kwargs) + +class dict(iterable, /, **kwargs) + +Return a new dictionary initialized from an optional positional argument +and a possibly empty set of keyword arguments. + +Dictionaries can be created by several means: + +- + +Use a comma-separated list of `key: value` pairs within braces: +`{'jack': 4098, 'sjoerd': 4127}` or `{4098: 'jack', 4127: 'sjoerd'}` + +- + +Use a dict comprehension: `{}`, `{x: x ** 2 for x in range(10)}` + +- + +Use the type constructor: `dict()`, +`dict([('foo', 100), ('bar', 200)])`, `dict(foo=100, bar=200)` diff --git a/pythonlings/docs/topics/enums.md b/pythonlings/docs/topics/enums.md index 41308c6..8aef606 100644 --- a/pythonlings/docs/topics/enums.md +++ b/pythonlings/docs/topics/enums.md @@ -8,7 +8,7 @@ Enums define named constant values that are easier to read than raw literals. ## Extracted reference -enum — Support for enumerations — Python 3.14.5 documentation +enum — Support for enumerations — Python 3.14.6 documentation Navigation @@ -33,4 +33,64 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Data Types » + +- `enum` — Support for enumerations + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/enum.html#enum.Flag + +`Flag` enums combine members with bitwise operators. + +class enum.Flag + +`Flag` is the same as `Enum`, but its members support the bitwise +operators `&` (AND), `|` (OR), `^` (XOR), and `~` (INVERT); +the results of those operations are (aliases of) members of the enumeration. + +__contains__(self, value) + +Returns True if value is in self: + +```python +>>> from enum import Flag, auto +>>> class Color(Flag): +... RED = auto() +... GREEN = auto() +... BLUE = auto() +... +>>> purple = Color.RED | Color.BLUE +>>> white = Color.RED | Color.GREEN | Color.BLUE +>>> Color.GREEN in purple +False +>>> Color.GREEN in white +True +>>> purple in white +True +>>> white in purple +False + +``` + +__iter__(self) + +Returns all contained non-alias members: + +```python +>>> list(Color.RED) +[] +>>> list(purple) +[, ] diff --git a/pythonlings/docs/topics/exceptions.md b/pythonlings/docs/topics/exceptions.md index f2be000..119f3b7 100644 --- a/pythonlings/docs/topics/exceptions.md +++ b/pythonlings/docs/topics/exceptions.md @@ -33,3 +33,64 @@ The `try` statement works as follows. First, the try clause (the statement(s) between the `try` and `except` keywords) is executed. + +- + +If no exception occurs, the except clause is skipped and execution of the +`try` statement is finished. + +- + +If an exception occurs during execution of the `try` clause, the rest of the +clause is skipped. Then, if its type matches the exception named after the +`except` keyword, the except clause is executed, and then execution +continues after the try/except block. + +- + +## More reference + +Source: https://docs.python.org/3/library/exceptions.html#concrete-exceptions + +Concrete built-in exceptions like ValueError, KeyError, and TypeError. + +Concrete exceptions + +The following exceptions are the exceptions that are usually raised. + +exception AssertionError + +Raised when an `assert` statement fails. + +exception AttributeError + +Raised when an attribute reference (see Attribute references) or +assignment fails. (When an object does not support attribute references or +attribute assignments at all, `TypeError` is raised.) + +The optional name and obj keyword-only arguments +set the corresponding attributes: + +name + +The name of the attribute that was attempted to be accessed. + +obj + +The object that was accessed for the named attribute. + +Changed in version 3.10: Added the `name` and `obj` attributes. + +exception EOFError + +Raised when the `input()` function hits an end-of-file condition (EOF) +without reading any data. (Note: the `io.TextIOBase.read()` and +`io.IOBase.readline()` methods return an empty string when they hit EOF.) + +exception FloatingPointError + +Not currently used. + +exception GeneratorExit + +Raised when a generator or coroutine is closed; diff --git a/pythonlings/docs/topics/file_io.md b/pythonlings/docs/topics/file_io.md index 5c20b6d..6758138 100644 --- a/pythonlings/docs/topics/file_io.md +++ b/pythonlings/docs/topics/file_io.md @@ -34,3 +34,63 @@ If encoding is not specified, the default is platform dependent (see `open()`). Because UTF-8 is the modern de-facto standard, `encoding="utf-8"` is recommended unless you know that you need to use a different encoding. +Appending a `'b'` to the mode opens the file in binary mode. +Binary mode data is read and written as `bytes` objects. +You can not specify encoding when opening file in binary mode. + +In text mode, the default when reading is to convert platform-specific line +endings (`\n` on Unix, `\r\n` on Windows) to just `\n`. When writing in +text mode, the default is to convert occurrences of `\n` back to +platform-specific line endings. This behind-the-scenes modification +to file data is fine for text files, but will corrupt binary data like that in +`JPEG` or `EXE` files. Be very careful to use binary mode when +reading and writing such files. + +It is good practice to use the `with` keyword when dealing +with file objects. The advantage is that the file is properly closed + +## More reference + +Source: https://docs.python.org/3/library/io.html#high-level-module-interface + +The high-level `open()` interface and its modes. + +High-level Module Interface + +io.DEFAULT_BUFFER_SIZE + +An int containing the default buffer size used by the module’s buffered I/O +classes. `open()` uses the file’s blksize (as obtained by +`os.stat()`) if possible. + +io.open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) + +This is an alias for the builtin `open()` function. + +This function raises an auditing event `open` with +arguments path, mode and flags. The mode and flags +arguments may have been modified or inferred from the original call. + +io.open_code(path) + +Opens the provided file with mode `'rb'`. This function should be used +when the intent is to treat the contents as executable code. + +path should be a `str` and an absolute path. + +The behavior of this function may be overridden by an earlier call to the +`PyFile_SetOpenCodeHook()`. However, assuming that path is a +`str` and an absolute path, `open_code(path)` should always behave +the same as `open(path, 'rb')`. Overriding the behavior is intended for +additional validation or preprocessing of the file. + +Added in version 3.8. + +io.text_encoding(encoding, stacklevel=2, /) + +This is a helper function for callables that use `open()` or +`TextIOWrapper` and have an `encoding=None` parameter. + +This function returns encoding if it is not `None`. +Otherwise, it returns `"locale"` or `"utf-8"` depending on +UTF-8 Mode. diff --git a/pythonlings/docs/topics/functional.md b/pythonlings/docs/topics/functional.md index 16a0f67..5eef258 100644 --- a/pythonlings/docs/topics/functional.md +++ b/pythonlings/docs/topics/functional.md @@ -8,7 +8,7 @@ Functional style uses functions, iterators, and transformations as building bloc ## Extracted reference -Functional Programming HOWTO — Python 3.14.5 documentation +Functional Programming HOWTO — Python 3.14.6 documentation Navigation @@ -33,4 +33,64 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- Python HOWTOs » + +- Functional Programming HOWTO + +- + +| + +- + +Theme + +Auto + +## More reference + +Source: https://docs.python.org/3/library/itertools.html#itertool-functions + +itertools building blocks with signatures and usage patterns. + +Itertool Functions + +The following functions all construct and return iterators. Some provide +streams of infinite length, so they should only be accessed by functions or +loops that truncate the stream. + +itertools.accumulate(iterable[, function, *, initial=None]) + +Make an iterator that returns accumulated sums or accumulated +results from other binary functions. + +The function defaults to addition. The function should accept +two arguments, an accumulated total and a value from the iterable. + +If an initial value is provided, the accumulation will start with +that value and the output will have one more element than the input +iterable. + +Roughly equivalent to: + +```python +def accumulate(iterable, function=operator.add, *, initial=None): +'Return running totals' +# accumulate([1,2,3,4,5]) → 1 3 6 10 15 +# accumulate([1,2,3,4,5], initial=100) → 100 101 103 106 110 115 +# accumulate([1,2,3,4,5], operator.mul) → 1 2 6 24 120 + +iterator = iter(iterable) +total = initial +if initial is None: +try: +total = next(iterator) +except StopIteration: +return + +yield total +for element in iterator: +total = function(total, element) +yield total diff --git a/pythonlings/docs/topics/functions.md b/pythonlings/docs/topics/functions.md index 47c161a..134d9f0 100644 --- a/pythonlings/docs/topics/functions.md +++ b/pythonlings/docs/topics/functions.md @@ -34,3 +34,64 @@ The statements that form the body of the function start at the next line, and must be indented. The first statement of the function body can optionally be a string literal; +this string literal is the function’s documentation string, or docstring. +(More about docstrings can be found in the section Documentation Strings.) +There are tools which use docstrings to automatically produce online or printed +documentation, or to let the user interactively browse through code; it’s good +practice to include docstrings in code that you write, so make a habit of it. + +The execution of a function introduces a new symbol table used for the local +variables of the function. More precisely, all variable assignments in a +function store the value in the local symbol table; whereas variable references +first look in the local symbol table, then in the local symbol tables of +enclosing functions, then in the global symbol table, and finally in the table +of built-in names. Thus, global variables and variables of enclosing functions +cannot be directly assigned a value within a function (unless, for global +variables, named in a `global` statement, or, for variables of enclosing + +## More reference + +Source: https://docs.python.org/3/reference/compound_stmts.html#function-definitions + +Function definition syntax including parameters, annotations, and decorators. + +8.7. Function definitions + +A function definition defines a user-defined function object (see section +The standard type hierarchy): + +```python + +funcdef: [decorators] "def" funcname [type_params] "(" [parameter_list] ")" +["->" expression] ":" suite +decorators: decorator+ +decorator: "@" assignment_expression NEWLINE +parameter_list: defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]] +| parameter_list_no_posonly +parameter_list_no_posonly: defparameter ("," defparameter)* ["," [parameter_list_starargs]] +| parameter_list_starargs +parameter_list_starargs: "*" [star_parameter] ("," defparameter)* ["," [parameter_star_kwargs]] +| "*" ("," defparameter)+ ["," [parameter_star_kwargs]] +| parameter_star_kwargs +parameter_star_kwargs: "**" parameter [","] +parameter: identifier [":" expression] +star_parameter: identifier [":" ["*"] expression] +defparameter: parameter ["=" expression] +funcname: identifier + +``` + +A function definition is an executable statement. Its execution binds the +function name in the current local namespace to a function object (a wrapper +around the executable code for the function). This function object contains a +reference to the current global namespace as the global namespace to be used +when the function is called. + +The function definition does not execute the function body; this gets executed +only when the function is called. [4] + +A function definition may be wrapped by one or more decorator expressions. +Decorator expressions are evaluated when the function is defined, in the scope +that contains the function definition. The result must be a callable, which is +invoked with the function object as the only argument. The returned value is +bound to the function name instead of the function object. Multiple decorators diff --git a/pythonlings/docs/topics/generators.md b/pythonlings/docs/topics/generators.md index 8b1995f..8f91d39 100644 --- a/pythonlings/docs/topics/generators.md +++ b/pythonlings/docs/topics/generators.md @@ -34,3 +34,64 @@ o g ``` + +Anything that can be done with generators can also be done with class-based +iterators as described in the previous section. What makes generators so +compact is that the `__iter__()` and `__next__()` methods +are created automatically. + +Another key feature is that the local variables and execution state are +automatically saved between calls. This made the function easier to write and +much more clear than an approach using instance variables like `self.index` +and `self.data`. + +In addition to automatic method creation and saving program state, when +generators terminate, they automatically raise `StopIteration`. In +combination, these features make it easy to create iterators with no more effort + +## More reference + +Source: https://docs.python.org/3/reference/expressions.html#generator-expressions + +Generator expression syntax for compact lazy iterators. + +6.2.9. Generator expressions + +The syntax for generator expressions is the same as for +list comprehensions, except that they are enclosed in +parentheses instead of brackets. +For example: + +```python +>>> iterator = (x ** 2 for x in range(10)) +>>> iterator + at ...> + +``` + +At runtime, a generator expression evaluates to a generator iterator +which yields the same values as the corresponding list comprehension: + +```python +>>> list(iterator) +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +``` + +Thus, the example above is roughly equivalent to defining and calling +the following generator function: + +```python +def make_generator_of_squares(iterator): +for x in iterator: +yield x ** 2 + +make_generator_of_squares(iter(range(10))) + +``` + +The enclosing parentheses can be omitted in calls when the generator +expression is the only positional argument and there are no keyword +arguments. +See the Calls section for details. +For example: diff --git a/pythonlings/docs/topics/itertools.md b/pythonlings/docs/topics/itertools.md index 4ebaba3..63930a4 100644 --- a/pythonlings/docs/topics/itertools.md +++ b/pythonlings/docs/topics/itertools.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -itertools — Functions creating iterators for efficient looping — Python 3.14.5 documentation +itertools — Functions creating iterators for efficient looping — Python 3.14.6 documentation Navigation @@ -33,4 +33,64 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Functional Programming Modules » + +- `itertools` — Functions creating iterators for efficient looping + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/howto/functional.html#the-itertools-module + +The functional HOWTO's tour of itertools with examples. + +The itertools module + +The `itertools` module contains a number of commonly used iterators as well +as functions for combining several iterators. This section will introduce the +module’s contents by showing small examples. + +The module’s functions fall into a few broad classes: + +- + +Functions that create a new iterator based on an existing iterator. + +- + +Functions for treating an iterator’s elements as function arguments. + +- + +Functions for selecting portions of an iterator’s output. + +- + +A function for grouping an iterator’s output. + +Creating new iterators + +`itertools.count(start, step)` returns an infinite +stream of evenly spaced values. You can optionally supply the starting number, +which defaults to 0, and the interval between numbers, which defaults to 1: + +```python +itertools.count() => +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... +itertools.count(10) => +10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ... +itertools.count(10, 5) => +10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ... + +``` diff --git a/pythonlings/docs/topics/json.md b/pythonlings/docs/topics/json.md index 7077e1b..7e702a3 100644 --- a/pythonlings/docs/topics/json.md +++ b/pythonlings/docs/topics/json.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -json — JSON encoder and decoder — Python 3.14.5 documentation +json — JSON encoder and decoder — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Internet Data Handling » + +- `json` — JSON encoder and decoder + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/tutorial/inputoutput.html#saving-structured-data-with-json + +Tutorial on saving and loading structured data as JSON. + +7.2.2. Saving structured data with `json` + +Strings can easily be written to and read from a file. Numbers take a bit more +effort, since the `read()` method only returns strings, which will have to +be passed to a function like `int()`, which takes a string like `'123'` +and returns its numeric value 123. When you want to save more complex data +types like nested lists and dictionaries, parsing and serializing by hand +becomes complicated. + +Rather than having users constantly writing and debugging code to save +complicated data types to files, Python allows you to use the popular data +interchange format called JSON (JavaScript Object Notation). The standard module called `json` can take Python +data hierarchies, and convert them to string representations; this process is +called serializing. Reconstructing the data from the string representation +is called deserializing. Between serializing and deserializing, the +string representing the object may have been stored in a file or data, or +sent over a network connection to some distant machine. + +Note + +The JSON format is commonly used by modern applications to allow for data +exchange. Many programmers are already familiar with it, which makes +it a good choice for interoperability. + +If you have an object `x`, you can view its JSON string representation with a +simple line of code: + +```python +>>> import json +>>> x = [1, 'simple', 'list'] +>>> json.dumps(x) +'[1, "simple", "list"]' + +``` + +Another variant of the `dumps()` function, called `dump()`, +simply serializes the object to a text file. So if `f` is a +text file object opened for writing, we can do this: + +```python diff --git a/pythonlings/docs/topics/lists.md b/pythonlings/docs/topics/lists.md index bf0e470..63f7996 100644 --- a/pythonlings/docs/topics/lists.md +++ b/pythonlings/docs/topics/lists.md @@ -34,3 +34,64 @@ Remove the first item from the list whose value is equal to value. It raises a `ValueError` if there is no such item. list.pop(index=-1, /) + +Remove the item at the given position in the list, and return it. If no index +is specified, `a.pop()` removes and returns the last item in the list. +It raises an `IndexError` if the list is empty or the index is +outside the list range. + +list.clear() + +Remove all items from the list. Similar to `del a[:]`. + +list.index(value[, start[, stop]]) + +Return zero-based index of the first occurrence of value in the list. +Raises a `ValueError` if there is no such item. + +## More reference + +Source: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range + +Sequence types reference with list methods, operations, and type tables. + +Sequence Types — `list`, `tuple`, `range` + +There are three basic sequence types: lists, tuples, and range objects. +Additional sequence types tailored for processing of +binary data and text strings are +described in dedicated sections. + +Common Sequence Operations + +The operations in the following table are supported by most sequence types, +both mutable and immutable. The `collections.abc.Sequence` ABC is +provided to make it easier to correctly implement these operations on +custom sequence types. + +This table lists the sequence operations sorted in ascending priority. In the +table, s and t are sequences of the same type, n, i, j and k are +integers and x is an arbitrary object that meets any type and value +restrictions imposed by s. + +The `in` and `not in` operations have the same priorities as the +comparison operations. The `+` (concatenation) and `*` (repetition) +operations have the same priority as the corresponding numeric operations. [3] + +Operation + +Result + +Notes + +`x in s` + +`True` if an item of s is +equal to x, else `False` + +(1) + +`x not in s` + +`False` if an item of s is +equal to x, else `True` diff --git a/pythonlings/docs/topics/loops.md b/pythonlings/docs/topics/loops.md index cab96d4..fdc200c 100644 --- a/pythonlings/docs/topics/loops.md +++ b/pythonlings/docs/topics/loops.md @@ -34,3 +34,62 @@ be tricky to get right. Instead, it is usually more straight-forward to loop over a copy of the collection or to create a new collection: ```python +# Create a sample collection +users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'} + +# Strategy: Iterate over a copy +for user, status in users.copy().items(): +if status == 'inactive': +del users[user] + +# Strategy: Create a new collection +active_users = {} +for user, status in users.items(): +if status == 'active': +active_users[user] = status + +## More reference + +Source: https://docs.python.org/3/reference/compound_stmts.html#the-for-statement + +Formal for-statement syntax and iteration semantics. + +8.3. The `for` statement + +The `for` statement is used to iterate over the elements of a sequence +(such as a string, tuple or list) or other iterable object: + +```python + +for_stmt: "for" target_list "in" starred_expression_list ":" suite +["else" ":" suite] + +``` + +The `starred_expression_list` expression is evaluated +once; it should yield an iterable object. An iterator is +created for that iterable. The first item provided by the iterator is then +assigned to the target list using the standard rules for assignments +(see Assignment statements), and the suite is executed. This repeats for each +item provided by the iterator. When the iterator is exhausted, +the suite in the `else` clause, +if present, is executed, and the loop terminates. + +A `break` statement executed in the first suite terminates the loop +without executing the `else` clause’s suite. A `continue` +statement executed in the first suite skips the rest of the suite and continues +with the next item, or with the `else` clause if there is no next +item. + +The for-loop makes assignments to the variables in the target list. +This overwrites all previous assignments to those variables including +those made in the suite of the for-loop: + +```python +for i in range(10): +print(i) +i = 5 # this will not affect the for-loop +# because i will be overwritten with the next +# index in the range + +``` diff --git a/pythonlings/docs/topics/modules.md b/pythonlings/docs/topics/modules.md index 01581bc..5ebc8e8 100644 --- a/pythonlings/docs/topics/modules.md +++ b/pythonlings/docs/topics/modules.md @@ -33,3 +33,65 @@ called `fibo.py` in the current directory with the following contents: ```python # Fibonacci numbers module + +def fib(n): +"""Write Fibonacci series up to n.""" +a, b = 0, 1 +while a < n: +print(a, end=' ') +a, b = b, a+b +print() + +def fib2(n): +"""Return Fibonacci series up to n.""" +result = [] +a, b = 0, 1 +while a < n: +result.append(a) + +## More reference + +Source: https://docs.python.org/3/library/importlib.html#importlib.reload + +Reloading and dynamically importing modules with importlib. + +importlib.reload(module) + +Reload a previously imported module. The argument must be a module object, +so it must have been successfully imported before. This is useful if you +have edited the module source file using an external editor and want to try +out the new version without leaving the Python interpreter. The return value +is the module object (which can be different if re-importing causes a +different object to be placed in `sys.modules`). + +When `reload()` is executed: + +- + +Python module’s code is recompiled and the module-level code re-executed, +defining a new set of objects which are bound to names in the module’s +dictionary by reusing the loader which originally loaded the +module. The `init` function of extension modules is not called a second +time. + +- + +As with all other objects in Python the old objects are only reclaimed +after their reference counts drop to zero. + +- + +The names in the module namespace are updated to point to any new or +changed objects. + +- + +Other references to the old objects (such as names external to the module) are +not rebound to refer to the new objects and must be updated in each namespace +where they occur if that is desired. + +There are a number of other caveats: + +When a module is reloaded, its dictionary (containing the module’s global +variables) is retained. Redefinitions of names will override the old +definitions, so this is generally not a problem. If the new version of a diff --git a/pythonlings/docs/topics/oop_advanced.md b/pythonlings/docs/topics/oop_advanced.md index 907844b..9c5a6d2 100644 --- a/pythonlings/docs/topics/oop_advanced.md +++ b/pythonlings/docs/topics/oop_advanced.md @@ -8,7 +8,7 @@ Advanced class patterns build on attributes, methods, inheritance, and special m ## Extracted reference -9. Classes — Python 3.14.5 documentation +9. Classes — Python 3.14.6 documentation Navigation @@ -33,4 +33,39 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Tutorial » + +- 9. Classes + +- + +| + +- + +Theme + +Auto + +## More reference + +Source: https://docs.python.org/3/reference/datamodel.html#object.__repr__ + +`__repr__` and `__str__` for custom object representations. + +object.__repr__(self) + +Called by the `repr()` built-in function to compute the “official” string +representation of an object. If at all possible, this should look like a +valid Python expression that could be used to recreate an object with the +same value (given an appropriate environment). If this is not possible, a +string of the form `<...some useful description...>` should be returned. +The return value must be a string object. If a class defines `__repr__()` +but not `__str__()`, then `__repr__()` is also used when an +“informal” string representation of instances of that class is required. + +This is typically used for debugging, so it is important that the representation +is information-rich and unambiguous. A default implementation is provided by the +`object` class itself. diff --git a/pythonlings/docs/topics/pathlib.md b/pythonlings/docs/topics/pathlib.md index 1328c4f..5fbb8f0 100644 --- a/pythonlings/docs/topics/pathlib.md +++ b/pythonlings/docs/topics/pathlib.md @@ -8,7 +8,7 @@ This local reference is generated from the official Python documentation and tri ## Extracted reference -pathlib — Object-oriented filesystem paths — Python 3.14.5 documentation +pathlib — Object-oriented filesystem paths — Python 3.14.6 documentation Navigation @@ -33,4 +33,47 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- File and Directory Access » + +- `pathlib` — Object-oriented filesystem paths + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir + +`Path.mkdir` creates directories with parents/exist_ok options. + +Path.mkdir(mode=0o777, parents=False, exist_ok=False) + +Create a new directory at this given path. If mode is given, it is +combined with the process’s `umask` value to determine the file mode +and access flags. If the path already exists, `FileExistsError` +is raised. + +If parents is true, any missing parents of this path are created +as needed; they are created with the default permissions without taking +mode into account (mimicking the POSIX `mkdir -p` command). + +If parents is false (the default), a missing parent raises +`FileNotFoundError`. + +If exist_ok is false (the default), `FileExistsError` is +raised if the target directory already exists. + +If exist_ok is true, `FileExistsError` will not be raised unless the given +path already exists in the file system and is not a directory (same +behavior as the POSIX `mkdir -p` command). + +Changed in version 3.5: The exist_ok parameter was added. diff --git a/pythonlings/docs/topics/recursion.md b/pythonlings/docs/topics/recursion.md index a668840..8e80de8 100644 --- a/pythonlings/docs/topics/recursion.md +++ b/pythonlings/docs/topics/recursion.md @@ -34,3 +34,40 @@ The statements that form the body of the function start at the next line, and must be indented. The first statement of the function body can optionally be a string literal; +this string literal is the function’s documentation string, or docstring. +(More about docstrings can be found in the section Documentation Strings.) +There are tools which use docstrings to automatically produce online or printed +documentation, or to let the user interactively browse through code; it’s good +practice to include docstrings in code that you write, so make a habit of it. + +The execution of a function introduces a new symbol table used for the local +variables of the function. More precisely, all variable assignments in a +function store the value in the local symbol table; whereas variable references +first look in the local symbol table, then in the local symbol tables of +enclosing functions, then in the global symbol table, and finally in the table +of built-in names. Thus, global variables and variables of enclosing functions +cannot be directly assigned a value within a function (unless, for global +variables, named in a `global` statement, or, for variables of enclosing + +## More reference + +Source: https://docs.python.org/3/library/sys.html#sys.setrecursionlimit + +Adjusting the interpreter's recursion limit with `sys.setrecursionlimit`. + +sys.setrecursionlimit(limit) + +Set the maximum depth of the Python interpreter stack to limit. This limit +prevents infinite recursion from causing an overflow of the C stack and crashing +Python. + +The highest possible limit is platform-dependent. A user may need to set the +limit higher when they have a program that requires deep recursion and a platform +that supports a higher limit. This should be done with care, because a too-high +limit can lead to a crash. + +If the new limit is too low at the current recursion depth, a +`RecursionError` exception is raised. + +Changed in version 3.5.1: A `RecursionError` exception is now raised if the new limit is too +low at the current recursion depth. diff --git a/pythonlings/docs/topics/regex.md b/pythonlings/docs/topics/regex.md index 1bae38f..835b9f8 100644 --- a/pythonlings/docs/topics/regex.md +++ b/pythonlings/docs/topics/regex.md @@ -8,7 +8,7 @@ The `re` module searches, matches, and transforms text using regular expressions ## Extracted reference -re — Regular expression operations — Python 3.14.5 documentation +re — Regular expression operations — Python 3.14.6 documentation Navigation @@ -33,4 +33,64 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Text Processing Services » + +- `re` — Regular expression operations + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/howto/regex.html#search-and-replace + +Practical search-and-replace patterns from the regex HOWTO. + +Search and replace + +Another common task is to find all the matches for a pattern, and replace them +with a different string. The `sub()` method takes a replacement value, +which can be either a string or a function, and the string to be processed. + +.sub(replacement, string[, count=0]) + +Returns the string obtained by replacing the leftmost non-overlapping +occurrences of the RE in string by the replacement replacement. If the +pattern isn’t found, string is returned unchanged. + +The optional argument count is the maximum number of pattern occurrences to be +replaced; count must be a non-negative integer. The default value of 0 means +to replace all occurrences. + +Here’s a simple example of using the `sub()` method. It replaces colour +names with the word `colour`: + +```python +>>> p = re.compile('(blue|white|red)') +>>> p.sub('colour', 'blue socks and red shoes') +'colour socks and colour shoes' +>>> p.sub('colour', 'blue socks and red shoes', count=1) +'colour socks and red shoes' + +``` + +The `subn()` method does the same work, but returns a 2-tuple containing the +new string value and the number of replacements that were performed: + +```python +>>> p = re.compile('(blue|white|red)') +>>> p.subn('colour', 'blue socks and red shoes') +('colour socks and colour shoes', 2) +>>> p.subn('colour', 'no colours at all') +('no colours at all', 0) + +``` diff --git a/pythonlings/docs/topics/sets.md b/pythonlings/docs/topics/sets.md index 587de90..82aed83 100644 --- a/pythonlings/docs/topics/sets.md +++ b/pythonlings/docs/topics/sets.md @@ -33,3 +33,65 @@ Here is a brief demonstration: True >>> 'crabgrass' in basket False + +>>> # Demonstrate set operations on unique letters from two words +>>> +>>> a = set('abracadabra') +>>> b = set('alacazam') +>>> a # unique letters in a +{'a', 'r', 'b', 'c', 'd'} +>>> a - b # letters in a but not in b +{'r', 'd', 'b'} +>>> a | b # letters in a or b or both +{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'} +>>> a & b # letters in both a and b +{'a', 'c'} +>>> a ^ b # letters in a or b but not both +{'r', 'd', 'b', 'm', 'z', 'l'} + +## More reference + +Source: https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset + +set and frozenset methods for membership and set relationships. + +Set Types — `set`, `frozenset` + +A set object is an unordered collection of distinct hashable objects. +Common uses include membership testing, removing duplicates from a sequence, and +computing mathematical operations such as intersection, union, difference, and +symmetric difference. +(For other containers see the built-in `dict`, `list`, +and `tuple` classes, and the `collections` module.) + +Like other collections, sets support `x in set`, `len(set)`, and `for x in +set`. Being an unordered collection, sets do not record element position or +order of insertion. Accordingly, sets do not support indexing, slicing, or +other sequence-like behavior. + +There are currently two built-in set types, `set` and `frozenset`. +The `set` type is mutable — the contents can be changed using methods +like `add()` and `remove()`. +Since it is mutable, it has no hash value and cannot be used as +either a dictionary key or as an element of another set. +The `frozenset` type is immutable and hashable — +its contents cannot be altered after it is created; +it can therefore be used as a dictionary key or as an element of another set. + +Non-empty sets (not frozensets) can be created by placing a comma-separated list +of elements within braces, for example: `{'jack', 'sjoerd'}`, in addition to the +`set` constructor. + +The constructors for both classes work the same: + +class set(iterable=(), /) + +class frozenset(iterable=(), /) + +Return a new set or frozenset object whose elements are taken from +iterable. The elements of a set must be hashable. To +represent sets of sets, the inner sets must be `frozenset` +objects. If iterable is not specified, a new empty set is +returned. + +Sets can be created by several means: diff --git a/pythonlings/docs/topics/strings.md b/pythonlings/docs/topics/strings.md index 8782b36..cef6ac0 100644 --- a/pythonlings/docs/topics/strings.md +++ b/pythonlings/docs/topics/strings.md @@ -33,3 +33,64 @@ be included in the string literal. String literals that are part of a single expression and have only whitespace between them will be implicitly converted to a single string literal. That is, `("spam " "eggs") == "spam eggs"`. + +See String and Bytes literals for more about the various forms of string literal, +including supported escape sequences, and the `r` (“raw”) prefix that +disables most escape sequence processing. + +Strings may also be created from other objects using the `str` +constructor. + +Since there is no separate “character” type, indexing a string produces +strings of length 1. That is, for a non-empty string s, `s[0] == s[0:1]`. + +There is also no mutable string type, but `str.join()` or +`io.StringIO` can be used to efficiently construct strings from +multiple fragments. + +## More reference + +Source: https://docs.python.org/3/library/stdtypes.html#string-methods + +Full reference of string methods with signatures and behaviors. + +String Methods + +Strings implement all of the common sequence +operations, along with the additional methods described below. + +Strings also support two styles of string formatting, one providing a large +degree of flexibility and customization (see `str.format()`, +Format string syntax and Custom string formatting) and the other based on C +`printf` style formatting that handles a narrower range of types and is +slightly harder to use correctly, but is often faster for the cases it can +handle (printf-style String Formatting). + +The Text Processing Services section of the standard library covers a number of +other modules that provide various text related utilities (including regular +expression support in the `re` module). + +str.capitalize() + +Return a copy of the string with its first character capitalized and the +rest lowercased. + +Changed in version 3.8: The first character is now put into titlecase rather than uppercase. +This means that characters like digraphs will only have their first +letter capitalized, instead of the full character. + +str.casefold() + +Return a casefolded copy of the string. Casefolded strings may be used for +caseless matching. + +Casefolding is similar to lowercasing but more aggressive because it is +intended to remove all case distinctions in a string. For example, the German +lowercase letter `'ß'` is equivalent to `"ss"`. Since it is already +lowercase, `lower()` would do nothing to `'ß'`; `casefold()` +converts it to `"ss"`. +For example: + +```python +>>> 'straße'.lower() +'straße' diff --git a/pythonlings/docs/topics/testing.md b/pythonlings/docs/topics/testing.md index 9508ac7..ef287da 100644 --- a/pythonlings/docs/topics/testing.md +++ b/pythonlings/docs/topics/testing.md @@ -8,7 +8,7 @@ Tests encode expected behavior so code can be checked repeatedly. ## Extracted reference -unittest — Unit testing framework — Python 3.14.5 documentation +unittest — Unit testing framework — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Development Tools » + +- `unittest` — Unit testing framework + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/library/doctest.html#how-it-works + +How doctest finds and runs examples embedded in docstrings. + +How It Works + +This section examines in detail how doctest works: which docstrings it looks at, +how it finds interactive examples, what execution context it uses, how it +handles exceptions, and how option flags can be used to control its behavior. +This is the information that you need to know to write doctest examples; for +information about actually running doctest on these examples, see the following +sections. + +Which Docstrings Are Examined? + +The module docstring, and all function, class and method docstrings are +searched. Objects imported into the module are not searched. + +In addition, there are cases when you want tests to be part of a module but not part +of the help text, which requires that the tests not be included in the docstring. +Doctest looks for a module-level variable called `__test__` and uses it to locate other +tests. If `M.__test__` exists, it must be a dict, and each +entry maps a (string) name to a function object, class object, or string. +Function and class object docstrings found from `M.__test__` are searched, and +strings are treated as if they were docstrings. In output, a key `K` in +`M.__test__` appears with name `M.__test__.K`. + +For example, place this block of code at the top of `example.py`: + +```python +__test__ = { +'numbers': """ +>>> factorial(6) +720 + +>>> [factorial(n) for n in range(6)] +[1, 1, 2, 6, 24, 120] +""" +} + +``` + +The value of `example.__test__["numbers"]` will be treated as a +docstring and all the tests inside it will be run. It is diff --git a/pythonlings/docs/topics/tuples.md b/pythonlings/docs/topics/tuples.md index b24a696..d2832bc 100644 --- a/pythonlings/docs/topics/tuples.md +++ b/pythonlings/docs/topics/tuples.md @@ -34,3 +34,64 @@ Traceback (most recent call last): File "", line 1, in TypeError: 'tuple' object does not support item assignment >>> # but they can contain mutable objects: +>>> v = ([1, 2, 3], [3, 2, 1]) +>>> v +([1, 2, 3], [3, 2, 1]) + +``` + +As you see, on output tuples are always enclosed in parentheses, so that nested +tuples are interpreted correctly; they may be input with or without surrounding +parentheses, although often parentheses are necessary anyway (if the tuple is +part of a larger expression). It is not possible to assign to the individual +items of a tuple, however it is possible to create tuples which contain mutable +objects, such as lists. + +Though tuples may seem similar to lists, they are often used in different + +## More reference + +Source: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations + +Operations shared by all sequence types: indexing, slicing, concatenation. + +Common Sequence Operations + +The operations in the following table are supported by most sequence types, +both mutable and immutable. The `collections.abc.Sequence` ABC is +provided to make it easier to correctly implement these operations on +custom sequence types. + +This table lists the sequence operations sorted in ascending priority. In the +table, s and t are sequences of the same type, n, i, j and k are +integers and x is an arbitrary object that meets any type and value +restrictions imposed by s. + +The `in` and `not in` operations have the same priorities as the +comparison operations. The `+` (concatenation) and `*` (repetition) +operations have the same priority as the corresponding numeric operations. [3] + +Operation + +Result + +Notes + +`x in s` + +`True` if an item of s is +equal to x, else `False` + +(1) + +`x not in s` + +`False` if an item of s is +equal to x, else `True` + +(1) + +`s + t` + +the concatenation of s and +t diff --git a/pythonlings/docs/topics/type_hints.md b/pythonlings/docs/topics/type_hints.md index f5bc06e..e963e0b 100644 --- a/pythonlings/docs/topics/type_hints.md +++ b/pythonlings/docs/topics/type_hints.md @@ -8,7 +8,7 @@ Type hints annotate expected values and help tools reason about your code. ## Extracted reference -typing — Support for type hints — Python 3.14.5 documentation +typing — Support for type hints — Python 3.14.6 documentation Navigation @@ -33,4 +33,65 @@ previous | - - -3.14.5 Documentation » +3.14.6 Documentation » + +- The Python Standard Library » + +- Development Tools » + +- `typing` — Support for type hints + +- + +| + +- + +Theme + +## More reference + +Source: https://docs.python.org/3/reference/datamodel.html#annotations + +How annotations are stored in `__annotations__`. + +3.3.12. Annotations + +Functions, classes, and modules may contain annotations, +which are a way to associate information (usually type hints) +with a symbol. + +object.__annotations__ + +This attribute contains the annotations for an object. It is +lazily evaluated, so accessing the attribute may +execute arbitrary code and raise exceptions. If evaluation is successful, the +attribute is set to a dictionary mapping from variable names to annotations. + +Changed in version 3.14: Annotations are now lazily evaluated. + +object.__annotate__(format) + +An annotate function. +Returns a new dictionary object mapping attribute/parameter names to their annotation values. + +Takes a format parameter specifying the format in which annotations values should be provided. +It must be a member of the `annotationlib.Format` enum, or an integer with +a value corresponding to a member of the enum. + +If an annotate function doesn’t support the requested format, it must raise +`NotImplementedError`. Annotate functions must always support +`VALUE` format; they must not raise +`NotImplementedError()` when called with this format. + +When called with `VALUE` format, an annotate function may raise +`NameError`; it must not raise `NameError` when called requesting any other format. + +If an object does not have any annotations, `__annotate__` should preferably be set +to `None` (it can’t be deleted), rather than set to a function that returns an empty dict. + +Added in version 3.14. + +See also + +PEP 649 — Deferred evaluation of annotation using descriptors diff --git a/pythonlings/docs/topics/variables.md b/pythonlings/docs/topics/variables.md index 96fab9f..b3d38aa 100644 --- a/pythonlings/docs/topics/variables.md +++ b/pythonlings/docs/topics/variables.md @@ -34,3 +34,64 @@ For example: ``` The integer numbers (e.g. `2`, `4`, `20`) have type `int`, +the ones with a fractional part (e.g. `5.0`, `1.6`) have type +`float`. We will see more about numeric types later in the tutorial. + +Division (`/`) always returns a float. To do floor division and +get an integer result you can use the `//` operator; to calculate +the remainder you can use `%`: + +```python +>>> 17 / 3 # classic division returns a float +5.666666666666667 +>>> +>>> 17 // 3 # floor division discards the fractional part +5 +>>> 17 % 3 # the % operator returns the remainder of the division + +## More reference + +Source: https://docs.python.org/3/reference/simple_stmts.html#assignment-statements + +Assignment statement syntax and semantics, including multiple and unpacking targets. + +7.2. Assignment statements + +Assignment statements are used to (re)bind names to values and to modify +attributes or items of mutable objects: + +```python + +assignment_stmt: (target_list "=")+ (starred_expression | yield_expression) +target_list: target ("," target)* [","] +target: identifier +| "(" [target_list] ")" +| "[" [target_list] "]" +| attributeref +| subscription +| "*" target + +``` + +(See section Primaries for the syntax definitions for attributeref +and subscription.) + +An assignment statement evaluates the expression list (remember that this can be +a single expression or a comma-separated list, the latter yielding a tuple) and +assigns the single resulting object to each of the target lists, from left to +right. + +Assignment is defined recursively depending on the form of the target (list). +When a target is part of a mutable object (an attribute reference or +subscription), the mutable object must ultimately perform the assignment and +decide about its validity, and may raise an exception if the assignment is +unacceptable. The rules observed by various types and the exceptions raised are +given with the definition of the object types (see section The standard type hierarchy). + +Assignment of an object to a target list, optionally enclosed in parentheses or +square brackets, is recursively defined as follows. + +- + +If the target list is a single target with no trailing comma, +optionally in parentheses, the object is assigned to that target. diff --git a/scripts/fetch_python_docs.py b/scripts/fetch_python_docs.py index 422a54b..3be2852 100644 --- a/scripts/fetch_python_docs.py +++ b/scripts/fetch_python_docs.py @@ -22,7 +22,7 @@ BASE_URL = "https://docs.python.org/3/" OUTPUT = Path("pythonlings/docs") -MAX_LINES = 26 +MAX_LINES = 40 @dataclass(frozen=True) @@ -31,50 +31,61 @@ class Source: title: str url: str summary: str + extra_url: str = "" + extra_summary: str = "" SOURCES: tuple[Source, ...] = ( - Source("variables", "Variables", BASE_URL + "tutorial/introduction.html#using-python-as-a-calculator", "Assigning names with `=` lets you keep values for later expressions."), - Source("strings", "Strings", BASE_URL + "library/stdtypes.html#text-sequence-type-str", "Strings are immutable text sequences with indexing, slicing, and many useful methods."), - Source("conditionals", "Conditionals", BASE_URL + "tutorial/controlflow.html#if-statements", "`if`, `elif`, and `else` choose which block runs based on boolean conditions."), - Source("loops", "Loops", BASE_URL + "tutorial/controlflow.html#for-statements", "`for` loops iterate over items; `while` loops continue while a condition is true."), - Source("functions", "Functions", BASE_URL + "tutorial/controlflow.html#defining-functions", "`def` creates reusable behavior with parameters, return values, and optional defaults."), - Source("lists", "Lists", BASE_URL + "tutorial/datastructures.html#more-on-lists", "Lists are mutable ordered collections with methods such as `append`, `pop`, and `sort`."), - Source("tuples", "Tuples", BASE_URL + "tutorial/datastructures.html#tuples-and-sequences", "Tuples group ordered values and are commonly unpacked into multiple names."), - Source("dictionaries", "Dictionaries", BASE_URL + "tutorial/datastructures.html#dictionaries", "Dictionaries map keys to values and are the standard way to represent lookup tables."), - Source("sets", "Sets", BASE_URL + "tutorial/datastructures.html#sets", "Sets store unique values and support membership tests and set operations."), - Source("comprehensions", "Comprehensions", BASE_URL + "tutorial/datastructures.html#list-comprehensions", "Comprehensions build new collections by combining an expression with one or more loops."), - Source("exceptions", "Exceptions", BASE_URL + "tutorial/errors.html#handling-exceptions", "`try` and `except` handle errors without crashing the whole program."), - Source("file_io", "File I/O", BASE_URL + "tutorial/inputoutput.html#reading-and-writing-files", "`open` and `with` are the usual tools for reading and writing files safely."), - Source("classes", "Classes", BASE_URL + "tutorial/classes.html#a-first-look-at-classes", "Classes combine data and behavior into reusable types."), - Source("functional", "Functional Tools", BASE_URL + "howto/functional.html", "Functional style uses functions, iterators, and transformations as building blocks."), - Source("decorators", "Decorators", BASE_URL + "reference/compound_stmts.html#function-definitions", "Decorators wrap or transform functions and classes with `@decorator` syntax."), - Source("generators", "Generators", BASE_URL + "tutorial/classes.html#generators", "Generators produce values lazily with `yield` instead of returning a whole collection."), - Source("context_managers", "Context Managers", BASE_URL + "reference/compound_stmts.html#the-with-statement", "Context managers run setup and cleanup around a block using `with`."), - Source("dataclasses", "Dataclasses", BASE_URL + "library/dataclasses.html", "Dataclasses generate common class methods for data-focused classes."), - Source("type_hints", "Type Hints", BASE_URL + "library/typing.html", "Type hints annotate expected values and help tools reason about your code."), - Source("regex", "Regular Expressions", BASE_URL + "library/re.html", "The `re` module searches, matches, and transforms text using regular expressions."), - Source("testing", "Testing", BASE_URL + "library/unittest.html", "Tests encode expected behavior so code can be checked repeatedly."), - Source("recursion", "Recursion", BASE_URL + "tutorial/controlflow.html#defining-functions", "Recursive functions solve a problem by calling themselves on smaller inputs."), - Source("modules", "Modules", BASE_URL + "tutorial/modules.html#modules", "Modules organize Python code into importable files and packages."), - Source("collections", "Collections", BASE_URL + "library/collections.html", "`collections` provides specialized containers beyond the built-in list, dict, set, and tuple."), - Source("itertools", "Itertools", BASE_URL + "library/itertools.html", "`itertools` provides fast iterator building blocks for loops and data pipelines."), - Source("json", "JSON", BASE_URL + "library/json.html", "`json` converts between Python values and JSON text."), - Source("datetime", "Datetime", BASE_URL + "library/datetime.html", "`datetime` represents dates, times, durations, and time zones."), - Source("enums", "Enums", BASE_URL + "library/enum.html", "Enums define named constant values that are easier to read than raw literals."), - Source("pathlib", "Pathlib", BASE_URL + "library/pathlib.html", "`pathlib.Path` represents filesystem paths with object-oriented helpers."), - Source("oop_advanced", "Advanced OOP", BASE_URL + "tutorial/classes.html", "Advanced class patterns build on attributes, methods, inheritance, and special methods."), - Source("async", "Asyncio", BASE_URL + "library/asyncio.html", "`asyncio` runs concurrent I/O-bound work with coroutines and an event loop."), + Source("variables", "Variables", BASE_URL + "tutorial/introduction.html#using-python-as-a-calculator", "Assigning names with `=` lets you keep values for later expressions.", BASE_URL + "reference/simple_stmts.html#assignment-statements", "Assignment statement syntax and semantics, including multiple and unpacking targets."), + Source("strings", "Strings", BASE_URL + "library/stdtypes.html#text-sequence-type-str", "Strings are immutable text sequences with indexing, slicing, and many useful methods.", BASE_URL + "library/stdtypes.html#string-methods", "Full reference of string methods with signatures and behaviors."), + Source("conditionals", "Conditionals", BASE_URL + "tutorial/controlflow.html#if-statements", "`if`, `elif`, and `else` choose which block runs based on boolean conditions.", BASE_URL + "tutorial/controlflow.html#match-statements", "`match` pattern matching with examples and syntax."), + Source("loops", "Loops", BASE_URL + "tutorial/controlflow.html#for-statements", "`for` loops iterate over items; `while` loops continue while a condition is true.", BASE_URL + "reference/compound_stmts.html#the-for-statement", "Formal for-statement syntax and iteration semantics."), + Source("functions", "Functions", BASE_URL + "tutorial/controlflow.html#defining-functions", "`def` creates reusable behavior with parameters, return values, and optional defaults.", BASE_URL + "reference/compound_stmts.html#function-definitions", "Function definition syntax including parameters, annotations, and decorators."), + Source("lists", "Lists", BASE_URL + "tutorial/datastructures.html#more-on-lists", "Lists are mutable ordered collections with methods such as `append`, `pop`, and `sort`.", BASE_URL + "library/stdtypes.html#sequence-types-list-tuple-range", "Sequence types reference with list methods, operations, and type tables."), + Source("tuples", "Tuples", BASE_URL + "tutorial/datastructures.html#tuples-and-sequences", "Tuples group ordered values and are commonly unpacked into multiple names.", BASE_URL + "library/stdtypes.html#common-sequence-operations", "Operations shared by all sequence types: indexing, slicing, concatenation."), + Source("dictionaries", "Dictionaries", BASE_URL + "tutorial/datastructures.html#dictionaries", "Dictionaries map keys to values and are the standard way to represent lookup tables.", BASE_URL + "library/stdtypes.html#mapping-types-dict", "The dict type with constructor forms, methods, and creation examples."), + Source("sets", "Sets", BASE_URL + "tutorial/datastructures.html#sets", "Sets store unique values and support membership tests and set operations.", BASE_URL + "library/stdtypes.html#set-types-set-frozenset", "set and frozenset methods for membership and set relationships."), + Source("comprehensions", "Comprehensions", BASE_URL + "tutorial/datastructures.html#list-comprehensions", "Comprehensions build new collections by combining an expression with one or more loops.", BASE_URL + "tutorial/datastructures.html#nested-list-comprehensions", "Nested comprehensions that build lists of lists from multiple loops."), + Source("exceptions", "Exceptions", BASE_URL + "tutorial/errors.html#handling-exceptions", "`try` and `except` handle errors without crashing the whole program.", BASE_URL + "library/exceptions.html#concrete-exceptions", "Concrete built-in exceptions like ValueError, KeyError, and TypeError."), + Source("file_io", "File I/O", BASE_URL + "tutorial/inputoutput.html#reading-and-writing-files", "`open` and `with` are the usual tools for reading and writing files safely.", BASE_URL + "library/io.html#high-level-module-interface", "The high-level `open()` interface and its modes."), + Source("classes", "Classes", BASE_URL + "tutorial/classes.html#a-first-look-at-classes", "Classes combine data and behavior into reusable types.", BASE_URL + "reference/datamodel.html#special-method-names", "Special methods like `__init__`, `__str__`, and `__repr__`."), + Source("functional", "Functional Tools", BASE_URL + "howto/functional.html", "Functional style uses functions, iterators, and transformations as building blocks.", BASE_URL + "library/itertools.html#itertool-functions", "itertools building blocks with signatures and usage patterns."), + Source("decorators", "Decorators", BASE_URL + "reference/compound_stmts.html#function-definitions", "Decorators wrap or transform functions and classes with `@decorator` syntax.", BASE_URL + "library/functools.html#functools.wraps", "`functools.wraps` preserves the wrapped function's metadata."), + Source("generators", "Generators", BASE_URL + "tutorial/classes.html#generators", "Generators produce values lazily with `yield` instead of returning a whole collection.", BASE_URL + "reference/expressions.html#generator-expressions", "Generator expression syntax for compact lazy iterators."), + Source("context_managers", "Context Managers", BASE_URL + "reference/compound_stmts.html#the-with-statement", "Context managers run setup and cleanup around a block using `with`.", BASE_URL + "library/contextlib.html#contextlib.contextmanager", "`contextlib.contextmanager` builds a context manager from a generator."), + Source("dataclasses", "Dataclasses", BASE_URL + "library/dataclasses.html", "Dataclasses generate common class methods for data-focused classes.", BASE_URL + "tutorial/classes.html#class-and-instance-variables", "Class vs instance variables — the distinction behind dataclass fields."), + Source("type_hints", "Type Hints", BASE_URL + "library/typing.html", "Type hints annotate expected values and help tools reason about your code.", BASE_URL + "reference/datamodel.html#annotations", "How annotations are stored in `__annotations__`."), + Source("regex", "Regular Expressions", BASE_URL + "library/re.html", "The `re` module searches, matches, and transforms text using regular expressions.", BASE_URL + "howto/regex.html#search-and-replace", "Practical search-and-replace patterns from the regex HOWTO."), + Source("testing", "Testing", BASE_URL + "library/unittest.html", "Tests encode expected behavior so code can be checked repeatedly.", BASE_URL + "library/doctest.html#how-it-works", "How doctest finds and runs examples embedded in docstrings."), + Source("recursion", "Recursion", BASE_URL + "tutorial/controlflow.html#defining-functions", "Recursive functions solve a problem by calling themselves on smaller inputs.", BASE_URL + "library/sys.html#sys.setrecursionlimit", "Adjusting the interpreter's recursion limit with `sys.setrecursionlimit`."), + Source("modules", "Modules", BASE_URL + "tutorial/modules.html#modules", "Modules organize Python code into importable files and packages.", BASE_URL + "library/importlib.html#importlib.reload", "Reloading and dynamically importing modules with importlib."), + Source("collections", "Collections", BASE_URL + "library/collections.html", "`collections` provides specialized containers beyond the built-in list, dict, set, and tuple.", BASE_URL + "library/collections.html#collections.Counter", "`Counter` tallies hashable objects and finds the most common ones."), + Source("itertools", "Itertools", BASE_URL + "library/itertools.html", "`itertools` provides fast iterator building blocks for loops and data pipelines.", BASE_URL + "howto/functional.html#the-itertools-module", "The functional HOWTO's tour of itertools with examples."), + Source("json", "JSON", BASE_URL + "library/json.html", "`json` converts between Python values and JSON text.", BASE_URL + "tutorial/inputoutput.html#saving-structured-data-with-json", "Tutorial on saving and loading structured data as JSON."), + Source("datetime", "Datetime", BASE_URL + "library/datetime.html", "`datetime` represents dates, times, durations, and time zones.", BASE_URL + "library/datetime.html#examples-of-usage-datetime", "Worked `datetime` examples beyond the API reference."), + Source("enums", "Enums", BASE_URL + "library/enum.html", "Enums define named constant values that are easier to read than raw literals.", BASE_URL + "library/enum.html#enum.Flag", "`Flag` enums combine members with bitwise operators."), + Source("pathlib", "Pathlib", BASE_URL + "library/pathlib.html", "`pathlib.Path` represents filesystem paths with object-oriented helpers.", BASE_URL + "library/pathlib.html#pathlib.Path.mkdir", "`Path.mkdir` creates directories with parents/exist_ok options."), + Source("oop_advanced", "Advanced OOP", BASE_URL + "tutorial/classes.html", "Advanced class patterns build on attributes, methods, inheritance, and special methods.", BASE_URL + "reference/datamodel.html#object.__repr__", "`__repr__` and `__str__` for custom object representations."), + Source("async", "Asyncio", BASE_URL + "library/asyncio.html", "`asyncio` runs concurrent I/O-bound work with coroutines and an event loop.", BASE_URL + "library/asyncio-task.html#coroutines", "Coroutines and `async`/`await` with execution examples."), ) class SectionParser(HTMLParser): + """Extract the text of the element bearing a target id (or the whole body). + + Triggers on any element whose id matches the URL fragment. A `
` + anchor captures that section; a `
` anchor (Sphinx object/method entry) + captures its enclosing `
` so the signature and description come together. + """ + def __init__(self, target_id: str | None) -> None: super().__init__(convert_charrefs=True) self.target_id = target_id self.capture = target_id is None self.found = target_id is None - self.section_depth = 0 + self.depth = 0 + self.capture_tag: str | None = None + self.dl_depth = 0 self.skip_depth = 0 self.in_pre = False self.in_code = False @@ -86,13 +97,22 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None self.skip_depth += 1 return - if tag == "section": - if self.capture: - self.section_depth += 1 - elif attr.get("id") == self.target_id: - self.capture = True - self.found = True - self.section_depth = 1 + if tag == "dl": + self.dl_depth += 1 + + if ( + not self.capture + and self.target_id is not None + and attr.get("id") == self.target_id + ): + self.capture = True + self.found = True + # A
is just a signature; capture the enclosing
instead so + # the description (
) is included. + self.capture_tag = "dl" if (tag == "dt" and self.dl_depth) else tag + self.depth = 1 + elif self.capture and tag == self.capture_tag: + self.depth += 1 if not self._collecting(): return @@ -125,11 +145,14 @@ def handle_endtag(self, tag: str) -> None: self.in_code = False self.parts.append("`") - if tag == "section" and self.capture and self.section_depth: - self.section_depth -= 1 - if self.section_depth == 0: + if self.capture and tag == self.capture_tag and self.depth: + self.depth -= 1 + if self.depth == 0: self.capture = False + if tag == "dl": + self.dl_depth -= 1 + def handle_data(self, data: str) -> None: if self._collecting(): self.parts.append(data) @@ -175,8 +198,8 @@ def normalize_lines(text: str) -> list[str]: return lines -def render_markdown(source: Source, extracted: str) -> str: - return ( +def render_markdown(source: Source, extracted: str, extra: str = "") -> str: + md = ( f"# {source.title}\n\n" f"Source: {source.url}\n\n" "This local reference is generated from the official Python documentation " @@ -185,6 +208,14 @@ def render_markdown(source: Source, extracted: str) -> str: "## Extracted reference\n\n" f"{extracted}\n" ) + if extra: + md += ( + "\n## More reference\n\n" + f"Source: {source.extra_url}\n\n" + f"{source.extra_summary}\n\n" + f"{extra}\n" + ) + return md def write_outputs(sources: tuple[Source, ...], output: Path) -> None: @@ -194,11 +225,16 @@ def write_outputs(sources: tuple[Source, ...], output: Path) -> None: for source in sources: print(f"fetch {source.topic}: {source.url}") - html_text = fetch(source.url) - extracted = extract(html_text, source.url) + extracted = extract(fetch(source.url), source.url) + extra = "" + if source.extra_url: + try: + extra = extract(fetch(source.extra_url), source.extra_url) + except Exception as e: # noqa: BLE001 — extra section is best-effort + print(f" warn: skipped extra for {source.topic}: {e}") filename = f"topics/{source.topic}.md" (output / filename).write_text( - render_markdown(source, extracted), encoding="utf-8" + render_markdown(source, extracted, extra), encoding="utf-8" ) index["topics"][source.topic] = { "title": source.title, From 987a3518416ce172440c960f13a9c44258188ab0 Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 20:55:19 +0530 Subject: [PATCH 7/9] fix: migrate legacy .pylings dir for --path commands too main() chose the workspace with 'getattr(args, "root") or getattr(args, "path")', but --root defaults to cwd and is always truthy, so the --path branch was dead. 'update --path ' run from another directory never migrated the workspace's .pylings/, silently orphaning the learner's progress behind a fresh empty .pythonlings/. Migrate both the --path and --root targets (both no-ops without a legacy dir). --- pythonlings/cli.py | 12 ++++++++---- tests/integration/test_cli_workspace.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pythonlings/cli.py b/pythonlings/cli.py index 40d9046..0001a69 100644 --- a/pythonlings/cli.py +++ b/pythonlings/cli.py @@ -254,10 +254,14 @@ 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)) + # Migrate any legacy .pylings/ state dir. init/update target --path; the + # in-workspace commands target --root. Both are no-ops without a legacy dir. + from pythonlings.core.curriculum import migrate_legacy_state_dir + + for attr in ("path", "root"): + workspace = getattr(args, attr, None) + if workspace is not None: + migrate_legacy_state_dir(Path(workspace)) try: if getattr(args, "debug", False): diff --git a/tests/integration/test_cli_workspace.py b/tests/integration/test_cli_workspace.py index 632a5be..d6ba721 100644 --- a/tests/integration/test_cli_workspace.py +++ b/tests/integration/test_cli_workspace.py @@ -27,6 +27,26 @@ def test_init_command_requires_force_for_non_empty_directory( assert "already exists and is not empty" in capsys.readouterr().err +def test_update_via_path_migrates_legacy_state_dir(tmp_path: Path) -> None: + target = tmp_path / "legacy-ws" + main(["init", "--path", str(target)]) + (target / ".pythonlings").rename(target / ".pylings") + (target / ".pylings" / "state.json").write_text( + '{"format_version": 2, "completed": ["variables1"], "seen_intro": true,' + ' "last_topic": "variables", "last_exercise": "variables2"}', + encoding="utf-8", + ) + + # --path points at the workspace; cwd is elsewhere. The legacy dir must move. + main(["update", "--path", str(target)]) + + assert not (target / ".pylings").exists() + assert (target / ".pythonlings" / "state.json").exists() + from pythonlings.core.state import load as load_state + + assert "variables1" in load_state(target).completed + + def test_update_command_preserves_user_exercises(tmp_path: Path) -> None: target = tmp_path / "learn-python" assert main(["init", "--path", str(target)]) == 0 From abfb95eaf916da5f2563cbb9de6fb5238ed5755a Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 20:56:53 +0530 Subject: [PATCH 8/9] refactor: drop dead ExerciseTree.render_manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render_manifest had no callers (render_topic is the only one used); its Manifest/State type imports went with it. Left reset's name-based snapshot fallback in place — the reset unit tests construct Exercises with rel_path=None and depend on it. --- pythonlings/widgets/exercise_tree.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pythonlings/widgets/exercise_tree.py b/pythonlings/widgets/exercise_tree.py index 57d494a..fa3522c 100644 --- a/pythonlings/widgets/exercise_tree.py +++ b/pythonlings/widgets/exercise_tree.py @@ -7,8 +7,6 @@ if TYPE_CHECKING: from pythonlings.core.exercise import Exercise - from pythonlings.core.manifest import Manifest - from pythonlings.core.state import State class ExerciseTree(Tree[str]): @@ -16,23 +14,6 @@ def __init__(self) -> None: super().__init__("exercises", id="tree") self.show_root = False - def render_manifest( - self, manifest: "Manifest", state: "State", current: str | None - ) -> None: - self.clear() - topics: dict[str, object] = {} - for ex in manifest.exercises: - if ex.topic not in topics: - topics[ex.topic] = self.root.add(ex.topic, expand=True) - parent = topics[ex.topic] - if ex.name in state.completed: - marker = "✓" - elif ex.name == current: - marker = "●" - else: - marker = "🔒" - parent.add_leaf(f"{marker} {ex.name}", data=ex.name) - def render_topic( self, topic: str, From c265d747bf3e04d15db79d5f525d68abd8ccab53 Mon Sep 17 00:00:00 2001 From: Abhik Sarkar Date: Thu, 18 Jun 2026 21:00:12 +0530 Subject: [PATCH 9/9] docs: document 0.3.0 docs, UX, and dependency changes in CHANGELOG --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da0772d..24f30cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Semantic Versioning. ## [0.3.0] - 2026-06-10 +### Added + +- The in-app reference (`F5`) now bundles a second complementary section from + the official Python documentation for every topic, roughly tripling each + topic's offline reference content. + ### Changed - The project is renamed from pylings to **pythonlings**. The PyPI package @@ -12,6 +18,17 @@ Semantic Versioning. 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. +- The README and quick-start now describe the built-in editor accurately: + checks rerun as you type in the TUI editor. + +### Fixed + +- Running `pythonlings` outside a workspace now explains how to create one + with `pythonlings init` instead of a bare "info.toml not found". + +### Removed + +- The unused `watchdog` dependency, for a lighter install. ## [0.2.0] - 2026-05-30