From 5911d6a41fe65046c7fed9f9d63cfc522c26b6e0 Mon Sep 17 00:00:00 2001 From: yukaty <254470+yukaty@users.noreply.github.com> Date: Mon, 17 Aug 2026 06:58:40 -0600 Subject: [PATCH 1/3] Add TDD with AI agents tutorial --- tdd-ai-agents/.claude/settings.json | 15 ++++++++ tdd-ai-agents/.gitignore | 13 +++++++ tdd-ai-agents/pyproject.toml | 7 ++++ tdd-ai-agents/tests/test_core.py | 46 ++++++++++++++++++++++++ tdd-ai-agents/version_check/__init__.py | 3 ++ tdd-ai-agents/version_check/core.py | 48 +++++++++++++++++++++++++ 6 files changed, 132 insertions(+) create mode 100644 tdd-ai-agents/.claude/settings.json create mode 100644 tdd-ai-agents/.gitignore create mode 100644 tdd-ai-agents/pyproject.toml create mode 100644 tdd-ai-agents/tests/test_core.py create mode 100644 tdd-ai-agents/version_check/__init__.py create mode 100644 tdd-ai-agents/version_check/core.py diff --git a/tdd-ai-agents/.claude/settings.json b/tdd-ai-agents/.claude/settings.json new file mode 100644 index 0000000000..9e4680d640 --- /dev/null +++ b/tdd-ai-agents/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "jq -e '.tool_input.file_path | contains(\"/tests/\")' >/dev/null && echo 'tests/ is read-only during implementation' >&2 && exit 2 || exit 0" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tdd-ai-agents/.gitignore b/tdd-ai-agents/.gitignore new file mode 100644 index 0000000000..38876dcbd2 --- /dev/null +++ b/tdd-ai-agents/.gitignore @@ -0,0 +1,13 @@ +# Virtual environment +venv/ +.venv/ + +# Python cache +__pycache__/ +*.py[cod] + +# Testing +.pytest_cache/ + +# OS files +.DS_Store diff --git a/tdd-ai-agents/pyproject.toml b/tdd-ai-agents/pyproject.toml new file mode 100644 index 0000000000..20fb19b765 --- /dev/null +++ b/tdd-ai-agents/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "version-check" +version = "0.1.0" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/tdd-ai-agents/tests/test_core.py b/tdd-ai-agents/tests/test_core.py new file mode 100644 index 0000000000..790a35db94 --- /dev/null +++ b/tdd-ai-agents/tests/test_core.py @@ -0,0 +1,46 @@ +import pytest +from version_check import Version + + +def test_equal_versions_compare_equal(): + assert Version("1.2.0") == Version("1.2.0") + + +def test_smaller_release_sorts_first(): + assert Version("1.2.0") < Version("1.3.0") + + +def test_double_digit_segments_sort_numerically(): + assert Version("3.9") < Version("3.10") + + +@pytest.mark.parametrize( + "lower, higher", + [ + ("1.0.dev1", "1.0"), # Dev before final + ("1.0a1", "1.0"), # Pre-release before final + ("1.0", "1.0.post1"), # Post-release after final + ("1.0.dev1", "1.0a1"), # Dev before pre-release + ("1.0a1", "1.0b1"), # Alpha before beta + ("1.0b1", "1.0rc1"), # Beta before release candidate + ("1.0.dev1", "1.0.post1"), # Dev before post, spanning the release + ], +) +def test_phase_ordering(lower, higher): + assert Version(lower) < Version(higher) + + +@pytest.mark.parametrize( + "text", + [ + "", # Empty string + " ", # Whitespace only + "1.x.0", # Non-numeric segment + "banana", # Not a version at all + "1..0", # Empty segment + "v1.0", # Leading junk + ], +) +def test_invalid_input_raises(text): + with pytest.raises(ValueError): + Version(text) diff --git a/tdd-ai-agents/version_check/__init__.py b/tdd-ai-agents/version_check/__init__.py new file mode 100644 index 0000000000..ec4fbc4920 --- /dev/null +++ b/tdd-ai-agents/version_check/__init__.py @@ -0,0 +1,3 @@ +from version_check.core import Version + +__all__ = ["Version"] diff --git a/tdd-ai-agents/version_check/core.py b/tdd-ai-agents/version_check/core.py new file mode 100644 index 0000000000..c0f1724006 --- /dev/null +++ b/tdd-ai-agents/version_check/core.py @@ -0,0 +1,48 @@ +import re +from functools import total_ordering + +_VERSION = re.compile( + r"^\s*(?P\d+(?:\.\d+)*)" + r"(?:(?Pa|b|rc)(?P\d+))?" + r"(?:\.dev(?P\d+))?" + r"(?:\.post(?P\d+))?\s*$" +) +_PRE_ORDER = {"a": 0, "b": 1, "rc": 2} + + +@total_ordering +class Version: + def __init__(self, text): + match = _VERSION.match(text) + if match is None: + raise ValueError(f"invalid version string: {text!r}") + self._release = tuple( + int(part) for part in match["release"].split(".") + ) + if match["pre_label"] is not None: + self._pre = (_PRE_ORDER[match["pre_label"]], int(match["pre_num"])) + else: + self._pre = None + self._dev = int(match["dev"]) if match["dev"] else None + self._post = int(match["post"]) if match["post"] else None + + def _key(self): + if self._dev is not None and self._pre is None and self._post is None: + phase = (0, self._dev) + elif self._pre is not None: + phase = (1, self._pre) + elif self._post is not None: + phase = (3, self._post) + else: + phase = (2, 0) + return (self._release, phase) + + def __eq__(self, other): + if not isinstance(other, Version): + return NotImplemented + return self._key() == other._key() + + def __lt__(self, other): + if not isinstance(other, Version): + return NotImplemented + return self._key() < other._key() From 5529179a2a26352ecade83bd4fd46b00955e0894 Mon Sep 17 00:00:00 2001 From: yukaty <254470+yukaty@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:47:02 -0600 Subject: [PATCH 2/3] Add README --- tdd-ai-agents/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tdd-ai-agents/README.md diff --git a/tdd-ai-agents/README.md b/tdd-ai-agents/README.md new file mode 100644 index 0000000000..5df6af529f --- /dev/null +++ b/tdd-ai-agents/README.md @@ -0,0 +1,2 @@ +# TDD With AI Agents: Design the Tests First, Let the Agent Implement +This folder contains sample code for the Real Python tutorial [TDD With AI Agents: Design the Tests First, Let the Agent Implement]. From d805146cbdf533f7553ffc6034fae8201bf7e56c Mon Sep 17 00:00:00 2001 From: yukaty <254470+yukaty@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:22:22 -0600 Subject: [PATCH 3/3] Update for TR#1 --- tdd-ai-agents/.claude/settings.json | 14 ++------ tdd-ai-agents/tests/test_core.py | 50 +++++++++++++++++++++-------- tdd-ai-agents/version_check/core.py | 8 ++--- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/tdd-ai-agents/.claude/settings.json b/tdd-ai-agents/.claude/settings.json index 9e4680d640..4f1bb96858 100644 --- a/tdd-ai-agents/.claude/settings.json +++ b/tdd-ai-agents/.claude/settings.json @@ -1,15 +1,5 @@ { - "hooks": { - "PreToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "jq -e '.tool_input.file_path | contains(\"/tests/\")' >/dev/null && echo 'tests/ is read-only during implementation' >&2 && exit 2 || exit 0" - } - ] - } - ] + "permissions": { + "deny": ["Edit(tests/**)"] } } \ No newline at end of file diff --git a/tdd-ai-agents/tests/test_core.py b/tdd-ai-agents/tests/test_core.py index 790a35db94..4a3d60b49e 100644 --- a/tdd-ai-agents/tests/test_core.py +++ b/tdd-ai-agents/tests/test_core.py @@ -10,20 +10,41 @@ def test_smaller_release_sorts_first(): assert Version("1.2.0") < Version("1.3.0") +def test_versions_with_different_lengths_compare(): + assert Version("1.2") < Version("1.2.1") + + +def test_greater_than_works(): + assert Version("2.0.0") > Version("1.9.9") + + +def test_unequal_versions_are_not_equal(): + assert Version("1.0.0") != Version("1.0.1") + + def test_double_digit_segments_sort_numerically(): assert Version("3.9") < Version("3.10") +def test_sorting_a_list_of_versions(): + versions = [Version("1.10.0"), Version("1.2.0"), Version("1.9.0")] + assert sorted(versions) == [ + Version("1.2.0"), + Version("1.9.0"), + Version("1.10.0"), + ] + + @pytest.mark.parametrize( "lower, higher", [ - ("1.0.dev1", "1.0"), # Dev before final - ("1.0a1", "1.0"), # Pre-release before final - ("1.0", "1.0.post1"), # Post-release after final - ("1.0.dev1", "1.0a1"), # Dev before pre-release - ("1.0a1", "1.0b1"), # Alpha before beta - ("1.0b1", "1.0rc1"), # Beta before release candidate - ("1.0.dev1", "1.0.post1"), # Dev before post, spanning the release + ("1.0.dev1", "1.0"), + ("1.0a1", "1.0"), + ("1.0", "1.0.post1"), + ("1.0.dev1", "1.0a1"), + ("1.0a1", "1.0b1"), + ("1.0b1", "1.0rc1"), + ("1.0.dev1", "1.0.post1"), ], ) def test_phase_ordering(lower, higher): @@ -33,14 +54,17 @@ def test_phase_ordering(lower, higher): @pytest.mark.parametrize( "text", [ - "", # Empty string - " ", # Whitespace only - "1.x.0", # Non-numeric segment - "banana", # Not a version at all - "1..0", # Empty segment - "v1.0", # Leading junk + "", + " ", + "1.x.0", + "banana", + "1..0", ], ) def test_invalid_input_raises(text): with pytest.raises(ValueError): Version(text) + + +def test_comparing_against_other_types_returns_notimplemented(): + assert Version("1.0").__eq__("1.0") is NotImplemented diff --git a/tdd-ai-agents/version_check/core.py b/tdd-ai-agents/version_check/core.py index c0f1724006..d4cfbc2480 100644 --- a/tdd-ai-agents/version_check/core.py +++ b/tdd-ai-agents/version_check/core.py @@ -4,8 +4,8 @@ _VERSION = re.compile( r"^\s*(?P\d+(?:\.\d+)*)" r"(?:(?Pa|b|rc)(?P\d+))?" - r"(?:\.dev(?P\d+))?" - r"(?:\.post(?P\d+))?\s*$" + r"(?:\.post(?P\d+))?" + r"(?:\.dev(?P\d+))?\s*$" ) _PRE_ORDER = {"a": 0, "b": 1, "rc": 2} @@ -23,11 +23,11 @@ def __init__(self, text): self._pre = (_PRE_ORDER[match["pre_label"]], int(match["pre_num"])) else: self._pre = None - self._dev = int(match["dev"]) if match["dev"] else None self._post = int(match["post"]) if match["post"] else None + self._dev = int(match["dev"]) if match["dev"] else None def _key(self): - if self._dev is not None and self._pre is None and self._post is None: + if self._dev is not None and self._pre is None: phase = (0, self._dev) elif self._pre is not None: phase = (1, self._pre)