Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ env:
# Oldest version that can reliably be downloaded
gams-version: 48.6.1
os: ubuntu-latest
python-version: "3.13"
python-version: "3.14"

permissions: {contents: read}

Expand Down
21 changes: 5 additions & 16 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ env:
GAMS_VERSION: 48.6.1 # Oldest version of GAMS that can reliably be downloaded
depth: 100 # Must be large enough to include the most recent release
label: "safe to test" # Label that must be applied to run on PRs from forks
python-version: "3.13" # For non-matrixed jobs
python-version: "3.14" # For non-matrixed jobs
# Install:
# - dask: to work around https://github.com/khaeru/genno/issues/171
# - ixmp: from its `main` branch.
Expand Down Expand Up @@ -57,34 +57,23 @@ jobs:
strategy:
matrix:
os:
- macos-13
- macos-latest
- ubuntu-latest
- windows-latest
python-version:
- "3.9" # Earliest version supported by message_ix
- "3.10"
- "3.10" # Earliest version supported by message_ix
- "3.11"
- "3.12"
- "3.13" # Latest version supported by message_ix
- "3.13"
- "3.14" # Latest version supported by message_ix

# Below this comment are newly released or development versions of
# Python. For these versions, binary wheels are not available for some
# dependencies, e.g. llvmlite, numba, numpy, and/or pandas. Compiling
# these on the job runner requires a more elaborate build environment,
# currently out of scope for the message_ix project.

# - "3.14.0-alpha.1" # Development version

exclude:
# Specific version combinations that are invalid / not to be used
# No arm64 distributions of JPype for these Pythons
- { os: macos-latest, python-version: "3.9" }
# Redundant with macos-latest
- { os: macos-13, python-version: "3.10" }
- { os: macos-13, python-version: "3.11" }
- { os: macos-13, python-version: "3.12" }
- { os: macos-13, python-version: "3.13" }
# - "3.15.0-alpha.1" # Development version

fail-fast: false

Expand Down
3 changes: 3 additions & 0 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Adjust any imports like the following:
All changes
-----------

- :mod:`message_ix` is tested and compatible with `Python 3.14 <https://www.python.org/downloads/release/python-3140/>`__ (:pull:`985`).
- Support for Python 3.9 is dropped (:pull:`985`), as it has reached end-of-life.
- Add representation of commodity flows associated with construction and retirement of technology capacity (:pull:`451`).

- New parameters
Expand All @@ -48,6 +50,7 @@ All changes
- Document the :ref:`minimum version of Java <install-java>` required for :class:`ixmp.JDBCBackend <ixmp.backend.jdbc.JDBCBackend>` (:pull:`962`).
- Improve type hinting (:pull:`963`).
- Fix capitalization in auxiliary_settings.gms to enable GDX output file compression on MacOS and Linux. (:pull:`965`)

All changes
-----------

Expand Down
3 changes: 1 addition & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import os
from importlib.metadata import version as get_version
from pathlib import Path
from typing import Optional

# -- Project information ---------------------------------------------------------------

Expand Down Expand Up @@ -193,7 +192,7 @@
# -- Options for sphinx.ext.intersphinx ------------------------------------------------


def local_inv(name: str, *parts: str) -> Optional[str]:
def local_inv(name: str, *parts: str) -> str | None:
"""Construct the path to a local intersphinx inventory."""

from importlib.util import find_spec
Expand Down
8 changes: 4 additions & 4 deletions message_ix/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from copy import copy
from dataclasses import InitVar, dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any

import ixmp.model.gams
from ixmp import config
Expand Down Expand Up @@ -96,7 +96,7 @@ class Item:
dims: tuple[str, ...] = field(default_factory=tuple)

#: Text description of the item.
description: Optional[str] = None
description: str | None = None

def __post_init__(self, expr):
if expr == "":
Expand Down Expand Up @@ -169,12 +169,12 @@ class GAMSModel(ixmp.model.gams.GAMSModel):
model_dir: Path

#: Optional minimum version of GAMS.
GAMS_min_version: Optional[str] = None
GAMS_min_version: str | None = None

#: Keyword arguments to map to GAMS `solve_args`.
keyword_to_solve_arg: list[tuple[str, type, str]]

def __init__(self, name: Optional[str] = None, **model_options) -> None:
def __init__(self, name: str | None = None, **model_options) -> None:
if gmv := self.GAMS_min_version:
# Check the minimum GAMS version.
version = ixmp.model.gams.gams_version() or ""
Expand Down
41 changes: 21 additions & 20 deletions message_ix/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Iterable, Mapping, Sequence
from functools import lru_cache
from itertools import chain, product, zip_longest
from typing import Optional, TypeVar, Union
from typing import TypeVar
from warnings import warn

