Repository: mitsuhiko/minijinja (language: rust)

Issue:
Relaxing maturin version pin for minijinja-py
### Discussed in https://github.com/mitsuhiko/minijinja/discussions/566

<div type='discussions-op-text'>

<sup>Originally posted by **synapticarbors** August 27, 2024</sup>
Currently `minijinja-py` has a pyproject.toml defining:

```toml
[build-system]
requires = ["maturin>=0.15,<0.16"]
```

I was wondering if there were any blockers that would make it difficult to relax the `maturin` constraint to allow for newer versions? The last compatible maturin release (0.15.3) was from May 2023, and the latest version is 1.7.1.</div>


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pyright.yml b/.github/workflows/pyright.yml
new file mode 100644
index 00000000..bffdd637
--- /dev/null
+++ b/.github/workflows/pyright.yml
@@ -0,0 +1,15 @@
+name: Pyright
+
+on: [push, pull_request]
+
+jobs:
+  pyright:
+    name: Pyright
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: '3.9'
+      - name: Run pyright
+        run: make python-type-check
diff --git a/Makefile b/Makefile
index 98779171..301ef333 100644
--- a/Makefile
+++ b/Makefile
@@ -32,6 +32,10 @@ wasi-test:
 python-test:
 	@make -C minijinja-py
 
+.PHONY: python-type-check
+python-type-check:
+	@make -C minijinja-py type-check
+
 .PHONY: snapshot-tests
 snapshot-tests:
 	@cd minijinja; cargo insta test --all-features --review
diff --git a/minijinja-py/.gitignore b/minijinja-py/.gitignore
index 02cc31fa..b565db96 100644
--- a/minijinja-py/.gitignore
+++ b/minijinja-py/.gitignore
@@ -1,3 +1,4 @@
 .venv
 __pycache__
 python/minijinja/_lowlevel.*
+!python/minijinja/_lowlevel.pyi
diff --git a/minijinja-py/Makefile b/minijinja-py/Makefile
index b0c428f0..efc006c7 100644
--- a/minijinja-py/Makefile
+++ b/minijinja-py/Makefile
@@ -4,7 +4,7 @@ all: develop test
 .venv:
 	python3 -mvenv .venv
 	.venv/bin/pip install --upgrade pip
-	.venv/bin/pip install maturin==0.15.1 pytest markupsafe black
+	.venv/bin/pip install maturin pytest markupsafe black pyright
 
 .PHONY: test
 develop: .venv
@@ -19,5 +19,9 @@ test: .venv
 	.venv/bin/pytest
 
 .PHONY: format
-format:
+format: .venv
 	.venv/bin/black tests python
+
+.PHONY: type-check
+type-check: .venv
+	.venv/bin/pyright python
diff --git a/minijinja-py/pyproject.toml b/minijinja-py/pyproject.toml
index 624ae2f0..c87384bf 100644
--- a/minijinja-py/pyproject.toml
+++ b/minijinja-py/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["maturin>=0.15,<0.16"]
+requires = ["maturin>=1.5"]
 build-backend = "maturin"
 
 [project]
@@ -34,3 +34,9 @@ Repository = "https://github.com/mitsuhiko/minijinja"
 module-name = "minijinja._lowlevel"
 python-source = "python"
 strip = true
