Skip to content

Commit b994eb8

Browse files
committed
chore: enable mypy strict
1 parent 67732f3 commit b994eb8

File tree

13 files changed

+80
-55
lines changed

13 files changed

+80
-55
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ repos:
1919
rev: v1.17.0
2020
hooks:
2121
- id: mypy
22+
exclude: ^(tests/|conftest.py)
2223
additional_dependencies:
2324
- pydantic[email]>=2.7.0
2425
- repo: https://github.com/codespell-project/codespell

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ exclude = [
114114
"tests/",
115115
"conftest.py",
116116
]
117+
strict=true
117118

118119
[tool.pydantic-mypy]
119120
init_forbid_extra = true

scim2_models/attributes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class MultiValuedComplexAttribute(ComplexAttribute):
4343
value: Optional[Any] = None
4444
"""The value of an entitlement."""
4545

46-
ref: Optional[Reference] = Field(None, serialization_alias="$ref")
46+
ref: Optional[Reference[Any]] = Field(None, serialization_alias="$ref")
4747
"""The reference URI of a target resource, if the attribute is a
4848
reference."""
4949

scim2_models/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,8 @@ def normalize_attribute_names(
220220
"""
221221

222222
def normalize_dict_keys(
223-
input_dict: dict, model_class: type["BaseModel"]
224-
) -> dict:
223+
input_dict: dict[str, Any], model_class: type["BaseModel"]
224+
) -> dict[str, Any]:
225225
"""Normalize dictionary keys, preserving case for Any fields."""
226226
result = {}
227227

scim2_models/messages/message.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def __new__(
112112
return klass
113113

114114

115-
def _get_resource_class(obj) -> Optional[type[Resource]]:
115+
def _get_resource_class(obj: BaseModel) -> type[Resource[Any]]:
116116
"""Extract the resource class from generic type parameter."""
117117
metadata = getattr(obj.__class__, "__pydantic_generic_metadata__", {"args": [None]})
118118
resource_class = metadata["args"][0]
119-
return resource_class
119+
return resource_class # type: ignore[no-any-return]

scim2_models/messages/patch_op.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Generic
66
from typing import Optional
77
from typing import TypeVar
8+
from typing import Union
89

910
from pydantic import Field
1011
from pydantic import ValidationInfo
@@ -26,7 +27,7 @@
2627
from .message import Message
2728
from .message import _get_resource_class
2829

29-
T = TypeVar("T", bound=Resource)
30+
T = TypeVar("T", bound=Resource[Any])
3031

3132

3233
class PatchOperation(ComplexAttribute):
@@ -143,7 +144,7 @@ class PatchOp(Message, Generic[T]):
143144
- Using PatchOp without a type parameter raises TypeError
144145
"""
145146

146-
def __new__(cls, *args: Any, **kwargs: Any):
147+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
147148
"""Create new PatchOp instance with type parameter validation.
148149
149150
Only handles the case of direct instantiation without type parameter (PatchOp()).
@@ -162,39 +163,48 @@ def __new__(cls, *args: Any, **kwargs: Any):
162163

163164
return super().__new__(cls)
164165

165-
def __class_getitem__(cls, item):
166+
def __class_getitem__(
167+
cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
168+
) -> Any:
166169
"""Validate type parameter when creating parameterized type.
167170
168171
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
169172
or a TypeVar bound to Resource. Rejects invalid types (str, int, etc.) and Union types.
170173
"""
171-
# Allow TypeVar as type parameter
172-
if isinstance(item, TypeVar):
174+
if isinstance(typevar_values, TypeVar):
173175
# Check if TypeVar is bound to Resource or its subclass
174-
if item.__bound__ is not None and (
175-
item.__bound__ is Resource
176-
or (isclass(item.__bound__) and issubclass(item.__bound__, Resource))
176+
if typevar_values.__bound__ is not None and (
177+
typevar_values.__bound__ is Resource
178+
or (
179+
isclass(typevar_values.__bound__)
180+
and issubclass(typevar_values.__bound__, Resource)
181+
)
177182
):
178-
return super().__class_getitem__(item)
183+
return super().__class_getitem__(typevar_values)
179184
else:
180185
raise TypeError(
181-
f"PatchOp TypeVar must be bound to Resource or its subclass, got {item}. "
186+
f"PatchOp TypeVar must be bound to Resource or its subclass, got {typevar_values}. "
182187
"Example: T = TypeVar('T', bound=Resource)"
183188
)
184189

185190
# Check if type parameter is a concrete Resource subclass (not Resource itself)
186-
if item is Resource:
191+
if typevar_values is Resource:
187192
raise TypeError(
188193
"PatchOp requires a concrete Resource subclass, not Resource itself. "
189194
"Use PatchOp[User], PatchOp[Group], etc. instead of PatchOp[Resource]."
190195
)
191-
if not (isclass(item) and issubclass(item, Resource) and item is not Resource):
196+
197+
if not (
198+
isclass(typevar_values)
199+
and issubclass(typevar_values, Resource)
200+
and typevar_values is not Resource
201+
):
192202
raise TypeError(
193-
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {item}. "
203+
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {typevar_values}. "
194204
"Use PatchOp[User], PatchOp[Group], etc."
195205
)
196206

197-
return super().__class_getitem__(item)
207+
return super().__class_getitem__(typevar_values)
198208

199209
schemas: Annotated[list[str], Required.true] = [
200210
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
@@ -254,7 +264,9 @@ def patch(self, resource: T) -> bool:
254264

255265
return modified
256266

257-
def _apply_operation(self, resource: Resource, operation: PatchOperation) -> bool:
267+
def _apply_operation(
268+
self, resource: Resource[Any], operation: PatchOperation
269+
) -> bool:
258270
"""Apply a single patch operation to a resource.
259271
260272
:return: :data:`True` if the resource was modified, else :data:`False`.
@@ -266,7 +278,9 @@ def _apply_operation(self, resource: Resource, operation: PatchOperation) -> boo
266278

267279
raise ValueError(Error.make_invalid_value_error().detail)
268280

269-
def _apply_add_replace(self, resource: Resource, operation: PatchOperation) -> bool:
281+
def _apply_add_replace(
282+
self, resource: Resource[Any], operation: PatchOperation
283+
) -> bool:
270284
"""Apply an add or replace operation."""
271285
# RFC 7644 Section 3.5.2.1: "If path is specified, add/replace at that path"
272286
if operation.path is not None:
@@ -280,7 +294,7 @@ def _apply_add_replace(self, resource: Resource, operation: PatchOperation) -> b
280294
# RFC 7644 Section 3.5.2.1: "If no path specified, add/replace at root level"
281295
return self._apply_root_attributes(resource, operation.value)
282296

283-
def _apply_remove(self, resource: Resource, operation: PatchOperation) -> bool:
297+
def _apply_remove(self, resource: Resource[Any], operation: PatchOperation) -> bool:
284298
"""Apply a remove operation."""
285299
# RFC 7644 Section 3.5.2.3: "Path is required for remove operations"
286300
if operation.path is None:
@@ -313,7 +327,7 @@ def _apply_root_attributes(self, resource: BaseModel, value: Any) -> bool:
313327
return modified
314328

315329
def _set_value_at_path(
316-
self, resource: Resource, path: str, value: Any, is_add: bool
330+
self, resource: Resource[Any], path: str, value: Any, is_add: bool
317331
) -> bool:
318332
"""Set a value at a specific path."""
319333
target, attr_path = _resolve_path_to_target(resource, path)
@@ -384,7 +398,11 @@ def _handle_multivalued_add(
384398
return self._add_single_value(resource, field_name, current_list, value)
385399

386400
def _add_multiple_values(
387-
self, resource: BaseModel, field_name: str, current_list: list, values: list
401+
self,
402+
resource: BaseModel,
403+
field_name: str,
404+
current_list: list[Any],
405+
values: list[Any],
388406
) -> bool:
389407
"""Add multiple values to a multi-valued attribute."""
390408
new_values = []
@@ -400,7 +418,7 @@ def _add_multiple_values(
400418
return True
401419

402420
def _add_single_value(
403-
self, resource: BaseModel, field_name: str, current_list: list, value: Any
421+
self, resource: BaseModel, field_name: str, current_list: list[Any], value: Any
404422
) -> bool:
405423
"""Add a single value to a multi-valued attribute."""
406424
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
@@ -411,7 +429,7 @@ def _add_single_value(
411429
setattr(resource, field_name, current_list)
412430
return True
413431

414-
def _value_exists_in_list(self, current_list: list, new_value: Any) -> bool:
432+
def _value_exists_in_list(self, current_list: list[Any], new_value: Any) -> bool:
415433
"""Check if a value already exists in a list."""
416434
return any(self._values_match(item, new_value) for item in current_list)
417435

@@ -425,7 +443,7 @@ def _create_parent_object(self, resource: BaseModel, parent_field_name: str) ->
425443
setattr(resource, parent_field_name, parent_obj)
426444
return parent_obj
427445

428-
def _remove_value_at_path(self, resource: Resource, path: str) -> bool:
446+
def _remove_value_at_path(self, resource: Resource[Any], path: str) -> bool:
429447
"""Remove a value at a specific path."""
430448
target, attr_path = _resolve_path_to_target(resource, path)
431449

@@ -451,7 +469,7 @@ def _remove_value_at_path(self, resource: Resource, path: str) -> bool:
451469
return self._remove_value_at_path(parent_obj, sub_path)
452470

453471
def _remove_specific_value(
454-
self, resource: Resource, path: str, value_to_remove: Any
472+
self, resource: Resource[Any], path: str, value_to_remove: Any
455473
) -> bool:
456474
"""Remove a specific value from a multi-valued attribute."""
457475
target, attr_path = _resolve_path_to_target(resource, path)
@@ -486,7 +504,7 @@ def _remove_specific_value(
486504
def _values_match(self, value1: Any, value2: Any) -> bool:
487505
"""Check if two values match, converting BaseModel to dict for comparison."""
488506

489-
def to_dict(value):
507+
def to_dict(value: Any) -> dict[str, Any]:
490508
return value.model_dump() if isinstance(value, BaseModel) else value
491509

492510
return to_dict(value1) == to_dict(value2)

scim2_models/resources/group.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Annotated
2+
from typing import Any
23
from typing import ClassVar
34
from typing import Literal
45
from typing import Optional
@@ -33,7 +34,7 @@ class GroupMember(MultiValuedComplexAttribute):
3334
display: Annotated[Optional[str], Mutability.read_only] = None
3435

3536

36-
class Group(Resource):
37+
class Group(Resource[Any]):
3738
schemas: Annotated[list[str], Required.true] = [
3839
"urn:ietf:params:scim:schemas:core:2.0:Group"
3940
]

scim2_models/resources/resource.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
148148
"""A complex attribute containing resource metadata."""
149149

150150
@classmethod
151-
def __class_getitem__(cls, item: Any) -> type["Resource"]:
151+
def __class_getitem__(cls, item: Any) -> type["Resource[Any]"]:
152152
"""Create a Resource class with extension fields dynamically added."""
153153
if hasattr(cls, "__scim_extension_metadata__"):
154154
return cls
@@ -241,12 +241,12 @@ def get_extension_model(
241241

242242
@staticmethod
243243
def get_by_schema(
244-
resource_types: list[type["Resource"]],
244+
resource_types: list[type["Resource[Any]"]],
245245
schema: str,
246246
with_extensions: bool = True,
247-
) -> Optional[Union[type["Resource"], type["Extension"]]]:
247+
) -> Optional[Union[type["Resource[Any]"], type["Extension"]]]:
248248
"""Given a resource type list and a schema, find the matching resource type."""
249-
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
249+
by_schema: dict[str, Union[type[Resource[Any]], type[Extension]]] = {
250250
resource_type.model_fields["schemas"].default[0].lower(): resource_type
251251
for resource_type in (resource_types or [])
252252
}
@@ -263,7 +263,7 @@ def get_by_schema(
263263

264264
@staticmethod
265265
def get_by_payload(
266-
resource_types: list[type["Resource"]],
266+
resource_types: list[type["Resource[Any]"]],
267267
payload: dict[str, Any],
268268
**kwargs: Any,
269269
) -> Optional[type]:
@@ -291,7 +291,7 @@ def to_schema(cls) -> "Schema":
291291
return _model_to_schema(cls)
292292

293293
@classmethod
294-
def from_schema(cls, schema: "Schema") -> type["Resource"]:
294+
def from_schema(cls, schema: "Schema") -> type["Resource[Any]"]:
295295
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
296296
from .schema import _make_python_model
297297

@@ -372,7 +372,7 @@ def model_dump_json(
372372
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
373373

374374

375-
AnyResource = TypeVar("AnyResource", bound="Resource")
375+
AnyResource = TypeVar("AnyResource", bound="Resource[Any]")
376376

377377

378378
def _dedicated_attributes(

scim2_models/resources/resource_type.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Annotated
2+
from typing import Any
23
from typing import Optional
34

45
from pydantic import Field
@@ -34,7 +35,7 @@ class SchemaExtension(ComplexAttribute):
3435
"""
3536

3637

37-
class ResourceType(Resource):
38+
class ResourceType(Resource[Any]):
3839
schemas: Annotated[list[str], Required.true] = [
3940
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
4041
]
@@ -78,7 +79,7 @@ class ResourceType(Resource):
7879
"""A list of URIs of the resource type's schema extensions."""
7980

8081
@classmethod
81-
def from_resource(cls, resource_model: type[Resource]) -> Self:
82+
def from_resource(cls, resource_model: type[Resource[Any]]) -> Self:
8283
"""Build a naive ResourceType from a resource model."""
8384
schema = resource_model.model_fields["schemas"].default[0]
8485
name = schema.split(":")[-1]

scim2_models/resources/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def __getitem__(self, name: str) -> "Attribute":
258258
raise KeyError(f"This attribute has no '{name}' sub-attribute")
259259

260260

261-
class Schema(Resource):
261+
class Schema(Resource[Any]):
262262
schemas: Annotated[list[str], Required.true] = [
263263
"urn:ietf:params:scim:schemas:core:2.0:Schema"
264264
]

0 commit comments

Comments
 (0)