From ac995f860ebba16237fea9d1a06f897e1ea4716f Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:57:57 +0300 Subject: [PATCH 01/15] Sample prompts for: Cursor vs GitHub Copilot: Which AI Editor is Better for Python? --- cursor-vs-copilot-python/prompts.md | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 cursor-vs-copilot-python/prompts.md diff --git a/cursor-vs-copilot-python/prompts.md b/cursor-vs-copilot-python/prompts.md new file mode 100644 index 0000000000..af6997fe7e --- /dev/null +++ b/cursor-vs-copilot-python/prompts.md @@ -0,0 +1,117 @@ +# Prompts Used in Cursor vs GitHub Copilot + +This file contains the prompts used in the **Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?** comparison. The same prompts are used in both editors where applicable to compare how each editor handles the same development task. + +## Project Setup + +Use this prompt in **Agent** mode to set up the Markdown note manager project. It asks the editor to create the Python environment, install the required dependencies, add the test directory, and follow standard Python packaging conventions. :contentReference[oaicite:0]{index=0} + +```text +Set up a Python project in this directory, following standard Python +packaging conventions: +- Create a virtual environment +- Install pyyaml==6.0.2 and pytest==9.0.3 +- Add a tests/ directory +- Use the directory name as the package name +- Only include the dependencies listed above +``` + +## Implementing the Application + +Use this prompt in **Agent** mode after setting up the project. It defines the requirements for the command-line Markdown note manager, including Markdown storage, YAML frontmatter, the note data model, SQLite persistence, and the command line interface. :contentReference[oaicite:1]{index=1} + +```text +Build a command-line Markdown note manager for this project. + +Requirements: + +- Store notes as Markdown files with YAML frontmatter containing a + title and tags. +- Represent each note as a dataclass with title, body, tags, and + created_at fields. +- Create a SQLite-backed NoteStore that can add notes, search notes, + list notes by tag, and retrieve notes by title. +- Build an argparse command-line interface that exposes those + operations. +``` + +## Testing and Debugging + +Use this prompt in **Agent** mode after deliberately removing the `self._conn.commit()` call from `NoteStore.add_note()`. It asks the editor to run the existing tests, investigate any failures, fix the underlying problem, and verify the fix by running the complete test suite again. :contentReference[oaicite:2]{index=2} + +```text +Run the existing pytest test suite. + +If any tests fail, investigate the root cause, fix the underlying issue, +and rerun the tests until the entire suite passes. +``` + +## Planning the Archiving Feature + +Use this prompt in **Plan** mode to compare how Cursor and GitHub Copilot plan a multi-file change before modifying the project. The feature adds support for archiving notes while keeping archived notes out of normal searches and listings unless explicitly requested. :contentReference[oaicite:3]{index=3} + +```text +Create a plan to add support for archiving notes. + +- Archived notes shouldn't appear in normal searches or listings. +- Add an `--include-archived` option to the search and list commands + so archived notes can be included when needed. +- Integrate the feature cleanly with the existing application without + introducing duplicate logic. +``` + +## Reviewing the Database Layer + +Use this prompt in **Ask** mode after deliberately replacing the parameterized search query with an interpolated SQL query. It asks the editor to inspect the database layer for correctness, SQL safety, and code quality without changing the implementation. :contentReference[oaicite:4]{index=4} + +```text +Review the database layer for correctness, SQL safety, +and general code quality. +Identify any issues and suggest improvements without modifying the code. +``` + +## Reviewing Pending Changes in Cursor + +Use the `/review` command in Cursor after introducing the SQL injection vulnerability. Unlike the broader review in Ask mode, `/review` focuses on the changes in the current diff and identifies issues introduced by those changes. :contentReference[oaicite:5]{index=5} + +```text +/review +``` + +## Persistent Project Guidance + +The comparison also uses persistent project instructions to define coding conventions that the editors should follow in future tasks. These are stored in project files rather than submitted as individual chat prompts. + +### Cursor Project Rules + +Add the following instructions to `.cursor/rules/markdown-note-manager-rules.mdc`. The rule applies to Python files and gives Cursor persistent guidance about type hints, YAML parsing, file operations, SQL queries, and the note representation. :contentReference[oaicite:6]{index=6} + +```markdown +--- +globs: "**/*.py" +alwaysApply: false +--- + +# Instructions + +- Use type hints for all functions, return values, and dataclass fields. +- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. +- Use `pathlib.Path` for all file and directory operations. +- Use parameterized SQL queries for every SQLite operation. +- Represent notes as dataclasses rather than dictionaries. +``` + +### GitHub Copilot Repository Instructions + +Add the following instructions to `.github/copilot-instructions.md`. GitHub Copilot uses this file as persistent repository guidance when working on the project. :contentReference[oaicite:7]{index=7} + +```markdown +# Repository Instructions + +- Use type hints for all functions, return values, and dataclass fields. +- Parse YAML frontmatter with `yaml.safe_load()`. + Do not manually parse YAML. +- Use `pathlib.Path` for all file and directory operations. +- Use parameterized SQL queries for every SQLite operation. +- Represent notes as dataclasses rather than dictionaries. +``` From 703df3cc2de0f9cec3425c89cf97ff4e2d547e4b Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:01:49 +0300 Subject: [PATCH 02/15] Update: Sample prompts for: Cursor vs GitHub Copilot: Which AI Editor is Better for Python? --- cursor-vs-copilot-python/prompts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cursor-vs-copilot-python/prompts.md b/cursor-vs-copilot-python/prompts.md index af6997fe7e..04deb8aa11 100644 --- a/cursor-vs-copilot-python/prompts.md +++ b/cursor-vs-copilot-python/prompts.md @@ -1,6 +1,6 @@ # Prompts Used in Cursor vs GitHub Copilot -This file contains the prompts used in the **Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?** comparison. The same prompts are used in both editors where applicable to compare how each editor handles the same development task. +This file contains the prompts used in the **Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?** comparison. The same prompts are used in both editors to compare how each editor handles the same development task. ## Project Setup @@ -18,7 +18,7 @@ packaging conventions: ## Implementing the Application -Use this prompt in **Agent** mode after setting up the project. It defines the requirements for the command-line Markdown note manager, including Markdown storage, YAML frontmatter, the note data model, SQLite persistence, and the command line interface. :contentReference[oaicite:1]{index=1} +Use this prompt in **Agent** mode after setting up the project. It defines the requirements for the command-line Markdown note manager, including Markdown storage, YAML frontmatter, the note data model, SQLite persistence, and the command-line interface. :contentReference[oaicite:1]{index=1} ```text Build a command-line Markdown note manager for this project. From 8a7e3238cc15d9e6dd9c180a92d5b838a1cfbd4c Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:09:44 +0300 Subject: [PATCH 03/15] Add README for Cursor vs GitHub Copilot comparison --- cursor-vs-copilot-python/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 cursor-vs-copilot-python/README.md diff --git a/cursor-vs-copilot-python/README.md b/cursor-vs-copilot-python/README.md new file mode 100644 index 0000000000..f430baf396 --- /dev/null +++ b/cursor-vs-copilot-python/README.md @@ -0,0 +1,3 @@ +# Cursor vs Copilot: Which AI Editor Is Better for Python? + +This folder provides the prompts used in the Real Python tutorial [Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?](https://realpython.com/cursor-vs-github-copilot-python/) From 56ea8f99e633151f30c3fefa7bb22ff3c7b425c2 Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:11:47 +0300 Subject: [PATCH 04/15] Update README --- cursor-vs-copilot-python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursor-vs-copilot-python/README.md b/cursor-vs-copilot-python/README.md index f430baf396..a47aa5f4e2 100644 --- a/cursor-vs-copilot-python/README.md +++ b/cursor-vs-copilot-python/README.md @@ -1,3 +1,3 @@ -# Cursor vs Copilot: Which AI Editor Is Better for Python? +# Cursor vs GitHub Copilot: Which AI Editor Is Better for Python? This folder provides the prompts used in the Real Python tutorial [Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?](https://realpython.com/cursor-vs-github-copilot-python/) From 72b86bec19a8b948ee52d98d4c77134531800b57 Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:15:40 +0300 Subject: [PATCH 05/15] Update: Sample prompts for: Cursor vs GitHub Copilot: Which AI Editor is Better for Python? --- cursor-vs-copilot-python/prompts.md | 38 ----------------------------- 1 file changed, 38 deletions(-) diff --git a/cursor-vs-copilot-python/prompts.md b/cursor-vs-copilot-python/prompts.md index 04deb8aa11..43379bea78 100644 --- a/cursor-vs-copilot-python/prompts.md +++ b/cursor-vs-copilot-python/prompts.md @@ -77,41 +77,3 @@ Use the `/review` command in Cursor after introducing the SQL injection vulnerab ```text /review ``` - -## Persistent Project Guidance - -The comparison also uses persistent project instructions to define coding conventions that the editors should follow in future tasks. These are stored in project files rather than submitted as individual chat prompts. - -### Cursor Project Rules - -Add the following instructions to `.cursor/rules/markdown-note-manager-rules.mdc`. The rule applies to Python files and gives Cursor persistent guidance about type hints, YAML parsing, file operations, SQL queries, and the note representation. :contentReference[oaicite:6]{index=6} - -```markdown ---- -globs: "**/*.py" -alwaysApply: false ---- - -# Instructions - -- Use type hints for all functions, return values, and dataclass fields. -- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. -- Use `pathlib.Path` for all file and directory operations. -- Use parameterized SQL queries for every SQLite operation. -- Represent notes as dataclasses rather than dictionaries. -``` - -### GitHub Copilot Repository Instructions - -Add the following instructions to `.github/copilot-instructions.md`. GitHub Copilot uses this file as persistent repository guidance when working on the project. :contentReference[oaicite:7]{index=7} - -```markdown -# Repository Instructions - -- Use type hints for all functions, return values, and dataclass fields. -- Parse YAML frontmatter with `yaml.safe_load()`. - Do not manually parse YAML. -- Use `pathlib.Path` for all file and directory operations. -- Use parameterized SQL queries for every SQLite operation. -- Represent notes as dataclasses rather than dictionaries. -``` From 51f0fd8d23eeeb307836d13d80bdb7e4c384c1a2 Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:20:19 +0300 Subject: [PATCH 06/15] Add Cursor Project Rules for Markdown Note Manager Added rules for the Cursor project regarding Markdown note management, including coding conventions and guidelines for Python files. --- .../cursor/markdown-note-manager-rules.mdc | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc diff --git a/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc b/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc new file mode 100644 index 0000000000..6f601535f6 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc @@ -0,0 +1,20 @@ +## Cursor Project Rules + +This project includes a Cursor Project Rule that defines the coding conventions Cursor should follow when working with the Markdown note manager. + +The rule is stored in `.cursor/rules/markdown-note-manager-rules.mdc` and applies to Python files in the project. + +```markdown +--- +globs: "**/*.py" +alwaysApply: false +--- + +# Instructions + +- Use type hints for all functions, return values, and dataclass fields. +- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. +- Use `pathlib.Path` for all file and directory operations. +- Use parameterized SQL queries for every SQLite operation. +- Represent notes as dataclasses rather than dictionaries. +``` From 01510932d61b482391768b035f2012ca3a366ab5 Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:22:27 +0300 Subject: [PATCH 07/15] Added repository instructions for GitHub Copilot --- .../github-copilot/copilot-instructions.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 cursor-vs-copilot-python/github-copilot/copilot-instructions.md diff --git a/cursor-vs-copilot-python/github-copilot/copilot-instructions.md b/cursor-vs-copilot-python/github-copilot/copilot-instructions.md new file mode 100644 index 0000000000..b828fef659 --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/copilot-instructions.md @@ -0,0 +1,15 @@ +## GitHub Copilot Repository Instructions + +This project includes Repository Instructions that define the coding conventions GitHub Copilot should follow when working with the Markdown note manager. + +The instructions are stored in `.github/copilot-instructions.md` and provide repository-wide guidance for Copilot. + +```markdown +# Repository Instructions + +- Use type hints for all functions, return values, and dataclass fields. +- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. +- Use `pathlib.Path` for all file and directory operations. +- Use parameterized SQL queries for every SQLite operation. +- Represent notes as dataclasses rather than dictionaries. +``` From a8060dd2c33adea7c55f17678c1a75e8562fa2d5 Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:27:36 +0300 Subject: [PATCH 08/15] Update Cursor rule for markdown note manager Removed the Cursor Project Rules section and its details from the markdown note manager rules. --- .../cursor/markdown-note-manager-rules.mdc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc b/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc index 6f601535f6..86e93bd778 100644 --- a/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc +++ b/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc @@ -1,10 +1,3 @@ -## Cursor Project Rules - -This project includes a Cursor Project Rule that defines the coding conventions Cursor should follow when working with the Markdown note manager. - -The rule is stored in `.cursor/rules/markdown-note-manager-rules.mdc` and applies to Python files in the project. - -```markdown --- globs: "**/*.py" alwaysApply: false @@ -17,4 +10,3 @@ alwaysApply: false - Use `pathlib.Path` for all file and directory operations. - Use parameterized SQL queries for every SQLite operation. - Represent notes as dataclasses rather than dictionaries. -``` From af317384ba232f56512237a9c8f28927686ef08f Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:28:37 +0300 Subject: [PATCH 09/15] Update repository instructions for GitHub Copilot Removed detailed instructions for GitHub Copilot from the markdown file. --- .../github-copilot/copilot-instructions.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cursor-vs-copilot-python/github-copilot/copilot-instructions.md b/cursor-vs-copilot-python/github-copilot/copilot-instructions.md index b828fef659..f2615de035 100644 --- a/cursor-vs-copilot-python/github-copilot/copilot-instructions.md +++ b/cursor-vs-copilot-python/github-copilot/copilot-instructions.md @@ -1,10 +1,3 @@ -## GitHub Copilot Repository Instructions - -This project includes Repository Instructions that define the coding conventions GitHub Copilot should follow when working with the Markdown note manager. - -The instructions are stored in `.github/copilot-instructions.md` and provide repository-wide guidance for Copilot. - -```markdown # Repository Instructions - Use type hints for all functions, return values, and dataclass fields. @@ -12,4 +5,3 @@ The instructions are stored in `.github/copilot-instructions.md` and provide rep - Use `pathlib.Path` for all file and directory operations. - Use parameterized SQL queries for every SQLite operation. - Represent notes as dataclasses rather than dictionaries. -``` From 07948eb43189c0408c836a7e82f079e57a387deb Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:33:18 +0300 Subject: [PATCH 10/15] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cursor-vs-copilot-python/prompts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursor-vs-copilot-python/prompts.md b/cursor-vs-copilot-python/prompts.md index 43379bea78..45427780b6 100644 --- a/cursor-vs-copilot-python/prompts.md +++ b/cursor-vs-copilot-python/prompts.md @@ -4,7 +4,7 @@ This file contains the prompts used in the **Cursor vs GitHub Copilot: Which AI ## Project Setup -Use this prompt in **Agent** mode to set up the Markdown note manager project. It asks the editor to create the Python environment, install the required dependencies, add the test directory, and follow standard Python packaging conventions. :contentReference[oaicite:0]{index=0} +Use this prompt in **Agent** mode to set up the Markdown note manager project. It asks the editor to create the Python environment, install the required dependencies, add the test directory, and follow standard Python packaging conventions. ```text Set up a Python project in this directory, following standard Python From b4787fe816344e77f47111db4ca2677278ccf0cb Mon Sep 17 00:00:00 2001 From: Brian Mutea <98642927+brianMutea@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:00:35 +0300 Subject: [PATCH 11/15] Add test snippets for debugging, AI code completion, and code review comparisons between Cursor and GitHub Copilot. --- cursor-vs-copilot-python/test-snippets.md | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 cursor-vs-copilot-python/test-snippets.md diff --git a/cursor-vs-copilot-python/test-snippets.md b/cursor-vs-copilot-python/test-snippets.md new file mode 100644 index 0000000000..f684b46b53 --- /dev/null +++ b/cursor-vs-copilot-python/test-snippets.md @@ -0,0 +1,119 @@ +# Test Snippets + +Code changes used for the debugging, code completion, and code review tests in **Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?** + +## Debugging + +Remove the `self._conn.commit()` call immediately after the SQLite insert operation in `NoteStore.add_note()`: + +```python +self._conn.commit() +``` + +## AI Code Completion + +### Add `updated_at` + +Add an `updated_at` field to the `Note` dataclass after `created_at`: + +```python +@dataclass +class Note: + title: str + body: str + tags: list[str] + created_at: datetime + updated_at +``` + +### Rename the Search Method + +Rename the `search_notes()` method: + +```python +def search_notes(self, query: str) -> list[Note]: +``` + +to: + +```python +def search(self, query: str) -> list[Note]: +``` + +## Code Review + +Replace the parameterized `search_notes()` implementation with the corresponding vulnerable version. + +### Cursor + +Original: + +```python +def search_notes(self, query: str) -> list[Note]: + """ + Return notes whose title or body contains `query` (case-insensitive). + """ + pattern = f"%{query}%" + rows = self._conn.execute( + """ + SELECT * FROM notes + WHERE title LIKE ? COLLATE NOCASE + OR body LIKE ? COLLATE NOCASE + ORDER BY created_at DESC + """, + (pattern, pattern), + ).fetchall() + return [self._row_to_note(row) for row in rows] +``` + +Replace with: + +```python +def search_notes(self, query: str) -> list[Note]: + """ + Return notes whose title or body contains `query` (case-insensitive). + """ + rows = self._conn.execute( + f""" + SELECT * FROM notes + WHERE title LIKE '%{query}%' COLLATE NOCASE + OR body LIKE '%{query}%' COLLATE NOCASE + ORDER BY created_at DESC + """ + ).fetchall() + return [self._row_to_note(row) for row in rows] +``` + +### GitHub Copilot + +Original: + +```python +def search_notes(self, query: str) -> list[Note]: + """Return notes whose title or body contains the given query text.""" + rows = self._conn.execute( + """ + SELECT * FROM notes + WHERE title LIKE ? OR body LIKE ? + ORDER BY created_at + """, + (f"%{query}%", f"%{query}%"), + ).fetchall() + return [self._row_to_note(row) for row in rows] +``` + +Replace with: + +```python +def search_notes(self, query: str) -> list[Note]: + """Return notes whose title or body contains the given query text.""" + rows = self._conn.execute( + f""" + SELECT * FROM notes + WHERE title LIKE '%{query}%' + OR body LIKE '%{query}%' + ORDER BY created_at + """ + ).fetchall() + return [self._row_to_note(row) for row in rows] +``` From 2100e9adc336377819ddda85fc2546e8e1b80cd3 Mon Sep 17 00:00:00 2001 From: brianMutea Date: Mon, 10 Aug 2026 21:14:32 +0300 Subject: [PATCH 12/15] Sample code from Cursor for: Cursor vs GitHub Copilot: Which AI Editor is Better for Python? --- cursor-vs-copilot-python/cursor/.gitignore | 17 ++ cursor-vs-copilot-python/cursor/README.md | 55 ++++++ .../cursor/markdown-note-manager-rules.mdc | 12 -- .../cursor/pyproject.toml | 19 ++ .../src/notes_manager_cursor_test/__init__.py | 6 + .../src/notes_manager_cursor_test/__main__.py | 4 + .../src/notes_manager_cursor_test/cli.py | 164 ++++++++++++++++++ .../notes_manager_cursor_test/markdown_io.py | 78 +++++++++ .../src/notes_manager_cursor_test/models.py | 22 +++ .../src/notes_manager_cursor_test/store.py | 151 ++++++++++++++++ .../cursor/tests/__init__.py | 1 + .../cursor/tests/test_cli.py | 64 +++++++ .../cursor/tests/test_markdown_io.py | 58 +++++++ .../cursor/tests/test_package.py | 5 + .../cursor/tests/test_store.py | 74 ++++++++ 15 files changed, 718 insertions(+), 12 deletions(-) create mode 100644 cursor-vs-copilot-python/cursor/.gitignore create mode 100644 cursor-vs-copilot-python/cursor/README.md delete mode 100644 cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc create mode 100644 cursor-vs-copilot-python/cursor/pyproject.toml create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__init__.py create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__main__.py create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/cli.py create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/markdown_io.py create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/models.py create mode 100644 cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/store.py create mode 100644 cursor-vs-copilot-python/cursor/tests/__init__.py create mode 100644 cursor-vs-copilot-python/cursor/tests/test_cli.py create mode 100644 cursor-vs-copilot-python/cursor/tests/test_markdown_io.py create mode 100644 cursor-vs-copilot-python/cursor/tests/test_package.py create mode 100644 cursor-vs-copilot-python/cursor/tests/test_store.py diff --git a/cursor-vs-copilot-python/cursor/.gitignore b/cursor-vs-copilot-python/cursor/.gitignore new file mode 100644 index 0000000000..1ba289cf67 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/.gitignore @@ -0,0 +1,17 @@ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +build/ +dist/ +.pytest_cache/ + +notes.db +notes.db-journal +notes.db-wal +notes.db-shm +/notes/ + +# Editor / OS +.DS_Store +*.swp diff --git a/cursor-vs-copilot-python/cursor/README.md b/cursor-vs-copilot-python/cursor/README.md new file mode 100644 index 0000000000..fbd2e991ac --- /dev/null +++ b/cursor-vs-copilot-python/cursor/README.md @@ -0,0 +1,55 @@ +# Notes Manager + +A simple command-line Markdown note manager. Each note is stored twice: + +- as a **Markdown file** (with YAML frontmatter) in a notes directory — the canonical, human-editable copy. +- as a row in a **SQLite index** (`notes.db`) — used for fast search and listing. + +## Install + +```bash +pip install -e . +``` + +This installs the `notes` command (entry point defined in `pyproject.toml`). + +## Usage + +```bash +notes [--notes-dir DIR] [--db PATH] [args] +``` + +`--notes-dir` (default `notes/`) and `--db` (default `notes.db`) let you point at a different notes store. + +### Commands + +| Command | Description | +|---|---| +| `notes add [--tags a,b] [--body TEXT \| --body-file PATH]` | Create or update a note (body read from `--body`, `--body-file`, or stdin). | +| `notes get <title>` | Print a note by its exact title. | +| `notes list` | List all notes, most recently created first. | +| `notes search <query>` | Search notes whose title or body contains `query` (case-insensitive). | +| `notes list-tag <tag>` | List notes with a given tag (case-insensitive, exact tag match). | +| `notes reindex` | Rebuild the SQLite index from the Markdown files on disk (fixes drift if the index and files get out of sync). | + +### Examples + +```bash +notes add "Git Rebase vs Merge" --tags git,vcs --body "Rebase rewrites history; merge preserves it." +notes search rebase +notes list-tag git +notes get "Git Rebase vs Merge" +``` + +## Notes on storage + +- Titles are unique; adding a note with an existing title updates (upserts) it. +- `search` and `list-tag` query the SQLite index only. If a Markdown file is added/edited outside the CLI (or the index gets out of sync), run `notes reindex`. +- The `notes/` directory and `notes.db` hold your actual note data and are gitignored — they aren't meant to be committed to source control. + +## Development + +```bash +pip install -e . +pytest +``` diff --git a/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc b/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc deleted file mode 100644 index 86e93bd778..0000000000 --- a/cursor-vs-copilot-python/cursor/markdown-note-manager-rules.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -globs: "**/*.py" -alwaysApply: false ---- - -# Instructions - -- Use type hints for all functions, return values, and dataclass fields. -- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. -- Use `pathlib.Path` for all file and directory operations. -- Use parameterized SQL queries for every SQLite operation. -- Represent notes as dataclasses rather than dictionaries. diff --git a/cursor-vs-copilot-python/cursor/pyproject.toml b/cursor-vs-copilot-python/cursor/pyproject.toml new file mode 100644 index 0000000000..9cba48884e --- /dev/null +++ b/cursor-vs-copilot-python/cursor/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "notes-manager-cursor-test" +version = "0.1.0" +description = "" +requires-python = ">=3.9" +dependencies = [ + "pyyaml==6.0.2", + "pytest==9.0.3", +] + +[project.scripts] +notes = "notes_manager_cursor_test.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__init__.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__init__.py new file mode 100644 index 0000000000..186214dee4 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__init__.py @@ -0,0 +1,6 @@ +"""A command-line Markdown note manager.""" + +from .models import Note +from .store import NoteStore + +__all__ = ["Note", "NoteStore"] diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__main__.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__main__.py new file mode 100644 index 0000000000..bfdcd0c115 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/cli.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/cli.py new file mode 100644 index 0000000000..53c0fe4b14 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/cli.py @@ -0,0 +1,164 @@ +"""Command-line interface for the Markdown note manager.""" + +from __future__ import annotations + +import argparse +import sys +from collections.abc import Sequence +from pathlib import Path + +from .models import Note +from .store import NoteStore + +DEFAULT_NOTES_DIR = Path("notes") +DEFAULT_DB_PATH = Path("notes.db") + + +def _parse_tags(raw: str | None) -> list[str]: + if not raw: + return [] + return [tag.strip() for tag in raw.split(",") if tag.strip()] + + +def _read_body(args: argparse.Namespace) -> str: + if args.body_file: + return Path(args.body_file).read_text(encoding="utf-8") + if args.body is not None: + return args.body + return sys.stdin.read() + + +def _print_note(note: Note) -> None: + tags = ", ".join(note.tags) if note.tags else "-" + print(f"# {note.title}") + print(f"tags: {tags}") + print(f"created_at: {note.created_at.isoformat()}") + print() + print(note.body) + + +def _print_note_summary(note: Note) -> None: + tags = ", ".join(note.tags) if note.tags else "-" + print(f"{note.title}\t[{tags}]\t{note.created_at.isoformat()}") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="notes", + description="A command-line Markdown note manager.", + ) + parser.add_argument( + "--notes-dir", + default=DEFAULT_NOTES_DIR, + type=Path, + help=f"Directory to store Markdown note files in (default: {DEFAULT_NOTES_DIR})", + ) + parser.add_argument( + "--db", + default=DEFAULT_DB_PATH, + type=Path, + help=f"Path to the SQLite index database (default: {DEFAULT_DB_PATH})", + ) + + subparsers = parser.add_subparsers(dest="command", required=True) + + add_parser = subparsers.add_parser("add", help="Add a new note") + add_parser.add_argument("title", help="Title of the note") + add_parser.add_argument( + "--tags", default="", help="Comma-separated list of tags" + ) + body_group = add_parser.add_mutually_exclusive_group() + body_group.add_argument("--body", help="Body text of the note") + body_group.add_argument( + "--body-file", help="Path to a file containing the note body" + ) + + search_parser = subparsers.add_parser( + "search", help="Search notes by title or body content" + ) + search_parser.add_argument("query", help="Text to search for") + + list_tag_parser = subparsers.add_parser( + "list-tag", help="List notes that have a given tag" + ) + list_tag_parser.add_argument("tag", help="Tag to filter by") + + get_parser = subparsers.add_parser( + "get", help="Retrieve a note by its exact title" + ) + get_parser.add_argument("title", help="Title of the note") + + subparsers.add_parser("list", help="List all notes") + + subparsers.add_parser( + "reindex", + help="Rebuild the SQLite search index from the Markdown files on disk", + ) + + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + with NoteStore(args.notes_dir, args.db) as store: + if args.command == "add": + note = Note( + title=args.title, + body=_read_body(args), + tags=_parse_tags(args.tags), + ) + store.add_note(note) + print(f"Added note '{note.title}'") + return 0 + + if args.command == "search": + results = store.find_notes_by_title_and_body(args.query) + if not results: + print("No notes found.") + return 0 + for note in results: + _print_note_summary(note) + return 0 + + if args.command == "list-tag": + results = store.find_notes_by_tag(args.tag) + if not results: + print(f"No notes found with tag '{args.tag}'.") + return 0 + for note in results: + _print_note_summary(note) + return 0 + + if args.command == "get": + note = store.find_note_by_title(args.title) + if note is None: + print( + f"No note found with title '{args.title}'.", + file=sys.stderr, + ) + return 1 + _print_note(note) + return 0 + + if args.command == "list": + results = store.find_all() + if not results: + print("No notes found.") + return 0 + for note in results: + _print_note_summary(note) + return 0 + + if args.command == "reindex": + count = store.reindex() + print(f"Reindexed {count} note(s) from '{args.notes_dir}'.") + return 0 + + parser.error(f"Unknown command: {args.command}") + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/markdown_io.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/markdown_io.py new file mode 100644 index 0000000000..32b36bf1c9 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/markdown_io.py @@ -0,0 +1,78 @@ +"""Read and write notes as Markdown files with YAML frontmatter. + +The on-disk format looks like:: + + --- + title: My Note + tags: + - foo + - bar + --- + The body of the note goes here. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + +from .models import Note + +_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?\n)---\s*\n?(.*)\Z", re.DOTALL) + + +def slugify(title: str) -> str: + """Turn a note title into a filesystem-friendly slug.""" + slug = re.sub(r"[^a-zA-Z0-9]+", "-", title.strip().lower()).strip("-") + return slug or "note" + + +def serialize_note(note: Note) -> str: + """Render a Note as Markdown text with YAML frontmatter.""" + frontmatter = yaml.safe_dump( + {"title": note.title, "tags": list(note.tags)}, + sort_keys=False, + ) + return f"---\n{frontmatter}---\n{note.body}" + + +def deserialize_note(text: str, *, created_at=None) -> Note: + """Parse Markdown text with YAML frontmatter into a Note. + + ``created_at`` is not stored in the frontmatter (only title and tags + are), so it must be supplied by the caller (e.g. from a database + record or the file's modification time). If omitted, the Note's + default (the current time) is used. + """ + match = _FRONTMATTER_RE.match(text) + if not match: + raise ValueError("Note text is missing YAML frontmatter") + + raw_frontmatter, body = match.groups() + metadata = yaml.safe_load(raw_frontmatter) or {} + + title = metadata.get("title", "") + tags = list(metadata.get("tags") or []) + body = body.lstrip("\n") + + kwargs = {"title": title, "body": body, "tags": tags} + if created_at is not None: + kwargs["created_at"] = created_at + return Note(**kwargs) + + +def write_note_file(note: Note, directory: Path) -> Path: + """Write ``note`` to a Markdown file inside ``directory`` and return its path.""" + directory.mkdir(parents=True, exist_ok=True) + path = directory / f"{slugify(note.title)}.md" + path.write_text(serialize_note(note), encoding="utf-8") + return path + + +def read_note_file(path: Path, *, created_at=None) -> Note: + """Read a Note from a Markdown file on disk.""" + return deserialize_note( + path.read_text(encoding="utf-8"), created_at=created_at + ) diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/models.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/models.py new file mode 100644 index 0000000000..14654f462d --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/models.py @@ -0,0 +1,22 @@ +"""Data model for notes.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timezone + + +@dataclass +class Note: + """A single Markdown note.""" + + title: str + body: str + tags: list[str] = field(default_factory=list) + created_at: datetime = field( + default_factory=lambda: datetime.now(timezone.utc) + ) + updated_at: datetime = field( + default_factory=lambda: datetime.now(timezone.utc) + ) + is_archived: bool = False diff --git a/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/store.py b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/store.py new file mode 100644 index 0000000000..9ffe19790e --- /dev/null +++ b/cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/store.py @@ -0,0 +1,151 @@ +"""SQLite-backed storage for notes. + +Each note is persisted twice: + +* as a Markdown file (with YAML frontmatter) on disk, which is the + canonical, human-editable representation, and +* as a row in a SQLite database, which acts as a fast, queryable index + used for search/listing operations. +""" + +from __future__ import annotations + +import sqlite3 +from datetime import datetime +from pathlib import Path + +from . import markdown_io +from .models import Note + +_TAG_SEPARATOR = "," + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS notes ( + title TEXT PRIMARY KEY, + body TEXT NOT NULL, + tags TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + filepath TEXT NOT NULL +); +""" + + +class NoteStore: + """Add, search, and retrieve Markdown notes backed by SQLite.""" + + def __init__(self, notes_dir: Path | str, db_path: Path | str): + self.notes_dir = Path(notes_dir) + self.db_path = Path(db_path) + self.notes_dir.mkdir(parents=True, exist_ok=True) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + + self._conn = sqlite3.connect(self.db_path) + self._conn.row_factory = sqlite3.Row + self._conn.execute(_SCHEMA) + self._conn.commit() + + def close(self) -> None: + self._conn.close() + + def __enter__(self) -> "NoteStore": + return self + + def __exit__(self, *exc_info) -> None: + self.close() + + def add_note(self, note: Note) -> Note: + """Write ``note`` to disk and index it in the database. + + Returns the note as-is (useful when chaining). + """ + filepath = markdown_io.write_note_file(note, self.notes_dir) + self._conn.execute( + """ + INSERT INTO notes (title, body, tags, created_at, filepath) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(title) DO UPDATE SET + body = excluded.body, + tags = excluded.tags, + created_at = excluded.created_at, + filepath = excluded.filepath + """, + ( + note.title, + note.body, + _TAG_SEPARATOR.join(note.tags), + note.created_at.isoformat(), + str(filepath), + ), + ) + self._conn.commit() + return note + + def reindex(self) -> int: + """Rebuild the SQLite index from the Markdown files on disk. + + Every ``*.md`` file in ``self.notes_dir`` is read and upserted into + the database, so files that were added or edited outside of + :meth:`add_note` (or whose index row was lost) become searchable + again. Returns the number of notes indexed. + """ + count = 0 + for path in sorted(self.notes_dir.glob("*.md")): + created_at = datetime.fromtimestamp( + path.stat().st_mtime + ).astimezone() + note = markdown_io.read_note_file(path, created_at=created_at) + self.add_note(note) + count += 1 + return count + + def find_notes_by_title_and_body(self, query: str) -> list[Note]: + """Return notes whose title or body contains `query` (case-insensitive).""" + rows = self._conn.execute( + """ + SELECT * FROM notes + WHERE title LIKE ? COLLATE NOCASE + OR body LIKE ? COLLATE NOCASE + ORDER BY created_at DESC + """, + (f"%{query}%", f"%{query}%"), + ).fetchall() + return [self._row_to_note(row) for row in rows] + + def find_notes_by_tag(self, tag: str) -> list[Note]: + """Return all notes tagged with ``tag`` (case-insensitive, exact tag match).""" + rows = self._conn.execute( + "SELECT * FROM notes ORDER BY created_at DESC" + ).fetchall() + return [ + note + for row in rows + if tag.lower() in {t.lower() for t in _split_tags(row["tags"])} + for note in [self._row_to_note(row)] + ] + + def find_note_by_title(self, title: str) -> Note | None: + """Retrieve a single note by its exact title, or ``None`` if not found.""" + row = self._conn.execute( + "SELECT * FROM notes WHERE title = ?", (title,) + ).fetchone() + return self._row_to_note(row) if row else None + + def find_all(self) -> list[Note]: + """Return every note in the store, most recently created first.""" + rows = self._conn.execute( + "SELECT * FROM notes ORDER BY created_at DESC" + ).fetchall() + return [self._row_to_note(row) for row in rows] + + @staticmethod + def _row_to_note(row: sqlite3.Row) -> Note: + return Note( + title=row["title"], + body=row["body"], + tags=_split_tags(row["tags"]), + created_at=datetime.fromisoformat(row["created_at"]), + ) + + +def _split_tags(raw: str) -> list[str]: + return [tag for tag in raw.split(_TAG_SEPARATOR) if tag] diff --git a/cursor-vs-copilot-python/cursor/tests/__init__.py b/cursor-vs-copilot-python/cursor/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/cursor-vs-copilot-python/cursor/tests/test_cli.py b/cursor-vs-copilot-python/cursor/tests/test_cli.py new file mode 100644 index 0000000000..2d335f5069 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/tests/test_cli.py @@ -0,0 +1,64 @@ +from notes_manager_cursor_test.cli import main + + +def run(tmp_path, *args): + notes_dir = tmp_path / "notes" + db_path = tmp_path / "notes.db" + return main(["--notes-dir", str(notes_dir), "--db", str(db_path), *args]) + + +def test_add_and_get(tmp_path, capsys): + exit_code = run( + tmp_path, + "add", + "My Note", + "--tags", + "foo,bar", + "--body", + "Hello world", + ) + assert exit_code == 0 + + exit_code = run(tmp_path, "get", "My Note") + assert exit_code == 0 + out = capsys.readouterr().out + assert "My Note" in out + assert "foo, bar" in out + assert "Hello world" in out + + +def test_get_missing_note_returns_error(tmp_path): + assert run(tmp_path, "get", "Nope") == 1 + + +def test_search(tmp_path, capsys): + run(tmp_path, "add", "Trip Plan", "--body", "Visit the mountains") + run(tmp_path, "add", "Unrelated", "--body", "Nothing relevant") + capsys.readouterr() + + run(tmp_path, "search", "mountains") + out = capsys.readouterr().out + assert "Trip Plan" in out + assert "Unrelated" not in out + + +def test_list_tag(tmp_path, capsys): + run(tmp_path, "add", "Note A", "--tags", "red", "--body", "a") + run(tmp_path, "add", "Note B", "--tags", "blue", "--body", "b") + capsys.readouterr() + + run(tmp_path, "list-tag", "red") + out = capsys.readouterr().out + assert "Note A" in out + assert "Note B" not in out + + +def test_list_all(tmp_path, capsys): + run(tmp_path, "add", "Note A", "--body", "a") + run(tmp_path, "add", "Note B", "--body", "b") + capsys.readouterr() + + run(tmp_path, "list") + out = capsys.readouterr().out + assert "Note A" in out + assert "Note B" in out diff --git a/cursor-vs-copilot-python/cursor/tests/test_markdown_io.py b/cursor-vs-copilot-python/cursor/tests/test_markdown_io.py new file mode 100644 index 0000000000..668cbedbb0 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/tests/test_markdown_io.py @@ -0,0 +1,58 @@ +from datetime import datetime, timezone + +from notes_manager_cursor_test.markdown_io import ( + deserialize_note, + read_note_file, + serialize_note, + slugify, + write_note_file, +) +from notes_manager_cursor_test.models import Note + + +def test_slugify(): + assert slugify("Hello, World!") == "hello-world" + assert slugify(" ") == "note" + + +def test_serialize_roundtrip(): + note = Note(title="My Note", body="Some body text.\n", tags=["foo", "bar"]) + text = serialize_note(note) + + assert text.startswith("---\n") + assert "title: My Note" in text + assert "Some body text." in text + + parsed = deserialize_note(text, created_at=note.created_at) + assert parsed.title == note.title + assert parsed.tags == note.tags + assert parsed.body.strip() == note.body.strip() + assert parsed.created_at == note.created_at + + +def test_deserialize_requires_frontmatter(): + try: + deserialize_note("no frontmatter here") + except ValueError: + pass + else: + raise AssertionError("Expected ValueError for missing frontmatter") + + +def test_write_and_read_note_file(tmp_path): + created_at = datetime(2024, 1, 1, tzinfo=timezone.utc) + note = Note( + title="Grocery List", + body="- milk\n- eggs\n", + tags=["home"], + created_at=created_at, + ) + + path = write_note_file(note, tmp_path) + assert path.exists() + assert path.name == "grocery-list.md" + + loaded = read_note_file(path, created_at=created_at) + assert loaded.title == note.title + assert loaded.tags == note.tags + assert loaded.body.strip() == note.body.strip() diff --git a/cursor-vs-copilot-python/cursor/tests/test_package.py b/cursor-vs-copilot-python/cursor/tests/test_package.py new file mode 100644 index 0000000000..dd86f63d15 --- /dev/null +++ b/cursor-vs-copilot-python/cursor/tests/test_package.py @@ -0,0 +1,5 @@ +import notes_manager_cursor_test + + +def test_package_importable(): + assert notes_manager_cursor_test is not None diff --git a/cursor-vs-copilot-python/cursor/tests/test_store.py b/cursor-vs-copilot-python/cursor/tests/test_store.py new file mode 100644 index 0000000000..39c06dee4e --- /dev/null +++ b/cursor-vs-copilot-python/cursor/tests/test_store.py @@ -0,0 +1,74 @@ +import pytest + +from notes_manager_cursor_test.models import Note +from notes_manager_cursor_test.store import NoteStore + + +@pytest.fixture +def store(tmp_path): + with NoteStore(tmp_path / "notes", tmp_path / "notes.db") as store: + yield store + + +def test_add_and_get_note(store): + note = Note( + title="Recipe", body="Mix flour and water.", tags=["cooking", "bread"] + ) + store.add_note(note) + + fetched = store.find_note_by_title("Recipe") + assert fetched is not None + assert fetched.title == "Recipe" + assert fetched.body == "Mix flour and water." + assert set(fetched.tags) == {"cooking", "bread"} + + +def test_get_missing_note_returns_none(store): + assert store.find_note_by_title("Nonexistent") is None + + +def test_add_note_writes_markdown_file(store): + note = Note(title="Shopping", body="- bread\n- butter", tags=["home"]) + store.add_note(note) + + files = list(store.notes_dir.glob("*.md")) + assert len(files) == 1 + assert "title: Shopping" in files[0].read_text() + + +def test_search_notes_matches_title_and_body(store): + store.add_note( + Note(title="Trip Plan", body="Visit the mountains", tags=["travel"]) + ) + store.add_note( + Note(title="Work Notes", body="Discuss trip budget", tags=["work"]) + ) + store.add_note( + Note(title="Unrelated", body="Nothing to see here", tags=[]) + ) + + results = store.find_notes_by_title_and_body("trip") + titles = {note.title for note in results} + assert titles == {"Trip Plan", "Work Notes"} + + +def test_list_notes_by_tag(store): + store.add_note(Note(title="Note A", body="a", tags=["red", "blue"])) + store.add_note(Note(title="Note B", body="b", tags=["blue"])) + store.add_note(Note(title="Note C", body="c", tags=["green"])) + + results = store.find_notes_by_tag("blue") + titles = {note.title for note in results} + assert titles == {"Note A", "Note B"} + + assert store.find_notes_by_tag("purple") == [] + + +def test_add_note_upserts_on_same_title(store): + store.add_note(Note(title="Duplicate", body="first version", tags=["v1"])) + store.add_note(Note(title="Duplicate", body="second version", tags=["v2"])) + + fetched = store.find_note_by_title("Duplicate") + assert fetched.body == "second version" + assert fetched.tags == ["v2"] + assert len(store.find_all()) == 1 From 0c60c15c248965be86d099cc89560b0c33543b98 Mon Sep 17 00:00:00 2001 From: brianMutea <brianmuteak@gmail.com> Date: Mon, 10 Aug 2026 21:33:22 +0300 Subject: [PATCH 13/15] Sample code from GitHub Copilot for: Cursor vs GitHub Copilot: Which AI Editor is Better for Python? --- asyncio-walkthrough/areq.py | 2 +- .../github-copilot/.gitignore | 6 + .../github-copilot/README.md | 36 ++++++ .../github-copilot/copilot-instructions.md | 7 -- .../github-copilot/pyproject.toml | 22 ++++ .../notes_manager_copilot_test/__init__.py | 0 .../src/notes_manager_copilot_test/cli.py | 112 ++++++++++++++++++ .../notes_manager_copilot_test/markdown_io.py | 42 +++++++ .../src/notes_manager_copilot_test/models.py | 20 ++++ .../src/notes_manager_copilot_test/store.py | 80 +++++++++++++ .../github-copilot/tests/__init__.py | 0 .../github-copilot/tests/test_cli.py | 54 +++++++++ .../github-copilot/tests/test_markdown_io.py | 40 +++++++ .../github-copilot/tests/test_models.py | 20 ++++ .../github-copilot/tests/test_store.py | 93 +++++++++++++++ cursor-vs-windsurf-python/prompts.md | 2 + django-pagination/README.md | 1 - flask-connexion-rest-part-2/README.md | 1 - flush-print/README.md | 2 +- python-copy/benchmark.py | 2 +- python-eval-mathrepl/mathrepl.py | 2 +- .../local/serializers/factory.py | 2 +- .../population_quiz/population_quiz.py | 2 +- python-multiple-exceptions/exception_pass.py | 2 +- .../multiple_exceptions.py | 2 +- python-type-checking/hearts.py | 2 +- structural-pattern-matching/guessing_game.py | 2 +- wordcount/tests/realpython/HOWTO.md | 68 ++++++----- 28 files changed, 570 insertions(+), 54 deletions(-) create mode 100644 cursor-vs-copilot-python/github-copilot/.gitignore create mode 100644 cursor-vs-copilot-python/github-copilot/README.md delete mode 100644 cursor-vs-copilot-python/github-copilot/copilot-instructions.md create mode 100644 cursor-vs-copilot-python/github-copilot/pyproject.toml create mode 100644 cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/__init__.py create mode 100644 cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/cli.py create mode 100644 cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/markdown_io.py create mode 100644 cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/models.py create mode 100644 cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/store.py create mode 100644 cursor-vs-copilot-python/github-copilot/tests/__init__.py create mode 100644 cursor-vs-copilot-python/github-copilot/tests/test_cli.py create mode 100644 cursor-vs-copilot-python/github-copilot/tests/test_markdown_io.py create mode 100644 cursor-vs-copilot-python/github-copilot/tests/test_models.py create mode 100644 cursor-vs-copilot-python/github-copilot/tests/test_store.py diff --git a/asyncio-walkthrough/areq.py b/asyncio-walkthrough/areq.py index 12ae0cde14..97bb63cc45 100644 --- a/asyncio-walkthrough/areq.py +++ b/asyncio-walkthrough/areq.py @@ -74,7 +74,7 @@ async def parse(url: str, session: ClientSession, **kwargs) -> set: try: # Ensure we return an absolute path. abslink = urllib.parse.urljoin(url, link) - except (urllib.error.URLError, ValueError): + except urllib.error.URLError, ValueError: logger.exception("Error parsing URL: %s", link) pass else: diff --git a/cursor-vs-copilot-python/github-copilot/.gitignore b/cursor-vs-copilot-python/github-copilot/.gitignore new file mode 100644 index 0000000000..c393f8501d --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/.gitignore @@ -0,0 +1,6 @@ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +.pytest_cache/ +.DS_Store diff --git a/cursor-vs-copilot-python/github-copilot/README.md b/cursor-vs-copilot-python/github-copilot/README.md new file mode 100644 index 0000000000..ab701ef17b --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/README.md @@ -0,0 +1,36 @@ +# Notes Manager + +A CLI for managing Markdown notes, backed by a SQLite database. + +## Install + +```bash +pip install -e . +``` + +## Usage + +``` +notes [--db PATH] <command> [args] +``` + +`--db` sets the SQLite database path (default: `~/.notes-manager-copilot-test/notes.db`). + +| Command | Description | +|---|---| +| `notes add <title> <body> [--tags TAG ...]` | Create a note | +| `notes get <title>` | Fetch a note by exact title | +| `notes search <query>` | Find notes whose title or body contains `query` | +| `notes list-tag <tag>` | List notes with a given tag | + +## Storage + +- Notes are stored in a SQLite database at the `--db` path. +- Each note added via `add` is also written as a Markdown file (with YAML frontmatter for `title`, `tags`, `created_at`) to a `notes/` folder next to the database. + +## Development + +```bash +pip install -e ".[dev]" +pytest +``` diff --git a/cursor-vs-copilot-python/github-copilot/copilot-instructions.md b/cursor-vs-copilot-python/github-copilot/copilot-instructions.md deleted file mode 100644 index f2615de035..0000000000 --- a/cursor-vs-copilot-python/github-copilot/copilot-instructions.md +++ /dev/null @@ -1,7 +0,0 @@ -# Repository Instructions - -- Use type hints for all functions, return values, and dataclass fields. -- Parse YAML frontmatter with `yaml.safe_load()`. Do not manually parse YAML. -- Use `pathlib.Path` for all file and directory operations. -- Use parameterized SQL queries for every SQLite operation. -- Represent notes as dataclasses rather than dictionaries. diff --git a/cursor-vs-copilot-python/github-copilot/pyproject.toml b/cursor-vs-copilot-python/github-copilot/pyproject.toml new file mode 100644 index 0000000000..74b7d88390 --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "notes-manager-copilot-test" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "pyyaml==6.0.2", +] + +[project.optional-dependencies] +dev = [ + "pytest==9.0.3", +] + +[project.scripts] +notes = "notes_manager_copilot_test.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/__init__.py b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/cli.py b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/cli.py new file mode 100644 index 0000000000..b550c0f9b9 --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/cli.py @@ -0,0 +1,112 @@ +"""Command-line interface for the note manager.""" + +import argparse +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +from .markdown_io import note_to_markdown +from .models import Note +from .store import NoteStore + +DEFAULT_DB_PATH = Path.home() / ".notes-manager-copilot-test" / "notes.db" + + +def _notes_dir(db_path: Path) -> Path: + return db_path.parent / "notes" + + +def _slugify(title: str) -> str: + slug = re.sub(r"[^A-Za-z0-9._-]+", "_", title).strip("_") + return slug or "untitled" + + +def _write_note_file(notes_dir: Path, note: Note) -> None: + notes_dir.mkdir(parents=True, exist_ok=True) + (notes_dir / f"{_slugify(note.title)}.md").write_text( + note_to_markdown(note) + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="notes", description="Manage Markdown notes" + ) + parser.add_argument( + "--db", + type=Path, + default=DEFAULT_DB_PATH, + help="Path to the SQLite database file", + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + add_parser = subparsers.add_parser("add", help="Add a note") + add_parser.add_argument("title") + add_parser.add_argument("body") + add_parser.add_argument("--tags", nargs="*", default=[]) + + search_parser = subparsers.add_parser( + "search", help="Search notes by title or body" + ) + search_parser.add_argument("query") + + list_tag_parser = subparsers.add_parser( + "list-tag", help="List notes with a given tag" + ) + list_tag_parser.add_argument("tag") + + get_parser = subparsers.add_parser("get", help="Get a note by title") + get_parser.add_argument("title") + + return parser + + +def _print_note(note: Note) -> None: + print(note_to_markdown(note)) + print() + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + args.db.parent.mkdir(parents=True, exist_ok=True) + store = NoteStore(args.db) + try: + if args.command == "add": + note = Note( + title=args.title, + body=args.body, + tags=list(args.tags), + created_at=datetime.now(timezone.utc), + ) + store.add_note(note) + _write_note_file(_notes_dir(args.db), note) + print(f"Added note {note.title!r}") + elif args.command == "search": + notes = store.search_notes(args.query) + if not notes: + print("No notes found") + for note in notes: + _print_note(note) + elif args.command == "list-tag": + notes = store.list_by_tag(args.tag) + if not notes: + print("No notes found") + for note in notes: + _print_note(note) + elif args.command == "get": + note = store.get_by_title(args.title) + if note is None: + print(f"No note titled {args.title!r}", file=sys.stderr) + return 1 + _print_note(note) + finally: + store.close() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/markdown_io.py b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/markdown_io.py new file mode 100644 index 0000000000..006c0dbbdb --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/markdown_io.py @@ -0,0 +1,42 @@ +"""Serialize and parse notes as Markdown files with YAML frontmatter.""" + +from datetime import datetime + +import yaml + +from .models import Note + +FRONTMATTER_DELIMITER = "---" + + +def note_to_markdown(note: Note) -> str: + """Render a note as Markdown text with a YAML frontmatter header.""" + frontmatter = { + "title": note.title, + "tags": note.tags, + "created_at": note.created_at.isoformat(), + } + frontmatter_text = yaml.safe_dump(frontmatter, sort_keys=False) + return f"{FRONTMATTER_DELIMITER}\n{frontmatter_text}{FRONTMATTER_DELIMITER}\n{note.body}" + + +def note_from_markdown(text: str) -> Note: + """Parse Markdown text with a YAML frontmatter header into a note.""" + if not text.startswith(f"{FRONTMATTER_DELIMITER}\n"): + raise ValueError("Markdown text is missing a YAML frontmatter header") + + _, frontmatter_text, body = text.split(FRONTMATTER_DELIMITER, 2) + frontmatter = yaml.safe_load(frontmatter_text) or {} + + title = frontmatter.get("title", "") + tags = list(frontmatter.get("tags") or []) + created_at_raw = frontmatter.get("created_at") + created_at = ( + datetime.fromisoformat(created_at_raw) + if created_at_raw + else datetime.utcnow() + ) + + return Note( + title=title, body=body.lstrip("\n"), tags=tags, created_at=created_at + ) diff --git a/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/models.py b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/models.py new file mode 100644 index 0000000000..198fba5c0a --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/models.py @@ -0,0 +1,20 @@ +"""Data model for notes.""" + +from dataclasses import dataclass, field +from datetime import datetime, timezone + + +@dataclass +class Note: + """A single note.""" + + title: str + body: str + tags: list[str] = field(default_factory=list) + created_at: datetime = field( + default_factory=lambda: datetime.now(timezone.utc) + ) + updated_at: datetime = field( + default_factory=lambda: datetime.now(timezone.utc) + ) + is_archived: bool = False diff --git a/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/store.py b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/store.py new file mode 100644 index 0000000000..87f322a28d --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/src/notes_manager_copilot_test/store.py @@ -0,0 +1,80 @@ +"""SQLite-backed storage for notes.""" + +import json +import sqlite3 +from datetime import datetime +from os import PathLike + +from .models import Note + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL UNIQUE, + body TEXT NOT NULL, + tags TEXT NOT NULL, + created_at TEXT NOT NULL +) +""" + + +class NoteStore: + """Stores and queries notes in a SQLite database.""" + + def __init__(self, db_path: str | PathLike[str]) -> None: + self._conn = sqlite3.connect(db_path) + self._conn.row_factory = sqlite3.Row + self._conn.execute(_SCHEMA) + self._conn.commit() + + def add_note(self, note: Note) -> None: + """Insert a new note into the store.""" + self._conn.execute( + "INSERT INTO notes (title, body, tags, created_at) VALUES (?, ?, ?, ?)", + ( + note.title, + note.body, + json.dumps(note.tags), + note.created_at.isoformat(), + ), + ) + self._conn.commit() + + def search_notes(self, query: str) -> list[Note]: + """Return notes whose title or body contains the given query text.""" + rows = self._conn.execute( + "SELECT * FROM notes WHERE title LIKE ? OR body LIKE ? ORDER BY created_at", + (f"%{query}%", f"%{query}%"), + ).fetchall() + return [self._row_to_note(row) for row in rows] + + def list_by_tag(self, tag: str) -> list[Note]: + """Return notes tagged with the given tag.""" + rows = self._conn.execute( + "SELECT * FROM notes ORDER BY created_at" + ).fetchall() + return [ + note + for row in rows + if tag in (note := self._row_to_note(row)).tags + ] + + def get_by_title(self, title: str) -> Note | None: + """Return the note with the given title, or None if it doesn't exist.""" + row = self._conn.execute( + "SELECT * FROM notes WHERE title = ?", (title,) + ).fetchone() + return self._row_to_note(row) if row is not None else None + + def close(self) -> None: + """Close the underlying database connection.""" + self._conn.close() + + @staticmethod + def _row_to_note(row: sqlite3.Row) -> Note: + return Note( + title=row["title"], + body=row["body"], + tags=json.loads(row["tags"]), + created_at=datetime.fromisoformat(row["created_at"]), + ) diff --git a/cursor-vs-copilot-python/github-copilot/tests/__init__.py b/cursor-vs-copilot-python/github-copilot/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cursor-vs-copilot-python/github-copilot/tests/test_cli.py b/cursor-vs-copilot-python/github-copilot/tests/test_cli.py new file mode 100644 index 0000000000..1035d0fc0e --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/tests/test_cli.py @@ -0,0 +1,54 @@ +import pytest + +from notes_manager_copilot_test.cli import main + + +@pytest.fixture +def db_path(tmp_path): + return tmp_path / "notes.db" + + +def test_add_and_get(db_path, capsys): + exit_code = main( + ["--db", str(db_path), "add", "Title", "Body text", "--tags", "a", "b"] + ) + assert exit_code == 0 + capsys.readouterr() + + exit_code = main(["--db", str(db_path), "get", "Title"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "title: Title" in output + assert "Body text" in output + + +def test_get_missing_returns_error(db_path, capsys): + exit_code = main(["--db", str(db_path), "get", "Nope"]) + assert exit_code == 1 + + +def test_search(db_path, capsys): + main( + ["--db", str(db_path), "add", "Shopping", "Buy milk", "--tags", "home"] + ) + capsys.readouterr() + + exit_code = main(["--db", str(db_path), "search", "milk"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "Shopping" in output + + +def test_list_tag(db_path, capsys): + main(["--db", str(db_path), "add", "Note1", "Body1", "--tags", "work"]) + capsys.readouterr() + + exit_code = main(["--db", str(db_path), "list-tag", "work"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "Note1" in output + + exit_code = main(["--db", str(db_path), "list-tag", "missing"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "No notes found" in output diff --git a/cursor-vs-copilot-python/github-copilot/tests/test_markdown_io.py b/cursor-vs-copilot-python/github-copilot/tests/test_markdown_io.py new file mode 100644 index 0000000000..f7bc7faa43 --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/tests/test_markdown_io.py @@ -0,0 +1,40 @@ +from datetime import datetime + +import pytest + +from notes_manager_copilot_test.markdown_io import ( + note_from_markdown, + note_to_markdown, +) +from notes_manager_copilot_test.models import Note + + +def test_round_trip(): + note = Note( + title="My Note", + body="Some body text.\n", + tags=["work", "ideas"], + created_at=datetime(2024, 1, 1, 12, 30), + ) + markdown = note_to_markdown(note) + parsed = note_from_markdown(markdown) + + assert parsed.title == note.title + assert parsed.body == note.body + assert parsed.tags == note.tags + assert parsed.created_at == note.created_at + + +def test_note_to_markdown_contains_frontmatter(): + note = Note( + title="T", body="B", tags=["x"], created_at=datetime(2024, 1, 1) + ) + markdown = note_to_markdown(note) + assert markdown.startswith("---\n") + assert "title: T" in markdown + assert "tags:" in markdown + + +def test_note_from_markdown_missing_frontmatter_raises(): + with pytest.raises(ValueError): + note_from_markdown("no frontmatter here") diff --git a/cursor-vs-copilot-python/github-copilot/tests/test_models.py b/cursor-vs-copilot-python/github-copilot/tests/test_models.py new file mode 100644 index 0000000000..cafcceb78e --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/tests/test_models.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from notes_manager_copilot_test.models import Note + + +def test_note_defaults(): + note = Note(title="Title", body="Body") + assert note.title == "Title" + assert note.body == "Body" + assert note.tags == [] + assert isinstance(note.created_at, datetime) + + +def test_note_explicit_fields(): + created_at = datetime(2024, 1, 1) + note = Note( + title="Title", body="Body", tags=["a", "b"], created_at=created_at + ) + assert note.tags == ["a", "b"] + assert note.created_at == created_at diff --git a/cursor-vs-copilot-python/github-copilot/tests/test_store.py b/cursor-vs-copilot-python/github-copilot/tests/test_store.py new file mode 100644 index 0000000000..038c8b4b7d --- /dev/null +++ b/cursor-vs-copilot-python/github-copilot/tests/test_store.py @@ -0,0 +1,93 @@ +from datetime import datetime + +import pytest + +from notes_manager_copilot_test.models import Note +from notes_manager_copilot_test.store import NoteStore + + +@pytest.fixture +def store(tmp_path): + db_path = tmp_path / "notes.db" + note_store = NoteStore(db_path) + yield note_store + note_store.close() + + +def test_add_and_get_by_title(store): + note = Note( + title="Groceries", + body="Milk, eggs", + tags=["home"], + created_at=datetime(2024, 1, 1), + ) + store.add_note(note) + + fetched = store.get_by_title("Groceries") + assert fetched is not None + assert fetched.title == "Groceries" + assert fetched.body == "Milk, eggs" + assert fetched.tags == ["home"] + assert fetched.created_at == datetime(2024, 1, 1) + + +def test_get_by_title_missing_returns_none(store): + assert store.get_by_title("Nope") is None + + +def test_search_notes(store): + store.add_note( + Note( + title="Trip plan", + body="Visit museum", + tags=["travel"], + created_at=datetime(2024, 1, 1), + ) + ) + store.add_note( + Note( + title="Recipe", + body="Bake bread", + tags=["food"], + created_at=datetime(2024, 1, 2), + ) + ) + + results = store.search_notes("museum") + assert len(results) == 1 + assert results[0].title == "Trip plan" + + title_results = store.search_notes("Recipe") + assert len(title_results) == 1 + assert title_results[0].title == "Recipe" + + assert store.search_notes("nonexistent") == [] + + +def test_list_by_tag(store): + store.add_note( + Note( + title="A", body="a", tags=["work"], created_at=datetime(2024, 1, 1) + ) + ) + store.add_note( + Note( + title="B", + body="b", + tags=["personal"], + created_at=datetime(2024, 1, 2), + ) + ) + store.add_note( + Note( + title="C", + body="c", + tags=["work", "urgent"], + created_at=datetime(2024, 1, 3), + ) + ) + + work_notes = store.list_by_tag("work") + assert {n.title for n in work_notes} == {"A", "C"} + + assert store.list_by_tag("missing") == [] diff --git a/cursor-vs-windsurf-python/prompts.md b/cursor-vs-windsurf-python/prompts.md index e89ca1d639..43ff7eb5de 100644 --- a/cursor-vs-windsurf-python/prompts.md +++ b/cursor-vs-windsurf-python/prompts.md @@ -62,6 +62,8 @@ Type this into a file and let each editor complete it: @dataclass class RetryMetadata: attempts_made: int + + # ... ``` diff --git a/django-pagination/README.md b/django-pagination/README.md index 006ca99b16..3aeffd42b3 100644 --- a/django-pagination/README.md +++ b/django-pagination/README.md @@ -67,7 +67,6 @@ Add the Python keywords to your database: >>> for kw in keyword.kwlist: ... k = Keyword(name=kw) ... k.save() -... ``` Verify that the keywords were added to your database: diff --git a/flask-connexion-rest-part-2/README.md b/flask-connexion-rest-part-2/README.md index ae4f41da5e..74e1298a27 100644 --- a/flask-connexion-rest-part-2/README.md +++ b/flask-connexion-rest-part-2/README.md @@ -51,7 +51,6 @@ Navigate inside the `rp_flask_api/`, enter the [Python interactive shell](https: >>> for person_data in people: ... insert_cmd = f"INSERT INTO person VALUES ({person_data})" ... conn.execute(insert_cmd) -... <sqlite3.Cursor object at 0x104ac4dc0> <sqlite3.Cursor object at 0x104ac4f40> <sqlite3.Cursor object at 0x104ac4fc0> diff --git a/flush-print/README.md b/flush-print/README.md index c9a940b00e..d826c8492c 100644 --- a/flush-print/README.md +++ b/flush-print/README.md @@ -42,7 +42,7 @@ SLIGHTLY_TOO_LARGE_FOR_BUFFER = 80_000 # Script paused at 10919 bufsize = 80_000 - 10919 -print(bufsize) # 69081 <-- Your buffer size approximation +print(bufsize) # 69081 <-- Your buffer size approximation ``` You can divide the number you get by `1000` to get an estimation of your buffer size for stdout in kilobytes. In the example above, on a macOS system with a M1 chip, the buffer size of stdout when interacting with it through Python's `print()` would therefore be approximately 69 kilobytes. diff --git a/python-copy/benchmark.py b/python-copy/benchmark.py index 5533ee28c0..1f73eccd10 100644 --- a/python-copy/benchmark.py +++ b/python-copy/benchmark.py @@ -32,7 +32,7 @@ def benchmark(container, executions): def sliceable(instance): try: instance[0:1] - except (TypeError, KeyError): + except TypeError, KeyError: return False else: return True diff --git a/python-eval-mathrepl/mathrepl.py b/python-eval-mathrepl/mathrepl.py index 4a03e266d1..411a3e869c 100644 --- a/python-eval-mathrepl/mathrepl.py +++ b/python-eval-mathrepl/mathrepl.py @@ -53,7 +53,7 @@ def main(): # Read user's input try: expression = input(f"{PS1} ") - except (KeyboardInterrupt, EOFError): + except KeyboardInterrupt, EOFError: raise SystemExit() # Handle special commands diff --git a/python-import/namespace_package/local/serializers/factory.py b/python-import/namespace_package/local/serializers/factory.py index 917036f10a..d50ac4ce68 100644 --- a/python-import/namespace_package/local/serializers/factory.py +++ b/python-import/namespace_package/local/serializers/factory.py @@ -7,7 +7,7 @@ def get_serializer(format): try: module = importlib.import_module(f"serializers.{format}") serializer = getattr(module, f"{format.title()}Serializer") - except (ImportError, AttributeError): + except ImportError, AttributeError: raise ValueError(f"Unknown format {format!r}") from None return serializer() diff --git a/python-import/population_quiz/population_quiz.py b/python-import/population_quiz/population_quiz.py index ddea1252a1..f37ac6c5aa 100644 --- a/python-import/population_quiz/population_quiz.py +++ b/python-import/population_quiz/population_quiz.py @@ -46,7 +46,7 @@ def run_quiz(population, num_questions, num_countries): try: guess_idx = int(guess_str) - 1 guess = countries[guess_idx] - except (ValueError, IndexError): + except ValueError, IndexError: print(f"Please answer between 1 and {num_countries}") else: break diff --git a/python-multiple-exceptions/exception_pass.py b/python-multiple-exceptions/exception_pass.py index 959665791b..7169a4ac67 100644 --- a/python-multiple-exceptions/exception_pass.py +++ b/python-multiple-exceptions/exception_pass.py @@ -1,5 +1,5 @@ try: with open("file.txt", mode="rt") as f: print(f.readlines()) -except (FileNotFoundError, PermissionError): +except FileNotFoundError, PermissionError: pass diff --git a/python-multiple-exceptions/multiple_exceptions.py b/python-multiple-exceptions/multiple_exceptions.py index f880feff26..da5b258cae 100644 --- a/python-multiple-exceptions/multiple_exceptions.py +++ b/python-multiple-exceptions/multiple_exceptions.py @@ -2,5 +2,5 @@ first = float(input("What is your first number? ")) second = float(input("What is your second number? ")) print(f"{first} divided by {second} is {first / second}") -except (ZeroDivisionError, ValueError): +except ZeroDivisionError, ValueError: print("There was an error") diff --git a/python-type-checking/hearts.py b/python-type-checking/hearts.py index 4f93f572e2..e2c02c9030 100644 --- a/python-type-checking/hearts.py +++ b/python-type-checking/hearts.py @@ -146,7 +146,7 @@ def play_card(self, played: List[Card], hearts_broken: bool) -> Card: try: card_num = int(input(f" {self.name}, choose card: ")) card = playable[card_num] - except (ValueError, IndexError): + except ValueError, IndexError: pass else: break diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py index e1094c004a..1ba5e50a8c 100644 --- a/structural-pattern-matching/guessing_game.py +++ b/structural-pattern-matching/guessing_game.py @@ -61,6 +61,6 @@ def bye(): if __name__ == "__main__": try: main() - except (KeyboardInterrupt, EOFError): + except KeyboardInterrupt, EOFError: print() bye() diff --git a/wordcount/tests/realpython/HOWTO.md b/wordcount/tests/realpython/HOWTO.md index 8ca1bd7be5..a6aaa420d5 100644 --- a/wordcount/tests/realpython/HOWTO.md +++ b/wordcount/tests/realpython/HOWTO.md @@ -21,20 +21,18 @@ Inside each task file, create a class decorated with the `@task()` decorator: ```python from realpython import task + @task( number=1, name="Run the wordcount Command", url="https://realpython.com/lessons/run-the-wordcount-command-task/", ) class Test: - def test_one(self): - ... - - def test_two(self): - ... + def test_one(self): ... - def test_three(self): - ... + def test_two(self): ... + + def test_three(self): ... ``` This class can be named anything, e.g., `Test`, and you can reuse this name across different files if you want to. @@ -57,6 +55,7 @@ You can associate resources common to all test methods by placing the correspond ```python from realpython import task, tutorial, course, podcast + @task( number=1, name="Run the wordcount Command", @@ -64,8 +63,7 @@ from realpython import task, tutorial, course, podcast ) @tutorial("python-comments-guide") @course("writing-comments-python", "Writing Comments in Python") -class Test: - ... +class Test: ... ``` This will cascade down to the individual test methods, meaning that if one of them fails, then we'll include that resource on the list of hints. @@ -75,23 +73,20 @@ In contrast, decorating the individual test methods will let you associate resou ```python from realpython import task, tutorial, course, podcast + @task( number=1, name="Run the wordcount Command", url="https://realpython.com/lessons/run-the-wordcount-command-task/", ) class Test: - - def test_one(self): - ... - + def test_one(self): ... + @course("writing-comments-python", "Writing Comments in Python") - def test_two(self): - ... - + def test_two(self): ... + @tutorial("python-comments-guide") - def test_three(self): - ... + def test_three(self): ... ``` These decorators expect the **slug** to identify a resource in the CMS. If you don't provide a title, which is an optional parameter, then the slug will be automatically prettified and used as a link label. @@ -101,8 +96,7 @@ These decorators expect the **slug** to identify a resource in the CMS. If you d By default, the plugin will try to prettify the acceptance criteria shown in the report based on the name of the corresponding test method, e.g.: ```python -def test_reports_zeros_on_an_empty_stream(self): - ... +def test_reports_zeros_on_an_empty_stream(self): ... ``` ...becomes "_Reports zeros on an empty stream_." @@ -121,15 +115,18 @@ pytest allows you to run the same test method against different parameters (data ```python import pytest -@pytest.mark.parametrize("flags", [ - [], - ["-l"], - ["-w"], - ["-c"], - ["-l", "-w", "-c"], -]) -def test_always_displays_counts_in_the_same_order(self, flags): - ... + +@pytest.mark.parametrize( + "flags", + [ + [], + ["-l"], + ["-w"], + ["-c"], + ["-l", "-w", "-c"], + ], +) +def test_always_displays_counts_in_the_same_order(self, flags): ... ``` The resulting report will append the values of the parameters to the name of the acceptance criteria. This will work regardless of whether you provde a docstring or not. @@ -141,11 +138,11 @@ By default, each test method will time out after a predefined number of seconds. ```python import pytest + @task(...) class Test: @pytest.mark.timeout(3.5) - def test_one(self): - ... + def test_one(self): ... ``` ## Running Tests in DEBUG Mode @@ -187,13 +184,12 @@ However, it sill won't show the **expected vs. actual**. If you want to do that, ```python from realpython import task, assert_equals + @task(...) class Test: def test_one(self): assert_equals( - "expected", - function(), - "Your function should return XYZ" + "expected", function(), "Your function should return XYZ" ) ``` @@ -202,13 +198,14 @@ Note that the order of these arguments matters! The _expected_ value always come ```python from realpython import task, assert_equals + @task(...) class Test: def test_one(self): assert_equals( expected="expected", actual=function(), - message="Your function should return XYZ" + message="Your function should return XYZ", ) ``` @@ -217,6 +214,7 @@ If you just want to show the expected vs actual without any extra message, then ```python from realpython import task, assert_equals + @task(...) class Test: def test_one(self): From 9f1950298590f0ef4a65dcf94eb7320a3a78dfaf Mon Sep 17 00:00:00 2001 From: Real Python Bot <42617967+realpython-bot@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:17:56 +0200 Subject: [PATCH 14/15] Revert repo-wide formatter changes outside the article folder (#803) A repo-wide `ruff format` run reformatted 14 files belonging to 13 unrelated tutorials. Because pyproject.toml sets target-version = "py314", ruff applied PEP 758 and stripped the parentheses from multi-exception except clauses in 9 published sample files: except (ZeroDivisionError, ValueError): -> except ZeroDivisionError, ValueError: That syntax is valid on Python 3.14 but a SyntaxError on 3.13 and earlier, so those materials would stop running for most readers - and the CI matrix is Python 3.14 only, so it stays green either way. Reverts all 14 files to master. No changes to cursor-vs-copilot-python/. Co-authored-by: RPBot <dan+rpbot@realpython.com> --- asyncio-walkthrough/areq.py | 2 +- cursor-vs-windsurf-python/prompts.md | 2 - django-pagination/README.md | 1 + flask-connexion-rest-part-2/README.md | 1 + flush-print/README.md | 2 +- python-copy/benchmark.py | 2 +- python-eval-mathrepl/mathrepl.py | 2 +- .../local/serializers/factory.py | 2 +- .../population_quiz/population_quiz.py | 2 +- python-multiple-exceptions/exception_pass.py | 2 +- .../multiple_exceptions.py | 2 +- python-type-checking/hearts.py | 2 +- structural-pattern-matching/guessing_game.py | 2 +- wordcount/tests/realpython/HOWTO.md | 68 ++++++++++--------- 14 files changed, 47 insertions(+), 45 deletions(-) diff --git a/asyncio-walkthrough/areq.py b/asyncio-walkthrough/areq.py index 97bb63cc45..12ae0cde14 100644 --- a/asyncio-walkthrough/areq.py +++ b/asyncio-walkthrough/areq.py @@ -74,7 +74,7 @@ async def parse(url: str, session: ClientSession, **kwargs) -> set: try: # Ensure we return an absolute path. abslink = urllib.parse.urljoin(url, link) - except urllib.error.URLError, ValueError: + except (urllib.error.URLError, ValueError): logger.exception("Error parsing URL: %s", link) pass else: diff --git a/cursor-vs-windsurf-python/prompts.md b/cursor-vs-windsurf-python/prompts.md index 43ff7eb5de..e89ca1d639 100644 --- a/cursor-vs-windsurf-python/prompts.md +++ b/cursor-vs-windsurf-python/prompts.md @@ -62,8 +62,6 @@ Type this into a file and let each editor complete it: @dataclass class RetryMetadata: attempts_made: int - - # ... ``` diff --git a/django-pagination/README.md b/django-pagination/README.md index 3aeffd42b3..006ca99b16 100644 --- a/django-pagination/README.md +++ b/django-pagination/README.md @@ -67,6 +67,7 @@ Add the Python keywords to your database: >>> for kw in keyword.kwlist: ... k = Keyword(name=kw) ... k.save() +... ``` Verify that the keywords were added to your database: diff --git a/flask-connexion-rest-part-2/README.md b/flask-connexion-rest-part-2/README.md index 74e1298a27..ae4f41da5e 100644 --- a/flask-connexion-rest-part-2/README.md +++ b/flask-connexion-rest-part-2/README.md @@ -51,6 +51,7 @@ Navigate inside the `rp_flask_api/`, enter the [Python interactive shell](https: >>> for person_data in people: ... insert_cmd = f"INSERT INTO person VALUES ({person_data})" ... conn.execute(insert_cmd) +... <sqlite3.Cursor object at 0x104ac4dc0> <sqlite3.Cursor object at 0x104ac4f40> <sqlite3.Cursor object at 0x104ac4fc0> diff --git a/flush-print/README.md b/flush-print/README.md index d826c8492c..c9a940b00e 100644 --- a/flush-print/README.md +++ b/flush-print/README.md @@ -42,7 +42,7 @@ SLIGHTLY_TOO_LARGE_FOR_BUFFER = 80_000 # Script paused at 10919 bufsize = 80_000 - 10919 -print(bufsize) # 69081 <-- Your buffer size approximation +print(bufsize) # 69081 <-- Your buffer size approximation ``` You can divide the number you get by `1000` to get an estimation of your buffer size for stdout in kilobytes. In the example above, on a macOS system with a M1 chip, the buffer size of stdout when interacting with it through Python's `print()` would therefore be approximately 69 kilobytes. diff --git a/python-copy/benchmark.py b/python-copy/benchmark.py index 1f73eccd10..5533ee28c0 100644 --- a/python-copy/benchmark.py +++ b/python-copy/benchmark.py @@ -32,7 +32,7 @@ def benchmark(container, executions): def sliceable(instance): try: instance[0:1] - except TypeError, KeyError: + except (TypeError, KeyError): return False else: return True diff --git a/python-eval-mathrepl/mathrepl.py b/python-eval-mathrepl/mathrepl.py index 411a3e869c..4a03e266d1 100644 --- a/python-eval-mathrepl/mathrepl.py +++ b/python-eval-mathrepl/mathrepl.py @@ -53,7 +53,7 @@ def main(): # Read user's input try: expression = input(f"{PS1} ") - except KeyboardInterrupt, EOFError: + except (KeyboardInterrupt, EOFError): raise SystemExit() # Handle special commands diff --git a/python-import/namespace_package/local/serializers/factory.py b/python-import/namespace_package/local/serializers/factory.py index d50ac4ce68..917036f10a 100644 --- a/python-import/namespace_package/local/serializers/factory.py +++ b/python-import/namespace_package/local/serializers/factory.py @@ -7,7 +7,7 @@ def get_serializer(format): try: module = importlib.import_module(f"serializers.{format}") serializer = getattr(module, f"{format.title()}Serializer") - except ImportError, AttributeError: + except (ImportError, AttributeError): raise ValueError(f"Unknown format {format!r}") from None return serializer() diff --git a/python-import/population_quiz/population_quiz.py b/python-import/population_quiz/population_quiz.py index f37ac6c5aa..ddea1252a1 100644 --- a/python-import/population_quiz/population_quiz.py +++ b/python-import/population_quiz/population_quiz.py @@ -46,7 +46,7 @@ def run_quiz(population, num_questions, num_countries): try: guess_idx = int(guess_str) - 1 guess = countries[guess_idx] - except ValueError, IndexError: + except (ValueError, IndexError): print(f"Please answer between 1 and {num_countries}") else: break diff --git a/python-multiple-exceptions/exception_pass.py b/python-multiple-exceptions/exception_pass.py index 7169a4ac67..959665791b 100644 --- a/python-multiple-exceptions/exception_pass.py +++ b/python-multiple-exceptions/exception_pass.py @@ -1,5 +1,5 @@ try: with open("file.txt", mode="rt") as f: print(f.readlines()) -except FileNotFoundError, PermissionError: +except (FileNotFoundError, PermissionError): pass diff --git a/python-multiple-exceptions/multiple_exceptions.py b/python-multiple-exceptions/multiple_exceptions.py index da5b258cae..f880feff26 100644 --- a/python-multiple-exceptions/multiple_exceptions.py +++ b/python-multiple-exceptions/multiple_exceptions.py @@ -2,5 +2,5 @@ first = float(input("What is your first number? ")) second = float(input("What is your second number? ")) print(f"{first} divided by {second} is {first / second}") -except ZeroDivisionError, ValueError: +except (ZeroDivisionError, ValueError): print("There was an error") diff --git a/python-type-checking/hearts.py b/python-type-checking/hearts.py index e2c02c9030..4f93f572e2 100644 --- a/python-type-checking/hearts.py +++ b/python-type-checking/hearts.py @@ -146,7 +146,7 @@ def play_card(self, played: List[Card], hearts_broken: bool) -> Card: try: card_num = int(input(f" {self.name}, choose card: ")) card = playable[card_num] - except ValueError, IndexError: + except (ValueError, IndexError): pass else: break diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py index 1ba5e50a8c..e1094c004a 100644 --- a/structural-pattern-matching/guessing_game.py +++ b/structural-pattern-matching/guessing_game.py @@ -61,6 +61,6 @@ def bye(): if __name__ == "__main__": try: main() - except KeyboardInterrupt, EOFError: + except (KeyboardInterrupt, EOFError): print() bye() diff --git a/wordcount/tests/realpython/HOWTO.md b/wordcount/tests/realpython/HOWTO.md index a6aaa420d5..8ca1bd7be5 100644 --- a/wordcount/tests/realpython/HOWTO.md +++ b/wordcount/tests/realpython/HOWTO.md @@ -21,18 +21,20 @@ Inside each task file, create a class decorated with the `@task()` decorator: ```python from realpython import task - @task( number=1, name="Run the wordcount Command", url="https://realpython.com/lessons/run-the-wordcount-command-task/", ) class Test: - def test_one(self): ... - - def test_two(self): ... + def test_one(self): + ... + + def test_two(self): + ... - def test_three(self): ... + def test_three(self): + ... ``` This class can be named anything, e.g., `Test`, and you can reuse this name across different files if you want to. @@ -55,7 +57,6 @@ You can associate resources common to all test methods by placing the correspond ```python from realpython import task, tutorial, course, podcast - @task( number=1, name="Run the wordcount Command", @@ -63,7 +64,8 @@ from realpython import task, tutorial, course, podcast ) @tutorial("python-comments-guide") @course("writing-comments-python", "Writing Comments in Python") -class Test: ... +class Test: + ... ``` This will cascade down to the individual test methods, meaning that if one of them fails, then we'll include that resource on the list of hints. @@ -73,20 +75,23 @@ In contrast, decorating the individual test methods will let you associate resou ```python from realpython import task, tutorial, course, podcast - @task( number=1, name="Run the wordcount Command", url="https://realpython.com/lessons/run-the-wordcount-command-task/", ) class Test: - def test_one(self): ... - + + def test_one(self): + ... + @course("writing-comments-python", "Writing Comments in Python") - def test_two(self): ... - + def test_two(self): + ... + @tutorial("python-comments-guide") - def test_three(self): ... + def test_three(self): + ... ``` These decorators expect the **slug** to identify a resource in the CMS. If you don't provide a title, which is an optional parameter, then the slug will be automatically prettified and used as a link label. @@ -96,7 +101,8 @@ These decorators expect the **slug** to identify a resource in the CMS. If you d By default, the plugin will try to prettify the acceptance criteria shown in the report based on the name of the corresponding test method, e.g.: ```python -def test_reports_zeros_on_an_empty_stream(self): ... +def test_reports_zeros_on_an_empty_stream(self): + ... ``` ...becomes "_Reports zeros on an empty stream_." @@ -115,18 +121,15 @@ pytest allows you to run the same test method against different parameters (data ```python import pytest - -@pytest.mark.parametrize( - "flags", - [ - [], - ["-l"], - ["-w"], - ["-c"], - ["-l", "-w", "-c"], - ], -) -def test_always_displays_counts_in_the_same_order(self, flags): ... +@pytest.mark.parametrize("flags", [ + [], + ["-l"], + ["-w"], + ["-c"], + ["-l", "-w", "-c"], +]) +def test_always_displays_counts_in_the_same_order(self, flags): + ... ``` The resulting report will append the values of the parameters to the name of the acceptance criteria. This will work regardless of whether you provde a docstring or not. @@ -138,11 +141,11 @@ By default, each test method will time out after a predefined number of seconds. ```python import pytest - @task(...) class Test: @pytest.mark.timeout(3.5) - def test_one(self): ... + def test_one(self): + ... ``` ## Running Tests in DEBUG Mode @@ -184,12 +187,13 @@ However, it sill won't show the **expected vs. actual**. If you want to do that, ```python from realpython import task, assert_equals - @task(...) class Test: def test_one(self): assert_equals( - "expected", function(), "Your function should return XYZ" + "expected", + function(), + "Your function should return XYZ" ) ``` @@ -198,14 +202,13 @@ Note that the order of these arguments matters! The _expected_ value always come ```python from realpython import task, assert_equals - @task(...) class Test: def test_one(self): assert_equals( expected="expected", actual=function(), - message="Your function should return XYZ", + message="Your function should return XYZ" ) ``` @@ -214,7 +217,6 @@ If you just want to show the expected vs actual without any extra message, then ```python from realpython import task, assert_equals - @task(...) class Test: def test_one(self): From fc70d8fed5c71c5f3e3fa6d8c415f5c98ce3a24b Mon Sep 17 00:00:00 2001 From: Real Python Bot <42617967+realpython-bot@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:10:20 +0200 Subject: [PATCH 15/15] Fix prompt artifacts, README link, and requires-python (#804) - prompts.md: remove 5 stray `:contentReference[oaicite:N]{index=N}` citation artifacts left over from pasted assistant output - README.md: point at the real article URL (CMS post 2234, slug `cursor-vs-copilot`) instead of `cursor-vs-github-copilot-python`, which would 404, and match the published title - pyproject.toml (both packages): requires-python >=3.9 -> >=3.10. The pinned pytest==9.0.3 needs >=3.10, so dependency resolution failed outright on the declared floor Verified: both suites pass (16 + 13), ruff 0.14.1 format/check clean. Co-authored-by: RPBot <dan+rpbot@realpython.com> --- cursor-vs-copilot-python/README.md | 4 ++-- cursor-vs-copilot-python/cursor/pyproject.toml | 2 +- cursor-vs-copilot-python/github-copilot/pyproject.toml | 2 +- cursor-vs-copilot-python/prompts.md | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cursor-vs-copilot-python/README.md b/cursor-vs-copilot-python/README.md index a47aa5f4e2..00ce5f49eb 100644 --- a/cursor-vs-copilot-python/README.md +++ b/cursor-vs-copilot-python/README.md @@ -1,3 +1,3 @@ -# Cursor vs GitHub Copilot: Which AI Editor Is Better for Python? +# Cursor vs Copilot: Which AI Editor Is Better for Python? -This folder provides the prompts used in the Real Python tutorial [Cursor vs GitHub Copilot: Which AI Editor Is Better for Python?](https://realpython.com/cursor-vs-github-copilot-python/) +This folder provides the prompts used in the Real Python tutorial [Cursor vs Copilot: Which AI Editor Is Better for Python?](https://realpython.com/cursor-vs-copilot/) diff --git a/cursor-vs-copilot-python/cursor/pyproject.toml b/cursor-vs-copilot-python/cursor/pyproject.toml index 9cba48884e..714d81dfa2 100644 --- a/cursor-vs-copilot-python/cursor/pyproject.toml +++ b/cursor-vs-copilot-python/cursor/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "notes-manager-cursor-test" version = "0.1.0" description = "" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "pyyaml==6.0.2", "pytest==9.0.3", diff --git a/cursor-vs-copilot-python/github-copilot/pyproject.toml b/cursor-vs-copilot-python/github-copilot/pyproject.toml index 74b7d88390..8f1bdfde1d 100644 --- a/cursor-vs-copilot-python/github-copilot/pyproject.toml +++ b/cursor-vs-copilot-python/github-copilot/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "notes-manager-copilot-test" version = "0.1.0" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "pyyaml==6.0.2", ] diff --git a/cursor-vs-copilot-python/prompts.md b/cursor-vs-copilot-python/prompts.md index 45427780b6..08accb8c27 100644 --- a/cursor-vs-copilot-python/prompts.md +++ b/cursor-vs-copilot-python/prompts.md @@ -18,7 +18,7 @@ packaging conventions: ## Implementing the Application -Use this prompt in **Agent** mode after setting up the project. It defines the requirements for the command-line Markdown note manager, including Markdown storage, YAML frontmatter, the note data model, SQLite persistence, and the command-line interface. :contentReference[oaicite:1]{index=1} +Use this prompt in **Agent** mode after setting up the project. It defines the requirements for the command-line Markdown note manager, including Markdown storage, YAML frontmatter, the note data model, SQLite persistence, and the command-line interface. ```text Build a command-line Markdown note manager for this project. @@ -37,7 +37,7 @@ Requirements: ## Testing and Debugging -Use this prompt in **Agent** mode after deliberately removing the `self._conn.commit()` call from `NoteStore.add_note()`. It asks the editor to run the existing tests, investigate any failures, fix the underlying problem, and verify the fix by running the complete test suite again. :contentReference[oaicite:2]{index=2} +Use this prompt in **Agent** mode after deliberately removing the `self._conn.commit()` call from `NoteStore.add_note()`. It asks the editor to run the existing tests, investigate any failures, fix the underlying problem, and verify the fix by running the complete test suite again. ```text Run the existing pytest test suite. @@ -48,7 +48,7 @@ and rerun the tests until the entire suite passes. ## Planning the Archiving Feature -Use this prompt in **Plan** mode to compare how Cursor and GitHub Copilot plan a multi-file change before modifying the project. The feature adds support for archiving notes while keeping archived notes out of normal searches and listings unless explicitly requested. :contentReference[oaicite:3]{index=3} +Use this prompt in **Plan** mode to compare how Cursor and GitHub Copilot plan a multi-file change before modifying the project. The feature adds support for archiving notes while keeping archived notes out of normal searches and listings unless explicitly requested. ```text Create a plan to add support for archiving notes. @@ -62,7 +62,7 @@ Create a plan to add support for archiving notes. ## Reviewing the Database Layer -Use this prompt in **Ask** mode after deliberately replacing the parameterized search query with an interpolated SQL query. It asks the editor to inspect the database layer for correctness, SQL safety, and code quality without changing the implementation. :contentReference[oaicite:4]{index=4} +Use this prompt in **Ask** mode after deliberately replacing the parameterized search query with an interpolated SQL query. It asks the editor to inspect the database layer for correctness, SQL safety, and code quality without changing the implementation. ```text Review the database layer for correctness, SQL safety, @@ -72,7 +72,7 @@ Identify any issues and suggest improvements without modifying the code. ## Reviewing Pending Changes in Cursor -Use the `/review` command in Cursor after introducing the SQL injection vulnerability. Unlike the broader review in Ask mode, `/review` focuses on the changes in the current diff and identifies issues introduced by those changes. :contentReference[oaicite:5]{index=5} +Use the `/review` command in Cursor after introducing the SQL injection vulnerability. Unlike the broader review in Ask mode, `/review` focuses on the changes in the current diff and identifies issues introduced by those changes. ```text /review