--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 13104
+
+ Fixed issue when using ORM mappings with Python 3.14's :pep:`649` feature
+ that no longer requires "future annotations", where the ORM's introspection
+ of the ``__init__`` method of mapped classes would fail if non-present
+ identifiers in annotations were present. The vendored ``getfullargspec()``
+ method has been amended to use ``Format.FORWARDREF`` under Python 3.14 to
+ prevent resolution of names that aren't present.
+
lambda: util.py314, "Python 3.14 or above not supported"
)
+ @property
+ def pep649(self):
+ """pep649 deferred evaluation of annotations without future mode"""
+ return self.python314
+
@property
def cpython(self):
return exclusions.only_if(
from .compat import decode_backslashreplace as decode_backslashreplace
from .compat import dottedgetter as dottedgetter
from .compat import freethreading as freethreading
+from .compat import get_annotations as get_annotations
from .compat import has_refcount_gc as has_refcount_gc
from .compat import inspect_getfullargspec as inspect_getfullargspec
from .compat import is64bit as is64bit
from .langhelpers import format_argspec_plus as format_argspec_plus
from .langhelpers import generic_fn_descriptor as generic_fn_descriptor
from .langhelpers import generic_repr as generic_repr
-from .langhelpers import get_annotations as get_annotations
from .langhelpers import get_callable_argspec as get_callable_argspec
from .langhelpers import get_cls_kwargs as get_cls_kwargs
from .langhelpers import get_func_kwargs as get_func_kwargs
import typing
from typing import Any
from typing import Callable
+from typing import cast
from typing import Dict
from typing import Iterable
from typing import List
_T_co = TypeVar("_T_co", covariant=True)
+if py314:
+ # vendor a minimal form of get_annotations per
+ # https://github.com/python/cpython/issues/133684#issuecomment-2863841891
+
+ from annotationlib import call_annotate_function # type: ignore[import-not-found,unused-ignore] # noqa: E501
+ from annotationlib import Format
+
+ def _get_and_call_annotate(obj, format): # noqa: A002
+ annotate = getattr(obj, "__annotate__", None)
+ if annotate is not None:
+ ann = call_annotate_function(annotate, format, owner=obj)
+ if not isinstance(ann, dict):
+ raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
+ return ann
+ return None
+
+ # this is ported from py3.13.0a7
+ _BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
+
+ def _get_dunder_annotations(obj):
+ if isinstance(obj, type):
+ try:
+ ann = _BASE_GET_ANNOTATIONS(obj)
+ except AttributeError:
+ # For static types, the descriptor raises AttributeError.
+ return {}
+ else:
+ ann = getattr(obj, "__annotations__", None)
+ if ann is None:
+ return {}
+
+ if not isinstance(ann, dict):
+ raise ValueError(
+ f"{obj!r}.__annotations__ is neither a dict nor None"
+ )
+ return dict(ann)
+
+ def _vendored_get_annotations(
+ obj: Any, *, format: Format # noqa: A002
+ ) -> Mapping[str, Any]:
+ """A sparse implementation of annotationlib.get_annotations()"""
+
+ try:
+ ann = _get_dunder_annotations(obj)
+ except Exception:
+ pass
+ else:
+ if ann is not None:
+ return dict(ann)
+
+ # But if __annotations__ threw a NameError, we try calling __annotate__
+ ann = _get_and_call_annotate(obj, format)
+ if ann is None:
+ # If that didn't work either, we have a very weird object:
+ # evaluating
+ # __annotations__ threw NameError and there is no __annotate__.
+ # In that case,
+ # we fall back to trying __annotations__ again.
+ ann = _get_dunder_annotations(obj)
+
+ if ann is None:
+ if isinstance(obj, type) or callable(obj):
+ return {}
+ raise TypeError(f"{obj!r} does not have annotations")
+
+ if not ann:
+ return {}
+
+ return dict(ann)
+
+ def get_annotations(obj: Any) -> Mapping[str, Any]:
+ # FORWARDREF has the effect of giving us ForwardRefs and not
+ # actually trying to evaluate the annotations. We need this so
+ # that the annotations act as much like
+ # "from __future__ import annotations" as possible, which is going
+ # away in future python as a separate mode
+ return _vendored_get_annotations(obj, format=Format.FORWARDREF)
+
+elif py310:
+
+ def get_annotations(obj: Any) -> Mapping[str, Any]:
+ return inspect.get_annotations(obj)
+
+else:
+
+ def get_annotations(obj: Any) -> Mapping[str, Any]:
+ # it's been observed that cls.__annotations__ can be non present.
+ # it's not clear what causes this, running under tox py37/38 it
+ # happens, running straight pytest it doesnt
+
+ # https://docs.python.org/3/howto/annotations.html#annotations-howto
+ if isinstance(obj, type):
+ ann = obj.__dict__.get("__annotations__", None)
+ else:
+ ann = getattr(obj, "__annotations__", None)
+
+ from . import _collections
+
+ if ann is None:
+ return _collections.EMPTY_DICT
+ else:
+ return cast("Mapping[str, Any]", ann)
+
+
class FullArgSpec(typing.NamedTuple):
args: List[str]
varargs: Optional[str]
defaults: Optional[Tuple[Any, ...]]
kwonlyargs: List[str]
kwonlydefaults: Optional[Dict[str, Any]]
- annotations: Dict[str, Any]
+ annotations: Mapping[str, Any]
def inspect_getfullargspec(func: Callable[..., Any]) -> FullArgSpec:
func.__defaults__,
kwonlyargs,
func.__kwdefaults__,
- func.__annotations__,
+ get_annotations(func),
)
from typing import Generic
from typing import Iterator
from typing import List
-from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import overload
_HM = TypeVar("_HM", bound="hybridmethod[Any]")
-if compat.py314:
- # vendor a minimal form of get_annotations per
- # https://github.com/python/cpython/issues/133684#issuecomment-2863841891
-
- from annotationlib import call_annotate_function # type: ignore[import-not-found,unused-ignore] # noqa: E501
- from annotationlib import Format
-
- def _get_and_call_annotate(obj, format): # noqa: A002
- annotate = getattr(obj, "__annotate__", None)
- if annotate is not None:
- ann = call_annotate_function(annotate, format, owner=obj)
- if not isinstance(ann, dict):
- raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
- return ann
- return None
-
- # this is ported from py3.13.0a7
- _BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
-
- def _get_dunder_annotations(obj):
- if isinstance(obj, type):
- try:
- ann = _BASE_GET_ANNOTATIONS(obj)
- except AttributeError:
- # For static types, the descriptor raises AttributeError.
- return {}
- else:
- ann = getattr(obj, "__annotations__", None)
- if ann is None:
- return {}
-
- if not isinstance(ann, dict):
- raise ValueError(
- f"{obj!r}.__annotations__ is neither a dict nor None"
- )
- return dict(ann)
-
- def _vendored_get_annotations(
- obj: Any, *, format: Format # noqa: A002
- ) -> Mapping[str, Any]:
- """A sparse implementation of annotationlib.get_annotations()"""
-
- try:
- ann = _get_dunder_annotations(obj)
- except Exception:
- pass
- else:
- if ann is not None:
- return dict(ann)
-
- # But if __annotations__ threw a NameError, we try calling __annotate__
- ann = _get_and_call_annotate(obj, format)
- if ann is None:
- # If that didn't work either, we have a very weird object:
- # evaluating
- # __annotations__ threw NameError and there is no __annotate__.
- # In that case,
- # we fall back to trying __annotations__ again.
- ann = _get_dunder_annotations(obj)
-
- if ann is None:
- if isinstance(obj, type) or callable(obj):
- return {}
- raise TypeError(f"{obj!r} does not have annotations")
-
- if not ann:
- return {}
-
- return dict(ann)
-
- def get_annotations(obj: Any) -> Mapping[str, Any]:
- # FORWARDREF has the effect of giving us ForwardRefs and not
- # actually trying to evaluate the annotations. We need this so
- # that the annotations act as much like
- # "from __future__ import annotations" as possible, which is going
- # away in future python as a separate mode
- return _vendored_get_annotations(obj, format=Format.FORWARDREF)
-
-elif compat.py310:
-
- def get_annotations(obj: Any) -> Mapping[str, Any]:
- return inspect.get_annotations(obj)
-
-else:
-
- def get_annotations(obj: Any) -> Mapping[str, Any]:
- # it's been observed that cls.__annotations__ can be non present.
- # it's not clear what causes this, running under tox py37/38 it
- # happens, running straight pytest it doesnt
-
- # https://docs.python.org/3/howto/annotations.html#annotations-howto
- if isinstance(obj, type):
- ann = obj.__dict__.get("__annotations__", None)
- else:
- ann = getattr(obj, "__annotations__", None)
-
- if ann is None:
- return _collections.EMPTY_DICT
- else:
- return cast("Mapping[str, Any]", ann)
-
-
def md5_hex(x: Any) -> str:
x = x.encode("utf-8")
m = compat.md5_not_for_security()
import typing_extensions
from sqlalchemy import Column
+from sqlalchemy import util
from sqlalchemy.testing import eq_
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
types.add(lt)
is_(lt in ti, True)
eq_(len(ti), len(types), k)
+
+ @requires.pep649
+ def test_pep649_getfullargspec(self):
+ """test for #13104"""
+
+ def foo(x: Frobnizzle): # type: ignore # noqa: F821
+ pass
+
+ anno = util.get_annotations(foo)
+ eq_(
+ util.inspect_getfullargspec(foo),
+ util.compat.FullArgSpec(
+ args=["x"],
+ varargs=None,
+ varkw=None,
+ defaults=None,
+ kwonlyargs=[],
+ kwonlydefaults=None,
+ annotations=anno,
+ ),
+ )
else:
inh_type.fail()
+ @requires.pep649
+ def test_pep649_init(self, decl_base):
+
+ class A(decl_base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(
+ BigInteger, Identity(always=True), primary_key=True
+ )
+
+ bs: Mapped[list[B]] = relationship(back_populates="a")
+
+ def __init__(self, bs: list[B], *args, **kwargs):
+ super().__init__(*args, bs=bs, **kwargs)
+
+ class B(decl_base):
+ __tablename__ = "b"
+
+ id: Mapped[int] = mapped_column(
+ BigInteger, Identity(always=True), primary_key=True
+ )
+
+ a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+ a: Mapped[A] = relationship(back_populates="bs")
+
class WriteOnlyRelationshipTest(fixtures.TestBase):
def _assertions(self, A, B, lazy):
else:
inh_type.fail()
+ @requires.pep649
+ def test_pep649_init(self, decl_base):
+
+ class A(decl_base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(
+ BigInteger, Identity(always=True), primary_key=True
+ )
+
+ bs: Mapped[list[B]] = relationship(back_populates="a")
+
+ def __init__(self, bs: list[B], *args, **kwargs):
+ super().__init__(*args, bs=bs, **kwargs)
+
+ class B(decl_base):
+ __tablename__ = "b"
+
+ id: Mapped[int] = mapped_column(
+ BigInteger, Identity(always=True), primary_key=True
+ )
+
+ a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+ a: Mapped[A] = relationship(back_populates="bs")
+
class WriteOnlyRelationshipTest(fixtures.TestBase):
def _assertions(self, A, B, lazy):