diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index c084ea4be..065454992 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -42,7 +42,7 @@ jobs: with: # If the "Latest version testable on GitHub Actions" in pytest.yaml # is not the latest 3.x stable version, adjust here to match: - python-version: "3.10" + python-version: "3.12" cache: pip cache-dependency-path: "**/setup.cfg" diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 37e45d2af..25da5566d 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -13,7 +13,11 @@ concurrency: cancel-in-progress: true env: - GAMS_VERSION: 29.1.0 + # Version used until 2024-11-20; disabled + # GAMS_VERSION: 29.1.0 + # First version including a macOS arm64 distribution + GAMS_VERSION: 43.4.1 + # See description in lint.yml depth: 100 @@ -23,14 +27,15 @@ jobs: matrix: os: - macos-13 + - macos-latest - ubuntu-latest - windows-latest python-version: - - "3.8" # Earliest version supported by message_ix - - "3.9" + - "3.9" # Earliest version supported by message_ix - "3.10" - "3.11" - - "3.12" # Latest version supported by message_ix + - "3.12" + - "3.13" # 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 @@ -38,7 +43,17 @@ jobs: # these on the job runner requires a more elaborate build environment, # currently out of scope for the message_ix project. - # - "3.13.0-alpha.1" # Development version + # - "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" } fail-fast: false @@ -76,7 +91,7 @@ jobs: - uses: iiasa/actions/setup-gams@main with: version: ${{ env.GAMS_VERSION }} - # license: ${{ secrets.GAMS_LICENSE }} + license: ${{ secrets.GAMS_LICENSE }} - name: Install Python package and dependencies # By default, the below installs ixmp from the main branch. To run against @@ -86,9 +101,9 @@ jobs: pip install --upgrade "ixmp @ git+https://github.com/iiasa/ixmp.git@main" pip install .[tests] - # TEMPORARY Work around dask v2024.11.0; - # see https://github.com/khaeru/genno/issues/149 - pip install "dask < 2024.11.0" + # TEMPORARY With Python 3.13 pyam-iamc resolves to 1.3.1, which in turn + # limits pint < 0.17. Override. + pip install --upgrade pint - name: Run test suite using pytest env: @@ -111,7 +126,7 @@ jobs: strategy: matrix: os: - - macos-13 + - macos-latest - ubuntu-latest - windows-latest @@ -128,7 +143,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" cache: pip cache-dependency-path: "**/pyproject.toml" @@ -160,7 +175,7 @@ jobs: - uses: iiasa/actions/setup-gams@main with: version: ${{ env.GAMS_VERSION }} - # license: ${{ secrets.GAMS_LICENSE }} + license: ${{ secrets.GAMS_LICENSE }} - name: Install Python package and dependencies # By default, the below installs ixmp from the main branch. To run against @@ -170,9 +185,9 @@ jobs: pip install --upgrade "ixmp @ git+https://github.com/iiasa/ixmp.git@main" pip install .[tests] - # TEMPORARY Work around dask v2024.11.0; - # see https://github.com/khaeru/genno/issues/149 - pip install "dask < 2024.11.0" + # TEMPORARY With Python 3.13 pyam-iamc resolves to 1.3.1, which in turn + # limits pint < 0.17. Override. + pip install --upgrade pint - name: Install R dependencies and tutorial requirements run: | diff --git a/INSTALL.rst b/INSTALL.rst index 086fc1381..24f91ce17 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -16,7 +16,7 @@ Use the :ref:`install-quick` steps on this page if *all* of the following apply: - You have already installed on your system: - - :ref:`Python ` (version 3.8 or later) installed, along with either :program:`pip` or :program:`conda`; + - :ref:`Python ` (version 3.9 or later) installed, along with either :program:`pip` or :program:`conda`; - a :ref:`Java Runtime Environment (JRE) ` (if *not* using :program:`conda`; see :ref:`here `); and - :ref:`GAMS ` (version 24.8.1 or later). diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index e90475245..24a7501c9 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -10,6 +10,8 @@ Please familiarize yourself with these to foster an open and welcoming community All changes ----------- +- :mod:`message_ix` is tested and compatible with `Python 3.13 `__ (:pull:`881`). +- Support for Python 3.8 is dropped (:pull:`881`), as it has reached end-of-life. - Add option to :func:`.util.copy_model` from a non-default location of model files (:pull:`877`). .. _v3.9.0: diff --git a/doc/install-adv.rst b/doc/install-adv.rst index 97b3b808b..0a47d4554 100644 --- a/doc/install-adv.rst +++ b/doc/install-adv.rst @@ -18,8 +18,8 @@ Install system dependencies Python (required) ----------------- -|MESSAGEix| requires Python version 3.8 or greater. -We recommend the latest version; currently Python 3.12. +|MESSAGEix| requires Python version 3.9 or greater. +We recommend the latest version; currently Python 3.13. Common ways to install Python include: - Use the official `Python releases `_. diff --git a/message_ix/__init__.py b/message_ix/__init__.py index 98d674c7d..0b001c37e 100644 --- a/message_ix/__init__.py +++ b/message_ix/__init__.py @@ -1,12 +1,8 @@ import logging import sys +from importlib.metadata import PackageNotFoundError, version from pathlib import Path -try: - from importlib.metadata import PackageNotFoundError, version -except ImportError: # Python 3.7 - from importlib_metadata import PackageNotFoundError, version # type: ignore - from ixmp import ModelError, config from ixmp.model import MODELS from ixmp.util import DeprecatedPathFinder diff --git a/message_ix/core.py b/message_ix/core.py index 377f903c2..c6840c6b3 100755 --- a/message_ix/core.py +++ b/message_ix/core.py @@ -1,8 +1,9 @@ import logging import os +from collections.abc import Iterable, Mapping, Sequence from functools import lru_cache, partial from itertools import chain, product, zip_longest -from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Optional, Union from warnings import warn import ixmp @@ -236,7 +237,7 @@ def add_par( self, name: str, key_or_data: Optional[ - Union[int, str, Sequence[Union[int, str]], Dict, pd.DataFrame] + Union[int, str, Sequence[Union[int, str]], dict, pd.DataFrame] ] = None, value=None, unit: Optional[str] = None, @@ -253,7 +254,7 @@ def add_par( def add_set( self, name: str, - key: Union[int, str, Sequence[Union[str, int]], Dict, pd.DataFrame], + key: Union[int, str, Sequence[Union[str, int]], dict, pd.DataFrame], comment: Union[str, Sequence[str], None] = None, ) -> None: # ixmp.Scenario.add_par() is typed as accepting only str, but this method also @@ -450,7 +451,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: Union[tuple[str, str], tuple[str, str, Union[int, str]], None] = None, tl_only: bool = True, **kwargs, ) -> pd.DataFrame: @@ -600,7 +601,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: Union[int, str]) -> list[int]: """Return periods in which `tec` hnology of `yr_vtg` can be active in `node`. The :ref:`parameters ` ``duration_period`` and diff --git a/message_ix/macro.py b/message_ix/macro.py index 148ab8c14..6946f5e8a 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,21 +1,11 @@ import logging import os +from collections.abc import Collection, Hashable, Iterable, Mapping, MutableMapping from dataclasses import dataclass from functools import partial from operator import itemgetter, mul from pathlib import Path -from typing import ( - TYPE_CHECKING, - Collection, - Hashable, - Iterable, - List, - Mapping, - MutableMapping, - Optional, - Set, - Union, -) +from typing import TYPE_CHECKING, Optional, Union import numpy as np import pandas as pd @@ -129,11 +119,11 @@ def add_par( class Structures: """MACRO structure information.""" - level: Set[str] - node: Set[str] - sector: Set[str] + level: set[str] + node: set[str] + sector: set[str] #: Model years for which MACRO is calibrated. - year: Set[int] + year: set[int] def add_structure( @@ -323,7 +313,7 @@ def growth(gdp_calibrate) -> "DataFrame": return growth.dropna() -def macro_periods(demand: "Quantity", config: "DataFrame") -> Set[int]: +def macro_periods(demand: "Quantity", config: "DataFrame") -> set[int]: """Periods ("years") for the MACRO model. The intersection of those appearing in the `config` data and in the ``DEMAND`` @@ -433,7 +423,7 @@ def total_cost(model_cost: "DataFrame", cost_ref: "DataFrame", ym1: int) -> "Dat ) -def unique_set(column: str, df: "DataFrame") -> Set: +def unique_set(column: str, df: "DataFrame") -> set: """A :class:`set` of the unique elements in `column` of `df`.""" return set(df[column].dropna().unique()) @@ -475,7 +465,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: Optional[str], df: "DataFrame", s: Structures) -> list: """Validate input `df` against `s` for MACRO parameter `name` calibration . Parameters @@ -502,7 +492,7 @@ def _validate_data(name: Optional[str], df: "DataFrame", s: Structures) -> List: # Check required dimensions if name is None: - dims: List[str] = [] + dims: list[str] = [] else: item_name = name.replace("_ref", "_MESSAGE") item = MACRO.items[item_name] diff --git a/message_ix/models.py b/message_ix/models.py index 5c9253f71..e1567d316 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -1,10 +1,11 @@ import logging from collections import ChainMap +from collections.abc import Mapping, MutableMapping from copy import copy from dataclasses import InitVar, dataclass, field from functools import partial from pathlib import Path -from typing import Mapping, MutableMapping, Optional, Tuple +from typing import Optional from warnings import warn import ixmp.model.gams @@ -79,10 +80,10 @@ class Item: #: Coordinates of the item; that is, the names of sets that index its dimensions. #: The same set name may be repeated if it indexes multiple dimensions. - coords: Tuple[str, ...] = field(default_factory=tuple) + coords: tuple[str, ...] = field(default_factory=tuple) #: Dimensions of the item. - dims: Tuple[str, ...] = field(default_factory=tuple) + dims: tuple[str, ...] = field(default_factory=tuple) #: Text description of the item. description: Optional[str] = None diff --git a/message_ix/report/__init__.py b/message_ix/report/__init__.py index 01c8de6a7..5e17d7744 100644 --- a/message_ix/report/__init__.py +++ b/message_ix/report/__init__.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from functools import lru_cache, partial from operator import itemgetter -from typing import TYPE_CHECKING, List, Mapping, Tuple, Union, cast +from typing import TYPE_CHECKING, Union, cast from genno.operator import broadcast_map from ixmp.report import ( @@ -53,7 +54,7 @@ #: contains the value 1 at every valid (type_addon, ta) location, and 0 elsewhere. #: 2. Simple products of 2 or mode quantities. #: 3. Other derived quantities. -TASKS0: Tuple[Tuple, ...] = ( +TASKS0: tuple[tuple, ...] = ( # Mapping sets ("map_addon", "map_as_qty", "cat_addon", "t"), ("map_emission", "map_as_qty", "cat_emission", "e"), @@ -112,7 +113,7 @@ #: Quantities to automatically convert to IAMC format using #: :func:`~genno.compat.pyam.operator.as_pyam`. -PYAM_CONVERT: List[Tuple[str, "CollapseMessageColsKw"]] = [ +PYAM_CONVERT: list[tuple[str, "CollapseMessageColsKw"]] = [ ("out:nl-t-ya-m-nd-c-l", dict(kind="ene", var="out")), ("in:nl-t-ya-m-no-c-l", dict(kind="ene", var="in")), ("CAP:nl-t-ya", dict(var="capacity")), @@ -148,11 +149,11 @@ @lru_cache(1) -def get_tasks() -> List[Tuple[Tuple, Mapping]]: +def get_tasks() -> list[tuple[tuple, Mapping]]: """Return a list of tasks describing MESSAGE reporting calculations.""" # Assemble queue of items to add. Each element is a 2-tuple of (positional, keyword) # arguments for Reporter.add() - to_add: List[Tuple[Tuple, Mapping]] = [] + to_add: list[tuple[tuple, Mapping]] = [] strict = dict(strict=True) @@ -160,7 +161,7 @@ def get_tasks() -> List[Tuple[Tuple, Mapping]]: if len(t) == 2 and isinstance(t[1], dict): # (args, kwargs) → update kwargs with strict t[1].update(strict) - to_add.append(cast(Tuple[Tuple, Mapping], t)) + to_add.append(cast(tuple[tuple, Mapping], t)) else: # args only → use strict as kwargs to_add.append((t, strict)) diff --git a/message_ix/report/operator.py b/message_ix/report/operator.py index f0bb3dd6b..36df38ea9 100644 --- a/message_ix/report/operator.py +++ b/message_ix/report/operator.py @@ -1,12 +1,5 @@ -from typing import ( - TYPE_CHECKING, - Dict, - List, - Literal, - Mapping, - Tuple, - overload, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Literal, overload import pandas as pd @@ -30,7 +23,7 @@ def as_message_df( dims: Mapping, common: Mapping, wrap: Literal[True] = True, -) -> Dict[str, pd.DataFrame]: ... +) -> dict[str, pd.DataFrame]: ... @overload @@ -87,7 +80,7 @@ def as_message_df(qty, name, dims, common, wrap=True): return {name: df} if wrap else df -def model_periods(y: List[int], cat_year: pd.DataFrame) -> List[int]: +def model_periods(y: list[int], cat_year: pd.DataFrame) -> list[int]: """Return the elements of `y` beyond the firstmodelyear of `cat_year`.""" return list( filter( @@ -98,7 +91,7 @@ def model_periods(y: List[int], cat_year: pd.DataFrame) -> List[int]: ) -def plot_cumulative(x: "AnyQuantity", y: "AnyQuantity", labels: Tuple[str, str, str]): +def plot_cumulative(x: "AnyQuantity", y: "AnyQuantity", labels: tuple[str, str, str]): """Plot a supply curve. - `x` and `y` must share the first two dimensions. @@ -161,7 +154,7 @@ def plot_cumulative(x: "AnyQuantity", y: "AnyQuantity", labels: Tuple[str, str, def stacked_bar( qty: "AnyQuantity", - dims: Tuple[str, ...] = ("nl", "t", "ya"), + dims: tuple[str, ...] = ("nl", "t", "ya"), units: str = "", title: str = "", cf: float = 1.0, diff --git a/message_ix/report/pyam.py b/message_ix/report/pyam.py index ec41e658c..716154671 100644 --- a/message_ix/report/pyam.py +++ b/message_ix/report/pyam.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional, TypedDict +from typing import TYPE_CHECKING, Optional, TypedDict if TYPE_CHECKING: import pandas @@ -10,7 +10,7 @@ class CollapseMessageColsKw(TypedDict, total=False): df: "pandas.DataFrame" var: Optional[str] kind: Optional[str] - var_cols: List[str] + var_cols: list[str] def collapse_message_cols( diff --git a/message_ix/testing/__init__.py b/message_ix/testing/__init__.py index 351ae320a..d80692a29 100644 --- a/message_ix/testing/__init__.py +++ b/message_ix/testing/__init__.py @@ -1,8 +1,9 @@ import io import os +from collections.abc import Generator from itertools import product from pathlib import Path -from typing import TYPE_CHECKING, Generator, List, Optional, Union +from typing import TYPE_CHECKING, Optional, Union import numpy as np import pandas as pd @@ -48,7 +49,7 @@ def pytest_sessionstart(): # Create and populate ixmp databases -_ms: List[Union[str, float]] = [ +_ms: list[Union[str, float]] = [ SCENARIO["dantzig"]["model"], SCENARIO["dantzig"]["scenario"], ] diff --git a/message_ix/tests/report/test_operator.py b/message_ix/tests/report/test_operator.py index 2a82c552c..c4e5146ca 100644 --- a/message_ix/tests/report/test_operator.py +++ b/message_ix/tests/report/test_operator.py @@ -1,5 +1,6 @@ +from collections.abc import Mapping from functools import partial -from typing import Any, Mapping, Tuple +from typing import Any import matplotlib import pandas as pd @@ -17,7 +18,7 @@ def test_as_message_df(test_mp) -> None: q = random_qty(dict(c=3, h=2, nl=5)) q.units = "kg" - args: Tuple[Any, Mapping, Mapping] = ( + args: tuple[Any, Mapping, Mapping] = ( literal("demand"), dict(commodity="c", node="nl", time="h"), dict(level="l", year=2022), diff --git a/message_ix/tests/test_feature_duration_time.py b/message_ix/tests/test_feature_duration_time.py index 9147fecd1..09b8870e0 100644 --- a/message_ix/tests/test_feature_duration_time.py +++ b/message_ix/tests/test_feature_duration_time.py @@ -13,8 +13,7 @@ # A function for generating a simple model with sub-annual time slices -# FIXME reduce complexity 14 → ≤13 -def model_generator( # noqa: C901 +def model_generator( test_mp, comment, tec_time, @@ -66,23 +65,21 @@ def model_generator( # noqa: C901 map_time = {} for [tmp_lvl, number, parent] in time_steps: scen.add_set("lvl_temporal", tmp_lvl) - if parent == "year": - times = [tmp_lvl[0] + "-" + str(x + 1) for x in range(number)] - else: - times = [ + times = ( + [tmp_lvl[0] + "-" + str(x + 1) for x in range(number)] + if parent == "year" + else [ p + "_" + tmp_lvl[0] + "-" + str(x + 1) for (p, x) in product(map_time[parent], range(number)) ] + ) map_time[tmp_lvl] = times scen.add_set("time", times) # Adding "map_temporal_hierarchy" and "duration_time" for h in times: - if parent == "year": - p = "year" - else: - p = h.split("_" + tmp_lvl[0])[0] + p = "year" if parent == "year" else h.split("_" + tmp_lvl[0])[0] # Temporal hierarchy (order: temporal level, time, parent time) scen.add_set("map_temporal_hierarchy", [tmp_lvl, h, p]) @@ -111,10 +108,11 @@ def model_generator( # noqa: C901 "time" ] # If technology is linking two different temporal levels - if tmp_lvl_in != tmp_lvl_out: - time_pairs = product(times_in, times_out) - else: - time_pairs = zip(times_in, times_out) + time_pairs = ( + product(times_in, times_out) + if tmp_lvl_in != tmp_lvl_out + else zip(times_in, times_out) + ) # Configuring data for "time_origin" and "time" in "input" for h_in, h_act in time_pairs: diff --git a/message_ix/tests/test_feature_vintage_and_active_years.py b/message_ix/tests/test_feature_vintage_and_active_years.py index d596229b7..e47605d0e 100644 --- a/message_ix/tests/test_feature_vintage_and_active_years.py +++ b/message_ix/tests/test_feature_vintage_and_active_years.py @@ -1,5 +1,6 @@ +from collections.abc import Sequence from functools import lru_cache -from typing import Optional, Sequence, Tuple +from typing import Optional import numpy as np import pandas as pd @@ -12,7 +13,7 @@ @lru_cache() -def _generate_yv_ya(periods: Tuple[int, ...]) -> pd.DataFrame: +def _generate_yv_ya(periods: tuple[int, ...]) -> pd.DataFrame: """All meaningful combinations of (vintage year, active year) given `periods`.""" # commented: currently unused, this does the same as the line below, using (start # period, final period, uniform ``duration_period). The intermediate periods are @@ -34,7 +35,7 @@ def _generate_yv_ya(periods: Tuple[int, ...]) -> pd.DataFrame: def _setup( mp: Platform, years: Sequence[int], firstmodelyear: int, tl_years=None -) -> Tuple[Scenario, pd.DataFrame]: +) -> tuple[Scenario, pd.DataFrame]: """Common setup for test of :meth:`.vintage_and_active_years`. Adds: diff --git a/message_ix/tests/test_nightly.py b/message_ix/tests/test_nightly.py index b981ac647..389db7bcb 100644 --- a/message_ix/tests/test_nightly.py +++ b/message_ix/tests/test_nightly.py @@ -1,9 +1,6 @@ """Slow-running tests for nightly continuous integration.""" -from functools import partial # noqa: F401 - import ixmp -import numpy as np # noqa: F401 import pytest import message_ix diff --git a/message_ix/tests/test_tutorials.py b/message_ix/tests/test_tutorials.py index 4b5b5e2f9..3e92de825 100644 --- a/message_ix/tests/test_tutorials.py +++ b/message_ix/tests/test_tutorials.py @@ -2,7 +2,7 @@ import os import sys from shutil import copyfile -from typing import List, Tuple, Union +from typing import Union import numpy as np import pytest @@ -69,7 +69,7 @@ def _t(group: Union[str, None], basename: str, *, check=None, marks=None): #: Argument values to parametrize :func:`test_tutorial`. -TUTORIALS: List[Tuple] = [ +TUTORIALS: list[tuple] = [ # IPython kernel _t("w0", f"{W}_baseline", check=[("solve-objective-value", 159025.82812)]), # NB could also check objective function values in the following tutorials; however, diff --git a/message_ix/tools/add_year/__init__.py b/message_ix/tools/add_year/__init__.py index d0f593a11..62e6c7f94 100644 --- a/message_ix/tools/add_year/__init__.py +++ b/message_ix/tools/add_year/__init__.py @@ -12,7 +12,7 @@ # %% I) Importing required packages import logging -from typing import List, Literal, Optional, Union +from typing import Literal, Optional, Union import numpy as np import pandas as pd @@ -49,9 +49,9 @@ def intpol( def slice_df( df: pd.DataFrame, - idx: List[str], + idx: list[str], level: str, - locator: List, + locator: list, value: Union[int, str, None], ) -> pd.DataFrame: """Slice a MultiIndex DataFrame and set a value to a specific level. @@ -105,13 +105,13 @@ def unit_uniform(df: pd.DataFrame) -> pd.DataFrame: def add_year( sc_ref: Scenario, sc_new: Scenario, - years_new: List[int], + years_new: list[int], firstyear_new: Optional[int] = None, lastyear_new: Optional[int] = None, macro: bool = False, baseyear_macro: Optional[int] = None, - parameter: Union[List[str], Literal["all"]] = "all", - region: Union[List[str], Literal["all"]] = "all", + parameter: Union[list[str], Literal["all"]] = "all", + region: Union[list[str], Literal["all"]] = "all", rewrite: bool = True, unit_check: bool = True, extrapol_neg: Optional[float] = None, @@ -160,7 +160,7 @@ def add_year( # ------------------------------------------------------------------------- # III.B) Adding parameters and calculating the missing values for the # additonal years - par_list: List[str] + par_list: list[str] if parameter in ("all", ["all"]): par_list = sorted(sc_ref.par_list()) elif isinstance(parameter, list): @@ -174,7 +174,7 @@ def add_year( if "technical_lifetime" in par_list: par_list.insert(0, par_list.pop(par_list.index("technical_lifetime"))) - reg_list: List[str] + reg_list: list[str] if region in ("all", ["all"]): nodes: pd.Series = sc_ref.set("node") # type: ignore reg_list = nodes.tolist() @@ -287,7 +287,7 @@ def add_year( def add_year_set( # noqa: C901 sc_ref: Scenario, sc_new: Scenario, - years_new: List[int], + years_new: list[int], firstyear_new: Optional[int] = None, lastyear_new: Optional[int] = None, baseyear_macro: Optional[int] = None, @@ -336,7 +336,7 @@ def add_year_set( # noqa: C901 baseyear_macro ) - yr_pair: List[List[Union[int, str]]] = [] + yr_pair: list[list[Union[int, str]]] = [] for yr in years_new: yr_pair.append([yr, yr]) yr_pair.append(["cumulative", yr]) @@ -385,7 +385,7 @@ def add_year_set( # noqa: C901 log.info("All the sets updated and added to the new scenario") -def next_step_bigger_than_previous(x: List[int], i: int) -> bool: +def next_step_bigger_than_previous(x: list[int], i: int) -> bool: return x[i + 1] - x[i] > x[i] - x[i - 1] @@ -393,9 +393,9 @@ def next_step_bigger_than_previous(x: List[int], i: int) -> bool: def add_year_par( sc_ref: Scenario, sc_new: Scenario, - yrs_new: List[int], + yrs_new: list[int], parname: str, - reg_list: List[str], + reg_list: list[str], firstyear_new: int, extrapolate: bool = False, rewrite: bool = True, @@ -561,8 +561,8 @@ def add_year_par( # FIXME reduce complexity 18 → ≤13 def interpolate_1d( # noqa: C901 df: pd.DataFrame, - yrs_new: List[int], - horizon: List[int], + yrs_new: list[int], + horizon: list[int], year_col: str, value_col: str = "value", extrapolate: bool = False, @@ -717,16 +717,16 @@ def interpolate_1d( # noqa: C901 # FIXME reduce complexity 38 → ≤13 def interpolate_2d( # noqa: C901 df: pd.DataFrame, - yrs_new: List[int], - horizon: List[int], + yrs_new: list[int], + horizon: list[int], year_ref: str, year_col: str, - tec_list: List[str], + tec_list: list[str], par_tec: pd.DataFrame, value_col: str = "value", extrapolate: bool = False, extrapol_neg: Optional[float] = None, - year_diff: Optional[List[int]] = None, + year_diff: Optional[list[int]] = None, bound_extend: bool = True, ): """Interpolate parameters with two dimensions related year. diff --git a/message_ix/tools/lp_diag/__init__.py b/message_ix/tools/lp_diag/__init__.py index 37714c204..e69033dd3 100644 --- a/message_ix/tools/lp_diag/__init__.py +++ b/message_ix/tools/lp_diag/__init__.py @@ -1,10 +1,9 @@ """Analyse MPS-format files.""" + # Written by Marek Makowski, ECE Program of IIASA, in March 2023. import math -import typing from collections import Counter -from typing import List import numpy as np import pandas as pd @@ -226,7 +225,7 @@ def mps_sum(self): ) print(f"Distribution of the GF (objective) values:\n{df.describe()}") - def add_row(self, words: List[str], n_line: int): + def add_row(self, words: list[str], n_line: int): """Process current line of the ROWS section. While processing the ROWS section the row attributes are initialized to the @@ -268,7 +267,7 @@ def add_row(self, words: List[str], n_line: int): # (to be changed in rhs/ranges) [lo_bnd, upp_bnd] self.row_att(row_seq, row_name, row_type, "rows") - def add_coeff(self, words: List[str], n_line: int): + def add_coeff(self, words: list[str], n_line: int): """Process current line of the COLUMNS section. The section defines both column names and values of the matrix coefficients. @@ -349,7 +348,7 @@ def add_coeff(self, words: List[str], n_line: int): self.mat_col.append(col_seq) self.mat_val.append(val) - def add_rhs(self, words: List[str], n_line: int): + def add_rhs(self, words: list[str], n_line: int): """Process current line of the RHS section. The section defines both column names and values of the matrix coefficients. @@ -497,7 +496,7 @@ def add_range(self, words: str, n_line: int): self.row_att(row_seq, row_name, row_type, "ranges", val) self.n_ranges += 1 - def add_bnd(self, words: List[str], n_line: int): + def add_bnd(self, words: list[str], n_line: int): """Process current line of the BOUNDS section. The section defines both column names and values of the matrix coefficients. @@ -850,7 +849,7 @@ def locate_outliers(self, small: bool = True, thresh: int = -7, max_rec: int = 5 def get_entity_info( self, mat_row: pd.Series, by_row: bool = True - ) -> typing.Tuple[int, str]: + ) -> tuple[int, str]: """Return info on the entity (row or col) defining the given matrix coefficient. Each row of the dataFrame contains the definition (composed of the row_seq, diff --git a/message_ix/tools/lp_diag/cli.py b/message_ix/tools/lp_diag/cli.py index 917f7ebf9..2843d28d1 100644 --- a/message_ix/tools/lp_diag/cli.py +++ b/message_ix/tools/lp_diag/cli.py @@ -1,4 +1,5 @@ """Command-line interface to :mod:`.lp_diag`.""" + # Written by Marek Makowski, ECE Program of IIASA, in March 2023. # Developed in PyCharm, with Python 3.10.4. # diff --git a/pyproject.toml b/pyproject.toml index ab8b7306d..7f47e878e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,19 +20,18 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: R", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "click", - "importlib_resources; python_version <= '3.8'", "ixmp >= 3.9.0", "genno[pyam] >= 1.20", "numpy", @@ -87,7 +86,10 @@ omit = [ ] [tool.mypy] -exclude = ["doc/"] +exclude = [ + "build/", + "doc/", +] # TODO Remove this once it has become default with mypy 2.0: local_partial_types = true