Skip to content

Commit

Permalink
Support cursor.execute(psycopg2.sql.Composable)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
andersk committed Jun 28, 2022
1 parent fe2d228 commit 1cf8dfc
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 4 deletions.
3 changes: 2 additions & 1 deletion django-stubs/db/backends/postgresql/base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ from typing import Any, Dict, Tuple, Type

from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.utils import CursorDebugWrapper as BaseCursorDebugWrapper
from django.db.backends.utils import _ExecuteQuery

from .client import DatabaseClient
from .creation import DatabaseCreation
Expand Down Expand Up @@ -37,5 +38,5 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def pg_version(self) -> int: ...

class CursorDebugWrapper(BaseCursorDebugWrapper):
def copy_expert(self, sql: str, file: IOBase, *args: Any): ...
def copy_expert(self, sql: _ExecuteQuery, file: IOBase, *args: Any): ...
def copy_to(self, file: IOBase, table: str, *args: Any, **kwargs: Any): ...
28 changes: 25 additions & 3 deletions django-stubs/db/backends/utils.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import sys
import types
from contextlib import contextmanager
from decimal import Decimal
from typing import Any, Dict, Generator, Iterator, List, Mapping, Optional, Sequence, Tuple, Type, Union, overload
from typing import (
Any,
Dict,
Generator,
Iterator,
List,
Mapping,
Optional,
Protocol,
Sequence,
Tuple,
Type,
Union,
overload,
)
from uuid import UUID

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

logger: Any

# Protocol matching psycopg2.sql.Composable, to avoid depending psycopg2
class _Composable(Protocol):
def as_string(self, context: Any) -> str: ...
def __add__(self, other: _Composable) -> _Composable: ...
def __mul__(self, n: int) -> _Composable: ...

_ExecuteQuery = Union[str, _Composable]

# Python types that can be adapted to SQL.
_SQLType = Union[
None, bool, int, float, Decimal, str, bytes, datetime.date, datetime.datetime, UUID, Tuple[Any, ...], List[Any]
Expand All @@ -36,8 +58,8 @@ class CursorWrapper:
def callproc(
self, procname: str, params: Optional[Sequence[Any]] = ..., kparams: Optional[Dict[str, int]] = ...
) -> Any: ...
def execute(self, sql: str, params: _ExecuteParameters = ...) -> Any: ...
def executemany(self, sql: str, param_list: Sequence[_ExecuteParameters]) -> Any: ...
def execute(self, sql: _ExecuteQuery, params: _ExecuteParameters = ...) -> Any: ...
def executemany(self, sql: _ExecuteQuery, param_list: Sequence[_ExecuteParameters]) -> Any: ...

class CursorDebugWrapper(CursorWrapper):
cursor: Any
Expand Down
10 changes: 10 additions & 0 deletions tests/typecheck/db/test_connection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
with connection.cursor() as cursor:
reveal_type(cursor) # N: Revealed type is "django.db.backends.utils.CursorWrapper"
cursor.execute("SELECT %s", [123])
- case: raw_connection_psycopg2_composable
main: |
from django.db import connection
from psycopg2.sql import SQL, Identifier
with connection.cursor() as cursor:
cursor.execute(SQL("INSERT INTO {} VALUES (%s)").format(Identifier("my_table")), [123])
- case: raw_connections
main: |
from django.db import connections
Expand Down

0 comments on commit 1cf8dfc

Please sign in to comment.