Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6552b5d
fix[cartesian]: detect '...' indices without 'ast.Ellipsis'
egparedes Aug 5, 2026
2a35005
fix[next]: accept 'types.UnionType' in '_type_conversion_helper'
egparedes Aug 5, 2026
5ee8d91
fix[eve]: repair the 'frozen="strict"' hashability check
egparedes Aug 5, 2026
13e1da6
build: align configuration with the Python 3.12 floor
egparedes Aug 5, 2026
37035af
docs: correct statements that still assume a Python 3.10 floor
egparedes Aug 5, 2026
6da11b0
refactor[next]: drop diagnostics shims written for the 3.10 floor
egparedes Aug 6, 2026
7b642fa
fix[eve]: apply the 'frozen="strict"' check to union members
egparedes Aug 6, 2026
d0480d3
refactor[cartesian]: reuse '_is_ellipsis_node' in the interval parser
egparedes Aug 6, 2026
068e322
style[next]: avoid shadowing 't' in '_type_conversion_helper'
egparedes Aug 6, 2026
4b33952
fix[eve]: decompose containers in the 'frozen="strict"' immutability …
egparedes Aug 7, 2026
8f5f24f
refactor[eve]: drop the dead Python 3.9 guard in 'extended_typing'
egparedes Aug 7, 2026
50a74fa
fix[next]: keep the typing-export tests parametrized over Python vers…
egparedes Aug 7, 2026
db6e0ac
docs[next]: record TODOs for the 'try' diagnostics and 'ExceptionGroup'
egparedes Aug 7, 2026
9ce5be5
Merge remote-tracking branch 'upstream/main' into py312-fix-latent-bugs
egparedes Aug 7, 2026
a86bfb9
refactor[eve]: stop re-exporting the deprecated 'typing' builtin aliases
egparedes Aug 10, 2026
203d610
fix[eve]: tighten 'frozen="strict"' for bare containers and type vari…
egparedes Aug 10, 2026
6ca8100
fix[eve]: keep deprecated 'typing' aliases resolvable in forward refe…
egparedes Aug 10, 2026
cdefb90
fix[next]: keep 'implicit_reexport' in the typing-export mypy config
egparedes Aug 10, 2026
7e7d399
docs[eve]: add an ADR for dropping the deprecated 'typing' aliases
egparedes Aug 10, 2026
4350c05
Revert "docs[eve]: add an ADR for dropping the deprecated 'typing' al…
egparedes Aug 10, 2026
8ad3029
Merge remote-tracking branch 'upstream/main' into py312-fix-latent-bugs
egparedes Aug 10, 2026
bac34fe
Merge remote-tracking branch 'upstream/main' into py312-fix-latent-bugs
egparedes Aug 13, 2026
ac1dd6c
fix[eve]: resolve PEP 695 aliases in the 'frozen="strict"' check
egparedes Aug 13, 2026
4163a51
docs[cartesian]: trim the '_is_ellipsis_node' docstring to what it does
egparedes Aug 14, 2026
3bd102d
fix[next]: group multiple compilation failures in an ExceptionGroup
egparedes Aug 14, 2026
069b942
refactor[eve]: drop the dead 'typing_extensions.Any' branches
egparedes Aug 14, 2026
532fdb2
docs[next]: clarify the 'add_note' rendering split and post-floor 'as…
egparedes Aug 14, 2026
d7617ef
Merge remote-tracking branch 'upstream/main' into py312-fix-latent-bugs
egparedes Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitpod.Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
28 changes: 12 additions & 16 deletions docs/development/next/error-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
11 changes: 9 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
# ///

Expand Down Expand Up @@ -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,
)
Expand Down
10 changes: 3 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -397,7 +394,6 @@ known-third-party = [
'devtools',
'factory',
'hypothesis',
'importlib_resources',
'jinja2',
'mako',
'networkx',
Expand Down
64 changes: 31 additions & 33 deletions src/gt4py/_core/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
Protocol,
Self,
Sequence,
Tuple,
Type,
TypeAlias,
TypeGuard,
TypeVar,
Expand Down Expand Up @@ -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]
)

Expand All @@ -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]
)

Expand All @@ -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]
)

Expand All @@ -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)


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand All @@ -370,7 +368,7 @@ class GTDimsInterface(Protocol):
"""

@property
def __gt_dims__(self) -> Tuple[str, ...]: ...
def __gt_dims__(self) -> tuple[str, ...]: ...


class GTOriginInterface(Protocol):
Expand All @@ -381,7 +379,7 @@ class GTOriginInterface(Protocol):
"""

@property
def __gt_origin__(self) -> Tuple[int, ...]: ...
def __gt_origin__(self) -> tuple[int, ...]: ...


# -- Device representation --
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 8 additions & 3 deletions src/gt4py/cartesian/frontend/gtscript_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading