Skip to content

Commit 4da8588

Browse files
authored
Merge pull request #124 from dapper91/dev
- request validator refactored. Validator is built once on method initialization to increase request handling performance.
2 parents 4bec2cb + 4276c88 commit 4da8588

File tree

12 files changed

+144
-84
lines changed

12 files changed

+144
-84
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
Changelog
22
=========
33

4-
1.14.0 (2025-04-08)
4+
1.15.0 (2025-05-18)
55
-------------------
66

7-
- pydantic 2.11 compatibility.
8-
- request kwargs passed to a tracer.
9-
- pydantic validator model cached.
7+
- request validator refactored. Validator is built once on method initialization to increase request handling performance.
108

119

1210
1.13.0 (2025-02-15)

pjrpc/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__description__ = 'Extensible JSON-RPC library'
33
__url__ = 'https://github.com/dapper91/pjrpc'
44

5-
__version__ = '1.14.0'
5+
__version__ = '1.15.0'
66

77
__author__ = 'Dmitry Pershin'
88
__email__ = '[email protected]'

pjrpc/server/dispatcher.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,18 @@ def __init__(
5151

5252
meta = utils.set_meta(method, method_name=self.name, context_name=context)
5353

54-
self.validator, self.validator_args = meta.get('validator', default_validator), meta.get('validator_args', {})
54+
validator_factory: validators.BaseValidator = meta.get('validator', default_validator)
55+
validator_args = meta.get('validator_args', {})
56+
57+
self.validator = validator_factory.build_method_validator(
58+
method,
59+
exclude=(self.context,) if self.context else (),
60+
**validator_args,
61+
)
5562

5663
def bind(self, params: Optional['JsonRpcParams'], context: Optional[Any] = None) -> MethodType:
5764
method_args = []
58-
method_kwargs = self.validator.validate_method(
59-
self.method, params, exclude=(self.context,) if self.context else (), **self.validator_args,
60-
)
65+
method_kwargs = self.validator.validate_params(params)
6166

6267
if self.context is not None:
6368
if self.positional:
@@ -99,16 +104,17 @@ def __init__(
99104
context: Optional[Any] = None,
100105
positional: bool = False,
101106
):
102-
super().__init__(getattr(view_cls, method_name), name or method_name, context, positional)
107+
super().__init__(getattr(view_cls, method_name), name or method_name, 'self', positional)
108+
self.view_context = context
103109

104110
self.view_cls = view_cls
105111
self.method_name = method_name
106112

107113
def bind(self, params: Optional['JsonRpcParams'], context: Optional[Any] = None) -> MethodType:
108-
view = self.view_cls(context) if self.context else self.view_cls()
114+
view = self.view_cls(context) if self.view_context else self.view_cls()
109115
method = getattr(view, self.method_name)
110116

111-
method_params = self.validator.validate_method(method, params, **self.validator_args)
117+
method_params = self.validator.validate_params(params)
112118

113119
return ft.partial(method, **method_params)
114120

pjrpc/server/validators/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
JSON-RPC method parameters validators.
33
"""
44

5-
from .base import BaseValidator, ExcludeFunc, ValidationError
5+
from .base import BaseMethodValidator, BaseValidator, ExcludeFunc, ValidationError
66

77
__all__ = [
88
'BaseValidator',
9+
'BaseMethodValidator',
910
'ExcludeFunc',
1011
'ValidationError',
1112
]

pjrpc/server/validators/base.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import functools as ft
21
import inspect
32
from typing import Any, Dict, Iterable, List, Optional, Tuple
43

@@ -15,7 +14,7 @@ class ValidationError(Exception):
1514

1615
class BaseValidator:
1716
"""
18-
Base method parameters validator. Uses :py:func:`inspect.signature` for validation.
17+
Base method parameters validator factory. Uses :py:func:`inspect.signature` for validation.
1918
2019
:param exclude_param: a function that decides if the parameters must be excluded
2120
from validation (useful for dependency injection)
@@ -29,7 +28,7 @@ def validate(self, maybe_method: Optional[MethodType] = None, **kwargs: Any) ->
2928
Decorator marks a method the parameters of which to be validated when calling it using JSON-RPC protocol.
3029
3130
:param maybe_method: method the parameters of which to be validated or ``None`` if called as @validate(...)
32-
:param kwargs: validator arguments
31+
:param kwargs: method validator arguments
3332
"""
3433

3534
def decorator(method: MethodType) -> MethodType:
@@ -43,28 +42,39 @@ def decorator(method: MethodType) -> MethodType:
4342
else:
4443
return decorator(maybe_method)
4544

46-
def validate_method(
47-
self, method: MethodType, params: Optional['JsonRpcParams'], exclude: Iterable[str] = (), **kwargs: Any,
48-
) -> Dict[str, Any]:
45+
def build_method_validator(
46+
self,
47+
method: MethodType,
48+
exclude: Iterable[str] = (),
49+
**kwargs: Any,
50+
) -> 'BaseMethodValidator':
51+
return BaseMethodValidator(method, self._exclude_param, exclude)
52+
53+
54+
class BaseMethodValidator:
55+
"""
56+
Base method parameters validator.
57+
"""
58+
59+
def __init__(self, method: MethodType, exclude_func: ExcludeFunc, exclude: Iterable[str] = ()):
60+
self._method = method
61+
self._signature = self._build_signature(method, exclude_func, tuple(exclude))
62+
63+
def validate_params(self, params: Optional['JsonRpcParams']) -> Dict[str, Any]:
4964
"""
5065
Validates params against method signature.
5166
52-
:param method: method to validate parameters against
5367
:param params: parameters to be validated
54-
:param exclude: parameter names to be excluded from validation
55-
:param kwargs: additional validator arguments
5668
5769
:raises: :py:class:`pjrpc.server.validators.ValidationError`
5870
:returns: bound method parameters
5971
"""
6072

61-
signature = self.signature(method, tuple(exclude))
62-
return self.bind(signature, params).arguments
73+
return self._bind(params).arguments
6374

64-
def bind(self, signature: inspect.Signature, params: Optional['JsonRpcParams']) -> inspect.BoundArguments:
75+
def _bind(self, params: Optional['JsonRpcParams']) -> inspect.BoundArguments:
6576
"""
6677
Binds parameters to method.
67-
:param signature: method to bind parameters to
6878
:param params: parameters to be bound
6979
7080
:raises: ValidationError is parameters binding failed
@@ -75,12 +85,16 @@ def bind(self, signature: inspect.Signature, params: Optional['JsonRpcParams'])
7585
method_kwargs = params if isinstance(params, dict) else {}
7686

7787
try:
78-
return signature.bind(*method_args, **method_kwargs)
88+
return self._signature.bind(*method_args, **method_kwargs)
7989
except TypeError as e:
8090
raise ValidationError(str(e)) from e
8191

82-
@ft.lru_cache(None)
83-
def signature(self, method: MethodType, exclude: Tuple[str, ...]) -> inspect.Signature:
92+
def _build_signature(
93+
self,
94+
method: MethodType,
95+
exclude_func: ExcludeFunc,
96+
exclude: Tuple[str, ...] = (),
97+
) -> inspect.Signature:
8498
"""
8599
Returns method signature.
86100
@@ -93,7 +107,7 @@ def signature(self, method: MethodType, exclude: Tuple[str, ...]) -> inspect.Sig
93107

94108
method_parameters: List[inspect.Parameter] = []
95109
for param in signature.parameters.values():
96-
if param.name not in exclude and not self._exclude_param(param.name, param.annotation, param.default):
110+
if param.name not in exclude and not exclude_func(param.name, param.annotation, param.default):
97111
method_parameters.append(param)
98112

99113
return signature.replace(parameters=method_parameters)

pjrpc/server/validators/jsonschema.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class JsonSchemaValidator(base.BaseValidator):
1212
"""
13-
Parameters validator based on `jsonschema <https://python-jsonschema.readthedocs.io/en/stable/>`_ library.
13+
Parameters validator factory based on `jsonschema <https://python-jsonschema.readthedocs.io/en/stable/>`_ library.
1414
1515
:param kwargs: default jsonschema validator arguments
1616
:param exclude_param: a function that decides if the parameters must be excluded
@@ -20,27 +20,36 @@ class JsonSchemaValidator(base.BaseValidator):
2020
def __init__(self, exclude_param: Optional[ExcludeFunc] = None, **kwargs: Any):
2121
super().__init__(exclude_param=exclude_param)
2222
kwargs.setdefault('types', {'array': (list, tuple)})
23-
self.default_kwargs = kwargs
23+
self._default_kwargs = kwargs
2424

25-
def validate_method(
26-
self, method: MethodType, params: Optional['JsonRpcParams'], exclude: Iterable[str] = (), **kwargs: Any,
27-
) -> Dict[str, Any]:
25+
def build_method_validator(
26+
self,
27+
method: MethodType,
28+
exclude: Iterable[str] = (),
29+
**kwargs: Any,
30+
) -> 'JsonSchemaMethodValidator':
31+
return JsonSchemaMethodValidator(method, self._exclude_param, exclude, **dict(self._default_kwargs, **kwargs))
32+
33+
34+
class JsonSchemaMethodValidator(base.BaseMethodValidator):
35+
def __init__(self, method: MethodType, exclude_func: ExcludeFunc, exclude: Iterable[str] = (), **kwargs: Any):
36+
super().__init__(method, exclude_func, exclude)
37+
self._signature = self._build_signature(method, exclude_func, tuple(exclude))
38+
self._validator_args = kwargs
39+
40+
def validate_params(self, params: Optional['JsonRpcParams']) -> Dict[str, Any]:
2841
"""
2942
Validates params against method using ``pydantic`` validator.
3043
31-
:param method: method to validate parameters against
3244
:param params: parameters to be validated
33-
:param exclude: parameter names to be excluded from validation
34-
:param kwargs: jsonschema validator arguments
3545
3646
:raises: :py:class:`pjrpc.server.validators.ValidationError`
3747
"""
3848

39-
arguments = super().validate_method(method, params, exclude)
49+
arguments = super().validate_params(params)
4050

4151
try:
42-
kwargs = {**self.default_kwargs, **kwargs}
43-
jsonschema.validate(arguments, **kwargs)
52+
jsonschema.validate(arguments, **self._validator_args)
4453
except jsonschema.ValidationError as e:
4554
raise base.ValidationError(str(e)) from e
4655

pjrpc/server/validators/pydantic.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import functools as ft
21
import inspect
3-
from typing import Any, Callable, Dict, Iterable, List, Optional, Type
2+
from typing import Any, Dict, Iterable, List, Optional, Type
43

54
import pydantic
65

7-
from pjrpc.common.typedefs import JsonRpcParams
6+
from pjrpc.common.typedefs import JsonRpcParams, MethodType
87
from pjrpc.server.typedefs import ExcludeFunc
98

109
from . import base
1110

1211

1312
class PydanticValidator(base.BaseValidator):
1413
"""
15-
Parameters validator based on `pydantic <https://pydantic-docs.helpmanual.io/>`_ library.
14+
Method parameters validator factory based on `pydantic <https://pydantic-docs.helpmanual.io/>`_ library.
1615
Uses python type annotations for parameters validation.
1716
1817
:param coerce: if ``True`` returns converted (coerced) parameters according to parameter type annotation
@@ -30,36 +29,56 @@ def __init__(self, coerce: bool = True, exclude_param: Optional[ExcludeFunc] = N
3029
# https://pydantic-docs.helpmanual.io/usage/model_config/
3130
self._model_config = pydantic.ConfigDict(**config_args) # type: ignore[typeddict-item]
3231

33-
def validate_method(
34-
self, method: Callable[..., Any], params: Optional['JsonRpcParams'], exclude: Iterable[str] = (), **kwargs: Any,
35-
) -> Dict[str, Any]:
32+
def build_method_validator(
33+
self,
34+
method: MethodType,
35+
exclude: Iterable[str] = (),
36+
**kwargs: Any,
37+
) -> 'PydanticMethodValidator':
38+
return PydanticMethodValidator(method, self._exclude_param, exclude, self._coerce, self._model_config)
39+
40+
41+
class PydanticMethodValidator(base.BaseMethodValidator):
42+
"""
43+
Pydantic method parameters validator based on `pydantic <https://pydantic-docs.helpmanual.io/>`_ library.
44+
"""
45+
46+
def __init__(
47+
self,
48+
method: MethodType,
49+
exclude_func: ExcludeFunc,
50+
exclude: Iterable[str],
51+
coerce: bool,
52+
model_config: pydantic.ConfigDict,
53+
):
54+
super().__init__(method, exclude_func, exclude)
55+
self._coerce = coerce
56+
self._model_config = model_config
57+
self._params_model = self._build_validation_model(method.__name__)
58+
59+
def validate_params(self, params: Optional['JsonRpcParams']) -> Dict[str, Any]:
3660
"""
3761
Validates params against method using ``pydantic`` validator.
3862
39-
:param method: method to validate parameters against
4063
:param params: parameters to be validated
41-
:param exclude: parameter names to be excluded from validation
4264
4365
:returns: coerced parameters if `coerce` flag is ``True`` otherwise parameters as is
4466
:raises: ValidationError
4567
"""
4668

47-
signature = self.signature(method, tuple(exclude))
48-
params_model = self.build_validation_model(method.__name__, signature)
49-
bound_params = self.bind(signature, params)
69+
bound_params = self._bind(params)
5070
try:
51-
obj = params_model(**bound_params.arguments)
71+
obj = self._params_model(**bound_params.arguments)
5272
except pydantic.ValidationError as e:
5373
raise base.ValidationError(*e.errors()) from e
5474

5575
return {attr: getattr(obj, attr) for attr in obj.model_fields} if self._coerce else bound_params.arguments
5676

57-
@ft.lru_cache(maxsize=None)
58-
def build_validation_model(self, method_name: str, signature: inspect.Signature) -> Type[pydantic.BaseModel]:
59-
schema = self.build_validation_schema(signature)
77+
def _build_validation_model(self, method_name: str) -> Type[pydantic.BaseModel]:
78+
schema = self._build_validation_schema(self._signature)
6079
return pydantic.create_model(method_name, **schema, __config__=self._model_config)
6180

62-
def build_validation_schema(self, signature: inspect.Signature) -> Dict[str, Any]:
81+
def _build_validation_schema(self, signature: inspect.Signature) -> Dict[str, Any]:
6382
"""
6483
Builds pydantic model based validation schema from method signature.
6584

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pjrpc"
3-
version = "1.14.0"
3+
version = "1.15.0"
44
description = "Extensible JSON-RPC library"
55
authors = ["Dmitry Pershin <[email protected]>"]
66
license = "Unlicense"

tests/server/test_base_validator.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
], indirect=['dyn_method'],
2020
)
2121
def test_validation_success(dyn_method, params):
22-
validator = validators.BaseValidator()
23-
validator.validate_method(dyn_method, params)
22+
validator_factory = validators.BaseValidator()
23+
validator = validator_factory.build_method_validator(dyn_method)
24+
25+
validator.validate_params(params)
2426