+
+[tool.pyright]
+include = ["python/**/*.pyi"]
+exclude = ["python/**/*.py"]
+typeCheckingMode = "strict"
+pythonVersion = "3.9"
diff --git a/minijinja-py/python/minijinja/__init__.pyi b/minijinja-py/python/minijinja/__init__.pyi
new file mode 100644
index 00000000..02baa53f
--- /dev/null
+++ b/minijinja-py/python/minijinja/__init__.pyi
@@ -0,0 +1,151 @@
+from pathlib import PurePath
+from typing import (
+    Any,
+    Callable,
+    Literal,
+    TypeVar,
+    Protocol,
+    overload,
+)
+from typing_extensions import Final, TypeAlias, Self
+from minijinja._lowlevel import State
+from collections.abc import Mapping
+
+__all__ = [
+    "Environment",
+    "TemplateError",
+    "safe",
+    "escape",
+    "render_str",
+    "eval_expr",
+    "pass_state",
+]
+
+_A_contra = TypeVar("_A_contra", contravariant=True)
+_R_co = TypeVar("_R_co", covariant=True)
+
+class _PassesState(Protocol[_A_contra, _R_co]):
+    def __call__(self, state: State, value: _A_contra, /) -> _R_co: ...
+
+    __minijinja_pass_state__: Literal[True]
+
+_StrPath: TypeAlias = PurePath | str
+_Behavior = Literal["strict", "lenient", "chainable"]
+
+DEFAULT_ENVIRONMENT: Final[Environment]
+
+def render_str(source: str, name: str | None = None, /, **context: Any) -> str: ...
+def eval_expr(expression: str, /, **context: Any) -> Any: ...
+
+class Environment:
+    loader: Callable[[str], str] | None
+    fuel: int | None
+    debug: bool
+    undefined_behavior: _Behavior
+    auto_escape_callback: Callable[[str], bool] | None
+    path_join_callback: Callable[[str, str], _StrPath] | None
+    keep_trailing_newline: bool
+    trim_blocks: bool
+    lstrip_blocks: bool
+    finalizer: _PassesState[Any, Any] | Callable[[Any], Any] | None
+    reload_before_render: bool
+    block_start_string: str
+    block_end_string: str
+    variable_start_string: str
+    variable_end_string: str
+    comment_start_string: str
+    comment_end_string: str
+    line_statement_prefix: str | None
+    line_comment_prefix: str | None
+
+    @overload
+    def __init__(
+        self,
+        loader: Callable[[str], str] | None = None,
+        templates: Mapping[str, str] | None = None,
+        filters: Mapping[str, Callable[[Any], Any]] | None = None,
+        tests: Mapping[str, Callable[[Any], bool]] | None = None,
+        globals: Mapping[str, Any] | None = None,
+        debug: bool = True,
+        fuel: int | None = None,
+        undefined_behavior: _Behavior = "lenient",
+        auto_escape_callback: Callable[[str], bool] | None = None,
+        path_join_callback: Callable[[str, str], _StrPath] | None = None,
+        keep_trailing_newline: bool = False,
+        trim_blocks: bool = False,
+        lstrip_blocks: bool = False,
+        finalizer: _PassesState[Any, Any] | None = None,
+        reload_before_render: bool = False,
+        block_start_string: str = "{%",
+        block_end_string: str = "%}",
+        variable_start_string: str = "{{",
+        variable_end_string: str = "}}",
+        comment_start_string: str = "{#",
+        comment_end_string: str = "#}",
+        line_statement_prefix: str | None = None,
+        line_comment_prefix: str | None = None,
+    ) -> None: ...
+    @overload
+    def __init__(
+        self,
+        loader: Callable[[str], str] | None = None,
+        templates: Mapping[str, str] | None = None,
+        filters: Mapping[str, Callable[[Any], Any]] | None = None,
+        tests: Mapping[str, Callable[[Any], bool]] | None = None,
+        globals: Mapping[str, Any] | None = None,
+        debug: bool = True,
+        fuel: int | None = None,
+        undefined_behavior: _Behavior = "lenient",
+        auto_escape_callback: Callable[[str], bool] | None = None,
+        path_join_callback: Callable[[str, str], _StrPath] | None = None,
+        keep_trailing_newline: bool = False,
+        trim_blocks: bool = False,
+        lstrip_blocks: bool = False,
+        finalizer: Callable[[Any], Any] | None = None,
+        reload_before_render: bool = False,
+        block_start_string: str = "{%",
+        block_end_string: str = "%}",
+        variable_start_string: str = "{{",
+        variable_end_string: str = "}}",
+        comment_start_string: str = "{#",
+        comment_end_string: str = "#}",
+        line_statement_prefix: str | None = None,
+        line_comment_prefix: str | None = None,
+    ) -> None: ...
+    def remove_filter(self, name: str) -> None: ...
+    def add_test(self, name: str, test: Callable[[Any], bool]) -> None: ...
+    def remove_test(self, name: str) -> None: ...
+    def add_global(self, name: str, value: Any) -> None: ...
+    def remove_global(self, name: str) -> None: ...
+    def render_template(self, template_name: str, /, **context: Any) -> str: ...
+    def render_str(
+        self, source: str, name: str | None = None, /, **context: Any
+    ) -> str: ...
+    def eval_expr(self, expression: str, /, **context: Any) -> Any: ...
+
+class TemplateError(RuntimeError):
+    def __init__(self, message: str) -> None: ...
+    @property
+    def message(self) -> str: ...
+    @property
+    def kind(self) -> str: ...
+    @property
+    def name(self) -> str | None: ...
+    @property
+    def detail(self) -> str | None: ...
+    @property
+    def line(self) -> int | None: ...
+    @property
+    def range(self) -> int | None: ...
+    @property
+    def template_source(self) -> str | None: ...
+    def __str__(self) -> str: ...
+
+class Markup(str):
+    def __html__(self) -> Self: ...
+
+def safe(value: str) -> str: ...
+def escape(value: Any) -> str: ...
+def pass_state(
+    f: Callable[[State, _A_contra], _R_co],
+) -> _PassesState[_A_contra, _R_co]: ...
diff --git a/minijinja-py/python/minijinja/_lowlevel.pyi b/minijinja-py/python/minijinja/_lowlevel.pyi
new file mode 100644
index 00000000..0a5cc0ba
--- /dev/null
+++ b/minijinja-py/python/minijinja/_lowlevel.pyi
@@ -0,0 +1,12 @@
+from typing import Any
+from . import Environment
+from typing_extensions import final
+
+@final
+class State:
+    name: str
+    env: Environment
+    current_block: str | None
+    auto_escape: bool
+
+    def lookup(self, name: str) -> Any: ...


