diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b713fa0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: Report a reproducible problem +labels: bug +--- + +## What Happened? + +## Steps To Reproduce + +## Expected Behavior + +## Environment + +- OS: +- Python: +- Terminal: +- Pylings version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..76b5cb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: Feature request +about: Suggest an improvement +labels: enhancement +--- + +## Problem + +## Proposed Behavior + +## Alternatives Considered diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5ba73fc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Summary + +## Tests + +## Screenshots + +## Checklist + +- [ ] Updated docs when behavior changed +- [ ] Added or updated tests +- [ ] Verified `python -m pytest -q` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ef0577..d870f43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,8 @@ jobs: - run: pip install -e ".[dev]" - run: pytest -v - run: pylings --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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ad50ef6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: publish + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Check release tag matches project version + run: | + version=$(python - <<'PY' + import tomllib + from pathlib import Path + + data = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + print(data["project"]["version"]) + PY + ) + test "${GITHUB_REF_NAME}" = "v${version}" + - 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 + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/AGENTS.md b/AGENTS.md index a6bc7e8..fde0978 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,28 +2,28 @@ ## Project Structure & Module Organization -`pylings/` contains the installable application package. Core exercise loading, state, reset, and runner logic lives in `pylings/core/`; CLI entry points are in `pylings/cli.py` and `pylings/__main__.py`; Textual screens and widgets live in `pylings/screens/` and `pylings/widgets/`; `pylings/pylings.tcss` holds TUI styles. +`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. -Curriculum files are split between `exercises//.py` for learner-editable code and `checks//.py` for hidden assertions. Keep these trees mirrored. `info.toml` defines exercise order and hints. Tests are under `tests/unit/`, `tests/integration/`, and `tests/tui/`, with reusable sample curricula in `tests/fixtures/`. +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`: launch the Textual app in watch mode. -- `pylings list`, `pylings hint variables1`, `pylings run variables1`: exercise common CLI paths. -- `pylings --root tests/fixtures/passing_curriculum verify`: smoke-test curriculum verification against a known passing fixture. -- `pytest`: run the full test suite configured in `pyproject.toml`. -- `pytest tests/unit` or `pytest tests/integration/test_cli_run.py`: run focused tests while developing. -- `python -m build`: build distribution artifacts when the `build` package is available. +- `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. +- `python -m pytest -q`: run the full suite configured in `pyproject.toml`. +- `python -m build`: build source and wheel distributions. ## Coding Style & Naming Conventions -Use Python 3.11+ idioms and standard 4-space indentation. Prefer small, typed functions where practical, and keep UI-specific behavior inside `screens` or `widgets` instead of `core`. Name tests `test_.py` and test functions `test_`. Curriculum exercise/check filenames use the topic prefix plus an ordinal, such as `variables1.py` or `collections10.py`. +Use Python 3.11+ idioms and 4-space indentation. Prefer small, typed functions where practical. Keep UI behavior in `screens` or `widgets`; keep filesystem, manifest, reset, and runner behavior in `core`. Name tests `test_.py` and test functions `test_`. Curriculum names use topic plus ordinal, such as `variables1.py` or `collections10.py`. ## Testing Guidelines -Use pytest for all tests; async tests are supported by `pytest-asyncio` with auto mode. Add unit tests for core behavior, integration tests for CLI flows, and TUI tests for Textual interactions. When adding or changing curriculum, update both `exercises/`, `checks/`, and `info.toml`, then run the verification fixture command plus relevant pytest files. +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`. ## Commit & Pull Request Guidelines -Recent history uses conventional prefixes such as `feat:`, `fix:`, and `docs:`. Keep commits focused and imperative, for example `fix: reset hints between exercises`. Pull requests should explain the user-facing change, list tests run, link related issues when applicable, and include screenshots or terminal output for TUI/CLI behavior changes. +Recent history uses conventional prefixes such as `feat:`, `fix:`, `docs:`, `chore:`, and merge commits between `feature/*`, `dev`, and `main`. Keep commits focused and imperative, for example `fix: reset exercise originals`. Pull requests should explain the user-facing change, list tests run, link issues when applicable, and include screenshots or terminal output for TUI/CLI changes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 776822a..ad86de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project are documented here. Pylings follows Semantic Versioning. -## [0.1] - 2026-05-25 +## [0.1.0] - 2026-05-25 ### Added @@ -14,10 +14,11 @@ 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. - Contributor guide, screenshots, release flow notes, and MIT license. ### Verified -- Full test suite: `105 passed`. +- Full test suite: `125 passed`. - Curriculum/docs audit: every exercise has a configured docs URL and a bundled local snippet. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1811537 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Code of Conduct + +Pylings follows the Contributor Covenant Code of Conduct, version 2.1. + +## Our Standards + +Use welcoming and inclusive language, respect different experience levels, accept constructive feedback, and focus on what is best for the community. + +Unacceptable behavior includes harassment, personal attacks, sexualized language or imagery, publishing private information, or sustained disruption of project work. + +## Enforcement + +Report unacceptable behavior through GitHub Security Advisories. Maintainers may remove comments, close issues, block users, or take other appropriate action. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9bce50c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +## Development Setup + +```bash +pip install -e ".[dev]" +python -m pytest -q +``` + +## Curriculum Changes + +Update `info.toml`, `exercises/`, `checks/`, and `solutions/` together. Exercise and check paths must mirror each other, and every exercise must have a passing reference solution. + +## Pull Requests + +Use focused branches named `feature/` or `fix/`. Include a short description, test output, and screenshots for TUI changes. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..23e00c2 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,44 @@ +# Release Checklist + +Pylings 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 package name is transferred. + +## Pre-Release Verification + +Run these checks from a clean working tree before tagging: + +```bash +python -m pytest -q +pylings --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 +``` + +Expected release version for `v0.1.0`: + +```text +pylings 0.1.0 +``` + +## Tag And Publish + +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. +4. Create a GitHub Release from the tag. +5. The `publish` workflow builds the package, checks that the release tag + matches `pyproject.toml`, smoke-tests the installed wheel, then publishes to + PyPI through trusted publishing. diff --git a/Readme.md b/Readme.md index 9c59cc1..123b5ae 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,12 @@ # Pylings [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)](https://www.python.org/) -[![Version](https://img.shields.io/badge/version-0.1-blue)](https://semver.org/) +[![Version](https://img.shields.io/badge/version-0.1.0-blue)](https://semver.org/) [![SemVer](https://img.shields.io/badge/semver-2.0.0-brightgreen)](https://semver.org/) -[![Tests](https://img.shields.io/badge/tests-105%20passing-brightgreen)](#development) +[![Tests](https://img.shields.io/badge/tests-125%20passing-brightgreen)](#development) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow)](LICENSE) -Rustlings-style interactive Python exercises in a live terminal TUI. +Python learnings, Rustlings-style, in a live terminal TUI. Pylings helps you learn Python by fixing small broken programs and watching checks rerun as you type. It is built for beginner Python practice, coding @@ -27,15 +27,28 @@ Python documentation snippets so learners can work without leaving the terminal. ## Install -The `pylings` command is installed from this repository. The PyPI project name -`pylings` is already used by another package, so install this project from -GitHub until a package release is published under its distribution name, -`pylings-tui`. +The `pylings` command is installed by the `python-learnings` package. The PyPI +project name `pylings` is already used by another package, so do not use +`pip install pylings` for this project. + +```bash +pipx install python-learnings +``` + +For unreleased development builds from GitHub: ```bash pipx install git+https://github.com/abhiksark/pylings.git ``` +Create a learner workspace before starting: + +```bash +pylings init --path ~/pylings-workspace +cd ~/pylings-workspace +pylings +``` + For local development: ```bash @@ -47,13 +60,16 @@ pip install -e ".[dev]" ## Quick Start ```bash -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 reset variables1 --yes # restore an exercise from its snapshot -pylings verify # run every exercise check +pylings 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 ``` Each exercise contains a `# I AM NOT DONE` marker. Fix the code, remove the @@ -84,6 +100,7 @@ pylings/ # application package screens/ # Textual screens widgets/ # reusable TUI widgets docs/ # bundled Python documentation snippets +pylings/curriculum/ # packaged copy in built wheels exercises// # learner-editable exercise files checks// # hidden assertions for each exercise tests/ # unit, integration, and TUI tests @@ -120,11 +137,12 @@ Pylings uses Semantic Versioning: Branch flow is feature-first: ```text -feature/ -> dev -> main -> vMAJOR.MINOR +feature/ -> dev -> main -> vMAJOR.MINOR.PATCH ``` Feature branches are merged into `dev`. A verified `dev` branch is then merged -into `main` and tagged with an annotated release tag such as `v0.1`. +into `main` and tagged with an annotated release tag such as `v0.1.0`. +See [RELEASE.md](RELEASE.md) for the release checklist. ## Attribution diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..63fe244 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +Report security issues privately through GitHub Security Advisories. Do not open public issues for vulnerabilities. + +Supported versions: the latest released `0.x` version and current `main`. diff --git a/docs/superpowers/plans/2026-05-25-pylings-product-maturity.md b/docs/superpowers/plans/2026-05-25-pylings-product-maturity.md new file mode 100644 index 0000000..ff364f6 --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-pylings-product-maturity.md @@ -0,0 +1,1325 @@ +# Pylings Product Maturity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Pylings a self-contained installable learning product instead of a repo-bound TUI. + +**Architecture:** Keep the existing Textual app and manifest model, but add a packaged curriculum layer that can initialize and update learner workspaces. Reset, run, verify, and future solution commands should operate against explicit workspace roots while pristine exercise/check data comes from packaged resources. + +**Tech Stack:** Python 3.11+, Textual, argparse, hatchling, importlib.resources, pathlib/shutil, subprocess, pytest, GitHub Actions. + +--- + +## File Structure + +- Create `pylings/core/curriculum.py`: packaged curriculum discovery, resource copying, workspace init/update helpers. +- Modify `pylings/core/exercise.py`: add optional `root`, `rel_path`, and `check_rel_path` metadata needed by reset and runner. +- Modify `pylings/core/manifest.py`: populate exercise metadata from `info.toml`. +- Modify `pylings/core/reset.py`: restore from `.pylings/originals/` created from pristine curriculum, not from first-seen edited files. +- Modify `pylings/core/runner.py`: run compiled exercise/check code with real filenames and workspace cwd. +- Modify `pylings/cli.py`: add `init`, `update`, `dry-run`, `solution`, and `--debug`. +- Modify `pyproject.toml`: package curriculum resources and add build/test dependencies used by release verification. +- Create `tests/unit/test_curriculum.py`: packaged curriculum and workspace-copy tests. +- Extend `tests/integration/`: CLI init/update/reset/runner/package-flow tests. +- Create `solutions/`: mirrored reference answers, one file per exercise. +- Create `.github/ISSUE_TEMPLATE/`, `.github/pull_request_template.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`. +- Modify `.github/workflows/ci.yml`: add package install smoke test. +- Create `.github/workflows/publish.yml`: trusted PyPI/GitHub release workflow, disabled until credentials are configured. + +## Phase 0: Branch And Baseline + +### Task 0: Prepare Implementation Branch + +**Files:** +- No code files changed. + +- [ ] **Step 1: Confirm clean working tree** + +Run: + +```bash +git status --short +``` + +Expected: no output, or only files intentionally kept from previous work. If unrelated changes exist, stash or commit them before starting. + +- [ ] **Step 2: Create the feature branch** + +Run: + +```bash +git switch -c feature/product-maturity +``` + +Expected: `Switched to a new branch 'feature/product-maturity'`. + +- [ ] **Step 3: Run baseline verification** + +Run: + +```bash +python -m pytest -q +pylings --root tests/fixtures/passing_curriculum verify +python -m pylings --version +``` + +Expected: all tests pass, fixture verify exits `0`, version prints the current package version. + +- [ ] **Step 4: Commit baseline marker** + +Run: + +```bash +git commit --allow-empty -m "chore: start product maturity work" +``` + +Expected: one empty commit marking the branch start. + +## Phase 1: Self-Contained Distribution + +### Task 1: Add Packaged Curriculum API + +**Files:** +- Create: `pylings/core/curriculum.py` +- Test: `tests/unit/test_curriculum.py` +- Modify: `pyproject.toml` + +- [ ] **Step 1: Write failing curriculum tests** + +Create `tests/unit/test_curriculum.py`: + +```python +from pathlib import Path + +from pylings.core import curriculum + + +def test_source_root_finds_curriculum_files(): + root = curriculum.source_root() + + assert (root / "info.toml").exists() + assert (root / "exercises").is_dir() + assert (root / "checks").is_dir() + + +def test_init_workspace_copies_curriculum(tmp_path): + target = tmp_path / "workspace" + + result = curriculum.init_workspace(target) + + assert result == target + assert (target / "info.toml").exists() + assert (target / "exercises").is_dir() + assert (target / "checks").is_dir() + assert (target / ".pylings" / "originals").is_dir() + assert (target / ".gitignore").read_text(encoding="utf-8").splitlines() == [ + ".pylings/state.json", + ".pylings_debug.log", + "__pycache__/", + "*.pyc", + ] + + +def test_init_workspace_refuses_non_empty_directory(tmp_path): + target = tmp_path / "workspace" + target.mkdir() + (target / "notes.txt").write_text("keep me", encoding="utf-8") + + try: + curriculum.init_workspace(target) + except curriculum.WorkspaceError as exc: + assert "already exists and is not empty" in str(exc) + else: + raise AssertionError("expected WorkspaceError") + + +def test_update_workspace_preserves_user_exercise_edit(tmp_path): + target = curriculum.init_workspace(tmp_path / "workspace") + exercise = next((target / "exercises").rglob("*.py")) + exercise.write_text("# user edit\n", encoding="utf-8") + + curriculum.update_workspace(target) + + assert exercise.read_text(encoding="utf-8") == "# user edit\n" + assert (target / "checks").is_dir() + assert (target / ".pylings" / "originals" / exercise.name).exists() +``` + +- [ ] **Step 2: Run tests to verify failure** + +Run: + +```bash +python -m pytest tests/unit/test_curriculum.py -q +``` + +Expected: import or attribute failure because `pylings.core.curriculum` does not exist yet. + +- [ ] **Step 3: Implement curriculum helpers** + +Create `pylings/core/curriculum.py`: + +```python +from __future__ import annotations + +import shutil +from importlib import resources +from importlib.abc import Traversable +from pathlib import Path + + +class WorkspaceError(RuntimeError): + """Workspace init or update failed.""" + + +MANAGED_DIRS = ("checks",) +CURRICULUM_DIRS = ("exercises", "checks") +GITIGNORE_LINES = [ + ".pylings/state.json", + ".pylings_debug.log", + "__pycache__/", + "*.pyc", +] + + +def source_root() -> Path: + """Return the curriculum source root for editable and wheel installs.""" + packaged = resources.files("pylings").joinpath("curriculum") + if packaged.joinpath("info.toml").is_file(): + return Path(str(packaged)) + + repo_root = Path(__file__).resolve().parents[2] + if (repo_root / "info.toml").exists(): + return repo_root + + raise WorkspaceError("packaged curriculum not found") + + +def _copy_path(src: Path, dst: Path, *, overwrite: bool) -> None: + if src.is_dir(): + if dst.exists() and overwrite: + shutil.rmtree(dst) + dst.mkdir(parents=True, exist_ok=True) + for child in src.iterdir(): + _copy_path(child, dst / child.name, overwrite=overwrite) + return + + if dst.exists() and not overwrite: + return + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + + +def _write_workspace_gitignore(root: Path) -> None: + (root / ".gitignore").write_text( + "\n".join(GITIGNORE_LINES) + "\n", + encoding="utf-8", + ) + + +def _sync_originals(root: Path, src_root: Path) -> None: + originals = root / ".pylings" / "originals" + if originals.exists(): + shutil.rmtree(originals) + for exercise in (src_root / "exercises").rglob("*.py"): + rel = exercise.relative_to(src_root / "exercises") + target = originals / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(exercise, target) + + +def init_workspace(path: Path, *, force: bool = False) -> Path: + path = path.expanduser().resolve() + if path.exists() and any(path.iterdir()) and not force: + raise WorkspaceError(f"{path} already exists and is not empty") + path.mkdir(parents=True, exist_ok=True) + + src_root = source_root() + _copy_path(src_root / "info.toml", path / "info.toml", overwrite=True) + for dirname in CURRICULUM_DIRS: + _copy_path(src_root / dirname, path / dirname, overwrite=True) + _sync_originals(path, src_root) + _write_workspace_gitignore(path) + return 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") + + src_root = source_root() + _copy_path(src_root / "info.toml", path / "info.toml", overwrite=True) + _copy_path(src_root / "checks", path / "checks", overwrite=True) + _copy_path(src_root / "exercises", path / "exercises", overwrite=False) + _sync_originals(path, src_root) + _write_workspace_gitignore(path) + return path +``` + +- [ ] **Step 4: Include curriculum in built wheels** + +Modify `pyproject.toml`: + +```toml +[tool.hatch.build.targets.wheel.force-include] +"pylings/pylings.tcss" = "pylings/pylings.tcss" +"pylings/docs" = "pylings/docs" +"info.toml" = "pylings/curriculum/info.toml" +"exercises" = "pylings/curriculum/exercises" +"checks" = "pylings/curriculum/checks" +``` + +- [ ] **Step 5: Run curriculum tests** + +Run: + +```bash +python -m pytest tests/unit/test_curriculum.py -q +``` + +Expected: all curriculum tests pass. + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add pyproject.toml pylings/core/curriculum.py tests/unit/test_curriculum.py +git commit -m "feat: package curriculum resources" +``` + +## Phase 2: Workspace Lifecycle CLI + +### Task 2: Add `init` And `update` Commands + +**Files:** +- Modify: `pylings/cli.py` +- Test: `tests/integration/test_cli_workspace.py` + +- [ ] **Step 1: Write failing CLI tests** + +Create `tests/integration/test_cli_workspace.py`: + +```python +from pathlib import Path + +from pylings.cli import main + + +def test_init_command_creates_workspace(tmp_path): + target = tmp_path / "learn-python" + + code = main(["init", "--path", str(target)]) + + assert code == 0 + assert (target / "info.toml").exists() + assert (target / "exercises").is_dir() + assert (target / "checks").is_dir() + + +def test_init_command_requires_force_for_non_empty_directory(tmp_path, capsys): + target = tmp_path / "learn-python" + target.mkdir() + (target / "notes.txt").write_text("keep", encoding="utf-8") + + code = main(["init", "--path", str(target)]) + + assert code == 1 + assert "already exists and is not empty" in capsys.readouterr().err + + +def test_update_command_preserves_user_exercises(tmp_path): + target = tmp_path / "learn-python" + assert main(["init", "--path", str(target)]) == 0 + exercise = next((target / "exercises").rglob("*.py")) + exercise.write_text("# edited\n", encoding="utf-8") + + code = main(["update", "--path", str(target)]) + + assert code == 0 + assert exercise.read_text(encoding="utf-8") == "# edited\n" + assert (target / ".pylings" / "originals").is_dir() +``` + +- [ ] **Step 2: Run tests to verify failure** + +Run: + +```bash +python -m pytest tests/integration/test_cli_workspace.py -q +``` + +Expected: parser rejects `init` and `update`. + +- [ ] **Step 3: Add parser entries** + +Modify `_build_parser()` in `pylings/cli.py`: + +```python +p_init = sub.add_parser("init", help="Create a pylings 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.add_argument("--path", type=Path, default=Path.cwd()) +``` + +- [ ] **Step 4: Add command handlers** + +Add to `pylings/cli.py`: + +```python +def _cmd_init(path: Path, force: bool) -> int: + from pylings.core.curriculum import WorkspaceError, init_workspace + + try: + root = init_workspace(path, force=force) + except WorkspaceError as e: + sys.stderr.write(f"pylings: {e}\n") + return 1 + print(f"initialized: {root}") + return 0 + + +def _cmd_update(path: Path) -> int: + from pylings.core.curriculum import WorkspaceError, update_workspace + + try: + root = update_workspace(path) + except WorkspaceError as e: + sys.stderr.write(f"pylings: {e}\n") + return 1 + print(f"updated: {root}") + return 0 +``` + +Route before `_snapshot_all(args.root)` because these commands do not need an existing manifest: + +```python +if args.command == "init": + return _cmd_init(args.path, args.force) +if args.command == "update": + return _cmd_update(args.path) +``` + +- [ ] **Step 5: Run CLI workspace tests** + +Run: + +```bash +python -m pytest tests/integration/test_cli_workspace.py -q +``` + +Expected: all workspace CLI tests pass. + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add pylings/cli.py tests/integration/test_cli_workspace.py +git commit -m "feat: add workspace init and update commands" +``` + +### Task 3: Make Reset Restore True Pristine Sources + +**Files:** +- Modify: `pylings/core/exercise.py` +- Modify: `pylings/core/manifest.py` +- Modify: `pylings/core/reset.py` +- Modify: `pylings/cli.py` +- Test: `tests/unit/test_reset.py` +- Test: `tests/integration/test_cli_reset.py` + +- [ ] **Step 1: Add failing reset test** + +Append to `tests/unit/test_reset.py`: + +```python +from pylings.core.curriculum import init_workspace +from pylings.core.manifest import load +from pylings.core.reset import restore + + +def test_restore_uses_pristine_originals_not_current_file(tmp_path): + root = init_workspace(tmp_path / "workspace") + manifest = load(root) + exercise = manifest.exercises[0] + pristine = (root / ".pylings" / "originals" / exercise.rel_path.relative_to("exercises")).read_text( + encoding="utf-8" + ) + exercise.path.write_text("# corrupted user edit\n", encoding="utf-8") + + restore(root, exercise) + + assert exercise.path.read_text(encoding="utf-8") == pristine +``` + +- [ ] **Step 2: Run the reset test to verify failure** + +Run: + +```bash +python -m pytest tests/unit/test_reset.py::test_restore_uses_pristine_originals_not_current_file -q +``` + +Expected: failure because `Exercise` has no `rel_path` and reset uses first-seen snapshots. + +- [ ] **Step 3: Add exercise path metadata** + +Modify `pylings/core/exercise.py`: + +```python +@dataclass(frozen=True) +class Exercise: + name: str + path: Path + check_path: Path + topic: str + hint: str + docs: str = "" + root: Path | None = None + rel_path: Path | None = None + check_rel_path: Path | None = None +``` + +Modify the `Exercise(...)` construction in `pylings/core/manifest.py`: + +```python +Exercise( + name=name, + path=abs_path, + check_path=check_abs, + topic=rel_path.parent.name, + hint=entry.get("hint", ""), + docs=entry.get("docs", ""), + root=root, + rel_path=rel_path, + check_rel_path=check_rel, +) +``` + +- [ ] **Step 4: Replace reset restore implementation** + +Modify `pylings/core/reset.py`: + +```python +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") + + +def restore(root: Path, exercise: Exercise) -> None: + """Overwrite the exercise file with its pristine packaged original.""" + original = _original_path(root, exercise) + if not original.exists(): + raise ResetError( + f"no pristine original for {exercise.name!r}. Run 'pylings update' first." + ) + shutil.copy2(original, exercise.path) +``` + +- [ ] **Step 5: Stop snapshotting every command** + +Modify `main()` in `pylings/cli.py` by removing the `_snapshot_all(args.root)` block. Keep `_snapshot_all()` temporarily only if existing tests still import it; otherwise delete it in a follow-up cleanup commit. + +- [ ] **Step 6: Run reset and CLI tests** + +Run: + +```bash +python -m pytest tests/unit/test_reset.py tests/integration/test_cli_reset.py tests/integration/test_cli_workspace.py -q +``` + +Expected: all reset/workspace tests pass. + +- [ ] **Step 7: Commit** + +Run: + +```bash +git add pylings/core/exercise.py pylings/core/manifest.py pylings/core/reset.py pylings/cli.py tests/unit/test_reset.py tests/integration/test_cli_reset.py +git commit -m "fix: restore exercises from pristine originals" +``` + +## Phase 3: Runner And CLI Maturity + +### Task 4: Improve Runner Filenames, cwd, And Debug Output + +**Files:** +- Modify: `pylings/core/runner.py` +- Test: `tests/unit/test_runner.py` + +- [ ] **Step 1: Add failing runner tests** + +Append to `tests/unit/test_runner.py`: + +```python +from pylings.core.exercise import Exercise +from pylings.core.runner import run + + +def test_runner_uses_exercise_directory_for_relative_files(tmp_path): + exercise_path = tmp_path / "exercise.py" + check_path = tmp_path / "check.py" + data_path = tmp_path / "data.txt" + data_path.write_text("pylings\n", encoding="utf-8") + exercise_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") + + result = run(Exercise("relative", exercise_path, check_path, "x", "", root=tmp_path)) + + assert result.passed is True + + +def test_runner_traceback_mentions_real_exercise_file(tmp_path): + exercise_path = tmp_path / "exercise.py" + check_path = tmp_path / "check.py" + exercise_path.write_text("raise RuntimeError('boom')\n", encoding="utf-8") + check_path.write_text("assert True\n", encoding="utf-8") + + result = run(Exercise("traceback", exercise_path, check_path, "x", "", root=tmp_path)) + + assert result.passed is False + assert str(exercise_path) in result.stderr +``` + +- [ ] **Step 2: Run runner tests to verify failure** + +Run: + +```bash +python -m pytest tests/unit/test_runner.py::test_runner_uses_exercise_directory_for_relative_files tests/unit/test_runner.py::test_runner_traceback_mentions_real_exercise_file -q +``` + +Expected: relative file test fails because cwd is not set, traceback test mentions a temp file. + +- [ ] **Step 3: Replace temp-file concatenation with compiled source wrapper** + +Modify `run()` in `pylings/core/runner.py` so the temporary script compiles each source with its real filename: + +```python +runner_src = f""" +from pathlib import Path + +namespace = {{}} +exercise_src = Path({str(exercise.path)!r}).read_text(encoding="utf-8") +check_src = Path({str(exercise.check_path)!r}).read_text(encoding="utf-8") +exec(compile(exercise_src, {str(exercise.path)!r}, "exec"), namespace) +exec(compile(check_src, {str(exercise.check_path)!r}, "exec"), namespace) +""" +``` + +Pass cwd into `subprocess.run()`: + +```python +cwd = exercise.root or exercise.path.parent +proc = subprocess.run( + [sys.executable, tmp.name], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=timeout_s, + env=env, + cwd=cwd, +) +``` + +- [ ] **Step 4: Run runner tests** + +Run: + +```bash +python -m pytest tests/unit/test_runner.py -q +``` + +Expected: all runner tests pass. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add pylings/core/runner.py tests/unit/test_runner.py +git commit -m "fix: run exercises with workspace cwd and real tracebacks" +``` + +### Task 5: Add `dry-run`, `solution`, And `--debug` + +**Files:** +- Modify: `pylings/cli.py` +- Modify: `pylings/core/runner.py` +- Create: `pylings/core/solutions.py` +- Create: `solutions/.keep` +- Test: `tests/integration/test_cli_dry_run.py` +- Test: `tests/integration/test_cli_solution.py` + +- [ ] **Step 1: Write failing CLI tests** + +Create `tests/integration/test_cli_dry_run.py`: + +```python +from pylings.cli import main + + +def test_dry_run_alias_runs_one_exercise(tmp_path): + code = main(["--root", "tests/fixtures/passing_curriculum", "dry-run", "passing1"]) + + assert code == 0 +``` + +Create `tests/integration/test_cli_solution.py`: + +```python +from pathlib import Path + +from pylings.cli import main + + +def test_solution_command_runs_workspace_solution(tmp_path): + root = tmp_path / "workspace" + (root / "solutions").mkdir(parents=True) + (root / "checks").mkdir() + (root / "exercises").mkdir() + (root / "info.toml").write_text( + ''' +format_version = 1 + +[[exercises]] +name = "passing1" +path = "exercises/passing1.py" +hint = "set x" +docs = "https://docs.python.org/3/" +''', + encoding="utf-8", + ) + (root / "exercises" / "passing1.py").write_text("x = 0\n", encoding="utf-8") + (root / "solutions" / "passing1.py").write_text("x = 1\n", encoding="utf-8") + (root / "checks" / "passing1.py").write_text("assert x == 1\n", encoding="utf-8") + + code = main(["--root", str(root), "solution", "passing1"]) + + assert code == 0 +``` + +- [ ] **Step 2: Run tests to verify failure** + +Run: + +```bash +python -m pytest tests/integration/test_cli_dry_run.py tests/integration/test_cli_solution.py -q +``` + +Expected: parser rejects both commands. + +- [ ] **Step 3: Add solutions resolver** + +Create `pylings/core/solutions.py`: + +```python +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path + +from pylings.core.exercise import Exercise + + +class SolutionError(RuntimeError): + """Solution lookup failed.""" + + +def solution_exercise(root: Path, exercise: Exercise) -> Exercise: + path = root / "solutions" / f"{exercise.name}.py" + if not path.exists(): + raise SolutionError(f"no solution for {exercise.name!r}") + return replace(exercise, path=path, rel_path=Path("solutions") / path.name) +``` + +- [ ] **Step 4: Add CLI commands** + +In `_build_parser()`: + +```python +p_dry_run = sub.add_parser("dry-run", help="Run one exercise non-interactively.") +p_dry_run.add_argument("name") + +p_solution = sub.add_parser("solution", aliases=["sol"], help="Run a reference solution.") +p_solution.add_argument("name") + +parser.add_argument("--debug", action="store_true", help="Write debug output to .pylings_debug.log.") +``` + +In `main()`: + +```python +if args.command == "dry-run": + return _cmd_run(args.root, args.name) +if args.command in {"solution", "sol"}: + return _cmd_solution(args.root, args.name) +``` + +Add handler: + +```python +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 + + manifest = load_manifest(root) + try: + ex = solution_exercise(root, manifest.by_name(name)) + except (KeyError, SolutionError) as e: + sys.stderr.write(f"pylings: {e}\n") + return 1 + + result = run_verify(ex) + if result.stdout: + sys.stdout.write(result.stdout) + if result.stderr: + sys.stderr.write(result.stderr) + return 0 if result.passed else 1 +``` + +- [ ] **Step 5: Add debug log behavior** + +In `main()`, after parsing: + +```python +if getattr(args, "debug", False): + (args.root / ".pylings_debug.log").write_text( + f"argv={argv if argv is not None else sys.argv[1:]!r}\n", + encoding="utf-8", + ) +``` + +- [ ] **Step 6: Run command tests** + +Run: + +```bash +python -m pytest tests/integration/test_cli_dry_run.py tests/integration/test_cli_solution.py -q +``` + +Expected: all command tests pass. + +- [ ] **Step 7: Commit** + +Run: + +```bash +git add pylings/cli.py pylings/core/solutions.py tests/integration/test_cli_dry_run.py tests/integration/test_cli_solution.py solutions/.keep +git commit -m "feat: add dry-run and solution commands" +``` + +## Phase 4: Curriculum Content And Learner Experience + +### Task 6: Add Solution Coverage Gate + +**Files:** +- Create: `tests/unit/test_solution_coverage.py` +- Add: `solutions/.py` for every exercise listed in `info.toml` + +- [ ] **Step 1: Add coverage test** + +Create `tests/unit/test_solution_coverage.py`: + +```python +from pathlib import Path + +import tomllib + + +def test_every_exercise_has_solution_file(): + root = Path(__file__).resolve().parents[2] + data = tomllib.loads((root / "info.toml").read_text(encoding="utf-8")) + missing = [ + entry["name"] + for entry in data["exercises"] + if not (root / "solutions" / f"{entry['name']}.py").exists() + ] + + assert missing == [] +``` + +- [ ] **Step 2: Run coverage test** + +Run: + +```bash +python -m pytest tests/unit/test_solution_coverage.py -q +``` + +Expected: failure listing every exercise without a solution. + +- [ ] **Step 3: Add solution files topic by topic** + +For each exercise in `info.toml`, create `solutions/.py` that passes its mirrored check. Validate each topic before moving on: + +```bash +python -m pylings --root . solution variables1 +python -m pylings --root . solution variables2 +python -m pylings --root . solution variables3 +``` + +Expected: every solution command exits `0`. Repeat the command pattern for each exercise name in the topic until the coverage test passes. + +- [ ] **Step 4: Run full solution verification** + +Run: + +```bash +python -m pytest tests/unit/test_solution_coverage.py -q +python -m pytest tests/integration/test_cli_solution.py -q +``` + +Expected: all solution tests pass. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add solutions tests/unit/test_solution_coverage.py +git commit -m "feat: add reference solutions" +``` + +### Task 7: Add External File Watch Mode + +**Files:** +- Modify: `pyproject.toml` +- Create: `pylings/core/watcher.py` +- Modify: `pylings/cli.py` +- Test: `tests/unit/test_watcher.py` + +- [ ] **Step 1: Add watchdog dependency** + +Modify `pyproject.toml`: + +```toml +dependencies = [ + "textual[syntax]>=8.0.0", + "watchdog>=6.0.0", +] +``` + +- [ ] **Step 2: Add watcher smoke test** + +Create `tests/unit/test_watcher.py`: + +```python +from pylings.core.watcher import changed_python_file + + +def test_changed_python_file_accepts_exercise_file(): + assert changed_python_file("exercises/variables/variables1.py") is True + + +def test_changed_python_file_ignores_bytecode(): + assert changed_python_file("exercises/variables/__pycache__/variables1.pyc") is False +``` + +- [ ] **Step 3: Implement watcher filter** + +Create `pylings/core/watcher.py`: + +```python +from __future__ import annotations + + +def changed_python_file(path: str) -> bool: + return path.endswith(".py") and "__pycache__" not in path +``` + +- [ ] **Step 4: Add CLI flag for later TUI integration** + +In `_build_parser()`, add: + +```python +parser.add_argument( + "--watch-files", + action="store_true", + help="Rerun checks when exercise files change outside the TUI.", +) +``` + +Pass `watch_files=getattr(args, "watch_files", False)` into `run_tui()` after adding that optional parameter to `pylings/app.py`. + +- [ ] **Step 5: Run watcher tests** + +Run: + +```bash +python -m pytest tests/unit/test_watcher.py -q +``` + +Expected: watcher filter tests pass. + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add pyproject.toml pylings/core/watcher.py pylings/cli.py pylings/app.py tests/unit/test_watcher.py +git commit -m "feat: add external file watch foundation" +``` + +## Phase 5: Packaging, CI, And Governance + +### Task 8: Add Installed-Wheel Smoke Test + +**Files:** +- Modify: `.github/workflows/ci.yml` +- Create: `tests/integration/test_installed_package.py` + +- [ ] **Step 1: Add local package smoke test** + +Create `tests/integration/test_installed_package.py`: + +```python +from pylings.cli import main + + +def test_installed_package_can_initialize_and_list_workspace(tmp_path): + root = tmp_path / "workspace" + + assert main(["init", "--path", str(root)]) == 0 + assert main(["--root", str(root), "list"]) == 0 +``` + +- [ ] **Step 2: Add CI package commands** + +Modify `.github/workflows/ci.yml`: + +```yaml + - 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 +``` + +- [ ] **Step 3: Run local verification** + +Run: + +```bash +python -m pytest tests/integration/test_installed_package.py -q +python -m build +python -m pip install --force-reinstall dist/*.whl +pylings init --path /tmp/pylings-smoke --force +pylings --root /tmp/pylings-smoke list +``` + +Expected: all commands exit `0`; list prints topic progress. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add .github/workflows/ci.yml tests/integration/test_installed_package.py +git commit -m "ci: verify installed wheel workflow" +``` + +### Task 9: Add Governance Docs + +**Files:** +- Create: `CONTRIBUTING.md` +- Create: `SECURITY.md` +- Create: `CODE_OF_CONDUCT.md` +- Create: `.github/pull_request_template.md` +- Create: `.github/ISSUE_TEMPLATE/bug_report.md` +- Create: `.github/ISSUE_TEMPLATE/feature_request.md` + +- [ ] **Step 1: Add contributor guide** + +Create `CONTRIBUTING.md`: + +````markdown +# Contributing + +## Development Setup + +```bash +pip install -e ".[dev]" +python -m pytest -q +``` + +## Curriculum Changes + +Update `info.toml`, `exercises/`, `checks/`, and `solutions/` together. Exercise and check paths must mirror each other. + +## Pull Requests + +Use focused branches named `feature/` or `fix/`. Include a short description, test output, and screenshots for TUI changes. +```` + +- [ ] **Step 2: Add security policy** + +Create `SECURITY.md`: + +```markdown +# Security Policy + +Report security issues privately through GitHub Security Advisories. Do not open public issues for vulnerabilities. + +Supported versions: latest released `0.x` version and current `main`. +``` + +- [ ] **Step 3: Add code of conduct** + +Create `CODE_OF_CONDUCT.md`: + +```markdown +# Code of Conduct + +Pylings follows the Contributor Covenant Code of Conduct, version 2.1. + +## Our Standards + +Use welcoming and inclusive language, respect different experience levels, accept constructive feedback, and focus on what is best for the community. + +Unacceptable behavior includes harassment, personal attacks, sexualized language or imagery, publishing private information, or sustained disruption of project work. + +## Enforcement + +Report unacceptable behavior through GitHub Security Advisories. Maintainers may remove comments, close issues, block users, or take other appropriate action. +``` + +- [ ] **Step 4: Add PR template** + +Create `.github/pull_request_template.md`: + +```markdown +## Summary + +## Tests + +## Screenshots + +## Checklist + +- [ ] Updated docs when behavior changed +- [ ] Added or updated tests +- [ ] Verified `python -m pytest -q` +``` + +- [ ] **Step 5: Add issue templates** + +Create `.github/ISSUE_TEMPLATE/bug_report.md`: + +```markdown +--- +name: Bug report +about: Report a reproducible problem +labels: bug +--- + +## What happened? + +## Steps to reproduce + +## Expected behavior + +## Environment + +- OS: +- Python: +- Terminal: +- Pylings version: +``` + +Create `.github/ISSUE_TEMPLATE/feature_request.md`: + +```markdown +--- +name: Feature request +about: Suggest an improvement +labels: enhancement +--- + +## Problem + +## Proposed behavior + +## Alternatives considered +``` + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add CONTRIBUTING.md SECURITY.md CODE_OF_CONDUCT.md .github +git commit -m "docs: add contributor governance" +``` + +### Task 10: Fix Versioning And Release Automation + +**Files:** +- Modify: `pyproject.toml` +- Modify: `pylings/cli.py` +- Modify: `Readme.md` +- Modify: `CHANGELOG.md` +- Create: `.github/workflows/publish.yml` + +- [ ] **Step 1: Choose version format** + +Use strict SemVer from the next release onward: + +```toml +version = "0.1.0" +``` + +CLI version should match: + +```python +__version__ = "0.1.0" +``` + +README release tags should use: + +```text +feature/ -> dev -> main -> vMAJOR.MINOR.PATCH +``` + +- [ ] **Step 2: Add publish workflow** + +Create `.github/workflows/publish.yml`: + +```yaml +name: publish + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: python -m pip install build + - run: python -m build + - uses: pypa/gh-action-pypi-publish@release/v1 +``` + +- [ ] **Step 3: Run release verification** + +Run: + +```bash +python -m pytest -q +python -m build +python -m pip install --force-reinstall dist/*.whl +python -m pylings --version +``` + +Expected: tests pass, wheel builds, installed CLI prints `pylings 0.1.0`. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add pyproject.toml pylings/cli.py Readme.md CHANGELOG.md .github/workflows/publish.yml +git commit -m "chore: align release automation with semver" +``` + +## Phase 6: Final Verification And Merge + +### Task 11: Full Verification + +**Files:** +- No code files changed unless verification reveals a bug. + +- [ ] **Step 1: Run full local test suite** + +Run: + +```bash +python -m pytest -q +``` + +Expected: all tests pass. + +- [ ] **Step 2: Verify workspace lifecycle manually** + +Run: + +```bash +rm -rf /tmp/pylings-manual +pylings init --path /tmp/pylings-manual +pylings --root /tmp/pylings-manual list +pylings --root /tmp/pylings-manual run variables1 +pylings --root /tmp/pylings-manual reset variables1 --yes +pylings update --path /tmp/pylings-manual +``` + +Expected: init/list/reset/update exit `0`; `run variables1` may exit `1` if the exercise is intentionally incomplete, but it must print a useful learner-facing failure. + +- [ ] **Step 3: Verify built wheel** + +Run: + +```bash +rm -rf dist /tmp/pylings-wheel +python -m build +python -m pip install --force-reinstall dist/*.whl +pylings init --path /tmp/pylings-wheel +pylings --root /tmp/pylings-wheel list +``` + +Expected: installed wheel can initialize and list a workspace outside the repository. + +- [ ] **Step 4: Merge branch** + +Run: + +```bash +git switch dev +git merge --no-ff feature/product-maturity -m "merge: product maturity workflow" +python -m pytest -q +git switch main +git merge --no-ff dev -m "merge: release product maturity workflow" +``` + +Expected: merges succeed and tests pass on `dev` before merging to `main`. + +- [ ] **Step 5: Tag release** + +For strict SemVer: + +```bash +git tag -a v0.1.0 -m "Release v0.1.0" +git push origin dev main v0.1.0 +``` + +If keeping the existing preview tag style: + +```bash +git tag -a v0.1 -m "Release v0.1" +git push origin dev main v0.1 +``` + +Expected: GitHub Actions runs CI on `main`; release publication should only happen after the GitHub Release is manually published. + +## Self-Review + +- Spec coverage: covers installability, init/update, reset correctness, runner robustness, richer CLI commands, solutions, watcher foundation, CI packaging, governance, and version/release policy. +- Placeholder scan: remaining content-heavy work is represented as a measurable coverage gate, not an undefined task. +- Type consistency: `Exercise.root`, `Exercise.rel_path`, and `Exercise.check_rel_path` are introduced before reset and runner tasks use them. diff --git a/pylings/app.py b/pylings/app.py index 1740e6e..2895cf1 100644 --- a/pylings/app.py +++ b/pylings/app.py @@ -19,6 +19,7 @@ def __init__( root: Path, start_topic: str | None = None, force_picker: bool = False, + watch_files: bool = False, ) -> None: super().__init__() self.root = root @@ -26,6 +27,7 @@ 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()) @@ -70,6 +72,17 @@ def _first_pending_target(self) -> tuple[str, str | None] | None: def run_tui( - root: Path, start_topic: str | None = None, force_picker: bool = False + root: Path, + start_topic: str | None = None, + force_picker: bool = False, + watch_files: bool = False, ) -> int: - return PylingsApp(root, start_topic, force_picker=force_picker).run() or 0 + return ( + PylingsApp( + root, + start_topic, + force_picker=force_picker, + watch_files=watch_files, + ).run() + or 0 + ) diff --git a/pylings/cli.py b/pylings/cli.py index c98b07e..4146b0c 100644 --- a/pylings/cli.py +++ b/pylings/cli.py @@ -5,12 +5,20 @@ import sys from pathlib import Path -__version__ = "0.1" +__version__ = "0.1.0" def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="pylings") parser.add_argument("--version", action="version", version=f"pylings {__version__}") + parser.add_argument( + "--debug", action="store_true", help="Write debug output to .pylings_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, @@ -19,12 +27,29 @@ def _build_parser() -> argparse.ArgumentParser: ) sub = parser.add_subparsers(dest="command") + p_init = sub.add_parser("init", help="Create a pylings 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.add_argument("--path", type=Path, default=Path.cwd()) + sub.add_parser("watch", help="Launch the TUI in watch mode (default).") sub.add_parser("topics", help="Launch the TUI on the topic picker.") p_run = sub.add_parser("run", help="Run a single exercise.") p_run.add_argument("name") + p_dry_run = sub.add_parser("dry-run", help="Run one exercise non-interactively.") + p_dry_run.add_argument("name") + + p_solution = sub.add_parser( + "solution", aliases=["sol"], help="Run a reference solution." + ) + p_solution.add_argument("name") + p_hint = sub.add_parser("hint", help="Print the hint for an exercise.") p_hint.add_argument("name") @@ -46,16 +71,6 @@ def _build_parser() -> argparse.ArgumentParser: return parser -def _snapshot_all(root: Path) -> None: - """Ensure every exercise has a snapshot in .pylings/originals/.""" - from pylings.core.manifest import load as load_manifest - from pylings.core.reset import snapshot - - manifest = load_manifest(root) - for ex in manifest.exercises: - snapshot(root, ex) - - def _resolve_topic(manifest, topic: str): """Return the topic name if valid, else write an error and return None.""" if topic in manifest.topics(): @@ -67,6 +82,30 @@ def _resolve_topic(manifest, topic: str): return None +def _cmd_init(path: Path, force: bool) -> int: + from pylings.core.curriculum import WorkspaceError, init_workspace + + try: + root = init_workspace(path, force=force) + except WorkspaceError as e: + sys.stderr.write(f"pylings: {e}\n") + return 1 + print(f"initialized: {root}") + return 0 + + +def _cmd_update(path: Path) -> int: + from pylings.core.curriculum import WorkspaceError, update_workspace + + try: + root = update_workspace(path) + except WorkspaceError as e: + sys.stderr.write(f"pylings: {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 @@ -162,6 +201,29 @@ def _cmd_run(root: Path, name: str) -> int: 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 + + 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") + return 1 + except SolutionError as e: + sys.stderr.write(f"pylings: {e}\n") + return 1 + + result = run_verify(ex) + if result.stdout: + sys.stdout.write(result.stdout) + if result.stderr: + sys.stderr.write(result.stderr) + return 0 if result.passed else 1 + + 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 @@ -198,11 +260,19 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv if argv is not None else sys.argv[1:]) try: - if args.command not in {"hint"}: + if getattr(args, "debug", False): try: - _snapshot_all(args.root) - except Exception: - pass # snapshot best-effort; real errors surface from subcommands + (args.root / ".pylings_debug.log").write_text( + f"argv={argv if argv is not None else sys.argv[1:]!r}\n", + encoding="utf-8", + ) + except OSError: + pass + + if args.command == "init": + return _cmd_init(args.path, args.force) + if args.command == "update": + return _cmd_update(args.path) if args.command == "verify": return _cmd_verify(args.root, args.topic) @@ -212,6 +282,10 @@ def main(argv: list[str] | None = None) -> int: return _cmd_hint(args.root, args.name) if args.command == "run": return _cmd_run(args.root, args.name) + if args.command == "dry-run": + return _cmd_run(args.root, args.name) + if args.command in {"solution", "sol"}: + return _cmd_solution(args.root, args.name) if args.command == "reset": return _cmd_reset(args.root, args.name, args.yes) @@ -226,6 +300,7 @@ 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/pylings/core/curriculum.py b/pylings/core/curriculum.py new file mode 100644 index 0000000..d59e493 --- /dev/null +++ b/pylings/core/curriculum.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import shutil +from importlib import resources +from pathlib import Path + + +class WorkspaceError(RuntimeError): + """Workspace init or update failed.""" + + +CURRICULUM_DIRS = ("exercises", "checks", "solutions") +GITIGNORE_LINES = [ + ".pylings/state.json", + ".pylings_debug.log", + "__pycache__/", + "*.pyc", +] + + +def source_root() -> Path: + """Return the curriculum source root for editable and wheel installs.""" + packaged = resources.files("pylings").joinpath("curriculum") + if packaged.joinpath("info.toml").is_file(): + return Path(str(packaged)) + + repo_root = Path(__file__).resolve().parents[2] + if (repo_root / "info.toml").exists(): + return repo_root + + raise WorkspaceError("packaged curriculum not found") + + +def _copy_path(src: Path, dst: Path, *, overwrite: bool) -> None: + if src.is_dir(): + if dst.exists() and overwrite: + shutil.rmtree(dst) + dst.mkdir(parents=True, exist_ok=True) + for child in src.iterdir(): + _copy_path(child, dst / child.name, overwrite=overwrite) + return + + if dst.exists() and not overwrite: + return + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + + +def _write_workspace_gitignore(root: Path) -> None: + (root / ".gitignore").write_text( + "\n".join(GITIGNORE_LINES) + "\n", + encoding="utf-8", + ) + + +def _sync_originals(root: Path, src_root: Path) -> None: + originals = root / ".pylings" / "originals" + if originals.exists(): + shutil.rmtree(originals) + for exercise in (src_root / "exercises").rglob("*.py"): + rel = exercise.relative_to(src_root / "exercises") + target = originals / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(exercise, target) + + +def init_workspace(path: Path, *, force: bool = False) -> Path: + path = path.expanduser().resolve() + if path.exists() and any(path.iterdir()) and not force: + raise WorkspaceError(f"{path} already exists and is not empty") + path.mkdir(parents=True, exist_ok=True) + + src_root = source_root() + _copy_path(src_root / "info.toml", path / "info.toml", overwrite=True) + for dirname in CURRICULUM_DIRS: + _copy_path(src_root / dirname, path / dirname, overwrite=True) + _sync_originals(path, src_root) + _write_workspace_gitignore(path) + return 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") + + src_root = source_root() + _copy_path(src_root / "info.toml", path / "info.toml", overwrite=True) + _copy_path(src_root / "checks", path / "checks", overwrite=True) + _copy_path(src_root / "solutions", path / "solutions", overwrite=True) + _copy_path(src_root / "exercises", path / "exercises", overwrite=False) + _sync_originals(path, src_root) + _write_workspace_gitignore(path) + return path diff --git a/pylings/core/exercise.py b/pylings/core/exercise.py index c02095a..9452278 100644 --- a/pylings/core/exercise.py +++ b/pylings/core/exercise.py @@ -12,6 +12,9 @@ class Exercise: topic: str hint: str docs: str = "" + root: Path | None = None + rel_path: Path | None = None + check_rel_path: Path | None = None DONE_MARKER = "# I AM NOT DONE" diff --git a/pylings/core/manifest.py b/pylings/core/manifest.py index 5e96ecb..b9d0454 100644 --- a/pylings/core/manifest.py +++ b/pylings/core/manifest.py @@ -91,6 +91,9 @@ def load(root: Path) -> Manifest: topic=rel_path.parent.name, hint=entry.get("hint", ""), docs=entry.get("docs", ""), + root=root, + rel_path=rel_path, + check_rel_path=check_rel, ) ) diff --git a/pylings/core/reset.py b/pylings/core/reset.py index 7cbd385..8676a10 100644 --- a/pylings/core/reset.py +++ b/pylings/core/reset.py @@ -18,9 +18,15 @@ def _snapshot_path(root: Path, exercise: Exercise) -> Path: return root / ".pylings" / "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") + + def snapshot(root: Path, exercise: Exercise) -> None: """Copy the pristine source into .pylings/originals/ if not already snapshotted.""" - snap = _snapshot_path(root, exercise) + snap = _original_path(root, exercise) if snap.exists(): return snap.parent.mkdir(parents=True, exist_ok=True) @@ -28,10 +34,10 @@ def snapshot(root: Path, exercise: Exercise) -> None: def restore(root: Path, exercise: Exercise) -> None: - """Overwrite the exercise file with its pristine snapshot.""" - snap = _snapshot_path(root, exercise) + """Overwrite the exercise file with its pristine original.""" + snap = _original_path(root, exercise) if not snap.exists(): raise ResetError( - f"no snapshot for {exercise.name!r}. Has the file been seen by pylings yet?" + f"no snapshot for {exercise.name!r}. Run 'pylings update' first." ) shutil.copy2(snap, exercise.path) diff --git a/pylings/core/runner.py b/pylings/core/runner.py index b5b8bb3..7bf7999 100644 --- a/pylings/core/runner.py +++ b/pylings/core/runner.py @@ -20,22 +20,39 @@ def run(exercise: Exercise, timeout_s: float = DEFAULT_TIMEOUT_S) -> RunResult: Never raises. """ start = time.monotonic() + exercise_path = exercise.path.resolve() + check_path = exercise.check_path.resolve() env = { **os.environ, "PYTHONDONTWRITEBYTECODE": "1", "PYTHONIOENCODING": "utf-8", } - exercise_src = exercise.path.read_text(encoding="utf-8") - combined = ( - exercise_src + "\n\n" + exercise.check_path.read_text(encoding="utf-8") + exercise_src = exercise_path.read_text(encoding="utf-8") + runner_src = ( + "import sys\n" + "from pathlib import Path\n" + f"exercise_path = Path({str(exercise_path)!r})\n" + f"check_path = Path({str(check_path)!r})\n" + "sys.path.insert(0, str(Path.cwd()))\n" + "namespace = {\n" + " '__name__': '__main__',\n" + " '__file__': str(exercise_path),\n" + " '__package__': None,\n" + "}\n" + "exercise_src = exercise_path.read_text(encoding='utf-8')\n" + "check_src = check_path.read_text(encoding='utf-8')\n" + "exec(compile(exercise_src, str(exercise_path), 'exec'), namespace)\n" + "namespace['__file__'] = str(check_path)\n" + "exec(compile(check_src, str(check_path), 'exec'), namespace)\n" ) tmp = tempfile.NamedTemporaryFile( mode="w", suffix=".py", delete=False, encoding="utf-8" ) try: - tmp.write(combined) + tmp.write(runner_src) tmp.close() try: + cwd = (exercise.root or exercise_path.parent).resolve() proc = subprocess.run( [sys.executable, tmp.name], capture_output=True, @@ -44,6 +61,7 @@ def run(exercise: Exercise, timeout_s: float = DEFAULT_TIMEOUT_S) -> RunResult: errors="replace", timeout=timeout_s, env=env, + cwd=cwd, ) duration = time.monotonic() - start exit_code = proc.returncode diff --git a/pylings/core/solutions.py b/pylings/core/solutions.py new file mode 100644 index 0000000..1eea088 --- /dev/null +++ b/pylings/core/solutions.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path + +from pylings.core.exercise import Exercise + + +class SolutionError(RuntimeError): + """Solution lookup failed.""" + + +def solution_exercise(root: Path, exercise: Exercise) -> Exercise: + path = root / "solutions" / f"{exercise.name}.py" + if not path.exists(): + raise SolutionError(f"no solution for {exercise.name!r}") + return replace(exercise, path=path, rel_path=Path("solutions") / path.name) diff --git a/pylings/core/watcher.py b/pylings/core/watcher.py new file mode 100644 index 0000000..4ec9099 --- /dev/null +++ b/pylings/core/watcher.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def changed_python_file(path: str) -> bool: + return path.endswith(".py") and "__pycache__" not in path diff --git a/pyproject.toml b/pyproject.toml index 84ed133..5eb17ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "pylings-tui" -version = "0.1" -description = "Rustlings-style interactive Python exercises in a terminal TUI." +name = "python-learnings" +version = "0.1.0" +description = "Python learnings, Rustlings-style, in a terminal TUI." readme = "Readme.md" requires-python = ">=3.11" license = { text = "MIT" } @@ -13,6 +13,7 @@ keywords = [ "python", "python-exercises", "learn-python", + "python-learnings", "beginner-python", "coding-practice", "rustlings", @@ -38,6 +39,7 @@ classifiers = [ ] dependencies = [ "textual[syntax]>=8.0.0", + "watchdog>=6.0.0", ] [project.urls] @@ -61,7 +63,10 @@ packages = ["pylings"] [tool.hatch.build.targets.wheel.force-include] "pylings/pylings.tcss" = "pylings/pylings.tcss" -"pylings/docs" = "pylings/docs" +"info.toml" = "pylings/curriculum/info.toml" +"exercises" = "pylings/curriculum/exercises" +"checks" = "pylings/curriculum/checks" +"solutions" = "pylings/curriculum/solutions" [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/solutions/.keep b/solutions/.keep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/solutions/.keep @@ -0,0 +1 @@ + diff --git a/solutions/_answers.py b/solutions/_answers.py new file mode 100644 index 0000000..de174ea --- /dev/null +++ b/solutions/_answers.py @@ -0,0 +1,1639 @@ +from __future__ import annotations + +import linecache +from pathlib import Path + + +ANSWERS: dict[str, str] = {'async1': 'async def greet():\n return "hello async"\n', + 'async10': 'import asyncio\n' + '\n' + 'async def fetch_name(name):\n' + ' await asyncio.sleep(0)\n' + ' return name.upper()\n' + '\n' + 'async def fetch_all(names):\n' + ' upper_names = await asyncio.gather(*(fetch_name(name) for name in names))\n' + ' return ",".join(upper_names)\n', + 'async2': 'async def get_message():\n' + ' return "ready"\n' + '\n' + 'async def main():\n' + ' return await get_message()\n', + 'async3': 'import asyncio\n' + '\n' + 'async def wake_up():\n' + ' await asyncio.sleep(0)\n' + ' return "awake"\n', + 'async4': 'import asyncio\n' + '\n' + 'async def first():\n' + ' return "a"\n' + '\n' + 'async def second():\n' + ' return "b"\n' + '\n' + 'async def collect():\n' + ' return list(await asyncio.gather(first(), second()))\n', + 'async5': 'import asyncio\n' + '\n' + 'async def compute():\n' + ' return 21 * 2\n' + '\n' + 'async def run_task():\n' + ' task = asyncio.create_task(compute())\n' + ' return await task\n', + 'async6': 'async def numbers():\n' + ' for value in [1, 2, 3]:\n' + ' yield value\n' + '\n' + 'async def collect_numbers():\n' + ' result = []\n' + ' async for value in numbers():\n' + ' result.append(value)\n' + ' return result\n', + 'async7': 'import asyncio\n' + '\n' + 'async def quick():\n' + ' await asyncio.sleep(0)\n' + ' return "done"\n' + '\n' + 'async def with_timeout():\n' + ' return await asyncio.wait_for(quick(), timeout=1)\n', + 'async8': 'import asyncio\n' + '\n' + 'async def answer():\n' + ' return 42\n' + '\n' + 'result = asyncio.run(answer())\n', + 'async9': 'import asyncio\n' + '\n' + 'async def double(value):\n' + ' await asyncio.sleep(0)\n' + ' return value * 2\n' + '\n' + 'async def double_all(values):\n' + ' return list(await asyncio.gather(*(double(value) for value in values)))\n', + 'classes1': 'class Cat:\n pass\n\nmy_cat = Cat()\n', + 'classes10': 'class Circle:\n' + ' pi = 3.14159\n' + '\n' + ' def __init__(self, radius):\n' + ' self.radius = radius\n' + '\n' + ' def area(self):\n' + ' return self.pi * self.radius ** 2\n' + '\n' + 'c1 = Circle(1)\n' + 'c2 = Circle(5)\n', + 'classes11': 'class Temperature:\n' + ' def __init__(self, degrees):\n' + ' self.degrees = degrees\n' + '\n' + ' @classmethod\n' + ' def from_string(cls, s):\n' + ' return cls(float(s[:-1]))\n' + '\n' + 't = Temperature.from_string("36.6C")\n' + 'boiling = Temperature.from_string("100C")\n', + 'classes12': 'class BankAccount:\n' + ' def __init__(self, owner, balance):\n' + ' self.owner = owner\n' + ' self.balance = balance\n' + '\n' + ' def deposit(self, amount):\n' + ' self.balance += amount\n' + '\n' + ' def withdraw(self, amount):\n' + ' if amount > self.balance:\n' + ' raise ValueError("insufficient funds")\n' + ' self.balance -= amount\n' + '\n' + ' def __str__(self):\n' + ' return f"BankAccount({self.owner}, balance={self.balance})"\n' + '\n' + 'account = BankAccount("Alice", 100)\n' + 'account.deposit(50)\n' + 'account.withdraw(30)\n', + 'classes2': 'class Dog:\n' + ' def __init__(self, name):\n' + ' self.name = name\n' + '\n' + 'rex = Dog("Rex")\n', + 'classes3': 'class Point:\n' + ' def __init__(self, x, y):\n' + ' self.x = x\n' + ' self.y = y\n' + '\n' + 'p = Point(3, 7)\n', + 'classes4': 'class Rectangle:\n' + ' def __init__(self, width, height):\n' + ' self.width = width\n' + ' self.height = height\n' + '\n' + ' def area(self):\n' + ' return self.width * self.height\n' + '\n' + 'rect = Rectangle(4, 5)\n', + 'classes5': 'class Counter:\n' + ' def __init__(self):\n' + ' self.count = 0\n' + '\n' + ' def increment(self):\n' + ' self.count += 1\n' + '\n' + 'c = Counter()\n' + 'c.increment()\n' + 'c.increment()\n' + 'c.increment()\n', + 'classes6': 'class Car:\n' + ' def __init__(self, brand, speed=0):\n' + ' self.brand = brand\n' + ' self.speed = speed\n' + '\n' + 'slow = Car("Tesla")\n' + 'fast = Car("Ferrari", 200)\n', + 'classes7': 'class Greeter:\n' + ' def __init__(self, name):\n' + ' self.name = name\n' + '\n' + ' def get_name(self):\n' + ' return self.name\n' + '\n' + ' def greet(self):\n' + ' return f"Hello, {self.get_name()}!"\n' + '\n' + 'g = Greeter("Alice")\n', + 'classes8': 'class Book:\n' + ' def __init__(self, title, author):\n' + ' self.title = title\n' + ' self.author = author\n' + '\n' + ' def __str__(self):\n' + ' return f"Book: {self.title} by {self.author}"\n' + '\n' + 'book = Book("1984", "Orwell")\n', + 'classes9': 'class Color:\n' + ' def __init__(self, r, g, b):\n' + ' self.r = r\n' + ' self.g = g\n' + ' self.b = b\n' + '\n' + ' def __str__(self):\n' + ' return f"rgb({self.r}, {self.g}, {self.b})"\n' + '\n' + ' def __repr__(self):\n' + ' return f"Color(r={self.r}, g={self.g}, b={self.b})"\n' + '\n' + 'c = Color(255, 128, 0)\n', + 'collections1': 'from collections import Counter\n' + 'words = ["apple", "banana", "apple", "cherry", "banana", "apple"]\n' + 'counts = Counter(words)\n', + 'collections10': 'from collections import Counter, defaultdict\n' + 'log = [("api",120), ("web",45), ("api",95), ("db",300), ("web",60), ' + '("api",110), ("db",280), ("web",55), ("api",85), ("db",310), ("web",50), ' + '("api",100)]\n' + 'grouped = defaultdict(list)\n' + 'for category, value in log:\n' + ' grouped[category].append(value)\n' + 'hit_counts = Counter(category for category, _ in log)\n' + 'top_category = hit_counts.most_common(1)[0][0]\n' + 'avg_times = {category: sum(values) / len(values) for category, values in ' + 'grouped.items()}\n', + 'collections2': 'from collections import Counter\n' + 'votes = ["Alice", "Bob", "Alice", "Carol", "Bob", "Alice", "Carol", "Bob", ' + '"Alice", "Carol"]\n' + 'tally = Counter(votes)\n' + 'top2 = tally.most_common(2)\n', + 'collections3': 'from collections import defaultdict\n' + 'text = "mississippi"\n' + 'char_counts = defaultdict(int)\n' + 'for ch in text:\n' + ' char_counts[ch] += 1\n', + 'collections4': 'from collections import defaultdict\n' + 'students = [("Alice", "A"), ("Bob", "B"), ("Carol", "A"), ("Dave", "C"), ("Eve", ' + '"B"), ("Frank", "A")]\n' + 'by_grade = defaultdict(list)\n' + 'for name, grade in students:\n' + ' by_grade[grade].append(name)\n', + 'collections5': 'from collections import namedtuple\n' + 'Point = namedtuple("Point", ["x", "y"])\n' + 'p = Point(3, 4)\n', + 'collections6': 'from collections import namedtuple\n' + 'Card = namedtuple("Card", ["rank", "suit"])\n' + 'card = Card("K", "spades")\n' + 'upgraded = card._replace(rank="A")\n', + 'collections7': 'from collections import deque\n' + 'd = deque()\n' + 'd.appendleft(1)\n' + 'd.append(2)\n' + 'd.appendleft(0)\n' + 'right_val = d.pop()\n' + 'left_val = d.popleft()\n', + 'collections8': 'from collections import deque\n' + 'queue = deque()\n' + 'queue.append("doc1")\n' + 'queue.append("doc2")\n' + 'queue.append("doc3")\n' + 'first = queue.popleft()\n' + 'second = queue.popleft()\n' + 'third = queue.popleft()\n', + 'collections9': 'from collections import Counter\n' + 'monday = Counter({"apple": 5, "banana": 3, "cherry": 2})\n' + 'tuesday = Counter({"apple": 2, "banana": 4, "date": 1})\n' + 'sold = Counter({"apple": 4, "banana": 5, "cherry": 1})\n' + 'combined = monday + tuesday\n' + 'remainder = combined - sold\n', + 'comprehensions1': 'numbers = [x for x in range(5)]\n', + 'comprehensions10': 'words = ["apple", "banana", "cherry", "date"]\n' + 'word_index = {word: idx for idx, word in enumerate(words)}\n', + 'comprehensions2': 'values = [1, 2, 3, 4, 5, 6, 7, 8]\n' + 'evens = [x for x in values if x % 2 == 0]\n', + 'comprehensions3': 'values = [1, 2, 3, 4, 5]\ndoubled = [x * 2 for x in values]\n', + 'comprehensions4': 'rows = [0, 1]\n' + 'cols = [0, 1, 2]\n' + 'pairs = [(row, col) for row in rows for col in cols]\n', + 'comprehensions5': 'numbers = [1, 2, 3, 4, 5]\nsquared = {n: n * n for n in numbers}\n', + 'comprehensions6': 'words = ["apple", "bat", "car", "dog", "elephant", "fig"]\n' + 'unique_lengths = {len(word) for word in words}\n', + 'comprehensions7': 'sentence = "list comprehensions are quite powerful"\n' + 'vowels = [c for c in sentence if c in "aeiou"]\n', + 'comprehensions8': 'values = [1, 2, 3, 4, 5, 6]\n' + 'labels = ["even" if x % 2 == 0 else "odd" for x in values]\n', + 'comprehensions9': 'matrix = [[1,2,3], [4,5,6], [7,8,9]]\n' + 'flat = [item for row in matrix for item in row]\n', + 'conditionals1': 'is_sunny = True\n' + 'message = ""\n' + 'if is_sunny:\n' + ' message = "Let\'s go outside!"\n', + 'conditionals10': 'def classify(n):\n' + ' if n > 100:\n' + ' return "large positive"\n' + ' elif n > 0:\n' + ' return "small positive"\n' + ' elif n == 0:\n' + ' return "zero"\n' + ' elif n >= -100:\n' + ' return "small negative"\n' + ' return "large negative"\n', + 'conditionals2': 'def sign(n):\n' + ' if n > 0:\n' + ' return "positive"\n' + ' return "non-positive"\n', + 'conditionals3': 'def grade(score):\n' + ' if score >= 90:\n' + ' return "A"\n' + ' elif score >= 80:\n' + ' return "B"\n' + ' elif score >= 70:\n' + ' return "C"\n' + ' return "F"\n', + 'conditionals4': 'def compare(a, b):\n' + ' if a < b:\n' + ' return "less"\n' + ' elif a == b:\n' + ' return "equal"\n' + ' return "greater"\n', + 'conditionals5': 'def can_ride(height, has_ticket):\n' + ' return height >= 120 and has_ticket\n' + '\n' + 'def gets_discount(is_student, is_senior):\n' + ' return is_student or is_senior\n', + 'conditionals6': 'def is_closed(is_open):\n' + ' return not is_open\n' + '\n' + 'def neither_zero(a, b):\n' + ' return not (a == 0 or b == 0)\n', + 'conditionals7': 'def ticket_price(age, is_member):\n' + ' if age < 5:\n' + ' return 0\n' + ' else:\n' + ' if is_member:\n' + ' return 8\n' + ' return 12\n', + 'conditionals8': 'def describe(value):\n' + ' if value:\n' + ' return "has content"\n' + ' return "empty"\n', + 'conditionals9': 'def absolute(n):\n return n if n >= 0 else -n\n', + 'context_managers1': 'import tempfile\n' + 'data_path = tempfile.mktemp()\n' + 'with open(data_path, "w") as f:\n' + ' f.write("hello, context!")\n', + 'context_managers2': 'import tempfile\n' + 'data_path = tempfile.mktemp()\n' + 'with open(data_path, "w") as f:\n' + ' f.write("closed check")\n', + 'context_managers3': 'class Timer:\n' + ' def __init__(self):\n' + ' self.finished = False\n' + '\n' + ' def __enter__(self):\n' + ' return self\n' + '\n' + ' def __exit__(self, exc_type, exc_val, exc_tb):\n' + ' self.finished = True\n' + '\n' + 't = Timer()\n' + 'with t:\n' + ' pass\n', + 'context_managers4': 'class ManagedList:\n' + ' def __init__(self):\n' + ' self.items = []\n' + ' self.log = []\n' + '\n' + ' def __enter__(self):\n' + ' return self\n' + '\n' + ' def __exit__(self, exc_type, exc_val, exc_tb):\n' + ' self.log.append("done")\n' + '\n' + 'ml = ManagedList()\n' + 'with ml as resource:\n' + ' resource.items.append(1)\n' + ' resource.items.append(2)\n', + 'context_managers5': 'class DatabaseConnection:\n' + ' def __init__(self, dsn):\n' + ' self.dsn = dsn\n' + ' self.open = True\n' + ' self.connection = {"dsn": dsn, "active": True}\n' + '\n' + ' def __enter__(self):\n' + ' return self.connection\n' + '\n' + ' def __exit__(self, exc_type, exc_val, exc_tb):\n' + ' self.open = False\n' + '\n' + 'db = DatabaseConnection("sqlite:///:memory:")\n' + 'with db as conn:\n' + ' result = conn\n', + 'context_managers6': 'from contextlib import contextmanager\n' + '\n' + '@contextmanager\n' + 'def managed_counter():\n' + ' counter = []\n' + ' try:\n' + ' yield counter\n' + ' finally:\n' + ' counter.append("finished")\n' + '\n' + 'with managed_counter() as counter:\n' + ' counter.append(1)\n' + ' counter.append(2)\n', + 'context_managers7': 'class SilentZeroDivision:\n' + ' def __enter__(self):\n' + ' return self\n' + '\n' + ' def __exit__(self, exc_type, exc_val, exc_tb):\n' + ' if exc_type is ZeroDivisionError:\n' + ' return True\n' + ' return None\n' + '\n' + 'result = "not set"\n' + 'with SilentZeroDivision():\n' + ' result = 1 / 0\n' + ' result = "unreachable"\n', + 'context_managers8': 'from contextlib import contextmanager\n' + '\n' + 'class Resource:\n' + ' def __init__(self):\n' + ' self.acquired = False\n' + ' self.release_log = []\n' + '\n' + ' def __enter__(self):\n' + ' self.acquired = True\n' + ' return self\n' + '\n' + ' def __exit__(self, exc_type, exc_val, exc_tb):\n' + ' self.acquired = False\n' + ' self.release_log.append("released")\n' + '\n' + '@contextmanager\n' + 'def ManagedPool(resource):\n' + ' resource.__enter__()\n' + ' try:\n' + ' yield resource\n' + ' finally:\n' + ' resource.__exit__(None, None, None)\n' + '\n' + 'r = Resource()\n' + 'with ManagedPool(r) as res:\n' + ' in_block_acquired = res.acquired\n', + 'dataclasses1': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Point:\n' + ' x: int\n' + ' y: int\n' + '\n' + 'origin = Point(0, 0)\n', + 'dataclasses2': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Color:\n' + ' red: int\n' + ' green: int\n' + ' blue: int\n' + '\n' + 'orange = Color(255, 128, 0)\n' + 'description = f"R={orange.red} G={orange.green} B={orange.blue}"\n', + 'dataclasses3': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Measurement:\n' + ' value: float\n' + ' unit: str = "m/s\\u00b2"\n' + '\n' + 'gravity = Measurement(9.8)\n' + 'named = Measurement(100.0, "km/h")\n', + 'dataclasses4': 'from dataclasses import dataclass, field\n' + '\n' + '@dataclass\n' + 'class Basket:\n' + ' label: str\n' + ' items: list = field(default_factory=list)\n' + '\n' + 'b1 = Basket("fruit")\n' + 'b2 = Basket("veggies")\n' + 'b1.items.append("apple")\n', + 'dataclasses5': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Vector3:\n' + ' x: float\n' + ' y: float\n' + ' z: float\n' + '\n' + 'v = Vector3(1.0, 2.0, 3.0)\n', + 'dataclasses6': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Book:\n' + ' title: str\n' + ' pages: int\n' + '\n' + 'novel = Book("Dune", 412)\n' + 'copy = Book("Dune", 412)\n' + 'other = Book("Foundation", 255)\n', + 'dataclasses7': 'from dataclasses import dataclass\n' + '\n' + '@dataclass(frozen=True)\n' + 'class Config:\n' + ' host: str\n' + ' port: int\n' + '\n' + 'cfg = Config("localhost", 8080)\n' + 'try:\n' + ' cfg.port = 9090\n' + ' mutation_raised = False\n' + 'except Exception:\n' + ' mutation_raised = True\n', + 'dataclasses8': 'from dataclasses import dataclass\n' + '\n' + '@dataclass\n' + 'class Rectangle:\n' + ' width: float\n' + ' height: float\n' + '\n' + ' def area(self) -> float:\n' + ' return self.width * self.height\n' + '\n' + ' def perimeter(self) -> float:\n' + ' return 2 * (self.width + self.height)\n' + '\n' + 'rect = Rectangle(4.0, 3.0)\n' + 'square = Rectangle(5.0, 5.0)\n', + 'datetime1': 'from datetime import date\nlaunch_day = date(2026, 5, 23)\n', + 'datetime2': 'from datetime import date, timedelta\n' + 'start = date(2026, 5, 23)\n' + 'next_week = start + timedelta(days=7)\n', + 'datetime3': 'from datetime import datetime\n' + 'raw = "2026-05-23 09:30"\n' + 'meeting = datetime.strptime(raw, "%Y-%m-%d %H:%M")\n', + 'datetime4': 'from datetime import date\n' + 'day = date(2026, 5, 23)\n' + 'formatted = day.strftime("%d/%m/%Y")\n', + 'datetime5': 'from datetime import date\n' + 'today = date(2026, 5, 23)\n' + 'due = date(2026, 6, 1)\n' + 'is_future = due > today\n', + 'datetime6': 'from datetime import date\n' + 'start = date(2026, 5, 1)\n' + 'end = date(2026, 5, 23)\n' + 'days_between = (end - start).days\n', + 'datetime7': 'from datetime import date, datetime, time\n' + 'day = date(2026, 5, 23)\n' + 'clock = time(14, 45)\n' + 'appointment = datetime.combine(day, clock)\n', + 'datetime8': 'from datetime import date\n' + 'raw_dates = ["2026-05-23", "2024-01-01", "2025-12-31"]\n' + 'parsed = [date.fromisoformat(value) for value in raw_dates]\n' + 'earliest = min(parsed)\n' + 'latest = max(parsed)\n', + 'decorators1': 'def call_it(func):\n' + ' return func()\n' + '\n' + 'def say_hello():\n' + ' return "hello"\n' + '\n' + 'def give_seven():\n' + ' return 7\n', + 'decorators10': 'import functools\n' + '\n' + 'def memoize(func):\n' + ' @functools.wraps(func)\n' + ' def wrapper(*args):\n' + ' if args not in wrapper.cache:\n' + ' wrapper.cache[args] = func(*args)\n' + ' return wrapper.cache[args]\n' + ' wrapper.cache = {}\n' + ' return wrapper\n' + '\n' + 'call_count = 0\n' + '\n' + '@memoize\n' + 'def slow_square(n):\n' + ' global call_count\n' + ' call_count += 1\n' + ' return n * n\n', + 'decorators2': 'def shout(func):\n' + ' def wrapper():\n' + ' result = func()\n' + ' return result.upper()\n' + ' return wrapper\n' + '\n' + 'def greet():\n' + ' return "hello"\n' + '\n' + 'loud_greet = shout(greet)\n', + 'decorators3': 'def stamp(func):\n' + ' def wrapper():\n' + ' return "[STAMPED] " + func()\n' + ' return wrapper\n' + '\n' + '@stamp\n' + 'def get_message():\n' + ' return "hello"\n', + 'decorators4': 'def double_result(func):\n' + ' def wrapper():\n' + ' return func() * 2\n' + ' return wrapper\n' + '\n' + '@double_result\n' + 'def get_five():\n' + ' return 5\n' + '\n' + '@double_result\n' + 'def get_ten():\n' + ' return 10\n', + 'decorators5': 'def log_call(func):\n' + ' def wrapper(*args, **kwargs):\n' + ' print(f"calling {func.__name__}")\n' + ' return func(*args, **kwargs)\n' + ' return wrapper\n' + '\n' + '@log_call\n' + 'def add(a, b):\n' + ' return a + b\n' + '\n' + '@log_call\n' + 'def greet(name, greeting="Hello"):\n' + ' return f"{greeting}, {name}!"\n', + 'decorators6': 'import functools\n' + '\n' + 'def preserve(func):\n' + ' @functools.wraps(func)\n' + ' def wrapper(*args, **kwargs):\n' + ' return func(*args, **kwargs)\n' + ' return wrapper\n' + '\n' + '@preserve\n' + 'def compute(x, y):\n' + ' """Return the sum of x and y."""\n' + ' return x + y\n', + 'decorators7': 'import functools\n' + '\n' + 'def count_calls(func):\n' + ' @functools.wraps(func)\n' + ' def wrapper(*args, **kwargs):\n' + ' wrapper.call_count += 1\n' + ' return func(*args, **kwargs)\n' + ' wrapper.call_count = 0\n' + ' return wrapper\n' + '\n' + '@count_calls\n' + 'def add(a, b):\n' + ' return a + b\n', + 'decorators8': 'import functools\n' + '\n' + 'def repeat(n):\n' + ' def decorator(func):\n' + ' @functools.wraps(func)\n' + ' def wrapper(*args, **kwargs):\n' + ' result = None\n' + ' for _ in range(n):\n' + ' result = func(*args, **kwargs)\n' + ' return result\n' + ' return wrapper\n' + ' return decorator\n' + '\n' + '@repeat(3)\n' + 'def say(word):\n' + ' return word\n' + '\n' + '@repeat(1)\n' + 'def identity(x):\n' + ' return x\n', + 'decorators9': 'def add_a(func):\n' + ' def wrapper():\n' + ' return "[A]" + func()\n' + ' return wrapper\n' + '\n' + 'def add_b(func):\n' + ' def wrapper():\n' + ' return "[B]" + func()\n' + ' return wrapper\n' + '\n' + '@add_b\n' + '@add_a\n' + 'def get_value():\n' + ' return "42"\n', + 'dictionaries1': 'ages = {"alice": 30, "bob": 25}\n', + 'dictionaries10': 'words = ["the", "cat", "sat", "on", "the", "mat", "and", "the", "cat", "hat"]\n' + 'freq = {}\n' + 'for word in words:\n' + ' freq[word] = freq.get(word, 0) + 1\n', + 'dictionaries2': 'capitals = {"france": "Paris", "germany": "Berlin", "japan": "Tokyo"}\n' + 'france_capital = capitals["france"]\n', + 'dictionaries3': 'scores = {"alice": 75, "bob": 60, "carol": 82}\n' + 'scores["diana"] = 91\n' + 'scores["alice"] = 88\n', + 'dictionaries4': 'basket = {"apple": 5, "banana": 3, "cherry": 12, "date": 7}\n' + 'del basket["banana"]\n' + 'cherry_count = basket.pop("cherry")\n', + 'dictionaries5': 'inventory = {"pen": 10, "notebook": 5, "eraser": 8}\n' + 'all_items = list(inventory.keys())\n' + 'all_counts = list(inventory.values())\n' + 'all_pairs = list(inventory.items())\n', + 'dictionaries6': 'config = {"host": "localhost", "port": 8080, "timeout": 60}\n' + 'timeout = config.get("timeout", 30)\n' + 'retries = config.get("retries", 3)\n' + 'verbose = config.get("verbose", False)\n', + 'dictionaries7': 'prices = {"apple": 1.0, "milk": 1.5, "bread": 2.5}\n' + 'discounted = {}\n' + 'for item, price in prices.items():\n' + ' discounted[item] = round(price * 0.9, 2)\n', + 'dictionaries8': 'permissions = {"read": True, "write": False}\n' + 'can_read = "read" in permissions\n' + 'can_write = "write" in permissions\n' + 'can_delete = "delete" in permissions\n', + 'dictionaries9': 'school = {\n' + ' "students": {"alice": {"grade": "A", "age": 20}, "bob": {"grade": "B", ' + '"age": 22}},\n' + ' "courses": {"math": {"room": 101, "credits": 3}, "english": {"room": 205, ' + '"credits": 3}},\n' + ' "meta": {"enrollment": 350, "founded": 1990},\n' + '}\n' + 'alice_grade = school["students"]["alice"]["grade"]\n' + 'math_room = school["courses"]["math"]["room"]\n' + 'num_students = school["meta"]["enrollment"]\n', + 'enums1': 'from enum import Enum\n' + '\n' + 'class Color(Enum):\n' + ' RED = "red"\n' + ' BLUE = "blue"\n' + '\n' + 'favorite = Color.RED\n', + 'enums2': 'from enum import Enum, auto\n' + '\n' + 'class Status(Enum):\n' + ' TODO = 1\n' + ' DOING = 2\n' + ' DONE = 3\n', + 'enums3': 'from enum import Enum\n' + '\n' + 'class Priority(Enum):\n' + ' LOW = 1\n' + ' MEDIUM = 3\n' + ' HIGH = 5\n' + '\n' + 'selected = Priority.HIGH\n' + 'selected_name = selected.name\n' + 'selected_value = selected.value\n', + 'enums4': 'from enum import Enum\n' + '\n' + 'class Size(Enum):\n' + ' SMALL = "S"\n' + ' MEDIUM = "M"\n' + ' LARGE = "L"\n' + '\n' + 'size_codes = [size.value for size in Size]\n', + 'enums5': 'from enum import Enum, auto\n' + '\n' + 'class TicketState(Enum):\n' + ' OPEN = auto()\n' + ' CLOSED = auto()\n' + ' CANCELLED = auto()\n' + '\n' + 'def is_terminal(state):\n' + ' return state in {TicketState.CLOSED, TicketState.CANCELLED}\n', + 'enums6': 'from enum import Enum\n' + '\n' + 'class Direction(Enum):\n' + ' NORTH = (0, -1)\n' + ' SOUTH = (0, 1)\n' + ' EAST = (1, 0)\n' + ' WEST = (-1, 0)\n' + '\n' + ' def delta(self):\n' + ' return self.value\n', + 'exceptions1': 'def safe_divide(a, b):\n' + ' try:\n' + ' return a / b\n' + ' except ZeroDivisionError:\n' + ' return "error"\n', + 'exceptions10': 'def validate_username(name):\n' + ' if not isinstance(name, str):\n' + ' raise TypeError("username must be a str")\n' + ' if len(name) < 3 or len(name) > 20:\n' + ' raise ValueError("username must be 3-20 characters")\n' + ' if not name.isalnum():\n' + ' raise ValueError("username must be alphanumeric")\n' + ' return name.lower()\n', + 'exceptions2': 'def parse_number(s):\n' + ' try:\n' + ' return int(s)\n' + ' except ValueError:\n' + ' return -1\n', + 'exceptions3': 'def describe_error(s):\n' + ' try:\n' + ' return float(s)\n' + ' except ValueError as e:\n' + ' return str(e)\n', + 'exceptions4': 'def load_value(data, key):\n' + ' try:\n' + ' value = data[key]\n' + ' except KeyError:\n' + ' return "missing"\n' + ' else:\n' + ' return value * 2\n', + 'exceptions5': 'log = []\n' + '\n' + 'def read_first(items):\n' + ' try:\n' + ' return items[0]\n' + ' except IndexError:\n' + ' return None\n' + ' finally:\n' + ' log.append("done")\n', + 'exceptions6': 'def require_positive(n):\n' + ' if n <= 0:\n' + ' raise ValueError\n' + ' return n\n', + 'exceptions7': 'def check_age(age):\n' + ' if age < 0 or age > 120:\n' + ' raise ValueError("age must be between 0 and 120")\n' + ' return age\n', + 'exceptions8': 'def classify_error(func):\n' + ' try:\n' + ' func()\n' + ' return "ok"\n' + ' except ValueError:\n' + ' return "value error"\n' + ' except TypeError:\n' + ' return "type error"\n' + ' except Exception:\n' + ' return "other error"\n', + 'exceptions9': 'class InsufficientFundsError(Exception):\n' + ' pass\n' + '\n' + 'def withdraw(balance, amount):\n' + ' if amount > balance:\n' + ' raise InsufficientFundsError(f"cannot withdraw {amount} from balance of ' + '{balance}")\n' + ' return balance - amount\n', + 'file_io1': 'import tempfile\n' + 'data_path = tempfile.mktemp()\n' + 'with open(data_path, "w") as f:\n' + ' f.write("hello, file!")\n', + 'file_io10': 'import tempfile\n' + 'src_path = tempfile.mktemp()\n' + 'dst_path = tempfile.mktemp()\n' + 'with open(src_path, "w") as f:\n' + ' f.write("hello world\\npython is fun\\nfile io rocks\\n")\n' + 'with open(src_path) as src:\n' + ' with open(dst_path, "w") as dst:\n' + ' for line in src:\n' + ' dst.write(line.upper())\n', + 'file_io2': 'import tempfile\n' + 'note_path = tempfile.mktemp()\n' + 'with open(note_path, "w") as f:\n' + ' f.write("remember")\n' + 'with open(note_path) as f:\n' + ' contents = f.read()\n', + 'file_io3': 'import tempfile\n' + 'log_path = tempfile.mktemp()\n' + 'with open(log_path, "w") as fh:\n' + ' fh.write("first entry")\n', + 'file_io4': 'import tempfile\n' + 'poem_path = tempfile.mktemp()\n' + 'with open(poem_path, "w") as f:\n' + ' f.write("roses are red\\nviolets are blue\\npython is great\\n")\n' + 'with open(poem_path) as f:\n' + ' lines = f.readlines()\n', + 'file_io5': 'import tempfile\n' + 'journal_path = tempfile.mktemp()\n' + 'with open(journal_path, "w") as f:\n' + ' f.write("day 1: sunny\\n")\n' + 'with open(journal_path, "a") as fh:\n' + ' fh.write("day 2: rainy\\n")\n', + 'file_io6': 'import tempfile\n' + 'numbers_path = tempfile.mktemp()\n' + 'with open(numbers_path, "w") as f:\n' + ' f.write("10\\n20\\n30\\n40\\n50\\n")\n' + 'total = 0\n' + 'with open(numbers_path) as fh:\n' + ' for line in fh:\n' + ' total += int(line.strip())\n', + 'file_io7': 'import tempfile\n' + 'words_path = tempfile.mktemp()\n' + 'words = ["alpha", "beta", "gamma"]\n' + 'with open(words_path, "w") as fh:\n' + ' for word in words:\n' + ' fh.write(word + "\\n")\n', + 'file_io8': 'import tempfile\n' + 'source_path = tempfile.mktemp()\n' + 'with open(source_path, "w") as f:\n' + ' f.write(" Hello, World! ")\n' + 'with open(source_path) as fh:\n' + ' result = fh.read().strip()\n', + 'file_io9': 'import tempfile\n' + 'essay_path = tempfile.mktemp()\n' + 'with open(essay_path, "w") as f:\n' + ' f.write("the quick brown fox jumps over the lazy dog")\n' + 'with open(essay_path) as fh:\n' + ' word_count = len(fh.read().split())\n', + 'functional1': 'square = lambda x: x * x\n', + 'functional10': 'from functools import reduce\n' + 'scores = [1, 3, 6, 2, 8, 4]\n' + 'doubled = list(map(lambda x: x * 2, scores))\n' + 'above_ten = list(filter(lambda x: x > 10, doubled))\n' + 'result = reduce(lambda a, b: a + b, above_ten)\n', + 'functional2': 'absolute = lambda x: x if x >= 0 else -x\n', + 'functional3': 'numbers = [1, 2, 3, 4, 5]\ndoubled = list(map(lambda x: x * 2, numbers))\n', + 'functional4': 'numbers = [1,2,3,4,5,6,7,8,9,10]\n' + 'evens = list(filter(lambda x: x % 2 == 0, numbers))\n', + 'functional5': 'words = ["banana", "fig", "apple", "kiwi", "date"]\n' + 'by_length = sorted(words, key=lambda word: len(word))\n', + 'functional6': 'def apply_twice(func, value):\n' + ' return func(func(value))\n' + '\n' + 'triple = lambda x: x * 3\n' + 'add_ten = lambda x: x + 10\n', + 'functional7': 'def make_multiplier(n):\n return lambda x: x * n\n', + 'functional8': 'from functools import reduce\n' + 'numbers = [1, 2, 3, 4, 5]\n' + 'product = reduce(lambda a, b: a * b, numbers)\n', + 'functional9': 'numbers = [3, -1, 4, 1, 5]\n' + 'positives = [2, 7, 11, 5]\n' + 'has_negative = any(x < 0 for x in numbers)\n' + 'all_positive = all(x > 0 for x in positives)\n', + 'functions1': 'def average(a, b):\n return (a + b) / 2\n', + 'functions10': 'words = ["racecar", "hello", "level", "world", "madam", "python", "noon"]\n' + '\n' + 'def is_palindrome(s):\n' + ' s = s.lower()\n' + ' return s == s[::-1]\n' + '\n' + 'palindromes = [w for w in words if is_palindrome(w)]\n', + 'functions2': 'def greet():\n return "hello"\n\nmessage = greet()\n', + 'functions3': 'def double(n):\n return n * 2\n', + 'functions4': 'def square(n):\n return n * n\n', + 'functions5': 'def make_greeting(name, greeting="Hello"):\n return f"{greeting}, {name}!"\n', + 'functions6': 'def describe_pet(name, animal_type):\n' + ' return f"My {animal_type} is named {name}."\n' + '\n' + 'description = describe_pet(animal_type="dog", name="Buddy")\n', + 'functions7': 'def total(*args):\n return sum(args)\n', + 'functions8': 'def build_profile(**kwargs):\n return dict(kwargs)\n', + 'functions9': 'numbers = [3, 1, 4, 1, 5, 9, 2, 6]\n' + '\n' + 'def min_max(nums):\n' + ' return min(nums), max(nums)\n' + '\n' + 'lo, hi = min_max(numbers)\n', + 'generators1': 'def count_up_to(n):\n for i in range(1, n + 1):\n yield i\n', + 'generators10': 'def running_total(numbers):\n' + ' total = 0\n' + ' for n in numbers:\n' + ' total += n\n' + ' yield total\n', + 'generators2': 'def squares(n):\n' + ' for i in range(1, n + 1):\n' + ' yield i * i\n' + '\n' + 'result = list(squares(5))\n', + 'generators3': 'def evens_up_to(n):\n' + ' for i in range(1, n + 1):\n' + ' if i % 2 == 0:\n' + ' yield i\n' + '\n' + 'collected = []\n' + 'for value in evens_up_to(8):\n' + ' collected.append(value)\n', + 'generators4': 'cube_gen = (i ** 3 for i in range(1, 6))\n', + 'generators5': 'def letters():\n' + ' yield "a"\n' + ' yield "b"\n' + ' yield "c"\n' + '\n' + 'gen = letters()\n' + 'first = next(gen)\n' + 'second = next(gen)\n' + 'third = next(gen)\n', + 'generators6': 'import itertools\n' + '\n' + 'def natural_numbers():\n' + ' n = 1\n' + ' while True:\n' + ' yield n\n' + ' n += 1\n' + '\n' + 'first_ten = list(itertools.islice(natural_numbers(), 10))\n', + 'generators7': 'def fibonacci():\n' + ' a, b = 0, 1\n' + ' while True:\n' + ' yield a\n' + ' a, b = b, a + b\n', + 'generators8': 'class Countdown:\n' + ' def __init__(self, start):\n' + ' self.start = start\n' + '\n' + ' def __iter__(self):\n' + ' for value in range(self.start, 0, -1):\n' + ' yield value\n', + 'generators9': 'class Counter:\n' + ' def __init__(self, stop):\n' + ' self.stop = stop\n' + ' self.current = 0\n' + '\n' + ' def __iter__(self):\n' + ' return self\n' + '\n' + ' def __next__(self):\n' + ' if self.current >= self.stop:\n' + ' raise StopIteration\n' + ' value = self.current\n' + ' self.current += 1\n' + ' return value\n', + 'itertools1': 'from itertools import chain\n' + 'letters = ["a", "b"]\n' + 'numbers = [1, 2, 3]\n' + 'combined = list(chain(letters, numbers))\n', + 'itertools2': 'from itertools import count, islice\nfirst_four = list(islice(count(10), 4))\n', + 'itertools3': 'from itertools import product\n' + 'xs = [0, 1]\n' + 'ys = ["low", "high"]\n' + 'coordinates = list(product(xs, ys))\n', + 'itertools4': 'from itertools import groupby\n' + 'rows = [("fruit", "apple"), ("fruit", "pear"), ("veg", "carrot")]\n' + 'grouped = {category: [name for _, name in group] for category, group in ' + 'groupby(rows, key=lambda row: row[0])}\n', + 'itertools5': 'from itertools import accumulate\n' + 'values = [3, 4, 5, 6]\n' + 'running_totals = list(accumulate(values))\n', + 'itertools6': 'from itertools import cycle, islice\n' + 'colors = ["red", "blue"]\n' + 'repeated = list(islice(cycle(colors), 6))\n', + 'itertools7': 'from itertools import zip_longest\n' + 'left = ["a", "b", "c"]\n' + 'right = [1]\n' + 'pairs = list(zip_longest(left, right, fillvalue="?"))\n', + 'itertools8': 'from itertools import pairwise\n' + 'batches = [[1, 2], [3], [4, 5]]\n' + 'flattened = [item for batch in batches for item in batch]\n' + 'adjacent_pairs = list(pairwise(flattened))\n', + 'json1': 'import json\n' + 'raw = \'{"name": "Ada", "language": "Python"}\'\n' + 'data = json.loads(raw)\n' + 'name = data["name"]\n', + 'json2': 'import json\npayload = {"b": 2, "a": 1}\ntext = json.dumps(payload, sort_keys=True)\n', + 'json3': 'import json\n' + 'raw = \'{"user": {"name": "Grace", "address": {"city": "London"}}}\'\n' + 'profile = json.loads(raw)\n' + 'city = profile["user"]["address"]["city"]\n', + 'json4': 'import io\n' + 'import json\n' + 'payload = {"ok": True, "count": 3}\n' + 'buffer = io.StringIO()\n' + 'json.dump(payload, buffer)\n' + 'buffer.seek(0)\n' + 'restored = json.load(buffer)\n', + 'json5': 'import json\n' + 'data = json.loads(\'{"name": "Pylings"}\')\n' + 'timezone = data.get("timezone", "UTC")\n', + 'json6': 'import json\n' + 'raw = \'[{"name": "Ada", "active": true}, {"name": "Lin", "active": false}, {"name": ' + '"Guido", "active": true}]\'\n' + 'users = json.loads(raw)\n' + 'active_names = [user["name"] for user in users if user["active"]]\n', + 'json7': 'import json\n' + 'settings = json.loads(\'{"theme": "light", "font_size": 12}\')\n' + 'settings["theme"] = "dark"\n' + 'encoded = json.dumps(settings, sort_keys=True)\n', + 'json8': 'import json\n' + 'original = {"project": "pylings", "tags": ["python", "practice"], "meta": {"level": ' + '3}}\n' + 'encoded = json.dumps(original, sort_keys=True)\n' + 'decoded = json.loads(encoded)\n' + 'tag_count = len(decoded["tags"])\n' + 'level = decoded["meta"]["level"]\n', + 'lists1': 'colors = ["red", "green", "blue"]\n', + 'lists10': 'sales = [\n' + ' {"product": "apple", "units": 5, "price_per_unit": 1.20},\n' + ' {"product": "banana", "units": 15, "price_per_unit": 18.0},\n' + ' {"product": "cherry", "units": 8, "price_per_unit": 3.50},\n' + ' {"product": "date", "units": 20, "price_per_unit": 25.0},\n' + ' {"product": "elderberry", "units": 40, "price_per_unit": 22.0},\n' + ']\n' + 'revenue_list = []\n' + 'for record in sales:\n' + ' if record["units"] > 10:\n' + ' revenue_list.append(record["units"] * record["price_per_unit"])\n' + 'total_revenue = sum(revenue_list)\n', + 'lists2': 'numbers = [10, 20, 30, 40, 50]\nfirst = numbers[0]\nnumbers[2] = 99\n', + 'lists3': 'items = ["apple", "banana"]\nitems.append("cherry")\nitems.insert(1, "mango")\n', + 'lists4': 'items = ["apple", "banana", "cherry", "date"]\n' + 'items.remove("banana")\n' + 'last = items.pop()\n', + 'lists5': 'letters = ["a", "b", "c", "d", "e", "f"]\n' + 'first_three = letters[:3]\n' + 'last_two = letters[-2:]\n' + 'middle = letters[2:5]\n', + 'lists6': 'scores = [42, 7, 99, 55, 3, 76]\nscores.sort()\ndesc = sorted(scores, reverse=True)\n', + 'lists7': 'fruits = ["apple", "banana", "cherry", "banana", "apple", "apple"]\n' + 'total = len(fruits)\n' + 'has_kiwi = "kiwi" in fruits\n' + 'apple_count = fruits.count("apple")\n', + 'lists8': 'temperatures = [0, 100, -40, 37, 212, -273]\n' + 'fahrenheit = []\n' + 'for temp in temperatures:\n' + ' fahrenheit.append(temp * 9 / 5 + 32)\n', + 'lists9': 'matrix = [[1,2,3],[4,5,6],[7,8,9]]\n' + 'center = matrix[1][1]\n' + 'bottom_right = matrix[2][2]\n' + 'matrix[0][2] = 99\n', + 'loops1': 'total = 0\nfor n in range(1, 6):\n total += n\n', + 'loops10': 'transactions = [\n' + ' {"amount": 100, "type": "credit"},\n' + ' {"amount": 0, "type": "debit"},\n' + ' {"amount": 40, "type": "debit"},\n' + ' {"amount": 200, "type": "credit"},\n' + ' {"amount": 60, "type": "debit"},\n' + ']\n' + 'summary = []\n' + 'balance = 0\n' + 'for index, tx in enumerate(transactions, start=1):\n' + ' amount = tx["amount"]\n' + ' if amount == 0:\n' + ' continue\n' + ' if tx["type"] == "credit":\n' + ' balance += amount\n' + ' else:\n' + ' balance -= amount\n' + ' summary.append({"index": index, "amount": amount, "balance": balance})\n', + 'loops2': 'fruits = ["apple", "banana", "cherry"]\n' + 'upper_fruits = []\n' + 'for fruit in fruits:\n' + ' upper_fruits.append(fruit.upper())\n', + 'loops3': 'n = 5\ncountdown = []\nwhile n > 0:\n countdown.append(n)\n n -= 1\n', + 'loops4': 'numbers = [1,2,3,4,5,6,7,8,9,10]\n' + 'total = 0\n' + 'product = 1\n' + 'for n in numbers:\n' + ' total += n\n' + ' product *= n\n', + 'loops5': 'values = [3, 7, 2, 15, 8, 22]\n' + 'found = None\n' + 'i = 0\n' + 'while i < len(values):\n' + ' if values[i] > 10:\n' + ' found = values[i]\n' + ' break\n' + ' i += 1\n', + 'loops6': 'numbers = [1,2,3,4,5,6,7,8,9,10]\n' + 'evens = []\n' + 'for n in numbers:\n' + ' if n % 2 != 0:\n' + ' continue\n' + ' evens.append(n)\n', + 'loops7': 'table = []\n' + 'for i in range(3):\n' + ' row = []\n' + ' for j in range(3):\n' + ' row.append((i + 1) * (j + 1))\n' + ' table.append(row)\n', + 'loops8': 'words = ["python", "is", "fun"]\n' + 'labeled = []\n' + 'for i, word in enumerate(words, start=1):\n' + ' labeled.append(f"{i}: {word}")\n', + 'loops9': 'scores = {"Alice": 88, "Bob": 95, "Carol": 72, "Dave": 95 - 1}\n' + 'top_name = None\n' + 'top_score = -1\n' + 'for name, score in scores.items():\n' + ' if score > top_score:\n' + ' top_name = name\n' + ' top_score = score\n', + 'modules1': 'import math\nresult = math.sqrt(144)\n', + 'modules2': 'from math import sqrt\nroot = sqrt(81)\n', + 'modules3': 'import math as m\ncircle_area = m.pi * 5 ** 2\nfloored = m.floor(7.9)\n', + 'modules4': 'import random\nrandom.seed(42)\npick = random.randint(1, 10)\n', + 'modules5': 'import string\ndigits = string.digits\nletters = string.ascii_letters\n', + 'modules6': 'module_name = __name__\n', + 'modules7': 'ran_as_script = False\nif __name__ == "__main__":\n ran_as_script = True\n', + 'modules8': 'import math\n' + 'import random\n' + 'import statistics\n' + 'random.seed(0)\n' + 'data = [random.randint(1, 100) for _ in range(10)]\n' + 'data_mean = statistics.mean(data)\n' + 'data_max_sqrt = math.sqrt(max(data))\n', + 'oop_advanced1': 'class Animal:\n' + ' def speak(self):\n' + ' return "sound"\n' + '\n' + 'class Dog(Animal):\n' + ' pass\n', + 'oop_advanced10': 'from abc import ABC, abstractmethod\n' + '\n' + 'class Shape(ABC):\n' + ' @abstractmethod\n' + ' def area(self):\n' + ' pass\n' + '\n' + 'class Square(Shape):\n' + ' def __init__(self, side):\n' + ' self.side = side\n' + '\n' + ' def area(self):\n' + ' return self.side * self.side\n', + 'oop_advanced11': 'class DictMixin:\n' + ' def to_dict(self):\n' + ' return dict(self.__dict__)\n' + '\n' + 'class Product(DictMixin):\n' + ' def __init__(self, name, price):\n' + ' self.name = name\n' + ' self.price = price\n', + 'oop_advanced12': 'class Vector:\n' + ' def __init__(self, x, y):\n' + ' self.x = x\n' + ' self.y = y\n' + '\n' + ' def __add__(self, other):\n' + ' return Vector(self.x + other.x, self.y + other.y)\n', + 'oop_advanced2': 'class Person:\n' + ' def __init__(self, name):\n' + ' self.name = name\n' + '\n' + 'class Employee(Person):\n' + ' def __init__(self, name, role):\n' + ' super().__init__(name)\n' + ' self.role = role\n', + 'oop_advanced3': 'class Square:\n' + ' def __init__(self, side):\n' + ' self.side = side\n' + '\n' + ' def area(self):\n' + ' return self.side * self.side\n' + '\n' + 'class Rectangle:\n' + ' def __init__(self, width, height):\n' + ' self.width = width\n' + ' self.height = height\n' + '\n' + ' def area(self):\n' + ' return self.width * self.height\n' + '\n' + 'def total_area(shapes):\n' + ' return sum(shape.area() for shape in shapes)\n', + 'oop_advanced4': 'class User:\n' + ' def __init__(self, name):\n' + ' self._name = name\n' + '\n' + ' @property\n' + ' def name(self):\n' + ' return self._name\n', + 'oop_advanced5': 'class Playlist:\n' + ' def __init__(self, songs):\n' + ' self.songs = songs\n' + '\n' + ' def __len__(self):\n' + ' return len(self.songs)\n', + 'oop_advanced6': 'class Point:\n' + ' def __init__(self, x, y):\n' + ' self.x = x\n' + ' self.y = y\n' + '\n' + ' def __repr__(self):\n' + ' return f"Point(x={self.x}, y={self.y})"\n', + 'oop_advanced7': 'class Book:\n' + ' def __init__(self, title, isbn):\n' + ' self.title = title\n' + ' self.isbn = isbn\n' + '\n' + ' def __eq__(self, other):\n' + ' if not isinstance(other, Book):\n' + ' return NotImplemented\n' + ' return self.isbn == other.isbn\n', + 'oop_advanced8': 'class Coordinate:\n' + ' def __init__(self, x, y):\n' + ' self.x = x\n' + ' self.y = y\n' + '\n' + ' @classmethod\n' + ' def from_text(cls, text):\n' + ' x_text, y_text = text.split(",")\n' + ' return cls(int(x_text), int(y_text))\n', + 'oop_advanced9': 'class Grade:\n' + ' @staticmethod\n' + ' def is_valid(percent):\n' + ' 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' + 'source = project / "src" / "main.py"\n', + 'pathlib3': 'from pathlib import Path\n' + 'path = Path("docs/guide.md")\n' + 'parent = path.parent\n' + 'stem = path.stem\n' + 'suffix = path.suffix\n', + 'pathlib4': 'from pathlib import Path\n' + 'files = [Path("app.py"), Path("README.md"), Path("tests.py")]\n' + 'python_files = [path for path in files if path.suffix == ".py"]\n', + 'pathlib5': 'from pathlib import Path\n' + 'original = Path("reports/draft.txt")\n' + 'final = original.with_name("final.txt")\n', + 'pathlib6': 'from pathlib import Path\n' + 'source = Path("data/raw.csv")\n' + 'processed = source.with_suffix(".json")\n' + 'parts = processed.parts\n', + 'recursion1': 'def countdown(n):\n' + ' if n == 0:\n' + ' return [0]\n' + ' return [n] + countdown(n - 1)\n', + 'recursion2': 'def factorial(n):\n' + ' if n == 0:\n' + ' return 1\n' + ' return n * factorial(n - 1)\n', + 'recursion3': 'def recursive_sum(lst):\n' + ' if not lst:\n' + ' return 0\n' + ' return lst[0] + recursive_sum(lst[1:])\n', + 'recursion4': 'def count_items(lst):\n' + ' if not lst:\n' + ' return 0\n' + ' return 1 + count_items(lst[1:])\n', + 'recursion5': 'def fibonacci(n):\n' + ' if n == 0:\n' + ' return 0\n' + ' if n == 1:\n' + ' return 1\n' + ' return fibonacci(n - 1) + fibonacci(n - 2)\n', + 'recursion6': 'def reverse_string(s):\n' + ' if len(s) <= 1:\n' + ' return s\n' + ' return reverse_string(s[1:]) + s[0]\n', + 'recursion7': 'def deep_sum(nested):\n' + ' if not nested:\n' + ' return 0\n' + ' first = nested[0]\n' + ' rest = nested[1:]\n' + ' if isinstance(first, list):\n' + ' return deep_sum(first) + deep_sum(rest)\n' + ' return first + deep_sum(rest)\n', + 'recursion8': 'def tree_depth(tree):\n' + ' if not tree["children"]:\n' + ' return 1\n' + ' return 1 + max(tree_depth(child) for child in tree["children"])\n' + '\n' + 'def tree_search(tree, target):\n' + ' if tree["value"] == target:\n' + ' return True\n' + ' return any(tree_search(child, target) for child in tree["children"])\n', + 'regex1': 'import re\n' + 'text = "hello world"\n' + 'pattern = r"hello"\n' + 'result = re.match(pattern, text)\n', + 'regex10': 'import re\n' + 'log_line = "[ERROR] db.engine: connection refused"\n' + 'pattern = r"\\[(?P\\w+)\\] (?P[\\w.]+): (?P.+)"\n' + 'match = re.search(pattern, log_line)\n' + 'level = match.group("level")\n' + 'logger = match.group("logger")\n' + 'message = match.group("message")\n', + 'regex2': 'import re\n' + 'text = "I love python programming"\n' + 'pattern = r"python"\n' + 'result = re.search(pattern, text)\n', + 'regex3': 'import re\n' + 'text = "order 12 has 3 items and 450 points"\n' + 'numbers = re.findall(r"\\d+", text)\n', + 'regex4': 'import re\n' + 'text = "Hello world 123 foo_bar baz!"\n' + 'words = re.findall(r"[a-z]+", text)\n', + 'regex5': 'import re\n' + 'text = "requires python 3.10 or 2.7, not 1.x or 42"\n' + 'versions = re.findall(r"\\d+\\.\\d+", text)\n', + 'regex6': 'import re\n' + 'code = "ABCDE123"\n' + 'pattern = r"[A-Z]{5}\\d{3}"\n' + 'is_valid = bool(re.fullmatch(pattern, code))\n', + 'regex7': 'import re\n' + 'phone = "(415) 555-2671"\n' + 'pattern = r"\\((\\d{3})\\) \\d{3}-\\d{4}"\n' + 'match = re.search(pattern, phone)\n' + 'area_code = match.group(1)\n', + 'regex8': 'import re\n' + 'text = "This email is full of spam and junk, total spam!"\n' + 'pattern = r"spam|junk"\n' + 'clean = re.sub(pattern, "***", text)\n', + 'regex9': 'import re\n' + 'date_str = "2024-07-15"\n' + 'pattern = r"(?P\\d{4})-(?P\\d{2})-(?P\\d{2})"\n' + 'match = re.search(pattern, date_str)\n' + 'year = match.group("year")\n' + 'month = match.group("month")\n' + 'day = match.group("day")\n', + 'sets1': 'primes = {2, 3, 5, 7}\n', + 'sets10': 'day1 = ["alice", "bob", "carol", "dave"]\n' + 'day2 = ["bob", "carol", "eve", "frank"]\n' + 's1 = set(day1)\n' + 's2 = set(day2)\n' + 'attended_both = s1 & s2\n' + 'attended_any = s1 | s2\n' + 'only_day1 = s1 - s2\n' + 'only_day2 = s2 - s1\n', + 'sets2': 'colors = {"red", "blue", "orange"}\ncolors.add("purple")\ncolors.discard("orange")\n', + 'sets3': 'fruits = {"apple", "banana", "cherry"}\n' + 'has_apple = "apple" in fruits\n' + 'has_grape = "grape" in fruits\n', + 'sets4': 'a = {1, 2, 3, 4}\nb = {3, 4, 5, 6}\ncombined = a | b\n', + 'sets5': 'a = {1, 2, 3, 4}\nb = {3, 4, 5, 6}\nshared = a & b\n', + 'sets6': 'a = {1, 2, 3, 4}\nb = {3, 4, 5, 6}\nonly_in_a = a - b\n', + 'sets7': 'numbers = [1, 2, 3, 2, 4, 1, 5, 3, 2, 4]\n' + 'unique = set(numbers)\n' + 'unique_count = len(unique)\n', + 'sets8': 'small = {2, 4}\n' + 'big = {1, 2, 3, 4, 5}\n' + 'is_subset = small <= big\n' + 'is_superset = big >= small\n', + 'sets9': 'numbers = [1, 2, 3, 4, 5, 6, 7]\n' + 'even_squares = {n * n for n in numbers if n % 2 == 0}\n', + 'strings1': 'greeting = "hello"\n', + 'strings10': 'item = "widget"\n' + 'qty = 42\n' + 'price = 3.99\n' + 'report = f"Item: {item} | Qty: {qty} | Price: ${price:.2f}"\n', + 'strings2': 'first = "Ada"\nlast = "Lovelace"\nfull_name = first + " " + last\n', + 'strings3': 'word = "python"\nfirst_char = word[0]\nlast_char = word[-1]\n', + 'strings4': 'sentence = "the quick brown fox"\nword = sentence[4:9]\n', + 'strings5': 'text = "Hello, World!"\n' + 'length = len(text)\n' + 'upper = text.upper()\n' + 'lower = text.lower()\n', + 'strings6': 'raw = " I love cats and cats "\n' + 'clean = raw.strip()\n' + 'replaced = clean.replace("cat", "dog")\n', + 'strings7': 'csv_line = "apple,banana,cherry"\n' + 'items = csv_line.split(",")\n' + 'rejoined = ", ".join(items)\n', + 'strings8': 'name = "Grace"\nage = 30\nmessage = f"Hello, {name}! You are {age} years old."\n', + 'strings9': 'sentence = "the quick brown fox jumps"\n' + 'has_fox = "fox" in sentence\n' + 'fox_index = sentence.find("fox")\n', + 'testing1': 'def double(x):\n return x * 2\n\nresult = double(2)\nassert result == 4\n', + 'testing10': 'def sum_list(numbers):\n' + ' return sum(numbers)\n' + '\n' + 'def test_sum_list():\n' + ' assert sum_list([]) == 0\n' + ' assert sum_list([0]) == 0\n' + ' assert sum_list([-1, -2]) == -3\n' + ' assert sum_list([1, -1]) == 0\n' + ' assert sum_list([1, 2, 3]) == 6\n', + 'testing11': 'class Counter:\n' + ' def __init__(self):\n' + ' self.value = 0\n' + '\n' + ' def increment(self):\n' + ' self.value += 1\n' + '\n' + ' def reset(self):\n' + ' self.value = 0\n' + '\n' + 'def test_counter():\n' + ' counter = Counter()\n' + ' assert counter.value == 0\n' + ' counter.increment()\n' + ' assert counter.value == 1\n' + ' counter.increment()\n' + ' assert counter.value == 2\n' + ' counter.reset()\n' + ' assert counter.value == 0\n', + 'testing12': 'def is_palindrome(s):\n' + ' s = s.lower().replace(" ", "")\n' + ' return s == s[::-1]\n' + '\n' + 'def word_count(s):\n' + ' if not s.strip():\n' + ' return 0\n' + ' return len(s.split())\n' + '\n' + 'def to_title_case(s):\n' + ' return s.title()\n' + '\n' + 'def truncate(s, max_len):\n' + ' return s if len(s) <= max_len else s[:max_len] + "..."\n' + '\n' + 'def test_palindrome():\n' + ' assert is_palindrome("racecar") is True\n' + ' assert is_palindrome("hello") is False\n' + ' assert is_palindrome("") is True\n' + '\n' + 'def test_word_count():\n' + ' assert word_count("one two three") == 3\n' + ' assert word_count("hello") == 1\n' + ' assert word_count("") == 0\n' + '\n' + 'def test_title_case():\n' + ' assert to_title_case("hello world") == "Hello World"\n' + ' assert to_title_case("pylings") == "Pylings"\n' + '\n' + 'def test_truncate():\n' + ' assert truncate("short", 10) == "short"\n' + ' assert truncate("abcdef", 3) == "abc..."\n' + ' assert truncate("abc", 3) == "abc"\n', + 'testing2': 'def square(x):\n' + ' return x * x\n' + '\n' + 'assert square(5) == 25, "5 squared should be 25"\n', + 'testing3': 'def add(a, b):\n' + ' return a + b\n' + '\n' + 'def test_add():\n' + ' assert add(2, 3) == 5\n' + ' assert add(0, 0) == 0\n', + 'testing4': 'def is_even(n):\n' + ' return n % 2 == 0\n' + '\n' + 'def test_is_even():\n' + ' assert is_even(0) is True\n' + ' assert is_even(2) is True\n' + ' assert is_even(7) is False\n' + ' assert is_even(-4) is True\n', + 'testing5': 'def clamp(value, lo, hi):\n' + ' return max(lo, min(value, hi))\n' + '\n' + 'def test_clamp():\n' + ' assert clamp(5, 0, 10) == 5\n' + ' assert clamp(-3, 0, 10) == 0\n' + ' assert clamp(15, 0, 10) == 10\n' + ' assert clamp(0, 0, 10) == 0\n' + ' assert clamp(10, 0, 10) == 10\n', + 'testing6': 'def parse_positive(s):\n' + ' n = int(s)\n' + ' if n < 0:\n' + ' raise ValueError(f"{n} is not positive")\n' + ' return n\n' + '\n' + 'def test_parse_positive():\n' + ' assert parse_positive("42") == 42\n' + ' try:\n' + ' parse_positive("-1")\n' + ' assert False, "expected ValueError"\n' + ' except ValueError:\n' + ' pass\n', + 'testing7': 'def divide(a, b):\n' + ' if b == 0:\n' + ' raise ValueError("cannot divide by zero")\n' + ' return a / b\n' + '\n' + 'def test_divide_message():\n' + ' assert divide(10, 2) == 5.0\n' + ' try:\n' + ' divide(5, 0)\n' + ' assert False, "expected ValueError"\n' + ' except ValueError as e:\n' + ' assert "zero" in str(e)\n', + 'testing8': 'def fahrenheit_to_celsius(f):\n' + ' return round((f - 32) * 5 / 9, 10)\n' + '\n' + 'def test_fahrenheit_to_celsius():\n' + ' cases = [(32, 0.0), (212, 100.0), (98.6, 37.0), (-40, -40.0)]\n' + ' for f, expected in cases:\n' + ' assert abs(fahrenheit_to_celsius(f) - expected) < 1e-6, (\n' + ' f"fahrenheit_to_celsius({f}) expected {expected}"\n' + ' )\n', + 'testing9': 'def make_user():\n' + ' return {"name": "Alice", "age": 30}\n' + '\n' + 'def greet(user):\n' + ' return f"Hello, {user[\'name\']}!"\n' + '\n' + 'def test_user_greeting():\n' + ' user = make_user()\n' + ' assert greet(user) == "Hello, Alice!"\n', + 'tuples1': 'point = (3, 4)\n', + 'tuples10': 'results = [("Alice", 85), ("Bob", 42), ("Carol", 55), ("Diana", 91), ("Eve", 82)]\n' + 'top_student = max(results, key=lambda item: item[1])[0]\n' + 'average = sum(score for _, score in results) / len(results)\n' + 'passed = [name for name, score in results if score >= 60]\n', + 'tuples2': 'rgb = (255, 128, 0)\nred = rgb[0]\nblue = rgb[-1]\n', + 'tuples3': 'coordinates = (10, 20, 30)\nx, y, z = coordinates\n', + 'tuples4': 'solo = (42,)\n', + 'tuples5': 'original = (10, 20, 30)\nupdated = (original[0], 99, original[2])\n', + 'tuples6': 'def min_max(numbers):\n' + ' return (min(numbers), max(numbers))\n' + '\n' + 'result = min_max([4, 1, 7, 2, 9, 3])\n', + 'tuples7': 'a = 7\nb = 99\na, b = b, a\n', + 'tuples8': 'scores = (45, 85, 92, 55, 60, 74)\n' + 'passing = []\n' + 'for score in scores:\n' + ' if score >= 60:\n' + ' passing.append(score)\n', + 'tuples9': 'grid = {(0, 0): "origin", (1, 0): "right", (0, 1): "up"}\nlabel = grid[(1, 0)]\n', + 'type_hints1': 'count: int = 0\n', + 'type_hints2': 'def greet(name: str, times: int) -> str:\n return (name + " ") * times\n', + 'type_hints3': 'def total(scores: list[int], labels: dict[str, int]) -> int:\n' + ' return sum(scores) + sum(labels.values())\n', + 'type_hints4': 'def find_index(items: list[str], target: str) -> int | None:\n' + ' try:\n' + ' return items.index(target)\n' + ' except ValueError:\n' + ' return None\n', + 'type_hints5': 'def bounding_box(points: list[tuple[float, float]]) -> tuple[float, float, float, ' + 'float]:\n' + ' xs = [p[0] for p in points]\n' + ' ys = [p[1] for p in points]\n' + ' return (min(xs), min(ys), max(xs), max(ys))\n', + 'type_hints6': 'Vector = list[float]\n' + '\n' + 'def scale(v: Vector, factor: float) -> Vector:\n' + ' return [x * factor for x in v]\n', + 'type_hints7': 'from typing import Callable\n' + '\n' + 'def apply_twice(func: Callable[[int], int], value: int) -> int:\n' + ' return func(func(value))\n', + 'type_hints8': 'def summarise(records: list[dict[str, int]], key: str, default: int | None) -> ' + 'dict[str, int | None]:\n' + ' return {\n' + ' "count": len(records),\n' + ' "total": sum(r.get(key, default or 0) for r in records),\n' + ' "missing": default,\n' + ' }\n', + 'variables1': 'a = 0\nb = 0.0\nc = ""\n', + 'variables10': 'width = 3\n' + 'height = 4\n' + 'area = width * height\n' + 'perimeter = 2 * (width + height)\n' + 'diagonal = (width**2 + height**2) ** 0.5\n', + 'variables2': 'a = 10\nb = 3\nc = "hello"\n', + 'variables3': 'count = 7\nratio = 0.5\nis_ready = True\n', + 'variables4': 'score = 0\nscore = 20\nscore = 42\n', + 'variables5': 'total = 10\ntotal += 5\ntotal -= 3\ntotal *= 4\n', + 'variables6': 'x, y = 1, 2\n', + 'variables7': 'a = 10\nb = 20\na, b = b, a\n', + 'variables8': 'kind = type(3.14)\nas_int = int(3.14)\nas_str = str(42)\n', + 'variables9': 'TAX_RATE = 0.2\nprice = 50\ntotal_price = price * (1 + TAX_RATE)\n'} + + +def _source(solution_file: str) -> tuple[str, str]: + path = Path(solution_file) + name = path.stem + try: + source = ANSWERS[name] + except KeyError as exc: + raise RuntimeError(f"no reference solution for {name}") from exc + return str(path), source + + +def apply(solution_file: str, namespace: dict[str, object]) -> None: + filename, source = _source(solution_file) + lines = source.splitlines(keepends=True) + linecache.cache[filename] = (len(source), None, lines, filename) + namespace["__file__"] = filename + exec(compile(source, filename, "exec", dont_inherit=True), namespace) + + +def load(solution_file: str) -> dict[str, object]: + filename, source = _source(solution_file) + lines = source.splitlines(keepends=True) + linecache.cache[filename] = (len(source), None, lines, filename) + namespace: dict[str, object] = { + "__name__": "__main__", + "__file__": filename, + "__package__": None, + } + exec(compile(source, filename, "exec", dont_inherit=True), namespace) + return namespace diff --git a/solutions/async1.py b/solutions/async1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async10.py b/solutions/async10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async2.py b/solutions/async2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async3.py b/solutions/async3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async4.py b/solutions/async4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async5.py b/solutions/async5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async6.py b/solutions/async6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async7.py b/solutions/async7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async8.py b/solutions/async8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/async9.py b/solutions/async9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/async9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes1.py b/solutions/classes1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes10.py b/solutions/classes10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes11.py b/solutions/classes11.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes11.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes12.py b/solutions/classes12.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes12.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes2.py b/solutions/classes2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes3.py b/solutions/classes3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes4.py b/solutions/classes4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes5.py b/solutions/classes5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes6.py b/solutions/classes6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes7.py b/solutions/classes7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes8.py b/solutions/classes8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/classes9.py b/solutions/classes9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/classes9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections1.py b/solutions/collections1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections10.py b/solutions/collections10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections2.py b/solutions/collections2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections3.py b/solutions/collections3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections4.py b/solutions/collections4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections5.py b/solutions/collections5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections6.py b/solutions/collections6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections7.py b/solutions/collections7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections8.py b/solutions/collections8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/collections9.py b/solutions/collections9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/collections9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions1.py b/solutions/comprehensions1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions10.py b/solutions/comprehensions10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions2.py b/solutions/comprehensions2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions3.py b/solutions/comprehensions3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions4.py b/solutions/comprehensions4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions5.py b/solutions/comprehensions5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions6.py b/solutions/comprehensions6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions7.py b/solutions/comprehensions7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions8.py b/solutions/comprehensions8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/comprehensions9.py b/solutions/comprehensions9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/comprehensions9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals1.py b/solutions/conditionals1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals10.py b/solutions/conditionals10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals2.py b/solutions/conditionals2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals3.py b/solutions/conditionals3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals4.py b/solutions/conditionals4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals5.py b/solutions/conditionals5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals6.py b/solutions/conditionals6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals7.py b/solutions/conditionals7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals8.py b/solutions/conditionals8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/conditionals9.py b/solutions/conditionals9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/conditionals9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers1.py b/solutions/context_managers1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers2.py b/solutions/context_managers2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers3.py b/solutions/context_managers3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers4.py b/solutions/context_managers4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers5.py b/solutions/context_managers5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers6.py b/solutions/context_managers6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers7.py b/solutions/context_managers7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/context_managers8.py b/solutions/context_managers8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/context_managers8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses1.py b/solutions/dataclasses1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses2.py b/solutions/dataclasses2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses3.py b/solutions/dataclasses3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses4.py b/solutions/dataclasses4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses5.py b/solutions/dataclasses5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses6.py b/solutions/dataclasses6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses7.py b/solutions/dataclasses7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dataclasses8.py b/solutions/dataclasses8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dataclasses8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime1.py b/solutions/datetime1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime2.py b/solutions/datetime2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime3.py b/solutions/datetime3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime4.py b/solutions/datetime4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime5.py b/solutions/datetime5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime6.py b/solutions/datetime6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime7.py b/solutions/datetime7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/datetime8.py b/solutions/datetime8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/datetime8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators1.py b/solutions/decorators1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators10.py b/solutions/decorators10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators2.py b/solutions/decorators2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators3.py b/solutions/decorators3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators4.py b/solutions/decorators4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators5.py b/solutions/decorators5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators6.py b/solutions/decorators6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators7.py b/solutions/decorators7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators8.py b/solutions/decorators8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/decorators9.py b/solutions/decorators9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/decorators9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries1.py b/solutions/dictionaries1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries10.py b/solutions/dictionaries10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries2.py b/solutions/dictionaries2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries3.py b/solutions/dictionaries3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries4.py b/solutions/dictionaries4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries5.py b/solutions/dictionaries5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries6.py b/solutions/dictionaries6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries7.py b/solutions/dictionaries7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries8.py b/solutions/dictionaries8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/dictionaries9.py b/solutions/dictionaries9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/dictionaries9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums1.py b/solutions/enums1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums2.py b/solutions/enums2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums3.py b/solutions/enums3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums4.py b/solutions/enums4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums5.py b/solutions/enums5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/enums6.py b/solutions/enums6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/enums6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions1.py b/solutions/exceptions1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions10.py b/solutions/exceptions10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions2.py b/solutions/exceptions2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions3.py b/solutions/exceptions3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions4.py b/solutions/exceptions4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions5.py b/solutions/exceptions5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions6.py b/solutions/exceptions6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions7.py b/solutions/exceptions7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions8.py b/solutions/exceptions8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/exceptions9.py b/solutions/exceptions9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/exceptions9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io1.py b/solutions/file_io1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io10.py b/solutions/file_io10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io2.py b/solutions/file_io2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io3.py b/solutions/file_io3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io4.py b/solutions/file_io4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io5.py b/solutions/file_io5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io6.py b/solutions/file_io6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io7.py b/solutions/file_io7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io8.py b/solutions/file_io8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/file_io9.py b/solutions/file_io9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/file_io9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional1.py b/solutions/functional1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional10.py b/solutions/functional10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional2.py b/solutions/functional2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional3.py b/solutions/functional3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional4.py b/solutions/functional4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional5.py b/solutions/functional5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional6.py b/solutions/functional6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional7.py b/solutions/functional7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional8.py b/solutions/functional8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functional9.py b/solutions/functional9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functional9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions1.py b/solutions/functions1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions10.py b/solutions/functions10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions2.py b/solutions/functions2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions3.py b/solutions/functions3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions4.py b/solutions/functions4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions5.py b/solutions/functions5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions6.py b/solutions/functions6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions7.py b/solutions/functions7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions8.py b/solutions/functions8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/functions9.py b/solutions/functions9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/functions9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators1.py b/solutions/generators1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators10.py b/solutions/generators10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators2.py b/solutions/generators2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators3.py b/solutions/generators3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators4.py b/solutions/generators4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators5.py b/solutions/generators5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators6.py b/solutions/generators6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators7.py b/solutions/generators7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators8.py b/solutions/generators8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/generators9.py b/solutions/generators9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/generators9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools1.py b/solutions/itertools1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools2.py b/solutions/itertools2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools3.py b/solutions/itertools3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools4.py b/solutions/itertools4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools5.py b/solutions/itertools5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools6.py b/solutions/itertools6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools7.py b/solutions/itertools7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/itertools8.py b/solutions/itertools8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/itertools8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json1.py b/solutions/json1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json2.py b/solutions/json2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json3.py b/solutions/json3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json4.py b/solutions/json4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json5.py b/solutions/json5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json6.py b/solutions/json6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json7.py b/solutions/json7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/json8.py b/solutions/json8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/json8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists1.py b/solutions/lists1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists10.py b/solutions/lists10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists2.py b/solutions/lists2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists3.py b/solutions/lists3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists4.py b/solutions/lists4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists5.py b/solutions/lists5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists6.py b/solutions/lists6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists7.py b/solutions/lists7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists8.py b/solutions/lists8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/lists9.py b/solutions/lists9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/lists9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops1.py b/solutions/loops1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops10.py b/solutions/loops10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops2.py b/solutions/loops2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops3.py b/solutions/loops3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops4.py b/solutions/loops4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops5.py b/solutions/loops5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops6.py b/solutions/loops6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops7.py b/solutions/loops7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops8.py b/solutions/loops8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/loops9.py b/solutions/loops9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/loops9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules1.py b/solutions/modules1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules2.py b/solutions/modules2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules3.py b/solutions/modules3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules4.py b/solutions/modules4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules5.py b/solutions/modules5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules6.py b/solutions/modules6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules7.py b/solutions/modules7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/modules8.py b/solutions/modules8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/modules8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced1.py b/solutions/oop_advanced1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced10.py b/solutions/oop_advanced10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced11.py b/solutions/oop_advanced11.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced11.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced12.py b/solutions/oop_advanced12.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced12.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced2.py b/solutions/oop_advanced2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced3.py b/solutions/oop_advanced3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced4.py b/solutions/oop_advanced4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced5.py b/solutions/oop_advanced5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced6.py b/solutions/oop_advanced6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced7.py b/solutions/oop_advanced7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced8.py b/solutions/oop_advanced8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/oop_advanced9.py b/solutions/oop_advanced9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/oop_advanced9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib1.py b/solutions/pathlib1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib2.py b/solutions/pathlib2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib3.py b/solutions/pathlib3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib4.py b/solutions/pathlib4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib5.py b/solutions/pathlib5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/pathlib6.py b/solutions/pathlib6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/pathlib6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion1.py b/solutions/recursion1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion2.py b/solutions/recursion2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion3.py b/solutions/recursion3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion4.py b/solutions/recursion4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion5.py b/solutions/recursion5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion6.py b/solutions/recursion6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion7.py b/solutions/recursion7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/recursion8.py b/solutions/recursion8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/recursion8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex1.py b/solutions/regex1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex10.py b/solutions/regex10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex2.py b/solutions/regex2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex3.py b/solutions/regex3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex4.py b/solutions/regex4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex5.py b/solutions/regex5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex6.py b/solutions/regex6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex7.py b/solutions/regex7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex8.py b/solutions/regex8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/regex9.py b/solutions/regex9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/regex9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets1.py b/solutions/sets1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets10.py b/solutions/sets10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets2.py b/solutions/sets2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets3.py b/solutions/sets3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets4.py b/solutions/sets4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets5.py b/solutions/sets5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets6.py b/solutions/sets6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets7.py b/solutions/sets7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets8.py b/solutions/sets8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/sets9.py b/solutions/sets9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/sets9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings1.py b/solutions/strings1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings10.py b/solutions/strings10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings2.py b/solutions/strings2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings3.py b/solutions/strings3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings4.py b/solutions/strings4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings5.py b/solutions/strings5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings6.py b/solutions/strings6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings7.py b/solutions/strings7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings8.py b/solutions/strings8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/strings9.py b/solutions/strings9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/strings9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing1.py b/solutions/testing1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing10.py b/solutions/testing10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing11.py b/solutions/testing11.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing11.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing12.py b/solutions/testing12.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing12.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing2.py b/solutions/testing2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing3.py b/solutions/testing3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing4.py b/solutions/testing4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing5.py b/solutions/testing5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing6.py b/solutions/testing6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing7.py b/solutions/testing7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing8.py b/solutions/testing8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/testing9.py b/solutions/testing9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/testing9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples1.py b/solutions/tuples1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples10.py b/solutions/tuples10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples2.py b/solutions/tuples2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples3.py b/solutions/tuples3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples4.py b/solutions/tuples4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples5.py b/solutions/tuples5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples6.py b/solutions/tuples6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples7.py b/solutions/tuples7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples8.py b/solutions/tuples8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/tuples9.py b/solutions/tuples9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/tuples9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints1.py b/solutions/type_hints1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints2.py b/solutions/type_hints2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints3.py b/solutions/type_hints3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints4.py b/solutions/type_hints4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints5.py b/solutions/type_hints5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints6.py b/solutions/type_hints6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints7.py b/solutions/type_hints7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/type_hints8.py b/solutions/type_hints8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/type_hints8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables1.py b/solutions/variables1.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables1.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables10.py b/solutions/variables10.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables10.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables2.py b/solutions/variables2.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables2.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables3.py b/solutions/variables3.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables3.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables4.py b/solutions/variables4.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables4.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables5.py b/solutions/variables5.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables5.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables6.py b/solutions/variables6.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables6.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables7.py b/solutions/variables7.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables7.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables8.py b/solutions/variables8.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables8.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/solutions/variables9.py b/solutions/variables9.py new file mode 100644 index 0000000..a782850 --- /dev/null +++ b/solutions/variables9.py @@ -0,0 +1,2 @@ +from solutions._answers import apply +apply(__file__, globals()) diff --git a/tests/integration/test_cli_dry_run.py b/tests/integration/test_cli_dry_run.py new file mode 100644 index 0000000..f94c1ff --- /dev/null +++ b/tests/integration/test_cli_dry_run.py @@ -0,0 +1,7 @@ +from pylings.cli import main + + +def test_dry_run_alias_runs_one_exercise() -> None: + code = main(["--root", "tests/fixtures/passing_curriculum", "dry-run", "passing1"]) + + assert code == 0 diff --git a/tests/integration/test_cli_reset.py b/tests/integration/test_cli_reset.py index 9234309..9de265f 100644 --- a/tests/integration/test_cli_reset.py +++ b/tests/integration/test_cli_reset.py @@ -4,6 +4,8 @@ import sys from pathlib import Path +from pylings.core.curriculum import init_workspace + FIXTURES = Path(__file__).parent.parent / "fixtures" / "tiny_curriculum" @@ -17,35 +19,40 @@ def _run(*args: str, input: str | None = None) -> subprocess.CompletedProcess[st def test_reset_restores_pristine_content_with_yes(tmp_path: Path) -> None: - work = tmp_path / "work" - shutil.copytree(FIXTURES, work, ignore=shutil.ignore_patterns(".pylings")) - target = work / "exercises" / "passing.py" + work = init_workspace(tmp_path / "work") + target = next((work / "exercises").rglob("variables1.py")) original = target.read_text() - # Take a snapshot (verify implicitly snapshots through state-aware code; - # for reset, we explicitly run verify first to populate originals/). - _run("--root", str(work), "list") # any command that triggers snapshot - target.write_text("# scrambled by learner\n", encoding="utf-8") - result = _run("--root", str(work), "reset", "passing", "--yes") + result = _run("--root", str(work), "reset", "variables1", "--yes") assert result.returncode == 0 assert target.read_text() == original def test_reset_without_yes_aborts_on_no(tmp_path: Path) -> None: - work = tmp_path / "work" - shutil.copytree(FIXTURES, work, ignore=shutil.ignore_patterns(".pylings")) - _run("--root", str(work), "list") - target = work / "exercises" / "passing.py" + work = init_workspace(tmp_path / "work") + target = next((work / "exercises").rglob("variables1.py")) target.write_text("scrambled", encoding="utf-8") - result = _run("--root", str(work), "reset", "passing", input="n\n") + result = _run("--root", str(work), "reset", "variables1", input="n\n") assert result.returncode == 0 assert target.read_text() == "scrambled" +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")) + 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 target.read_text() == "# learner edit before reset\n" + + def test_reset_unknown_exercise_exits_nonzero() -> None: result = _run("--root", str(FIXTURES), "reset", "nope", "--yes") assert result.returncode != 0 - diff --git a/tests/integration/test_cli_solution.py b/tests/integration/test_cli_solution.py new file mode 100644 index 0000000..8c64d8f --- /dev/null +++ b/tests/integration/test_cli_solution.py @@ -0,0 +1,54 @@ +from pathlib import Path + +from pylings.cli import main + + +def test_solution_command_runs_workspace_solution(tmp_path: Path) -> None: + root = tmp_path / "workspace" + (root / "solutions").mkdir(parents=True) + (root / "checks").mkdir() + (root / "exercises").mkdir() + (root / "info.toml").write_text( + """ +format_version = 1 + +[[exercises]] +name = "passing1" +path = "exercises/passing1.py" +hint = "set x" +docs = "https://docs.python.org/3/" +""", + encoding="utf-8", + ) + (root / "exercises" / "passing1.py").write_text("x = 0\n", encoding="utf-8") + (root / "solutions" / "passing1.py").write_text("x = 1\n", encoding="utf-8") + (root / "checks" / "passing1.py").write_text("assert x == 1\n", encoding="utf-8") + + code = main(["--root", str(root), "solution", "passing1"]) + + assert code == 0 + + +def test_debug_writes_workspace_log(tmp_path: Path) -> None: + root = tmp_path / "workspace" + (root / "checks").mkdir(parents=True) + (root / "exercises").mkdir() + (root / "info.toml").write_text( + """ +format_version = 1 + +[[exercises]] +name = "passing1" +path = "exercises/passing1.py" +hint = "set x" +docs = "https://docs.python.org/3/" +""", + encoding="utf-8", + ) + (root / "exercises" / "passing1.py").write_text("x = 1\n", encoding="utf-8") + (root / "checks" / "passing1.py").write_text("assert x == 1\n", encoding="utf-8") + + code = main(["--debug", "--root", str(root), "run", "passing1"]) + + assert code == 0 + assert "run" in (root / ".pylings_debug.log").read_text(encoding="utf-8") diff --git a/tests/integration/test_cli_workspace.py b/tests/integration/test_cli_workspace.py new file mode 100644 index 0000000..2eb8765 --- /dev/null +++ b/tests/integration/test_cli_workspace.py @@ -0,0 +1,40 @@ +from pathlib import Path + +from pylings.cli import main + + +def test_init_command_creates_workspace(tmp_path: Path) -> None: + target = tmp_path / "learn-python" + + code = main(["init", "--path", str(target)]) + + assert code == 0 + assert (target / "info.toml").exists() + assert (target / "exercises").is_dir() + assert (target / "checks").is_dir() + + +def test_init_command_requires_force_for_non_empty_directory( + tmp_path: Path, capsys +) -> None: + target = tmp_path / "learn-python" + target.mkdir() + (target / "notes.txt").write_text("keep", encoding="utf-8") + + code = main(["init", "--path", str(target)]) + + assert code == 1 + assert "already exists and is not empty" in capsys.readouterr().err + + +def test_update_command_preserves_user_exercises(tmp_path: Path) -> None: + target = tmp_path / "learn-python" + assert main(["init", "--path", str(target)]) == 0 + exercise = next((target / "exercises").rglob("*.py")) + exercise.write_text("# edited\n", encoding="utf-8") + + code = main(["update", "--path", str(target)]) + + assert code == 0 + assert exercise.read_text(encoding="utf-8") == "# edited\n" + assert (target / ".pylings" / "originals").is_dir() diff --git a/tests/integration/test_installed_package.py b/tests/integration/test_installed_package.py new file mode 100644 index 0000000..1a7f2dc --- /dev/null +++ b/tests/integration/test_installed_package.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from pylings.cli import main + + +def test_package_can_initialize_and_list_workspace(tmp_path: Path) -> None: + root = tmp_path / "workspace" + + assert main(["init", "--path", str(root)]) == 0 + assert main(["--root", str(root), "list"]) == 0 + assert (root / "solutions" / "_answers.py").exists() diff --git a/tests/integration/test_solution_verify.py b/tests/integration/test_solution_verify.py new file mode 100644 index 0000000..c0edda3 --- /dev/null +++ b/tests/integration/test_solution_verify.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from pylings.core.manifest import load +from pylings.core.runner import run_verify +from pylings.core.solutions import solution_exercise + + +def test_every_reference_solution_passes() -> None: + root = Path(__file__).resolve().parents[2] + manifest = load(root) + failures: list[str] = [] + + for exercise in manifest.exercises: + result = run_verify(solution_exercise(root, exercise)) + if not result.passed: + failures.append(f"{exercise.name}: {result.stderr or result.stdout}") + + assert failures == [] diff --git a/tests/unit/test_curriculum.py b/tests/unit/test_curriculum.py new file mode 100644 index 0000000..df243a6 --- /dev/null +++ b/tests/unit/test_curriculum.py @@ -0,0 +1,56 @@ +from pathlib import Path + +from pylings.core import curriculum + + +def test_source_root_finds_curriculum_files() -> None: + root = curriculum.source_root() + + assert (root / "info.toml").exists() + assert (root / "exercises").is_dir() + assert (root / "checks").is_dir() + + +def test_init_workspace_copies_curriculum(tmp_path: Path) -> None: + target = tmp_path / "workspace" + + result = curriculum.init_workspace(target) + + assert result == target.resolve() + assert (target / "info.toml").exists() + assert (target / "exercises").is_dir() + assert (target / "checks").is_dir() + assert (target / "solutions").is_dir() + assert (target / ".pylings" / "originals").is_dir() + assert (target / ".gitignore").read_text(encoding="utf-8").splitlines() == [ + ".pylings/state.json", + ".pylings_debug.log", + "__pycache__/", + "*.pyc", + ] + + +def test_init_workspace_refuses_non_empty_directory(tmp_path: Path) -> None: + target = tmp_path / "workspace" + target.mkdir() + (target / "notes.txt").write_text("keep me", encoding="utf-8") + + try: + curriculum.init_workspace(target) + except curriculum.WorkspaceError as exc: + assert "already exists and is not empty" in str(exc) + else: + raise AssertionError("expected WorkspaceError") + + +def test_update_workspace_preserves_user_exercise_edit(tmp_path: Path) -> None: + target = curriculum.init_workspace(tmp_path / "workspace") + exercise = next((target / "exercises").rglob("*.py")) + exercise.write_text("# user edit\n", encoding="utf-8") + + curriculum.update_workspace(target) + + assert exercise.read_text(encoding="utf-8") == "# user edit\n" + original = target / ".pylings" / "originals" / exercise.relative_to(target / "exercises") + assert original.exists() + assert (target / "solutions" / "_answers.py").exists() diff --git a/tests/unit/test_reset.py b/tests/unit/test_reset.py index 2e76091..bc33511 100644 --- a/tests/unit/test_reset.py +++ b/tests/unit/test_reset.py @@ -4,7 +4,9 @@ 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 @@ -85,3 +87,18 @@ def test_snapshot_keys_on_exercise_name_not_filename(tmp_path: Path) -> None: originals = tmp_path / ".pylings" / "originals" assert (originals / "variables_utils.py").read_text() == "variables-version\n" assert (originals / "functions_utils.py").read_text() == "functions-version\n" + + +def test_restore_uses_pristine_originals_not_current_file(tmp_path: Path) -> None: + root = init_workspace(tmp_path / "workspace") + manifest = load(root) + exercise = manifest.exercises[0] + original = root / ".pylings" / "originals" / exercise.rel_path.relative_to( + "exercises" + ) + pristine = original.read_text(encoding="utf-8") + exercise.path.write_text("# corrupted user edit\n", encoding="utf-8") + + restore(root, exercise) + + assert exercise.path.read_text(encoding="utf-8") == pristine diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index b04ceee..d9bad59 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -80,3 +80,49 @@ def test_utf8_output(tmp_path: Path) -> None: result = run(ex) assert result.passed is True assert "héllo 🐍" in result.stdout + + +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") + 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") + + result = run( + Exercise( + name="relative", + path=ex_path, + check_path=check_path, + topic="t", + hint="", + root=tmp_path, + ) + ) + + assert result.passed is True + + +def test_runner_traceback_mentions_real_exercise_file(tmp_path: Path) -> None: + ex_path = tmp_path / "exercise.py" + check_path = tmp_path / "check.py" + ex_path.write_text("raise RuntimeError('boom')\n", encoding="utf-8") + check_path.write_text("assert True\n", encoding="utf-8") + + result = run( + Exercise( + name="traceback", + path=ex_path, + check_path=check_path, + topic="t", + hint="", + root=tmp_path, + ) + ) + + assert result.passed is False + assert str(ex_path) in result.stderr diff --git a/tests/unit/test_solution_coverage.py b/tests/unit/test_solution_coverage.py new file mode 100644 index 0000000..7a2afcd --- /dev/null +++ b/tests/unit/test_solution_coverage.py @@ -0,0 +1,15 @@ +from pathlib import Path + +import tomllib + + +def test_every_exercise_has_solution_file() -> None: + root = Path(__file__).resolve().parents[2] + data = tomllib.loads((root / "info.toml").read_text(encoding="utf-8")) + missing = [ + entry["name"] + for entry in data["exercises"] + if not (root / "solutions" / f"{entry['name']}.py").exists() + ] + + assert missing == [] diff --git a/tests/unit/test_watcher.py b/tests/unit/test_watcher.py new file mode 100644 index 0000000..73a18d2 --- /dev/null +++ b/tests/unit/test_watcher.py @@ -0,0 +1,13 @@ +from pylings.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