From: Federico Caselli Date: Thu, 30 Jan 2025 21:05:59 +0000 (+0100) Subject: Removed the deprecated mypy plugin. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d69765829c9fbc98f21a22dbc496f2b7b22dc52c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Removed the deprecated mypy plugin. The plugin was non-functional with newer version of mypy and it's no longer needed with modern SQLAlchemy declarative style. Fixes: #12293 Change-Id: If4581ab58623f0a2992f4e33a6dcdae002c68dad --- diff --git a/doc/build/changelog/unreleased_21/12293.rst b/doc/build/changelog/unreleased_21/12293.rst new file mode 100644 index 0000000000..c8782bb82a --- /dev/null +++ b/doc/build/changelog/unreleased_21/12293.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: typing + :tickets: 12293 + + Removed the deprecated mypy plugin. + The plugin was non-functional with newer version of mypy and it's no + longer needed with modern SQLAlchemy declarative style. diff --git a/doc/build/orm/declarative_mixins.rst b/doc/build/orm/declarative_mixins.rst index 9f26207c07..1c6179809a 100644 --- a/doc/build/orm/declarative_mixins.rst +++ b/doc/build/orm/declarative_mixins.rst @@ -141,7 +141,7 @@ attribute is used on the newly defined class. :func:`_orm.mapped_column`. .. versionchanged:: 2.0 For users coming from the 1.4 series of SQLAlchemy - who may have been using the :ref:`mypy plugin `, the + who may have been using the ``mypy plugin``, the :func:`_orm.declarative_mixin` class decorator is no longer needed to mark declarative mixins, assuming the mypy plugin is no longer in use. diff --git a/doc/build/orm/extensions/index.rst b/doc/build/orm/extensions/index.rst index 0dda58affa..ba040b9f65 100644 --- a/doc/build/orm/extensions/index.rst +++ b/doc/build/orm/extensions/index.rst @@ -20,7 +20,6 @@ behavior. In particular the "Horizontal Sharding", "Hybrid Attributes", and automap baked declarative/index - mypy mutable orderinglist horizontal_shard diff --git a/doc/build/orm/extensions/mypy.rst b/doc/build/orm/extensions/mypy.rst deleted file mode 100644 index dbca3f35f9..0000000000 --- a/doc/build/orm/extensions/mypy.rst +++ /dev/null @@ -1,606 +0,0 @@ -.. _mypy_toplevel: - -Mypy / Pep-484 Support for ORM Mappings -======================================== - -Support for :pep:`484` typing annotations as well as the -MyPy_ type checking tool when using SQLAlchemy -:ref:`declarative ` mappings -that refer to the :class:`_schema.Column` object directly, rather than -the :func:`_orm.mapped_column` construct introduced in SQLAlchemy 2.0. - -.. deprecated:: 2.0 - - **The SQLAlchemy Mypy Plugin is DEPRECATED, and will be removed possibly - as early as the SQLAlchemy 2.1 release. We would urge users to please - migrate away from it ASAP. The mypy plugin also works only up until - mypy version 1.10.1. version 1.11.0 and greater may not work properly.** - - This plugin cannot be maintained across constantly changing releases - of mypy and its stability going forward CANNOT be guaranteed. - - Modern SQLAlchemy now offers - :ref:`fully pep-484 compliant mapping syntaxes `; - see the linked section for migration details. - -.. topic:: SQLAlchemy Mypy Plugin Status Update - - **Updated July 2024** - - The mypy plugin is supported **only up until mypy 1.10.1, and it will have - issues running with 1.11.0 or greater**. Use with mypy 1.11.0 or greater - may have error conditions which currently cannot be resolved. - - For SQLAlchemy 2.0, the Mypy plugin continues to work at the level at which - it reached in the SQLAlchemy 1.4 release. SQLAlchemy 2.0 however features - an - :ref:`all new typing system ` - for ORM Declarative models that removes the need for the Mypy plugin and - delivers much more consistent behavior with generally superior capabilities. - Note that this new capability is **not - part of SQLAlchemy 1.4, it is only in SQLAlchemy 2.0**. - - The SQLAlchemy Mypy plugin, while it has technically never left the "alpha" - stage, should **now be considered as deprecated in SQLAlchemy 2.0, even - though it is still necessary for full Mypy support when using - SQLAlchemy 1.4**. - - The Mypy plugin itself does not solve the issue of supplying correct typing - with other typing tools such as Pylance/Pyright, Pytype, Pycharm, etc, which - cannot make use of Mypy plugins. Additionally, Mypy plugins are extremely - difficult to develop, maintain and test, as a Mypy plugin must be deeply - integrated with Mypy's internal datastructures and processes, which itself - are not stable within the Mypy project itself. The SQLAlchemy Mypy plugin - has lots of limitations when used with code that deviates from very basic - patterns which are reported regularly. - - For these reasons, new non-regression issues reported against the Mypy - plugin are unlikely to be fixed. **Existing code that passes Mypy checks - using the plugin with SQLAlchemy 1.4 installed will continue to pass all - checks in SQLAlchemy 2.0 without any changes required, provided the plugin - is still used. SQLAlchemy 2.0's API is fully - backwards compatible with the SQLAlchemy 1.4 API and Mypy plugin behavior.** - - End-user code that passes all checks under SQLAlchemy 1.4 with the Mypy - plugin may incrementally migrate to the new structures, once - that code is running exclusively on SQLAlchemy 2.0. See the section - :ref:`whatsnew_20_orm_declarative_typing` for background on how this - migration may proceed. - - Code that is running exclusively on SQLAlchemy version - 2.0 and has fully migrated to the new declarative constructs will enjoy full - compliance with pep-484 as well as working correctly within IDEs and other - typing tools, without the need for plugins. - - -Installation ------------- - -For **SQLAlchemy 2.0 only**: No stubs should be installed and packages -like sqlalchemy-stubs_ and sqlalchemy2-stubs_ should be fully uninstalled. - -The Mypy_ package itself is a dependency. - -Mypy may be installed using the "mypy" extras hook using pip: - -.. sourcecode:: text - - pip install sqlalchemy[mypy] - -The plugin itself is configured as described in -`Configuring mypy to use Plugins `_, -using the ``sqlalchemy.ext.mypy.plugin`` module name, such as within -``setup.cfg``:: - - [mypy] - plugins = sqlalchemy.ext.mypy.plugin - -.. _sqlalchemy-stubs: https://github.com/dropbox/sqlalchemy-stubs - -.. _sqlalchemy2-stubs: https://github.com/sqlalchemy/sqlalchemy2-stubs - -What the Plugin Does --------------------- - -The primary purpose of the Mypy plugin is to intercept and alter the static -definition of SQLAlchemy -:ref:`declarative mappings ` so that -they match up to how they are structured after they have been -:term:`instrumented` by their :class:`_orm.Mapper` objects. This allows both -the class structure itself as well as code that uses the class to make sense to -the Mypy tool, which otherwise would not be the case based on how declarative -mappings currently function. The plugin is not unlike similar plugins -that are required for libraries like -`dataclasses `_ which -alter classes dynamically at runtime. - -To cover the major areas where this occurs, consider the following ORM -mapping, using the typical example of the ``User`` class:: - - from sqlalchemy import Column, Integer, String, select - from sqlalchemy.orm import declarative_base - - # "Base" is a class that is created dynamically from the - # declarative_base() function - Base = declarative_base() - - - class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - - # "some_user" is an instance of the User class, which - # accepts "id" and "name" kwargs based on the mapping - some_user = User(id=5, name="user") - - # it has an attribute called .name that's a string - print(f"Username: {some_user.name}") - - # a select() construct makes use of SQL expressions derived from the - # User class itself - select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s")) - -Above, the steps that the Mypy extension can take include: - -* Interpretation of the ``Base`` dynamic class generated by - :func:`_orm.declarative_base`, so that classes which inherit from it - are known to be mapped. It also can accommodate the class decorator - approach described at :ref:`orm_declarative_decorator`. - -* Type inference for ORM mapped attributes that are defined in declarative - "inline" style, in the above example the ``id`` and ``name`` attributes of - the ``User`` class. This includes that an instance of ``User`` will use - ``int`` for ``id`` and ``str`` for ``name``. It also includes that when the - ``User.id`` and ``User.name`` class-level attributes are accessed, as they - are above in the ``select()`` statement, they are compatible with SQL - expression behavior, which is derived from the - :class:`_orm.InstrumentedAttribute` attribute descriptor class. - -* Application of an ``__init__()`` method to mapped classes that do not - already include an explicit constructor, which accepts keyword arguments - of specific types for all mapped attributes detected. - -When the Mypy plugin processes the above file, the resulting static class -definition and Python code passed to the Mypy tool is equivalent to the -following:: - - from sqlalchemy import Column, Integer, String, select - from sqlalchemy.orm import Mapped - from sqlalchemy.orm.decl_api import DeclarativeMeta - - - class Base(metaclass=DeclarativeMeta): - __abstract__ = True - - - class User(Base): - __tablename__ = "user" - - id: Mapped[Optional[int]] = Mapped._special_method( - Column(Integer, primary_key=True) - ) - name: Mapped[Optional[str]] = Mapped._special_method(Column(String)) - - def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ... - - - some_user = User(id=5, name="user") - - print(f"Username: {some_user.name}") - - select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s")) - -The key steps which have been taken above include: - -* The ``Base`` class is now defined in terms of the :class:`_orm.DeclarativeMeta` - class explicitly, rather than being a dynamic class. - -* The ``id`` and ``name`` attributes are defined in terms of the - :class:`_orm.Mapped` class, which represents a Python descriptor that - exhibits different behaviors at the class vs. instance levels. The - :class:`_orm.Mapped` class is now the base class for the :class:`_orm.InstrumentedAttribute` - class that is used for all ORM mapped attributes. - - :class:`_orm.Mapped` is defined as a generic class against arbitrary Python - types, meaning specific occurrences of :class:`_orm.Mapped` are associated - with a specific Python type, such as ``Mapped[Optional[int]]`` and - ``Mapped[Optional[str]]`` above. - -* The right-hand side of the declarative mapped attribute assignments are - **removed**, as this resembles the operation that the :class:`_orm.Mapper` - class would normally be doing, which is that it would be replacing these - attributes with specific instances of :class:`_orm.InstrumentedAttribute`. - The original expression is moved into a function call that will allow it to - still be type-checked without conflicting with the left-hand side of the - expression. For Mypy purposes, the left-hand typing annotation is sufficient - for the attribute's behavior to be understood. - -* A type stub for the ``User.__init__()`` method is added which includes the - correct keywords and datatypes. - -Usage ------- - -The following subsections will address individual uses cases that have -so far been considered for pep-484 compliance. - - -Introspection of Columns based on TypeEngine -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For mapped columns that include an explicit datatype, when they are mapped -as inline attributes, the mapped type will be introspected automatically:: - - class MyClass(Base): - # ... - - id = Column(Integer, primary_key=True) - name = Column("employee_name", String(50), nullable=False) - other_name = Column(String(50)) - -Above, the ultimate class-level datatypes of ``id``, ``name`` and -``other_name`` will be introspected as ``Mapped[Optional[int]]``, -``Mapped[Optional[str]]`` and ``Mapped[Optional[str]]``. The types are by -default **always** considered to be ``Optional``, even for the primary key and -non-nullable column. The reason is because while the database columns "id" and -"name" can't be NULL, the Python attributes ``id`` and ``name`` most certainly -can be ``None`` without an explicit constructor:: - - >>> m1 = MyClass() - >>> m1.id - None - -The types of the above columns can be stated **explicitly**, providing the -two advantages of clearer self-documentation as well as being able to -control which types are optional:: - - class MyClass(Base): - # ... - - id: int = Column(Integer, primary_key=True) - name: str = Column("employee_name", String(50), nullable=False) - other_name: Optional[str] = Column(String(50)) - -The Mypy plugin will accept the above ``int``, ``str`` and ``Optional[str]`` -and convert them to include the ``Mapped[]`` type surrounding them. The -``Mapped[]`` construct may also be used explicitly:: - - from sqlalchemy.orm import Mapped - - - class MyClass(Base): - # ... - - id: Mapped[int] = Column(Integer, primary_key=True) - name: Mapped[str] = Column("employee_name", String(50), nullable=False) - other_name: Mapped[Optional[str]] = Column(String(50)) - -When the type is non-optional, it simply means that the attribute as accessed -from an instance of ``MyClass`` will be considered to be non-None:: - - mc = MyClass(...) - - # will pass mypy --strict - name: str = mc.name - -For optional attributes, Mypy considers that the type must include None -or otherwise be ``Optional``:: - - mc = MyClass(...) - - # will pass mypy --strict - other_name: Optional[str] = mc.name - -Whether or not the mapped attribute is typed as ``Optional``, the -generation of the ``__init__()`` method will **still consider all keywords -to be optional**. This is again matching what the SQLAlchemy ORM actually -does when it creates the constructor, and should not be confused with the -behavior of a validating system such as Python ``dataclasses`` which will -generate a constructor that matches the annotations in terms of optional -vs. required attributes. - - -Columns that Don't have an Explicit Type -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Columns that include a :class:`_schema.ForeignKey` modifier do not need -to specify a datatype in a SQLAlchemy declarative mapping. For -this type of attribute, the Mypy plugin will inform the user that it -needs an explicit type to be sent:: - - # .. other imports - from sqlalchemy.sql.schema import ForeignKey - - Base = declarative_base() - - - class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - - class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id = Column(ForeignKey("user.id")) - -The plugin will deliver the message as follows: - -.. sourcecode:: text - - $ mypy test3.py --strict - test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from - ORM mapped expression assigned to attribute 'user_id'; please specify a - Python type or Mapped[] on the left hand side. - Found 1 error in 1 file (checked 1 source file) - -To resolve, apply an explicit type annotation to the ``Address.user_id`` -column:: - - class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - -Mapping Columns with Imperative Table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In :ref:`imperative table style `, the -:class:`_schema.Column` definitions are given inside of a :class:`_schema.Table` -construct which is separate from the mapped attributes themselves. The Mypy -plugin does not consider this :class:`_schema.Table`, but instead supports that -the attributes can be explicitly stated with a complete annotation that -**must** use the :class:`_orm.Mapped` class to identify them as mapped attributes:: - - class MyClass(Base): - __table__ = Table( - "mytable", - Base.metadata, - Column(Integer, primary_key=True), - Column("employee_name", String(50), nullable=False), - Column(String(50)), - ) - - id: Mapped[int] - name: Mapped[str] - other_name: Mapped[Optional[str]] - -The above :class:`_orm.Mapped` annotations are considered as mapped columns and -will be included in the default constructor, as well as provide the correct -typing profile for ``MyClass`` both at the class level and the instance level. - -Mapping Relationships -^^^^^^^^^^^^^^^^^^^^^^ - -The plugin has limited support for using type inference to detect the types -for relationships. For all those cases where it can't detect the type, -it will emit an informative error message, and in all cases the appropriate -type may be provided explicitly, either with the :class:`_orm.Mapped` -class or optionally omitting it for an inline declaration. The plugin -also needs to determine whether or not the relationship refers to a collection -or a scalar, and for that it relies upon the explicit value of -the :paramref:`_orm.relationship.uselist` and/or :paramref:`_orm.relationship.collection_class` -parameters. An explicit type is needed if neither of these parameters are -present, as well as if the target type of the :func:`_orm.relationship` -is a string or callable, and not a class:: - - class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - - class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - - user = relationship(User) - -The above mapping will produce the following error: - -.. sourcecode:: text - - test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or - collection for ORM mapped expression assigned to attribute 'user' - if both 'uselist' and 'collection_class' arguments are absent from the - relationship(); please specify a type annotation on the left hand side. - Found 1 error in 1 file (checked 1 source file) - -The error can be resolved either by using ``relationship(User, uselist=False)`` -or by providing the type, in this case the scalar ``User`` object:: - - class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - - user: User = relationship(User) - -For collections, a similar pattern applies, where in the absence of -``uselist=True`` or a :paramref:`_orm.relationship.collection_class`, -a collection annotation such as ``List`` may be used. It is also fully -appropriate to use the string name of the class in the annotation as supported -by pep-484, ensuring the class is imported with in -the `TYPE_CHECKING block `_ -as appropriate:: - - from typing import TYPE_CHECKING, List - - from .mymodel import Base - - if TYPE_CHECKING: - # if the target of the relationship is in another module - # that cannot normally be imported at runtime - from .myaddressmodel import Address - - - class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - addresses: List["Address"] = relationship("Address") - -As is the case with columns, the :class:`_orm.Mapped` class may also be -applied explicitly:: - - class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user") - - - class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - - user: Mapped[User] = relationship(User, back_populates="addresses") - -.. _mypy_declarative_mixins: - -Using @declared_attr and Declarative Mixins -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :class:`_orm.declared_attr` class allows Declarative mapped attributes to -be declared in class level functions, and is particularly useful when using -:ref:`declarative mixins `. For these functions, the return -type of the function should be annotated using either the ``Mapped[]`` -construct or by indicating the exact kind of object returned by the function. -Additionally, "mixin" classes that are not otherwise mapped (i.e. don't extend -from a :func:`_orm.declarative_base` class nor are they mapped with a method -such as :meth:`_orm.registry.mapped`) should be decorated with the -:func:`_orm.declarative_mixin` decorator, which provides a hint to the Mypy -plugin that a particular class intends to serve as a declarative mixin:: - - from sqlalchemy.orm import declarative_mixin, declared_attr - - - @declarative_mixin - class HasUpdatedAt: - @declared_attr - def updated_at(cls) -> Column[DateTime]: # uses Column - return Column(DateTime) - - - @declarative_mixin - class HasCompany: - @declared_attr - def company_id(cls) -> Mapped[int]: # uses Mapped - return mapped_column(ForeignKey("company.id")) - - @declared_attr - def company(cls) -> Mapped["Company"]: - return relationship("Company") - - - class Employee(HasUpdatedAt, HasCompany, Base): - __tablename__ = "employee" - - id = Column(Integer, primary_key=True) - name = Column(String) - -Note the mismatch between the actual return type of a method like -``HasCompany.company`` vs. what is annotated. The Mypy plugin converts -all ``@declared_attr`` functions into simple annotated attributes to avoid -this complexity:: - - # what Mypy sees - class HasCompany: - company_id: Mapped[int] - company: Mapped["Company"] - -Combining with Dataclasses or Other Type-Sensitive Attribute Systems -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The examples of Python dataclasses integration at :ref:`orm_declarative_dataclasses` -presents a problem; Python dataclasses expect an explicit type that it will -use to build the class, and the value given in each assignment statement -is significant. That is, a class as follows has to be stated exactly -as it is in order to be accepted by dataclasses:: - - mapper_registry: registry = registry() - - - @mapper_registry.mapped - @dataclass - class User: - __table__ = Table( - "user", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("fullname", String(50)), - Column("nickname", String(12)), - ) - id: int = field(init=False) - name: Optional[str] = None - fullname: Optional[str] = None - nickname: Optional[str] = None - addresses: List[Address] = field(default_factory=list) - - __mapper_args__ = { # type: ignore - "properties": {"addresses": relationship("Address")} - } - -We can't apply our ``Mapped[]`` types to the attributes ``id``, ``name``, -etc. because they will be rejected by the ``@dataclass`` decorator. Additionally, -Mypy has another plugin for dataclasses explicitly which can also get in the -way of what we're doing. - -The above class will actually pass Mypy's type checking without issue; the -only thing we are missing is the ability for attributes on ``User`` to be -used in SQL expressions, such as:: - - stmt = select(User.name).where(User.id.in_([1, 2, 3])) - -To provide a workaround for this, the Mypy plugin has an additional feature -whereby we can specify an extra attribute ``_mypy_mapped_attrs``, that is -a list that encloses the class-level objects or their string names. -This attribute can be conditional within the ``TYPE_CHECKING`` variable:: - - @mapper_registry.mapped - @dataclass - class User: - __table__ = Table( - "user", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("fullname", String(50)), - Column("nickname", String(12)), - ) - id: int = field(init=False) - name: Optional[str] = None - fullname: Optional[str] - nickname: Optional[str] - addresses: List[Address] = field(default_factory=list) - - if TYPE_CHECKING: - _mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses] - - __mapper_args__ = { # type: ignore - "properties": {"addresses": relationship("Address")} - } - -With the above recipe, the attributes listed in ``_mypy_mapped_attrs`` -will be applied with the :class:`_orm.Mapped` typing information so that the -``User`` class will behave as a SQLAlchemy mapped class when used in a -class-bound context. - -.. _Mypy: https://mypy.readthedocs.io/ diff --git a/lib/sqlalchemy/ext/mypy/__init__.py b/lib/sqlalchemy/ext/mypy/__init__.py deleted file mode 100644 index b5827cb8d3..0000000000 --- a/lib/sqlalchemy/ext/mypy/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# ext/mypy/__init__.py -# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/lib/sqlalchemy/ext/mypy/apply.py b/lib/sqlalchemy/ext/mypy/apply.py deleted file mode 100644 index 02908cc14b..0000000000 --- a/lib/sqlalchemy/ext/mypy/apply.py +++ /dev/null @@ -1,324 +0,0 @@ -# ext/mypy/apply.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -from __future__ import annotations - -from typing import List -from typing import Optional -from typing import Union - -from mypy.nodes import ARG_NAMED_OPT -from mypy.nodes import Argument -from mypy.nodes import AssignmentStmt -from mypy.nodes import CallExpr -from mypy.nodes import ClassDef -from mypy.nodes import MDEF -from mypy.nodes import MemberExpr -from mypy.nodes import NameExpr -from mypy.nodes import RefExpr -from mypy.nodes import StrExpr -from mypy.nodes import SymbolTableNode -from mypy.nodes import TempNode -from mypy.nodes import TypeInfo -from mypy.nodes import Var -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.plugins.common import add_method_to_class -from mypy.types import AnyType -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import NoneTyp -from mypy.types import ProperType -from mypy.types import TypeOfAny -from mypy.types import UnboundType -from mypy.types import UnionType - -from . import infer -from . import util -from .names import expr_to_mapped_constructor -from .names import NAMED_TYPE_SQLA_MAPPED - - -def apply_mypy_mapped_attr( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - item: Union[NameExpr, StrExpr], - attributes: List[util.SQLAlchemyAttribute], -) -> None: - if isinstance(item, NameExpr): - name = item.name - elif isinstance(item, StrExpr): - name = item.value - else: - return None - - for stmt in cls.defs.body: - if ( - isinstance(stmt, AssignmentStmt) - and isinstance(stmt.lvalues[0], NameExpr) - and stmt.lvalues[0].name == name - ): - break - else: - util.fail(api, f"Can't find mapped attribute {name}", cls) - return None - - if stmt.type is None: - util.fail( - api, - "Statement linked from _mypy_mapped_attrs has no " - "typing information", - stmt, - ) - return None - - left_hand_explicit_type = get_proper_type(stmt.type) - assert isinstance( - left_hand_explicit_type, (Instance, UnionType, UnboundType) - ) - - attributes.append( - util.SQLAlchemyAttribute( - name=name, - line=item.line, - column=item.column, - typ=left_hand_explicit_type, - info=cls.info, - ) - ) - - apply_type_to_mapped_statement( - api, stmt, stmt.lvalues[0], left_hand_explicit_type, None - ) - - -def re_apply_declarative_assignments( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - attributes: List[util.SQLAlchemyAttribute], -) -> None: - """For multiple class passes, re-apply our left-hand side types as mypy - seems to reset them in place. - - """ - mapped_attr_lookup = {attr.name: attr for attr in attributes} - update_cls_metadata = False - - for stmt in cls.defs.body: - # for a re-apply, all of our statements are AssignmentStmt; - # @declared_attr calls will have been converted and this - # currently seems to be preserved by mypy (but who knows if this - # will change). - if ( - isinstance(stmt, AssignmentStmt) - and isinstance(stmt.lvalues[0], NameExpr) - and stmt.lvalues[0].name in mapped_attr_lookup - and isinstance(stmt.lvalues[0].node, Var) - ): - left_node = stmt.lvalues[0].node - - python_type_for_type = mapped_attr_lookup[ - stmt.lvalues[0].name - ].type - - left_node_proper_type = get_proper_type(left_node.type) - - # if we have scanned an UnboundType and now there's a more - # specific type than UnboundType, call the re-scan so we - # can get that set up correctly - if ( - isinstance(python_type_for_type, UnboundType) - and not isinstance(left_node_proper_type, UnboundType) - and ( - isinstance(stmt.rvalue, CallExpr) - and isinstance(stmt.rvalue.callee, MemberExpr) - and isinstance(stmt.rvalue.callee.expr, NameExpr) - and stmt.rvalue.callee.expr.node is not None - and stmt.rvalue.callee.expr.node.fullname - == NAMED_TYPE_SQLA_MAPPED - and stmt.rvalue.callee.name == "_empty_constructor" - and isinstance(stmt.rvalue.args[0], CallExpr) - and isinstance(stmt.rvalue.args[0].callee, RefExpr) - ) - ): - new_python_type_for_type = ( - infer.infer_type_from_right_hand_nameexpr( - api, - stmt, - left_node, - left_node_proper_type, - stmt.rvalue.args[0].callee, - ) - ) - - if new_python_type_for_type is not None and not isinstance( - new_python_type_for_type, UnboundType - ): - python_type_for_type = new_python_type_for_type - - # update the SQLAlchemyAttribute with the better - # information - mapped_attr_lookup[stmt.lvalues[0].name].type = ( - python_type_for_type - ) - - update_cls_metadata = True - - if ( - not isinstance(left_node.type, Instance) - or left_node.type.type.fullname != NAMED_TYPE_SQLA_MAPPED - ): - assert python_type_for_type is not None - left_node.type = api.named_type( - NAMED_TYPE_SQLA_MAPPED, [python_type_for_type] - ) - - if update_cls_metadata: - util.set_mapped_attributes(cls.info, attributes) - - -def apply_type_to_mapped_statement( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - lvalue: NameExpr, - left_hand_explicit_type: Optional[ProperType], - python_type_for_type: Optional[ProperType], -) -> None: - """Apply the Mapped[] annotation and right hand object to a - declarative assignment statement. - - This converts a Python declarative class statement such as:: - - class User(Base): - # ... - - attrname = Column(Integer) - - To one that describes the final Python behavior to Mypy:: - - ... format: off - - class User(Base): - # ... - - attrname : Mapped[Optional[int]] = - - ... format: on - - """ - left_node = lvalue.node - assert isinstance(left_node, Var) - - # to be completely honest I have no idea what the difference between - # left_node.type and stmt.type is, what it means if these are different - # vs. the same, why in order to get tests to pass I have to assign - # to stmt.type for the second case and not the first. this is complete - # trying every combination until it works stuff. - - if left_hand_explicit_type is not None: - lvalue.is_inferred_def = False - left_node.type = api.named_type( - NAMED_TYPE_SQLA_MAPPED, [left_hand_explicit_type] - ) - else: - lvalue.is_inferred_def = False - left_node.type = api.named_type( - NAMED_TYPE_SQLA_MAPPED, - ( - [AnyType(TypeOfAny.special_form)] - if python_type_for_type is None - else [python_type_for_type] - ), - ) - - # so to have it skip the right side totally, we can do this: - # stmt.rvalue = TempNode(AnyType(TypeOfAny.special_form)) - - # however, if we instead manufacture a new node that uses the old - # one, then we can still get type checking for the call itself, - # e.g. the Column, relationship() call, etc. - - # rewrite the node as: - # : Mapped[] = - # _sa_Mapped._empty_constructor() - # the original right-hand side is maintained so it gets type checked - # internally - stmt.rvalue = expr_to_mapped_constructor(stmt.rvalue) - - if stmt.type is not None and python_type_for_type is not None: - stmt.type = python_type_for_type - - -def add_additional_orm_attributes( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - attributes: List[util.SQLAlchemyAttribute], -) -> None: - """Apply __init__, __table__ and other attributes to the mapped class.""" - - info = util.info_for_cls(cls, api) - - if info is None: - return - - is_base = util.get_is_base(info) - - if "__init__" not in info.names and not is_base: - mapped_attr_names = {attr.name: attr.type for attr in attributes} - - for base in info.mro[1:-1]: - if "sqlalchemy" not in info.metadata: - continue - - base_cls_attributes = util.get_mapped_attributes(base, api) - if base_cls_attributes is None: - continue - - for attr in base_cls_attributes: - mapped_attr_names.setdefault(attr.name, attr.type) - - arguments = [] - for name, typ in mapped_attr_names.items(): - if typ is None: - typ = AnyType(TypeOfAny.special_form) - arguments.append( - Argument( - variable=Var(name, typ), - type_annotation=typ, - initializer=TempNode(typ), - kind=ARG_NAMED_OPT, - ) - ) - - add_method_to_class(api, cls, "__init__", arguments, NoneTyp()) - - if "__table__" not in info.names and util.get_has_table(info): - _apply_placeholder_attr_to_class( - api, cls, "sqlalchemy.sql.schema.Table", "__table__" - ) - if not is_base: - _apply_placeholder_attr_to_class( - api, cls, "sqlalchemy.orm.mapper.Mapper", "__mapper__" - ) - - -def _apply_placeholder_attr_to_class( - api: SemanticAnalyzerPluginInterface, - cls: ClassDef, - qualified_name: str, - attrname: str, -) -> None: - sym = api.lookup_fully_qualified_or_none(qualified_name) - if sym: - assert isinstance(sym.node, TypeInfo) - type_: ProperType = Instance(sym.node, []) - else: - type_ = AnyType(TypeOfAny.special_form) - var = Var(attrname) - var._fullname = cls.fullname + "." + attrname - var.info = cls.info - var.type = type_ - cls.info.names[attrname] = SymbolTableNode(MDEF, var) diff --git a/lib/sqlalchemy/ext/mypy/decl_class.py b/lib/sqlalchemy/ext/mypy/decl_class.py deleted file mode 100644 index 2ce7ad56cc..0000000000 --- a/lib/sqlalchemy/ext/mypy/decl_class.py +++ /dev/null @@ -1,515 +0,0 @@ -# ext/mypy/decl_class.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -from __future__ import annotations - -from typing import List -from typing import Optional -from typing import Union - -from mypy.nodes import AssignmentStmt -from mypy.nodes import CallExpr -from mypy.nodes import ClassDef -from mypy.nodes import Decorator -from mypy.nodes import LambdaExpr -from mypy.nodes import ListExpr -from mypy.nodes import MemberExpr -from mypy.nodes import NameExpr -from mypy.nodes import PlaceholderNode -from mypy.nodes import RefExpr -from mypy.nodes import StrExpr -from mypy.nodes import SymbolNode -from mypy.nodes import SymbolTableNode -from mypy.nodes import TempNode -from mypy.nodes import TypeInfo -from mypy.nodes import Var -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.types import AnyType -from mypy.types import CallableType -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import NoneType -from mypy.types import ProperType -from mypy.types import Type -from mypy.types import TypeOfAny -from mypy.types import UnboundType -from mypy.types import UnionType - -from . import apply -from . import infer -from . import names -from . import util - - -def scan_declarative_assignments_and_apply_types( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - is_mixin_scan: bool = False, -) -> Optional[List[util.SQLAlchemyAttribute]]: - info = util.info_for_cls(cls, api) - - if info is None: - # this can occur during cached passes - return None - elif cls.fullname.startswith("builtins"): - return None - - mapped_attributes: Optional[List[util.SQLAlchemyAttribute]] = ( - util.get_mapped_attributes(info, api) - ) - - # used by assign.add_additional_orm_attributes among others - util.establish_as_sqlalchemy(info) - - if mapped_attributes is not None: - # ensure that a class that's mapped is always picked up by - # its mapped() decorator or declarative metaclass before - # it would be detected as an unmapped mixin class - - if not is_mixin_scan: - # mypy can call us more than once. it then *may* have reset the - # left hand side of everything, but not the right that we removed, - # removing our ability to re-scan. but we have the types - # here, so lets re-apply them, or if we have an UnboundType, - # we can re-scan - - apply.re_apply_declarative_assignments(cls, api, mapped_attributes) - - return mapped_attributes - - mapped_attributes = [] - - if not cls.defs.body: - # when we get a mixin class from another file, the body is - # empty (!) but the names are in the symbol table. so use that. - - for sym_name, sym in info.names.items(): - _scan_symbol_table_entry( - cls, api, sym_name, sym, mapped_attributes - ) - else: - for stmt in util.flatten_typechecking(cls.defs.body): - if isinstance(stmt, AssignmentStmt): - _scan_declarative_assignment_stmt( - cls, api, stmt, mapped_attributes - ) - elif isinstance(stmt, Decorator): - _scan_declarative_decorator_stmt( - cls, api, stmt, mapped_attributes - ) - _scan_for_mapped_bases(cls, api) - - if not is_mixin_scan: - apply.add_additional_orm_attributes(cls, api, mapped_attributes) - - util.set_mapped_attributes(info, mapped_attributes) - - return mapped_attributes - - -def _scan_symbol_table_entry( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - name: str, - value: SymbolTableNode, - attributes: List[util.SQLAlchemyAttribute], -) -> None: - """Extract mapping information from a SymbolTableNode that's in the - type.names dictionary. - - """ - value_type = get_proper_type(value.type) - if not isinstance(value_type, Instance): - return - - left_hand_explicit_type = None - type_id = names.type_id_for_named_node(value_type.type) - # type_id = names._type_id_for_unbound_type(value.type.type, cls, api) - - err = False - - # TODO: this is nearly the same logic as that of - # _scan_declarative_decorator_stmt, likely can be merged - if type_id in { - names.MAPPED, - names.RELATIONSHIP, - names.COMPOSITE_PROPERTY, - names.MAPPER_PROPERTY, - names.SYNONYM_PROPERTY, - names.COLUMN_PROPERTY, - }: - if value_type.args: - left_hand_explicit_type = get_proper_type(value_type.args[0]) - else: - err = True - elif type_id is names.COLUMN: - if not value_type.args: - err = True - else: - typeengine_arg: Union[ProperType, TypeInfo] = get_proper_type( - value_type.args[0] - ) - if isinstance(typeengine_arg, Instance): - typeengine_arg = typeengine_arg.type - - if isinstance(typeengine_arg, (UnboundType, TypeInfo)): - sym = api.lookup_qualified(typeengine_arg.name, typeengine_arg) - if sym is not None and isinstance(sym.node, TypeInfo): - if names.has_base_type_id(sym.node, names.TYPEENGINE): - left_hand_explicit_type = UnionType( - [ - infer.extract_python_type_from_typeengine( - api, sym.node, [] - ), - NoneType(), - ] - ) - else: - util.fail( - api, - "Column type should be a TypeEngine " - "subclass not '{}'".format(sym.node.fullname), - value_type, - ) - - if err: - msg = ( - "Can't infer type from attribute {} on class {}. " - "please specify a return type from this function that is " - "one of: Mapped[], relationship[], " - "Column[], MapperProperty[]" - ) - util.fail(api, msg.format(name, cls.name), cls) - - left_hand_explicit_type = AnyType(TypeOfAny.special_form) - - if left_hand_explicit_type is not None: - assert value.node is not None - attributes.append( - util.SQLAlchemyAttribute( - name=name, - line=value.node.line, - column=value.node.column, - typ=left_hand_explicit_type, - info=cls.info, - ) - ) - - -def _scan_declarative_decorator_stmt( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - stmt: Decorator, - attributes: List[util.SQLAlchemyAttribute], -) -> None: - """Extract mapping information from a @declared_attr in a declarative - class. - - E.g.:: - - @reg.mapped - class MyClass: - # ... - - @declared_attr - def updated_at(cls) -> Column[DateTime]: - return Column(DateTime) - - Will resolve in mypy as:: - - @reg.mapped - class MyClass: - # ... - - updated_at: Mapped[Optional[datetime.datetime]] - - """ - for dec in stmt.decorators: - if ( - isinstance(dec, (NameExpr, MemberExpr, SymbolNode)) - and names.type_id_for_named_node(dec) is names.DECLARED_ATTR - ): - break - else: - return - - dec_index = cls.defs.body.index(stmt) - - left_hand_explicit_type: Optional[ProperType] = None - - if util.name_is_dunder(stmt.name): - # for dunder names like __table_args__, __tablename__, - # __mapper_args__ etc., rewrite these as simple assignment - # statements; otherwise mypy doesn't like if the decorated - # function has an annotation like ``cls: Type[Foo]`` because - # it isn't @classmethod - any_ = AnyType(TypeOfAny.special_form) - left_node = NameExpr(stmt.var.name) - left_node.node = stmt.var - new_stmt = AssignmentStmt([left_node], TempNode(any_)) - new_stmt.type = left_node.node.type - cls.defs.body[dec_index] = new_stmt - return - elif isinstance(stmt.func.type, CallableType): - func_type = stmt.func.type.ret_type - if isinstance(func_type, UnboundType): - type_id = names.type_id_for_unbound_type(func_type, cls, api) - else: - # this does not seem to occur unless the type argument is - # incorrect - return - - if ( - type_id - in { - names.MAPPED, - names.RELATIONSHIP, - names.COMPOSITE_PROPERTY, - names.MAPPER_PROPERTY, - names.SYNONYM_PROPERTY, - names.COLUMN_PROPERTY, - } - and func_type.args - ): - left_hand_explicit_type = get_proper_type(func_type.args[0]) - elif type_id is names.COLUMN and func_type.args: - typeengine_arg = func_type.args[0] - if isinstance(typeengine_arg, UnboundType): - sym = api.lookup_qualified(typeengine_arg.name, typeengine_arg) - if sym is not None and isinstance(sym.node, TypeInfo): - if names.has_base_type_id(sym.node, names.TYPEENGINE): - left_hand_explicit_type = UnionType( - [ - infer.extract_python_type_from_typeengine( - api, sym.node, [] - ), - NoneType(), - ] - ) - else: - util.fail( - api, - "Column type should be a TypeEngine " - "subclass not '{}'".format(sym.node.fullname), - func_type, - ) - - if left_hand_explicit_type is None: - # no type on the decorated function. our option here is to - # dig into the function body and get the return type, but they - # should just have an annotation. - msg = ( - "Can't infer type from @declared_attr on function '{}'; " - "please specify a return type from this function that is " - "one of: Mapped[], relationship[], " - "Column[], MapperProperty[]" - ) - util.fail(api, msg.format(stmt.var.name), stmt) - - left_hand_explicit_type = AnyType(TypeOfAny.special_form) - - left_node = NameExpr(stmt.var.name) - left_node.node = stmt.var - - # totally feeling around in the dark here as I don't totally understand - # the significance of UnboundType. It seems to be something that is - # not going to do what's expected when it is applied as the type of - # an AssignmentStatement. So do a feeling-around-in-the-dark version - # of converting it to the regular Instance/TypeInfo/UnionType structures - # we see everywhere else. - if isinstance(left_hand_explicit_type, UnboundType): - left_hand_explicit_type = get_proper_type( - util.unbound_to_instance(api, left_hand_explicit_type) - ) - - left_node.node.type = api.named_type( - names.NAMED_TYPE_SQLA_MAPPED, [left_hand_explicit_type] - ) - - # this will ignore the rvalue entirely - # rvalue = TempNode(AnyType(TypeOfAny.special_form)) - - # rewrite the node as: - # : Mapped[] = - # _sa_Mapped._empty_constructor(lambda: ) - # the function body is maintained so it gets type checked internally - rvalue = names.expr_to_mapped_constructor( - LambdaExpr(stmt.func.arguments, stmt.func.body) - ) - - new_stmt = AssignmentStmt([left_node], rvalue) - new_stmt.type = left_node.node.type - - attributes.append( - util.SQLAlchemyAttribute( - name=left_node.name, - line=stmt.line, - column=stmt.column, - typ=left_hand_explicit_type, - info=cls.info, - ) - ) - cls.defs.body[dec_index] = new_stmt - - -def _scan_declarative_assignment_stmt( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - attributes: List[util.SQLAlchemyAttribute], -) -> None: - """Extract mapping information from an assignment statement in a - declarative class. - - """ - lvalue = stmt.lvalues[0] - if not isinstance(lvalue, NameExpr): - return - - sym = cls.info.names.get(lvalue.name) - - # this establishes that semantic analysis has taken place, which - # means the nodes are populated and we are called from an appropriate - # hook. - assert sym is not None - node = sym.node - - if isinstance(node, PlaceholderNode): - return - - assert node is lvalue.node - assert isinstance(node, Var) - - if node.name == "__abstract__": - if api.parse_bool(stmt.rvalue) is True: - util.set_is_base(cls.info) - return - elif node.name == "__tablename__": - util.set_has_table(cls.info) - elif node.name.startswith("__"): - return - elif node.name == "_mypy_mapped_attrs": - if not isinstance(stmt.rvalue, ListExpr): - util.fail(api, "_mypy_mapped_attrs is expected to be a list", stmt) - else: - for item in stmt.rvalue.items: - if isinstance(item, (NameExpr, StrExpr)): - apply.apply_mypy_mapped_attr(cls, api, item, attributes) - - left_hand_mapped_type: Optional[Type] = None - left_hand_explicit_type: Optional[ProperType] = None - - if node.is_inferred or node.type is None: - if isinstance(stmt.type, UnboundType): - # look for an explicit Mapped[] type annotation on the left - # side with nothing on the right - - # print(stmt.type) - # Mapped?[Optional?[A?]] - - left_hand_explicit_type = stmt.type - - if stmt.type.name == "Mapped": - mapped_sym = api.lookup_qualified("Mapped", cls) - if ( - mapped_sym is not None - and mapped_sym.node is not None - and names.type_id_for_named_node(mapped_sym.node) - is names.MAPPED - ): - left_hand_explicit_type = get_proper_type( - stmt.type.args[0] - ) - left_hand_mapped_type = stmt.type - - # TODO: do we need to convert from unbound for this case? - # left_hand_explicit_type = util._unbound_to_instance( - # api, left_hand_explicit_type - # ) - else: - node_type = get_proper_type(node.type) - if ( - isinstance(node_type, Instance) - and names.type_id_for_named_node(node_type.type) is names.MAPPED - ): - # print(node.type) - # sqlalchemy.orm.attributes.Mapped[] - left_hand_explicit_type = get_proper_type(node_type.args[0]) - left_hand_mapped_type = node_type - else: - # print(node.type) - # - left_hand_explicit_type = node_type - left_hand_mapped_type = None - - if isinstance(stmt.rvalue, TempNode) and left_hand_mapped_type is not None: - # annotation without assignment and Mapped is present - # as type annotation - # equivalent to using _infer_type_from_left_hand_type_only. - - python_type_for_type = left_hand_explicit_type - elif isinstance(stmt.rvalue, CallExpr) and isinstance( - stmt.rvalue.callee, RefExpr - ): - python_type_for_type = infer.infer_type_from_right_hand_nameexpr( - api, stmt, node, left_hand_explicit_type, stmt.rvalue.callee - ) - - if python_type_for_type is None: - return - - else: - return - - assert python_type_for_type is not None - - attributes.append( - util.SQLAlchemyAttribute( - name=node.name, - line=stmt.line, - column=stmt.column, - typ=python_type_for_type, - info=cls.info, - ) - ) - - apply.apply_type_to_mapped_statement( - api, - stmt, - lvalue, - left_hand_explicit_type, - python_type_for_type, - ) - - -def _scan_for_mapped_bases( - cls: ClassDef, - api: SemanticAnalyzerPluginInterface, -) -> None: - """Given a class, iterate through its superclass hierarchy to find - all other classes that are considered as ORM-significant. - - Locates non-mapped mixins and scans them for mapped attributes to be - applied to subclasses. - - """ - - info = util.info_for_cls(cls, api) - - if info is None: - return - - for base_info in info.mro[1:-1]: - if base_info.fullname.startswith("builtins"): - continue - - # scan each base for mapped attributes. if they are not already - # scanned (but have all their type info), that means they are unmapped - # mixins - scan_declarative_assignments_and_apply_types( - base_info.defn, api, is_mixin_scan=True - ) diff --git a/lib/sqlalchemy/ext/mypy/infer.py b/lib/sqlalchemy/ext/mypy/infer.py deleted file mode 100644 index 26a83cca83..0000000000 --- a/lib/sqlalchemy/ext/mypy/infer.py +++ /dev/null @@ -1,590 +0,0 @@ -# ext/mypy/infer.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -from __future__ import annotations - -from typing import Optional -from typing import Sequence - -from mypy.maptype import map_instance_to_supertype -from mypy.nodes import AssignmentStmt -from mypy.nodes import CallExpr -from mypy.nodes import Expression -from mypy.nodes import FuncDef -from mypy.nodes import LambdaExpr -from mypy.nodes import MemberExpr -from mypy.nodes import NameExpr -from mypy.nodes import RefExpr -from mypy.nodes import StrExpr -from mypy.nodes import TypeInfo -from mypy.nodes import Var -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.subtypes import is_subtype -from mypy.types import AnyType -from mypy.types import CallableType -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import NoneType -from mypy.types import ProperType -from mypy.types import TypeOfAny -from mypy.types import UnionType - -from . import names -from . import util - - -def infer_type_from_right_hand_nameexpr( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], - infer_from_right_side: RefExpr, -) -> Optional[ProperType]: - type_id = names.type_id_for_callee(infer_from_right_side) - if type_id is None: - return None - elif type_id is names.MAPPED: - python_type_for_type = _infer_type_from_mapped( - api, stmt, node, left_hand_explicit_type, infer_from_right_side - ) - elif type_id is names.COLUMN: - python_type_for_type = _infer_type_from_decl_column( - api, stmt, node, left_hand_explicit_type - ) - elif type_id is names.RELATIONSHIP: - python_type_for_type = _infer_type_from_relationship( - api, stmt, node, left_hand_explicit_type - ) - elif type_id is names.COLUMN_PROPERTY: - python_type_for_type = _infer_type_from_decl_column_property( - api, stmt, node, left_hand_explicit_type - ) - elif type_id is names.SYNONYM_PROPERTY: - python_type_for_type = infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - elif type_id is names.COMPOSITE_PROPERTY: - python_type_for_type = _infer_type_from_decl_composite_property( - api, stmt, node, left_hand_explicit_type - ) - else: - return None - - return python_type_for_type - - -def _infer_type_from_relationship( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], -) -> Optional[ProperType]: - """Infer the type of mapping from a relationship. - - E.g.:: - - @reg.mapped - class MyClass: - # ... - - addresses = relationship(Address, uselist=True) - - order: Mapped["Order"] = relationship("Order") - - Will resolve in mypy as:: - - @reg.mapped - class MyClass: - # ... - - addresses: Mapped[List[Address]] - - order: Mapped["Order"] - - """ - - assert isinstance(stmt.rvalue, CallExpr) - target_cls_arg = stmt.rvalue.args[0] - python_type_for_type: Optional[ProperType] = None - - if isinstance(target_cls_arg, NameExpr) and isinstance( - target_cls_arg.node, TypeInfo - ): - # type - related_object_type = target_cls_arg.node - python_type_for_type = Instance(related_object_type, []) - - # other cases not covered - an error message directs the user - # to set an explicit type annotation - # - # node.type == str, it's a string - # if isinstance(target_cls_arg, NameExpr) and isinstance( - # target_cls_arg.node, Var - # ) - # points to a type - # isinstance(target_cls_arg, NameExpr) and isinstance( - # target_cls_arg.node, TypeAlias - # ) - # string expression - # isinstance(target_cls_arg, StrExpr) - - uselist_arg = util.get_callexpr_kwarg(stmt.rvalue, "uselist") - collection_cls_arg: Optional[Expression] = util.get_callexpr_kwarg( - stmt.rvalue, "collection_class" - ) - type_is_a_collection = False - - # this can be used to determine Optional for a many-to-one - # in the same way nullable=False could be used, if we start supporting - # that. - # innerjoin_arg = util.get_callexpr_kwarg(stmt.rvalue, "innerjoin") - - if ( - uselist_arg is not None - and api.parse_bool(uselist_arg) is True - and collection_cls_arg is None - ): - type_is_a_collection = True - if python_type_for_type is not None: - python_type_for_type = api.named_type( - names.NAMED_TYPE_BUILTINS_LIST, [python_type_for_type] - ) - elif ( - uselist_arg is None or api.parse_bool(uselist_arg) is True - ) and collection_cls_arg is not None: - type_is_a_collection = True - if isinstance(collection_cls_arg, CallExpr): - collection_cls_arg = collection_cls_arg.callee - - if isinstance(collection_cls_arg, NameExpr) and isinstance( - collection_cls_arg.node, TypeInfo - ): - if python_type_for_type is not None: - # this can still be overridden by the left hand side - # within _infer_Type_from_left_and_inferred_right - python_type_for_type = Instance( - collection_cls_arg.node, [python_type_for_type] - ) - elif ( - isinstance(collection_cls_arg, NameExpr) - and isinstance(collection_cls_arg.node, FuncDef) - and collection_cls_arg.node.type is not None - ): - if python_type_for_type is not None: - # this can still be overridden by the left hand side - # within _infer_Type_from_left_and_inferred_right - - # TODO: handle mypy.types.Overloaded - if isinstance(collection_cls_arg.node.type, CallableType): - rt = get_proper_type(collection_cls_arg.node.type.ret_type) - - if isinstance(rt, CallableType): - callable_ret_type = get_proper_type(rt.ret_type) - if isinstance(callable_ret_type, Instance): - python_type_for_type = Instance( - callable_ret_type.type, - [python_type_for_type], - ) - else: - util.fail( - api, - "Expected Python collection type for " - "collection_class parameter", - stmt.rvalue, - ) - python_type_for_type = None - elif uselist_arg is not None and api.parse_bool(uselist_arg) is False: - if collection_cls_arg is not None: - util.fail( - api, - "Sending uselist=False and collection_class at the same time " - "does not make sense", - stmt.rvalue, - ) - if python_type_for_type is not None: - python_type_for_type = UnionType( - [python_type_for_type, NoneType()] - ) - - else: - if left_hand_explicit_type is None: - msg = ( - "Can't infer scalar or collection for ORM mapped expression " - "assigned to attribute '{}' if both 'uselist' and " - "'collection_class' arguments are absent from the " - "relationship(); please specify a " - "type annotation on the left hand side." - ) - util.fail(api, msg.format(node.name), node) - - if python_type_for_type is None: - return infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - elif left_hand_explicit_type is not None: - if type_is_a_collection: - assert isinstance(left_hand_explicit_type, Instance) - assert isinstance(python_type_for_type, Instance) - return _infer_collection_type_from_left_and_inferred_right( - api, node, left_hand_explicit_type, python_type_for_type - ) - else: - return _infer_type_from_left_and_inferred_right( - api, - node, - left_hand_explicit_type, - python_type_for_type, - ) - else: - return python_type_for_type - - -def _infer_type_from_decl_composite_property( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], -) -> Optional[ProperType]: - """Infer the type of mapping from a Composite.""" - - assert isinstance(stmt.rvalue, CallExpr) - target_cls_arg = stmt.rvalue.args[0] - python_type_for_type = None - - if isinstance(target_cls_arg, NameExpr) and isinstance( - target_cls_arg.node, TypeInfo - ): - related_object_type = target_cls_arg.node - python_type_for_type = Instance(related_object_type, []) - else: - python_type_for_type = None - - if python_type_for_type is None: - return infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - elif left_hand_explicit_type is not None: - return _infer_type_from_left_and_inferred_right( - api, node, left_hand_explicit_type, python_type_for_type - ) - else: - return python_type_for_type - - -def _infer_type_from_mapped( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], - infer_from_right_side: RefExpr, -) -> Optional[ProperType]: - """Infer the type of mapping from a right side expression - that returns Mapped. - - - """ - assert isinstance(stmt.rvalue, CallExpr) - - # (Pdb) print(stmt.rvalue.callee) - # NameExpr(query_expression [sqlalchemy.orm._orm_constructors.query_expression]) # noqa: E501 - # (Pdb) stmt.rvalue.callee.node - # - # (Pdb) stmt.rvalue.callee.node.type - # def [_T] (default_expr: sqlalchemy.sql.elements.ColumnElement[_T`-1] =) -> sqlalchemy.orm.base.Mapped[_T`-1] # noqa: E501 - # sqlalchemy.orm.base.Mapped[_T`-1] - # the_mapped_type = stmt.rvalue.callee.node.type.ret_type - - # TODO: look at generic ref and either use that, - # or reconcile w/ what's present, etc. - the_mapped_type = util.type_for_callee(infer_from_right_side) # noqa - - return infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - - -def _infer_type_from_decl_column_property( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], -) -> Optional[ProperType]: - """Infer the type of mapping from a ColumnProperty. - - This includes mappings against ``column_property()`` as well as the - ``deferred()`` function. - - """ - assert isinstance(stmt.rvalue, CallExpr) - - if stmt.rvalue.args: - first_prop_arg = stmt.rvalue.args[0] - - if isinstance(first_prop_arg, CallExpr): - type_id = names.type_id_for_callee(first_prop_arg.callee) - - # look for column_property() / deferred() etc with Column as first - # argument - if type_id is names.COLUMN: - return _infer_type_from_decl_column( - api, - stmt, - node, - left_hand_explicit_type, - right_hand_expression=first_prop_arg, - ) - - if isinstance(stmt.rvalue, CallExpr): - type_id = names.type_id_for_callee(stmt.rvalue.callee) - # this is probably not strictly necessary as we have to use the left - # hand type for query expression in any case. any other no-arg - # column prop objects would go here also - if type_id is names.QUERY_EXPRESSION: - return _infer_type_from_decl_column( - api, - stmt, - node, - left_hand_explicit_type, - ) - - return infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - - -def _infer_type_from_decl_column( - api: SemanticAnalyzerPluginInterface, - stmt: AssignmentStmt, - node: Var, - left_hand_explicit_type: Optional[ProperType], - right_hand_expression: Optional[CallExpr] = None, -) -> Optional[ProperType]: - """Infer the type of mapping from a Column. - - E.g.:: - - @reg.mapped - class MyClass: - # ... - - a = Column(Integer) - - b = Column("b", String) - - c: Mapped[int] = Column(Integer) - - d: bool = Column(Boolean) - - Will resolve in MyPy as:: - - @reg.mapped - class MyClass: - # ... - - a: Mapped[int] - - b: Mapped[str] - - c: Mapped[int] - - d: Mapped[bool] - - """ - assert isinstance(node, Var) - - callee = None - - if right_hand_expression is None: - if not isinstance(stmt.rvalue, CallExpr): - return None - - right_hand_expression = stmt.rvalue - - for column_arg in right_hand_expression.args[0:2]: - if isinstance(column_arg, CallExpr): - if isinstance(column_arg.callee, RefExpr): - # x = Column(String(50)) - callee = column_arg.callee - type_args: Sequence[Expression] = column_arg.args - break - elif isinstance(column_arg, (NameExpr, MemberExpr)): - if isinstance(column_arg.node, TypeInfo): - # x = Column(String) - callee = column_arg - type_args = () - break - else: - # x = Column(some_name, String), go to next argument - continue - elif isinstance(column_arg, (StrExpr,)): - # x = Column("name", String), go to next argument - continue - elif isinstance(column_arg, (LambdaExpr,)): - # x = Column("name", String, default=lambda: uuid.uuid4()) - # go to next argument - continue - else: - assert False - - if callee is None: - return None - - if isinstance(callee.node, TypeInfo) and names.mro_has_id( - callee.node.mro, names.TYPEENGINE - ): - python_type_for_type = extract_python_type_from_typeengine( - api, callee.node, type_args - ) - - if left_hand_explicit_type is not None: - return _infer_type_from_left_and_inferred_right( - api, node, left_hand_explicit_type, python_type_for_type - ) - - else: - return UnionType([python_type_for_type, NoneType()]) - else: - # it's not TypeEngine, it's typically implicitly typed - # like ForeignKey. we can't infer from the right side. - return infer_type_from_left_hand_type_only( - api, node, left_hand_explicit_type - ) - - -def _infer_type_from_left_and_inferred_right( - api: SemanticAnalyzerPluginInterface, - node: Var, - left_hand_explicit_type: ProperType, - python_type_for_type: ProperType, - orig_left_hand_type: Optional[ProperType] = None, - orig_python_type_for_type: Optional[ProperType] = None, -) -> Optional[ProperType]: - """Validate type when a left hand annotation is present and we also - could infer the right hand side:: - - attrname: SomeType = Column(SomeDBType) - - """ - - if orig_left_hand_type is None: - orig_left_hand_type = left_hand_explicit_type - if orig_python_type_for_type is None: - orig_python_type_for_type = python_type_for_type - - if not is_subtype(left_hand_explicit_type, python_type_for_type): - effective_type = api.named_type( - names.NAMED_TYPE_SQLA_MAPPED, [orig_python_type_for_type] - ) - - msg = ( - "Left hand assignment '{}: {}' not compatible " - "with ORM mapped expression of type {}" - ) - util.fail( - api, - msg.format( - node.name, - util.format_type(orig_left_hand_type, api.options), - util.format_type(effective_type, api.options), - ), - node, - ) - - return orig_left_hand_type - - -def _infer_collection_type_from_left_and_inferred_right( - api: SemanticAnalyzerPluginInterface, - node: Var, - left_hand_explicit_type: Instance, - python_type_for_type: Instance, -) -> Optional[ProperType]: - orig_left_hand_type = left_hand_explicit_type - orig_python_type_for_type = python_type_for_type - - if left_hand_explicit_type.args: - left_hand_arg = get_proper_type(left_hand_explicit_type.args[0]) - python_type_arg = get_proper_type(python_type_for_type.args[0]) - else: - left_hand_arg = left_hand_explicit_type - python_type_arg = python_type_for_type - - assert isinstance(left_hand_arg, (Instance, UnionType)) - assert isinstance(python_type_arg, (Instance, UnionType)) - - return _infer_type_from_left_and_inferred_right( - api, - node, - left_hand_arg, - python_type_arg, - orig_left_hand_type=orig_left_hand_type, - orig_python_type_for_type=orig_python_type_for_type, - ) - - -def infer_type_from_left_hand_type_only( - api: SemanticAnalyzerPluginInterface, - node: Var, - left_hand_explicit_type: Optional[ProperType], -) -> Optional[ProperType]: - """Determine the type based on explicit annotation only. - - if no annotation were present, note that we need one there to know - the type. - - """ - if left_hand_explicit_type is None: - msg = ( - "Can't infer type from ORM mapped expression " - "assigned to attribute '{}'; please specify a " - "Python type or " - "Mapped[] on the left hand side." - ) - util.fail(api, msg.format(node.name), node) - - return api.named_type( - names.NAMED_TYPE_SQLA_MAPPED, [AnyType(TypeOfAny.special_form)] - ) - - else: - # use type from the left hand side - return left_hand_explicit_type - - -def extract_python_type_from_typeengine( - api: SemanticAnalyzerPluginInterface, - node: TypeInfo, - type_args: Sequence[Expression], -) -> ProperType: - if node.fullname == "sqlalchemy.sql.sqltypes.Enum" and type_args: - first_arg = type_args[0] - if isinstance(first_arg, RefExpr) and isinstance( - first_arg.node, TypeInfo - ): - for base_ in first_arg.node.mro: - if base_.fullname == "enum.Enum": - return Instance(first_arg.node, []) - # TODO: support other pep-435 types here - else: - return api.named_type(names.NAMED_TYPE_BUILTINS_STR, []) - - assert node.has_base("sqlalchemy.sql.type_api.TypeEngine"), ( - "could not extract Python type from node: %s" % node - ) - - type_engine_sym = api.lookup_fully_qualified_or_none( - "sqlalchemy.sql.type_api.TypeEngine" - ) - - assert type_engine_sym is not None and isinstance( - type_engine_sym.node, TypeInfo - ) - type_engine = map_instance_to_supertype( - Instance(node, []), - type_engine_sym.node, - ) - return get_proper_type(type_engine.args[-1]) diff --git a/lib/sqlalchemy/ext/mypy/names.py b/lib/sqlalchemy/ext/mypy/names.py deleted file mode 100644 index 319786288f..0000000000 --- a/lib/sqlalchemy/ext/mypy/names.py +++ /dev/null @@ -1,335 +0,0 @@ -# ext/mypy/names.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -from __future__ import annotations - -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Union - -from mypy.nodes import ARG_POS -from mypy.nodes import CallExpr -from mypy.nodes import ClassDef -from mypy.nodes import Decorator -from mypy.nodes import Expression -from mypy.nodes import FuncDef -from mypy.nodes import MemberExpr -from mypy.nodes import NameExpr -from mypy.nodes import OverloadedFuncDef -from mypy.nodes import SymbolNode -from mypy.nodes import TypeAlias -from mypy.nodes import TypeInfo -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.types import CallableType -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import UnboundType - -from ... import util - -COLUMN: int = util.symbol("COLUMN") -RELATIONSHIP: int = util.symbol("RELATIONSHIP") -REGISTRY: int = util.symbol("REGISTRY") -COLUMN_PROPERTY: int = util.symbol("COLUMN_PROPERTY") -TYPEENGINE: int = util.symbol("TYPEENGNE") -MAPPED: int = util.symbol("MAPPED") -DECLARATIVE_BASE: int = util.symbol("DECLARATIVE_BASE") -DECLARATIVE_META: int = util.symbol("DECLARATIVE_META") -MAPPED_DECORATOR: int = util.symbol("MAPPED_DECORATOR") -SYNONYM_PROPERTY: int = util.symbol("SYNONYM_PROPERTY") -COMPOSITE_PROPERTY: int = util.symbol("COMPOSITE_PROPERTY") -DECLARED_ATTR: int = util.symbol("DECLARED_ATTR") -MAPPER_PROPERTY: int = util.symbol("MAPPER_PROPERTY") -AS_DECLARATIVE: int = util.symbol("AS_DECLARATIVE") -AS_DECLARATIVE_BASE: int = util.symbol("AS_DECLARATIVE_BASE") -DECLARATIVE_MIXIN: int = util.symbol("DECLARATIVE_MIXIN") -QUERY_EXPRESSION: int = util.symbol("QUERY_EXPRESSION") - -# names that must succeed with mypy.api.named_type -NAMED_TYPE_BUILTINS_OBJECT = "builtins.object" -NAMED_TYPE_BUILTINS_STR = "builtins.str" -NAMED_TYPE_BUILTINS_LIST = "builtins.list" -NAMED_TYPE_SQLA_MAPPED = "sqlalchemy.orm.base.Mapped" - -_RelFullNames = { - "sqlalchemy.orm.relationships.Relationship", - "sqlalchemy.orm.relationships.RelationshipProperty", - "sqlalchemy.orm.relationships._RelationshipDeclared", - "sqlalchemy.orm.Relationship", - "sqlalchemy.orm.RelationshipProperty", -} - -_lookup: Dict[str, Tuple[int, Set[str]]] = { - "Column": ( - COLUMN, - { - "sqlalchemy.sql.schema.Column", - "sqlalchemy.sql.Column", - }, - ), - "Relationship": (RELATIONSHIP, _RelFullNames), - "RelationshipProperty": (RELATIONSHIP, _RelFullNames), - "_RelationshipDeclared": (RELATIONSHIP, _RelFullNames), - "registry": ( - REGISTRY, - { - "sqlalchemy.orm.decl_api.registry", - "sqlalchemy.orm.registry", - }, - ), - "ColumnProperty": ( - COLUMN_PROPERTY, - { - "sqlalchemy.orm.properties.MappedSQLExpression", - "sqlalchemy.orm.MappedSQLExpression", - "sqlalchemy.orm.properties.ColumnProperty", - "sqlalchemy.orm.ColumnProperty", - }, - ), - "MappedSQLExpression": ( - COLUMN_PROPERTY, - { - "sqlalchemy.orm.properties.MappedSQLExpression", - "sqlalchemy.orm.MappedSQLExpression", - "sqlalchemy.orm.properties.ColumnProperty", - "sqlalchemy.orm.ColumnProperty", - }, - ), - "Synonym": ( - SYNONYM_PROPERTY, - { - "sqlalchemy.orm.descriptor_props.Synonym", - "sqlalchemy.orm.Synonym", - "sqlalchemy.orm.descriptor_props.SynonymProperty", - "sqlalchemy.orm.SynonymProperty", - }, - ), - "SynonymProperty": ( - SYNONYM_PROPERTY, - { - "sqlalchemy.orm.descriptor_props.Synonym", - "sqlalchemy.orm.Synonym", - "sqlalchemy.orm.descriptor_props.SynonymProperty", - "sqlalchemy.orm.SynonymProperty", - }, - ), - "Composite": ( - COMPOSITE_PROPERTY, - { - "sqlalchemy.orm.descriptor_props.Composite", - "sqlalchemy.orm.Composite", - "sqlalchemy.orm.descriptor_props.CompositeProperty", - "sqlalchemy.orm.CompositeProperty", - }, - ), - "CompositeProperty": ( - COMPOSITE_PROPERTY, - { - "sqlalchemy.orm.descriptor_props.Composite", - "sqlalchemy.orm.Composite", - "sqlalchemy.orm.descriptor_props.CompositeProperty", - "sqlalchemy.orm.CompositeProperty", - }, - ), - "MapperProperty": ( - MAPPER_PROPERTY, - { - "sqlalchemy.orm.interfaces.MapperProperty", - "sqlalchemy.orm.MapperProperty", - }, - ), - "TypeEngine": (TYPEENGINE, {"sqlalchemy.sql.type_api.TypeEngine"}), - "Mapped": (MAPPED, {NAMED_TYPE_SQLA_MAPPED}), - "declarative_base": ( - DECLARATIVE_BASE, - { - "sqlalchemy.ext.declarative.declarative_base", - "sqlalchemy.orm.declarative_base", - "sqlalchemy.orm.decl_api.declarative_base", - }, - ), - "DeclarativeMeta": ( - DECLARATIVE_META, - { - "sqlalchemy.ext.declarative.DeclarativeMeta", - "sqlalchemy.orm.DeclarativeMeta", - "sqlalchemy.orm.decl_api.DeclarativeMeta", - }, - ), - "mapped": ( - MAPPED_DECORATOR, - { - "sqlalchemy.orm.decl_api.registry.mapped", - "sqlalchemy.orm.registry.mapped", - }, - ), - "as_declarative": ( - AS_DECLARATIVE, - { - "sqlalchemy.ext.declarative.as_declarative", - "sqlalchemy.orm.decl_api.as_declarative", - "sqlalchemy.orm.as_declarative", - }, - ), - "as_declarative_base": ( - AS_DECLARATIVE_BASE, - { - "sqlalchemy.orm.decl_api.registry.as_declarative_base", - "sqlalchemy.orm.registry.as_declarative_base", - }, - ), - "declared_attr": ( - DECLARED_ATTR, - { - "sqlalchemy.orm.decl_api.declared_attr", - "sqlalchemy.orm.declared_attr", - }, - ), - "declarative_mixin": ( - DECLARATIVE_MIXIN, - { - "sqlalchemy.orm.decl_api.declarative_mixin", - "sqlalchemy.orm.declarative_mixin", - }, - ), - "query_expression": ( - QUERY_EXPRESSION, - { - "sqlalchemy.orm.query_expression", - "sqlalchemy.orm._orm_constructors.query_expression", - }, - ), -} - - -def has_base_type_id(info: TypeInfo, type_id: int) -> bool: - for mr in info.mro: - check_type_id, fullnames = _lookup.get(mr.name, (None, None)) - if check_type_id == type_id: - break - else: - return False - - if fullnames is None: - return False - - return mr.fullname in fullnames - - -def mro_has_id(mro: List[TypeInfo], type_id: int) -> bool: - for mr in mro: - check_type_id, fullnames = _lookup.get(mr.name, (None, None)) - if check_type_id == type_id: - break - else: - return False - - if fullnames is None: - return False - - return mr.fullname in fullnames - - -def type_id_for_unbound_type( - type_: UnboundType, cls: ClassDef, api: SemanticAnalyzerPluginInterface -) -> Optional[int]: - sym = api.lookup_qualified(type_.name, type_) - if sym is not None: - if isinstance(sym.node, TypeAlias): - target_type = get_proper_type(sym.node.target) - if isinstance(target_type, Instance): - return type_id_for_named_node(target_type.type) - elif isinstance(sym.node, TypeInfo): - return type_id_for_named_node(sym.node) - - return None - - -def type_id_for_callee(callee: Expression) -> Optional[int]: - if isinstance(callee, (MemberExpr, NameExpr)): - if isinstance(callee.node, Decorator) and isinstance( - callee.node.func, FuncDef - ): - if callee.node.func.type and isinstance( - callee.node.func.type, CallableType - ): - ret_type = get_proper_type(callee.node.func.type.ret_type) - - if isinstance(ret_type, Instance): - return type_id_for_fullname(ret_type.type.fullname) - - return None - - elif isinstance(callee.node, OverloadedFuncDef): - if ( - callee.node.impl - and callee.node.impl.type - and isinstance(callee.node.impl.type, CallableType) - ): - ret_type = get_proper_type(callee.node.impl.type.ret_type) - - if isinstance(ret_type, Instance): - return type_id_for_fullname(ret_type.type.fullname) - - return None - elif isinstance(callee.node, FuncDef): - if callee.node.type and isinstance(callee.node.type, CallableType): - ret_type = get_proper_type(callee.node.type.ret_type) - - if isinstance(ret_type, Instance): - return type_id_for_fullname(ret_type.type.fullname) - - return None - elif isinstance(callee.node, TypeAlias): - target_type = get_proper_type(callee.node.target) - if isinstance(target_type, Instance): - return type_id_for_fullname(target_type.type.fullname) - elif isinstance(callee.node, TypeInfo): - return type_id_for_named_node(callee) - return None - - -def type_id_for_named_node( - node: Union[NameExpr, MemberExpr, SymbolNode] -) -> Optional[int]: - type_id, fullnames = _lookup.get(node.name, (None, None)) - - if type_id is None or fullnames is None: - return None - elif node.fullname in fullnames: - return type_id - else: - return None - - -def type_id_for_fullname(fullname: str) -> Optional[int]: - tokens = fullname.split(".") - immediate = tokens[-1] - - type_id, fullnames = _lookup.get(immediate, (None, None)) - - if type_id is None or fullnames is None: - return None - elif fullname in fullnames: - return type_id - else: - return None - - -def expr_to_mapped_constructor(expr: Expression) -> CallExpr: - column_descriptor = NameExpr("__sa_Mapped") - column_descriptor.fullname = NAMED_TYPE_SQLA_MAPPED - member_expr = MemberExpr(column_descriptor, "_empty_constructor") - return CallExpr( - member_expr, - [expr], - [ARG_POS], - ["arg1"], - ) diff --git a/lib/sqlalchemy/ext/mypy/plugin.py b/lib/sqlalchemy/ext/mypy/plugin.py deleted file mode 100644 index 1ec2c02b9c..0000000000 --- a/lib/sqlalchemy/ext/mypy/plugin.py +++ /dev/null @@ -1,303 +0,0 @@ -# ext/mypy/plugin.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -""" -Mypy plugin for SQLAlchemy ORM. - -""" -from __future__ import annotations - -from typing import Callable -from typing import List -from typing import Optional -from typing import Tuple -from typing import Type as TypingType -from typing import Union - -from mypy import nodes -from mypy.mro import calculate_mro -from mypy.mro import MroError -from mypy.nodes import Block -from mypy.nodes import ClassDef -from mypy.nodes import GDEF -from mypy.nodes import MypyFile -from mypy.nodes import NameExpr -from mypy.nodes import SymbolTable -from mypy.nodes import SymbolTableNode -from mypy.nodes import TypeInfo -from mypy.plugin import AttributeContext -from mypy.plugin import ClassDefContext -from mypy.plugin import DynamicClassDefContext -from mypy.plugin import Plugin -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import Type - -from . import decl_class -from . import names -from . import util - -try: - __import__("sqlalchemy-stubs") -except ImportError: - pass -else: - raise ImportError( - "The SQLAlchemy mypy plugin in SQLAlchemy " - "2.0 does not work with sqlalchemy-stubs or " - "sqlalchemy2-stubs installed, as well as with any other third party " - "SQLAlchemy stubs. Please uninstall all SQLAlchemy stubs " - "packages." - ) - - -class SQLAlchemyPlugin(Plugin): - def get_dynamic_class_hook( - self, fullname: str - ) -> Optional[Callable[[DynamicClassDefContext], None]]: - if names.type_id_for_fullname(fullname) is names.DECLARATIVE_BASE: - return _dynamic_class_hook - return None - - def get_customize_class_mro_hook( - self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: - return _fill_in_decorators - - def get_class_decorator_hook( - self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: - sym = self.lookup_fully_qualified(fullname) - - if sym is not None and sym.node is not None: - type_id = names.type_id_for_named_node(sym.node) - if type_id is names.MAPPED_DECORATOR: - return _cls_decorator_hook - elif type_id in ( - names.AS_DECLARATIVE, - names.AS_DECLARATIVE_BASE, - ): - return _base_cls_decorator_hook - elif type_id is names.DECLARATIVE_MIXIN: - return _declarative_mixin_hook - - return None - - def get_metaclass_hook( - self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: - if names.type_id_for_fullname(fullname) is names.DECLARATIVE_META: - # Set any classes that explicitly have metaclass=DeclarativeMeta - # as declarative so the check in `get_base_class_hook()` works - return _metaclass_cls_hook - - return None - - def get_base_class_hook( - self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: - sym = self.lookup_fully_qualified(fullname) - - if ( - sym - and isinstance(sym.node, TypeInfo) - and util.has_declarative_base(sym.node) - ): - return _base_cls_hook - - return None - - def get_attribute_hook( - self, fullname: str - ) -> Optional[Callable[[AttributeContext], Type]]: - if fullname.startswith( - "sqlalchemy.orm.attributes.QueryableAttribute." - ): - return _queryable_getattr_hook - - return None - - def get_additional_deps( - self, file: MypyFile - ) -> List[Tuple[int, str, int]]: - return [ - # - (10, "sqlalchemy.orm", -1), - (10, "sqlalchemy.orm.attributes", -1), - (10, "sqlalchemy.orm.decl_api", -1), - ] - - -def plugin(version: str) -> TypingType[SQLAlchemyPlugin]: - return SQLAlchemyPlugin - - -def _dynamic_class_hook(ctx: DynamicClassDefContext) -> None: - """Generate a declarative Base class when the declarative_base() function - is encountered.""" - - _add_globals(ctx) - - cls = ClassDef(ctx.name, Block([])) - cls.fullname = ctx.api.qualified_name(ctx.name) - - info = TypeInfo(SymbolTable(), cls, ctx.api.cur_mod_id) - cls.info = info - _set_declarative_metaclass(ctx.api, cls) - - cls_arg = util.get_callexpr_kwarg(ctx.call, "cls", expr_types=(NameExpr,)) - if cls_arg is not None and isinstance(cls_arg.node, TypeInfo): - util.set_is_base(cls_arg.node) - decl_class.scan_declarative_assignments_and_apply_types( - cls_arg.node.defn, ctx.api, is_mixin_scan=True - ) - info.bases = [Instance(cls_arg.node, [])] - else: - obj = ctx.api.named_type(names.NAMED_TYPE_BUILTINS_OBJECT) - - info.bases = [obj] - - try: - calculate_mro(info) - except MroError: - util.fail( - ctx.api, "Not able to calculate MRO for declarative base", ctx.call - ) - obj = ctx.api.named_type(names.NAMED_TYPE_BUILTINS_OBJECT) - info.bases = [obj] - info.fallback_to_any = True - - ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info)) - util.set_is_base(info) - - -def _fill_in_decorators(ctx: ClassDefContext) -> None: - for decorator in ctx.cls.decorators: - # set the ".fullname" attribute of a class decorator - # that is a MemberExpr. This causes the logic in - # semanal.py->apply_class_plugin_hooks to invoke the - # get_class_decorator_hook for our "registry.map_class()" - # and "registry.as_declarative_base()" methods. - # this seems like a bug in mypy that these decorators are otherwise - # skipped. - - if ( - isinstance(decorator, nodes.CallExpr) - and isinstance(decorator.callee, nodes.MemberExpr) - and decorator.callee.name == "as_declarative_base" - ): - target = decorator.callee - elif ( - isinstance(decorator, nodes.MemberExpr) - and decorator.name == "mapped" - ): - target = decorator - else: - continue - - if isinstance(target.expr, NameExpr): - sym = ctx.api.lookup_qualified( - target.expr.name, target, suppress_errors=True - ) - else: - continue - - if sym and sym.node: - sym_type = get_proper_type(sym.type) - if isinstance(sym_type, Instance): - target.fullname = f"{sym_type.type.fullname}.{target.name}" - else: - # if the registry is in the same file as where the - # decorator is used, it might not have semantic - # symbols applied and we can't get a fully qualified - # name or an inferred type, so we are actually going to - # flag an error in this case that they need to annotate - # it. The "registry" is declared just - # once (or few times), so they have to just not use - # type inference for its assignment in this one case. - util.fail( - ctx.api, - "Class decorator called %s(), but we can't " - "tell if it's from an ORM registry. Please " - "annotate the registry assignment, e.g. " - "my_registry: registry = registry()" % target.name, - sym.node, - ) - - -def _cls_decorator_hook(ctx: ClassDefContext) -> None: - _add_globals(ctx) - assert isinstance(ctx.reason, nodes.MemberExpr) - expr = ctx.reason.expr - - assert isinstance(expr, nodes.RefExpr) and isinstance(expr.node, nodes.Var) - - node_type = get_proper_type(expr.node.type) - - assert ( - isinstance(node_type, Instance) - and names.type_id_for_named_node(node_type.type) is names.REGISTRY - ) - - decl_class.scan_declarative_assignments_and_apply_types(ctx.cls, ctx.api) - - -def _base_cls_decorator_hook(ctx: ClassDefContext) -> None: - _add_globals(ctx) - - cls = ctx.cls - - _set_declarative_metaclass(ctx.api, cls) - - util.set_is_base(ctx.cls.info) - decl_class.scan_declarative_assignments_and_apply_types( - cls, ctx.api, is_mixin_scan=True - ) - - -def _declarative_mixin_hook(ctx: ClassDefContext) -> None: - _add_globals(ctx) - util.set_is_base(ctx.cls.info) - decl_class.scan_declarative_assignments_and_apply_types( - ctx.cls, ctx.api, is_mixin_scan=True - ) - - -def _metaclass_cls_hook(ctx: ClassDefContext) -> None: - util.set_is_base(ctx.cls.info) - - -def _base_cls_hook(ctx: ClassDefContext) -> None: - _add_globals(ctx) - decl_class.scan_declarative_assignments_and_apply_types(ctx.cls, ctx.api) - - -def _queryable_getattr_hook(ctx: AttributeContext) -> Type: - # how do I....tell it it has no attribute of a certain name? - # can't find any Type that seems to match that - return ctx.default_attr_type - - -def _add_globals(ctx: Union[ClassDefContext, DynamicClassDefContext]) -> None: - """Add __sa_DeclarativeMeta and __sa_Mapped symbol to the global space - for all class defs - - """ - - util.add_global(ctx, "sqlalchemy.orm", "Mapped", "__sa_Mapped") - - -def _set_declarative_metaclass( - api: SemanticAnalyzerPluginInterface, target_cls: ClassDef -) -> None: - info = target_cls.info - sym = api.lookup_fully_qualified_or_none( - "sqlalchemy.orm.decl_api.DeclarativeMeta" - ) - assert sym is not None and isinstance(sym.node, TypeInfo) - info.declared_metaclass = info.metaclass_type = Instance(sym.node, []) diff --git a/lib/sqlalchemy/ext/mypy/util.py b/lib/sqlalchemy/ext/mypy/util.py deleted file mode 100644 index 16761b9ab3..0000000000 --- a/lib/sqlalchemy/ext/mypy/util.py +++ /dev/null @@ -1,357 +0,0 @@ -# ext/mypy/util.py -# Copyright (C) 2021-2025 the SQLAlchemy authors and contributors -# -# -# This module is part of SQLAlchemy and is released under -# the MIT License: https://www.opensource.org/licenses/mit-license.php - -from __future__ import annotations - -import re -from typing import Any -from typing import Iterable -from typing import Iterator -from typing import List -from typing import Optional -from typing import overload -from typing import Tuple -from typing import Type as TypingType -from typing import TypeVar -from typing import Union - -from mypy import version -from mypy.messages import format_type as _mypy_format_type -from mypy.nodes import CallExpr -from mypy.nodes import ClassDef -from mypy.nodes import CLASSDEF_NO_INFO -from mypy.nodes import Context -from mypy.nodes import Expression -from mypy.nodes import FuncDef -from mypy.nodes import IfStmt -from mypy.nodes import JsonDict -from mypy.nodes import MemberExpr -from mypy.nodes import NameExpr -from mypy.nodes import Statement -from mypy.nodes import SymbolTableNode -from mypy.nodes import TypeAlias -from mypy.nodes import TypeInfo -from mypy.options import Options -from mypy.plugin import ClassDefContext -from mypy.plugin import DynamicClassDefContext -from mypy.plugin import SemanticAnalyzerPluginInterface -from mypy.plugins.common import deserialize_and_fixup_type -from mypy.typeops import map_type_from_supertype -from mypy.types import CallableType -from mypy.types import get_proper_type -from mypy.types import Instance -from mypy.types import NoneType -from mypy.types import Type -from mypy.types import TypeVarType -from mypy.types import UnboundType -from mypy.types import UnionType - -_vers = tuple( - [int(x) for x in version.__version__.split(".") if re.match(r"^\d+$", x)] -) -mypy_14 = _vers >= (1, 4) - - -_TArgType = TypeVar("_TArgType", bound=Union[CallExpr, NameExpr]) - - -class SQLAlchemyAttribute: - def __init__( - self, - name: str, - line: int, - column: int, - typ: Optional[Type], - info: TypeInfo, - ) -> None: - self.name = name - self.line = line - self.column = column - self.type = typ - self.info = info - - def serialize(self) -> JsonDict: - assert self.type - return { - "name": self.name, - "line": self.line, - "column": self.column, - "type": serialize_type(self.type), - } - - def expand_typevar_from_subtype(self, sub_type: TypeInfo) -> None: - """Expands type vars in the context of a subtype when an attribute is - inherited from a generic super type. - """ - if not isinstance(self.type, TypeVarType): - return - - self.type = map_type_from_supertype(self.type, sub_type, self.info) - - @classmethod - def deserialize( - cls, - info: TypeInfo, - data: JsonDict, - api: SemanticAnalyzerPluginInterface, - ) -> SQLAlchemyAttribute: - data = data.copy() - typ = deserialize_and_fixup_type(data.pop("type"), api) - return cls(typ=typ, info=info, **data) - - -def name_is_dunder(name: str) -> bool: - return bool(re.match(r"^__.+?__$", name)) - - -def _set_info_metadata(info: TypeInfo, key: str, data: Any) -> None: - info.metadata.setdefault("sqlalchemy", {})[key] = data - - -def _get_info_metadata(info: TypeInfo, key: str) -> Optional[Any]: - return info.metadata.get("sqlalchemy", {}).get(key, None) - - -def _get_info_mro_metadata(info: TypeInfo, key: str) -> Optional[Any]: - if info.mro: - for base in info.mro: - metadata = _get_info_metadata(base, key) - if metadata is not None: - return metadata - return None - - -def establish_as_sqlalchemy(info: TypeInfo) -> None: - info.metadata.setdefault("sqlalchemy", {}) - - -def set_is_base(info: TypeInfo) -> None: - _set_info_metadata(info, "is_base", True) - - -def get_is_base(info: TypeInfo) -> bool: - is_base = _get_info_metadata(info, "is_base") - return is_base is True - - -def has_declarative_base(info: TypeInfo) -> bool: - is_base = _get_info_mro_metadata(info, "is_base") - return is_base is True - - -def set_has_table(info: TypeInfo) -> None: - _set_info_metadata(info, "has_table", True) - - -def get_has_table(info: TypeInfo) -> bool: - is_base = _get_info_metadata(info, "has_table") - return is_base is True - - -def get_mapped_attributes( - info: TypeInfo, api: SemanticAnalyzerPluginInterface -) -> Optional[List[SQLAlchemyAttribute]]: - mapped_attributes: Optional[List[JsonDict]] = _get_info_metadata( - info, "mapped_attributes" - ) - if mapped_attributes is None: - return None - - attributes: List[SQLAlchemyAttribute] = [] - - for data in mapped_attributes: - attr = SQLAlchemyAttribute.deserialize(info, data, api) - attr.expand_typevar_from_subtype(info) - attributes.append(attr) - - return attributes - - -def format_type(typ_: Type, options: Options) -> str: - if mypy_14: - return _mypy_format_type(typ_, options) - else: - return _mypy_format_type(typ_) # type: ignore - - -def set_mapped_attributes( - info: TypeInfo, attributes: List[SQLAlchemyAttribute] -) -> None: - _set_info_metadata( - info, - "mapped_attributes", - [attribute.serialize() for attribute in attributes], - ) - - -def fail(api: SemanticAnalyzerPluginInterface, msg: str, ctx: Context) -> None: - msg = "[SQLAlchemy Mypy plugin] %s" % msg - return api.fail(msg, ctx) - - -def add_global( - ctx: Union[ClassDefContext, DynamicClassDefContext], - module: str, - symbol_name: str, - asname: str, -) -> None: - module_globals = ctx.api.modules[ctx.api.cur_mod_id].names - - if asname not in module_globals: - lookup_sym: SymbolTableNode = ctx.api.modules[module].names[ - symbol_name - ] - - module_globals[asname] = lookup_sym - - -@overload -def get_callexpr_kwarg( - callexpr: CallExpr, name: str, *, expr_types: None = ... -) -> Optional[Union[CallExpr, NameExpr]]: ... - - -@overload -def get_callexpr_kwarg( - callexpr: CallExpr, - name: str, - *, - expr_types: Tuple[TypingType[_TArgType], ...], -) -> Optional[_TArgType]: ... - - -def get_callexpr_kwarg( - callexpr: CallExpr, - name: str, - *, - expr_types: Optional[Tuple[TypingType[Any], ...]] = None, -) -> Optional[Any]: - try: - arg_idx = callexpr.arg_names.index(name) - except ValueError: - return None - - kwarg = callexpr.args[arg_idx] - if isinstance( - kwarg, expr_types if expr_types is not None else (NameExpr, CallExpr) - ): - return kwarg - - return None - - -def flatten_typechecking(stmts: Iterable[Statement]) -> Iterator[Statement]: - for stmt in stmts: - if ( - isinstance(stmt, IfStmt) - and isinstance(stmt.expr[0], NameExpr) - and stmt.expr[0].fullname == "typing.TYPE_CHECKING" - ): - yield from stmt.body[0].body - else: - yield stmt - - -def type_for_callee(callee: Expression) -> Optional[Union[Instance, TypeInfo]]: - if isinstance(callee, (MemberExpr, NameExpr)): - if isinstance(callee.node, FuncDef): - if callee.node.type and isinstance(callee.node.type, CallableType): - ret_type = get_proper_type(callee.node.type.ret_type) - - if isinstance(ret_type, Instance): - return ret_type - - return None - elif isinstance(callee.node, TypeAlias): - target_type = get_proper_type(callee.node.target) - if isinstance(target_type, Instance): - return target_type - elif isinstance(callee.node, TypeInfo): - return callee.node - return None - - -def unbound_to_instance( - api: SemanticAnalyzerPluginInterface, typ: Type -) -> Type: - """Take the UnboundType that we seem to get as the ret_type from a FuncDef - and convert it into an Instance/TypeInfo kind of structure that seems - to work as the left-hand type of an AssignmentStatement. - - """ - - if not isinstance(typ, UnboundType): - return typ - - # TODO: figure out a more robust way to check this. The node is some - # kind of _SpecialForm, there's a typing.Optional that's _SpecialForm, - # but I can't figure out how to get them to match up - if typ.name == "Optional": - # convert from "Optional?" to the more familiar - # UnionType[..., NoneType()] - return unbound_to_instance( - api, - UnionType( - [unbound_to_instance(api, typ_arg) for typ_arg in typ.args] - + [NoneType()] - ), - ) - - node = api.lookup_qualified(typ.name, typ) - - if ( - node is not None - and isinstance(node, SymbolTableNode) - and isinstance(node.node, TypeInfo) - ): - bound_type = node.node - - return Instance( - bound_type, - [ - ( - unbound_to_instance(api, arg) - if isinstance(arg, UnboundType) - else arg - ) - for arg in typ.args - ], - ) - else: - return typ - - -def info_for_cls( - cls: ClassDef, api: SemanticAnalyzerPluginInterface -) -> Optional[TypeInfo]: - if cls.info is CLASSDEF_NO_INFO: - sym = api.lookup_qualified(cls.name, cls) - if sym is None: - return None - assert sym and isinstance(sym.node, TypeInfo) - return sym.node - - return cls.info - - -def serialize_type(typ: Type) -> Union[str, JsonDict]: - try: - return typ.serialize() - except Exception: - pass - if hasattr(typ, "args"): - typ.args = tuple( - ( - a.resolve_string_annotation() - if hasattr(a, "resolve_string_annotation") - else a - ) - for a in typ.args - ) - elif hasattr(typ, "resolve_string_annotation"): - typ = typ.resolve_string_annotation() - return typ.serialize() diff --git a/lib/sqlalchemy/testing/fixtures/mypy.py b/lib/sqlalchemy/testing/fixtures/mypy.py index 7718b2bd8f..3a1ae2e9bd 100644 --- a/lib/sqlalchemy/testing/fixtures/mypy.py +++ b/lib/sqlalchemy/testing/fixtures/mypy.py @@ -21,6 +21,17 @@ from .. import config from ..assertions import eq_ from ... import util +try: + from mypy import version + + _mypy_vers_tuple = tuple( + int(x) for x in version.__version__.split(".") if x.isdecimal() + ) +except ImportError: + _mypy_vers_tuple = (0, 0, 0) + +mypy_14 = _mypy_vers_tuple >= (1, 4) + @config.add_to_marker.mypy class MypyTest(TestBase): @@ -39,22 +50,6 @@ class MypyTest(TestBase): mypy_path = "" with tempfile.TemporaryDirectory() as cachedir: - with open( - Path(cachedir) / "sqla_mypy_config.cfg", "w" - ) as config_file: - config_file.write( - f""" - [mypy]\n - plugins = sqlalchemy.ext.mypy.plugin\n - show_error_codes = True\n - {mypy_path} - disable_error_code = no-untyped-call - - [mypy-sqlalchemy.*] - ignore_errors = True - - """ - ) with open( Path(cachedir) / "plain_mypy_config.cfg", "w" ) as config_file: @@ -75,7 +70,7 @@ class MypyTest(TestBase): def mypy_runner(self, cachedir): from mypy import api - def run(path, use_plugin=False, use_cachedir=None): + def run(path, use_cachedir=None): if use_cachedir is None: use_cachedir = cachedir args = [ @@ -84,14 +79,7 @@ class MypyTest(TestBase): "--cache-dir", use_cachedir, "--config-file", - os.path.join( - use_cachedir, - ( - "sqla_mypy_config.cfg" - if use_plugin - else "plain_mypy_config.cfg" - ), - ), + os.path.join(use_cachedir, "plain_mypy_config.cfg"), ] # mypy as of 0.990 is more aggressively blocking messaging @@ -116,9 +104,9 @@ class MypyTest(TestBase): @config.fixture def mypy_typecheck_file(self, mypy_runner): - def run(path, use_plugin=False): + def run(path): expected_messages = self._collect_messages(path) - stdout, stderr, exitcode = mypy_runner(path, use_plugin=use_plugin) + stdout, stderr, exitcode = mypy_runner(path) self._check_output( path, expected_messages, stdout, stderr, exitcode ) @@ -140,8 +128,6 @@ class MypyTest(TestBase): return files def _collect_messages(self, path): - from sqlalchemy.ext.mypy.util import mypy_14 - expected_messages = [] expected_re = re.compile(r"\s*# EXPECTED(_MYPY)?(_RE)?(_TYPE)?: (.+)") py_ver_re = re.compile(r"^#\s*PYTHON_VERSION\s?>=\s?(\d+\.\d+)") diff --git a/pyproject.toml b/pyproject.toml index 7e6b12b37a..ade402dd6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,10 +126,7 @@ black-line-length = 79 [tool.slotscheck] exclude-modules = ''' -^sqlalchemy\.( - testing - |ext\.mypy # see slotscheck/issues/178 -) +^sqlalchemy\.testing ''' diff --git a/setup.cfg b/setup.cfg index bbb765c0aa..76e1f1825b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,6 @@ per-file-ignores = **/__init__.py:F401 test/*:FA100 test/typing/plain_files/*:F821,E501,FA100 - test/ext/mypy/plugin_files/*:F821,E501,FA100 lib/sqlalchemy/events.py:F401 lib/sqlalchemy/schema.py:F401 lib/sqlalchemy/types.py:F401 diff --git a/test/ext/mypy/incremental/stubs_14/__init__.py b/test/ext/mypy/incremental/stubs_14/__init__.py deleted file mode 100644 index 31696458ee..0000000000 --- a/test/ext/mypy/incremental/stubs_14/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import TYPE_CHECKING - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import as_declarative -from sqlalchemy.orm import declared_attr -from sqlalchemy.orm import Mapped -from .address import Address -from .user import User - -if TYPE_CHECKING: - from sqlalchemy.orm.decl_api import DeclarativeMeta - - -@as_declarative() -class Base: - @declared_attr - def __tablename__(self) -> Mapped[str]: - return self.__name__.lower() - - id = Column(Integer, primary_key=True) - - -__all__ = ["User", "Address"] diff --git a/test/ext/mypy/incremental/stubs_14/address.py b/test/ext/mypy/incremental/stubs_14/address.py deleted file mode 100644 index 061dbc6daf..0000000000 --- a/test/ext/mypy/incremental/stubs_14/address.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import TYPE_CHECKING - -from . import Base -from .user import HasUser - -if TYPE_CHECKING: - from sqlalchemy import Column # noqa - from sqlalchemy import Integer # noqa - from sqlalchemy.orm import RelationshipProperty # noqa - from .user import User # noqa - - -class Address(Base, HasUser): - pass diff --git a/test/ext/mypy/incremental/stubs_14/patch1.testpatch b/test/ext/mypy/incremental/stubs_14/patch1.testpatch deleted file mode 100644 index 528236a00e..0000000000 --- a/test/ext/mypy/incremental/stubs_14/patch1.testpatch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/test/ext/mypy/incremental/stubs_14/user.py b/test/ext/mypy/incremental/stubs_14/user.py -index 2c60403e4..c7e8f8874 100644 ---- a/user.py -+++ b/user.py -@@ -18,6 +18,8 @@ if TYPE_CHECKING: - class User(Base): - name = Column(String) - -+ othername = Column(String) -+ - addresses: Mapped[List["Address"]] = relationship( - "Address", back_populates="user" - ) diff --git a/test/ext/mypy/incremental/stubs_14/user.py b/test/ext/mypy/incremental/stubs_14/user.py deleted file mode 100644 index c7e8f88747..0000000000 --- a/test/ext/mypy/incremental/stubs_14/user.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import List -from typing import TYPE_CHECKING - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import relationship -from sqlalchemy.orm.decl_api import declared_attr -from sqlalchemy.orm.relationships import RelationshipProperty -from . import Base - -if TYPE_CHECKING: - from .address import Address - - -class User(Base): - name = Column(String) - - othername = Column(String) - - addresses: Mapped[List["Address"]] = relationship( - "Address", back_populates="user" - ) - - -class HasUser: - @declared_attr - def user_id(self) -> "Column[Integer]": - return Column( - Integer, - ForeignKey(User.id, ondelete="CASCADE", onupdate="CASCADE"), - nullable=False, - ) - - @declared_attr - def user(self) -> RelationshipProperty[User]: - return relationship(User) diff --git a/test/ext/mypy/incremental/ticket_6147/__init__.py b/test/ext/mypy/incremental/ticket_6147/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/ext/mypy/incremental/ticket_6147/base.py b/test/ext/mypy/incremental/ticket_6147/base.py deleted file mode 100644 index 59be70308c..0000000000 --- a/test/ext/mypy/incremental/ticket_6147/base.py +++ /dev/null @@ -1,3 +0,0 @@ -from sqlalchemy.orm import declarative_base - -Base = declarative_base() diff --git a/test/ext/mypy/incremental/ticket_6147/one.py b/test/ext/mypy/incremental/ticket_6147/one.py deleted file mode 100644 index 17fb075ac4..0000000000 --- a/test/ext/mypy/incremental/ticket_6147/one.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from .base import Base - - -class One(Base): - __tablename__ = "one" - id = Column(Integer, primary_key=True) - - -o1 = One(id=5) - -One.id.in_([1, 2]) diff --git a/test/ext/mypy/incremental/ticket_6147/patch1.testpatch b/test/ext/mypy/incremental/ticket_6147/patch1.testpatch deleted file mode 100644 index b1d9bde011..0000000000 --- a/test/ext/mypy/incremental/ticket_6147/patch1.testpatch +++ /dev/null @@ -1,19 +0,0 @@ ---- a/one.py 2021-04-03 15:32:22.214287290 -0400 -+++ b/one.py 2021-04-03 15:34:56.397398510 -0400 -@@ -1,15 +1,13 @@ - from sqlalchemy import Column - from sqlalchemy import Integer --from sqlalchemy import String - from .base import Base - - - class One(Base): - __tablename__ = "one" - id = Column(Integer, primary_key=True) -- name = Column(String(50)) - - --o1 = One(id=5, name="name") -+o1 = One(id=5) - - One.id.in_([1, 2]) diff --git a/test/ext/mypy/incremental/ticket_6147/patch2.testpatch b/test/ext/mypy/incremental/ticket_6147/patch2.testpatch deleted file mode 100644 index 7551659571..0000000000 --- a/test/ext/mypy/incremental/ticket_6147/patch2.testpatch +++ /dev/null @@ -1,38 +0,0 @@ ---- a/base.py 2021-04-03 16:36:30.201594994 -0400 -+++ b/base.py 2021-04-03 16:38:26.404475025 -0400 -@@ -1,3 +1,15 @@ -+from sqlalchemy import Column -+from sqlalchemy import Integer -+from sqlalchemy import String - from sqlalchemy.orm import declarative_base -+from sqlalchemy.orm import declarative_mixin -+from sqlalchemy.orm import Mapped - - Base = declarative_base() -+ -+ -+@declarative_mixin -+class Mixin: -+ mixed = Column(String) -+ -+ b_int: Mapped[int] = Column(Integer) ---- a/one.py 2021-04-03 16:37:17.906956282 -0400 -+++ b/one.py 2021-04-03 16:38:33.469528528 -0400 -@@ -1,13 +1,15 @@ - from sqlalchemy import Column - from sqlalchemy import Integer -+ - from .base import Base -+from .base import Mixin - - --class One(Base): -+class One(Mixin, Base): - __tablename__ = "one" - id = Column(Integer, primary_key=True) - - --o1 = One(id=5) -+o1 = One(id=5, mixed="mixed", b_int=5) - - One.id.in_([1, 2]) diff --git a/test/ext/mypy/incremental/ticket_6435/__init__.py b/test/ext/mypy/incremental/ticket_6435/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/ext/mypy/incremental/ticket_6435/enum_col_import1.py b/test/ext/mypy/incremental/ticket_6435/enum_col_import1.py deleted file mode 100644 index fbdbb4fbff..0000000000 --- a/test/ext/mypy/incremental/ticket_6435/enum_col_import1.py +++ /dev/null @@ -1,11 +0,0 @@ -import enum - - -class StrEnum(enum.Enum): - one = "one" - two = "two" - - -class IntEnum(enum.Enum): - one = 1 - two = 2 diff --git a/test/ext/mypy/incremental/ticket_6435/enum_col_import2.py b/test/ext/mypy/incremental/ticket_6435/enum_col_import2.py deleted file mode 100644 index 161dce0875..0000000000 --- a/test/ext/mypy/incremental/ticket_6435/enum_col_import2.py +++ /dev/null @@ -1,29 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Enum -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Mapped -from . import enum_col_import1 -from .enum_col_import1 import IntEnum -from .enum_col_import1 import StrEnum - -Base = declarative_base() - - -class TestEnum(Base): - __tablename__ = "test_enum" - - e1: Mapped[StrEnum] = Column(Enum(StrEnum)) - e2: StrEnum = Column(Enum(StrEnum)) - - e3: Mapped[IntEnum] = Column(Enum(IntEnum)) - e4: IntEnum = Column(Enum(IntEnum)) - - e5: Mapped[enum_col_import1.StrEnum] = Column( - Enum(enum_col_import1.StrEnum) - ) - e6: enum_col_import1.StrEnum = Column(Enum(enum_col_import1.StrEnum)) - - e7: Mapped[enum_col_import1.IntEnum] = Column( - Enum(enum_col_import1.IntEnum) - ) - e8: enum_col_import1.IntEnum = Column(Enum(enum_col_import1.IntEnum)) diff --git a/test/ext/mypy/incremental/ticket_6476/__init__.py b/test/ext/mypy/incremental/ticket_6476/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/ext/mypy/incremental/ticket_6476/base.py b/test/ext/mypy/incremental/ticket_6476/base.py deleted file mode 100644 index fc14e4719c..0000000000 --- a/test/ext/mypy/incremental/ticket_6476/base.py +++ /dev/null @@ -1,8 +0,0 @@ -from sqlalchemy.ext.declarative import declarative_base - - -class CustomBase: - x = 5 - - -sql_base = declarative_base(cls=CustomBase) diff --git a/test/ext/mypy/incremental/ticket_6476/patch1.testpatch b/test/ext/mypy/incremental/ticket_6476/patch1.testpatch deleted file mode 100644 index ee4ec68350..0000000000 --- a/test/ext/mypy/incremental/ticket_6476/patch1.testpatch +++ /dev/null @@ -1,8 +0,0 @@ ---- a/table.py 2021-06-21 15:52:47.733131711 -0400 -+++ b/table.py 2021-06-21 16:32:25.437594701 -0400 -@@ -3,3 +3,5 @@ - - class Table(sql_base): - pass -+ -+x = Table.x diff --git a/test/ext/mypy/incremental/ticket_6476/table.py b/test/ext/mypy/incremental/ticket_6476/table.py deleted file mode 100644 index bf971dba67..0000000000 --- a/test/ext/mypy/incremental/ticket_6476/table.py +++ /dev/null @@ -1,5 +0,0 @@ -from .base import sql_base - - -class Table(sql_base): - pass diff --git a/test/ext/mypy/plugin_files/abstract_one.py b/test/ext/mypy/plugin_files/abstract_one.py deleted file mode 100644 index d11631d75b..0000000000 --- a/test/ext/mypy/plugin_files/abstract_one.py +++ /dev/null @@ -1,28 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base - - -Base = declarative_base() - - -class FooBase(Base): - __abstract__ = True - - updated_at = Column(Integer) - - -class Foo(FooBase): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -Foo.updated_at.in_([1, 2, 3]) - -f1 = Foo(name="name", updated_at=5) - -# test that we read the __abstract__ flag and don't apply a constructor -# EXPECTED_MYPY: Unexpected keyword argument "updated_at" for "FooBase" -FooBase(updated_at=5) diff --git a/test/ext/mypy/plugin_files/as_declarative.py b/test/ext/mypy/plugin_files/as_declarative.py deleted file mode 100644 index 08f08f913c..0000000000 --- a/test/ext/mypy/plugin_files/as_declarative.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import as_declarative -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import relationship -from sqlalchemy.sql.schema import ForeignKey - - -@as_declarative() -class Base: - updated_at = Column(Integer) - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: Mapped[str] = Column(String) - - bar: List["Bar"] = relationship("Bar") - - -class Bar(Base): - __tablename__ = "bar" - id: int = Column(Integer(), primary_key=True) - foo_id: int = Column(ForeignKey("foo.id")) - - foo: Optional[Foo] = relationship(Foo) - - -f1 = Foo() - -val: int = f1.id - -p: str = f1.name - -Foo.id.property - -f2 = Foo(name="some name", updated_at=5) diff --git a/test/ext/mypy/plugin_files/as_declarative_base.py b/test/ext/mypy/plugin_files/as_declarative_base.py deleted file mode 100644 index ba62e7276c..0000000000 --- a/test/ext/mypy/plugin_files/as_declarative_base.py +++ /dev/null @@ -1,28 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.as_declarative_base() -class Base: - updated_at = Column(Integer) - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -f1 = Foo() - -val: int = f1.id - -p: str = f1.name - -Foo.id.property - -f2 = Foo(name="some name", updated_at=5) diff --git a/test/ext/mypy/plugin_files/boolean_col.py b/test/ext/mypy/plugin_files/boolean_col.py deleted file mode 100644 index 3e361ad102..0000000000 --- a/test/ext/mypy/plugin_files/boolean_col.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional - -from sqlalchemy import Boolean -from sqlalchemy import Column -from sqlalchemy.orm import declarative_base - -Base = declarative_base() - - -class TestBoolean(Base): - __tablename__ = "test_boolean" - - flag = Column(Boolean) - - bflag: bool = Column(Boolean(create_constraint=True)) - - -expr = TestBoolean.flag.is_(True) - -t1 = TestBoolean(flag=True) - -x: Optional[bool] = t1.flag - -y: bool = t1.bflag diff --git a/test/ext/mypy/plugin_files/cols_noninferred_plain_nonopt.py b/test/ext/mypy/plugin_files/cols_noninferred_plain_nonopt.py deleted file mode 100644 index a2825e0037..0000000000 --- a/test/ext/mypy/plugin_files/cols_noninferred_plain_nonopt.py +++ /dev/null @@ -1,36 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - - -reg: registry = registry() - - -@reg.mapped -class Foo: - id: int = Column(Integer()) - name: str = Column(String) - other_name: str = Column(String(50)) - - # has a string key in it - third_name = Column("foo", String(50)) - - some_name = "fourth_name" - - fourth_name = Column(some_name, String(50)) - - -f1 = Foo() - -# This needs to work, e.g., value is "int" at the instance level -val: int = f1.id # noqa - -# also, the type are not optional, since we used an explicit -# type without Optional -p: str = f1.name - -Foo.id.property - - -Foo(name="n", other_name="on", third_name="tn", fourth_name="fn") diff --git a/test/ext/mypy/plugin_files/cols_notype_on_fk_col.py b/test/ext/mypy/plugin_files/cols_notype_on_fk_col.py deleted file mode 100644 index 3195714ae7..0000000000 --- a/test/ext/mypy/plugin_files/cols_notype_on_fk_col.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - name = Column(String) - - -@reg.mapped -class Address: - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: Mapped[int] = Column(ForeignKey("user.id")) - email_address = Column(String) - - -ad1 = Address() - -p: Optional[int] = ad1.user_id - -# it's not optional because we called it Mapped[int] -# and not Mapped[Optional[int]] -p2: int = ad1.user_id - - -# class-level descriptor access -User.name.in_(["x", "y"]) - - -# class-level descriptor access -Address.user_id.in_([1, 2]) diff --git a/test/ext/mypy/plugin_files/composite_props.py b/test/ext/mypy/plugin_files/composite_props.py deleted file mode 100644 index d717ca0489..0000000000 --- a/test/ext/mypy/plugin_files/composite_props.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Any -from typing import Tuple - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import composite - -Base = declarative_base() - - -class Point: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - - def __composite_values__(self) -> Tuple[int, int]: - return self.x, self.y - - def __repr__(self) -> str: - return "Point(x=%r, y=%r)" % (self.x, self.y) - - def __eq__(self, other: Any) -> bool: - return ( - isinstance(other, Point) - and other.x == self.x - and other.y == self.y - ) - - def __ne__(self, other: Any) -> bool: - return not self.__eq__(other) - - -class Vertex(Base): - __tablename__ = "vertices" - - id = Column(Integer, primary_key=True) - x1 = Column(Integer) - y1 = Column(Integer) - x2 = Column(Integer) - y2 = Column(Integer) - - # inferred from right hand side - start = composite(Point, x1, y1) - - # taken from left hand side - end: Point = composite(Point, x2, y2) - - -v1 = Vertex(start=Point(3, 4), end=Point(5, 6)) - -# I'm not even sure composites support this but it should work from a -# typing perspective -stmt = select(Vertex).where(Vertex.start.in_([Point(3, 4)])) - -p1: Point = v1.start -p2: Point = v1.end - -y3: int = v1.end.y diff --git a/test/ext/mypy/plugin_files/constr_cols_only.py b/test/ext/mypy/plugin_files/constr_cols_only.py deleted file mode 100644 index cd4da5586b..0000000000 --- a/test/ext/mypy/plugin_files/constr_cols_only.py +++ /dev/null @@ -1,29 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - data = Column(String) - x = Column(Integer) - y = Column(Integer) - - -a1 = A(data="d", x=5, y=4) - - -# EXPECTED_MYPY: Argument "data" to "A" has incompatible type "int"; expected "Optional[str]" # noqa -a2 = A(data=5) - -# EXPECTED_MYPY: Unexpected keyword argument "nonexistent" for "A" -a3 = A(nonexistent="hi") - -print(a1) -print(a2) -print(a3) diff --git a/test/ext/mypy/plugin_files/dataclasses_workaround.py b/test/ext/mypy/plugin_files/dataclasses_workaround.py deleted file mode 100644 index a4d1389993..0000000000 --- a/test/ext/mypy/plugin_files/dataclasses_workaround.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from dataclasses import field -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import TYPE_CHECKING - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy import String -from sqlalchemy import Table -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship - -mapper_registry: registry = registry() - - -@mapper_registry.mapped -@dataclass -class User: - __table__ = Table( - "user", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("fullname", String(50)), - Column("nickname", String(12)), - ) - id: int = field(init=False) - name: Optional[str] = None - fullname: Optional[str] = None - nickname: Optional[str] = None - addresses: List[Address] = field(default_factory=list) - - if TYPE_CHECKING: - _mypy_mapped_attrs = [id, name, fullname, nickname, addresses] - - __mapper_args__: Dict[str, Any] = { - "properties": {"addresses": relationship("Address")} - } - - -@mapper_registry.mapped -@dataclass -class Address: - __table__ = Table( - "address", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("user.id")), - Column("email_address", String(50)), - ) - - id: int = field(init=False) - user_id: int = field(init=False) - email_address: Optional[str] = None - - if TYPE_CHECKING: - _mypy_mapped_attrs = [id, user_id, email_address] - - -stmt1 = select(User.name).where(User.id.in_([1, 2, 3])) -stmt2 = select(Address).where(Address.email_address.contains(["foo"])) diff --git a/test/ext/mypy/plugin_files/decl_attrs_one.py b/test/ext/mypy/plugin_files/decl_attrs_one.py deleted file mode 100644 index 1f2261cfcc..0000000000 --- a/test/ext/mypy/plugin_files/decl_attrs_one.py +++ /dev/null @@ -1,37 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry -from sqlalchemy.sql.schema import ForeignKey -from sqlalchemy.sql.schema import MetaData -from sqlalchemy.sql.schema import Table - - -reg: registry = registry() - - -@reg.mapped -class Foo: - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -@reg.mapped -class Bar(Foo): - __tablename__ = "bar" - id: int = Column(ForeignKey("foo.id"), primary_key=True) - - -@reg.mapped -class Bat(Foo): - pass - - -m1: MetaData = reg.metadata - -t1: Table = Foo.__table__ - -t2: Table = Bar.__table__ - -t3: Table = Bat.__table__ diff --git a/test/ext/mypy/plugin_files/decl_attrs_two.py b/test/ext/mypy/plugin_files/decl_attrs_two.py deleted file mode 100644 index a20af490dc..0000000000 --- a/test/ext/mypy/plugin_files/decl_attrs_two.py +++ /dev/null @@ -1,39 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import registry -from sqlalchemy.sql.schema import ForeignKey -from sqlalchemy.sql.schema import MetaData -from sqlalchemy.sql.schema import Table - - -Base = declarative_base() - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -class Bar(Foo): - __tablename__ = "bar" - id: int = Column(ForeignKey("foo.id"), primary_key=True) - - -class Bat(Foo): - pass - - -m0: MetaData = Base.metadata -r0: registry = Base.registry - -t1: Table = Foo.__table__ -m1: MetaData = Foo.metadata - -t2: Table = Bar.__table__ -m2: MetaData = Bar.metadata - -t3: Table = Bat.__table__ -m3: MetaData = Bat.metadata diff --git a/test/ext/mypy/plugin_files/decl_base_subclass_one.py b/test/ext/mypy/plugin_files/decl_base_subclass_one.py deleted file mode 100644 index abe28a4956..0000000000 --- a/test/ext/mypy/plugin_files/decl_base_subclass_one.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base - - -class _Base: - updated_at = Column(Integer) - - -Base = declarative_base(cls=_Base) - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -class Bar(Base): - __tablename__ = "bar" - id = Column(Integer(), primary_key=True) - num = Column(Integer) - - -Foo.updated_at.in_([1, 2, 3]) - -f1 = Foo(name="name", updated_at=5) - -b1 = Bar(num=5, updated_at=6) diff --git a/test/ext/mypy/plugin_files/decl_base_subclass_two.py b/test/ext/mypy/plugin_files/decl_base_subclass_two.py deleted file mode 100644 index 78b7a9b633..0000000000 --- a/test/ext/mypy/plugin_files/decl_base_subclass_two.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship -from sqlalchemy.orm.decl_api import declared_attr -from sqlalchemy.sql.schema import ForeignKey -from sqlalchemy.sql.sqltypes import Integer -from sqlalchemy.sql.sqltypes import String - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String(50)) - - name3 = Column(String(50)) - - addresses: List["Address"] = relationship("Address") - - -@reg.mapped -class SubUser(User): - __tablename__ = "subuser" - - id: int = Column(ForeignKey("user.id"), primary_key=True) - - @declared_attr - def name(cls) -> Column[String]: - return Column(String(50)) - - @declared_attr - def name2(cls) -> Mapped[Optional[str]]: - return Column(String(50)) - - @declared_attr - def name3(cls) -> Mapped[str]: - return Column(String(50)) - - subname = Column(String) - - -@reg.mapped -class Address: - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - email = Column(String(50)) - - user = relationship(User, uselist=False) - - -s1 = SubUser() - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str" # noqa -x1: str = s1.name - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") # noqa -x2: str = s1.name2 - -x3: str = s1.name3 - -u1 = User() - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") # noqa -x4: str = u1.name3 diff --git a/test/ext/mypy/plugin_files/declarative_base_dynamic.py b/test/ext/mypy/plugin_files/declarative_base_dynamic.py deleted file mode 100644 index eee9b31104..0000000000 --- a/test/ext/mypy/plugin_files/declarative_base_dynamic.py +++ /dev/null @@ -1,31 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base - -# this is actually in orm now - - -Base = declarative_base() - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - other_name: str = Column(String(50)) - - -f1 = Foo() - -val: int = f1.id - -p: str = f1.name - -Foo.id.property - -# TODO: getitem checker? this should raise -Foo.id.property_nonexistent - - -f2 = Foo(name="some name", other_name="some other name") diff --git a/test/ext/mypy/plugin_files/declarative_base_explicit.py b/test/ext/mypy/plugin_files/declarative_base_explicit.py deleted file mode 100644 index b1b02bfb85..0000000000 --- a/test/ext/mypy/plugin_files/declarative_base_explicit.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry -from sqlalchemy.orm.decl_api import DeclarativeMeta - - -class Base(metaclass=DeclarativeMeta): - __abstract__ = True - registry = registry() - metadata = registry.metadata - - -class Foo(Base): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - other_name: str = Column(String(50)) - - -f1 = Foo() - -val: int = f1.id - -p: str = f1.name - -Foo.id.property - -# TODO: getitem checker? this should raise -Foo.id.property_nonexistent diff --git a/test/ext/mypy/plugin_files/ensure_descriptor_type_fully_inferred.py b/test/ext/mypy/plugin_files/ensure_descriptor_type_fully_inferred.py deleted file mode 100644 index 9ee9c76f46..0000000000 --- a/test/ext/mypy/plugin_files/ensure_descriptor_type_fully_inferred.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - name = Column(String, nullable=False) - - -u1 = User() - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") # noqa: E501 -p: str = u1.name diff --git a/test/ext/mypy/plugin_files/ensure_descriptor_type_noninferred.py b/test/ext/mypy/plugin_files/ensure_descriptor_type_noninferred.py deleted file mode 100644 index e8ce35114e..0000000000 --- a/test/ext/mypy/plugin_files/ensure_descriptor_type_noninferred.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - name: Mapped[Optional[str]] = Column(String) - - -u1 = User() - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "Optional[int]") # noqa: E501 -p: Optional[int] = u1.name diff --git a/test/ext/mypy/plugin_files/ensure_descriptor_type_semiinferred.py b/test/ext/mypy/plugin_files/ensure_descriptor_type_semiinferred.py deleted file mode 100644 index d72649b62a..0000000000 --- a/test/ext/mypy/plugin_files/ensure_descriptor_type_semiinferred.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - - # we will call this "semi-inferred", since the real - # type will be Mapped[Optional[str]], but the Optional[str] - # which is not inferred, we use that to create it - name: Optional[str] = Column(String) - - -u1 = User() - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") # noqa: E501 -p: str = u1.name diff --git a/test/ext/mypy/plugin_files/enum_col.py b/test/ext/mypy/plugin_files/enum_col.py deleted file mode 100644 index cfea38803e..0000000000 --- a/test/ext/mypy/plugin_files/enum_col.py +++ /dev/null @@ -1,40 +0,0 @@ -import enum -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Enum -from sqlalchemy.orm import declarative_base - - -class MyEnum(enum.Enum): - one = 1 - two = 2 - three = 3 - - -Base = declarative_base() - -one, two, three = "one", "two", "three" - - -class TestEnum(Base): - __tablename__ = "test_enum" - - e1: str = Column(Enum("one", "two", "three")) - - e2: MyEnum = Column(Enum(MyEnum)) - - e3 = Column(Enum(one, two, three)) - - e4 = Column(Enum(MyEnum)) - - -t1 = TestEnum(e1="two", e2=MyEnum.three, e3="one", e4=MyEnum.one) - -x: str = t1.e1 - -y: MyEnum = t1.e2 - -z: Optional[str] = t1.e3 - -z2: Optional[MyEnum] = t1.e4 diff --git a/test/ext/mypy/plugin_files/imperative_table.py b/test/ext/mypy/plugin_files/imperative_table.py deleted file mode 100644 index 0548a79268..0000000000 --- a/test/ext/mypy/plugin_files/imperative_table.py +++ /dev/null @@ -1,37 +0,0 @@ -import datetime -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import DateTime -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy import Table -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Mapped - - -Base = declarative_base() - - -class MyMappedClass(Base): - __table_ = Table( - "some_table", - Base.metadata, - Column("id", Integer, primary_key=True), - Column("data", String(50)), - Column("created_at", DateTime), - ) - - id: Mapped[int] - data: Mapped[Optional[str]] - created_at: Mapped[datetime.datetime] - - -m1 = MyMappedClass(id=5, data="string", created_at=datetime.datetime.now()) - -# EXPECTED_MYPY: Argument "created_at" to "MyMappedClass" has incompatible type "int"; expected "datetime" # noqa -m2 = MyMappedClass(id=5, data="string", created_at=12) - - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") # noqa -x: str = MyMappedClass().data diff --git a/test/ext/mypy/plugin_files/invalid_noninferred_lh_type.py b/test/ext/mypy/plugin_files/invalid_noninferred_lh_type.py deleted file mode 100644 index e9ff303ca7..0000000000 --- a/test/ext/mypy/plugin_files/invalid_noninferred_lh_type.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - # EXPECTED: Left hand assignment 'name: "int"' not compatible with ORM mapped expression # noqa: E501 - name: int = Column(String()) diff --git a/test/ext/mypy/plugin_files/issue_7321.py b/test/ext/mypy/plugin_files/issue_7321.py deleted file mode 100644 index d4cd7f2c43..0000000000 --- a/test/ext/mypy/plugin_files/issue_7321.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Any -from typing import Dict - -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import declared_attr - - -Base = declarative_base() - - -class Foo(Base): - @declared_attr - def __tablename__(cls) -> str: - return "name" - - @declared_attr - def __mapper_args__(cls) -> Dict[Any, Any]: - return {} - - @declared_attr - def __table_args__(cls) -> Dict[Any, Any]: - return {} diff --git a/test/ext/mypy/plugin_files/issue_7321_part2.py b/test/ext/mypy/plugin_files/issue_7321_part2.py deleted file mode 100644 index 4227f2797e..0000000000 --- a/test/ext/mypy/plugin_files/issue_7321_part2.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any -from typing import Dict -from typing import Type - -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import declared_attr - - -Base = declarative_base() - - -class Foo(Base): - # no mypy error emitted regarding the - # Type[Foo] part - @declared_attr - def __tablename__(cls: Type["Foo"]) -> str: - return "name" - - @declared_attr - def __mapper_args__(cls: Type["Foo"]) -> Dict[Any, Any]: - return {} - - # this was a workaround that works if there's no plugin present, make - # sure that doesn't crash anything - @classmethod - @declared_attr - def __table_args__(cls: Type["Foo"]) -> Dict[Any, Any]: - return {} diff --git a/test/ext/mypy/plugin_files/issue_9102.py b/test/ext/mypy/plugin_files/issue_9102.py deleted file mode 100644 index aec840189c..0000000000 --- a/test/ext/mypy/plugin_files/issue_9102.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import registry - - -class BackendMeta: - __abstract__ = True - mapped_registry: registry = registry() - metadata = mapped_registry.metadata - - -# this decorator is not picked up now, but at least it doesn't crash -@BackendMeta.mapped_registry.mapped -class User: - __tablename__ = "user" - - # EXPECTED_MYPY: Incompatible types in assignment (expression has type "Column[int]", variable has type "int") - id: int = Column(Integer(), primary_key=True) diff --git a/test/ext/mypy/plugin_files/issue_9102_workaround.py b/test/ext/mypy/plugin_files/issue_9102_workaround.py deleted file mode 100644 index 3682d29b23..0000000000 --- a/test/ext/mypy/plugin_files/issue_9102_workaround.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import registry - - -class BackendMeta: - __abstract__ = True - mapped_registry: registry = registry() - metadata = mapped_registry.metadata - - -reg: registry = BackendMeta.mapped_registry - - -@reg.mapped -class User: - __tablename__ = "user" - - id: int = Column(Integer(), primary_key=True) diff --git a/test/ext/mypy/plugin_files/issue_9156.py b/test/ext/mypy/plugin_files/issue_9156.py deleted file mode 100644 index e67f64442a..0000000000 --- a/test/ext/mypy/plugin_files/issue_9156.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any -from typing import Type - -from sqlalchemy.sql.elements import ColumnElement -from sqlalchemy.sql.type_api import TypeEngine - -col: ColumnElement[Any] -type_: Type[TypeEngine[Any]] -obj: TypeEngine[Any] - -col.cast(type_) -col.cast(obj) diff --git a/test/ext/mypy/plugin_files/lambda_default.py b/test/ext/mypy/plugin_files/lambda_default.py deleted file mode 100644 index a1019f0d02..0000000000 --- a/test/ext/mypy/plugin_files/lambda_default.py +++ /dev/null @@ -1,11 +0,0 @@ -import uuid - -from sqlalchemy import Column -from sqlalchemy import String -from sqlalchemy.orm import declarative_base - -Base = declarative_base() - - -class MyClass(Base): - id = Column(String, default=lambda: uuid.uuid4(), primary_key=True) diff --git a/test/ext/mypy/plugin_files/mapped_attr_assign.py b/test/ext/mypy/plugin_files/mapped_attr_assign.py deleted file mode 100644 index c7244c27a6..0000000000 --- a/test/ext/mypy/plugin_files/mapped_attr_assign.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Test patterns that can be used for assignment of mapped attributes -after the mapping is complete - - -""" - -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import inspect -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import column_property -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class B(Base): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - a_id: int = Column(ForeignKey("a.id")) - - # to attach attrs after the fact, declare them with Mapped - # on the class... - data: Mapped[str] - - a: Mapped[Optional["A"]] - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - data = Column(String) - bs = relationship(B, uselist=True, back_populates="a") - - -# There's no way to intercept the __setattr__() from the metaclass -# here, and also when @reg.mapped() is used there is no metaclass. -# so have them do it the old way -inspect(B).add_property( - "data", - column_property(select(A.data).where(A.id == B.a_id).scalar_subquery()), -) -inspect(B).add_property("a", relationship(A)) - - -# the constructor will pick them up -a1 = A() -b1 = B(data="b", a=a1) - -# and it's mapped -B.data.in_(["x", "y"]) -B.a.any() diff --git a/test/ext/mypy/plugin_files/mixin_not_mapped.py b/test/ext/mypy/plugin_files/mixin_not_mapped.py deleted file mode 100644 index e9aa336c8d..0000000000 --- a/test/ext/mypy/plugin_files/mixin_not_mapped.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import registry - - -reg: registry = registry() - -Base = declarative_base() - - -class SomeAbstract(Base): - __abstract__ = True - - -class HasUpdatedAt: - updated_at = Column(Integer) - - -@reg.mapped -class Foo(SomeAbstract): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -class Bar(HasUpdatedAt, Base): - __tablename__ = "bar" - id = Column(Integer(), primary_key=True) - num = Column(Integer) - - -Bar.__mapper__ - -# EXPECTED_MYPY: "type[HasUpdatedAt]" has no attribute "__mapper__" -HasUpdatedAt.__mapper__ - - -# EXPECTED_MYPY: "type[SomeAbstract]" has no attribute "__mapper__" -SomeAbstract.__mapper__ diff --git a/test/ext/mypy/plugin_files/mixin_one.py b/test/ext/mypy/plugin_files/mixin_one.py deleted file mode 100644 index a471edf6c7..0000000000 --- a/test/ext/mypy/plugin_files/mixin_one.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import registry - - -reg: registry = registry() - -# TODO: also reg.as_declarative_base() -Base = declarative_base() - - -class HasUpdatedAt: - updated_at = Column(Integer) - - -@reg.mapped -class Foo(HasUpdatedAt): - __tablename__ = "foo" - id: int = Column(Integer(), primary_key=True) - name: str = Column(String) - - -class Bar(HasUpdatedAt, Base): - __tablename__ = "bar" - id = Column(Integer(), primary_key=True) - num = Column(Integer) - - -Foo.updated_at.in_([1, 2, 3]) -Bar.updated_at.in_([1, 2, 3]) - -f1 = Foo(name="name", updated_at=5) - -b1 = Bar(num=5, updated_at=6) - - -# test that we detected this as an unmapped mixin -# EXPECTED_MYPY: Unexpected keyword argument "updated_at" for "HasUpdatedAt" -HasUpdatedAt(updated_at=5) diff --git a/test/ext/mypy/plugin_files/mixin_three.py b/test/ext/mypy/plugin_files/mixin_three.py deleted file mode 100644 index cb8e30df81..0000000000 --- a/test/ext/mypy/plugin_files/mixin_three.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Callable - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import deferred -from sqlalchemy.orm import Mapped -from sqlalchemy.orm.decl_api import declarative_mixin -from sqlalchemy.orm.decl_api import declared_attr -from sqlalchemy.orm.interfaces import MapperProperty - - -def some_other_decorator(fn: Callable[..., None]) -> Callable[..., None]: - return fn - - -@declarative_mixin -class HasAMixin: - x: Mapped[int] = Column(Integer) - - y = Column(String) - - @declared_attr - def data(cls) -> Column[String]: - return Column(String) - - @declared_attr - def data2(cls) -> MapperProperty[str]: - return deferred(Column(String)) - - @some_other_decorator - def q(cls) -> None: - return None diff --git a/test/ext/mypy/plugin_files/mixin_two.py b/test/ext/mypy/plugin_files/mixin_two.py deleted file mode 100644 index 900b28fa49..0000000000 --- a/test/ext/mypy/plugin_files/mixin_two.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Callable - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import deferred -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship -from sqlalchemy.orm import RelationshipProperty -from sqlalchemy.orm.decl_api import declared_attr -from sqlalchemy.orm.interfaces import MapperProperty -from sqlalchemy.sql.schema import ForeignKey - - -reg: registry = registry() - - -@reg.mapped -class C: - __tablename__ = "c" - id = Column(Integer, primary_key=True) - - -def some_other_decorator(fn: Callable[..., None]) -> Callable[..., None]: - return fn - - -class HasAMixin: - @declared_attr - def a(cls) -> Mapped["A"]: - return relationship("A", back_populates="bs") - - # EXPECTED: Can't infer type from @declared_attr on function 'a2'; - @declared_attr - def a2(cls): - return relationship("A", back_populates="bs") - - @declared_attr - def a3(cls) -> RelationshipProperty["A"]: - return relationship("A", back_populates="bs") - - @declared_attr - def c1(cls) -> RelationshipProperty[C]: - return relationship(C, back_populates="bs") - - @declared_attr - def c2(cls) -> Mapped[C]: - return relationship(C, back_populates="bs") - - @declared_attr - def data(cls) -> Column[String]: - return Column(String) - - @declared_attr - def data2(cls) -> MapperProperty[str]: - return deferred(Column(String)) - - @some_other_decorator - def q(cls) -> None: - return None - - -@reg.mapped -class B(HasAMixin): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - a_id: int = Column(ForeignKey("a.id")) - c_id: int = Column(ForeignKey("c.id")) - - -@reg.mapped -class A: - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - - @declared_attr - def data(cls) -> Column[String]: - return Column(String) - - # EXPECTED: Can't infer type from @declared_attr on function 'data2'; - @declared_attr - def data2(cls): - return Column(String) - - bs = relationship(B, uselist=True, back_populates="a") - - -a1 = A(id=1, data="d1", data2="d2") - - -b1 = B(a=A(), a2=A(), c1=C(), c2=C(), data="d1", data2="d2") - -# descriptor access as Mapped[] -B.a.any() -B.a2.any() -B.c1.any() -B.c2.any() - -# sanity check against another fn that isn't mapped -# EXPECTED_MYPY: "Callable[..., None]" has no attribute "any" -B.q.any() - -B.data.in_(["a", "b"]) -B.data2.in_(["a", "b"]) diff --git a/test/ext/mypy/plugin_files/mixin_w_tablename.py b/test/ext/mypy/plugin_files/mixin_w_tablename.py deleted file mode 100644 index cfbe83d35d..0000000000 --- a/test/ext/mypy/plugin_files/mixin_w_tablename.py +++ /dev/null @@ -1,27 +0,0 @@ -# test #6937 -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import declared_attr -from sqlalchemy.orm import Mapped - - -Base = declarative_base() - - -class UpdatedCls: - @declared_attr - def __tablename__(cls) -> Mapped[str]: - return cls.__name__.lower() - - updated_at = Column(Integer) - - -class Bar(UpdatedCls, Base): - id = Column(Integer(), primary_key=True) - num = Column(Integer) - - -Bar.updated_at.in_([1, 2, 3]) - -b1 = Bar(num=5, updated_at=6) diff --git a/test/ext/mypy/plugin_files/orderinglist1.py b/test/ext/mypy/plugin_files/orderinglist1.py deleted file mode 100644 index fb05b767a5..0000000000 --- a/test/ext/mypy/plugin_files/orderinglist1.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy.ext.orderinglist import ordering_list -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship - -mapper_registry: registry = registry() - - -@mapper_registry.mapped -class A: - __tablename__ = "a" - id = Column(Integer, primary_key=True) - - # EXPECTED: Can't infer type from ORM mapped expression assigned to attribute 'parents'; please specify a Python type or Mapped[] on the left hand side. # noqa - parents = relationship("A", collection_class=ordering_list("ordering")) - parent_id = Column(Integer, ForeignKey("a.id")) - ordering = Column(Integer) - - -a1 = A(id=5, ordering=10) - -# EXPECTED_MYPY: Argument "parents" to "A" has incompatible type "list[A]"; expected "Mapped[Any]" # noqa -a2 = A(parents=[a1]) diff --git a/test/ext/mypy/plugin_files/orderinglist2.py b/test/ext/mypy/plugin_files/orderinglist2.py deleted file mode 100644 index d8b179e9a7..0000000000 --- a/test/ext/mypy/plugin_files/orderinglist2.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import List - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy.ext.orderinglist import ordering_list -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship - -mapper_registry: registry = registry() - - -@mapper_registry.mapped -class B: - __tablename__ = "b" - id = Column(Integer, primary_key=True) - parent_id = Column(Integer, ForeignKey("a.id")) - ordering = Column(Integer) - - -@mapper_registry.mapped -class C: - __tablename__ = "c" - id = Column(Integer, primary_key=True) - parent_id = Column(Integer, ForeignKey("a.id")) - ordering = Column(Integer) - - -@mapper_registry.mapped -class A: - __tablename__ = "a" - id = Column(Integer, primary_key=True) - - bs = relationship(B, collection_class=ordering_list("ordering")) - - bs_w_list: List[B] = relationship( - B, collection_class=ordering_list("ordering") - ) - - # EXPECTED: Left hand assignment 'cs: "list[B]"' not compatible with ORM mapped expression of type "Mapped[list[C]]" # noqa - cs: List[B] = relationship(C, uselist=True) - - # EXPECTED: Left hand assignment 'cs_2: "B"' not compatible with ORM mapped expression of type "Mapped[list[C]]" # noqa - cs_2: B = relationship(C, uselist=True) - - -b1 = B(ordering=10) - -# in this case, the plugin infers OrderingList as the type. not great -a1 = A() -a1.bs.append(b1) - -# so we want to support being able to override it at least -a2 = A(bs_w_list=[b1]) diff --git a/test/ext/mypy/plugin_files/other_mapper_props.py b/test/ext/mypy/plugin_files/other_mapper_props.py deleted file mode 100644 index d87165fea2..0000000000 --- a/test/ext/mypy/plugin_files/other_mapper_props.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import column_property -from sqlalchemy.orm import deferred -from sqlalchemy.orm import registry -from sqlalchemy.orm import Session -from sqlalchemy.orm import synonym -from sqlalchemy.sql.functions import func -from sqlalchemy.sql.sqltypes import Text - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - name = Column(String) - - # this gets inferred - big_col = deferred(Column(Text)) - - # this gets inferred - explicit_col = column_property(Column(Integer)) - - # EXPECTED: Can't infer type from ORM mapped expression assigned to attribute 'lower_name'; # noqa - lower_name = column_property(func.lower(name)) - - # EXPECTED: Can't infer type from ORM mapped expression assigned to attribute 'syn_name'; # noqa - syn_name = synonym("name") - - # this uses our type - lower_name_exp: str = column_property(func.lower(name)) - - # this uses our type - syn_name_exp: Optional[str] = synonym("name") - - -s = Session() - -u1: Optional[User] = s.get(User, 5) -assert u1 - -q1: Optional[str] = u1.big_col - -q2: Optional[int] = u1.explicit_col - - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "str", variable has type "int") # noqa -x: int = u1.lower_name_exp - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "Optional[str]", variable has type "int") # noqa -y: int = u1.syn_name_exp diff --git a/test/ext/mypy/plugin_files/plugin_doesnt_break_one.py b/test/ext/mypy/plugin_files/plugin_doesnt_break_one.py deleted file mode 100644 index 19cb2bfb41..0000000000 --- a/test/ext/mypy/plugin_files/plugin_doesnt_break_one.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class Foo: - pass - id: int = Column(Integer()) - name: str = Column(String) - - -f1 = Foo() - - -# EXPECTED_MYPY: Name 'u1' is not defined -p: str = u1.name # noqa diff --git a/test/ext/mypy/plugin_files/relationship_6255_one.py b/test/ext/mypy/plugin_files/relationship_6255_one.py deleted file mode 100644 index 15961c703a..0000000000 --- a/test/ext/mypy/plugin_files/relationship_6255_one.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import mapped_column -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class User(Base): - __tablename__ = "user" - - id = mapped_column(Integer, primary_key=True) - name: Mapped[Optional[str]] = mapped_column(String, nullable=True) - - addresses: Mapped[List["Address"]] = relationship( - "Address", back_populates="user" - ) - - @property - def some_property(self) -> List[Optional[int]]: - return [i.id for i in self.addresses] - - -class Address(Base): - __tablename__ = "address" - - id = mapped_column(Integer, primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) - - user: Mapped["User"] = relationship("User", back_populates="addresses") - - @property - def some_other_property(self) -> Optional[str]: - return self.user.name - - -# it's in the constructor, correct type -u1 = User(addresses=[Address()]) - -# knows it's an iterable -[x for x in u1.addresses] - -# knows it's Mapped -stmt = select(User).where(User.addresses.any(id=5)) diff --git a/test/ext/mypy/plugin_files/relationship_6255_three.py b/test/ext/mypy/plugin_files/relationship_6255_three.py deleted file mode 100644 index 121d8de40a..0000000000 --- a/test/ext/mypy/plugin_files/relationship_6255_three.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - addresses: List["Address"] = relationship("Address", back_populates="user") - - @property - def some_property(self) -> List[Optional[int]]: - return [i.id for i in self.addresses] - - -class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - - user: "User" = relationship("User", back_populates="addresses") - - @property - def some_other_property(self) -> Optional[str]: - return self.user.name - - -# it's in the constructor, correct type -u1 = User(addresses=[Address()]) - -# knows it's an iterable -[x for x in u1.addresses] - -# knows it's Mapped -stmt = select(User).where(User.addresses.any(id=5)) diff --git a/test/ext/mypy/plugin_files/relationship_6255_two.py b/test/ext/mypy/plugin_files/relationship_6255_two.py deleted file mode 100644 index 121d8de40a..0000000000 --- a/test/ext/mypy/plugin_files/relationship_6255_two.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import select -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - name = Column(String) - - addresses: List["Address"] = relationship("Address", back_populates="user") - - @property - def some_property(self) -> List[Optional[int]]: - return [i.id for i in self.addresses] - - -class Address(Base): - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - user_id: int = Column(ForeignKey("user.id")) - - user: "User" = relationship("User", back_populates="addresses") - - @property - def some_other_property(self) -> Optional[str]: - return self.user.name - - -# it's in the constructor, correct type -u1 = User(addresses=[Address()]) - -# knows it's an iterable -[x for x in u1.addresses] - -# knows it's Mapped -stmt = select(User).where(User.addresses.any(id=5)) diff --git a/test/ext/mypy/plugin_files/relationship_direct_cls.py b/test/ext/mypy/plugin_files/relationship_direct_cls.py deleted file mode 100644 index 1c4efdee27..0000000000 --- a/test/ext/mypy/plugin_files/relationship_direct_cls.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class B(Base): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - a_id: int = Column(ForeignKey("a.id")) - data = Column(String) - - a: Optional["A"] = relationship("A", back_populates="bs") - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - data = Column(String) - bs = relationship(B, uselist=True, back_populates="a") - - -a1 = A(bs=[B(data="b"), B(data="b")]) - -x: List[B] = a1.bs - - -b1 = B(a=A()) diff --git a/test/ext/mypy/plugin_files/relationship_err1.py b/test/ext/mypy/plugin_files/relationship_err1.py deleted file mode 100644 index 46e7067d34..0000000000 --- a/test/ext/mypy/plugin_files/relationship_err1.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import List - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class B(Base): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - - # EXPECTED: Expected Python collection type for collection_class parameter # noqa - as_: List["A"] = relationship("A", collection_class=None) - - # EXPECTED: Can't infer type from ORM mapped expression assigned to attribute 'another_as_'; # noqa - another_as_ = relationship("A", uselist=True) - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - b_id: int = Column(ForeignKey("b.id")) - - # EXPECTED: Sending uselist=False and collection_class at the same time does not make sense # noqa - b: B = relationship(B, uselist=False, collection_class=set) diff --git a/test/ext/mypy/plugin_files/relationship_err2.py b/test/ext/mypy/plugin_files/relationship_err2.py deleted file mode 100644 index 04db946abf..0000000000 --- a/test/ext/mypy/plugin_files/relationship_err2.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Set - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class B(Base): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - a_id: int = Column(ForeignKey("a.id")) - data = Column(String) - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - data = Column(String) - bs = relationship(B, uselist=True) - - -# EXPECTED_MYPY: List item 1 has incompatible type "A"; expected "B" -a1 = A(bs=[B(data="b"), A()]) - -# EXPECTED_MYPY: Incompatible types in assignment (expression has type "list[B]", variable has type "set[B]") # noqa -x: Set[B] = a1.bs diff --git a/test/ext/mypy/plugin_files/relationship_err3.py b/test/ext/mypy/plugin_files/relationship_err3.py deleted file mode 100644 index 95d77fde59..0000000000 --- a/test/ext/mypy/plugin_files/relationship_err3.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Optional -from typing import Set - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - - -class B(Base): - __tablename__ = "b" - id = Column(Integer, primary_key=True) - a_id: int = Column(ForeignKey("a.id")) - data = Column(String) - a: Optional["A"] = relationship("A", back_populates="bs") - - -class A(Base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - data = Column(String) - - bs: Set[B] = relationship(B, uselist=True, back_populates="a") - - # EXPECTED: Left hand assignment 'another_bs: "set[B]"' not compatible with ORM mapped expression of type "Mapped[B]" # noqa - another_bs: Set[B] = relationship(B, viewonly=True) - - -# EXPECTED_MYPY: Argument "a" to "B" has incompatible type "str"; expected "Optional[A]" # noqa -b1 = B(a="not an a") diff --git a/test/ext/mypy/plugin_files/sa_module_prefix.py b/test/ext/mypy/plugin_files/sa_module_prefix.py deleted file mode 100644 index a37ae6b06f..0000000000 --- a/test/ext/mypy/plugin_files/sa_module_prefix.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import List -from typing import Optional - -import sqlalchemy as sa -from sqlalchemy import orm as saorm - - -Base = saorm.declarative_base() - - -class B(Base): - __tablename__ = "b" - id = sa.Column(sa.Integer, primary_key=True) - a_id: int = sa.Column(sa.ForeignKey("a.id")) - data = sa.Column(sa.String) - - a: Optional["A"] = saorm.relationship("A", back_populates="bs") - - -class A(Base): - __tablename__ = "a" - - id = sa.Column(sa.Integer, primary_key=True) - data = sa.Column(sa.String) - bs = saorm.relationship(B, uselist=True, back_populates="a") - - -a1 = A(bs=[B(data="b"), B(data="b")]) - -x: List[B] = a1.bs - - -b1 = B(a=A()) diff --git a/test/ext/mypy/plugin_files/t_6950.py b/test/ext/mypy/plugin_files/t_6950.py deleted file mode 100644 index 3ebbf66389..0000000000 --- a/test/ext/mypy/plugin_files/t_6950.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import cast - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import query_expression -from sqlalchemy.orm import Session -from sqlalchemy.orm import with_expression - -Base = declarative_base() - - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True) - - foo = Column(Integer) - - question_count: Mapped[int] = query_expression() - answer_count: int = query_expression() - - -s = Session() - -q = s.query(User).options(with_expression(User.question_count, User.foo + 5)) - -u1: User = cast(User, q.first()) - -qc: int = u1.question_count -print(qc) diff --git a/test/ext/mypy/plugin_files/type_decorator.py b/test/ext/mypy/plugin_files/type_decorator.py deleted file mode 100644 index 07a13caee4..0000000000 --- a/test/ext/mypy/plugin_files/type_decorator.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any -from typing import Optional - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy import TypeDecorator -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - - -class IntToStr(TypeDecorator[int]): - impl = String - cache_ok = True - - def process_bind_param( - self, - value: Any, - dialect: Any, - ) -> Optional[str]: - return str(value) if value is not None else value - - def process_result_value( - self, - value: Any, - dialect: Any, - ) -> Optional[int]: - return int(value) if value is not None else value - - def copy(self, **kwargs: Any) -> "IntToStr": - return IntToStr(self.impl.length) - - -class Thing(Base): - __tablename__ = "things" - - id: int = Column(Integer, primary_key=True) - intToStr: int = Column(IntToStr) - - -t1 = Thing(intToStr=5) - -i5: int = t1.intToStr - -t1.intToStr = 8 diff --git a/test/ext/mypy/plugin_files/typeless_fk_col_cant_infer.py b/test/ext/mypy/plugin_files/typeless_fk_col_cant_infer.py deleted file mode 100644 index 0b933db478..0000000000 --- a/test/ext/mypy/plugin_files/typeless_fk_col_cant_infer.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import registry - -reg: registry = registry() - - -@reg.mapped -class User: - __tablename__ = "user" - - id = Column(Integer(), primary_key=True) - name = Column(String) - - -@reg.mapped -class Address: - __tablename__ = "address" - - id = Column(Integer, primary_key=True) - # EXPECTED: Can't infer type from ORM mapped expression assigned to attribute 'user_id'; # noqa: E501 - user_id = Column(ForeignKey("user.id")) - email_address = Column(String) diff --git a/test/ext/mypy/plugin_files/typing_err1.py b/test/ext/mypy/plugin_files/typing_err1.py deleted file mode 100644 index f262cd55b6..0000000000 --- a/test/ext/mypy/plugin_files/typing_err1.py +++ /dev/null @@ -1,31 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy.orm import registry -from sqlalchemy.types import TypeEngine - - -# EXPECTED_MYPY: Missing type parameters for generic type "TypeEngine" -class MyCustomType(TypeEngine): - pass - - -# correct way -class MyOtherCustomType(TypeEngine[str]): - pass - - -reg: registry = registry() - - -@reg.mapped -class Foo: - id: int = Column(Integer()) - - name = Column(MyCustomType()) - other_name: str = Column(MyCustomType()) - - name2 = Column(MyOtherCustomType()) - other_name2: str = Column(MyOtherCustomType()) - - -Foo(name="x", other_name="x", name2="x", other_name2="x") diff --git a/test/ext/mypy/plugin_files/typing_err2.py b/test/ext/mypy/plugin_files/typing_err2.py deleted file mode 100644 index 5b8dfe4af0..0000000000 --- a/test/ext/mypy/plugin_files/typing_err2.py +++ /dev/null @@ -1,38 +0,0 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declared_attr -from sqlalchemy.orm import registry -from sqlalchemy.orm import relationship -from sqlalchemy.orm import RelationshipProperty - -reg: registry = registry() - - -@reg.mapped -class Foo: - id: int = Column(Integer()) - - # EXPECTED: Can't infer type from @declared_attr on function 'name'; # noqa - @declared_attr - # EXPECTED: Column type should be a TypeEngine subclass not 'builtins.str' - def name(cls) -> Column[str]: - return Column(String) - - # EXPECTED: Left hand assignment 'other_name: "Column[String]"' not compatible with ORM mapped expression of type "Mapped[str]" # noqa - other_name: Column[String] = Column(String) - - # EXPECTED: Can't infer type from @declared_attr on function 'third_name'; - @declared_attr - # EXPECTED_MYPY: Missing type parameters for generic type "Column" - def third_name(cls) -> Column: - return Column(String) - - # EXPECTED: Can't infer type from @declared_attr on function 'some_relationship' # noqa - @declared_attr - # EXPECTED_MYPY: Missing type parameters for generic type "RelationshipProperty" - def some_relationship(cls) -> RelationshipProperty: - return relationship("Bar") - - -Foo(name="x") diff --git a/test/ext/mypy/plugin_files/typing_err3.py b/test/ext/mypy/plugin_files/typing_err3.py deleted file mode 100644 index 146b96b2a7..0000000000 --- a/test/ext/mypy/plugin_files/typing_err3.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Test that the right-hand expressions we normally "replace" are actually -type checked. - -""" - -from typing import List - -from sqlalchemy import Column -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Mapped -from sqlalchemy.orm import relationship -from sqlalchemy.orm.decl_api import declared_attr - - -Base = declarative_base() - - -class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True) - - addresses: Mapped[List["Address"]] = relationship( - "Address", wrong_arg="imwrong" - ) - - -class SubUser(User): - __tablename__ = "subuser" - - id: int = Column(Integer, ForeignKey("user.id"), primary_key=True) - - -class Address(Base): - __tablename__ = "address" - - id: int = Column(Integer, primary_key=True) - - user_id: int = Column(ForeignKey("user.id")) - - @declared_attr - def email_address(cls) -> Column[String]: - # EXPECTED_MYPY: Argument 1 to "Column" has incompatible type "bool"; - return Column(True) - - @declared_attr - # EXPECTED_MYPY: Invalid type comment or annotation - def thisisweird(cls) -> Column(String): - # EXPECTED_MYPY: Argument 1 to "Column" has incompatible type "bool"; - return Column(False) diff --git a/test/ext/mypy/test_mypy_plugin_py3k.py b/test/ext/mypy/test_mypy_plugin_py3k.py deleted file mode 100644 index 1d75137a04..0000000000 --- a/test/ext/mypy/test_mypy_plugin_py3k.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -import pathlib -import shutil - -try: - from mypy.version import __version__ as _mypy_version_str -except ImportError: - _mypy_version = None -else: - _mypy_version = tuple(int(x) for x in _mypy_version_str.split(".")) - -from sqlalchemy import testing -from sqlalchemy.testing import eq_ -from sqlalchemy.testing import fixtures - - -def _incremental_dirs(): - path = os.path.join(os.path.dirname(__file__), "incremental") - files = [] - for d in os.listdir(path): - if os.path.isdir(os.path.join(path, d)): - files.append( - os.path.join(os.path.dirname(__file__), "incremental", d) - ) - - for extra_dir in testing.config.options.mypy_extra_test_paths: - if extra_dir and os.path.isdir(extra_dir): - for d in os.listdir(os.path.join(extra_dir, "incremental")): - if os.path.isdir(os.path.join(path, d)): - files.append(os.path.join(extra_dir, "incremental", d)) - return files - - -def _mypy_missing_or_incompatible(): - return not _mypy_version or _mypy_version > (1, 10, 1) - - -class MypyPluginTest(fixtures.MypyTest): - @testing.skip_if( - _mypy_missing_or_incompatible, - "Mypy must be present and compatible (<= 1.10.1)", - ) - @testing.combinations( - *[ - (pathlib.Path(pathname).name, pathname) - for pathname in _incremental_dirs() - ], - argnames="pathname", - id_="ia", - ) - @testing.requires.patch_library - def test_incremental(self, mypy_runner, per_func_cachedir, pathname): - import patch - - cachedir = per_func_cachedir - - dest = os.path.join(cachedir, "mymodel") - os.mkdir(dest) - - patches = set() - - print("incremental test: %s" % pathname) - - for fname in os.listdir(pathname): - if fname.endswith(".py"): - shutil.copy( - os.path.join(pathname, fname), os.path.join(dest, fname) - ) - print("copying to: %s" % os.path.join(dest, fname)) - elif fname.endswith(".testpatch"): - patches.add(fname) - - for patchfile in [None] + sorted(patches): - if patchfile is not None: - print("Applying patchfile %s" % patchfile) - patch_obj = patch.fromfile(os.path.join(pathname, patchfile)) - assert patch_obj.apply(1, dest), ( - "pathfile %s failed" % patchfile - ) - print("running mypy against %s" % dest) - result = mypy_runner( - dest, - use_plugin=True, - use_cachedir=cachedir, - ) - eq_( - result[2], - 0, - msg="Failure after applying patch %s: %s" - % (patchfile, result[0]), - ) - - @testing.skip_if( - _mypy_missing_or_incompatible, - "Mypy must be present and compatible (<= 1.10.1)", - ) - @testing.combinations( - *( - (os.path.basename(path), path, True) - for path in fixtures.MypyTest.file_combinations("plugin_files") - ), - argnames="path", - id_="ia", - ) - def test_plugin_files(self, mypy_typecheck_file, path): - mypy_typecheck_file(path, use_plugin=True) diff --git a/tox.ini b/tox.ini index 79d872b58d..789bef0e2b 100644 --- a/tox.ini +++ b/tox.ini @@ -204,8 +204,7 @@ deps= pytest>=7.0.0rc1,<8 pytest-xdist greenlet != 0.4.17 - mypy >= 1.7.0,<1.11.0 - patch==1.* + mypy >= 1.14 types-greenlet extras= {[greenletextras]extras}