Skip to content

Commit c79b30a

Browse files
authored
Merge branch 'main' into validate-table-models-on-construction
2 parents 1ca48d9 + c113f5a commit c79b30a

17 files changed

+533
-514
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ repos:
2828
language: unsupported
2929
types: [python]
3030

31-
- id: local-mypy
32-
name: mypy check
33-
entry: uv run mypy sqlmodel tests/test_select_typing.py
31+
- id: local-ty
32+
name: ty check
33+
entry: uv run ty check sqlmodel tests/test_select_typing.py
3434
require_serial: true
3535
language: unsupported
3636
pass_filenames: false
@@ -41,6 +41,13 @@ repos:
4141
entry: uv run ./scripts/generate_select.py
4242
files: ^scripts/generate_select\.py|sqlmodel/sql/_expression_select_gen\.py\.jinja2$
4343

44+
- id: add-release-date
45+
language: unsupported
46+
name: add date to latest release header
47+
entry: uv run python scripts/add_latest_release_date.py
48+
files: ^docs/release-notes\.md$
49+
pass_filenames: false
50+
4451
- id: generate-readme
4552
language: unsupported
4653
name: generate README.md from index.md

docs/contributing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ That way, you can edit the documentation/source files and see the changes live.
9292

9393
/// tip
9494

95-
Alternatively, you can perform the same steps that scripts does manually.
95+
Alternatively, you can perform the same steps that the script does manually.
9696

97-
Go into the docs director at `docs/`:
97+
Go into the docs directory at `docs/`:
9898

