From 3a4137e7064c3667cd947aad50bbe6f5d11521bf Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:23:08 +0100 Subject: [PATCH 1/7] mise: add difftastic so git can diff as we want --- mise.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mise.toml b/mise.toml index ffbd3a8..ca3c768 100644 --- a/mise.toml +++ b/mise.toml @@ -3,6 +3,7 @@ DATABASE_URL = "sqlite:///database.db" [tools] +difftastic = "latest" markdownlint-cli2 = "latest" prek = "latest" python = "3.14" From 736fd02b50cb1ced9a9d50e095aaa73003e1adbe Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:23:48 +0100 Subject: [PATCH 2/7] tests: create custom mark for test functions --- conftest.py | 11 +++++++++++ pyproject.toml | 1 + 2 files changed, 12 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..88fe65b --- /dev/null +++ b/conftest.py @@ -0,0 +1,11 @@ +from pytest import Config + + +def pytest_configure(config: Config): + """ + Registers a new mark 'final' that can be applied to tests. + + I did not want to add these in pyproject.toml. + """ + + config.addinivalue_line("markers", "final: the final test") diff --git a/pyproject.toml b/pyproject.toml index d4c43ed..c0b2f35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ omit = ["tests.py"] source_dirs = ["src/"] [tool.pytest] +# custom marks are created in conftest.py asyncio_mode = "auto" [tool.ruff] From 3268de32eaaa091e38747168c300b2d72cefaee3 Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:24:50 +0100 Subject: [PATCH 3/7] tests: add a bunch of tests; refactor helper structures --- tests.py | 75 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/tests.py b/tests.py index 0b547c6..b4a3f08 100644 --- a/tests.py +++ b/tests.py @@ -1,4 +1,3 @@ -from typing import TypedDict import pytest import logging @@ -7,23 +6,14 @@ from teilchensammler_cli.models import TeilchenCreate, make_teilchen_input logger = logging.getLogger(__name__) +@pytest.mark.final # don't run while we are fiddling with the app def test_initial_layout(snap_compare): from teilchensammler_cli.main import app assert snap_compare(app, terminal_size=(130, 40)) -class Teilchen(TypedDict): - """The things I do for my LSP...""" - - name: str - description: str - tags: str - text: str - number: int - - -empty_teilchen: Teilchen = { +empty_teilchen = { "name": "", "description": "", "tags": "", @@ -32,23 +22,62 @@ empty_teilchen: Teilchen = { } +def TC(**kwargs) -> TeilchenCreate | None: + """TC = TeilchenCreate + + Create Teilchen with desired attributes. + + Args: + input: mapping that must be well-formed enough to actually create a Teilchen + + Returns: + - `None` on empty input + - an instance of `TeilchenCreate` + """ + if kwargs: + arguments = empty_teilchen | kwargs + return TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type] + + @pytest.mark.parametrize( "example_input,expected", [ - ("", None), - ("a", None), - ("a.", {"name": "a", "text": "a."}), - ("aa", None), - ("aa.", {"name": "aa", "text": "aa."}), + # Not enough data + ("", TC()), + (".", TC()), + ("..", TC()), + (".a.", TC()), + ("a", TC()), + ("aa", TC()), + # Just enough for "name" + ("a.", TC(name="a", text="a.")), + ("aa.", TC(name="aa", text="aa.")), + ("a..", TC(name="a", text="a..")), + ("a.b.", TC(name="a", text="a.b.")), + # Just enough for "name" and "description" + ('a."b"', TC(name="a", description="b", text='a."b"')), + ('a. "b"', TC(name="a", description="b", text='a. "b"')), + ('a. ""', TC(name="a", description="", text='a. ""')), + ('. ""', TC()), + ('. "b"', TC()), + # Just enough for "name" and "description" and "tags" + ('a. "b" #c', TC(name="a", description="b", tags="#c", text='a. "b" #c')), + ('a. "b #d" #c', TC(name="a", description="b #d", tags="#c #d", text='a. "b #d" #c')), + # swap order in input, tag result is stable + ('a. "b #c" #d', TC(name="a", description="b #c", tags="#c #d", text='a. "b #c" #d')), + # Just enough for "name" and "tags" + ("a. #d", TC(name="a", description="", tags="#d", text="a. #d")), + ("a. #d#b", TC(name="a", description="", tags="#b #d", text="a. #d#b")), + ("a. ##b", TC(name="a", description="", tags="#b", text="a. ##b")), + ("a. #", TC(name="a", description="", tags="", text="a. #")), + ("a. ##", TC(name="a", description="", tags="", text="a. ##")), + # do we care about "number"? ], ) -async def test_teilchendata_must_include_period( +async def test_maketeilcheninput_can_create_desired_teilchen( example_input: str, expected: dict[str, str | int] | None ) -> None: - thing = expected - if expected: - arguments = empty_teilchen | expected - thing = TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type] + actual = await make_teilchen_input(example_input) - assert await make_teilchen_input(example_input) == thing + assert expected == actual From 00621a98590b92e91c6f2e1eb9bacd888c576e62 Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:25:58 +0100 Subject: [PATCH 4/7] project: ooops we moved textual-dev, didn't we? --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index d2b5c5a..9ece310 100644 --- a/uv.lock +++ b/uv.lock @@ -1161,7 +1161,6 @@ dependencies = [ { name = "orjson" }, { name = "sqlmodel" }, { name = "textual" }, - { name = "textual-dev" }, ] [package.dev-dependencies] @@ -1171,6 +1170,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-textual-snapshot" }, + { name = "textual-dev" }, { name = "twine" }, ] @@ -1181,7 +1181,6 @@ requires-dist = [ { name = "orjson", specifier = ">=3.11.4" }, { name = "sqlmodel", specifier = ">=0.0.27" }, { name = "textual", specifier = ">=6.7.1" }, - { name = "textual-dev", specifier = ">=1.8.0" }, ] [package.metadata.requires-dev] @@ -1191,6 +1190,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-textual-snapshot", specifier = ">=1.0.0" }, + { name = "textual-dev", specifier = ">=1.8.0" }, { name = "twine", specifier = ">=6.2.0" }, ] From dbc46d4e036109fe0a1616d6332c7a2360387a69 Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:26:34 +0100 Subject: [PATCH 5/7] just: default test recipe omits "final" tests --- justfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/justfile b/justfile index 9aec35a..3d77840 100644 --- a/justfile +++ b/justfile @@ -41,6 +41,9 @@ update-deps: # Run tests, ARGS are passed-through to pytest test *ARGS: + uv run pytest tests.py -m "not final" {{ ARGS }} + +alltests *ARGS: uv run pytest tests.py {{ ARGS }} # run tests and create coverage reports From 19f01c56bb678dc9fa9a982b8ffc4fb5964a7303 Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:27:34 +0100 Subject: [PATCH 6/7] models: skip leading quotation mark when extracting "description" --- src/teilchensammler_cli/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teilchensammler_cli/models.py b/src/teilchensammler_cli/models.py index f72650e..6a8764e 100644 --- a/src/teilchensammler_cli/models.py +++ b/src/teilchensammler_cli/models.py @@ -59,7 +59,7 @@ async def make_teilchen_input(text: str) -> TeilchenCreate | None: description_start = text.find('"', name_end + 1) description_end = text.find('"', description_start + 1) if description_end > description_start: - description = text[description_start:description_end] + description = text[description_start + 1 : description_end] else: description = "" From fe73b03bf3f3fef8776fb22072cb5ac946ee336c Mon Sep 17 00:00:00 2001 From: bronsen Date: Fri, 20 Feb 2026 21:28:11 +0100 Subject: [PATCH 7/7] codestyle: apparently, ty learned to deal with this situation or I changed something else in some other place --- src/teilchensammler_cli/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teilchensammler_cli/models.py b/src/teilchensammler_cli/models.py index 6a8764e..77619a8 100644 --- a/src/teilchensammler_cli/models.py +++ b/src/teilchensammler_cli/models.py @@ -21,7 +21,7 @@ class TeilchenCreate(SQLModel): class Teilchen(TeilchenCreate, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) # ty:ignore[unresolved-attribute] + id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) async def make_teilchen_input(text: str) -> TeilchenCreate | None: