Skip to content

Commit 214b0c7

Browse files
authored
Support cursor.execute(psycopg2.sql.Composable) (#1029)
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 33d4dc7 commit 214b0c7

File tree

3 files changed

+37
-4
lines changed

3 files changed

+37
-4
lines changed

django-stubs/db/backends/postgresql/base.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ from typing import Any, Dict, Tuple, Type
33

44
from django.db.backends.base.base import BaseDatabaseWrapper
55
from django.db.backends.utils import CursorDebugWrapper as BaseCursorDebugWrapper
6+
from django.db.backends.utils import _ExecuteQuery
67

78
from .client import DatabaseClient
89
from .creation import DatabaseCreation
@@ -37,5 +38,5 @@ class DatabaseWrapper(BaseDatabaseWrapper):
3738
def pg_version(self) -> int: ...
3839

3940
class CursorDebugWrapper(BaseCursorDebugWrapper):
40-
def copy_expert(self, sql: str, file: IOBase, *args: Any): ...
41+
def copy_expert(self, sql: _ExecuteQuery, file: IOBase, *args: Any): ...
4142
def copy_to(self, file: IOBase, table: str, *args: Any, **kwargs: Any): ...

django-stubs/db/backends/utils.pyi

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

1024
if sys.version_info < (3, 8):
@@ -14,6 +28,14 @@ else:
1428

1529
logger: Logger
1630

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

4365
class CursorDebugWrapper(CursorWrapper):
4466
cursor: Any

tests/typecheck/db/test_connection.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
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+
8+
9+
- case: raw_connection_psycopg2_composable
10+
main: |
11+
from django.db import connection
12+
from psycopg2.sql import SQL, Identifier
13+
with connection.cursor() as cursor:
14+
cursor.execute(SQL("INSERT INTO {} VALUES (%s)").format(Identifier("my_table")), [123])
15+
16+
717
- case: raw_connections
818
main: |
919
from django.db import connections

0 commit comments

Comments
 (0)