diff --git a/checks/type_hints/type_hints1.py b/checks/type_hints/type_hints1.py index 48e1141..a614792 100644 --- a/checks/type_hints/type_hints1.py +++ b/checks/type_hints/type_hints1.py @@ -1,3 +1,14 @@ -assert "count" in __annotations__, "count should have a type annotation" -assert __annotations__["count"] is int, "count should be annotated as int" +# checks/type_hints/type_hints1.py + +import inspect + +annotations = globals().get("__annotations__") +annotate = globals().get("__annotate__") +if annotations is None and callable(annotate): + # Python 3.14+ stores module annotations in a callable `__annotate__` + # instead of the `__annotations__` dict. Read them in VALUE format. + format_value = getattr(getattr(inspect, "Format", None), "VALUE", 1) + annotations = annotate(format_value) +annotations = annotations or {} +assert annotations.get("count") is int, "count should be annotated as int" print("type_hints1 ✓") diff --git a/docs-site/assets/logo.png b/docs-site/assets/logo.png index c054493..b04f92d 100644 Binary files a/docs-site/assets/logo.png and b/docs-site/assets/logo.png differ diff --git a/docs/assets/branding/pythonlings-hero-source.html b/docs/assets/branding/pythonlings-hero-source.html new file mode 100644 index 0000000..5c3e663 --- /dev/null +++ b/docs/assets/branding/pythonlings-hero-source.html @@ -0,0 +1,55 @@ + + + + + + + + + + +
+
+
+
$ pip install pythonlings
+
$ pythonlings
+
✓ variables1✓ variables2● variables32/292
+
+
pythonlings
+
Learn Python by fixing tiny broken programs.
+
+ +
+ + diff --git a/docs/assets/branding/pythonlings-hero.png b/docs/assets/branding/pythonlings-hero.png index 0e5d4d8..f58c784 100644 Binary files a/docs/assets/branding/pythonlings-hero.png and b/docs/assets/branding/pythonlings-hero.png differ diff --git a/docs/assets/branding/pythonlings-logo-monochrome.png b/docs/assets/branding/pythonlings-logo-monochrome.png index 2fade0b..1ca2fb2 100644 Binary files a/docs/assets/branding/pythonlings-logo-monochrome.png and b/docs/assets/branding/pythonlings-logo-monochrome.png differ diff --git a/docs/assets/branding/pythonlings-logo.png b/docs/assets/branding/pythonlings-logo.png index 4498c55..0739375 100644 Binary files a/docs/assets/branding/pythonlings-logo.png and b/docs/assets/branding/pythonlings-logo.png differ diff --git a/docs/assets/branding/pythonlings-mascot.png b/docs/assets/branding/pythonlings-mascot.png index 3e840b8..3b41e54 100644 Binary files a/docs/assets/branding/pythonlings-mascot.png and b/docs/assets/branding/pythonlings-mascot.png differ diff --git a/tests/unit/test_type_hints1_check.py b/tests/unit/test_type_hints1_check.py new file mode 100644 index 0000000..dd2d2c2 --- /dev/null +++ b/tests/unit/test_type_hints1_check.py @@ -0,0 +1,63 @@ +# tests/unit/test_type_hints1_check.py + +from pathlib import Path + +from pythonlings.core.exercise import Exercise +from pythonlings.core.runner import run + +ROOT = Path(__file__).resolve().parent.parent.parent +CHECK_SRC = (ROOT / "checks" / "type_hints" / "type_hints1.py").read_text( + encoding="utf-8" +) + + +def _run_case(tmp_path: Path, exercise_src: str) -> object: + ex_path = tmp_path / "ex.py" + check_path = tmp_path / "check.py" + ex_path.write_text(exercise_src, encoding="utf-8") + check_path.write_text(CHECK_SRC, encoding="utf-8") + exercise = Exercise( + name="type_hints1", + path=ex_path, + check_path=check_path, + topic="type_hints", + hint="", + ) + return run(exercise) + + +def test_correct_annotation_passes(tmp_path: Path) -> None: + result = _run_case(tmp_path, "count: int = 0\n") + assert result.passed is True + assert "type_hints1 ✓" in result.stdout + + +def test_wrong_type_fails_with_clear_message(tmp_path: Path) -> None: + result = _run_case(tmp_path, "count: str = ''\n") + assert result.passed is False + assert "AssertionError" in result.stderr + assert "count should be annotated as int" in result.stderr + assert "NameError" not in result.stderr + + +def test_missing_annotation_fails_with_clear_message(tmp_path: Path) -> None: + result = _run_case(tmp_path, "total = 0\n") + assert result.passed is False + assert "AssertionError" in result.stderr + assert "count should be annotated as int" in result.stderr + assert "NameError" not in result.stderr + + +def test_none_annotation_hook_fails_with_clear_message(tmp_path: Path) -> None: + result = _run_case(tmp_path, "__annotate__ = None\n") + assert result.passed is False + assert "AssertionError" in result.stderr + assert "count should be annotated as int" in result.stderr + assert "TypeError" not in result.stderr + + +def test_callable_annotation_hook_passes(tmp_path: Path) -> None: + exercise_src = "def __annotate__(format_value):\n return {'count': int}\n" + result = _run_case(tmp_path, exercise_src) + assert result.passed is True + assert "type_hints1 ✓" in result.stdout