Repository: trag1c/crossandra (language: python)

Issue:
Improve structure
- add a justfile
- ~~try putting dev-deps with regular deps~~ ([PEP 735](https://peps.python.org/pep-0735) is barely a draft)
- move `src/crossandra/` to `crossandra/`


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index f5bd58f..6769e21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,6 @@
 __pycache__/
 *.py[cod]
 *$py.class
-src/*
-!src/crossandra/
 
 # C extensions
 *.so
diff --git a/src/crossandra/__init__.py b/crossandra/__init__.py
similarity index 100%
rename from src/crossandra/__init__.py
rename to crossandra/__init__.py
diff --git a/src/crossandra/common.py b/crossandra/common.py
similarity index 99%
rename from src/crossandra/common.py
rename to crossandra/common.py
index c817b89..1747a15 100644
--- a/src/crossandra/common.py
+++ b/crossandra/common.py
@@ -1,4 +1,5 @@
 """A collection of commonly used Rules."""
+
 from .rule import Rule, RuleGroup
 
 _int = r"[0-9](?:[0-9_]*[0-9])?"
diff --git a/src/crossandra/exceptions.py b/crossandra/exceptions.py
similarity index 100%
rename from src/crossandra/exceptions.py
rename to crossandra/exceptions.py
diff --git a/src/crossandra/lib.py b/crossandra/lib.py
similarity index 91%
rename from src/crossandra/lib.py
rename to crossandra/lib.py
index 82a3881..79a55c0 100644
--- a/src/crossandra/lib.py
+++ b/crossandra/lib.py
@@ -36,15 +36,11 @@ def generate_tree(inp: Iterable[tuple[str, Enum]]) -> Tree:
     for k, v in inp:
         curr = result
         for c in k[:-1]:
-            _curr = curr.setdefault(c, {})
-            assert isinstance(_curr, dict)
-            curr = _curr
-        k = k[-1]
-        if dct := curr.get(k):
-            assert isinstance(dct, dict)
+            curr = cast(Tree, curr.setdefault(c, {}))
+        if dct := cast(Tree, curr.get(k[-1])):
             dct[""] = v
         else:
-            curr[k] = v
+            curr[k[-1]] = v
     return result
 
 
@@ -70,12 +66,12 @@ class Crossandra:
     """
 
     __slots__ = (
-        "__rules",
         "__conv_crlf",
         "__fast",
         "__ignored",
         "__keys",
         "__maxlen",
+        "__rules",
         "__suppress",
         "__tokens",
         "__tree",
@@ -119,7 +115,8 @@ def tokenize(self, code: str) -> list[Enum | Any]:
             toks = self.__tokenize_fast(code)
             if toks.is_ok():
                 return toks.unwrap()
-            raise CrossandraTokenizationError(f"invalid token: {toks.unwrap_err()!r}")
+            msg = f"invalid token: {toks.unwrap_err()!r}"
+            raise CrossandraTokenizationError(msg)
 
         tokens: list[Enum | Any] = []
         maxlen = self.__maxlen
@@ -142,9 +139,8 @@ def tokenize(self, code: str) -> list[Enum | Any]:
                     break
             else:
                 if not self.__suppress:
-                    raise CrossandraTokenizationError(
-                        f"invalid token: {token.unwrap_err()!r}"
-                    )
+                    msg = f"invalid token: {token.unwrap_err()!r}"
+                    raise CrossandraTokenizationError(msg)
                 code = code[1:]
 
         return tokens
@@ -170,9 +166,7 @@ def __handle(self, string: str) -> tuple[Result[Enum, str], int]:
             if c is None:
                 if "" not in tree:
                     break
-                c = tree[""]
-                assert isinstance(c, Enum)
-                return Ok(c), i
+                return Ok(cast(Enum, tree[""])), i
             if isinstance(c, Enum):
                 return Ok(c), i + 1
             tree = c
diff --git a/src/crossandra/py.typed b/crossandra/py.typed
similarity index 100%
rename from src/crossandra/py.typed
rename to crossandra/py.typed
diff --git a/src/crossandra/rule.py b/crossandra/rule.py
similarity index 97%
rename from src/crossandra/rule.py
rename to crossandra/rule.py
index 982e81b..373ac96 100644
--- a/src/crossandra/rule.py
+++ b/crossandra/rule.py
@@ -50,7 +50,8 @@ def __init__(
         ignore: bool = False,
     ) -> None:
         if ignore and converter:
-            raise CrossandraValueError("cannot use a converter when ignore=True")
+            msg = "cannot use a converter when ignore=True"
+            raise CrossandraValueError(msg)
         self.__ignore = ignore
         pattern_str = pattern.pattern if isinstance(pattern, Pattern) else pattern
         self.__pattern = pattern_str
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 0157b91..4d6e7cd 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,4 +1,3 @@
-black ~= 23.7.0
-mypy ~= 1.5.0
-pytest ~= 7.4.0
-ruff ~= 0.0.284
\ No newline at end of file
+mypy ~= 1.10.0
+pytest ~= 8.2.0
+ruff ~= 0.4.2
\ No newline at end of file
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..28289d1
--- /dev/null
+++ b/justfile
@@ -0,0 +1,14 @@
+[private]
+default:
+    @just --list
+
+install:
+    uv venv
+    uv pip install . -r dev-requirements.txt
+
+check:
+    python -m pytest
+    mypy --strict crossandra
+    mypy tests
+    ruff check
+    ruff format --check
diff --git a/pyproject.toml b/pyproject.toml
index 5ae5e73..03b7630 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,6 @@ requires-python = ">=3.8"
 dependencies = ["result ~= 0.9.0"]
 
 [tool.setuptools.packages.find]
-where = ["src"]
 include = ["crossandra*"]
 
 [tool.cibuildwheel.linux]
@@ -21,45 +20,14 @@ archs = ["x86_64", "universal2"]
 
 [tool.ruff]
 target-version = "py38"
-select = [
-    "A",
-    "ANN",
-    "ARG",
-    "B",
-    "BLE",
-    "C",
-    "E",
-    "ERA",
-    "F",
-    "FBT",
-    "I",
-    "ISC",
-    "N",
-    "PIE",
-    "Q",
-    "RET",
-    "RSE",
-    "RUF",
-    "SIM",
-    "SLF",
-    "TCH",
-    "TRY",
-    "UP",
-    "W",
-]
-ignore = [
-    "ANN101",
-    "ANN102",
-    "ANN401",
-    "RUF001",
-    "RUF002",
-    "RUF003",
-    "TRY003",
-]
-exclude = ["tests"]
 
-[tool.mypy]
-exclude = "tests"
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = ["COM", "D", "FIX", "ANN1", "ANN401", "ISC001", "ERA", "C9", "PLR0913"]
+
+[tool.ruff.lint.per-file-ignores]
+"crossandra/*" = ["PT"]
+"tests/*" = ["INP", "FBT", "PLC2701", "S101", "SLF001"]
 
 [build-system]
 requires = ["setuptools", "mypy", "result ~= 0.9.0"]
diff --git a/setup.py b/setup.py
index 8b30a80..2335c38 100644
--- a/setup.py
+++ b/setup.py
@@ -2,10 +2,10 @@
 from setuptools import setup
 
 sources = ("__init__.py", "lib.py", "rule.py", "common.py")
-ext_modules = mypycify([f"src/crossandra/{i}" for i in sources])
+ext_modules = mypycify([f"crossandra/{i}" for i in sources])
 
 setup(
     name="crossandra",
-    packages=["src/crossandra"],
+    packages=["crossandra"],
     ext_modules=ext_modules,
 )


Test patch:
diff --git a/tests/api_test.py b/tests/test_api.py
similarity index 51%
rename from tests/api_test.py
rename to tests/test_api.py
index 3d01210..63bdb1f 100644
--- a/tests/api_test.py
+++ b/tests/test_api.py
@@ -2,8 +2,10 @@
 
 import re
 from enum import Enum
+from typing import TYPE_CHECKING, Any
 
 import pytest
+
 from crossandra import (
     Crossandra,
     CrossandraTokenizationError,
@@ -14,23 +16,28 @@
 )
 from crossandra.rule import IGNORED, NOT_APPLIED
 
+if TYPE_CHECKING:
+    from test_common import RuleResult
+
 
 @pytest.mark.parametrize(
-    ["convert_crlf", "result"],
+    ("convert_crlf", "result"),
     [
         (True, ["a", "\n", "b", "\n", "c"]),
         (False, ["a", "\n", "b", "\r\n", "c"]),
-    ]
+    ],
 )
-def test_convert_crlf(convert_crlf, result):
-    assert Crossandra(
-        rules=[common.LETTER, common.NEWLINE],
-        convert_crlf=convert_crlf
-    ).tokenize("a\nb\r\nc") == result
+def test_convert_crlf(convert_crlf: bool, result: list[str]) -> None:
+    assert (
+        Crossandra(
+            rules=[common.LETTER, common.NEWLINE], convert_crlf=convert_crlf
+        ).tokenize("a\nb\r\nc")
+        == result
+    )
 
 
 @pytest.mark.parametrize(
-    ["ignored", "result"],
+    ("ignored", "result"),
     [
         ("", list("abbadeff")),
         ("ae", list("bbdff")),
@@ -38,34 +45,35 @@ def test_convert_crlf(convert_crlf, result):
         ("abdf", ["e"]),
         ("aaaaa", list("bbdeff")),
         ("fg", list("abbade")),
-    ]
+    ],
 )
-def test_ignored_characters(ignored, result):
-    assert Crossandra(
-        rules=[common.LETTER],
-        ignored_characters=ignored
-    ).tokenize("abbadeff") == result
+def test_ignored_characters(ignored: str, result: list[str]) -> None:
+    assert (
+        Crossandra(rules=[common.LETTER], ignored_characters=ignored).tokenize(
+            "abbadeff"
+        )
+        == result
+    )
 
 
-def test_ignore_whitespace_error():
+def test_ignore_whitespace_error() -> None:
     with pytest.raises(CrossandraTokenizationError):
         Crossandra(rules=[common.LETTER]).tokenize("a b")
 
 
-def test_ignore_whitespace():
-    assert Crossandra(
-        rules=[common.LETTER],
-        ignore_whitespace=True
-    ).tokenize("a b\nc\rde") == ["a", "b", "c", "d", "e"]
+def test_ignore_whitespace() -> None:
+    assert Crossandra(rules=[common.LETTER], ignore_whitespace=True).tokenize(
+        "a b\nc\rde"
+    ) == ["a", "b", "c", "d", "e"]
 
 
-def test_suppress_unknown_error():
+def test_suppress_unknown_error() -> None:
     with pytest.raises(CrossandraTokenizationError):
         Crossandra().tokenize("a")
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("1", []),
         ("hello there", ["hello", "there"]),
@@ -75,15 +83,15 @@ def test_suppress_unknown_error():
         ("hi.123-world", ["hi", "world"]),
         (
             "Mr. John Smith Jr. was born in the U.S.A.",
-            "Mr John Smith Jr was born in the U S A".split()
-        )
-    ]
+            "Mr John Smith Jr was born in the U S A".split(),
+        ),
+    ],
 )
-def test_suppress_unknown(string, result):
-    assert Crossandra(
-        suppress_unknown=True,
-        rules=[common.WORD]
-    ).tokenize(string) == result
+def test_suppress_unknown(string: str, result: list[str]) -> None:
+    assert (
+        Crossandra(suppress_unknown=True, rules=[common.WORD]).tokenize(string)
+        == result
+    )
 
 
 class BrainfuckToken(Enum):
@@ -105,88 +113,88 @@ class ArithmeticToken(Enum):
     POW = "**"
     MOD = "%"
 
+
 AT = ArithmeticToken
 
 
-def test_fast():
-    assert Crossandra(BrainfuckToken)._Crossandra__fast  # type: ignore
+def test_fast() -> None:
+    assert Crossandra(BrainfuckToken)._Crossandra__fast  # type: ignore[attr-defined]
 
 
-def test_not_fast_long_tokens():
-    assert not Crossandra(ArithmeticToken)._Crossandra__fast  # type: ignore
+def test_not_fast_long_tokens() -> None:
+    assert not Crossandra(ArithmeticToken)._Crossandra__fast  # type: ignore[attr-defined]
 
 
-def test_not_fast_rules():
-    assert not Crossandra(BrainfuckToken, rules=[common.NEWLINE])._Crossandra__fast  # type: ignore
+def test_not_fast_rules() -> None:
+    assert not Crossandra(BrainfuckToken, rules=[common.NEWLINE])._Crossandra__fast  # type: ignore[attr-defined]
 
 
-def test_tokenize_fast():
-    assert Crossandra(
-        BrainfuckToken,
-        suppress_unknown=True
-    ).tokenize("cat program: ,[.,]") == [
+def test_tokenize_fast() -> None:
+    assert Crossandra(BrainfuckToken, suppress_unknown=True).tokenize(
+        "cat program: ,[.,]"
+    ) == [
         BrainfuckToken.READ,
         BrainfuckToken.BEGIN_LOOP,
         BrainfuckToken.WRITE,
         BrainfuckToken.READ,
-        BrainfuckToken.END_LOOP
+        BrainfuckToken.END_LOOP,
     ]
 
 
 @pytest.mark.parametrize(
-    ["expression", "result"],
+    ("expression", "result"),
     [
         ("2 * 2 + 3 - 7", [2, AT.MUL, 2, AT.ADD, 3, AT.SUB, 7]),
         ("2**3", [2, AT.POW, 3]),
         ("-5", [AT.SUB, 5]),
         ("100 + -5", [100, AT.ADD, AT.SUB, 5]),
         ("4 - 2 ** 5 / 2", [4, AT.SUB, 2, AT.POW, 5, AT.DIV, 2]),
-        ("10 % 3", [10, AT.MOD, 3])
-    ]
+        ("10 % 3", [10, AT.MOD, 3]),
+    ],
 )
-def test_tokenize(expression, result):
-    assert Crossandra(
-        ArithmeticToken,
-        rules=[common.INT],
-        suppress_unknown=True
-    ).tokenize(expression) == result
+def test_tokenize(expression: str, result: list[Enum | Any]) -> None:
+    assert (
+        Crossandra(ArithmeticToken, rules=[common.INT], suppress_unknown=True).tokenize(
+            expression
+        )
+        == result
+    )
 
 
-def test_rule_conflict():
+def test_rule_conflict() -> None:
     with pytest.raises(CrossandraValueError):
         Rule(r"", int, ignore=True)
 
 
-def test_rule_ignore():
+def test_rule_ignore() -> None:
     assert Rule(r"\d", ignore=True).apply("1") == (IGNORED, 1)
 
 
-def test_rule_not_applied():
+def test_rule_not_applied() -> None:
     assert Rule(r"\d").apply("x") is NOT_APPLIED
 
 
-def test_rule_converter():
+def test_rule_converter() -> None:
     assert Rule(r"\d", int).apply("1") == (1, 1)
 
 
-def test_rule():
+def test_rule() -> None:
     assert Rule(r"\d").apply("1") == ("1", 1)
 
 
 @pytest.mark.parametrize(
-    ["flags", "result"],
-    [
-        (0, NOT_APPLIED),
-        (re.I, ("A", 1)),
-        (re.I | re.S, ("A\nb", 3))
-    ]
+    ("flags", "result"),
+    [(0, NOT_APPLIED), (re.I, ("A", 1)), (re.I | re.S, ("A\nb", 3))],
 )
-def test_flags(flags, result):
+def test_flags(flags: re.RegexFlag, result: RuleResult) -> None:
     assert Rule(r"a.?b?", flags=flags).apply("A\nb") == result
 
 
-def test_rule_group():
-    a, b = Rule("a"), Rule("b")
+def test_rule_group() -> None:
+    # type params required until Python 3.13's PEP 696 is here :')
+    a, b = Rule[str]("a"), Rule[str]("b")
+
     x, y = a | b
     p, r = RuleGroup((a, b))
+
     assert (a, b) == (x, y) == (p, r)
diff --git a/tests/common_test.py b/tests/test_common.py
similarity index 78%
rename from tests/common_test.py
rename to tests/test_common.py
index aa418a3..dc73d4c 100644
--- a/tests/common_test.py
+++ b/tests/test_common.py
@@ -1,8 +1,26 @@
-import pytest
+from __future__ import annotations
 
-from crossandra.common import *
-from crossandra.rule import *
+from typing import Tuple, Union
 
+import pytest
+
+from crossandra.common import (
+    C_NAME,
+    CHAR,
+    DECIMAL,
+    DIGIT,
+    DOUBLE_QUOTED_STRING,
+    FLOAT,
+    HEXDIGIT,
+    INT,
+    LETTER,
+    NEWLINE,
+    SIGNED_FLOAT,
+    SIGNED_INT,
+    SINGLE_QUOTED_STRING,
+    WORD,
+)
+from crossandra.rule import NOT_APPLIED, Ignored, NotApplied
 
 # ESC = ESCAPED
 # UC = UPPERCASE
@@ -11,9 +29,11 @@
 # NEG = NEGATIVE
 # USCORE = UNDERSCORE
 
+RuleResult = Union[Tuple[Union[Ignored, str], int], NotApplied]
+
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("'test'", ("'test'", 6)),  # CLASSIC
         ("'''", ("''", 2)),  # STRING CONTAINING QUOTE
@@ -25,12 +45,12 @@
         ("''", ("''", 2)),  # EMPTY
     ],
 )
-def test_SINGLE_QUOTED_STRING(string, result):
+def test_single_quoted_string(string: str, result: RuleResult) -> None:
     assert SINGLE_QUOTED_STRING.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ('"test"', ('"test"', 6)),  # CLASSIC
         ('"""', ('""', 2)),  # STRING CONTAINING QUOTE
@@ -42,7 +62,7 @@ def test_SINGLE_QUOTED_STRING(string, result):
         ('""', ('""', 2)),  # EMPTY
     ],
 )
-def test_DOUBLE_QUOTED_STRING(string, result):
+def test_double_quoted_string(string: str, result: RuleResult) -> None:
     assert DOUBLE_QUOTED_STRING.apply(string) == result
 
 
@@ -50,7 +70,7 @@ def test_DOUBLE_QUOTED_STRING(string, result):
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("'t'", ("'t'", 3)),  # CLASSIC
         ("'''", NOT_APPLIED),  # SINGLE QUOTE CHAR
@@ -62,12 +82,12 @@ def test_DOUBLE_QUOTED_STRING(string, result):
         ("''", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_CHAR(string, result):
+def test_char(string: str, result: RuleResult) -> None:
     assert CHAR.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("A", ("A", 1)),  # UC LETTER
         ("a", ("a", 1)),  # LC LETTER
@@ -77,23 +97,23 @@ def test_CHAR(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_LETTER(string, result):
+def test_letter(string: str, result: RuleResult) -> None:
     assert LETTER.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("word", ("word", 4)),  # CLASSIC
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_WORD(string, result):
+def test_word(string: str, result: RuleResult) -> None:
     assert WORD.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("0", (0, 1)),  # CLASSIC
         ("/", NOT_APPLIED),  # CHAR BEFORE DIGITS IN ASCII TABLE
@@ -101,12 +121,12 @@ def test_WORD(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_DIGIT(string, result):
+def test_digit(string: str, result: RuleResult) -> None:
     assert DIGIT.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("69", (69, 2)),  # CLASSIC
         ("069", (69, 3)),  # LEADING ZERO
@@ -115,12 +135,12 @@ def test_DIGIT(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_INT(string, result):
+def test_int(string: str, result: RuleResult) -> None:
     assert INT.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("+69", (69, 3)),  # POS INT
         ("-69", (-69, 3)),  # NEG INT
@@ -128,12 +148,12 @@ def test_INT(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_SIGNED_INT(string, result):
+def test_signed_int(string: str, result: RuleResult) -> None:
     assert SIGNED_INT.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("3.14", (3.14, 4)),  # CLASSIC
         ("3.0", (3.0, 3)),  # POST-COMMA TRAILING ZERO
@@ -144,12 +164,12 @@ def test_SIGNED_INT(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_DECIMAL(string, result):
+def test_decimal(string: str, result: RuleResult) -> None:
     assert DECIMAL.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("3", (3, 1)),  # CLASSIC: DIGIT
         ("D", (13, 1)),  # CLASSIC: UC LETTER
@@ -158,12 +178,12 @@ def test_DECIMAL(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_HEXDIGIT(string, result):
+def test_hexdigit(string: str, result: RuleResult) -> None:
     assert HEXDIGIT.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("W", ("W", 1)),  # ONE LETTER
         ("_", ("_", 1)),  # ONE USCORE
@@ -180,12 +200,12 @@ def test_HEXDIGIT(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_C_NAME(string, result):
+def test_c_name(string: str, result: RuleResult) -> None:
     assert C_NAME.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("\n", ("\n", 1)),  # LF NEWLINE
         ("\r\n", ("\r\n", 2)),  # CRLF NEWLINE
@@ -194,12 +214,12 @@ def test_C_NAME(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_NEWLINE(string, result):
+def test_newline(string: str, result: RuleResult) -> None:
     assert NEWLINE.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("1e3", (1_000.0, 3)),  # INT, LC E, INT
         ("1e+3", (1_000.0, 4)),  # INT, E, POS INT
@@ -211,12 +231,12 @@ def test_NEWLINE(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_FLOAT(string, result):
+def test_float(string: str, result: RuleResult) -> None:
     assert FLOAT.apply(string) == result
 
 
 @pytest.mark.parametrize(
-    ["string", "result"],
+    ("string", "result"),
     [
         ("+1e3", (1_000.0, 4)),  # POS INT, E, INT
         ("-1e3", (-1_000.0, 4)),  # NEG INT, E, INT
@@ -229,7 +249,7 @@ def test_FLOAT(string, result):
         ("", NOT_APPLIED),  # EMPTY
     ],
 )
-def test_SIGNED_FLOAT(string, result):
+def test_signed_float(string: str, result: RuleResult) -> None:
     assert SIGNED_FLOAT.apply(string) == result