Skip to content

Commit 50254f3

Browse files
committed
Support cursor.execute(psycopg2.sql.Composable)
In addition to str, PostgreSQL cursors accept the psycopg2.sql.Composable type, which is useful for guarding against SQL injections when building raw queries that can’t be parameterized in the normal way (e.g. interpolating identifiers). In order to avoid reintroducing a dependency on psycopg2, we define a Protocol that matches psycopg2.sql.Composable. Documentation: https://www.psycopg.org/docs/sql.html Related: python/typeshed#7494 Signed-off-by: Anders Kaseorg <[email protected]>
1 parent fe2d228 commit 50254f3

File tree

2 files changed

+32
-3
lines changed

2 files changed

+32
-3
lines changed

django-stubs/db/backends/utils.pyi

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@ import sys
33
import types
44
from contextlib import contextmanager
55
from decimal import Decimal
6-
from typing import Any, Dict, Generator, Iterator, List, Mapping, Optional, Sequence, Tuple, Type, Union, overload
6+
from typing import (
7+
Any,
8+
Dict,
9+
Generator,
10+
Iterator,
11+
List,
12+
Mapping,
13+
Optional,
14+
Protocol,
15+
Sequence,
16+
Tuple,
17+
Type,
18+
Union,
19+
overload,
20+
)
721
from uuid import UUID
822

923
if sys.version_info < (3, 8):
@@ -13,6 +27,14 @@ else:
1327

1428
logger: Any
1529

30+
# Protocol matching psycopg2.sql.Composable, to avoid depending psycopg2
31+
class _Composable(Protocol):
32+
def as_string(self, context: Any) -> str: ...
33+
def __add__(self, other: _Composable) -> _Composable: ...
34+
def __mul__(self, n: int) -> _Composable: ...
35+
36+
_ExecuteQuery = str | _Composable
37+
1638
# Python types that can be adapted to SQL.
1739
_SQLType = Union[
1840
None, bool, int, float, Decimal, str, bytes, datetime.date, datetime.datetime, UUID, Tuple[Any, ...], List[Any]
@@ -36,8 +58,8 @@ class CursorWrapper:
3658
def callproc(
3759
self, procname: str, params: Optional[Sequence[Any]] = ..., kparams: Optional[Dict[str, int]] = ...
3860
) -> Any: ...
39-
def execute(self, sql: str, params: _ExecuteParameters = ...) -> Any: ...
40-
def executemany(self, sql: str, param_list: Sequence[_ExecuteParameters]) -> Any: ...
61+
def execute(self, sql: _ExecuteQuery, params: _ExecuteParameters = ...) -> Any: ...
62+
def executemany(self, sql: _ExecuteQuery, param_list: Sequence[_ExecuteParameters]) -> Any: ...
4163

4264
class CursorDebugWrapper(CursorWrapper):
4365
cursor: Any

tests/typecheck/db/test_connection.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
with connection.cursor() as cursor:
55
reveal_type(cursor) # N: Revealed type is "django.db.backends.utils.CursorWrapper"
66
cursor.execute("SELECT %s", [123])
7+
- case: raw_connection_psycopg2_composable
8+
main: |
9+
from django.db import connection
10+
from psycopg2.sql import SQL, Identifier
11+
with connection.cursor() as cursor:
12+
reveal_type(cursor) # N: Revealed type is "django.db.backends.utils.CursorWrapper"
13+
cursor.execute(SQL("INSERT INTO {} VALUES (%s)").format(Identifier("my_table")), [123])
714
- case: raw_connections
815
main: |
916
from django.db import connections

0 commit comments

Comments
 (0)