import ixmp
Expand Down Expand Up @@ -255,12 +255,15 @@ def cat(self, name, cat):
def add_par(
self,
name: str,
key_or_data: Optional[
Union[int, str, Sequence[Union[int, str]], dict, pd.DataFrame]
] = None,
value: Union[float, Iterable[float], None] = None,
unit: Union[str, Iterable[str], None] = None,
comment: Union[str, Iterable[str], None] = None,
key_or_data: int
| str
| Sequence[int | str]
| dict
| pd.DataFrame
| None = None,
value: float | Iterable[float] | None = None,
unit: str | Iterable[str] | None = None,
comment: str | Iterable[str] | None = None,
) -> None:
# ixmp.Scenario.add_par() is typed as accepting only str, but this method also
# accepts int for "year"-like dimensions. Proxy the call to avoid type check
Expand Down Expand Up @@ -295,14 +298,12 @@ def add_par(
def add_set(
self,
name: str,
key: Union[
int,
str,
Iterable[object],
dict[str, Union[Sequence[int], Sequence[str]]],
pd.DataFrame,
],
comment: Union[str, Sequence[str], None] = None,
key: int
| str
| Iterable[object]
| dict[str, Sequence[int] | Sequence[str]]
| pd.DataFrame,
comment: str | Sequence[str] | None = None,
) -> None:
# ixmp.Scenario.add_par() is typed as accepting only str, but this method also
# accepts int for "year"-like dimensions. Proxy the call to avoid type check
Expand Down Expand Up @@ -373,8 +374,8 @@ def recurse(k, v, parent="World"):
def add_horizon(
self,
year: Iterable[int] = [],
firstmodelyear: Optional[int] = None,
data: Optional[dict] = None,
firstmodelyear: int | None = None,
data: dict | None = None,
) -> None:
"""Set the scenario time horizon via ``year`` and related categories.

Expand Down Expand Up @@ -502,7 +503,7 @@ def add_horizon(

def vintage_and_active_years(
self,
ya_args: Union[tuple[str, str], tuple[str, str, Union[int, str]], None] = None,
ya_args: tuple[str, str] | tuple[str, str, int | str] | None = None,
tl_only: bool = True,
**kwargs,
) -> pd.DataFrame:
Expand Down Expand Up @@ -653,7 +654,7 @@ def _valid(elem):
#: Alias for :meth:`vintage_and_active_years`.
yv_ya = vintage_and_active_years

def years_active(self, node: str, tec: str, yr_vtg: Union[int, str]) -> list[int]:
def years_active(self, node: str, tec: str, yr_vtg: int | str) -> list[int]:
"""Return periods in which `tec` hnology of `yr_vtg` can be active in `node`.

The :ref:`parameters <params-tech>` ``duration_period`` and
Expand Down Expand Up @@ -762,7 +763,7 @@ def solve(self, model="MESSAGE", solve_options={}, **kwargs):

def add_macro(
self,
data: Union[Mapping, os.PathLike],
data: Mapping | os.PathLike,
scenario=None,
check_convergence=True,
**kwargs,
Expand Down
10 changes: 5 additions & 5 deletions message_ix/macro/calibrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from functools import partial
from operator import itemgetter, mul
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -460,7 +460,7 @@ def validate_transform(
return df.set_index(idx)["value"]


def _validate_data(name: Optional[str], df: "DataFrame", s: Structures) -> list:
def _validate_data(name: str | None, df: "DataFrame", s: Structures) -> list:
"""Validate input `df` against `s` for MACRO parameter `name` calibration .

Parameters
Expand Down Expand Up @@ -553,7 +553,7 @@ def ym1(df: "Series", macro_periods: Collection[int]) -> int:


def add_model_data(
base: "Scenario", clone: "Scenario", data: Union[Mapping, os.PathLike]
base: "Scenario", clone: "Scenario", data: Mapping | os.PathLike
) -> None:
"""Calculate and add MACRO structure and data to `clone`.

Expand Down Expand Up @@ -643,8 +643,8 @@ def calibrate(s, check_convergence: bool = True, **kwargs):

def prepare_computer(
base: "Scenario",
target: Optional["Scenario"] = None,
data: Union[Mapping, os.PathLike, None] = None,
target: "Scenario | None" = None,
data: Mapping | os.PathLike | None = None,
) -> "genno.Computer":
"""Prepare a :class:`.Reporter` to perform MACRO calibration calculations.

Expand Down
6 changes: 3 additions & 3 deletions message_ix/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from collections.abc import Mapping
from functools import lru_cache, partial
from operator import itemgetter
from typing import TYPE_CHECKING, Union, cast
from typing import TYPE_CHECKING, cast

from genno.operator import broadcast_map
from ixmp.report import (
Expand Down Expand Up @@ -220,7 +220,7 @@ def from_scenario(cls, scenario, **kwargs) -> "Reporter":
f'Scenario "{scenario.model}/{scenario.scenario}" has no solution'
)
log.warning("Some reporting may not function as expected")
fail_action: Union[int, str] = logging.DEBUG
fail_action: int | str = logging.DEBUG
else:
fail_action = "raise"

Expand Down Expand Up @@ -288,7 +288,7 @@ def add_sankey(
# Generate the plotly.Figure object; return the key
return str(self.add(f"sankey figure {unique}", sankey, k[1], k[2]))

def add_tasks(self, fail_action: Union[int, str] = "raise") -> None:
def add_tasks(self, fail_action: int | str = "raise") -> None:
"""Add the pre-defined MESSAGEix reporting tasks to the Reporter.

Parameters
Expand Down
10 changes: 5 additions & 5 deletions message_ix/report/pyam.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional, TypedDict
from typing import TYPE_CHECKING, TypedDict

if TYPE_CHECKING:
import pandas
Expand All @@ -8,15 +8,15 @@ class CollapseMessageColsKw(TypedDict, total=False):
"""Type hint for :class:`dict` of keyword args to :func:`collapse_message_cols`."""

df: "pandas.DataFrame"
var: Optional[str]
kind: Optional[str]
var: str | None
kind: str | None
var_cols: list[str]


def collapse_message_cols(
df: "pandas.DataFrame",
var: Optional[str] = None,
kind: Optional[str] = None,
var: str | None = None,
kind: str | None = None,
var_cols=[],
) -> "pandas.DataFrame":
""":mod:`genno.compat.pyam` `collapse=...` callback for MESSAGEix quantities.
Expand Down
26 changes: 18 additions & 8 deletions message_ix/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import io
import os
import platform
from collections.abc import Callable, Generator
from itertools import product
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, Union
from typing import TYPE_CHECKING, Any

import ixmp
import numpy as np
Expand All @@ -28,12 +29,21 @@
def pytest_configure(config: pytest.Config) -> None:
"""Force iam-units to use a distinct cache for each worker.
Work around for https://github.com/hgrecco/flexcache/issues/6 /
https://github.com/IAMconsortium/units/issues/54.
Work arounds for:
1. https://github.com/hgrecco/flexcache/issues/6 /
https://github.com/IAMconsortium/units/issues/54.
2. https://github.com/python/cpython/issues/125235,
https://github.com/astral-sh/uv/issues/7036, or similar.
"""
name = f"iam-units-{os.environ.get('PYTEST_XDIST_WORKER', '')}".rstrip("-")
os.environ["IAM_UNITS_CACHE"] = str(config.cache.mkdir(name))

if GHA and platform.system() == "Windows":
import matplotlib

matplotlib.use("agg")


def pytest_report_header(config: pytest.Config, start_path: Path) -> str:
"""Add the message_ix import path to the pytest report header."""
Expand Down Expand Up @@ -90,7 +100,7 @@ def pytest_sessionstart() -> None:

# Create and populate ixmp databases

_ms: list[Union[str, float]] = [
_ms: list[str | float] = [
SCENARIO["dantzig"]["model"],
SCENARIO["dantzig"]["scenario"],
]
Expand Down Expand Up @@ -165,7 +175,7 @@ def make_austria( # noqa: C901
solve: bool = False,
quiet: bool = True,
*,
request: Optional["pytest.FixtureRequest"] = None,
request: "pytest.FixtureRequest | None" = None,
) -> Scenario:
"""Return an :class:`message_ix.Scenario` for the Austrian energy system.
Expand Down Expand Up @@ -352,7 +362,7 @@ def make_dantzig(
solve: bool = False,
multi_year: bool = False,
*,
request: Optional["pytest.FixtureRequest"] = None,
request: "pytest.FixtureRequest | None" = None,
**solve_opts,
) -> Scenario:
"""Return an :class:`message_ix.Scenario` for Dantzig's canning problem.
Expand Down Expand Up @@ -521,7 +531,7 @@ def make_westeros(
quiet: bool = True,
model_horizon: list[int] = [700, 710, 720],
*,
request: Optional["pytest.FixtureRequest"] = None,
request: "pytest.FixtureRequest | None" = None,
) -> Scenario:
"""Return a new :class:`message_ix.Scenario` containing the ‘Westeros’ model.
Expand Down Expand Up @@ -715,7 +725,7 @@ def make_subannual(
var_cost={},
operation_factor={},
*,
request: Optional["pytest.FixtureRequest"] = None,
request: "pytest.FixtureRequest | None" = None,
) -> Scenario:
"""Return an :class:`message_ix.Scenario` with subannual time resolution.
Expand Down
Loading
Loading