Last active
August 30, 2024 06:31
-
-
Save dimaqq/28e9b4fde1921e7c4474f9c89c613c0b to your computer and use it in GitHub Desktop.
Type tests via custom type guard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ... # exinsting code | |
@pytest_fixture | |
def charm_env(monkeypatch, ...): # existing code | |
... | |
from typing import Type, TypeVar, Any, Optional, Callable, Mapping | |
from typing_extensions import TypeGuard | |
import inspect | |
T = TypeVar('T', bound=Callable[..., Any]) | |
def type_guard(func: T) -> TypeGuard[T]: # ⬅️ NEW ❗️ | |
"""Validate that passed callable has the type signature we expect ops.main() to have. | |
- argument names in correct order | |
- both positional and keyword invocation is possible | |
- allows extending the signature with more arguments | |
""" | |
assert callable(func) | |
params: Mapping[str, inspect.Parameter] = inspect.signature(func).parameters | |
p1, p2 = tuple(params.values())[:2] | |
assert p1.name == "charm_class" | |
assert p1.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD | |
assert p1.annotation == Type[ops.charm.CharmBase] | |
assert p1.default is inspect.Parameter.empty | |
assert p2.name == 'use_juju_for_storage' | |
assert p2.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD | |
assert p2.annotation == Optional[bool] | |
assert p2.default is None | |
return True | |
def test_top_level_import(charm_env: None): | |
import ops | |
assert type_guard(ops.main) # ⬅️ NEW ❗️ | |
ops.main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
ops.main() # type: ignore | |
def test_top_level_import_legacy_call(charm_env: None): | |
import ops | |
assert type_guard(ops.main.main) # ⬅️ NEW ❗️ | |
with pytest.deprecated_call(): | |
ops.main.main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
ops.main.main() # type: ignore | |
def test_submodule_import(charm_env: None): | |
import ops.main | |
assert type_guard(ops.main) # type: ignore # ⬅️ NEW ❗️ | |
ops.main(ops.CharmBase) # type: ignore # https://github.com/microsoft/pyright/issues/8830 | |
with pytest.raises(TypeError): | |
ops.main() # type: ignore | |
def test_submodule_import_legacy_call(charm_env: None): | |
import ops.main | |
assert type_guard(ops.main.main) # ⬅️ NEW ❗️ | |
with pytest.deprecated_call(): | |
ops.main.main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
ops.main.main() # type: ignore | |
def test_import_from_top_level_module(charm_env: None): | |
from ops import main | |
assert type_guard(main) # ⬅️ NEW ❗️ | |
main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
main() # type: ignore | |
def test_import_from_top_level_module_legacy_call(charm_env: None): | |
from ops import main | |
assert type_guard(main.main) # ⬅️ NEW ❗️ | |
with pytest.deprecated_call(): | |
main.main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
main.main() # type: ignore | |
def test_legacy_import_from_submodule(charm_env: None): | |
from ops.main import main | |
assert type_guard(main) # ⬅️ NEW ❗️ | |
with pytest.deprecated_call(): | |
main(ops.CharmBase) | |
with pytest.raises(TypeError): | |
main() # type: ignore |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Caveats:
Supports main decorated by stdlib decorators, does not support proxies like
def foo(*args, **kw)
.Works when type is correct.
Shows reasonable error when type is wrong: