Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38ea9ce
Enable strict mypy settings
glatterf42 Jun 6, 2025
a70c972
Bump mypy version for pre-commit
glatterf42 Jun 6, 2025
40e5173
Satisfy mypy with latest dependecy versions
glatterf42 Jun 10, 2025
2cf5232
Satisfy pre-commit-mypy to pass CI checks
glatterf42 Jun 10, 2025
fecb7f8
Bump ruff version for pre-commit
glatterf42 Jun 10, 2025
9bd28d6
Make typing imports safe for all Python versions
glatterf42 Jun 10, 2025
a2d7cb2
Fix _lru_cache_wrapper[Any] annotation for docs build
glatterf42 Jun 10, 2025
6650329
Sort and comment mypy options in pyproject.toml
khaeru Jul 7, 2025
11cb3a2
Restore TimeSeries._backend() method
khaeru Jul 7, 2025
474773d
Add is_ixmp4backend() TypeGuard
khaeru Jul 7, 2025
2d903c7
Extend .types
khaeru Jul 7, 2025
e43e09a
Improve .backend.common.ItemType
khaeru Jul 7, 2025
634a165
Adjust type hints in .backend.base
khaeru Jul 7, 2025
7df928a
Improve .model.base
khaeru Jul 7, 2025
56b740f
Type maybe_commit(…, condition=…) as bool
khaeru Jul 7, 2025
d1e4693
Simplify imports from importlib_metadata
khaeru Jul 7, 2025
3f1d58b
Update type hints
khaeru Jul 7, 2025
5712140
Handle int in .util.as_str_list()
khaeru Jul 7, 2025
8f8506c
Simplify .backend.ixmp4 and its typing
khaeru Jul 7, 2025
aa24a9f
Add Scenario.iter_par_data()
khaeru Jul 7, 2025
edb6125
Flatten Scenario.add_set()
khaeru Jul 7, 2025
ba732b4
Add .core.item
khaeru Jul 7, 2025
c109c31
Use .core.item in .backend.jdbc
khaeru Jul 7, 2025
2c0ffa5
Ensure dataframe in TestScenario.test_var()
khaeru Jul 7, 2025
f6f3818
Satisfy mypy in .util.format_scenario_list()
khaeru Jul 7, 2025
b23e25a
Add pytest_httpserver to mypy/pre-commit config
khaeru Jul 8, 2025
19d0b15
Simplify .util.diff()
khaeru Jul 8, 2025
9f943ca
Handle scalars in IXMP4Backend
khaeru Jul 11, 2025
73116e3
Run 3 additional tests with IXMP4Backend.
khaeru Jul 11, 2025
141777e
Improve .types
khaeru Jul 18, 2025
b4f9e29
Add #581 to release notes
khaeru Jul 18, 2025
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
5 changes: 3 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.1
rev: v1.16.0
hooks:
- id: mypy
pass_filenames: false
Expand All @@ -11,11 +11,12 @@ repos:
- nbclient
- pandas-stubs
- pytest
- pytest_httpserver
- Sphinx
- werkzeug
- xarray
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.1
rev: v0.11.13
hooks:
- id: ruff
- id: ruff-format
Expand Down
51 changes: 51 additions & 0 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,62 @@
Next release
============

Migration notes
---------------

- Replace calls to :meth:`.Scenario.items` with :py:`par_data=True`:

.. code-block:: python

for name, df in scenario.items(..., par_data=True):
...

with:

.. code-block:: python

for name, df in scenario.iter_par_data(...):
...

- Replace use of the shorthand:

.. code-block:: python

scenario._backend("method_name", ...)

with:

.. code-block:: python

scenario.platform._backend.method_name(...)

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

- Improve :class:`.IXMP4Backend` (:pull:`581`):

- Support creation and modification of 0-dimensional parameters (:class:`ixmp4.Scalar`).

- New method :meth:`.Scenario.iter_par_data` (:pull:`581`).
:meth:`.Scenario.items` no longer supports iterating over item *contents*.
- Improve type hinting in :mod:`ixmp` (:pull:`581`).
This supports more precise and complete type checking of downstream code that uses :mod:`ixmp`.

- New module :mod:`ixmp.types` containing types for annotating code that uses :mod:`ixmp`.
- New type guard function :func:`.util.ixmp4.is_ixmp4backend`.

- Update :class:`.ItemType` (:pull:`581`):

- New method :meth:`~.ItemType.is_model_data`.
- Remove short aliases such as :py:`ItemType.S` for :attr:`~.ItemType.SET`.

- New module/class :class:`ixmp.core.item.Item` and subclasses (:pull:`581`).
These encapsulate structural information about :ref:`data-model-data`.
- Document (at :ref:`system-dependencies`) that JRE version ≥ 11 is required
when using :class:`.JDBCBackend` with :mod:`jpype` version ≥ 1.6.0 (:pull:`586`).
- The :meth:`.TimeSeries._backend` shorthand method is deprecated (:pull:`581`).
Calling this method emits :class:`DeprecationWarning`,
and the method will be removed in a future version of :mod:`ixmp`.

.. _v3.11.1:

Expand Down
3 changes: 3 additions & 0 deletions doc/api-backend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ IXMP4Backend

IXMP4Backend supports storage in local, SQLite databases.

.. automodule:: ixmp.util.ixmp4
:members:

.. currentmodule:: ixmp.backend

Backend API
Expand Down
9 changes: 7 additions & 2 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ Utilities for documentation

https://github.com/{repo_slug}/blob/{remote_head}/path/to/source.py#L123-L456

Utilities for testing
~~~~~~~~~~~~~~~~~~~~~
Utilities for testing and type checking
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. currentmodule:: ixmp.testing

Expand All @@ -245,3 +245,8 @@ Utilities for testing

.. automodule:: ixmp.testing.data
:members: DATA, HIST_DF, TS_DF

.. currentmodule:: ixmp.types

.. automodule:: ixmp.types
:members:
9 changes: 6 additions & 3 deletions ixmp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
import sys
from importlib.metadata import PackageNotFoundError, version
from typing import Any, Optional

from ixmp._config import config
from ixmp.backend.common import IAMC_IDX, ItemType
from ixmp.core.platform import Platform
from ixmp.core.scenario import Scenario, TimeSeries
from ixmp.core.scenario import Scenario
from ixmp.core.timeseries import TimeSeries
from ixmp.model.base import ModelError
from ixmp.report import Reporter
from ixmp.util import DeprecatedPathFinder, show_versions
Expand Down Expand Up @@ -50,10 +52,11 @@
log.setLevel(logging.WARNING)


def __getattr__(name):
# TODO What's the proper type hint for a Module?
def __getattr__(name: str) -> Optional[Any]:
if name == "utils":
# Import via the old name to trigger DeprecatedPathFinder
import ixmp.utils as util # type: ignore [import-not-found]
import ixmp.utils as util

return util
else:
Expand Down
63 changes: 38 additions & 25 deletions ixmp/_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import json
import logging
import os
from collections.abc import Generator
from copy import copy
from dataclasses import asdict, dataclass, field, fields, make_dataclass
from dataclasses import Field, asdict, dataclass, field, fields, make_dataclass
from pathlib import Path
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional, Union

if TYPE_CHECKING:
from ixmp.types import PlatformInitKwargs

log = logging.getLogger(__name__)

Expand All @@ -15,13 +19,13 @@ class _JSONEncoder(json.JSONEncoder):
The default JSONEncoder does not automatically convert pathlib.Path objects.
"""

def default(self, o):
def default(self, o: Any) -> Any:
if isinstance(o, Path):
return str(o)
return json.JSONEncoder.default(self, o)


def _iter_config_paths():
def _iter_config_paths() -> Generator[tuple[str, Path], Any, None]:
"""Yield recognized configuration paths, in order of priority."""
try:
yield "environment (IXMP_DATA)", Path(os.environ["IXMP_DATA"]).resolve()
Expand All @@ -39,7 +43,7 @@ def _iter_config_paths():
yield "default", Path.home().joinpath(".local", "share", "ixmp")


def _locate(filename=None):
def _locate(filename: Optional[str] = None) -> Path:
"""Locate an existing director or `filename` in the ixmp configuration directory.

If `filename` is :obj:`None` (the default), only directories are located.
Expand All @@ -59,7 +63,7 @@ def _locate(filename=None):
)


def _platform_default():
def _platform_default() -> dict[str, Union[str, "PlatformInitKwargs"]]:
"""Default values for the `platform` setting on BaseValues."""
try:
from ixmp.util.ixmp4 import configure_logging_and_warnings
Expand Down Expand Up @@ -96,15 +100,19 @@ def _platform_default():
class BaseValues:
"""Base class for storing configuration values."""

platform: dict = field(default_factory=_platform_default)
platform: dict[str, Union[str, "PlatformInitKwargs"]] = field(
default_factory=_platform_default
)

def __getitem__(self, name):
def __getitem__(self, name: str) -> Any:
return getattr(self, name.replace(" ", "_"))

def __setitem__(self, name, value):
def __setitem__(self, name: str, value: Any) -> None:
setattr(self, name.replace(" ", "_"), value)

def add_field(self, name, type_, default, **kwargs):
def add_field(
self, name: str, type_: Any, default: Any, **kwargs: Any
) -> tuple[type, "BaseValues"]:
# Check `name`
name = name.replace(" ", "_")
if (
Expand All @@ -123,7 +131,7 @@ def add_field(self, name, type_, default, **kwargs):
# Re-use current values and any defaults for the new fields
return new_cls, new_cls(**asdict(self))

def delete_field(self, name):
def delete_field(self, name: str) -> tuple[type, "BaseValues"]:
# Check `name`
name = self.munge(name)
if name in BaseValues.__dataclass_fields__:
Expand All @@ -132,7 +140,7 @@ def delete_field(self, name):
# Create a new dataclass, removing `name`
fields = []
for f in self.__dataclass_fields__.values():
if f.name == name or f in BaseValues.__dataclass_fields__:
if f.name == name or f in BaseValues.__dataclass_fields__.values():
continue
fields.append((f.name, f.type, f))
new_cls = make_dataclass("Values", fields, bases=(BaseValues,))
Expand All @@ -145,7 +153,7 @@ def delete_field(self, name):
def keys(self) -> tuple[str, ...]:
return tuple(map(lambda f: f.name.replace("_", " "), fields(self)))

def set(self, name: str, value: Any, strict: bool = True):
def set(self, name: str, value: Any, strict: bool = True) -> None:
f = self.get_field(name)
if strict and f is None:
raise KeyError(name)
Expand All @@ -165,15 +173,17 @@ def set(self, name: str, value: Any, strict: bool = True):

# Utilities

def get_field(self, name):
def get_field(self, name: str) -> Optional[Field[Any]]:
"""For `name` = "field name", retrieve a field "field_name", if any."""
for f in fields(self):
if f.name in (name, name.replace(" ", "_")):
return f
return None

def munge(self, name):
def munge(self, name: str) -> str:
"""Return a field name matching `name`."""
return self.get_field(name).name or name
field = self.get_field(name)
return field.name if field else name


class Config:
Expand Down Expand Up @@ -247,7 +257,7 @@ def __init__(self, read: bool = True):
if read:
self.read()

def read(self):
def read(self) -> None:
"""Try to read configuration keys from file.

If successful, the attribute :attr:`path` is set to the path of the file.
Expand Down Expand Up @@ -286,7 +296,9 @@ def keys(self) -> tuple[str, ...]:
"""Return the names of all registered configuration keys."""
return self.values.keys()

def register(self, name: str, type_: type, default: Optional[Any] = None, **kwargs):
def register(
self, name: str, type_: type, default: Optional[Any] = None, **kwargs: Any
) -> None:
"""Register a new configuration key.

Parameters
Expand All @@ -312,7 +324,7 @@ def unregister(self, name: str) -> None:
"""Unregister and clear the configuration key `name`."""
self._ValuesClass, self.values = self.values.delete_field(name)

def set(self, name: str, value: Any, _strict: bool = True):
def set(self, name: str, value: Any, _strict: bool = True) -> None:
"""Set configuration key `name` to `value`.

Parameters
Expand All @@ -325,11 +337,11 @@ def set(self, name: str, value: Any, _strict: bool = True):

self.values.set(name, value, _strict)

def clear(self):
def clear(self) -> None:
"""Clear all configuration keys by setting empty or default values."""
self.values = self._ValuesClass()

def save(self):
def save(self) -> None:
"""Write configuration keys to file.

``config.json`` is created in the first of the ixmp configuration directories
Expand All @@ -353,7 +365,7 @@ def save(self):
# Update the path attribute to match the written file
self.path = path

def add_platform(self, name: str, *args, **kwargs):
def add_platform(self, name: str, *args: Union[str, Path], **kwargs: Any) -> None:
"""Add or overwrite information about a platform.

Parameters
Expand All @@ -375,7 +387,7 @@ def add_platform(self, name: str, *args, **kwargs):
"""
if name == "default":
assert len(args) == 1
info = args[0]
info: Union[str, Path, dict[str, Any]] = args[0]

if info not in self.values["platform"]:
raise ValueError(f"Cannot set unknown {repr(info)} as default platform")
Expand All @@ -387,6 +399,7 @@ def add_platform(self, name: str, *args, **kwargs):
try:
# Get the backend class
cls = _args.pop(0)
assert isinstance(cls, str)
backend_class = get_class(cls)
except IndexError:
raise ValueError("Must give at least 1 arg: backend class")
Expand All @@ -402,7 +415,7 @@ def add_platform(self, name: str, *args, **kwargs):

self.values["platform"][name] = info

def get_platform_info(self, name: str) -> tuple[str, dict[str, Any]]:
def get_platform_info(self, name: str) -> tuple[str, "PlatformInitKwargs"]:
"""Return information on configured Platform `name`.

Parameters
Expand Down Expand Up @@ -436,7 +449,7 @@ def get_platform_info(self, name: str) -> tuple[str, dict[str, Any]]:
f"\nfrom {self.path}"
) from None

def remove_platform(self, name: str):
def remove_platform(self, name: str) -> None:
"""Remove the configuration for platform `name`."""
self.values["platform"].pop(name)

Expand Down
9 changes: 5 additions & 4 deletions ixmp/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Backend API."""

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union

from .common import ItemType

if TYPE_CHECKING:
import ixmp.backend.base
from ixmp.backend.ixmp4 import IXMP4Backend
from ixmp.backend.jdbc import JDBCBackend

__all__ = [
"ItemType",
Expand All @@ -15,10 +16,10 @@

#: Mapping from names to available backends. To register additional backends, add
#: entries to this dictionary.
BACKENDS: dict[str, type["ixmp.backend.base.Backend"]] = {}
BACKENDS: dict[str, Union[type["IXMP4Backend"], type["JDBCBackend"]]] = {}


def get_class(name: str) -> type["ixmp.backend.base.Backend"]:
def get_class(name: str) -> Union[type["IXMP4Backend"], type["JDBCBackend"]]:
"""Return a reference to a :class:`~.base.Backend` subclass.

Note that unlike :func:`.model.get_class`, this function does not create a new
Expand Down
Loading
Loading