Test patch:
diff --git a/minijinja-py/tests/test_basic.py b/minijinja-py/tests/test_basic.py
index 1d2901f8..9bd6163c 100644
--- a/minijinja-py/tests/test_basic.py
+++ b/minijinja-py/tests/test_basic.py
@@ -4,7 +4,14 @@
 import types
 
 from _pytest.unraisableexception import catch_unraisable_exception
-from minijinja import Environment, TemplateError, safe, pass_state, eval_expr, render_str
+from minijinja import (
+    Environment,
+    TemplateError,
+    safe,
+    pass_state,
+    eval_expr,
+    render_str,
+)
 
 
 def test_expression():
@@ -268,19 +275,20 @@ def test_custom_syntax():
         comment_start_string="/*",
         comment_end_string="*/",
     )
-    rv = env.render_str('[% if true %]{value}[% endif %]/* nothing */', value=42)
-    assert rv == '42'
+    rv = env.render_str("[% if true %]{value}[% endif %]/* nothing */", value=42)
+    assert rv == "42"
 
 
 def test_path_join():
     def join_path(name, parent):
         return posixpath.join(posixpath.dirname(parent), name)
+
     env = Environment(
         path_join_callback=join_path,
         templates={
             "foo/bar.txt": "{% include 'baz.txt' %}",
             "foo/baz.txt": "I am baz!",
-        }
+        },
     )
 
     with catch_unraisable_exception() as cm:
@@ -339,5 +347,5 @@ def test_custom_delimiters():
         comment_start_string="<!--",
         comment_end_string="-->",
     )
-    rv = env.render_str('<% if true %>${ value }<% endif %><!-- nothing -->', value=42)
-    assert rv == '42'
\ No newline at end of file
+    rv = env.render_str("<% if true %>${ value }<% endif %><!-- nothing -->", value=42)
+    assert rv == "42"
diff --git a/minijinja-py/tests/test_security.py b/minijinja-py/tests/test_security.py
index 41b8af33..90b6823d 100644
--- a/minijinja-py/tests/test_security.py
+++ b/minijinja-py/tests/test_security.py
@@ -1,5 +1,4 @@
-from _pytest.unraisableexception import catch_unraisable_exception
-from minijinja import Environment, TemplateError, safe
+from minijinja import Environment
 
 
 def test_private_attrs():