fastapiutils / fastapi-utils

Reusable utilities for FastAPI
https://fastapiutils.github.io/fastapi-utils/
MIT License
1.94k stars 167 forks source link

[BUG] Required dependency `typing_inspect`? #318

Open kevinhikaruevans opened 4 months ago

kevinhikaruevans commented 4 months ago

Describe the bug If I'm using Pydantic 2, cbv.py imports package typing_inspect. However this is listed as an optional dependency.

To Reproduce Steps to reproduce the behavior:

  1. Install latest Fast API, fastapi-utils
  2. Add a Resource
  3. Run service using fastapi dev ...
  4. See error

Expected behavior It doesn't crash

Screenshots

│ /home/kevin/.../venv/lib/python3.11/site-packages/fastapi │
│ _utils/cbv_base.py:5 in <module>                                                                 │
│                                                                                                  │
│    2                                                                                             │
│    3 from fastapi import APIRouter, FastAPI                                                      │
│    4                                                                                             │
│ ❱  5 from .cbv import INCLUDE_INIT_PARAMS_KEY, RETURN_TYPES_FUNC_KEY, _cbv                       │
│    6                                                                                             │
│    7                                                                                             │
│    8 class Resource:                                                                             │
│                                                                                                  │
│ ╭────────────────────── locals ──────────────────────╮                                           │
│ │       Any = typing.Any                             │                                           │
│ │ APIRouter = <class 'fastapi.routing.APIRouter'>    │                                           │
│ │      Dict = typing.Dict                            │                                           │
│ │   FastAPI = <class 'fastapi.applications.FastAPI'> │                                           │
│ │  Optional = typing.Optional                        │                                           │
│ │     Tuple = typing.Tuple                           │                                           │
│ ╰────────────────────────────────────────────────────╯                                           │
│                                                                                                  │
│ /home/kevin/.../venv/lib/python3.11/site-packages/fastapi │
│ _utils/cbv.py:21 in <module>                                                                     │
│                                                                                                  │
│    18                                                                                            │
│    19 PYDANTIC_VERSION = pydantic.VERSION                                                        │
│    20 if PYDANTIC_VERSION[0] == "2":                                                             │
│ ❱  21 │   from typing_inspect import is_classvar                                                 │
│    22 else:                                                                                      │
│    23 │   from pydantic.typing import is_classvar  # type: ignore[no-redef]                      │
│    24                                                                                            │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │              Any = typing.Any                                                                │ │
│ │         APIRoute = <class 'fastapi.routing.APIRoute'>                                        │ │
│ │        APIRouter = <class 'fastapi.routing.APIRouter'>                                       │ │
│ │         Callable = typing.Callable                                                           │ │
│ │             cast = <function cast at 0x74cc440459e0>                                         │ │
│ │          Depends = <function Depends at 0x74cc4224dda0>                                      │ │
│ │   get_type_hints = <function get_type_hints at 0x74cc44045b20>                               │ │
│ │          inspect = <module 'inspect' from '/usr/lib/python3.11/inspect.py'>                  │ │
│ │             List = typing.List                                                               │ │
│ │         pydantic = <module 'pydantic' from                                                   │ │
│ │                    '/home/kevin/.../venv/lib/python3… │ │
│ │ PYDANTIC_VERSION = '2.7.4'                                                                   │ │
│ │            Route = <class 'starlette.routing.Route'>                                         │ │
│ │            Tuple = typing.Tuple                                                              │ │
│ │             Type = typing.Type                                                               │ │
│ │          TypeVar = <class 'typing.TypeVar'>                                                  │ │
│ │            Union = typing.Union                                                              │ │
│ │   WebSocketRoute = <class 'starlette.routing.WebSocketRoute'>                                │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ModuleNotFoundError: No module named 'typing_inspect'

Environment:

import fastapi_utils
import fastapi
import pydantic.utils
print(fastapi_utils.__version__)
print(fastapi.__version__)
print(pydantic.utils.version_info())

^ This also fails to run w/o typing_inspect.

After installing it:

0.7.0
0.111.0
/home/kevin/.../venv/lib/python3.11/site-packages/pydantic/_migration.py:283: UserWarning: `pydantic.utils:version_info` has been moved to `pydantic.version:version_info`.
  warnings.warn(f'`{import_path}` has been moved to `{new_location}`.')
             pydantic version: 2.7.4
        pydantic-core version: 2.18.4
          pydantic-core build: profile=release pgo=true
                 install path: /home/kevin/.../venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.6 (main, Oct  8 2023, 05:06:43) [GCC 13.2.0]
                     platform: Linux-6.5.0-41-generic-x86_64-with-glibc2.38
             related packages: typing_extensions-4.12.2 fastapi-0.111.0
                       commit: unknown

3.11.6

Additional context Add any other context about the problem here.

frost-nzcr4 commented 3 months ago

From the source of the function https://github.com/ilevkivskyi/typing_inspect/blob/master/typing_inspect.py#L261-L275:

NEW_TYPING = sys.version_info[:3] >= (3, 7, 0)  # it's always True, since fastapi-utils requires >=3.7

def is_classvar(tp):
    """Test if the type represents a class variable. Examples::

        is_classvar(int) == False
        is_classvar(ClassVar) == True
        is_classvar(ClassVar[int]) == True
        is_classvar(ClassVar[List[T]]) == True
    """
    if NEW_TYPING:
        return (tp is ClassVar or
                isinstance(tp, typingGenericAlias) and tp.__origin__ is ClassVar)
    elif WITH_CLASSVAR:
        return type(tp) is _ClassVar
    else:
        return False

we get that to solve this problem we need only a few lines instead of the undocumented requirement of typing_inspect:

from typing import _GenericAlias
if sys.version_info[:3] >= (3, 9, 0):
    from typing import _SpecialGenericAlias
    typingGenericAlias = (_GenericAlias, _SpecialGenericAlias, types.GenericAlias)
else:
    typingGenericAlias = (_GenericAlias,)

def is_classvar(tp):
    """Test if the type represents a class variable. Examples::

        is_classvar(int) == False
        is_classvar(ClassVar) == True
        is_classvar(ClassVar[int]) == True
        is_classvar(ClassVar[List[T]]) == True
    """
    return tp is ClassVar or isinstance(tp, typingGenericAlias) and tp.__origin__ is ClassVar

And after October 2024, when Python 3.8 reaches its end of life https://peps.python.org/pep-0569/, the number of lines will become even smaller.

frost-nzcr4 commented 3 months ago

There is another option using typing_extensions.get_origin. This function use the native typing.get_origin on Python >= 3.10 and has a workaround for older versions.

typing_extensions from version 4.6.1 (up to 4.12.2 at least) has this function and this module is required by Pydantic >= 2.0 (up to 2.8.2 at least) so it's always installed with the Pydantic and available to use like this:

def is_classvar(tp):
    return typing_extensions.get_origin(tp) is ClassVar