Skip to content

Commit d37afd6

Browse files
committed
fix: support SQLAlchemy 2.1
1 parent 70fb40e commit d37afd6

4 files changed

Lines changed: 270 additions & 205 deletions

File tree

‎src/scim2_sqlalchemy/mapping.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,21 @@ class _Storage(Enum):
4949
link_type = "link type"
5050

5151

52-
def _holds_text(expression: Any) -> bool:
53-
"""Whether an expression holds strings. A type that tells no Python type is taken as text."""
52+
def _python_type(expression: Any) -> type | None:
53+
"""Return the Python type of the values of an expression, or None if its type tells none.
54+
55+
SQLAlchemy 2.0 raises NotImplementedError for such a type, and 2.1 returns object.
56+
"""
5457
try:
55-
return expression.type.python_type is str
58+
python_type: type = expression.type.python_type
5659
except NotImplementedError:
57-
return True
60+
return None
61+
return None if python_type is object else python_type
62+
63+
64+
def _holds_text(expression: Any) -> bool:
65+
"""Whether an expression holds strings. A type that tells no Python type is taken as text."""
66+
return _python_type(expression) in (str, None)
5867

5968

6069
class Attribute:

‎src/scim2_sqlalchemy/query.py‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import operator
22
from collections.abc import Callable
33
from typing import Any
4+
from typing import TypeVarTuple
45

56
from scim2_models import AttributeBinding
67
from scim2_models import InvalidCursorException
@@ -46,6 +47,7 @@
4647
from .mapping import ResourceMapping
4748
from .mapping import _Collection
4849
from .mapping import _Column
50+
from .mapping import _python_type
4951

5052
_ORDERINGS: dict[CompareOperator, Callable[[Any, Any], Any]] = {
5153
CompareOperator.eq: operator.eq,
@@ -56,11 +58,13 @@
5658
}
5759

5860

61+
_Row = TypeVarTuple("_Row")
62+
63+
5964
def _as_key(column: _Column, value: Any) -> Any:
6065
"""Return an identifier in the type of its column, or None when it cannot have that type."""
61-
try:
62-
python_type = column.expression.type.python_type
63-
except NotImplementedError:
66+
python_type = _python_type(column.expression)
67+
if python_type is None:
6468
return value
6569
try:
6670
return python_type(value)
@@ -381,14 +385,18 @@ def _where(
381385
return _FilterTranslator(mapping, scim_filter, comparator).visit(scim_filter.ast)
382386

383387

384-
def _cursor_page(page: Select[Any], search_request: SearchRequest[Any]) -> Select[Any]:
388+
def _cursor_page(
389+
page: Select[*_Row], search_request: SearchRequest[Any]
390+
) -> Select[*_Row]:
385391
"""Limit a cursor page to count + 1 rows, the last one telling whether a page follows."""
386392
if search_request.count is None:
387393
return page
388394
return page.limit(search_request.count + 1)
389395

390396

391-
def _index_page(page: Select[Any], search_request: SearchRequest[Any]) -> Select[Any]:
397+
def _index_page(
398+
page: Select[*_Row], search_request: SearchRequest[Any]
399+
) -> Select[*_Row]:
392400
"""Limit a page to the records from startIndex, at most count of them."""
393401
page = page.offset(search_request.start_index_0 or 0)
394402
if search_request.count is None:
@@ -401,7 +409,7 @@ def _search_statements(
401409
search_request: SearchRequest[Any],
402410
comparator: _Comparator,
403411
anchor: _Anchor | None = None,
404-
) -> tuple[Select[tuple[int]], Select[tuple[Any, Any, str]]]:
412+
) -> tuple[Select[int], Select[Any, Any, str]]:
405413
"""Return the statement counting the matching records, and the one selecting a page of them.
406414
407415
The page lists each record with its sort value and its identifier as
@@ -443,7 +451,7 @@ def _root_statements(
443451
search_request: SearchRequest[Any],
444452
comparator: _Comparator,
445453
anchor: _Anchor | None = None,
446-
) -> tuple[Select[tuple[int]], Select[tuple[int, str, Any]]]:
454+
) -> tuple[Select[int], Select[int, str, Any]]:
447455
"""Return the statement counting the matching records of several mappings, and the one selecting a page of them.
448456
449457
The page lists the position of the mapping, the identifier of each
@@ -496,7 +504,7 @@ def _root_statements(
496504

497505
def _records_statement(
498506
mapping: ResourceMapping, ids: list[str], parameters: ResponseParameters[Any]
499-
) -> Select[tuple[str, Any]]:
507+
) -> Select[str, Any]:
500508
"""Return the statement loading the records of a page, each with its identifier as text."""
501509
keys = [_as_key(mapping._id, value) for value in ids]
502510
return (

‎tests/test_mapping.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ class Opaque(UserDefinedType[str]):
3939
cache_ok = True
4040

4141

42+
class Unknown(UserDefinedType[str]):
43+
"""A column type refusing to tell its Python type, as SQLAlchemy 2.0 types do."""
44+
45+
cache_ok = True
46+
47+
@property
48+
def python_type(self) -> type:
49+
"""Refuse to tell the Python type."""
50+
raise NotImplementedError
51+
52+
4253
class Person(Base):
4354
__tablename__ = "people"
4455

@@ -50,6 +61,7 @@ class Person(Base):
5061
other_version: Mapped[int] = mapped_column()
5162
emails: Mapped[list["Address"]] = relationship()
5263
opaque: Mapped[str] = mapped_column(Opaque())
64+
unknown: Mapped[str] = mapped_column(Unknown())
5365

5466
__mapper_args__ = {"version_id_col": version}
5567

@@ -182,12 +194,14 @@ def test_a_link_displays_nothing_without_a_name():
182194
assert groups._display() is None
183195

184196

185-
def test_a_column_type_without_python_type_holds_text():
197+
@pytest.mark.parametrize("record_column", ["opaque", "unknown"])
198+
def test_a_column_type_without_python_type_holds_text(record_column):
186199
"""A column whose type tells no Python type is compared as it is."""
187-
mapping = person_mapping(nickName=Person.opaque)
200+
expression = getattr(Person, record_column)
201+
mapping = person_mapping(nickName=expression)
188202
column = mapping._column("nickName")
189203

190-
assert _holds_text(Person.opaque)
204+
assert _holds_text(expression)
191205
assert _as_key(column, "x") == "x"
192206

193207

0 commit comments

Comments
 (0)