Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make pydantic optional #77

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
# Python
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
interval: "weekly"
groups:
pip:
patterns:
- "*"
31 changes: 30 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,37 @@ jobs:
path: coverage
if-no-files-found: error

test-no-pydantic:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/cache@v4
id: cache
with:
path: ${{ env.pythonLocation }}
key: no-pydantic-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements.test.txt
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh
env:
COVERAGE_FILE: coverage/.coverage.no-pydantic
CONTEXT: no-pydantic
- name: Store coverage files
uses: actions/upload-artifact@v4
with:
name: .coverage.no-pydantic
path: coverage
if-no-files-found: error

coverage-combine:
needs: [test]
needs: [test,test-no-pydantic]
runs-on: ubuntu-latest

steps:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

---

Documentation: https://lancetnik.github.io/FastDepends/
Documentation: <https://lancetnik.github.io/FastDepends/>

---

Expand Down
2 changes: 1 addition & 1 deletion fast_depends/__about__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""FastDepends - extracted and cleared from HTTP domain FastAPI Dependency Injection System"""

__version__ = "2.4.2"
__version__ = "3.0.0a0"
5 changes: 3 additions & 2 deletions fast_depends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fast_depends.dependencies import Provider, dependency_provider
from fast_depends.dependencies import Provider
from fast_depends.exceptions import ValidationError
from fast_depends.use import Depends, inject

__all__ = (
"Depends",
"dependency_provider",
"ValidationError",
"Provider",
"inject",
)
141 changes: 86 additions & 55 deletions fast_depends/_compat.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,104 @@
import sys
import typing
from importlib.metadata import version as get_version
from typing import Any, Dict, Optional, Tuple, Type

from pydantic import BaseModel, create_model
from pydantic.version import VERSION as PYDANTIC_VERSION

__all__ = (
"BaseModel",
"create_model",
"evaluate_forwardref",
"PYDANTIC_V2",
"get_config_base",
"ConfigDict",
"ExceptionGroup",
"evaluate_forwardref",
)


PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")

default_pydantic_config = {"arbitrary_types_allowed": True}

evaluate_forwardref: Any
# isort: off
if PYDANTIC_V2:
from pydantic import ConfigDict
from pydantic._internal._typing_extra import ( # type: ignore[no-redef]
eval_type_lenient as evaluate_forwardref,
)

def model_schema(model: Type[BaseModel]) -> Dict[str, Any]:
return model.model_json_schema()

def get_config_base(config_data: Optional[ConfigDict] = None) -> ConfigDict:
return config_data or ConfigDict(**default_pydantic_config) # type: ignore[typeddict-item]

def get_aliases(model: Type[BaseModel]) -> Tuple[str, ...]:
return tuple(f.alias or name for name, f in model.model_fields.items())

class CreateBaseModel(BaseModel):
"""Just to support FastStream < 0.3.7."""

model_config = ConfigDict(arbitrary_types_allowed=True)
ANYIO_V3 = get_version("anyio").startswith("3.")

if ANYIO_V3:
from anyio import ExceptionGroup as ExceptionGroup
else:
from pydantic.typing import evaluate_forwardref as evaluate_forwardref # type: ignore[no-redef]
from pydantic.config import get_config, ConfigDict, BaseConfig

def get_config_base(config_data: Optional[ConfigDict] = None) -> Type[BaseConfig]: # type: ignore[misc]
return get_config(config_data or ConfigDict(**default_pydantic_config)) # type: ignore[typeddict-item]
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup as ExceptionGroup
else:
ExceptionGroup = ExceptionGroup

def model_schema(model: Type[BaseModel]) -> Dict[str, Any]:
return model.schema()

def get_aliases(model: Type[BaseModel]) -> Tuple[str, ...]:
return tuple(f.alias or name for name, f in model.__fields__.items())
def evaluate_forwardref(
value: typing.Any,
globalns: typing.Optional[typing.Dict[str, typing.Any]] = None,
localns: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> typing.Any:
"""Behaves like typing._eval_type, except it won't raise an error if a forward reference can't be resolved."""
if value is None:
value = NoneType
elif isinstance(value, str):
value = _make_forward_ref(value, is_argument=False, is_class=True)

try:
return eval_type_backport(value, globalns, localns)
except NameError:
# the point of this function is to be tolerant to this case
return value


def eval_type_backport(
value: typing.Any,
globalns: typing.Optional[typing.Dict[str, typing.Any]] = None,
localns: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> typing.Any:
"""Like `typing._eval_type`, but falls back to the `eval_type_backport` package if it's
installed to let older Python versions use newer typing features.
Specifically, this transforms `X | Y` into `typing.Union[X, Y]`
and `list[X]` into `typing.List[X]` etc. (for all the types made generic in PEP 585)
if the original syntax is not supported in the current Python version.
"""
try:
return typing._eval_type( # type: ignore
value, globalns, localns
)
except TypeError as e:
if not (isinstance(value, typing.ForwardRef) and is_backport_fixable_error(e)):
raise
try:
from eval_type_backport import eval_type_backport
except ImportError:
raise TypeError(
f"You have a type annotation {value.__forward_arg__!r} "
f"which makes use of newer typing features than are supported in your version of Python. "
f"To handle this error, you should either remove the use of new syntax "
f"or install the `eval_type_backport` package."
) from e

return eval_type_backport(value, globalns, localns, try_default=False)


def is_backport_fixable_error(e: TypeError) -> bool:
msg = str(e)
return msg.startswith("unsupported operand type(s) for |: ") or "' object is not subscriptable" in msg


if sys.version_info < (3, 10):
NoneType = type(None)
else:
from types import NoneType as NoneType

class CreateBaseModel(BaseModel): # type: ignore[no-redef]
"""Just to support FastStream < 0.3.7."""

class Config:
arbitrary_types_allowed = True
if sys.version_info < (3, 9, 8) or (3, 10) <= sys.version_info < (3, 10, 1):
def _make_forward_ref(
arg: typing.Any,
is_argument: bool = True,
*,
is_class: bool = False,
) -> typing.ForwardRef:
"""Wrapper for ForwardRef that accounts for the `is_class` argument missing in older versions.
The `module` argument is omitted as it breaks <3.9.8, =3.10.0 and isn't used in the calls below.

See https://github.com/python/cpython/pull/28560 for some background.
The backport happened on 3.9.8, see:
https://github.com/pydantic/pydantic/discussions/6244#discussioncomment-6275458,
and on 3.10.1 for the 3.10 branch, see:
https://github.com/pydantic/pydantic/issues/6912

ANYIO_V3 = get_version("anyio").startswith("3.")
Implemented as EAFP with memory.
"""
return typing.ForwardRef(arg, is_argument)

if ANYIO_V3:
from anyio import ExceptionGroup as ExceptionGroup
else:
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup as ExceptionGroup
else:
ExceptionGroup = ExceptionGroup
_make_forward_ref = typing.ForwardRef

4 changes: 2 additions & 2 deletions fast_depends/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fast_depends.core.build import build_call_model
from fast_depends.core.model import CallModel
from .builder import build_call_model
from .model import CallModel

__all__ = (
"CallModel",
Expand Down
Loading