From 5146914879d85f45e06856b5653e0f4f97237649 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sat, 30 Nov 2024 17:45:11 +0100 Subject: [PATCH 1/5] chore(deps): install beartype --- poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index d3bd3a7..fbabf38 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,6 +31,24 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "beartype" +version = "0.19.0" +description = "Unbearably fast near-real-time hybrid runtime-static type-checking in pure Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "beartype-0.19.0-py3-none-any.whl", hash = "sha256:33b2694eda0daf052eb2aff623ed9a8a586703bbf0a90bbc475a83bbf427f699"}, + {file = "beartype-0.19.0.tar.gz", hash = "sha256:de42dfc1ba5c3710fde6c3002e3bd2cad236ed4d2aabe876345ab0b4234a6573"}, +] + +[package.extras] +dev = ["autoapi (>=0.9.0)", "coverage (>=5.5)", "equinox", "jax[cpu]", "jaxtyping", "mypy (>=0.800)", "numba", "numpy", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"] +doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] +test = ["coverage (>=5.5)", "equinox", "jax[cpu]", "jaxtyping", "mypy (>=0.800)", "numba", "numpy", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"] +test-tox = ["equinox", "jax[cpu]", "jaxtyping", "mypy (>=0.800)", "numba", "numpy", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx", "typing-extensions (>=3.10.0.0)"] +test-tox-coverage = ["coverage (>=5.5)"] + [[package]] name = "certifi" version = "2024.7.4" @@ -1246,4 +1264,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "4e42db8b57c13c2a19b9920a69d9d853bb73bd0c7d34fcf6fb9fbfacc93c5d1d" +content-hash = "492a4ae1580df06e9b092f869173903f0bf78e88af367f80bce35ea59782e329" diff --git a/pyproject.toml b/pyproject.toml index ee5a8e4..022b9c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ sparqlwrapper = "^2.0.0" pydantic = "^2.9.2" +beartype = "^0.19.0" [tool.poetry.group.dev.dependencies] ruff = "^0.7.0" deptry = "^0.20.0" From a65c42ad9fbe0a862c022543a588175c7a399594 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sat, 30 Nov 2024 17:45:25 +0100 Subject: [PATCH 2/5] feat: add beartype checking for rdfproxy beartype offers high performance runtime type checking based on type annotations. Runtime checking can contribute to a healthier code base (see fixes) and also allows fast failures with meaningful exceptions for library users. Closes #152. --- rdfproxy/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rdfproxy/__init__.py b/rdfproxy/__init__.py index caaad1e..b29be60 100644 --- a/rdfproxy/__init__.py +++ b/rdfproxy/__init__.py @@ -1,4 +1,9 @@ +from beartype.claw import beartype_this_package +beartype_this_package() + from rdfproxy.adapter import SPARQLModelAdapter # noqa: F401 from rdfproxy.mapper import ModelBindingsMapper # noqa: F401 from rdfproxy.utils._types import SPARQLBinding # noqa: F401 from rdfproxy.utils.models import Page # noqa: F401 + + From a90de3854ea97e198ec4d14adf347246ff89200b Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sat, 30 Nov 2024 19:36:54 +0100 Subject: [PATCH 3/5] fix(types): correct type annotations flagged by beartype beartype revealed several incorrect typed annotations which lead to failing tests. --- rdfproxy/utils/_types.py | 1 + rdfproxy/utils/utils.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/rdfproxy/utils/_types.py b/rdfproxy/utils/_types.py index 93b59d1..0e076b2 100644 --- a/rdfproxy/utils/_types.py +++ b/rdfproxy/utils/_types.py @@ -9,6 +9,7 @@ _TModelInstance = TypeVar("_TModelInstance", bound=BaseModel) +@runtime_checkable class ItemsQueryConstructor(Protocol): def __call__(self, query: str, limit: int, offset: int) -> str: ... diff --git a/rdfproxy/utils/utils.py b/rdfproxy/utils/utils.py index 0615d7d..ae15a12 100644 --- a/rdfproxy/utils/utils.py +++ b/rdfproxy/utils/utils.py @@ -9,20 +9,25 @@ MissingModelConfigException, UnboundGroupingKeyException, ) -from rdfproxy.utils._types import ModelBoolPredicate, SPARQLBinding, _TModelBoolValue +from rdfproxy.utils._types import ( + ModelBoolPredicate, + SPARQLBinding, + _TModelBoolValue, + _TModelInstance, +) -def _is_type(obj: type | None, _type: type) -> bool: +def _is_type(obj: Any, _type: type) -> bool: """Check if an obj is type _type or a GenericAlias with origin _type.""" return (obj is _type) or (get_origin(obj) is _type) -def _is_list_type(obj: type | None) -> bool: +def _is_list_type(obj: Any) -> bool: """Check if obj is a list type.""" return _is_type(obj, list) -def _is_list_basemodel_type(obj: type | None) -> bool: +def _is_list_basemodel_type(obj: Any) -> bool: """Check if a type is list[pydantic.BaseModel].""" return (get_origin(obj) is list) and all( issubclass(cls, BaseModel) for cls in get_args(obj) @@ -104,7 +109,7 @@ def _get_model_bool_predicate_from_config_value( ) -def get_model_bool_predicate(model: BaseModel) -> ModelBoolPredicate: +def get_model_bool_predicate(model: type[_TModelInstance]) -> ModelBoolPredicate: """Get the applicable model_bool predicate function given a model.""" if (model_bool_value := model.model_config.get("model_bool", None)) is None: model_bool_predicate = default_model_bool_predicate From 78d5d7480c69de1098b7e1de66c9588fb14f4e32 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sat, 30 Nov 2024 19:38:50 +0100 Subject: [PATCH 4/5] fix(test): adapt QueryResult mock to pass runtime checking Runtime checking mock objects is an interesting case! Mock objects rightfully get rejected by runtime type checking, the solution is to point the mock object's __class__ to the class of the mocked object. See https://github.com/beartype/beartype/issues/92. --- tests/unit/test_sad_path_get_bindings_from_query_result.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_sad_path_get_bindings_from_query_result.py b/tests/unit/test_sad_path_get_bindings_from_query_result.py index 2bc655c..fc61756 100644 --- a/tests/unit/test_sad_path_get_bindings_from_query_result.py +++ b/tests/unit/test_sad_path_get_bindings_from_query_result.py @@ -4,11 +4,14 @@ import pytest +from SPARQLWrapper.Wrapper import QueryResult from rdfproxy.utils.sparql_utils import get_bindings_from_query_result def test_basic_sad_path_get_bindings_from_query_result(): with mock.patch("SPARQLWrapper.QueryResult") as mock_query_result: + mock_query_result.__class__ = QueryResult + mock_query_result.return_value.requestedFormat = "xml" exception_message = ( "Only QueryResult objects with JSON format are currently supported." From 42661293a89ba8bb454c4b8dd125122107a2ec8a Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Sat, 30 Nov 2024 19:46:26 +0100 Subject: [PATCH 5/5] chore(init): ignore E402 in rdfproxy.__init__ Package-wide beartype runtime checking requires 'beartype_this_package' to run *before* all other __init__ imports. This is not PEP-8 compliant, see https://peps.python.org/pep-0008/#imports. --- rdfproxy/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rdfproxy/__init__.py b/rdfproxy/__init__.py index b29be60..27a2f4a 100644 --- a/rdfproxy/__init__.py +++ b/rdfproxy/__init__.py @@ -1,9 +1,8 @@ from beartype.claw import beartype_this_package -beartype_this_package() - -from rdfproxy.adapter import SPARQLModelAdapter # noqa: F401 -from rdfproxy.mapper import ModelBindingsMapper # noqa: F401 -from rdfproxy.utils._types import SPARQLBinding # noqa: F401 -from rdfproxy.utils.models import Page # noqa: F401 +beartype_this_package() +from rdfproxy.adapter import SPARQLModelAdapter # noqa: F401, E402 +from rdfproxy.mapper import ModelBindingsMapper # noqa: F401, E402 +from rdfproxy.utils._types import SPARQLBinding # noqa: F401, E402 +from rdfproxy.utils.models import Page # noqa: F401, E402