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: move litestar.contrib.attrs to litestar.plugins.attrs #3862

Merged
merged 5 commits into from
Nov 21, 2024
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ clean: ## Cleanup temporary build artifacts
@echo "=> Cleaning working directory"
@rm -rf .pytest_cache .ruff_cache .hypothesis build/ -rf dist/ .eggs/
@find . -name '*.egg-info' -exec rm -rf {} +
@find . -name '*.egg' -exec rm -f {} +
@find . -type f -name '*.egg' -exec rm -f {} +
@find . -name '*.pyc' -exec rm -f {} +
@find . -name '*.pyo' -exec rm -f {} +
@find . -name '*~' -exec rm -f {} +
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@
(PY_CLASS, "litestar.contrib.pydantic.PydanticDTO"),
(PY_CLASS, "litestar.contrib.pydantic.PydanticPlugin"),
(PY_CLASS, "typing.Self"),
(PY_CLASS, "attr.AttrsInstance"),
(PY_CLASS, "typing_extensions.TypeGuard"),
]

nitpick_ignore_regex = [
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/plugins/attrs.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
attrs
=====

.. automodule:: litestar.plugins.attrs
:members:
1 change: 1 addition & 0 deletions docs/reference/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins
:maxdepth: 1
:hidden:

attrs
flash_messages
htmx
problem_details
Expand Down
31 changes: 29 additions & 2 deletions litestar/contrib/attrs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
from .attrs_schema_plugin import AttrsSchemaPlugin
# ruff: noqa: TCH004, F401
from __future__ import annotations

__all__ = ("AttrsSchemaPlugin",)
from typing import TYPE_CHECKING, Any

from litestar.utils import warn_deprecation

__all__ = ("AttrsSchemaPlugin", "is_attrs_class")


def __getattr__(attr_name: str) -> object:
if attr_name in __all__:
from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class

warn_deprecation(
deprecated_name=f"litestar.contrib.attrs.{attr_name}",
version="2.13.0",
kind="import",
removal_in="3.0",
info=f"importing {attr_name} from 'litestar.contrib.attrs' is deprecated, please "
f"import it from 'litestar.plugins.attrs' instead",
)
value = globals()[attr_name] = locals()[attr_name]
return value

raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover


if TYPE_CHECKING:
from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class
70 changes: 18 additions & 52 deletions litestar/contrib/attrs/attrs_schema_plugin.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,30 @@
# ruff: noqa: TCH004, F401
from __future__ import annotations

from typing import TYPE_CHECKING, Any

from typing_extensions import TypeGuard
from litestar.utils import warn_deprecation

from litestar.exceptions import MissingDependencyException
from litestar.plugins import OpenAPISchemaPluginProtocol
from litestar.types import Empty
from litestar.typing import FieldDefinition
from litestar.utils import is_optional_union
__all__ = ("AttrsSchemaPlugin", "is_attrs_class")

try:
import attr
import attrs
except ImportError as e:
raise MissingDependencyException("attrs") from e

if TYPE_CHECKING:
from litestar._openapi.schema_generation import SchemaCreator
from litestar.openapi.spec import Schema


class AttrsSchemaPlugin(OpenAPISchemaPluginProtocol):
@staticmethod
def is_plugin_supported_type(value: Any) -> bool:
return is_attrs_class(value) or is_attrs_class(type(value))

def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema:
"""Given a type annotation, transform it into an OpenAPI schema class.
def __getattr__(attr_name: str) -> object:
if attr_name in __all__:
from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class

Args:
field_definition: FieldDefinition instance.
schema_creator: An instance of the schema creator class

Returns:
An :class:`OpenAPI <litestar.openapi.spec.schema.Schema>` instance.
"""

type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True)
attr_fields = attr.fields_dict(field_definition.type_)
return schema_creator.create_component_schema(
field_definition,
required=sorted(
field_name
for field_name, attribute in attr_fields.items()
if attribute.default is attrs.NOTHING and not is_optional_union(type_hints[field_name])
),
property_fields={
field_name: FieldDefinition.from_kwarg(type_hints[field_name], field_name) for field_name in attr_fields
},
warn_deprecation(
deprecated_name=f"litestar.contrib.attrs.attrs_schema_plugin.{attr_name}",
version="2.13.0",
kind="import",
removal_in="3.0",
info=f"importing {attr_name} from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated, please "
f"import it from 'litestar.plugins.attrs' instead",
)
value = globals()[attr_name] = locals()[attr_name]
return value

raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover

def is_attrs_class(annotation: Any) -> TypeGuard[type[attrs.AttrsInstance]]: # pyright: ignore
"""Given a type annotation determine if the annotation is a class that includes an attrs attribute.

Args:
annotation: A type.

Returns:
A typeguard determining whether the type is an attrs class.
"""
return attrs.has(annotation) if attrs is not Empty else False # type: ignore[comparison-overlap]
if TYPE_CHECKING:
from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class
66 changes: 66 additions & 0 deletions litestar/plugins/attrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any

from typing_extensions import TypeGuard

from litestar.exceptions import MissingDependencyException
from litestar.plugins import OpenAPISchemaPluginProtocol
from litestar.types import Empty
from litestar.typing import FieldDefinition
from litestar.utils import is_optional_union

try:
import attr
import attrs
except ImportError as e:
raise MissingDependencyException("attrs") from e

if TYPE_CHECKING:
from litestar._openapi.schema_generation import SchemaCreator
from litestar.openapi.spec import Schema

__all__ = ("AttrsSchemaPlugin", "is_attrs_class")


class AttrsSchemaPlugin(OpenAPISchemaPluginProtocol):
@staticmethod
def is_plugin_supported_type(value: Any) -> bool:
return is_attrs_class(value) or is_attrs_class(type(value))

def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema:
"""Given a type annotation, transform it into an OpenAPI schema class.

Args:
field_definition: FieldDefinition instance.
schema_creator: An instance of the schema creator class

Returns:
An :class:`OpenAPI <litestar.openapi.spec.schema.Schema>` instance.
"""

type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True)
attr_fields = attr.fields_dict(field_definition.type_)
return schema_creator.create_component_schema(
field_definition,
required=sorted(
field_name
for field_name, attribute in attr_fields.items()
if attribute.default is attrs.NOTHING and not is_optional_union(type_hints[field_name])
),
property_fields={
field_name: FieldDefinition.from_kwarg(type_hints[field_name], field_name) for field_name in attr_fields
},
)


