From: Daniele Varrazzo Date: Tue, 12 Oct 2021 14:05:47 +0000 (+0200) Subject: Add typing docs page X-Git-Tag: 3.0~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ec671a6051c4eb6a95e4ce67fd9624ea69357553;p=thirdparty%2Fpsycopg.git Add typing docs page --- diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index f8d6f8317..a554ae239 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -12,6 +12,7 @@ usages. :caption: Contents: async + typing rows pool cursors diff --git a/docs/advanced/rows.rst b/docs/advanced/rows.rst index cf6d35c38..581a68da6 100644 --- a/docs/advanced/rows.rst +++ b/docs/advanced/rows.rst @@ -114,103 +114,3 @@ These can then be used by specifying a `row_factory` argument in cur = conn.execute("SELECT first_name, last_name, age FROM persons") person = cur.fetchone() print(f"{person['first_name']} {person['last_name']}") - - -.. _row-factory-static: - -Use with a static analyzer --------------------------- - -The `~psycopg.Connection` and `~psycopg.Cursor` classes are `generic types`__: -the parameter `!Row` is passed by the ``row_factory`` argument (of the -`~Connection.connect()` and the `~Connection.cursor()` method) and it controls -what type of record is returned by the fetch methods of the cursors. The -default `~psycopg.rows.tuple_row` returns a generic tuple as return type -(`Tuple[Any, ...]`). This information can be used for type checking using a -static analyzer such as mypy_. - -.. _mypy: https://mypy.readthedocs.io/ -.. __: https://mypy.readthedocs.io/en/stable/generics.html - -.. code:: python - - conn = psycopg.connect() - # conn type is psycopg.Connection[Tuple[Any, ...]] - - dconn = psycopg.connect(row_factory=dict_row) - # dconn type is psycopg.Connection[Dict[str, Any]] - - cur = conn.cursor() - # cur type is psycopg.Cursor[Tuple[Any, ...]] - - dcur = conn.cursor(row_factory=dict_row) - dcur = dconn.cursor() - # dcur type is psycopg.Cursor[Dict[str, Any]] in both cases - - rec = cur.fetchone() - # rec type is Optional[Tuple[Any, ...]] - - drec = dcur.fetchone() - # drec type is Optional[Dict[str, Any]] - - -Example: returning records as Pydantic models -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Using Pydantic_ it is possible to enforce static typing at runtime. Using a -Pydantic model factory the code can be checked statically using mypy and -querying the database will raise an exception if the rows returned is not -compatible with the model. - -.. _Pydantic: https://pydantic-docs.helpmanual.io/ - -The following example can be checked with ``mypy --strict`` without reporting -any issue. Pydantic will also raise a runtime error in case the -`!Person` is used with a query that returns incompatible data. - -.. code:: python - - from datetime import date - from typing import Optional - - import psycopg - from psycopg.rows import class_row - from pydantic import BaseModel - - class Person(BaseModel): - id: int - first_name: str - last_name: str - dob: Optional[date] - - def fetch_person(id: int) -> Person: - with psycopg.connect() as conn: - with conn.cursor(row_factory=class_row(Person)) as cur: - cur.execute( - """ - SELECT id, first_name, last_name, dob - FROM (VALUES - (1, 'John', 'Doe', '2000-01-01'::date), - (2, 'Jane', 'White', NULL) - ) AS data (id, first_name, last_name, dob) - WHERE id = %(id)s; - """, - {"id": id}, - ) - obj = cur.fetchone() - - # reveal_type(obj) would return 'Optional[Person]' here - - if not obj: - raise KeyError(f"person {id} not found") - - # reveal_type(obj) would return 'Person' here - - return obj - - for id in [1, 2]: - p = fetch_person(id) - if p.dob: - print(f"{p.first_name} was born in {p.dob.year}") - else: - print(f"Who knows when {p.first_name} was born") diff --git a/docs/advanced/typing.rst b/docs/advanced/typing.rst new file mode 100644 index 000000000..9b58372a8 --- /dev/null +++ b/docs/advanced/typing.rst @@ -0,0 +1,128 @@ +.. currentmodule:: psycopg + +.. _static-typing: + +Static Typing +============= + +Psycopg source code is annotated according to :pep:`0484` type hints and is +checked using the current version of Mypy_ in ``--strict`` mode. + +If your application is checked using Mypy too you can make use of Psycopg +types to validate the correct use of Psycopg objects and of the data returned +by the database. + +.. _Mypy: http://mypy-lang.org/ + + +Generic types +------------- + +Psycopg `Connection` and `Cursor` objects are `~typing.Generic` objects and +support a `!Row` parameter which is the type of the records returned. + +By default methods such as `Cursor.fetchall()` return normal tuples of unknown +size and content. As such, the `connect()` function returns an object of type +`!psycopg.Connection[Tuple[Any, ...]]` and `Connection.cursor()` returns an +object of type `!psycopg.Cursor[Tuple[Any, ...]]`. If you are writing generic +plumbing code it might be practical to use annotations such as +`!Connection[Any]` and `!Cursor[Any]`. + +.. code:: python + + conn = psycopg.connect() # type is psycopg.Connection[Tuple[Any, ...]] + + cur = conn.cursor() # type is psycopg.Cursor[Tuple[Any, ...]] + + rec = cur.fetchone() # type is Optional[Tuple[Any, ...]] + + recs = cur.fetchall() # type is List[Tuple[Any, ...]] + + +.. _row-factory-static: + +Type of rows returned +--------------------- + +If you want to use connections and cursors returning your data as different +types, for instance as dictionaries, you can use the `!row_factory` argument +of the `~Connection.connect()` and the `~Connection.cursor()` method, which +will control what type of record is returned by the fetch methods of the +cursors and annotate the returned objects accordingly. See +:ref:`row-factories` for more details. + +.. code:: python + + dconn = psycopg.connect(row_factory=dict_row) + # dconn type is psycopg.Connection[Dict[str, Any]] + + dcur = conn.cursor(row_factory=dict_row) + dcur = dconn.cursor() + # dcur type is psycopg.Cursor[Dict[str, Any]] in both cases + + drec = dcur.fetchone() + # drec type is Optional[Dict[str, Any]] + + +.. _example-pydantic: + +Example: returning records as Pydantic models +--------------------------------------------- + +Using Pydantic_ it is possible to enforce static typing at runtime. Using a +Pydantic model factory the code can be checked statically using Mypy and +querying the database will raise an exception if the rows returned is not +compatible with the model. + +.. _Pydantic: https://pydantic-docs.helpmanual.io/ + +The following example can be checked with ``mypy --strict`` without reporting +any issue. Pydantic will also raise a runtime error in case the +`!Person` is used with a query that returns incompatible data. + +.. code:: python + + from datetime import date + from typing import Optional + + import psycopg + from psycopg.rows import class_row + from pydantic import BaseModel + + class Person(BaseModel): + id: int + first_name: str + last_name: str + dob: Optional[date] + + def fetch_person(id: int) -> Person: + with psycopg.connect() as conn: + with conn.cursor(row_factory=class_row(Person)) as cur: + cur.execute( + """ + SELECT id, first_name, last_name, dob + FROM (VALUES + (1, 'John', 'Doe', '2000-01-01'::date), + (2, 'Jane', 'White', NULL) + ) AS data (id, first_name, last_name, dob) + WHERE id = %(id)s; + """, + {"id": id}, + ) + obj = cur.fetchone() + + # reveal_type(obj) would return 'Optional[Person]' here + + if not obj: + raise KeyError(f"person {id} not found") + + # reveal_type(obj) would return 'Person' here + + return obj + + for id in [1, 2]: + p = fetch_person(id) + if p.dob: + print(f"{p.first_name} was born in {p.dob.year}") + else: + print(f"Who knows when {p.first_name} was born") diff --git a/docs/basic/from_pg2.rst b/docs/basic/from_pg2.rst index 18f27f033..f0ae5c19b 100644 --- a/docs/basic/from_pg2.rst +++ b/docs/basic/from_pg2.rst @@ -204,8 +204,9 @@ What's new in Psycopg 3 - :ref:`Asynchronous support ` - :ref:`Server-side parameters binding ` -- :ref:`prepared statements ` +- :ref:`Prepared statements ` - :ref:`Binary communication ` - :ref:`Python-based COPY support ` -- :ref:`Support for static typing ` +- :ref:`Support for static typing ` +- :ref:`A redesigned connection pool ` - :ref:`Direct access to the libpq functionalities ` diff --git a/docs/index.rst b/docs/index.rst index f6c7d0389..f40d6deca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,8 @@ more modern PostgreSQL and Python features, such as: - :ref:`prepared statements ` - :ref:`binary communication ` - :ref:`great COPY support ` -- :ref:`support for static typing ` +- :ref:`support for static typing ` +- :ref:`a redesigned connection pool ` - :ref:`direct access to the libpq functionalities ` .. _Python: https://www.python.org/