diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile
index 5d02a0f436..11364c8774 100644
--- a/.gitpod.Dockerfile
+++ b/.gitpod.Dockerfile
@@ -1,4 +1,4 @@
-FROM gitpod/workspace-python-3.11
+FROM gitpod/workspace-python-3.12
USER root
RUN apt-get update \
&& apt-get install -y libboost-dev \
diff --git a/AGENTS.md b/AGENTS.md
index 85321d3e5d..c429eaa5ec 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,7 +13,7 @@ conventions in [`src/gt4py/next/AGENTS.md`](src/gt4py/next/AGENTS.md).
## Stack
-- Language: **Python 3.10–3.14** (see `.python-versions`).
+- Language: **Python 3.12–3.14** (see `.python-versions`).
- Environment / dependencies: **`uv`** (lockfile is `uv.lock`) — always go
through `uv`, never bare `pip` / `python`.
- Test runner: **`nox`** (sessions in `noxfile.py`).
diff --git a/docs/development/next/error-messages.md b/docs/development/next/error-messages.md
index 5f9342dc45..a77c99ad04 100644
--- a/docs/development/next/error-messages.md
+++ b/docs/development/next/error-messages.md
@@ -119,11 +119,13 @@ except errors.DSLError as err:
raise
```
-`DSLError.add_note` overrides `BaseException.add_note` to route the note into
-the structured `notes` field instead of `__notes__`: the traceback machinery
-(and therefore pytest and IPython/Jupyter) prints the exception via
-`str(err)`, which already renders the structured notes, so writing `__notes__`
-as well would duplicate them. The seam is wired at `func_to_foast`
+`add_note` puts the breadcrumb in `__notes__`, which is *not* the structured
+`notes` field: `notes` is reserved for content authored at the raise site. The
+two are rendered by different code, so do not expect one to show the other —
+`DSLError.__str__` emits only the structured parts, while `__notes__` is
+printed by the traceback machinery (and therefore by pytest and
+IPython/Jupyter). `errors/excepthook.py` replaces that machinery, so it
+appends `__notes__` itself. The seam is wired at `func_to_foast`
(`ffront/func_to_foast.py`); add it at later stages as they gain useful
context.
@@ -201,14 +203,8 @@ Unsupported operand type(s) for +: 'Field[[IDim], float64]' and 'Field[[IDim], b
## Python-version caveat
-The supported floor is Python 3.10, so the diagnostics code carries a few
-forward-compat shims; respect them:
-
-- Import `Self` from `gt4py.eve.extended_typing`, not `typing` (3.11+ only).
-- `DSLError.add_note` works on every Python because `DSLError` defines it;
- don't rely on `add_note` for *other* `GT4PyError`s — it is a builtin only on
- 3.11+.
-- The catalogue must not reference `ast` nodes added after 3.10 (e.g.
- `ast.TryStar`) unconditionally — that breaks import on 3.10.
-
-These spots are flagged with `TODO(havogt)`.
+`ast` nodes introduced *after* the supported floor cannot be named directly in
+the catalogue — `ast.TemplateStr` (PEP 750 t-strings, 3.14) does not exist on
+3.12, so the module would fail to import there. Register those by name in
+`_NEWER_UNSUPPORTED_FEATURE_HINTS` instead; entries the running interpreter
+does not have are skipped.
diff --git a/noxfile.py b/noxfile.py
index e3163edbaa..94ec333558 100755
--- a/noxfile.py
+++ b/noxfile.py
@@ -9,11 +9,11 @@
# SPDX-License-Identifier: BSD-3-Clause
#
# Note:
-# The explicit '--python 3.11' in the shebang is only needed due
+# The explicit '--python 3.12' in the shebang is only needed due
# to the existence of the .python-versions file, which overrides
# the PEP 723 'requires-python' metadata.
# /// script
-# requires-python = ">=3.11"
+# requires-python = ">=3.12"
# dependencies = ["nox>=2025.02.09", "uv>=0.6.10"]
# ///
@@ -321,11 +321,18 @@ def test_typing_exports(session: nox.Session) -> None:
"""Test GT4Py usability in a typed client context."""
install_session_venv(session, extras=["standard"], groups=["test", "typing_exports"])
+ # Pass the config explicitly: with no '--config-file', mypy discovers one by
+ # walking up from the plugin's temporary execution directory and reaches the
+ # project's own '[tool.mypy]' table, which pins 'python_version' to the supported
+ # floor and would collapse this session's 3.13/3.14 runs into the 3.12 one. See
+ # the comments in 'typing_tests/mypy.ini'.
session.run(
"pytest",
"-sv",
"--mypy-testing-base",
"typing_tests",
+ "--mypy-ini-file",
+ "typing_tests/mypy.ini",
"typing_tests",
*session.posargs,
)
diff --git a/pyproject.toml b/pyproject.toml
index f128e45fd0..c42ef74133 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -199,6 +199,9 @@ implicit_reexport = false
install_types = true
namespace_packages = false
plugins = ['gt4py.next.type_system.mypy_plugin']
+# Pin to the lowest supported version, otherwise the type-check floor silently
+# follows whichever interpreter happens to run mypy.
+python_version = '3.12'
# pretty = true
show_column_numbers = true
show_error_codes = true
@@ -263,12 +266,6 @@ implicit_reexport = true
# factory-boy is broken, see https://github.com/FactoryBoy/factory_boy/pull/1114
module = "factory.*"
-[[tool.mypy.overrides]]
-disallow_incomplete_defs = false
-disallow_untyped_defs = false
-ignore_errors = false
-module = "typing_tests.test_next_exports"
-
# -- pytest --
[tool.pytest]
@@ -397,7 +394,6 @@ known-third-party = [
'devtools',
'factory',
'hypothesis',
- 'importlib_resources',
'jinja2',
'mako',
'networkx',
diff --git a/src/gt4py/_core/definitions.py b/src/gt4py/_core/definitions.py
index 45a29121fc..69357968ba 100644
--- a/src/gt4py/_core/definitions.py
+++ b/src/gt4py/_core/definitions.py
@@ -30,8 +30,6 @@
Protocol,
Self,
Sequence,
- Tuple,
- Type,
TypeAlias,
TypeGuard,
TypeVar,
@@ -62,8 +60,8 @@
BoolScalar: TypeAlias = Union[core_types.bool, bool]
BoolT = TypeVar("BoolT", bound=BoolScalar)
-BOOL_TYPES: Final[Tuple[type, ...]] = cast(
- Tuple[type, ...],
+BOOL_TYPES: Final[tuple[type, ...]] = cast(
+ tuple[type, ...],
BoolScalar.__args__, # type: ignore[attr-defined]
)
@@ -72,8 +70,8 @@
core_types.int8, core_types.int16, core_types.int32, core_types.int64, int
]
IntT = TypeVar("IntT", bound=IntScalar)
-INT_TYPES: Final[Tuple[type, ...]] = cast(
- Tuple[type, ...],
+INT_TYPES: Final[tuple[type, ...]] = cast(
+ tuple[type, ...],
IntScalar.__args__, # type: ignore[attr-defined]
)
@@ -82,21 +80,21 @@
core_types.uint8, core_types.uint16, core_types.uint32, core_types.uint64
]
UnsignedIntT = TypeVar("UnsignedIntT", bound=UnsignedIntScalar)
-UINT_TYPES: Final[Tuple[type, ...]] = cast(
- Tuple[type, ...],
+UINT_TYPES: Final[tuple[type, ...]] = cast(
+ tuple[type, ...],
UnsignedIntScalar.__args__, # type: ignore[attr-defined]
)
IntegralScalar: TypeAlias = Union[IntScalar, UnsignedIntScalar]
IntegralT = TypeVar("IntegralT", bound=IntegralScalar)
-INTEGRAL_TYPES: Final[Tuple[type, ...]] = (*INT_TYPES, *UINT_TYPES)
+INTEGRAL_TYPES: Final[tuple[type, ...]] = (*INT_TYPES, *UINT_TYPES)
FloatingScalar: TypeAlias = Union[core_types.float32, core_types.float64, float]
FloatingT = TypeVar("FloatingT", bound=FloatingScalar)
-FLOAT_TYPES: Final[Tuple[type, ...]] = cast(
- Tuple[type, ...],
+FLOAT_TYPES: Final[tuple[type, ...]] = cast(
+ tuple[type, ...],
FloatingScalar.__args__, # type: ignore[attr-defined]
)
@@ -123,11 +121,11 @@ class PositiveIntegral(numbers.Integral):
...
-def is_boolean_integral_type(integral_type: type) -> TypeGuard[Type[BooleanIntegral]]:
+def is_boolean_integral_type(integral_type: type) -> TypeGuard[type[BooleanIntegral]]:
return issubclass(integral_type, BOOL_TYPES)
-def is_positive_integral_type(integral_type: type) -> TypeGuard[Type[PositiveIntegral]]:
+def is_positive_integral_type(integral_type: type) -> TypeGuard[type[PositiveIntegral]]:
return issubclass(integral_type, UINT_TYPES)
@@ -161,23 +159,23 @@ class DTypeKind(eve.StrEnum):
@overload
def dtype_kind(
- sc_type: Type[IntT] | Type[BoolT], # mypy doesn't distinguish IntT and BoolT
+ sc_type: type[IntT] | type[BoolT], # mypy doesn't distinguish IntT and BoolT
) -> Literal[DTypeKind.INT, DTypeKind.BOOL]: ...
@overload
-def dtype_kind(sc_type: Type[UnsignedIntT]) -> Literal[DTypeKind.UINT]: ... # type: ignore[overload-cannot-match] # precision blurring from mypy plugin seems to interfere
+def dtype_kind(sc_type: type[UnsignedIntT]) -> Literal[DTypeKind.UINT]: ... # type: ignore[overload-cannot-match] # precision blurring from mypy plugin seems to interfere
@overload
-def dtype_kind(sc_type: Type[FloatingT]) -> Literal[DTypeKind.FLOAT]: ...
+def dtype_kind(sc_type: type[FloatingT]) -> Literal[DTypeKind.FLOAT]: ...
@overload
-def dtype_kind(sc_type: Type[ScalarT]) -> DTypeKind: ...
+def dtype_kind(sc_type: type[ScalarT]) -> DTypeKind: ...
-def dtype_kind(sc_type: Type[ScalarT]) -> DTypeKind:
+def dtype_kind(sc_type: type[ScalarT]) -> DTypeKind:
"""Return the data type kind of the given scalar type."""
if issubclass(sc_type, numbers.Integral):
if is_boolean_integral_type(sc_type):
@@ -209,7 +207,7 @@ class DType(Generic[ScalarT]):
`dtype`s definitions due to the `.dtype` attribute.
"""
- scalar_type: Type[ScalarT]
+ scalar_type: type[ScalarT]
tensor_shape: TensorShape = dataclasses.field(default=())
def __post_init__(self) -> None:
@@ -266,28 +264,28 @@ class UnsignedIntDType(DType[UnsignedIntT]):
@dataclasses.dataclass(frozen=True)
class UInt8DType(UnsignedIntDType[core_types.uint8]):
- scalar_type: Final[Type[core_types.uint8]] = dataclasses.field(
+ scalar_type: Final[type[core_types.uint8]] = dataclasses.field(
default=core_types.uint8, init=False
)
@dataclasses.dataclass(frozen=True)
class UInt16DType(UnsignedIntDType[core_types.uint16]):
- scalar_type: Final[Type[core_types.uint16]] = dataclasses.field(
+ scalar_type: Final[type[core_types.uint16]] = dataclasses.field(
default=core_types.uint16, init=False
)
@dataclasses.dataclass(frozen=True)
class UInt32DType(UnsignedIntDType[core_types.uint32]):
- scalar_type: Final[Type[core_types.uint32]] = dataclasses.field(
+ scalar_type: Final[type[core_types.uint32]] = dataclasses.field(
default=core_types.uint32, init=False
)
@dataclasses.dataclass(frozen=True)
class UInt64DType(UnsignedIntDType[core_types.uint64]):
- scalar_type: Final[Type[core_types.uint64]] = dataclasses.field(
+ scalar_type: Final[type[core_types.uint64]] = dataclasses.field(
default=core_types.uint64, init=False
)
@@ -299,28 +297,28 @@ class SignedIntDType(DType[IntT]):
@dataclasses.dataclass(frozen=True)
class Int8DType(SignedIntDType[core_types.int8]):
- scalar_type: Final[Type[core_types.int8]] = dataclasses.field(
+ scalar_type: Final[type[core_types.int8]] = dataclasses.field(
default=core_types.int8, init=False
)
@dataclasses.dataclass(frozen=True)
class Int16DType(SignedIntDType[core_types.int16]):
- scalar_type: Final[Type[core_types.int16]] = dataclasses.field(
+ scalar_type: Final[type[core_types.int16]] = dataclasses.field(
default=core_types.int16, init=False
)
@dataclasses.dataclass(frozen=True)
class Int32DType(SignedIntDType[core_types.int32]):
- scalar_type: Final[Type[core_types.int32]] = dataclasses.field(
+ scalar_type: Final[type[core_types.int32]] = dataclasses.field(
default=core_types.int32, init=False
)
@dataclasses.dataclass(frozen=True)
class Int64DType(SignedIntDType[core_types.int64]):
- scalar_type: Final[Type[core_types.int64]] = dataclasses.field(
+ scalar_type: Final[type[core_types.int64]] = dataclasses.field(
default=core_types.int64, init=False
)
@@ -332,21 +330,21 @@ class FloatingDType(DType[FloatingT]):
@dataclasses.dataclass(frozen=True)
class Float32DType(FloatingDType[core_types.float32]):
- scalar_type: Final[Type[core_types.float32]] = dataclasses.field(
+ scalar_type: Final[type[core_types.float32]] = dataclasses.field(
default=core_types.float32, init=False
)
@dataclasses.dataclass(frozen=True)
class Float64DType(FloatingDType[core_types.float64]):
- scalar_type: Final[Type[core_types.float64]] = dataclasses.field(
+ scalar_type: Final[type[core_types.float64]] = dataclasses.field(
default=core_types.float64, init=False
)
@dataclasses.dataclass(frozen=True)
class BoolDType(DType[core_types.bool]):
- scalar_type: Final[Type[core_types.bool]] = dataclasses.field(
+ scalar_type: Final[type[core_types.bool]] = dataclasses.field(
default=core_types.bool, init=False
)
@@ -370,7 +368,7 @@ class GTDimsInterface(Protocol):
"""
@property
- def __gt_dims__(self) -> Tuple[str, ...]: ...
+ def __gt_dims__(self) -> tuple[str, ...]: ...
class GTOriginInterface(Protocol):
@@ -381,7 +379,7 @@ class GTOriginInterface(Protocol):
"""
@property
- def __gt_origin__(self) -> Tuple[int, ...]: ...
+ def __gt_origin__(self) -> tuple[int, ...]: ...
# -- Device representation --
@@ -447,7 +445,7 @@ def __iter__(self) -> Iterator[DeviceTypeT | int]:
# -- NDArrays and slices --
-SliceLike = Union[int, Tuple[int, ...], None, slice, "NDArrayObject"]
+SliceLike = Union[int, tuple[int, ...], None, slice, "NDArrayObject"]
class NDArrayObject(Protocol):
diff --git a/src/gt4py/cartesian/frontend/gtscript_frontend.py b/src/gt4py/cartesian/frontend/gtscript_frontend.py
index 61ae384068..a624600afa 100644
--- a/src/gt4py/cartesian/frontend/gtscript_frontend.py
+++ b/src/gt4py/cartesian/frontend/gtscript_frontend.py
@@ -52,6 +52,11 @@
PYTHON_AST_VERSION: Final = (3, 12)
+def _is_ellipsis_node(node: ast.AST) -> bool:
+ """Check whether an AST node is the '...' literal."""
+ return isinstance(node, ast.Constant) and node.value is Ellipsis
+
+
class AssertionChecker(ast.NodeTransformer):
"""Check assertions and remove from the AST for further parsing."""
@@ -298,7 +303,7 @@ def visit_Subscript(self, node: ast.Subscript) -> nodes.AxisBound:
class VerticalIntervalParser(IntervalParser):
"""Parse Python AST interval syntax in the form of a Slice.
- Corner cases: `ast.Ellipsis` refers to the entire interval, and
+ Corner cases: an ellipsis (`...`) constant refers to the entire interval, and
if an `ast.Subscript` is passed, this parses its slice attribute.
"""
@@ -344,7 +349,7 @@ def apply(
if isinstance(node, ast.Subscript):
raise parser.interval_error
- if isinstance(node, ast.Constant) and node.value is Ellipsis:
+ if _is_ellipsis_node(node):
interval = nodes.AxisInterval.full_interval()
interval.loc = loc
return interval
@@ -1372,7 +1377,7 @@ def _eval_index(
if any(isinstance(cn, ast.Slice) for cn in index_nodes):
raise GTScriptSyntaxError(message="Invalid target in assignment.", loc=node)
- if any(isinstance(cn, types.EllipsisType) for cn in index_nodes):
+ if any(_is_ellipsis_node(cn) for cn in index_nodes):
return None
# Determine if we are using the new-style axis syntax, or the old style.
diff --git a/src/gt4py/eve/codegen.py b/src/gt4py/eve/codegen.py
index 3869ff313b..11efe01bda 100644
--- a/src/gt4py/eve/codegen.py
+++ b/src/gt4py/eve/codegen.py
@@ -33,15 +33,11 @@
Callable,
ClassVar,
Collection,
- Dict,
Iterator,
- List,
Mapping,
Optional,
Protocol,
Sequence,
- Set,
- Tuple,
TypeVar,
Union,
overload,
@@ -52,7 +48,7 @@
SourceFormatter = Callable[[str], str]
-SOURCE_FORMATTERS: Dict[str, SourceFormatter] = {}
+SOURCE_FORMATTERS: dict[str, SourceFormatter] = {}
"""Global dict storing registered formatters."""
@@ -100,7 +96,7 @@ def format_python_source(
source: str,
*,
line_length: int = 100,
- python_versions: Optional[Set[str]] = None,
+ python_versions: Optional[set[str]] = None,
string_normalization: bool = True,
) -> str:
"""Format Python source code using black formatter."""
@@ -188,7 +184,7 @@ def format_source(language: str, source: str, *, skip_errors: bool = True, **kwa
class Name:
"""Text formatter with different case styles for symbol names in source code."""
- words: List[str]
+ words: list[str]
@classmethod
def from_string(cls, name: str, case_style: utils.CaseStyleConverter.CASE_STYLE) -> Name:
@@ -248,7 +244,7 @@ def __init__(
self.indent_size = indent_size
self.indent_char = indent_char
self.end_line = end_line
- self.lines: List[str] = []
+ self.lines: list[str] = []
def append(self, new_line: str, *, update_indent: int = 0) -> TextBlock:
if update_indent > 0:
@@ -404,7 +400,7 @@ class BaseTemplate(Template):
"""Helper class to add source location info of template definitions."""
definition: Any
- definition_loc: Optional[Tuple[str, int]]
+ definition_loc: Optional[tuple[str, int]]
def __init__(self) -> None:
self.definition_loc = None
@@ -617,7 +613,7 @@ def __init_subclass__(cls, *, inherit_templates: bool = True, **kwargs: Any) ->
if "__templates__" in cls.__dict__:
raise TypeError(f"Invalid '__templates__' member in class {cls}")
- templates: Dict[str, Template] = {}
+ templates: dict[str, Template] = {}
if inherit_templates:
for templated_gen_class in reversed(cls.__mro__[1:]):
if (
@@ -719,7 +715,7 @@ def generic_visit(self, node: RootNode, **kwargs: Any) -> Union[str, Collection[
return self.generic_dump(node, **kwargs)
- def get_template(self, node: RootNode) -> Tuple[Optional[Template], Optional[str]]:
+ def get_template(self, node: RootNode) -> tuple[Optional[Template], Optional[str]]:
"""Get a template for a node instance (see class documentation)."""
template: Optional[Template] = None
template_key = None
@@ -750,8 +746,8 @@ def render_template(
_this_module=sys.modules[type(self).__module__],
)
- def transform_children(self, node: Node, **kwargs: Any) -> Dict[str, Any]:
+ def transform_children(self, node: Node, **kwargs: Any) -> dict[str, Any]:
return {key: self.visit(value, **kwargs) for key, value in node.iter_children_items()} # type: ignore[misc]
- def transform_annexed_items(self, node: Node, **kwargs: Any) -> Dict[str, Any]:
+ def transform_annexed_items(self, node: Node, **kwargs: Any) -> dict[str, Any]:
return {key: self.visit(value, **kwargs) for key, value in node.annex.items()}
diff --git a/src/gt4py/eve/concepts.py b/src/gt4py/eve/concepts.py
index 9bd4e10ebf..2fd87ef6ac 100644
--- a/src/gt4py/eve/concepts.py
+++ b/src/gt4py/eve/concepts.py
@@ -16,20 +16,7 @@
from . import datamodels, exceptions, extended_typing as xtyping, trees, utils
from .datamodels import validators as _validators
-from .extended_typing import (
- Any,
- ClassVar,
- Dict,
- Final,
- Iterable,
- List,
- Optional,
- Set,
- Tuple,
- Type,
- TypeVar,
- Union,
-)
+from .extended_typing import Any, ClassVar, Final, Iterable, Optional, TypeVar, Union
from .type_definitions import ConstrainedStr, IntEnum, StrEnum
@@ -92,11 +79,11 @@ def __str__(self) -> str:
class SourceLocationGroup:
"""A group of merged source code locations (with optional info)."""
- locations: Tuple[SourceLocation, ...] = datamodels.field(validator=_validators.non_empty())
- context: Optional[Union[str, Tuple[str, ...]]]
+ locations: tuple[SourceLocation, ...] = datamodels.field(validator=_validators.non_empty())
+ context: Optional[Union[str, tuple[str, ...]]]
def __init__(
- self, *locations: SourceLocation, context: Optional[Union[str, Tuple[str, ...]]] = None
+ self, *locations: SourceLocation, context: Optional[Union[str, tuple[str, ...]]] = None
) -> None:
self.__auto_init__(locations=locations, context=context) # type: ignore[attr-defined] # __auto_init__ added dynamically
@@ -112,11 +99,11 @@ def __str__(self) -> str:
class AnnexManager:
- register: ClassVar[Dict[str, Any]] = {}
+ register: ClassVar[dict[str, Any]] = {}
@classmethod
def register_user(
- cls: Type[AnnexManager],
+ cls: type[AnnexManager],
key: str,
type_hint: xtyping.TypeAnnotation,
*,
@@ -193,7 +180,7 @@ def iter_children_values(self) -> Iterable:
for name in self.__datamodel_fields__.keys():
yield getattr(self, name)
- def iter_children_items(self) -> Iterable[Tuple[trees.TreeKey, Any]]:
+ def iter_children_items(self) -> Iterable[tuple[trees.TreeKey, Any]]:
for name in self.__datamodel_fields__.keys():
yield name, getattr(self, name)
@@ -209,7 +196,7 @@ def iter_children_items(self) -> Iterable[Tuple[trees.TreeKey, Any]]:
walk_items = trees.walk_items
walk_values = trees.walk_values
- def copy(self: _T, update: Dict[str, Any]) -> _T:
+ def copy(self: _T, update: dict[str, Any]) -> _T:
new_node = copy.deepcopy(self)
for k, v in update.items():
setattr(new_node, k, v)
@@ -219,7 +206,7 @@ def copy(self: _T, update: Dict[str, Any]) -> _T:
NodeT = TypeVar("NodeT", bound="Node")
ValueNode = Union[bool, bytes, int, float, str, IntEnum, StrEnum]
LeafNode = Union[NodeT, ValueNode]
-CollectionNode = Union[List[LeafNode], Dict[Any, LeafNode], Set[LeafNode]]
+CollectionNode = Union[list[LeafNode], dict[Any, LeafNode], set[LeafNode]]
RootNode = Union[NodeT, CollectionNode]
diff --git a/src/gt4py/eve/datamodels/__init__.py b/src/gt4py/eve/datamodels/__init__.py
index 5f6806c5dd..178b8ff276 100644
--- a/src/gt4py/eve/datamodels/__init__.py
+++ b/src/gt4py/eve/datamodels/__init__.py
@@ -72,7 +72,7 @@
>>> class AnotherSampleModel(DataModel):
... name: str
- ... friends: List[str]
+ ... friends: list[str]
...
... @root_validator
... def _root_validator(cls, instance):
diff --git a/src/gt4py/eve/datamodels/core.py b/src/gt4py/eve/datamodels/core.py
index c83da1a9e3..b67efdddcf 100644
--- a/src/gt4py/eve/datamodels/core.py
+++ b/src/gt4py/eve/datamodels/core.py
@@ -37,18 +37,14 @@
Any,
Callable,
ClassVar,
- Dict,
Final,
ForwardRef,
Generator,
- List,
Literal,
Mapping,
Optional,
Protocol,
Sequence,
- Tuple,
- Type,
TypeAlias,
TypeAnnotation,
TypeVar,
@@ -73,7 +69,7 @@
class _AttrsClassTP(Protocol):
- __attrs_attrs__: ClassVar[Tuple[attr.Attribute, ...]] = ()
+ __attrs_attrs__: ClassVar[tuple[attr.Attribute, ...]] = ()
Attribute: TypeAlias = attr.Attribute
@@ -89,7 +85,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ...
utils.FrozenNamespace[Attribute], None
)
__datamodel_root_validators__: ClassVar[
- Tuple[xtyping.NonDataDescriptor[DataModelTP, BoundRootValidator], ...]
+ tuple[xtyping.NonDataDescriptor[DataModelTP, BoundRootValidator], ...]
] = ()
# Optional
__auto_init__: ClassVar[Callable[..., None]] = cast(Callable[..., None], None)
@@ -122,12 +118,12 @@ def __subclasshook__(cls, subclass: type) -> bool:
class GenericDataModelTP(DataModelTP, Protocol):
- __args__: ClassVar[Tuple[Union[Type, TypeVar], ...]] = ()
- __parameters__: ClassVar[Tuple[TypeVar, ...]] = ()
+ __args__: ClassVar[tuple[Union[type[Any], TypeVar], ...]] = ()
+ __parameters__: ClassVar[tuple[TypeVar, ...]] = ()
@classmethod
def __class_getitem__(
- cls: Type[GenericDataModelTP], args: Union[Type, Tuple[Type, ...]]
+ cls: type[GenericDataModelTP], args: Union[type[Any], tuple[type[Any], ...]]
) -> Union[DataModelTP, GenericDataModelTP]: ...
@@ -139,7 +135,7 @@ def __class_getitem__(
FieldValidator = Callable[[_DM, Attribute, _T], None]
BoundFieldValidator = Callable[[Attribute, _T], None]
-RootValidator = Callable[[Type[_DM], _DM], None]
+RootValidator = Callable[[type[_DM], _DM], None]
BoundRootValidator = Callable[[_DM], None]
FieldTypeValidatorFactory = Callable[[TypeAnnotation, str], FieldValidator]
@@ -288,12 +284,12 @@ def datamodel(
coerce: bool = _COERCE_DEFAULT,
generic: bool = _GENERIC_DEFAULT,
type_validation_factory: Optional[FieldTypeValidatorFactory] = DefaultFieldTypeValidatorFactory,
-) -> Callable[[Type[_T]], Type[_T]]: ...
+) -> Callable[[type[_T]], type[_T]]: ...
@overload
def datamodel( # redefinition of unused symbol
- cls: Type[_T],
+ cls: type[_T],
/,
*,
repr: bool = _REPR_DEFAULT,
@@ -307,12 +303,12 @@ def datamodel( # redefinition of unused symbol
coerce: bool = _COERCE_DEFAULT,
generic: bool = _GENERIC_DEFAULT,
type_validation_factory: Optional[FieldTypeValidatorFactory] = DefaultFieldTypeValidatorFactory,
-) -> Type[_T]: ...
+) -> type[_T]: ...
# TODO(egparedes): Use @dataclass_transform(eq_default=True, field_specifiers=("field",))
def datamodel( # redefinition of unused symbol
- cls: Optional[Type[_T]] = None,
+ cls: Optional[type[_T]] = None,
/,
*,
repr: bool = _REPR_DEFAULT, # noqa: A002 [builtin-argument-shadowing]
@@ -326,7 +322,7 @@ def datamodel( # redefinition of unused symbol
coerce: bool = _COERCE_DEFAULT,
generic: bool = _GENERIC_DEFAULT,
type_validation_factory: Optional[FieldTypeValidatorFactory] = DefaultFieldTypeValidatorFactory,
-) -> Union[Type[_T], Callable[[Type[_T]], Type[_T]]]:
+) -> Union[type[_T], Callable[[type[_T]], type[_T]]]:
"""Add generated special methods to classes according to the specified attributes (class decorator).
It converts the class to an `attrs `_ with some extra features.
@@ -357,10 +353,12 @@ def datamodel( # redefinition of unused symbol
frozen: If ``True`` (default is ``False``), assigning to fields will generate an exception.
This emulates read-only frozen instances. The ``__setattr__()`` and
``__delattr__()`` methods should not be defined in the class.
+ The special value ``"strict"`` additionally requires every field annotation
+ to stand for strictly immutable values (other ``frozen="strict"`` datamodels
+ or types defining a custom ``__hash__``), raising ``EveTypeError`` otherwise.
match_args: If ``True`` (default) and ``__match_args__`` is not already defined in the class,
set ``__match_args__`` on the class to support PEP 634 (Structural Pattern Matching).
- It is a tuple of all positional-only ``__init__`` parameter names on
- Python 3.10 and later. Ignored on older Python versions.
+ It is a tuple of all positional-only ``__init__`` parameter names.
kw_only: If ``True`` (default is ``False``), make all fields keyword-only in the generated
``__init__`` (if ``init`` is ``False``, this parameter is ignored).
slots: slots: If ``True`` (the default is ``False``), ``__slots__`` attribute will be generated
@@ -401,7 +399,7 @@ def datamodel( # redefinition of unused symbol
class _DataModelDecoratorTP(Protocol[_T]):
def __call__(
self,
- cls: Optional[Type[_T]] = None,
+ cls: Optional[type[_T]] = None,
/,
*,
repr: bool = _REPR_DEFAULT, # noqa: A002 [builtin-argument-shadowing]
@@ -416,7 +414,7 @@ def __call__(
type_validation_factory: Optional[
FieldTypeValidatorFactory
] = DefaultFieldTypeValidatorFactory,
- ) -> Union[Type[_T], Callable[[Type[_T]], Type[_T]]]: ...
+ ) -> Union[type[_T], Callable[[type[_T]], type[_T]]]: ...
frozenmodel: _DataModelDecoratorTP = functools.partial(datamodel, frozen=True)
@@ -560,10 +558,9 @@ def field(
Examples:
- >>> from typing import List
>>> @datamodel
... class C:
- ... mylist: List[int] = field(default_factory=lambda: [1, 2, 3])
+ ... mylist: list[int] = field(default_factory=lambda: [1, 2, 3])
>>> c = C()
>>> c.mylist
[1, 2, 3]
@@ -642,25 +639,24 @@ def is_datamodel(obj: Any) -> bool:
return hasattr(cls, MODEL_FIELD_DEFINITIONS_ATTR)
-def is_generic_datamodel_class(cls: Type) -> bool:
+def is_generic_datamodel_class(cls: type[Any]) -> bool:
"""Return ``True`` if `obj` is a generic Data Model class with type parameters."""
assert isinstance(cls, type)
return is_datamodel(cls) and xtyping.has_type_parameters(cls)
-def get_fields(model: Union[DataModel, Type[DataModel]]) -> utils.FrozenNamespace:
+def get_fields(model: Union[DataModel, type[DataModel]]) -> utils.FrozenNamespace:
"""Return the field meta-information of a Data Model.
Arguments:
model: A Data Model class or instance.
Examples:
- >>> from typing import List
>>> @datamodel
... class Model:
... name: str
... amount: int = 1
- ... numbers: List[float] = field(default_factory=list)
+ ... numbers: list[float] = field(default_factory=list)
>>> fields(Model) # doctest:+ELLIPSIS
FrozenNamespace(...name=Attribute(name='name', default=NOTHING, ...
@@ -681,8 +677,8 @@ def get_fields(model: Union[DataModel, Type[DataModel]]) -> utils.FrozenNamespac
def asdict(
instance: DataModel,
*,
- value_serializer: Optional[Callable[[Type[DataModel], Attribute, Any], Any]] = None,
-) -> Dict[str, Any]:
+ value_serializer: Optional[Callable[[type[DataModel], Attribute, Any], Any]] = None,
+) -> dict[str, Any]:
"""Return the contents of a Data Model instance as a new mapping from field names to values.
Arguments:
@@ -705,7 +701,7 @@ def asdict(
return attrs.asdict(instance, value_serializer=value_serializer)
-def astuple(instance: DataModel) -> Tuple[Any, ...]:
+def astuple(instance: DataModel) -> tuple[Any, ...]:
"""Return the contents of a Data Model instance as a new tuple of field values.
Arguments:
@@ -735,8 +731,8 @@ def astuple(instance: DataModel) -> Tuple[Any, ...]:
def update_forward_refs(
- model_cls: Type[_DataModelT], localns: Optional[Dict[str, Any]] = None
-) -> Type[_DataModelT]:
+ model_cls: type[_DataModelT], localns: Optional[dict[str, Any]] = None
+) -> type[_DataModelT]:
"""Update Data Model class meta-information replacing forwarded type annotations with actual types.
Arguments:
@@ -782,14 +778,14 @@ def update_forward_refs(
def concretize(
- datamodel_cls: Type[GenericDataModelT],
+ datamodel_cls: type[GenericDataModelT],
/,
- *type_args: Type,
+ *type_args: type[Any],
class_name: Optional[str] = None,
module: Optional[str] = None,
support_pickling: bool = True,
overwrite_definition: bool = True,
-) -> Type[DataModelT]:
+) -> type[DataModelT]:
"""Generate a new concrete subclass of a generic Data Model.
Arguments:
@@ -809,7 +805,7 @@ def concretize(
the target module will be overwritten.
"""
- concrete_cls: Type[DataModelT] = _make_concrete_with_cache(
+ concrete_cls: type[DataModelT] = _make_concrete_with_cache(
datamodel_cls, # type: ignore[arg-type]
*type_args,
class_name=class_name,
@@ -836,7 +832,7 @@ def concretize(
# -- Helpers --
-def _collect_field_validators(cls: Type) -> Dict[str, FieldValidator]:
+def _collect_field_validators(cls: type[Any]) -> dict[str, FieldValidator]:
result = {}
for member in cls.__dict__.values():
if hasattr(member, _FIELD_VALIDATOR_TAG):
@@ -847,7 +843,7 @@ def _collect_field_validators(cls: Type) -> Dict[str, FieldValidator]:
return result
-def _collect_root_validators(cls: Type) -> List[RootValidator]:
+def _collect_root_validators(cls: type[Any]) -> list[RootValidator]:
result = []
for base in reversed(cls.__mro__[1:]):
for validator in getattr(base, MODEL_ROOT_VALIDATORS_ATTR, []):
@@ -863,7 +859,7 @@ def _collect_root_validators(cls: Type) -> List[RootValidator]:
def _get_attribute_from_bases(
- name: str, mro: Tuple[Type, ...], annotations: Optional[Dict[str, Any]] = None
+ name: str, mro: tuple[type[Any], ...], annotations: Optional[dict[str, Any]] = None
) -> Optional[Attribute]:
for base in mro:
for base_field_attrib in getattr(base, "__attrs_attrs__", []):
@@ -876,8 +872,8 @@ def _get_attribute_from_bases(
def _substitute_typevars(
- type_hint: Type, type_params_map: Mapping[TypeVar, Union[Type, TypeVar]]
-) -> Tuple[Union[Type, TypeVar], bool]:
+ type_hint: type[Any], type_params_map: Mapping[TypeVar, Union[type[Any], TypeVar]]
+) -> tuple[Union[type[Any], TypeVar], bool]:
if isinstance(type_hint, typing.TypeVar):
assert type_hint in type_params_map
return type_params_map[type_hint], True
@@ -962,14 +958,14 @@ def __pretty__(
def _make_data_model_class_getitem() -> classmethod:
def __class_getitem__(
- cls: Type[GenericDataModelT], args: Union[Type, Tuple[Type]]
- ) -> Type[DataModelT] | Type[GenericDataModelT]:
+ cls: type[GenericDataModelT], args: Union[type[Any], tuple[type[Any]]]
+ ) -> type[DataModelT] | type[GenericDataModelT]:
"""Return an instance compatible with aliases created by :class:`typing.Generic` classes.
See :class:`GenericDataModelAlias` for further information.
"""
- type_args: Tuple[Type] = args if isinstance(args, tuple) else (args,)
- concrete_cls: Type[DataModelT] = concretize(cls, *type_args)
+ type_args: tuple[type[Any]] = args if isinstance(args, tuple) else (args,)
+ concrete_cls: type[DataModelT] = concretize(cls, *type_args)
return concrete_cls
return classmethod(__class_getitem__)
@@ -1056,9 +1052,119 @@ def _type_converter(value: Any) -> _T:
_KNOWN_MUTABLE_TYPES: Final = (list, dict, set)
+#: Hashable container types folding the hashes of the items they hold into their own.
+#: Used unparametrized they say nothing about those items, so they are only accepted
+#: as the origin of a parametrized alias (``tuple[int, ...]``), never on their own.
+_ITEM_HASHING_CONTAINER_TYPES: Final = (tuple, frozenset)
+
+
+def _is_strictly_immutable_type(
+ type_annotation: TypeAnnotation, *, _as_container_origin: bool = False
+) -> bool:
+ """Check whether an annotation only admits strictly immutable (hashable) values.
+
+ A datamodel qualifies only if it is itself defined with ``frozen="strict"``.
+ Any other plain type is judged by its ``__hash__``, which is the only signal
+ the language gives here: a type that defines its own is claiming its values
+ hash by content, which they can only do if they do not change, while the
+ mutable builtins opt out by setting ``__hash__ = None`` and the inherited
+ ``object.__hash__`` just hashes by identity and says nothing about the value.
+ So neither of those two counts, and everything else does.
+
+ Composite annotations are decomposed and every part is checked with the same
+ rules, so that neither wrapping a type in a union nor hiding it in a generic
+ container (whose hash folds in the hashes of its items) weakens the check.
+ Annotations that cannot be resolved to any concrete type (``Any``, unbound
+ type variables, unresolved forward references) are conservatively rejected.
+
+ Arguments:
+ type_annotation: The annotation to check.
+
+ Keyword Arguments:
+ _as_container_origin: Internal flag set when checking the origin of a
+ parametrized alias, whose type arguments are checked separately.
+
+ Returns:
+ ``True`` if every value admitted by the annotation is strictly immutable.
+ """
+ if xtyping.is_type_alias(type_annotation) or xtyping.get_origin(type_annotation) is not None:
+ # A PEP 695 alias stands for the annotation it resolves to, so check that
+ # instead; an alias whose value cannot be evaluated (undefined name, recursive,
+ # ...) proves nothing and is rejected below like any other unresolved
+ # annotation. Non-aliases are returned unchanged, as the identical object.
+ try:
+ resolved_alias = xtyping.eval_type_alias(type_annotation)
+ except (NameError, TypeError):
+ return False
+ if resolved_alias is not type_annotation:
+ return _is_strictly_immutable_type(
+ resolved_alias, _as_container_origin=_as_container_origin
+ )
+
+ if is_datamodel(type_annotation):
+ return getattr(type_annotation, MODEL_PARAM_DEFINITIONS_ATTR).strict_frozen is True
+
+ origin_type = xtyping.get_origin(type_annotation)
+ type_args = xtyping.get_args(type_annotation)
+
+ if origin_type is Literal:
+ # 'Literal' arguments are values, not types.
+ return all(xtyping.is_type_with_custom_hash(type(arg)) for arg in type_args)
+
+ if origin_type is Union or origin_type is types.UnionType:
+ # 'Union[A, B]' and 'A | B' are different runtime objects before Python 3.14.
+ # A union is immutable only if every one of its members is.
+ return bool(type_args) and all(map(_is_strictly_immutable_type, type_args))
+
+ if origin_type is not None:
+ if not type_args:
+ # Unparametrized alias ('typing.Tuple', 'typing.List', ...): it says nothing
+ # more than the bare origin type does.
+ return _is_strictly_immutable_type(origin_type)
+
+ # Parametrized generic alias ('tuple[int, ...]', 'list[int]', ...). The alias
+ # itself is a hashable object, so it has to be decomposed: both the container
+ # type and every type argument must be strictly immutable, since the hash of
+ # a container folds in the hashes of the items it holds.
+ return _is_strictly_immutable_type(origin_type, _as_container_origin=True) and all(
+ _is_strictly_immutable_type(arg) for arg in type_args if arg is not Ellipsis
+ )
+
+ if xtyping.is_actual_type(type_annotation): # plain type, already known not to be a datamodel
+ if not _as_container_origin and issubclass(type_annotation, _ITEM_HASHING_CONTAINER_TYPES):
+ # Unparametrized container ('tuple', 'frozenset', a 'NamedTuple', ...): its
+ # hash folds in the hashes of items which nothing here proves immutable, the
+ # same reason why 'tuple[Any, ...]' is rejected.
+ return False
+ return xtyping.is_type_with_custom_hash(type_annotation)
+
+ if isinstance(type_annotation, TypeVar):
+ # Check the concrete annotations the type variable can stand for. Note that the
+ # bound/constraints are checked as annotations, not flattened to their origins,
+ # so that e.g. a 'tuple[list[int], ...]' bound is still decomposed.
+ if type_annotation.__bound__ is not None:
+ return _is_strictly_immutable_type(type_annotation.__bound__)
+ if type_annotation.__constraints__:
+ return all(map(_is_strictly_immutable_type, type_annotation.__constraints__))
+ if (typevar_default := getattr(type_annotation, "__default__", None)) is not None:
+ return _is_strictly_immutable_type(typevar_default)
+ return False
+
+ if isinstance(type_annotation, (ForwardRef, typing.ForwardRef)):
+ # Forward references that cannot be resolved at this point do not prove
+ # anything, so they are rejected instead of propagating the resolution error.
+ try:
+ resolved_annotation = xtyping.eval_forward_ref(type_annotation)
+ except Exception:
+ return False
+ return _is_strictly_immutable_type(resolved_annotation)
+
+ # Anything else ('Any', special forms, ...) proves nothing.
+ return False
+
def _make_datamodel(
- cls: Type[_T],
+ cls: type[_T],
*,
repr: bool, # noqa: A002 [builtin-argument-shadowing]
eq: bool,
@@ -1072,13 +1178,13 @@ def _make_datamodel(
generic: bool | Literal["True_no_checks"],
type_validation_factory: Optional[FieldTypeValidatorFactory],
_stacklevel_offset: int = 0,
-) -> Type[_T]:
+) -> type[_T]:
"""Actual implementation of the Data Model creation.
See :func:`datamodel` for the description of the parameters.
"""
- mro_bases: Tuple[Type, ...] = cls.__mro__[1:]
+ mro_bases: tuple[type[Any], ...] = cls.__mro__[1:]
if "__annotations__" not in cls.__dict__ and "__annotate_func__" not in cls.__dict__:
cls.__annotations__ = {}
@@ -1251,18 +1357,16 @@ def _make_datamodel(
# Final checks and postprocessing
if strict_frozen:
- unhashable_fields = set()
- for f_attr in new_cls.__attrs_attrs__:
- if is_datamodel(f_attr.type):
- if getattr(f_attr.type, MODEL_PARAM_DEFINITIONS_ATTR).strict_frozen is True:
- continue
- elif xtyping.is_hashable_type(f_attr.type):
- continue
- unhashable_fields.add(f_attr.name)
-
- if unhashable_fields:
+ mutable_fields = [
+ f_attr.name
+ for f_attr in new_cls.__attrs_attrs__
+ if not _is_strictly_immutable_type(f_attr.type)
+ ]
+ if mutable_fields:
+ names = ", ".join(f"'{name}'" for name in mutable_fields)
raise exceptions.EveTypeError(
- f"Some fields ({unhashable_fields}) can not be considered strictly immutable."
+ f"Fields ({names}) of datamodel '{new_cls.__name__}' "
+ "can not be considered strictly immutable."
)
if "__attrs_init__" in new_cls.__dict__:
@@ -1305,11 +1409,11 @@ def __get__(self, instance: Any, owner_class: type | None = None) -> str | None:
@utils.optional_lru_cache(maxsize=None, typed=True)
def _make_concrete_with_cache(
- datamodel_cls: Type[GenericDataModelT],
- *type_args: Type,
+ datamodel_cls: type[GenericDataModelT],
+ *type_args: type[Any],
class_name: Optional[str] = None,
module: Optional[str] = None,
-) -> Type[DataModelT]:
+) -> type[DataModelT]:
if not is_generic_datamodel_class(datamodel_cls):
raise TypeError(f"'{datamodel_cls.__name__}' is not a generic model class.")
for t in type_args:
@@ -1413,7 +1517,7 @@ class FrozenModel(DataModel, frozen=True):
class GenericDataModel(GenericDataModelTP):
@classmethod
def __class_getitem__(
- cls: Type[GenericDataModelTP], args: Union[Type, Tuple[Type, ...]]
+ cls: type[GenericDataModelTP], args: Union[type[Any], tuple[type[Any], ...]]
) -> Union[DataModelTP, GenericDataModelTP]: ...
else:
diff --git a/src/gt4py/eve/exceptions.py b/src/gt4py/eve/exceptions.py
index d240f2ffe8..eea27d0f6a 100644
--- a/src/gt4py/eve/exceptions.py
+++ b/src/gt4py/eve/exceptions.py
@@ -10,7 +10,7 @@
from __future__ import annotations
-from .extended_typing import Any, Dict, Optional
+from .extended_typing import Any, Optional
class EveError:
@@ -25,7 +25,7 @@ class EveError:
"""
message_template = "Generic Eve error [{info}]"
- info: Dict[str, Any]
+ info: dict[str, Any]
def __init__(self, message: Optional[str] = None, **kwargs: Any) -> None:
self.info = kwargs
diff --git a/src/gt4py/eve/extended_typing.py b/src/gt4py/eve/extended_typing.py
index 4c0debe45a..17fb667132 100644
--- a/src/gt4py/eve/extended_typing.py
+++ b/src/gt4py/eve/extended_typing.py
@@ -17,6 +17,7 @@
# ruff: noqa: F401, F405
import abc as _abc
import array as _array
+import builtins as _builtins
import collections.abc as _collections_abc
import dataclasses as _dataclasses
import functools as _functools
@@ -34,53 +35,100 @@
from typing_extensions import * # type: ignore[assignment,no-redef] # noqa: F403 [undefined-local-with-import-star]
-if _sys.version_info >= (3, 9):
- # Standard library already supports PEP 585 (Type Hinting Generics In Standard Collections)
- from builtins import ( # type: ignore[assignment]
- dict as Dict,
- frozenset as FrozenSet,
- list as List,
- set as Set,
- tuple as Tuple,
- type as Type,
- )
- from collections import (
- ChainMap as ChainMap,
- Counter as Counter,
- OrderedDict as OrderedDict,
- defaultdict as defaultdict,
- deque as deque,
- )
- from collections.abc import (
- AsyncGenerator as AsyncGenerator,
- AsyncIterable as AsyncIterable,
- AsyncIterator as AsyncIterator,
- Awaitable as Awaitable,
- ByteString as ByteString,
- Callable as Callable,
- Collection as Collection,
- Container as Container,
- Coroutine as Coroutine,
- Generator as Generator,
- ItemsView as ItemsView,
- Iterable as Iterable,
- Iterator as Iterator,
- KeysView as KeysView,
- Mapping as Mapping,
- MappingView as MappingView,
- MutableMapping as MutableMapping,
- MutableSequence as MutableSequence,
- MutableSet as MutableSet,
- Reversible as Reversible,
- Sequence as Sequence,
- Set as AbstractSet,
- ValuesView as ValuesView,
- )
- from contextlib import (
- AbstractAsyncContextManager as AsyncContextManager,
- AbstractContextManager as ContextManager,
- )
- from re import Match as Match, Pattern as Pattern
+# Re-export the standard collection types under the names the star imports above bind to
+# their deprecated 'typing' counterparts, so that e.g. 'Sequence' is
+# 'collections.abc.Sequence' rather than 'typing.Sequence'. This block must stay *below*
+# the star imports, since it deliberately rebinds names those imports also define; the
+# 'isort: split' marker keeps the import sorter from hoisting it. The builtin generics
+# are deliberately not re-exported under their old 'typing' spellings; see
+# '_DEPRECATED_TYPING_ALIASES' below.
+# isort: split
+from collections import (
+ ChainMap as ChainMap,
+ Counter as Counter,
+ OrderedDict as OrderedDict,
+ defaultdict as defaultdict,
+ deque as deque,
+)
+from collections.abc import (
+ AsyncGenerator as AsyncGenerator,
+ AsyncIterable as AsyncIterable,
+ AsyncIterator as AsyncIterator,
+ Awaitable as Awaitable,
+ ByteString as ByteString,
+ Callable as Callable,
+ Collection as Collection,
+ Container as Container,
+ Coroutine as Coroutine,
+ Generator as Generator,
+ ItemsView as ItemsView,
+ Iterable as Iterable,
+ Iterator as Iterator,
+ KeysView as KeysView,
+ Mapping as Mapping,
+ MappingView as MappingView,
+ MutableMapping as MutableMapping,
+ MutableSequence as MutableSequence,
+ MutableSet as MutableSet,
+ Reversible as Reversible,
+ Sequence as Sequence,
+ Set as AbstractSet,
+ ValuesView as ValuesView,
+)
+from contextlib import (
+ AbstractAsyncContextManager as AsyncContextManager,
+ AbstractContextManager as ContextManager,
+)
+from re import Match as Match, Pattern as Pattern
+
+
+# The 'typing' aliases of the builtin generics are deprecated since PEP 585 and are no
+# longer re-exported: use the builtin spelling instead. They are not simply absent from
+# this module -- the star imports above bind them, and '__getattr__' below would happily
+# forward them to 'typing' -- so they are dropped from the namespace here and rejected
+# explicitly. Without this, removing them would silently downgrade every use site from
+# the builtin to the deprecated 'typing' object. Note that this is a runtime guarantee
+# only: a type checker still resolves the names through the star imports.
+_DEPRECATED_TYPING_ALIASES: Final[Mapping[str, str]] = {
+ "Dict": "dict",
+ "FrozenSet": "frozenset",
+ "List": "list",
+ "Set": "set",
+ "Tuple": "tuple",
+ "Type": "type",
+}
+
+for _alias in _DEPRECATED_TYPING_ALIASES:
+ globals().pop(_alias, None)
+del _alias
+
+# The names are still valid in user-written annotations, and resolving a forward
+# reference through this module has always normalized them to the builtin generic
+# ('typing.List[int]' -> 'list[int]'). Keep that mapping available to the forward-ref
+# machinery below, which would otherwise either raise or hand back the deprecated
+# 'typing' object.
+_DEPRECATED_ALIAS_REPLACEMENTS: Final[Mapping[str, Any]] = {
+ name: getattr(_builtins, replacement)
+ for name, replacement in _DEPRECATED_TYPING_ALIASES.items()
+}
+
+
+class _ForwardRefTypingNamespace:
+ """Namespace bound to the name 'typing' while evaluating forward references.
+
+ Annotations resolve through this module, so that 'typing_extensions' definitions
+ take priority and the standard collection types are used. The deprecated builtin
+ aliases are not re-exported here, but they stay valid in user-written annotations,
+ so 'typing.List[int]' resolves to 'list[int]' rather than raising.
+ """
+
+ def __getattr__(self, name: str) -> Any:
+ if (replacement := _DEPRECATED_ALIAS_REPLACEMENTS.get(name)) is not None:
+ return replacement
+ return getattr(_sys.modules[__name__], name)
+
+
+_FORWARD_REF_TYPING_NS: Final = _ForwardRefTypingNamespace()
# These fallbacks are useful for public symbols not exported by default.
@@ -90,6 +138,12 @@ def __getattr__(name: str) -> Any:
import typing_extensions
+ if (replacement := _DEPRECATED_TYPING_ALIASES.get(name)) is not None:
+ raise AttributeError(
+ f"'{name}' is a deprecated 'typing' alias (PEP 585) and is not exported by"
+ f" '{__name__}'. Use '{replacement}' instead."
+ )
+
result = SENTINEL = object()
if not (name.startswith("__") and name.endswith("__")):
result = getattr(typing_extensions, name, SENTINEL)
@@ -106,16 +160,16 @@ def __getattr__(name: str) -> Any:
return result
-def __dir__() -> List[str]:
+def __dir__() -> list[str]:
if not hasattr(self_func := (globals()["__dir__"]), "__cached_dir"):
import typing
import typing_extensions
- orig_dir = typing.__dir__()
- self_func.__cached_dir = [*orig_dir] + [
- name for name in typing_extensions.__dir__() if name not in orig_dir
- ]
+ # Everything reachable through '__getattr__' plus this module's own definitions,
+ # minus the aliases '__getattr__' explicitly rejects.
+ names = {*typing.__dir__(), *typing_extensions.__dir__(), *globals()}
+ self_func.__cached_dir = sorted(names - _DEPRECATED_TYPING_ALIASES.keys())
return self_func.__cached_dir
@@ -133,8 +187,8 @@ def __call__(self, *args: _A) -> _R: ...
_T_co = TypeVar("_T_co", covariant=True)
NestedSequence = Sequence[Union[_T_co, "NestedSequence[_T_co]"]]
-NestedList = List[Union[_T_co, "NestedList[_T_co]"]]
-NestedTuple = Tuple[Union[_T_co, "NestedTuple[_T_co]"], ...]
+NestedList = list[Union[_T_co, "NestedList[_T_co]"]]
+NestedTuple = tuple[Union[_T_co, "NestedTuple[_T_co]"], ...]
MaybeNested = Union[_T_co, NestedSequence[_T_co]]
MaybeNestedInSequence = Union[_T_co, NestedSequence[_T_co]]
@@ -159,7 +213,7 @@ def is_maybe_nested_in_tuple_of(
# -- Typing annotations --
SingleTypeAnnotation = Union[
- Type,
+ type[Any],
_types.GenericAlias,
_typing._BaseGenericAlias, # type: ignore[name-defined] # _BaseGenericAlias is not exported in stub
# Both PEP 695 type alias implementations, for the same reason as in `_TypeAliasTypes`
@@ -172,13 +226,13 @@ def is_maybe_nested_in_tuple_of(
TypeAnnotation = Union[ForwardRef, SolvedTypeAnnotation]
SourceTypeAnnotation = Union[str, TypeAnnotation]
-StdGenericAliasType: Final[Type] = type(List[int])
+StdGenericAliasType: Final[type[Any]] = type(list[int])
if TYPE_CHECKING:
StdGenericAlias: TypeAlias = _types.GenericAlias
-_TypingSpecialFormType: Final[Type] = _typing._SpecialForm
-_TypingGenericAliasType: Final[Type] = _typing._BaseGenericAlias # type: ignore[attr-defined] # _BaseGenericAlias / _GenericAlias are not exported in stub
+_TypingSpecialFormType: Final[type[Any]] = _typing._SpecialForm
+_TypingGenericAliasType: Final[type[Any]] = _typing._BaseGenericAlias # type: ignore[attr-defined] # _BaseGenericAlias / _GenericAlias are not exported in stub
# -- Standard Python protocols --
@@ -194,14 +248,14 @@ class NonDataDescriptor(Protocol[_C, _V]):
@overload
def __get__(
- self, _instance: Literal[None], _owner_type: Optional[Type[_C]] = None
+ self, _instance: Literal[None], _owner_type: Optional[type[_C]] = None
) -> NonDataDescriptor[_C, _V]: ...
@overload
- def __get__(self, _instance: _C, _owner_type: Optional[Type[_C]] = None) -> _V: ...
+ def __get__(self, _instance: _C, _owner_type: Optional[type[_C]] = None) -> _V: ...
def __get__(
- self, _instance: Optional[_C], _owner_type: Optional[Type[_C]] = None
+ self, _instance: Optional[_C], _owner_type: Optional[type[_C]] = None
) -> _V | NonDataDescriptor[_C, _V]: ...
@@ -302,15 +356,15 @@ def supports_array(value: Any) -> TypeGuard[SupportsArray]:
class ArrayInterface(Protocol):
@property
- def __array_interface__(self) -> Dict[str, Any]: ...
+ def __array_interface__(self) -> dict[str, Any]: ...
class ArrayInterfaceTypedDict(TypedDict):
- shape: Tuple[int, ...]
+ shape: tuple[int, ...]
typestr: str
- descr: NotRequired[List[Tuple]]
- data: NotRequired[Tuple[int, bool]]
- strides: NotRequired[Optional[Tuple[int, ...]]]
+ descr: NotRequired[list[tuple]]
+ data: NotRequired[tuple[int, bool]]
+ strides: NotRequired[Optional[tuple[int, ...]]]
mask: NotRequired[Optional["StrictArrayInterface"]]
offset: NotRequired[int]
version: int
@@ -327,16 +381,16 @@ def supports_array_interface(value: Any) -> TypeGuard[ArrayInterface]:
class CUDAArrayInterface(Protocol):
@property
- def __cuda_array_interface__(self) -> Dict[str, Any]: ...
+ def __cuda_array_interface__(self) -> dict[str, Any]: ...
class CUDAArrayInterfaceTypedDict(TypedDict):
- shape: Tuple[int, ...]
+ shape: tuple[int, ...]
typestr: str
- data: Tuple[int, bool]
+ data: tuple[int, bool]
version: int
- strides: NotRequired[Optional[Tuple[int, ...]]]
- descr: NotRequired[List[Tuple]]
+ strides: NotRequired[Optional[tuple[int, ...]]]
+ descr: NotRequired[list[tuple]]
mask: NotRequired[Optional["StrictCUDAArrayInterface"]]
stream: NotRequired[Optional[int]]
@@ -351,7 +405,7 @@ def supports_cuda_array_interface(value: Any) -> TypeGuard[CUDAArrayInterface]:
return hasattr(value, "__cuda_array_interface__")
-DLPackDevice = Tuple[int, int]
+DLPackDevice = tuple[int, int]
class MultiStreamDLPackBuffer(Protocol):
@@ -385,20 +439,10 @@ def __pretty__(
# -- Added functionality --
-_ArtefactTypes: tuple[type, ...] = (_types.GenericAlias,)
-
-# `Any` is a class since Python 3.11
-if isinstance(_typing.Any, type): # Python >= 3.11
- _ArtefactTypes = (*_ArtefactTypes, _typing.Any)
-
-# `Any` is a class since typing_extensions >= 4.4 and Python 3.11
-if (typing_exts_any := getattr(_typing_extensions, "Any", None)) is not _typing.Any and isinstance(
- typing_exts_any, type
-):
- _ArtefactTypes = (*_ArtefactTypes, typing_exts_any)
+_ArtefactTypes: Final[tuple[type, ...]] = (_types.GenericAlias, _typing.Any)
-def is_actual_type(obj: Any) -> TypeGuard[Type]:
+def is_actual_type(obj: Any) -> TypeGuard[type[Any]]:
"""Check if an object has an actual type and instead of a typing artefact like ``GenericAlias`` or ``Any``.
This is needed because since Python 3.9:
@@ -518,25 +562,19 @@ def eval_type_alias(annotation: Any) -> Any:
)
-if hasattr(_typing_extensions, "Any") and _typing.Any is not _typing_extensions.Any: # type: ignore[attr-defined] # _typing_extensions.Any only from >= 4.4
- # When using Python < 3.11 and typing_extensions >= 4.4 there are
- # two different implementations of `Any`
+def is_Any(obj: Any) -> bool:
+ """Check if an object is the ``Any`` special form."""
+ # 'typing_extensions' re-exports 'typing.Any' on every supported version, so the
+ # two implementations that used to exist below the 3.11 floor are now one object.
+ return obj is _typing.Any
- def is_Any(obj: Any) -> bool:
- return obj is _typing.Any or obj is _typing_extensions.Any # type: ignore[attr-defined] # _typing_extensions.Any only from >= 4.4
-else:
-
- def is_Any(obj: Any) -> bool:
- return obj is _typing.Any
-
-
-def has_type_parameters(cls: Type) -> bool:
+def has_type_parameters(cls: type[Any]) -> bool:
"""Return ``True`` if obj is a generic class with type parameters."""
return issubclass(cls, Generic) and len(getattr(cls, "__parameters__", [])) > 0 # type: ignore[arg-type] # Generic not considered as a class
-def get_actual_type(obj: _T) -> Type[_T]:
+def get_actual_type(obj: _T) -> type[_T]:
"""Return type of an object (also working for GenericAlias instances which pretend to be an actual type)."""
return StdGenericAliasType if isinstance(obj, StdGenericAliasType) else type(obj)
@@ -544,8 +582,8 @@ def get_actual_type(obj: _T) -> Type[_T]:
def get_represented_types(
type_annotation: TypeAnnotation,
*,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
) -> tuple[type, ...]:
"""Return a tuple with all the actual types contained in a type annotation."""
recurse = _functools.partial(get_represented_types, globalns=globalns, localns=localns)
@@ -588,7 +626,7 @@ def recurse_all(annotations: Iterable[TypeAnnotation]) -> tuple[type, ...]:
return ()
-def is_type_with_custom_hash(type_: Type) -> bool:
+def is_type_with_custom_hash(type_: type[Any]) -> bool:
return type_.__hash__ not in (None, object.__hash__)
@@ -739,10 +777,10 @@ def get_partial_type_hints(
_types.MethodWrapperType,
_types.MethodDescriptorType,
],
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
include_extras: bool = False,
-) -> Dict[str, Union[Type, ForwardRef]]:
+) -> dict[str, Union[type[Any], ForwardRef]]:
"""Return a dictionary with type hints (using forward refs for undefined names) for a function, method, module or class object.
For each member type hint in the object a :class:`typing.ForwardRef` instance will be
@@ -756,7 +794,7 @@ def get_partial_type_hints(
obj, globalns=globalns, localns=localns, include_extras=include_extras
)
- hints: Dict[str, Union[Type, ForwardRef]] = {}
+ hints: dict[str, Union[type[Any], ForwardRef]] = {}
annotations = getattr(obj, "__annotations__", {})
for name, hint in annotations.items():
obj.__annotations__ = {name: hint}
@@ -783,8 +821,8 @@ def get_partial_type_hints(
def eval_forward_ref(
ref: Union[str, ForwardRef],
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
*,
include_extras: bool = False,
) -> SolvedTypeAnnotation:
@@ -798,9 +836,8 @@ def eval_forward_ref(
include_extras: if ``True``, ``Annotated`` hints will preserve the annotation.
Examples:
- >>> from typing import Dict, Tuple
- >>> print("Result:", eval_forward_ref("Dict[str, Tuple[int, float]]"))
- Result: ...ict[str, ...uple[int, float]]
+ >>> print("Result:", eval_forward_ref("dict[str, tuple[int, float]]"))
+ Result: dict[str, tuple[int, float]]
"""
@@ -809,16 +846,23 @@ def f() -> None: ...
f.__annotations__ = {"return": ForwardRef(ref) if isinstance(ref, str) else ref}
safe_localns = {**localns} if localns else {}
- safe_localns.setdefault("typing", _sys.modules[__name__])
+ safe_localns.setdefault("typing", _FORWARD_REF_TYPING_NS)
safe_localns.setdefault("NoneType", type(None))
+ if globalns is None:
+ # Without an explicit 'globalns' the reference is resolved in this module's
+ # namespace, which used to spell the deprecated aliases as the builtin generics.
+ # They are no longer defined here, so re-add them for this evaluation only; a
+ # caller-provided 'globalns' is left untouched, exactly as before.
+ globalns = {**globals(), **_DEPRECATED_ALIAS_REPLACEMENTS}
+
actual_type = get_type_hints(f, globalns, safe_localns, include_extras=include_extras)["return"]
assert not isinstance(actual_type, ForwardRef)
return actual_type
-def _collapse_type_args(*args: Any) -> Tuple[bool, Tuple]:
+def _collapse_type_args(*args: Any) -> tuple[bool, tuple]:
if args and all(args[0] == a for a in args[1:]):
return (True, args)
else:
@@ -828,7 +872,7 @@ def _collapse_type_args(*args: Any) -> Tuple[bool, Tuple]:
@final
@_dataclasses.dataclass
class CallableKwargsInfo:
- data: Dict[str, Any]
+ data: dict[str, Any]
def infer_type(
@@ -873,7 +917,7 @@ def infer_type(
>>> print("Result:", infer_type(f))
Result: ...Callable[..., int]
- >>> print("Result:", infer_type(Dict[int, Union[int, float]]))
+ >>> print("Result:", infer_type(dict[int, Union[int, float]]))
Result: ...ict[int, ...int...float...]
For advanced cases, using :func:`functools.singledispatch` with custom hooks
@@ -904,7 +948,7 @@ def infer_type(
return type(None) if none_as_type else None
if isinstance(value, type):
- return Type[value]
+ return type[value]
if isinstance(value, tuple) and not isinstance(value, TypedNamedTupleABC):
# Special case for tuples, which can have multiple types.
@@ -917,7 +961,7 @@ def infer_type(
return StdGenericAliasType(tuple, (Any, ...))
if isinstance(value, (list, set, frozenset)):
- t: Union[Type[List], Type[Set], Type[FrozenSet]] = type(value)
+ t: Union[type[list], type[set], type[frozenset]] = type(value)
unique_type, args = _collapse_type_args(*(_infer(item) for item in value))
return StdGenericAliasType(t, args[0] if unique_type else Any)
@@ -934,8 +978,8 @@ def infer_type(
return_type = annotations.get("return", Any)
sig = _inspect.signature(value)
- arg_types: List = []
- kwonly_arg_types: Dict[str, Any] = {}
+ arg_types: list = []
+ kwonly_arg_types: dict[str, Any] = {}
for p in sig.parameters.values():
if p.kind in (
_inspect.Parameter.POSITIONAL_ONLY,
diff --git a/src/gt4py/eve/pattern_matching.py b/src/gt4py/eve/pattern_matching.py
index 71c90a01f1..2f881fc657 100644
--- a/src/gt4py/eve/pattern_matching.py
+++ b/src/gt4py/eve/pattern_matching.py
@@ -12,7 +12,7 @@
from functools import singledispatch
-from .extended_typing import Any, Iterator, Tuple
+from .extended_typing import Any, Iterator
class ObjectPattern:
@@ -60,7 +60,7 @@ def __str__(self) -> str:
@singledispatch
-def get_differences(a: Any, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
+def get_differences(a: Any, b: Any, path: str = "") -> Iterator[tuple[str, str]]:
"""Compare two objects and return a list of differences.
If the arguments are lists or dictionaries comparison is recursively per
@@ -77,7 +77,7 @@ def get_differences(a: Any, b: Any, path: str = "") -> Iterator[Tuple[str, str]]
@get_differences.register
-def _(a: ObjectPattern, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
+def _(a: ObjectPattern, b: Any, path: str = "") -> Iterator[tuple[str, str]]:
if not isinstance(b, a.cls):
yield (path, f"Expected an instance of class {a.cls.__name__}, but got {type(b).__name__}")
else:
@@ -89,7 +89,7 @@ def _(a: ObjectPattern, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
@get_differences.register
-def _(a: list, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
+def _(a: list, b: Any, path: str = "") -> Iterator[tuple[str, str]]:
if not isinstance(b, list):
yield (path, f"Expected list, but got {type(b).__name__}")
elif len(a) != len(b):
@@ -100,7 +100,7 @@ def _(a: list, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
@get_differences.register
-def _(a: dict, b: Any, path: str = "") -> Iterator[Tuple[str, str]]:
+def _(a: dict, b: Any, path: str = "") -> Iterator[tuple[str, str]]:
if not isinstance(b, dict):
yield (path, f"Expected dict, but got {type(b).__name__}")
elif set(a.keys()) != set(b.keys()):
diff --git a/src/gt4py/eve/traits.py b/src/gt4py/eve/traits.py
index eb39a38152..73caee10fe 100644
--- a/src/gt4py/eve/traits.py
+++ b/src/gt4py/eve/traits.py
@@ -13,11 +13,11 @@
import collections
from . import concepts, datamodels, exceptions, visitors
-from .extended_typing import Any, Dict, Set, Type, no_type_check
+from .extended_typing import Any, no_type_check
# --- Node Traits ---
-@concepts.register_annex_user("symtable", Dict[str, concepts.Node], shared=True)
+@concepts.register_annex_user("symtable", dict[str, concepts.Node], shared=True)
@datamodels.datamodel
class SymbolTableTrait:
"""
@@ -33,13 +33,13 @@ class SymbolTableTrait:
@no_type_check
@datamodels.root_validator
@classmethod
- def _collect_symbol_names(cls: Type[SymbolTableTrait], instance: concepts.Node) -> None:
+ def _collect_symbol_names(cls: type[SymbolTableTrait], instance: concepts.Node) -> None:
collected_symbols = cls.SymbolsCollector.apply(instance)
instance.annex.symtable = collected_symbols
class SymbolsCollector(visitors.NodeVisitor):
def __init__(self) -> None:
- self.collected_symbols: Dict[str, concepts.Node] = {}
+ self.collected_symbols: dict[str, concepts.Node] = {}
def visit_Node(self, node: concepts.Node, /) -> None:
for field_name, attribute in node.__datamodel_fields__.items():
@@ -70,7 +70,7 @@ def visit(self, node: concepts.RootNode, **kwargs: Any) -> Any:
return super().visit(node, **kwargs)
@classmethod
- def apply(cls, node: concepts.Node) -> Dict[str, concepts.Node]:
+ def apply(cls, node: concepts.Node) -> dict[str, concepts.Node]:
collector = cls()
# If the passed root node already contains a symbol table, the check in `visit_Node()`
# will automatically stop the traversal. To avoid this premature stop, we start the
@@ -82,7 +82,7 @@ def apply(cls, node: concepts.Node) -> Dict[str, concepts.Node]:
return collector.collected_symbols
-@concepts.register_annex_user("symtable", Dict[str, concepts.Node], shared=True)
+@concepts.register_annex_user("symtable", dict[str, concepts.Node], shared=True)
@datamodels.datamodel
class SymbolRefsValidatorTrait:
"""Node trait adding automatic validation of symbol references appearing the node tree.
@@ -96,7 +96,7 @@ class SymbolRefsValidatorTrait:
@no_type_check
@datamodels.root_validator
@classmethod
- def _validate_symbol_refs(cls: Type[SymbolRefsValidatorTrait], instance: concepts.Node) -> None:
+ def _validate_symbol_refs(cls: type[SymbolRefsValidatorTrait], instance: concepts.Node) -> None:
validator = cls.SymbolRefsValidator()
symtable = instance.annex.symtable
for child_node in instance.iter_children_values():
@@ -109,10 +109,10 @@ def _validate_symbol_refs(cls: Type[SymbolRefsValidatorTrait], instance: concept
class SymbolRefsValidator(visitors.NodeVisitor):
def __init__(self) -> None:
- self.missing_symbols: Set[str] = set()
+ self.missing_symbols: set[str] = set()
def visit_Node(
- self, node: concepts.Node, *, symtable: Dict[str, Any], **kwargs: Any
+ self, node: concepts.Node, *, symtable: dict[str, Any], **kwargs: Any
) -> None:
for field_name, attribute in node.__datamodel_fields__.items():
if isinstance(attribute.type, type) and issubclass(
@@ -128,7 +128,7 @@ def visit_Node(
self.generic_visit(node, symtable=symtable, **kwargs)
@classmethod
- def apply(cls, node: concepts.Node, *, symtable: Dict[str, Any]) -> Set[str]:
+ def apply(cls, node: concepts.Node, *, symtable: dict[str, Any]) -> set[str]:
validator = cls()
validator.visit(node, symtable=symtable)
return validator.missing_symbols
diff --git a/src/gt4py/eve/trees.py b/src/gt4py/eve/trees.py
index bc938a0f85..6e0cce0872 100644
--- a/src/gt4py/eve/trees.py
+++ b/src/gt4py/eve/trees.py
@@ -20,11 +20,8 @@
Any,
Callable,
Iterable,
- List,
Optional,
Protocol,
- Tuple,
- Type,
TypeVar,
Union,
)
@@ -50,7 +47,7 @@ class Tree(Protocol):
def iter_children_values(self) -> Iterable: ...
@abc.abstractmethod
- def iter_children_items(self) -> Iterable[Tuple[TreeKey, Any]]: ...
+ def iter_children_items(self) -> Iterable[tuple[TreeKey, Any]]: ...
TreeLike.register(Tree)
@@ -65,15 +62,15 @@ def iter_children_values(node: TreeLike) -> Iterable:
@functools.singledispatch
-def iter_children_items(node: TreeLike) -> Iterable[Tuple[TreeKey, Any]]:
+def iter_children_items(node: TreeLike) -> Iterable[tuple[TreeKey, Any]]:
"""Create an iterator to traverse values as Eve tree nodes."""
return node.iter_children_items() if hasattr(node, "iter_children_items") else iter(())
def register_tree_like(
- *types: Type[_T],
+ *types: type[_T],
iter_values_fn: Callable[[_T], Iterable],
- iter_items_fn: Callable[[_T], Iterable[Tuple[TreeKey, Any]]],
+ iter_items_fn: Callable[[_T], Iterable[tuple[TreeKey, Any]]],
) -> None:
for t in types:
TreeLike.register(t)
@@ -109,7 +106,7 @@ class TraversalOrder(Enum):
def _pre_walk_items(
node: TreeLike, *, __key__: Optional[TreeKey] = None
-) -> Iterable[Tuple[Optional[TreeKey], Any]]:
+) -> Iterable[tuple[Optional[TreeKey], Any]]:
"""Create a pre-order tree traversal iterator of (key, value) pairs."""
yield __key__, node
for key, child in iter_children_items(node):
@@ -129,7 +126,7 @@ def _pre_walk_values(node: TreeLike) -> Iterable:
def _post_walk_items(
node: TreeLike, *, __key__: Optional[TreeKey] = None
-) -> Iterable[Tuple[Optional[TreeKey], Any]]:
+) -> Iterable[tuple[Optional[TreeKey], Any]]:
"""Create a post-order tree traversal iterator of (key, value) pairs."""
for key, child in iter_children_items(node):
yield from _post_walk_items(child, __key__=key)
@@ -149,8 +146,8 @@ def _post_walk_values(node: TreeLike) -> Iterable:
def _bfs_walk_items(
- node: TreeLike, *, __key__: Optional[TreeKey] = None, __queue__: Optional[List] = None
-) -> Iterable[Tuple[Optional[TreeKey], Any]]:
+ node: TreeLike, *, __key__: Optional[TreeKey] = None, __queue__: Optional[list] = None
+) -> Iterable[tuple[Optional[TreeKey], Any]]:
"""Create a tree traversal iterator of (key, value) pairs by tree levels (Breadth-First Search)."""
__queue__ = __queue__ or []
yield __key__, node
@@ -161,8 +158,8 @@ def _bfs_walk_items(
def _bfs_walk_values(
- node: TreeLike, *, __queue__: Optional[List] = None
-) -> Iterable[Tuple[TreeKey, Any]]:
+ node: TreeLike, *, __queue__: Optional[list] = None
+) -> Iterable[tuple[TreeKey, Any]]:
"""Create a tree traversal iterator of values by tree levels (Breadth-First Search)."""
__queue__ = __queue__ or []
yield node
@@ -179,7 +176,7 @@ def _bfs_walk_values(
def walk_items(
node: TreeLike, traversal_order: TraversalOrder = TraversalOrder.PRE_ORDER
-) -> utils.XIterable[Tuple[Optional[TreeKey], Any]]:
+) -> utils.XIterable[tuple[Optional[TreeKey], Any]]:
"""Create a tree traversal iterator of (key, value) pairs."""
if traversal_order is traversal_order.PRE_ORDER:
return pre_walk_items(node=node)
diff --git a/src/gt4py/eve/type_definitions.py b/src/gt4py/eve/type_definitions.py
index e68ea7af55..542b7425c3 100644
--- a/src/gt4py/eve/type_definitions.py
+++ b/src/gt4py/eve/type_definitions.py
@@ -16,15 +16,15 @@
from boltons.typeutils import classproperty as classproperty
-from .extended_typing import Any, ClassVar, NoReturn, Optional, Tuple, TypeVar, final
+from .extended_typing import Any, ClassVar, NoReturn, Optional, TypeVar, final
# -- Frozen collections --
_Tc = TypeVar("_Tc", covariant=True)
-class FrozenList(Tuple[_Tc, ...], metaclass=abc.ABCMeta):
- """Tuple subtype which works as an alias of ``Tuple[_Tc, ...]``."""
+class FrozenList(tuple[_Tc, ...], metaclass=abc.ABCMeta):
+ """Tuple subtype which works as an alias of ``tuple[_Tc, ...]``."""
__slots__ = ()
diff --git a/src/gt4py/eve/type_validation.py b/src/gt4py/eve/type_validation.py
index 456add5ebc..e1b95f12b5 100644
--- a/src/gt4py/eve/type_validation.py
+++ b/src/gt4py/eve/type_validation.py
@@ -20,14 +20,12 @@
from . import exceptions, extended_typing as xtyping, utils
from .extended_typing import (
Any,
- Dict,
Final,
ForwardRef,
Literal,
Optional,
Protocol,
Sequence,
- Type,
TypeAnnotation,
Union,
cast,
@@ -46,8 +44,8 @@ def __call__(
type_annotation: TypeAnnotation,
name: Optional[str] = None,
*,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
required: bool = True,
**kwargs: Any,
) -> None:
@@ -97,8 +95,8 @@ def __call__(
name: Optional[str] = None,
*,
required: Literal[True] = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> FixedTypeValidator: ...
@@ -109,8 +107,8 @@ def __call__(
name: Optional[str] = None,
*,
required: bool = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> Optional[FixedTypeValidator]: ...
@@ -121,8 +119,8 @@ def __call__(
name: Optional[str] = None,
*,
required: bool = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> Optional[FixedTypeValidator]:
"""Protocol for :class:`FixedTypeValidator`s.
@@ -154,8 +152,8 @@ def __call__(
name: Optional[str] = None,
*,
required: Literal[True] = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> FixedTypeValidator: ...
@@ -166,8 +164,8 @@ def __call__(
name: Optional[str] = None,
*,
required: bool = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> Optional[FixedTypeValidator]: ...
@@ -177,8 +175,8 @@ def __call__(
name: Optional[str] = None,
*,
required: bool = True,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> Optional[FixedTypeValidator]:
# TODO(egparedes): if a "typing tree" structure is implemented, refactor this code as a tree traversal.
@@ -474,7 +472,7 @@ def _is_optional(value: Any, **kwargs: Any) -> None:
@staticmethod
def combine_validators_as_or(
- name: str, *validators: FixedTypeValidator, error_type: Type[Exception] = TypeError
+ name: str, *validators: FixedTypeValidator, error_type: type[Exception] = TypeError
) -> FixedTypeValidator:
def _combined_validator(value: Any, **kwargs: Any) -> None:
for v in validators:
@@ -502,8 +500,8 @@ def simple_type_validator(
type_annotation: TypeAnnotation,
name: Optional[str] = None,
*,
- globalns: Optional[Dict[str, Any]] = None,
- localns: Optional[Dict[str, Any]] = None,
+ globalns: Optional[dict[str, Any]] = None,
+ localns: Optional[dict[str, Any]] = None,
required: bool = True,
**kwargs: Any,
) -> None:
diff --git a/src/gt4py/eve/utils.py b/src/gt4py/eve/utils.py
index 6f49c94108..014fbde6da 100644
--- a/src/gt4py/eve/utils.py
+++ b/src/gt4py/eve/utils.py
@@ -49,17 +49,12 @@
ArgsOnlyCallable,
Callable,
Collection,
- Dict,
Generic,
Iterable,
Iterator,
- List,
Literal,
Optional,
ParamSpec,
- Set,
- Tuple,
- Type,
TypeVar,
Union,
cast,
@@ -88,7 +83,7 @@ def first(iterable: Iterable[T], *, default: Union[T, NothingType] = NOTHING) ->
raise error
-def isinstancechecker(type_info: Union[Type, Iterable[Type]]) -> Callable[[Any], bool]:
+def isinstancechecker(type_info: Union[type[Any], Iterable[type[Any]]]) -> Callable[[Any], bool]:
"""Return a callable object that checks if operand is an instance of `type_info`.
Examples:
@@ -101,7 +96,7 @@ def isinstancechecker(type_info: Union[Type, Iterable[Type]]) -> Callable[[Any],
False
"""
- types: Tuple[Type, ...] = tuple()
+ types: tuple[type[Any], ...] = tuple()
if isinstance(type_info, type):
types = (type_info,)
elif not isinstance(type_info, tuple) and is_collection(type_info):
@@ -264,7 +259,7 @@ class IndexerCallable(Generic[_S, _T]):
func: ArgsOnlyCallable[_S, _T]
- def __getitem__(self, key: _S | Tuple[_S, ...]) -> _T:
+ def __getitem__(self, key: _S | tuple[_S, ...]) -> _T:
return self.func(*key) if isinstance(key, tuple) else self.func(key)
@@ -590,7 +585,7 @@ def _decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return _decorator(func) if func is not None else _decorator
-def register_subclasses(*subclasses: Type) -> Callable[[Type], Type]:
+def register_subclasses(*subclasses: type[Any]) -> Callable[[type[Any]], type[Any]]:
"""Class decorator to automatically register virtual subclasses.
Examples:
@@ -609,7 +604,7 @@ def register_subclasses(*subclasses: Type) -> Callable[[Type], Type]:
"""
- def _decorator(base_cls: Type) -> Type:
+ def _decorator(base_cls: type[Any]) -> type[Any]:
for s in subclasses:
base_cls.register(s)
return base_cls
@@ -617,7 +612,7 @@ def _decorator(base_cls: Type) -> Type:
return _decorator
-def noninstantiable(cls: Type[_T]) -> Type[_T]:
+def noninstantiable(cls: type[_T]) -> type[_T]:
"""Make a class without abstract method non-instantiable (subclasses should be instantiable)."""
if not isinstance(cls, type):
raise ValueError(f"Non-type value ({cls}) passed to 'noninstantiable()' class decorator.")
@@ -636,7 +631,7 @@ def _noninstantiable_init(self: _T, *args: Any, **kwargs: Any) -> None:
return cls
-def is_noninstantiable(cls: Type[_T]) -> bool:
+def is_noninstantiable(cls: type[_T]) -> bool:
"""Return True if `model` is a non-instantiable class."""
return "__noninstantiable__" in cls.__dict__
@@ -791,7 +786,7 @@ def dhash(obj: Any, **kwargs: Any) -> str:
def pprint_ddiff(
- old: Any, new: Any, *, pprint_opts: Optional[Dict[str, Any]] = None, **kwargs: Any
+ old: Any, new: Any, *, pprint_opts: Optional[dict[str, Any]] = None, **kwargs: Any
) -> None:
"""Pretty printing of deepdiff.diff.DeepDiff objects.
@@ -822,14 +817,14 @@ class CASE_STYLE(enum.Enum):
KEBAB = "kebab"
@classmethod
- def split(cls, name: str, case_style: Union[CASE_STYLE, str]) -> List[str]:
+ def split(cls, name: str, case_style: Union[CASE_STYLE, str]) -> list[str]:
if isinstance(case_style, str):
case_style = cls.CASE_STYLE(case_style)
assert isinstance(case_style, cls.CASE_STYLE)
if case_style == cls.CASE_STYLE.CONCATENATED:
raise ValueError("Impossible to split a simply concatenated string")
- splitter: Callable[[str], List[str]] = getattr(cls, f"split_{case_style.value}_case")
+ splitter: Callable[[str], list[str]] = getattr(cls, f"split_{case_style.value}_case")
return splitter(name)
@classmethod
@@ -888,22 +883,22 @@ def join_kebab_case(words: AnyWordsIterable) -> str:
# https://stackoverflow.com/a/29920015/7232525
#
@staticmethod
- def split_canonical_case(name: str) -> List[str]:
+ def split_canonical_case(name: str) -> list[str]:
return name.split()
@staticmethod
- def split_camel_case(name: str) -> List[str]:
+ def split_camel_case(name: str) -> list[str]:
matches = re.finditer(".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", name)
return [m.group(0) for m in matches]
split_pascal_case = split_camel_case
@staticmethod
- def split_snake_case(name: str) -> List[str]:
+ def split_snake_case(name: str) -> list[str]:
return name.split("_")
@staticmethod
- def split_kebab_case(name: str) -> List[str]:
+ def split_kebab_case(name: str) -> list[str]:
return name.split("-")
@@ -933,7 +928,7 @@ class Namespace(types.SimpleNamespace, Generic[T]):
def __contains__(self, key: str) -> bool:
return key in self.__dict__
- def items(self) -> Iterable[Tuple[str, T]]:
+ def items(self) -> Iterable[tuple[str, T]]:
return self.__dict__.items()
def keys(self) -> Iterable[str]:
@@ -942,12 +937,12 @@ def keys(self) -> Iterable[str]:
def values(self) -> Iterable[T]:
return self.__dict__.values()
- def reset(self, data: Optional[Dict[str, Any]] = None) -> None:
+ def reset(self, data: Optional[dict[str, Any]] = None) -> None:
self.__dict__.clear()
if data:
self.__dict__.update(data)
- def as_dict(self) -> Dict[str, T]:
+ def as_dict(self) -> dict[str, T]:
return {**self.__dict__}
asdict = as_dict
@@ -1117,7 +1112,7 @@ def filter(self, func: Callable[..., bool]) -> XIterable[T]: # A003: shadowing
raise TypeError(f"Invalid function or callable: '{func}'.")
return XIterable(filter(func, self.iterator))
- def if_isinstance(self, *types: Type) -> XIterable[T]:
+ def if_isinstance(self, *types: type[Any]) -> XIterable[T]:
"""Filter elements using :func:`isinstance` checks.
Equivalent to ``xiter(item for item in self if isinstance(item, types))``.
@@ -1130,7 +1125,7 @@ def if_isinstance(self, *types: Type) -> XIterable[T]:
"""
return XIterable(filter(isinstancechecker([*types]), self.iterator))
- def if_not_isinstance(self, *types: Type) -> XIterable[T]:
+ def if_not_isinstance(self, *types: type[Any]) -> XIterable[T]:
"""Filter elements using negated :func:`isinstance` checks.
Equivalent to ``xiter(item for item in self if not isinstance(item, types))``.
@@ -1229,7 +1224,7 @@ def if_contains(self, *values: Any) -> XIterable[T]:
"""
- def _contains(a: Any, collection: Tuple) -> bool:
+ def _contains(a: Any, collection: tuple) -> bool:
try:
return all(operator.contains(a, v) for v in collection)
except Exception:
@@ -1340,7 +1335,7 @@ def chain(self, *others: Iterable) -> XIterable[Union[T, S]]:
def diff(
self, *others: Iterable, default: Any = NOTHING, key: Union[NOTHING, Callable] = NOTHING
- ) -> XIterable[Tuple[T, S]]:
+ ) -> XIterable[tuple[T, S]]:
"""Diff iterators.
Equivalent to ``toolz.itertoolz.diff(self, *others)``.
@@ -1373,7 +1368,7 @@ def diff(
[('Bananas', 'oranges')]
"""
- kwargs: Dict[str, Any] = {}
+ kwargs: dict[str, Any] = {}
if default is not NOTHING:
kwargs["default"] = default
if key is not NOTHING:
@@ -1384,7 +1379,7 @@ def diff(
def product(
self, other: Union[Iterable[S], int]
- ) -> Union[XIterable[Tuple[T, S]], XIterable[Tuple[T, T]]]:
+ ) -> Union[XIterable[tuple[T, S]], XIterable[tuple[T, T]]]:
"""Product of iterators.
Equivalent to ``itertools.product(it_a, it_b)``.
@@ -1416,7 +1411,7 @@ def product(
def partition(
self, n: int, *, exact: bool = False, fill: Any = NOTHING
- ) -> XIterable[Tuple[T, ...]]:
+ ) -> XIterable[tuple[T, ...]]:
"""Partition iterator into tuples of length `n` (``exact=True``) or at most `n` (``exact=False``).
Equivalent to ``toolz.itertoolz.partition(n, self)`` or
@@ -1477,7 +1472,7 @@ def take_nth(self, n: int) -> XIterable[T]:
def zip( # A003: shadowing a python builtin
self, *others: Iterable, fill: Any = NOTHING
- ) -> XIterable[Tuple[T, S]]:
+ ) -> XIterable[tuple[T, S]]:
"""Zip iterators.
Equivalent to ``zip(self, *others)`` or ``itertools.zip_longest(self, *others, fillvalue=fill)``.
@@ -1508,7 +1503,7 @@ def zip( # A003: shadowing a python builtin
else:
return XIterable(itertools.zip_longest(self.iterator, *iterators, fillvalue=fill))
- def unzip(self) -> XIterable[Tuple[Any, ...]]:
+ def unzip(self) -> XIterable[tuple[Any, ...]]:
"""Unzip iterator.
Equivalent to ``zip(*self)``.
@@ -1609,21 +1604,21 @@ def unique(self, *, key: Union[NOTHING, Callable] = NOTHING) -> XIterable[T]:
@typing.overload
def groupby(
self, key: str, *other_keys: str, as_dict: bool = False
- ) -> XIterable[Tuple[Any, List[T]]]: ...
+ ) -> XIterable[tuple[Any, list[T]]]: ...
@typing.overload
def groupby(
- self, key: List[Any], *, as_dict: bool = False
- ) -> XIterable[Tuple[Any, List[T]]]: ...
+ self, key: list[Any], *, as_dict: bool = False
+ ) -> XIterable[tuple[Any, list[T]]]: ...
@typing.overload
def groupby(
self, key: Callable[[T], Any], *, as_dict: bool = False
- ) -> XIterable[Tuple[Any, List[T]]]: ...
+ ) -> XIterable[tuple[Any, list[T]]]: ...
def groupby(
- self, key: Union[str, List[Any], Callable[[T], Any]], *attr_keys: str, as_dict: bool = False
- ) -> Union[XIterable[Tuple[Any, List[T]]], Dict]:
+ self, key: Union[str, list[Any], Callable[[T], Any]], *attr_keys: str, as_dict: bool = False
+ ) -> Union[XIterable[tuple[Any, list[T]]], dict]:
"""Group a sequence by a given key.
More or less equivalent to ``toolz.itertoolz.groupby(key, self)`` with some caveats.
@@ -1748,7 +1743,7 @@ def reduceby(
*,
as_dict: Literal[False],
init: Union[S, NothingType],
- ) -> XIterable[Tuple[str, S]]: ...
+ ) -> XIterable[tuple[str, S]]: ...
@typing.overload
def reduceby(
@@ -1759,7 +1754,7 @@ def reduceby(
*attr_keys: str,
as_dict: Literal[False],
init: Union[S, NothingType],
- ) -> XIterable[Tuple[Tuple[str, ...], S]]: ...
+ ) -> XIterable[tuple[tuple[str, ...], S]]: ...
@typing.overload
def reduceby(
@@ -1769,7 +1764,7 @@ def reduceby(
*,
as_dict: Literal[True],
init: Union[S, NothingType],
- ) -> Dict[str, S]: ...
+ ) -> dict[str, S]: ...
@typing.overload
def reduceby(
@@ -1780,27 +1775,27 @@ def reduceby(
*attr_keys: str,
as_dict: Literal[True],
init: Union[S, NothingType],
- ) -> Dict[Tuple[str, ...], S]: ...
+ ) -> dict[tuple[str, ...], S]: ...
@typing.overload
def reduceby(
self,
bin_op_func: Callable[[S, T], S],
- key: List[K],
+ key: list[K],
*,
as_dict: Literal[False],
init: Union[S, NothingType],
- ) -> XIterable[Tuple[K, S]]: ...
+ ) -> XIterable[tuple[K, S]]: ...
@typing.overload
def reduceby(
self,
bin_op_func: Callable[[S, T], S],
- key: List[K],
+ key: list[K],
*,
as_dict: Literal[True],
init: Union[S, NothingType],
- ) -> Dict[K, S]: ...
+ ) -> dict[K, S]: ...
@typing.overload
def reduceby(
@@ -1810,7 +1805,7 @@ def reduceby(
*,
as_dict: Literal[False],
init: Union[S, NothingType],
- ) -> XIterable[Tuple[K, S]]: ...
+ ) -> XIterable[tuple[K, S]]: ...
@typing.overload
def reduceby(
@@ -1820,22 +1815,22 @@ def reduceby(
*,
as_dict: Literal[True],
init: Union[S, NothingType],
- ) -> Dict[K, S]: ...
+ ) -> dict[K, S]: ...
def reduceby(
self,
bin_op_func: Callable[[S, T], S],
- key: Union[str, List[K], Callable[[T], K]],
+ key: Union[str, list[K], Callable[[T], K]],
*attr_keys: str,
as_dict: bool = False,
init: Union[S, NothingType] = NOTHING,
) -> Union[
- XIterable[Tuple[str, S]],
- Dict[str, S],
- XIterable[Tuple[Tuple[str, ...], S]],
- Dict[Tuple[str, ...], S],
- XIterable[Tuple[K, S]],
- Dict[K, S],
+ XIterable[tuple[str, S]],
+ dict[str, S],
+ XIterable[tuple[tuple[str, ...], S]],
+ dict[tuple[str, ...], S],
+ XIterable[tuple[K, S]],
+ dict[K, S],
]:
"""Group a sequence by a given key and simultaneously perform a reduction inside the groups.
@@ -1908,7 +1903,7 @@ def reduceby(
groups = toolz.itertoolz.reduceby(groupby_key, bin_op_func, self.iterator)
return groups if as_dict else xiter(groups.items())
- def to_list(self) -> List[T]:
+ def to_list(self) -> list[T]:
"""Expand iterator into a ``list``.
Equivalent to ``list(self)``.
@@ -1921,7 +1916,7 @@ def to_list(self) -> List[T]:
"""
return list(self.iterator)
- def to_set(self) -> Set[T]:
+ def to_set(self) -> set[T]:
"""Expand iterator into a ``set``.
Equivalent to ``set(self)``.
diff --git a/src/gt4py/next/errors/exceptions.py b/src/gt4py/next/errors/exceptions.py
index 3533dd1fe8..1405cc714c 100644
--- a/src/gt4py/next/errors/exceptions.py
+++ b/src/gt4py/next/errors/exceptions.py
@@ -20,13 +20,9 @@
from __future__ import annotations
import difflib
-import sys
-from typing import Any, ClassVar, Iterable, Optional, Sequence
+from typing import Any, ClassVar, Iterable, Optional, Self, Sequence
from gt4py.eve import SourceLocation
-
-# TODO(havogt): import 'Self' from 'typing' directly once the Python floor is >=3.12.
-from gt4py.eve.extended_typing import Self
from gt4py.next.errors import formatting
@@ -95,18 +91,8 @@ def with_location(self, location: Optional[SourceLocation]) -> Self:
self.location = location
return self
- # TODO(havogt): drop this shim and the matching '__notes__' fold-in in
- # '__str__' once the Python floor is >=3.11, where 'BaseException.add_note'
- # (PEP 678) and its automatic '__notes__' traceback rendering are built in.
- if sys.version_info < (3, 11):
-
- def add_note(self, note: str) -> None:
- if not hasattr(self, "__notes__"):
- self.__notes__ = []
- self.__notes__.append(note)
-
def __str__(self) -> str:
- body = formatting.format_diagnostic_parts(
+ return formatting.format_diagnostic_parts(
self.message,
self.location,
label=self.label,
@@ -114,12 +100,6 @@ def __str__(self) -> str:
notes=self.notes,
hints=self.hints,
)
- if sys.version_info < (3, 11):
- # On 3.10 the traceback machinery doesn't render '__notes__'; fold
- # them in so they surface through 'str()' (pytest, IPython, logging)
- # the way the >=3.11 machinery does automatically.
- body += "".join(f"\n{note}" for note in getattr(self, "__notes__", []))
- return body
class UnsupportedPythonFeatureError(DSLError):
diff --git a/src/gt4py/next/ffront/dialect_parser.py b/src/gt4py/next/ffront/dialect_parser.py
index ab51613ac1..a7c27a3181 100644
--- a/src/gt4py/next/ffront/dialect_parser.py
+++ b/src/gt4py/next/ffront/dialect_parser.py
@@ -29,8 +29,6 @@
#: what to use instead. Constructs not listed here get a generic message naming
#: the `ast` class. Keep the hints actionable: name the closest supported
#: alternative, not just the restriction.
-# TODO(havogt): add 'ast.TryStar' ('try*' statement, Python >=3.11) once the
-# Python floor is >=3.12; referencing it unconditionally breaks import on 3.10.
_UNSUPPORTED_FEATURE_HINTS: dict[type[ast.AST], tuple[str, tuple[str, ...]]] = {
ast.For: (
"'for' loop",
@@ -62,7 +60,19 @@
"'lambda' expression",
("Define a separate function decorated with '@field_operator' instead.",),
),
+ # TODO(egparedes): make these two entries reachable for the common 'except :'
+ # shape. Naming an exception type turns it into a closure variable, and
+ # 'func_to_foast.FieldOperatorParser.visit_FunctionDef' types closure variables
+ # *before* visiting the body, so 'try: ... except ValueError: ...' fails first with
+ # "Unexpected object 'ValueError' of type '' encountered." Only
+ # 'try/finally' and bare 'try/except:' reach this catalogue; 'try*' never does,
+ # since 'except*' always names a type. Fixing it means running the
+ # unsupported-syntax scan ahead of closure-variable type deduction.
ast.Try: ("'try' statement", ("Exception handling is not available inside GT4Py functions.",)),
+ ast.TryStar: (
+ "'try*' statement",
+ ("Exception handling is not available inside GT4Py functions.",),
+ ),
ast.Raise: (
"'raise' statement",
("Exception handling is not available inside GT4Py functions.",),
@@ -74,6 +84,24 @@
ast.Match: ("'match' statement", ("Use 'if'/'elif' chains or 'where' instead.",)),
}
+#: Same as above, but keyed by node *name*, for constructs introduced after the
+#: supported Python floor: naming e.g. 'ast.TemplateStr' (3.14) directly in the
+#: catalogue above would raise 'AttributeError' on 3.12 and 3.13. Entries whose node
+#: type the running interpreter does not have are skipped, which is harmless: the
+#: construct cannot be parsed there in the first place.
+_NEWER_UNSUPPORTED_FEATURE_HINTS: dict[str, tuple[str, tuple[str, ...]]] = {
+ # PEP 750 t-strings (3.14).
+ "TemplateStr": ("t-string", ("Strings cannot be computed inside GT4Py functions.",))
+}
+
+_UNSUPPORTED_FEATURE_HINTS.update(
+ {
+ node_type: entry
+ for name, entry in _NEWER_UNSUPPORTED_FEATURE_HINTS.items()
+ if (node_type := getattr(ast, name, None)) is not None
+ }
+)
+
def _describe_unsupported_feature(node: ast.AST) -> tuple[str, tuple[str, ...]]:
if (entry := _UNSUPPORTED_FEATURE_HINTS.get(type(node))) is not None:
diff --git a/src/gt4py/next/ffront/fbuiltins.py b/src/gt4py/next/ffront/fbuiltins.py
index e77f090f99..602221fbcc 100644
--- a/src/gt4py/next/ffront/fbuiltins.py
+++ b/src/gt4py/next/ffront/fbuiltins.py
@@ -12,6 +12,7 @@
import math
import operator
from builtins import bool, float, int, tuple # noqa: A004 shadowing a Python built-in
+from types import UnionType
from typing import (
Any,
Callable,
@@ -23,6 +24,8 @@
TypeVar,
Union,
cast,
+ get_args,
+ get_origin,
overload,
)
@@ -138,12 +141,15 @@ def _type_conversion_helper(t: type) -> type[ts.TypeSpec] | tuple[type[ts.TypeSp
return (
ts.ConstructorType
) # our type of type is currently represented by the type constructor function
- elif t is Tuple or (hasattr(t, "__origin__") and t.__origin__ is tuple):
+ elif t is tuple or get_origin(t) is tuple:
return ts.TupleType
- elif hasattr(t, "__origin__") and t.__origin__ is Union:
- types = [_type_conversion_helper(e) for e in t.__args__] # type: ignore[attr-defined]
- assert all(type(t) is type and issubclass(t, ts.TypeSpec) for t in types)
- return cast(tuple[type[ts.TypeSpec], ...], tuple(types)) # `cast` to break the recursion
+ # 'Union[A, B]' and 'A | B' are different runtime objects: the latter is a
+ # 'types.UnionType', which carries no '__origin__' at all.
+ elif get_origin(t) in (Union, UnionType):
+ member_types = [_type_conversion_helper(e) for e in get_args(t)]
+ assert all(type(m) is type and issubclass(m, ts.TypeSpec) for m in member_types)
+ # `cast` to break the recursion
+ return cast(tuple[type[ts.TypeSpec], ...], tuple(member_types))
elif t in named_collections.CUSTOM_NAMED_COLLECTION_TYPES:
return ts.NamedCollectionType
else:
diff --git a/src/gt4py/next/ffront/func_to_foast.py b/src/gt4py/next/ffront/func_to_foast.py
index b762da8d9b..14dceb25d1 100644
--- a/src/gt4py/next/ffront/func_to_foast.py
+++ b/src/gt4py/next/ffront/func_to_foast.py
@@ -232,6 +232,12 @@ def _reject_invalid_return_annotation(self, node: ast.FunctionDef) -> None:
def visit_FunctionDef(self, node: ast.FunctionDef, **kwargs: Any) -> foast.FunctionDefinition:
loc = self.get_location(node)
self._check_not_a_reserved_name(node.name, loc)
+ # TODO(egparedes): run the unsupported-syntax scan before this loop. Typing the
+ # closure variables first means a name that only appears in unsupported syntax
+ # is reported as a bad closure variable instead of as the construct that
+ # introduced it -- e.g. 'try: ... except ValueError: ...' raises "Unexpected
+ # object 'ValueError' ..." spanning the whole signature, rather than the
+ # 'try'-statement diagnostic catalogued in 'dialect_parser'.
closure_var_symbols: list[foast.Symbol] = []
for name in self.closure_vars.keys():
try:
diff --git a/src/gt4py/next/fingerprinting.py b/src/gt4py/next/fingerprinting.py
index 3539165e6a..f0739a102b 100644
--- a/src/gt4py/next/fingerprinting.py
+++ b/src/gt4py/next/fingerprinting.py
@@ -571,11 +571,10 @@ def catabolize(
The traversal scheme is fixed: an iterative post-order walk over the
one-level deconstructions produced by `deconstructor`, so the structure
- depth is bounded neither by the Python recursion limit nor (on Python
- <= 3.10) by the C stack. The reduction logic is supplied as the
- `aggregator` (the algebra of the catamorphism): it aggregates an
- `EmptyDeconstruction` into a result and, for non-terminal objects,
- a `Deconstruction` whose pieces have been replaced by the
+ depth is bounded neither by the Python recursion limit nor by the C stack.
+ The reduction logic is supplied as the `aggregator` (the algebra of the
+ catamorphism): it aggregates an `EmptyDeconstruction` into a result and,
+ for non-terminal objects, a `Deconstruction` whose pieces have been replaced by the
already-aggregated results of the original pieces — in piece order, or in
canonical sorted order for an `OrderInsensitiveDeconstruction`
(which requires the results to be orderable).
diff --git a/src/gt4py/next/otf/compiled_program.py b/src/gt4py/next/otf/compiled_program.py
index bba32e16ed..d56e302ca4 100644
--- a/src/gt4py/next/otf/compiled_program.py
+++ b/src/gt4py/next/otf/compiled_program.py
@@ -170,12 +170,12 @@ def wait_for_compilation() -> None:
Raises:
Exception: The exception of the failed compilation. If several
- compilations failed, a `RuntimeError` summarizing all of them
- (chaining the first). Each failure is raised only once; the
- original exception is raised again when the failed program
- variant is called. Failures of programs that have been garbage
- collected in the meantime are not reported (they could never
- raise at call time either).
+ compilations failed, an `ExceptionGroup` (PEP 654) holding all of
+ them, so that every failure keeps its own traceback. Each failure
+ is raised only once; the original exception is raised again when
+ the failed program variant is called. Failures of programs that
+ have been garbage collected in the meantime are not reported
+ (they could never raise at call time either).
"""
# TODO(havogt): reconsider tearing down the default runner here: a pure wait
# on the tracked futures would keep the workers warm between compilation
@@ -190,11 +190,16 @@ def wait_for_compilation() -> None:
if len(failures) == 1:
raise failures[0][1]
if failures:
- # TODO(havogt): raise ExceptionGroup once Python 3.10 is dropped.
- raise RuntimeError(
- "Multiple compilations failed: "
- + "; ".join(f"'{label}': {error!r}" for label, error in failures)
- ) from failures[0][1]
+ # A group keeps every failure with its own traceback; flattening them into a
+ # single error would reduce failures 2..n to 'repr' text and let only the first
+ # one carry a '__cause__'. 'BaseExceptionGroup' is the constructor to use since
+ # 'Future.exception()' is typed as 'BaseException'; it returns a plain
+ # 'ExceptionGroup' whenever every member is an 'Exception', which is the case
+ # for anything a compilation realistically raises.
+ raise BaseExceptionGroup(
+ "Multiple compilations failed: " + ", ".join(f"'{label}'" for label, _ in failures),
+ [error for _, error in failures],
+ )
def _make_tuple_expr(el_exprs: list[str]) -> str:
diff --git a/src/gt4py/storage/allocators.py b/src/gt4py/storage/allocators.py
index 394374c2a4..b206067d0e 100644
--- a/src/gt4py/storage/allocators.py
+++ b/src/gt4py/storage/allocators.py
@@ -30,8 +30,6 @@
Optional,
Protocol,
Sequence,
- Tuple,
- Type,
TypeAlias,
TypeGuard,
Union,
@@ -96,11 +94,11 @@ class TensorBuffer(Generic[core_defs.DeviceTypeT, core_defs.ScalarT]):
device: core_defs.Device[core_defs.DeviceTypeT]
dtype: core_defs.DType[core_defs.ScalarT]
shape: core_defs.TensorShape
- strides: Tuple[int, ...]
+ strides: tuple[int, ...]
layout_map: BufferLayoutMap
byte_offset: int
byte_alignment: int
- aligned_index: Tuple[int, ...]
+ aligned_index: tuple[int, ...]
ndarray: core_defs.NDArrayObject = dataclasses.field(hash=False)
@property
@@ -141,9 +139,9 @@ def __dlpack_device__(self) -> xtyping.DLPackDevice:
if TYPE_CHECKING:
# TensorBuffer should be compatible with all the expected buffer interfaces
- __TensorBufferAsArrayInterfaceT: Type[xtyping.ArrayInterface] = TensorBuffer
- __TensorBufferAsCUDAArrayInterfaceT: Type[xtyping.CUDAArrayInterface] = TensorBuffer
- __TensorBufferAsDLPackBufferT: Type[xtyping.DLPackBuffer] = TensorBuffer
+ __TensorBufferAsArrayInterfaceT: type[xtyping.ArrayInterface] = TensorBuffer
+ __TensorBufferAsCUDAArrayInterfaceT: type[xtyping.CUDAArrayInterface] = TensorBuffer
+ __TensorBufferAsDLPackBufferT: type[xtyping.DLPackBuffer] = TensorBuffer
class BufferAllocator(Protocol[core_defs.DeviceTypeT]):
@@ -302,7 +300,7 @@ def tensorize(
class ArrayUtils:
array_ns: types.ModuleType
empty: Callable[..., _NDBuffer]
- byte_bounds: Callable[[_NDBuffer], Tuple[int, int]]
+ byte_bounds: Callable[[_NDBuffer], tuple[int, int]]
as_strided: Callable[..., core_defs.NDArrayObject]
diff --git a/tests/cartesian_tests/unit_tests/frontend_tests/test_gtscript_frontend.py b/tests/cartesian_tests/unit_tests/frontend_tests/test_gtscript_frontend.py
index ee4a65d3f2..1baeefecee 100644
--- a/tests/cartesian_tests/unit_tests/frontend_tests/test_gtscript_frontend.py
+++ b/tests/cartesian_tests/unit_tests/frontend_tests/test_gtscript_frontend.py
@@ -6,6 +6,7 @@
# Please, refer to the LICENSE file in the root directory.
# SPDX-License-Identifier: BSD-3-Clause
+import ast
from enum import IntEnum, StrEnum, Enum
import inspect
import functools
@@ -2479,6 +2480,25 @@ def stencil(in_field: gtscript.Field[float], out_field: gtscript.Field[float]):
)
+class TestEllipsisNodeDetection:
+ # 'ast.Ellipsis' is removed in Python 3.14 and 'types.EllipsisType' is the type of
+ # the '...' object rather than of its AST node, so this must not go back to being
+ # an 'isinstance()' check against either of them.
+ @pytest.mark.parametrize(
+ "source, expected", [("...", True), ("1", False), ("None", False), ("x", False)]
+ )
+ def test_is_ellipsis_node(self, source, expected):
+ node = ast.parse(source, mode="eval").body
+ assert gt_frontend._is_ellipsis_node(node) is expected
+
+ def test_ellipsis_index_parses(self):
+ def stencil(field_a: gtscript.Field[np.float64], field_b: gtscript.Field[np.float64]):
+ with computation(PARALLEL), interval(...): # noqa: F821 [undefined-name]
+ field_b[...] = field_a
+
+ parse_definition(stencil, name=inspect.stack()[0][3], module=self.__class__.__name__)
+
+
@gtscript.enum
class LocalEnum(IntEnum):
A = 42
diff --git a/tests/eve_tests/unit_tests/test_datamodels.py b/tests/eve_tests/unit_tests/test_datamodels.py
index 44dc7aa4a8..a9f6872353 100644
--- a/tests/eve_tests/unit_tests/test_datamodels.py
+++ b/tests/eve_tests/unit_tests/test_datamodels.py
@@ -40,7 +40,7 @@
import pytest
import pytest_factoryboy as pytfboy
-from gt4py.eve import datamodels, utils
+from gt4py.eve import datamodels, exceptions, utils
T = TypeVar("T")
@@ -1007,10 +1007,34 @@ class Model:
assert Model.__datamodel_fields__.value.metadata["my_metadata"] == "META"
+# Nested models for the 'frozen="strict"' tests. They have to live at module level:
+# this file uses PEP 563 annotations, and forward references are resolved against
+# module globals only, so a class defined inside a test method is not visible.
+@datamodels.datamodel(frozen="strict")
+class StrictFrozenInner:
+ value: int
+
+
+@datamodels.datamodel(frozen=True)
+class PlainFrozenInner:
+ values: List[int]
+
+
+MutableBoundT = TypeVar("MutableBoundT", bound=Tuple[List[int], ...])
+
+# Module level, since this file uses PEP 563: a function-local alias would only ever be
+# seen as an unresolvable forward reference and the tests below would pass for the wrong
+# reason.
+type ImmutableAlias = tuple[int, int]
+type MutableAlias = tuple[list[int], ...]
+type PairAlias[T] = tuple[T, T]
+type BrokenAlias = _undefined_alias_target # noqa: F821 [undefined-name]
+
+
# Test datamodel options
class TestDatamodelOptions:
def test_frozen(self):
- import attr # Missing library stubs for Python 3.10)
+ import attr
@datamodels.datamodel(frozen=True)
class FrozenModel:
@@ -1066,6 +1090,159 @@ class FrozenModel:
assert hash(FrozenModel(value=string_value)) == hash(FrozenModel(value=string_value))
+ def test_strict_frozen_with_hashable_fields(self):
+ @datamodels.datamodel(frozen="strict")
+ class StrictModel:
+ value: int
+ name: str
+
+ assert StrictModel.__datamodel_params__.frozen is True
+ assert StrictModel.__datamodel_params__.strict_frozen is True
+ assert hash(StrictModel(value=1, name="a")) == hash(StrictModel(value=1, name="a"))
+
+ def test_strict_frozen_rejects_unhashable_fields(self):
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class StrictModelWithList:
+ values: List[int]
+
+ def test_strict_frozen_accepts_nested_strict_frozen_model(self):
+ @datamodels.datamodel(frozen="strict")
+ class Outer:
+ inner: StrictFrozenInner
+
+ assert hash(Outer(inner=StrictFrozenInner(value=1))) == hash(
+ Outer(inner=StrictFrozenInner(value=1))
+ )
+
+ def test_strict_frozen_rejects_non_strict_datamodel_field(self):
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class Outer:
+ inner: PlainFrozenInner
+
+ def test_strict_frozen_rejects_union_wrapped_unhashable_fields(self):
+ # A union member must satisfy the same rules as a bare annotation, otherwise
+ # 'Optional[...]' would be a trivial way around the check.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class OptionalNonStrictModel:
+ inner: Optional[PlainFrozenInner] = None
+
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class OptionalListModel:
+ values: Optional[List[int]] = None
+
+ def test_strict_frozen_rejects_container_wrapped_unhashable_fields(self):
+ # A 'tuple' is hashable only if its items are, so the type arguments of a
+ # generic container must satisfy the same rules as a bare annotation.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class TupleOfListsModel:
+ values: Tuple[List[int], ...]
+
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class TupleOfNonStrictModels:
+ inners: Tuple[PlainFrozenInner, ...]
+
+ def test_strict_frozen_rejects_unparametrized_container_fields(self):
+ # A bare 'tuple' hashes its items, which nothing proves immutable: it must be
+ # rejected for exactly the same reason as the explicit 'Tuple[Any, ...]' below.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class BareTupleModel:
+ values: tuple
+
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class BareTypingTupleModel:
+ values: Tuple
+
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class AnyItemTupleModel:
+ values: Tuple[Any, ...]
+
+ def test_strict_frozen_rejects_type_var_with_mutable_bound(self):
+ # The bound has to be checked as an annotation, not flattened to its origin,
+ # otherwise 'tuple[list[int], ...]' would come back as a plain (hashable) 'tuple'.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class TypeVarModel:
+ value: MutableBoundT
+
+ def test_strict_frozen_resolves_pep695_type_aliases(self):
+ # A PEP 695 alias stands for the annotation it resolves to, so it has to be
+ # checked through that: otherwise an alias for an immutable type would be
+ # rejected just for being an alias, and one hiding a mutable type would only be
+ # rejected by accident.
+ @datamodels.datamodel(frozen="strict")
+ class ImmutableAliasModel:
+ value: ImmutableAlias
+
+ @datamodels.datamodel(frozen="strict")
+ class ParametrizedAliasModel:
+ value: PairAlias[int]
+
+ assert hash(ImmutableAliasModel(value=(1, 2))) is not None
+ assert hash(ParametrizedAliasModel(value=(1, 2))) is not None
+
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class MutableAliasModel:
+ value: MutableAlias
+
+ def test_strict_frozen_rejects_unevaluable_pep695_type_alias(self):
+ # Alias values are evaluated lazily, so a broken one only fails here. It proves
+ # nothing about immutability and must be rejected rather than escaping as the
+ # raw 'NameError' / 'TypeError' from the alias evaluation.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class BrokenAliasModel:
+ value: BrokenAlias
+
+ def test_strict_frozen_rejects_unresolved_forward_reference(self):
+ # A self-reference cannot be resolved while the class is being created, so it
+ # cannot be proven immutable: the check must reject it instead of raising the
+ # bare 'NameError' coming from the annotation resolution.
+ with pytest.raises(exceptions.EveTypeError, match="strictly immutable"):
+
+ @datamodels.datamodel(frozen="strict")
+ class RecursiveModel:
+ child: Optional[RecursiveModel] = None
+
+ def test_strict_frozen_accepts_optional_and_literal_fields(self):
+ @datamodels.datamodel(frozen="strict")
+ class StrictModel:
+ value: Optional[int] = None
+ mode: Literal["a", "b"] = "a"
+ inner: Optional[StrictFrozenInner] = None
+
+ assert hash(StrictModel()) == hash(StrictModel())
+
+ def test_strict_frozen_accepts_container_of_immutable_fields(self):
+ @datamodels.datamodel(frozen="strict")
+ class StrictModel:
+ values: Tuple[int, ...]
+ inners: Tuple[StrictFrozenInner, ...] = ()
+
+ model = StrictModel(values=(1, 2), inners=(StrictFrozenInner(value=1),))
+ assert hash(model) == hash(StrictModel(values=(1, 2), inners=(StrictFrozenInner(value=1),)))
+
# Test module functions
def test_info_functions():
diff --git a/tests/eve_tests/unit_tests/test_extended_typing.py b/tests/eve_tests/unit_tests/test_extended_typing.py
index 1909ce7f68..1b105d8a64 100644
--- a/tests/eve_tests/unit_tests/test_extended_typing.py
+++ b/tests/eve_tests/unit_tests/test_extended_typing.py
@@ -8,7 +8,11 @@
from __future__ import annotations
+import builtins
+import collections
import collections.abc
+import contextlib
+import re
import sys
import types
import typing
@@ -21,16 +25,10 @@
Annotated,
Any,
Callable,
- Dict,
ForwardRef,
- FrozenSet,
- List,
Mapping,
Optional,
Sequence,
- Set,
- Tuple,
- Type,
TypeVar,
Union,
)
@@ -172,6 +170,77 @@ def __dlpack__(self):
assert not supports_dlpack(DLPackBufferWithWrongDevice())
+DEPRECATED_TYPING_ALIASES = [
+ ("Dict", "dict"),
+ ("FrozenSet", "frozenset"),
+ ("List", "list"),
+ ("Set", "set"),
+ ("Tuple", "tuple"),
+ ("Type", "type"),
+]
+
+
+@pytest.mark.parametrize(["name", "replacement"], DEPRECATED_TYPING_ALIASES)
+def test_deprecated_typing_alias_is_not_exported(name, replacement):
+ # These names are still bound by the 'typing' / 'typing_extensions' star imports in
+ # 'extended_typing', and its module '__getattr__' would otherwise forward them. Pin
+ # the rejection: dropping the guard would not make the names disappear, it would
+ # silently resolve them to the deprecated 'typing' objects instead of the builtins.
+ with pytest.raises(AttributeError, match=f"'{name}' is a deprecated 'typing' alias"):
+ getattr(xtyping, name)
+
+ assert replacement in str(pytest.raises(AttributeError, lambda: getattr(xtyping, name)).value)
+
+ with pytest.raises(ImportError):
+ exec(f"from gt4py.eve.extended_typing import {name}")
+
+ assert name not in dir(xtyping)
+
+
+@pytest.mark.parametrize(
+ ["name", "expected"],
+ [
+ ("Sequence", collections.abc.Sequence),
+ ("Callable", collections.abc.Callable),
+ ("AbstractSet", collections.abc.Set),
+ ("Match", re.Match),
+ ("ContextManager", contextlib.AbstractContextManager),
+ ("deque", collections.deque),
+ ],
+)
+def test_non_deprecated_aliases_are_still_re_exported(name, expected):
+ # Unlike the builtin generics above, these names are not deprecated -- only their
+ # 'typing' home is -- so 'extended_typing' keeps pointing them at the modern object.
+ assert getattr(xtyping, name) is expected
+
+
+@pytest.mark.parametrize(
+ ["name", "replacement", "args"],
+ [
+ (name, replacement, "int, str" if name == "Dict" else "int")
+ for name, replacement in DEPRECATED_TYPING_ALIASES
+ ],
+)
+def test_deprecated_typing_alias_still_resolves_in_forward_refs(name, replacement, args):
+ # These names stay valid in user-written annotations, so resolving a forward
+ # reference must not hit the rejection above. Both the bare and the 'typing.'-
+ # qualified spelling normalize to the *builtin* generic, which is what resolving
+ # through this module has always produced.
+ expected_args = tuple(eval(a) for a in args.split(", "))
+
+ for ref in (f"{name}[{args}]", f"typing.{name}[{args}]"):
+ resolved = xtyping.eval_forward_ref(ref)
+
+ assert xtyping.get_origin(resolved) is getattr(builtins, replacement)
+ assert xtyping.get_args(resolved) == expected_args
+ # A builtin generic alias, not the deprecated 'typing._GenericAlias' object.
+ assert type(resolved) is types.GenericAlias
+
+ # An explicit 'globalns' is left alone, so the bare name is not injected there.
+ with pytest.raises(NameError):
+ xtyping.eval_forward_ref(f"{name}[{args}]", globalns={})
+
+
@pytest.mark.parametrize("t", (int, float, dict, tuple, frozenset, collections.abc.Mapping))
def test_is_actual_valid_type(t):
assert xtyping.is_actual_type(t)
@@ -180,11 +249,11 @@ def test_is_actual_valid_type(t):
@pytest.mark.parametrize(
"t",
(
- Tuple[int],
- Tuple[int, ...],
- Tuple[int, int],
- Dict[str, Any],
- Dict[str, float],
+ tuple[int],
+ tuple[int, ...],
+ tuple[int, int],
+ dict[str, Any],
+ dict[str, float],
Mapping[int, float],
),
)
@@ -199,8 +268,10 @@ def test_is_actual_wrong_type(t):
(int, type),
(tuple, type),
(list, type),
- (Tuple[int, float], type(Tuple[int, float])),
- (List[int], type(List[int])),
+ # The deprecated 'typing' aliases and the builtin generics are distinct objects with
+ # distinct alias types, so both spellings are covered.
+ (typing.Tuple[int, float], type(typing.Tuple[int, float])),
+ (typing.List[int], type(typing.List[int])),
(tuple[int, float], types.GenericAlias),
(list[int], types.GenericAlias),
]
@@ -284,15 +355,15 @@ def f_partial(a: int) -> MissingRef: ...
"return": int,
}
- def f_nested_partial(a: int) -> Dict[str, MissingRef]: ...
+ def f_nested_partial(a: int) -> dict[str, MissingRef]: ...
assert xtyping.get_partial_type_hints(f_nested_partial) == {
"a": int,
- "return": ForwardRef("Dict[str, MissingRef]"),
+ "return": ForwardRef("dict[str, MissingRef]"),
}
assert xtyping.get_partial_type_hints(f_nested_partial, localns={"MissingRef": MissingRef}) == {
"a": int,
- "return": Dict[str, MissingRef],
+ "return": dict[str, MissingRef],
}
def f_annotated(a: Annotated[int, "Foo"]) -> float: # type: ignore[name-defined] # used to work, now mypy is going berserk for unknown reasons
@@ -310,10 +381,10 @@ def f_annotated(a: Annotated[int, "Foo"]) -> float: # type: ignore[name-defined
def test_eval_forward_ref():
- assert xtyping.eval_forward_ref("Dict[str, Tuple[int, float]]") == Dict[str, Tuple[int, float]]
+ assert xtyping.eval_forward_ref("dict[str, tuple[int, float]]") == dict[str, tuple[int, float]]
assert (
- xtyping.eval_forward_ref(ForwardRef("Dict[str, Tuple[int, float]]"))
- == Dict[str, Tuple[int, float]]
+ xtyping.eval_forward_ref(ForwardRef("dict[str, tuple[int, float]]"))
+ == dict[str, tuple[int, float]]
)
class MissingRef: ...
@@ -359,19 +430,19 @@ def test_infer_type():
assert xtyping.infer_type(None, none_as_type=False) is None
assert xtyping.infer_type(type(None), none_as_type=False) is None
- assert xtyping.infer_type(Dict[str, int]) == Dict[str, int]
+ assert xtyping.infer_type(dict[str, int]) == dict[str, int]
- assert xtyping.infer_type({1, 2, 3}) == Set[int]
- assert xtyping.infer_type(frozenset({"1", "2", "3"})) == FrozenSet[str]
+ assert xtyping.infer_type({1, 2, 3}) == set[int]
+ assert xtyping.infer_type(frozenset({"1", "2", "3"})) == frozenset[str]
- assert xtyping.infer_type({"a": [0], "b": [1]}) == Dict[str, List[int]]
+ assert xtyping.infer_type({"a": [0], "b": [1]}) == dict[str, list[int]]
- assert xtyping.infer_type(str) == Type[str]
+ assert xtyping.infer_type(str) == type[str]
class A: ...
assert xtyping.infer_type(A()) == A
- assert xtyping.infer_type(A) == Type[A]
+ assert xtyping.infer_type(A) == type[A]
def f1(): ...
@@ -382,30 +453,30 @@ def f2(a: int, b: float) -> None: ...
assert xtyping.infer_type(f2) == Callable[[int, float], type(None)]
def f3(
- a: Dict[Tuple[str, ...], List[int]],
- b: List[Callable[[List[int]], Set[Set[int]]]],
- c: Type[List[int]],
+ a: dict[tuple[str, ...], list[int]],
+ b: list[Callable[[list[int]], set[set[int]]]],
+ c: type[list[int]],
) -> Any: ...
assert (
xtyping.infer_type(f3)
== Callable[
[
- Dict[Tuple[str, ...], List[int]],
- List[Callable[[List[int]], Set[Set[int]]]],
- Type[List[int]],
+ dict[tuple[str, ...], list[int]],
+ list[Callable[[list[int]], set[set[int]]]],
+ type[list[int]],
],
Any,
]
)
- def f4(a: int, b: float, *, foo: Tuple[str, ...] = ()) -> None: ...
+ def f4(a: int, b: float, *, foo: tuple[str, ...] = ()) -> None: ...
assert xtyping.infer_type(f4) == Callable[[int, float], type(None)]
assert (
xtyping.infer_type(f4, annotate_callable_kwargs=True)
== Annotated[
- Callable[[int, float], type(None)], xtyping.CallableKwargsInfo({"foo": Tuple[str, ...]})
+ Callable[[int, float], type(None)], xtyping.CallableKwargsInfo({"foo": tuple[str, ...]})
]
)
@@ -424,11 +495,11 @@ def test_is_single_dispatch_callable():
# -- PEP 695 type aliases --
type SampleIntAlias = int
type SampleChainedAlias = SampleIntAlias
-type SampleGenericAlias[T] = Tuple[T, T]
+type SampleGenericAlias[T] = tuple[T, T]
type SampleRecursiveAlias = SampleRecursiveAlias
type SampleMutualAliasA = SampleMutualAliasB
type SampleMutualAliasB = SampleMutualAliasA
-type SampleDivergingGenericAlias[T] = SampleDivergingGenericAlias[Tuple[T]]
+type SampleDivergingGenericAlias[T] = SampleDivergingGenericAlias[tuple[T]]
def test_is_type_alias():
@@ -439,18 +510,18 @@ def test_is_type_alias():
assert xtyping.is_type_alias(typing_extensions.TypeAliasType("Backported", str))
assert not xtyping.is_type_alias(int)
- assert not xtyping.is_type_alias(List[int])
+ assert not xtyping.is_type_alias(list[int])
assert not xtyping.is_type_alias(SampleGenericAlias[int])
def test_eval_type_alias():
assert xtyping.eval_type_alias(SampleIntAlias) is int
assert xtyping.eval_type_alias(SampleChainedAlias) is int
- assert xtyping.eval_type_alias(SampleGenericAlias[int]) == Tuple[int, int]
+ assert xtyping.eval_type_alias(SampleGenericAlias[int]) == tuple[int, int]
def test_eval_type_alias_passes_through_non_aliases():
- for annotation in (int, List[int], xtyping.Any, None):
+ for annotation in (int, list[int], xtyping.Any, None):
assert xtyping.eval_type_alias(annotation) is annotation
@@ -523,7 +594,7 @@ def test_get_represented_types():
SampleReprA,
SampleReprB,
)
- assert xtyping.get_represented_types(List[int]) == (list,)
+ assert xtyping.get_represented_types(list[int]) == (list,)
def test_get_represented_types_resolves_type_aliases():
diff --git a/tests/eve_tests/unit_tests/test_traits.py b/tests/eve_tests/unit_tests/test_traits.py
index a8f1e7cc27..d277de33f1 100644
--- a/tests/eve_tests/unit_tests/test_traits.py
+++ b/tests/eve_tests/unit_tests/test_traits.py
@@ -11,7 +11,7 @@
import pytest
from gt4py import eve
-from gt4py.eve.extended_typing import Any, ClassVar, List
+from gt4py.eve.extended_typing import Any, ClassVar
from .. import definitions
@@ -34,7 +34,7 @@ class _NodeWithSymbolName(eve.Node):
class _NodeWithSymbolTable(eve.Node, eve.SymbolTableTrait):
- symbols: List[_NodeWithSymbolName]
+ symbols: list[_NodeWithSymbolName]
@pytest.fixture
@@ -75,9 +75,9 @@ class NodeWithRef(eve.Node):
ref_name: eve.Coerced[eve.SymbolRef]
class NodeWithSymbolTable(eve.Node, eve.traits.ValidatedSymbolTableTrait):
- symbols: List[NodeWithRef]
+ symbols: list[NodeWithRef]
- _NODE_SYMBOLS_: ClassVar[List] = []
+ _NODE_SYMBOLS_: ClassVar[list] = []
NodeWithSymbolTable.update_forward_refs(locals())
diff --git a/tests/eve_tests/unit_tests/test_type_validation.py b/tests/eve_tests/unit_tests/test_type_validation.py
index 023388b7a9..3736e001e8 100644
--- a/tests/eve_tests/unit_tests/test_type_validation.py
+++ b/tests/eve_tests/unit_tests/test_type_validation.py
@@ -22,15 +22,11 @@
from gt4py.eve.extended_typing import (
Any,
Callable,
- Dict,
Final,
ForwardRef,
- List,
Optional,
Sequence,
- Set,
SourceTypeAnnotation,
- Tuple,
Union,
)
@@ -56,8 +52,8 @@ class SampleDataClass:
# Each item should be a tuple like:
# ( annotation: Any, valid_values: Sequence, wrong_values: Sequence,
# globalns: Optional[Dict[str, Any]], localns: Optional[Dict[str, Any]] )
-SAMPLE_TYPE_DEFINITIONS: List[
- Tuple[Any, Sequence, Sequence, Optional[Dict[str, Any]], Optional[Dict[str, Any]]]
+SAMPLE_TYPE_DEFINITIONS: list[
+ tuple[Any, Sequence, Sequence, Optional[dict[str, Any]], Optional[dict[str, Any]]]
] = [
(bool, [True, False], [1, "True"], None, None),
(int, [1, -1], [1.0, "1"], None, None),
@@ -96,7 +92,7 @@ class SampleDataClass:
(typing.Union[int, float, str], [1, 3.0, "one"], [[1], [], 1j], None, None),
(typing.Optional[int], [1, None], [[1], [], 1j], None, None),
(
- typing.Dict[Union[int, float, str], Union[Tuple[int, Optional[float]], Set[int]]],
+ typing.Dict[Union[int, float, str], Union[tuple[int, Optional[float]], set[int]]],
[{1: (2, 3.0)}, {1.0: (2, None)}, {"1": {1, 2}}],
[{(1, 1.0, "1"): set()}, {1: [1]}, {"1": (1,)}],
None,
@@ -160,8 +156,8 @@ class SampleSlottedDataClass:
# -- PEP 695 type aliases --
type SampleIntAlias = int
type SampleChainedAlias = SampleIntAlias
-type SampleListAlias = List[SampleIntAlias]
-type SamplePairAlias[T] = Tuple[T, T]
+type SampleListAlias = list[SampleIntAlias]
+type SamplePairAlias[T] = tuple[T, T]
SAMPLE_TYPE_DEFINITIONS.extend(
@@ -169,7 +165,7 @@ class SampleSlottedDataClass:
(SampleIntAlias, [1, -1], [1.0, "1"], None, None),
(SampleChainedAlias, [1, -1], [1.0, "1"], None, None),
(SampleListAlias, ([1, 2, 3], []), (1, [1.0]), None, None),
- (List[SampleIntAlias], ([1, 2, 3], []), (1, [1.0]), None, None),
+ (list[SampleIntAlias], ([1, 2, 3], []), (1, [1.0]), None, None),
(Optional[SampleIntAlias], [1, None], ["1"], None, None),
(SamplePairAlias[int], [(1, 2)], [(1, "2"), (1,)], None, None),
]
@@ -185,8 +181,8 @@ def test_validators(
type_hint: SourceTypeAnnotation,
valid_values: Sequence,
wrong_values: Sequence,
- globalns: Optional[Dict[str, Any]],
- localns: Optional[Dict[str, Any]],
+ globalns: Optional[dict[str, Any]],
+ localns: Optional[dict[str, Any]],
):
for value in valid_values:
validator(value, type_hint, "", globalns=globalns, localns=localns)
@@ -205,8 +201,8 @@ def test_validator_factories(
type_hint: SourceTypeAnnotation,
valid_values: Sequence,
wrong_values: Sequence,
- globalns: Optional[Dict[str, Any]],
- localns: Optional[Dict[str, Any]],
+ globalns: Optional[dict[str, Any]],
+ localns: Optional[dict[str, Any]],
):
validator = factory(type_hint, name="", globalns=globalns, localns=localns)
for value in valid_values:
@@ -234,8 +230,8 @@ def test_validator_factories_with_invalid_hints(
SampleEmptyClass,
SampleDataClass,
SampleEnum,
- List[int],
- Dict[Tuple[int, ...], List[Set[complex]]],
+ list[int],
+ dict[tuple[int, ...], list[set[complex]]],
],
)
def test_simple_validation_cache(type_hint):
@@ -244,7 +240,7 @@ def test_simple_validation_cache(type_hint):
assert type_val.simple_type_validator_factory(type_hint, "value_2") is not validator
assert type_val.simple_type_validator_factory(Optional[float], "value") is not validator
- assert type_val.simple_type_validator_factory(List[float], "value") is not validator
+ assert type_val.simple_type_validator_factory(list[float], "value") is not validator
opt_validator = type_val.simple_type_validator_factory(type_hint, "value", required=False)
assert opt_validator not in (validator, None)
diff --git a/tests/next_tests/unit_tests/ffront_tests/test_diagnostic_messages.py b/tests/next_tests/unit_tests/ffront_tests/test_diagnostic_messages.py
index 321e57e8d5..62072cab18 100644
--- a/tests/next_tests/unit_tests/ffront_tests/test_diagnostic_messages.py
+++ b/tests/next_tests/unit_tests/ffront_tests/test_diagnostic_messages.py
@@ -15,14 +15,15 @@
regress. When changing a message, update the expectation here alongside.
"""
+import ast
import re
-import sys
import types
import pytest
import gt4py.next as gtx
from gt4py.next import errors, float32, float64
+from gt4py.next.ffront import dialect_parser
from gt4py.next.ffront.func_to_foast import FieldOperatorParser
@@ -82,6 +83,54 @@ def with_while(a: gtx.Field[[IDim], float64]) -> gtx.Field[[IDim], float64]:
assert "Note: Only a subset of Python is valid inside GT4Py functions." in rendered
+def test_try_statement_names_construct():
+ # TODO(egparedes): cover 'try: ... except : ...' here once it is diagnosed
+ # correctly. 'try/finally' below is one of the only two shapes that reach the
+ # catalogue; the far more common 'except ValueError:' form is intercepted by
+ # closure-variable type deduction first (see the TODO in 'func_to_foast'), so this
+ # test must not be read as covering 'try' statements in general.
+ def with_try(a: gtx.Field[[IDim], float64]) -> gtx.Field[[IDim], float64]:
+ try:
+ a = a + 1.0
+ finally:
+ pass
+ return a
+
+ err = parse_error(with_try)
+
+ assert isinstance(err, errors.UnsupportedPythonFeatureError)
+ assert err.message == "Unsupported Python syntax: 'try' statement."
+ assert any("Exception handling" in hint for hint in err.hints)
+
+
+def test_try_star_statement_is_catalogued():
+ # 'try*' cannot be reached through the frontend: it always names an exception
+ # type in its 'except*' clause, and that name is rejected as an unsupported
+ # closure variable before the AST is visited. Pin the catalogue entry itself,
+ # so the construct is named correctly if it ever does surface.
+ node = ast.parse("try:\n pass\nexcept* ValueError:\n pass").body[0]
+ assert isinstance(node, ast.TryStar)
+
+ feature, hints = dialect_parser._describe_unsupported_feature(node)
+
+ assert feature == "'try*' statement"
+ assert any("Exception handling" in hint for hint in hints)
+
+
+@pytest.mark.skipif(
+ not hasattr(ast, "TemplateStr"), reason="PEP 750 t-strings require Python >= 3.14."
+)
+def test_post_floor_construct_is_catalogued():
+ # Constructs newer than the supported floor are registered by name, so the
+ # catalogue can name them without breaking the import on older interpreters.
+ node = ast.parse('t"{a}"', mode="eval").body
+
+ feature, hints = dialect_parser._describe_unsupported_feature(node)
+
+ assert feature == "t-string"
+ assert any("cannot be computed" in hint for hint in hints)
+
+
def test_unlisted_construct_falls_back_to_ast_name():
def with_string(a: gtx.Field[[IDim], float64]) -> gtx.Field[[IDim], float64]:
f"{a}"
@@ -157,17 +206,6 @@ def test_add_note_uses_pep678_notes():
assert err.notes == []
-@pytest.mark.skipif(
- sys.version_info >= (3, 11),
- reason="On >=3.11 the traceback machinery renders '__notes__'; 'str()' does not.",
-)
-def test_add_note_folded_into_str_on_py310():
- err = errors.DSLError(None, "A message.")
- err.add_note("Extra context.")
-
- assert "Extra context." in str(err)
-
-
def test_toolchain_step_attaches_definition_context():
from gt4py.next.ffront import stages as ffront_stages
from gt4py.next.ffront.func_to_foast import func_to_foast
diff --git a/tests/next_tests/unit_tests/ffront_tests/test_fbuiltins.py b/tests/next_tests/unit_tests/ffront_tests/test_fbuiltins.py
index 87787195ef..907703ba92 100644
--- a/tests/next_tests/unit_tests/ffront_tests/test_fbuiltins.py
+++ b/tests/next_tests/unit_tests/ffront_tests/test_fbuiltins.py
@@ -6,16 +6,37 @@
# Please, refer to the LICENSE file in the root directory.
# SPDX-License-Identifier: BSD-3-Clause
+import typing
+
import numpy as np
import pytest
+from gt4py.next import common
from gt4py.next.ffront import fbuiltins
+from gt4py.next.type_system import type_specifications as ts
# values inside the domain of every unary math builtin (0.5 is invalid for `arccosh`)
_SAFE_INPUT = {"arccosh": 2.0}
+@pytest.mark.parametrize("tuple_spelling", [typing.Tuple, tuple, typing.Tuple[int], tuple[int]])
+def test_type_conversion_helper_accepts_both_tuple_spellings(tuple_spelling):
+ assert fbuiltins._type_conversion_helper(tuple_spelling) is ts.TupleType
+
+
+@pytest.mark.parametrize(
+ "union",
+ [
+ typing.Union[common.Field, typing.Tuple],
+ # PEP 604 builds a 'types.UnionType', which has no '__origin__' at all
+ common.Field | tuple,
+ ],
+)
+def test_type_conversion_helper_accepts_both_union_spellings(union):
+ assert fbuiltins._type_conversion_helper(union) == (ts.FieldType, ts.TupleType)
+
+
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
@pytest.mark.parametrize(
"name", fbuiltins.UNARY_MATH_NUMBER_BUILTIN_NAMES + fbuiltins.UNARY_MATH_FP_BUILTIN_NAMES
diff --git a/tests/next_tests/unit_tests/otf_tests/test_runners.py b/tests/next_tests/unit_tests/otf_tests/test_runners.py
index ecaf91408a..75e713d51b 100644
--- a/tests/next_tests/unit_tests/otf_tests/test_runners.py
+++ b/tests/next_tests/unit_tests/otf_tests/test_runners.py
@@ -210,6 +210,31 @@ def test_wait_for_compilation_untracks_successful_futures():
assert future not in compiled_program._ongoing_compilations
+def test_wait_for_compilation_groups_multiple_failures():
+ errors = [ValueError("first boom"), TypeError("second boom")]
+ # The futures have to stay referenced: tracking is weak, so a collected future
+ # is not reported and this would degrade to the single-failure path.
+ futures = [concurrent.futures.Future() for _ in errors]
+ for i, (future, error) in enumerate(zip(futures, errors)):
+ future.set_exception(error)
+ compiled_program._ongoing_compilations[future] = f"testee_{i} (backend)"
+
+ with pytest.raises(ExceptionGroup) as exc_info:
+ compiled_program.wait_for_compilation()
+
+ # Every failure keeps its own traceback instead of being flattened into the
+ # message of a single error, and each program is named. It is a plain
+ # 'ExceptionGroup', not just a 'BaseExceptionGroup', so 'except*' on 'Exception'
+ # catches it.
+ assert type(exc_info.value) is ExceptionGroup
+ assert exc_info.value.exceptions == tuple(errors)
+ assert "testee_0 (backend)" in str(exc_info.value)
+ assert "testee_1 (backend)" in str(exc_info.value)
+
+ # each failure is reported only once
+ compiled_program.wait_for_compilation()
+
+
def test_detect_cuda_archs_prefers_cudaarchs_env():
with (
mock.patch.dict(os.environ, {"CUDAARCHS": "80;90"}),
diff --git a/typing_tests/mypy.ini b/typing_tests/mypy.ini
new file mode 100644
index 0000000000..307a97b27a
--- /dev/null
+++ b/typing_tests/mypy.ini
@@ -0,0 +1,26 @@
+; Mypy configuration for the 'test_typing_exports' session.
+;
+; These tests check GT4Py's usability from *downstream client* code, so they must not
+; inherit the project's own '[tool.mypy]' table from 'pyproject.toml'. Given no
+; '--config-file', mypy discovers one by walking up from the plugin's temporary
+; execution directory (created under this folder) and reaches that table, which both
+; applies gt4py-internal strictness to client snippets and — because the table pins
+; 'python_version' to the supported floor — makes the 3.13 and 3.14 runs of this nox
+; session type-check as 3.12, silently collapsing the parametrization. The noxfile
+; therefore points '--mypy-ini-file' here instead of relying on discovery order.
+;
+; Deliberately no 'python_version' here: it must follow the interpreter running the
+; session, so each parametrized run checks the version it claims to check.
+
+[mypy]
+; The plugin is what downstream users are told to enable; see the module docstring of
+; 'gt4py.next.type_system.mypy_plugin'.
+plugins = gt4py.next.type_system.mypy_plugin
+ignore_missing_imports = True
+show_column_numbers = True
+show_error_codes = True
+; Kept from the project table (which this file no longer inherits): it is what makes
+; these tests check *exports*, by rejecting names GT4Py only imports but does not
+; explicitly re-export. The remaining gt4py-internal strictness is deliberately not
+; carried over, since it would apply to the client snippets themselves.
+implicit_reexport = False