def is_attrs_class(annotation: Any) -> TypeGuard[type[attrs.AttrsInstance]]: # pyright: ignore
"""Given a type annotation determine if the annotation is a class that includes an attrs attribute.

Args:
annotation: A type.

Returns:
A typeguard determining whether the type is an attrs class.
"""
return attrs.has(annotation) if attrs is not Empty else False # type: ignore[comparison-overlap]
44 changes: 44 additions & 0 deletions tests/unit/test_contrib/test_attrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ruff: noqa: TCH004, F401
from __future__ import annotations

import sys
import warnings
from importlib.util import cache_from_source
from pathlib import Path

import pytest

from litestar.contrib import attrs as contrib_attrs
from litestar.plugins import attrs as plugin_attrs


def purge_module(module_names: list[str], path: str | Path) -> None:
for name in module_names:
if name in sys.modules:
del sys.modules[name]
Path(cache_from_source(str(path))).unlink(missing_ok=True)


def test_contrib_attrs_deprecation_warning() -> None:
"""Test that importing from contrib.attrs raises a deprecation warning."""
purge_module(["litestar.contrib.attrs"], __file__)
with pytest.warns(
DeprecationWarning, match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs' is deprecated"
):
from litestar.contrib.attrs import AttrsSchemaPlugin


def test_contrib_attrs_schema_deprecation_warning() -> None:
"""Test that importing from contrib.attrs raises a deprecation warning."""
purge_module(["litestar.contrib.attrs.attrs_schema_plugin"], __file__)
with pytest.warns(
DeprecationWarning,
match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated",
):
from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin


def test_functionality_parity() -> None:
"""Test that the functionality is identical between contrib and plugin versions."""
assert contrib_attrs.AttrsSchemaPlugin is plugin_attrs.AttrsSchemaPlugin
assert contrib_attrs.is_attrs_class is plugin_attrs.is_attrs_class
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from attrs import define
from typing_extensions import Annotated

from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin
from litestar.openapi.spec import OpenAPIType
from litestar.openapi.spec.schema import Schema
from litestar.plugins.attrs import AttrsSchemaPlugin
from litestar.typing import FieldDefinition
from litestar.utils.helpers import get_name
from tests.helpers import get_schema_for_field_definition
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_plugins/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

from litestar import Litestar, MediaType, get
from litestar.constants import UNDEFINED_SENTINELS
from litestar.contrib.attrs import AttrsSchemaPlugin
from litestar.plugins import CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPlugin, PluginRegistry
from litestar.plugins.attrs import AttrsSchemaPlugin
from litestar.plugins.core import MsgspecDIPlugin
from litestar.plugins.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin
from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin
Expand Down
Loading