2527

2628
@pytest.mark.parametrize(
@@ -35,10 +37,11 @@ def test_validation_success(dyn_method, params):
3537
], indirect=['dyn_method'],
3638
)
3739
def test_validation_error(dyn_method, params):
38-
validator = validators.BaseValidator()
40+
validator_factory = validators.BaseValidator()
41+
validator = validator_factory.build_method_validator(dyn_method)
3942

4043
with pytest.raises(validators.ValidationError):
41-
validator.validate_method(dyn_method, params)
44+
validator.validate_params(params)
4245

4346

4447
@pytest.mark.parametrize(
@@ -51,8 +54,10 @@ def test_validation_error(dyn_method, params):
5154
], indirect=['dyn_method'],
5255
)
5356
def test_validation_exclude_success(dyn_method, exclude, params):
54-
validator = validators.BaseValidator()
55-
validator.validate_method(dyn_method, params, exclude=exclude)
57+
validator_factory = validators.BaseValidator()
58+
validator = validator_factory.build_method_validator(dyn_method, exclude=exclude)
59+
60+
validator.validate_params(params)
5661

5762

5863
@pytest.mark.parametrize(
@@ -63,7 +68,8 @@ def test_validation_exclude_success(dyn_method, exclude, params):
6368
], indirect=['dyn_method'],
6469
)
6570
def test_validation_exclude_error(dyn_method, exclude, params):
66-
validator = validators.BaseValidator()
71+
validator_factory = validators.BaseValidator()
72+
validator = validator_factory.build_method_validator(dyn_method, exclude=exclude)
6773

6874
with pytest.raises(validators.ValidationError):
69-
validator.validate_method(dyn_method, params, exclude=exclude)
75+
validator.validate_params(params)

0 commit comments

Comments
 (0)