9999
```console
100100
$ cd docs/

docs/release-notes.md

Lines changed: 57 additions & 37 deletions
Large diffs are not rendered by default.

docs_src/tutorial/where/tutorial006b_py39.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

pyproject.toml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ classifiers = [
3737
dependencies = [
3838
"SQLAlchemy >=2.0.14,<2.1.0",
3939
"pydantic>=2.11.0",
40+
"typing-extensions>=4.5.0",
4041
]
4142

4243
[project.urls]
@@ -81,9 +82,9 @@ tests = [
8182
"fastapi >=0.128.0",
8283
"httpx >=0.28.1",
8384
"jinja2 >=3.1.6",
84-
"mypy >=1.19.1",
8585
"pytest >=7.0.1",
8686
"ruff >=0.15.6",
87+
"ty>=0.0.25",
8788
"typing-extensions >=4.15.0",
8889
]
8990

@@ -124,16 +125,6 @@ exclude_lines = [
124125
[tool.coverage.html]
125126
show_contexts = true
126127

127-
[tool.mypy]
128-
strict = true
129-
exclude = "sqlmodel.sql._expression_select_gen"
130-
131-
[[tool.mypy.overrides]]
132-
module = "docs_src.*"
133-
disallow_incomplete_defs = false
134-
disallow_untyped_defs = false
135-
disallow_untyped_calls = false
136-
137128
[tool.ruff.lint]
138129
select = [
139130
"E", # pycodestyle errors
@@ -160,3 +151,6 @@ known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"]
160151
[tool.ruff.lint.pyupgrade]
161152
# Preserve types, even if a file imports `from __future__ import annotations`.
162153
keep-runtime-typing = true
154+
155+
[tool.ty.terminal]
156+
error-on-warning = true

scripts/add_latest_release_date.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Check release-notes.md and add today's date to the latest release header if missing."""
2+
3+
import re
4+
import sys
5+
from datetime import date
6+
7+
RELEASE_NOTES_FILE = "docs/release-notes.md"
8+
RELEASE_HEADER_PATTERN = re.compile(r"^## (\d+\.\d+\.\d+)\s*(\(.*\))?\s*$")
9+
10+
11+
def main() -> None:
12+
with open(RELEASE_NOTES_FILE) as f:
13+
lines = f.readlines()
14+
15+
for i, line in enumerate(lines):
16+
match = RELEASE_HEADER_PATTERN.match(line)
17+
if not match:
18+
continue
19+
20+
version = match.group(1)
21+
date_part = match.group(2)
22+
23+
if date_part:
24+
print(f"Latest release {version} already has a date: {date_part}")
25+
sys.exit(0)
26+
27+
today = date.today().isoformat()
28+
lines[i] = f"## {version} ({today})\n"
29+
print(f"Added date: {version} ({today})")
30+
31+
with open(RELEASE_NOTES_FILE, "w") as f:
32+
f.writelines(lines)
33+
sys.exit(0)
34+
35+
print("No release header found")
36+
sys.exit(1)
37+
38+
39+
if __name__ == "__main__":
40+
main()

scripts/generate_select.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class Arg(BaseModel):
3737
else:
3838
t_type = f"_T{i}"
3939
t_var = f"_TCCA[{t_type}]"
40-
arg = Arg(name=f"__ent{i}", annotation=t_var)
40+
arg = Arg(name=f"ent{i}", annotation=t_var)
4141
ret_type = t_type
4242
args.append(arg)
4343
return_types.append(ret_type)

scripts/lint.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
set -e
44
set -x
55

6-
mypy sqlmodel
7-
mypy tests/test_select_typing.py
6+
ty check sqlmodel
7+
ty check tests/test_select_typing.py
88
ruff check sqlmodel tests docs_src scripts
99
ruff format sqlmodel tests docs_src scripts --check

sqlmodel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.0.37"
1+
__version__ = "0.0.38"
22

33
# Re-export from SQLAlchemy
44
from sqlalchemy.engine import create_engine as create_engine

sqlmodel/main.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import builtins
44
import ipaddress
55
import uuid
6-
import weakref
76
from collections.abc import Callable, Mapping, Sequence, Set
87
from dataclasses import dataclass
98
from datetime import date, datetime, time, timedelta
@@ -52,7 +51,7 @@
5251
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
5352
from typing_extensions import deprecated
5453

55-
from ._compat import ( # type: ignore[attr-defined]
54+
from ._compat import (
5655
PYDANTIC_MINOR_VERSION,
5756
BaseConfig,
5857
ModelMetaclass,
@@ -101,7 +100,7 @@ def __dataclass_transform__(
101100
return lambda a: a
102101

103102

104-
class FieldInfo(PydanticFieldInfo): # type: ignore[misc]
103+
class FieldInfo(PydanticFieldInfo): # ty: ignore[subclass-of-final-class]
105104
# mypy - ignore that PydanticFieldInfo is @final
106105
def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
107106
primary_key = kwargs.pop("primary_key", False)
@@ -177,7 +176,7 @@ def __init__(
177176
cascade_delete: bool | None = False,
178177
passive_deletes: bool | Literal["all"] | None = False,
179178
link_model: Any | None = None,
180-
sa_relationship: RelationshipProperty | None = None, # type: ignore
179+
sa_relationship: RelationshipProperty | None = None,
181180
sa_relationship_args: Sequence[Any] | None = None,
182181
sa_relationship_kwargs: Mapping[str, Any] | None = None,
183182
) -> None:
@@ -398,7 +397,7 @@ def Field(
398397
nullable: bool | UndefinedType = Undefined,
399398
index: bool | UndefinedType = Undefined,
400399
sa_type: type[Any] | UndefinedType = Undefined,
401-
sa_column: Column | UndefinedType = Undefined, # type: ignore
400+
sa_column: Column | UndefinedType = Undefined,
402401
sa_column_args: Sequence[Any] | UndefinedType = Undefined,
403402
sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined,
404403
schema_extra: dict[str, Any] | None = None,
@@ -525,13 +524,13 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
525524
model_fields: ClassVar[dict[str, FieldInfo]]
526525

527526
# Replicate SQLAlchemy
528-
def __setattr__(cls, name: str, value: Any) -> None:
527+
def __setattr__(cls, name: str, value: Any) -> None: # ty: ignore[invalid-method-override]
529528
if is_table_model_class(cls):
530529
DeclarativeMeta.__setattr__(cls, name, value)
531530
else:
532531
super().__setattr__(name, value)
533532

534-
def __delattr__(cls, name: str) -> None:
533+
def __delattr__(cls, name: str) -> None: # ty: ignore[invalid-method-override]
535534
if is_table_model_class(cls):
536535
DeclarativeMeta.__delattr__(cls, name)
537536
else:
@@ -609,10 +608,10 @@ def get_config(name: str) -> Any:
609608
# This could be done by reading new_cls.model_config['table'] in FastAPI, but
610609
# that's very specific about SQLModel, so let's have another config that
611610
# other future tools based on Pydantic can use.
612-
new_cls.model_config["read_from_attributes"] = True # type: ignore[typeddict-unknown-key]
611+
new_cls.model_config["read_from_attributes"] = True # ty: ignore[invalid-key]
613612
# For compatibility with older versions
614613
# TODO: remove this in the future
615-
new_cls.model_config["read_with_orm_mode"] = True # type: ignore[typeddict-unknown-key]
614+
new_cls.model_config["read_with_orm_mode"] = True # ty: ignore[invalid-key]
616615

617616
config_registry = get_config("registry")
618617
if config_registry is not Undefined:
@@ -649,7 +648,7 @@ def __init__(
649648
# Plain forward references, for models not yet defined, are not
650649
# handled well by SQLAlchemy without Mapped, so, wrap the
651650
# annotations in Mapped here
652-
cls.__annotations__[rel_name] = Mapped[ann] # type: ignore[valid-type]
651+
cls.__annotations__[rel_name] = Mapped[ann]
653652
relationship_to = get_relationship_to(
654653
name=rel_name, rel_info=rel_info, annotation=ann
655654
)
@@ -738,7 +737,7 @@ def get_sqlalchemy_type(field: Any) -> Any:
738737
raise ValueError(f"{type_} has no matching SQLAlchemy type")
739738

740739

741-
def get_column_from_field(field: Any) -> Column: # type: ignore
740+
def get_column_from_field(field: Any) -> Column:
742741
field_info = field
743742
sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined)
744743
if isinstance(sa_column, Column):
@@ -773,7 +772,7 @@ def get_column_from_field(field: Any) -> Column: # type: ignore
773772
assert isinstance(foreign_key, str)
774773
assert isinstance(ondelete_value, (str, type(None))) # for typing
775774
args.append(ForeignKey(foreign_key, ondelete=ondelete_value))
776-
kwargs = {
775+
kwargs: dict[str, Any] = {
777776
"primary_key": primary_key,
778777
"nullable": nullable,
779778
"index": index,
@@ -797,8 +796,6 @@ def get_column_from_field(field: Any) -> Column: # type: ignore
797796
return Column(sa_type, *args, **kwargs)
798797

799798

800-
class_registry = weakref.WeakValueDictionary() # type: ignore
801-
802799
default_registry = registry()
803800

804801
_TSQLModel = TypeVar("_TSQLModel", bound="SQLModel")
@@ -814,7 +811,9 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
814811
__allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six
815812
model_config = SQLModelConfig(from_attributes=True)
816813

817-
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
814+
# Typing spec says `__new__` returning `Any` overrides normal constructor
815+
# behavior, but a missing annotation does not:
816+
def __new__(cls, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def]
818817
new_object = super().__new__(cls)
819818
# SQLAlchemy doesn't call __init__ on the base class when querying from DB
820819
# Ref: https://docs.sqlalchemy.org/en/14/orm/constructors.html
@@ -850,7 +849,7 @@ def __setattr__(self, name: str, value: Any) -> None:
850849
return
851850
else:
852851
# Set in SQLAlchemy, before Pydantic to trigger events and updates
853-
if is_table_model_class(self.__class__) and is_instrumented(self, name): # type: ignore[no-untyped-call]
852+
if is_table_model_class(self.__class__) and is_instrumented(self, name):
854853
set_attribute(self, name, value)
855854
# Set in Pydantic model to trigger possible validation changes, only for
856855
# non relationship values
@@ -870,7 +869,7 @@ def __tablename__(cls) -> str:
870869
return cls.__name__.lower()
871870

872871
@classmethod
873-
def model_validate( # type: ignore[override]
872+
def model_validate( # ty: ignore[invalid-method-override]
874873
cls: type[_TSQLModel],
875874
obj: Any,
876875
*,

0 commit comments

Comments
 (0)