Skip to content

Commit 1edb1be

Browse files
committed
Add minimal test for descriptor behavior.
Couple more comments for some Any's that might get flagged.
1 parent 8413f13 commit 1edb1be

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
from typing_extensions import assert_type
4+
5+
from peewee import BigBitField, BigBitFieldData, CharField, ForeignKeyField, IntegerField, Model
6+
7+
8+
class User(Model):
9+
username = CharField()
10+
age = IntegerField()
11+
nickname = CharField(null=True)
12+
13+
14+
class Tweet(Model):
15+
user = ForeignKeyField(User)
16+
author = ForeignKeyField(User, null=True)
17+
18+
19+
class Event(Model):
20+
flags = BigBitField()
21+
22+
23+
# A field is a descriptor that resolves differently depending on whether it is
24+
# accessed on the model class or on an instance. `Model.field` is the Field
25+
# object itself (used to build queries), while `instance.field` is the stored
26+
# Python value.
27+
assert_type(User.username, CharField[str])
28+
assert_type(User().username, str)
29+
30+
assert_type(User.age, IntegerField[int])
31+
assert_type(User().age, int)
32+
33+
# `null=True` allows the value to include None, both in the field's own
34+
# parameterization and in the value produced on attribute access.
35+
assert_type(User.nickname, CharField[str | None])
36+
assert_type(User().nickname, str | None)
37+
38+
# Foreign keys resolve to the related model instance, or None when nullable.
39+
assert_type(Tweet.user, ForeignKeyField[User])
40+
assert_type(Tweet().user, User)
41+
assert_type(Tweet().author, User | None)
42+
43+
# BigBitField is a special case: the instance descriptor yields a
44+
# BigBitFieldData wrapper rather than the underlying bytes.
45+
assert_type(Event.flags, BigBitField)
46+
assert_type(Event().flags, BigBitFieldData)
47+
48+
# __set__ accepts the field's value type...
49+
user = User()
50+
user.username = "guido"
51+
user.age = 42
52+
user.nickname = None # nullable field accepts None
53+
54+
# ...and rejects incompatible values.
55+
user.age = "not an int" # type: ignore
56+
user.username = None # type: ignore # non-null field rejects None

stubs/peewee/peewee.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ def Default(value) -> SQL: ...
443443
class Function(ColumnBase):
444444
no_coerce_functions: ClassVar[set[str]]
445445
name: str | None
446-
arguments: tuple[Any, ...] | None
446+
arguments: tuple[Any, ...] | None # Positional SQL function args: values, columns, or other nodes
447447
def __init__(self, name, arguments, coerce: bool = True, python_value=None) -> None: ...
448448
# fn.COUNT(...), fn.SUM(...), etc. each build a Function node.
449449
def __getattr__(self, attr: str) -> Callable[..., Function]: ...
@@ -571,7 +571,7 @@ class BaseQuery(Node):
571571
def namedtuples(self, as_namedtuple: bool = True) -> Self: ...
572572
def objects(self, constructor=None) -> Self: ...
573573
def __sql__(self, ctx) -> None: ...
574-
def sql(self) -> tuple[str, list[Any]]: ...
574+
def sql(self) -> tuple[str, list[Any]]: ... # Returns (sql, params), params are bound query values
575575
def execute(self, database=None): ...
576576
async def aexecute(self, database=None): ...
577577
def iterator(self, database=None): ...
@@ -1565,6 +1565,7 @@ class ForeignKeyField(Field[_V]):
15651565
cls, model: type[_M], *args: Any, null: Literal[False] = ..., **kwargs: Unpack[_FKKwargs]
15661566
) -> ForeignKeyField[_M]: ...
15671567
@overload
1568+
# Untyped fallback: model given as a name/"self"/deferred ref, so the related type isn't known.
15681569
def __new__(cls, model: Any = ..., *args: Any, **kwargs: Unpack[_FKKwargs]) -> ForeignKeyField[Any]: ...
15691570

15701571
@property

0 commit comments

Comments
 (0)