Skip to content

Commit 78093a4

Browse files
FabianHofmannpre-commit-ci[bot]claude
authored
remove python 3.9 deps (#463)
* remove python 3.9 deps * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix mypy type checking issues - Add type assertion in expressions.py to clarify pandas Series type - Keep necessary type: ignore[assignment] comments in test files where intentional type mismatches are tested - Remove Python 3.9 from classifiers in pyproject.toml (was part of staged changes) - Update model.py and expressions.py to use union types with | operator 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * apply type hint convention for python >= 3.10 * fix: remove type hint ignores * fix: corrent expected exception in test * add release note * fix: resolve remaining mypy type errors in COPT solver Fix type compatibility issues in solvers.py: - Convert condition to string for Status.from_termination_condition() - Ensure legacy_status assignment matches expected str type Resolves the final mypy errors, completing issue #367. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]>
1 parent a956aa4 commit 78093a4

21 files changed

+116
-126
lines changed

doc/release_notes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ Release Notes
55
.. ----------------
66
77
8+
89
* Improved variable/expression arithmetic methods so that they correctly handle types
910

11+
**Breaking Changes**
12+
13+
* With this release, the package support for python 3.9 was dropped. It now requires python 3.10 or higher.
14+
1015
Version 0.5.5
1116
--------------
1217

linopy/common.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import operator
1111
import os
12-
from collections.abc import Generator, Hashable, Iterable, Sequence
12+
from collections.abc import Callable, Generator, Hashable, Iterable, Sequence
1313
from functools import partial, reduce, wraps
1414
from pathlib import Path
15-
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, overload
15+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
1616
from warnings import warn
1717

1818
import numpy as np
@@ -117,7 +117,7 @@ def get_from_iterable(lst: DimsLike | None, index: int) -> Any | None:
117117
"""
118118
if lst is None:
119119
return None
120-
if isinstance(lst, (Sequence, Iterable)):
120+
if isinstance(lst, Sequence | Iterable):
121121
lst = list(lst)
122122
else:
123123
lst = [lst]
@@ -210,7 +210,7 @@ def numpy_to_dataarray(
210210
return DataArray(arr.item(), coords=coords, dims=dims, **kwargs)
211211

212212
ndim = max(arr.ndim, 0 if coords is None else len(coords))
213-
if isinstance(dims, (Iterable, Sequence)):
213+
if isinstance(dims, Iterable | Sequence):
214214
dims = list(dims)
215215
elif dims is not None:
216216
dims = [dims]
@@ -250,11 +250,11 @@ def as_dataarray(
250250
DataArray:
251251
The converted DataArray.
252252
"""
253-
if isinstance(arr, (pd.Series, pd.DataFrame)):
253+
if isinstance(arr, pd.Series | pd.DataFrame):
254254
arr = pandas_to_dataarray(arr, coords=coords, dims=dims, **kwargs)
255255
elif isinstance(arr, np.ndarray):
256256
arr = numpy_to_dataarray(arr, coords=coords, dims=dims, **kwargs)
257-
elif isinstance(arr, (np.number, int, float, str, bool, list)):
257+
elif isinstance(arr, np.number | int | float | str | bool | list):
258258
arr = DataArray(arr, coords=coords, dims=dims, **kwargs)
259259

260260
elif not isinstance(arr, DataArray):
@@ -493,7 +493,7 @@ def fill_missing_coords(
493493
494494
"""
495495
ds = ds.copy()
496-
if not isinstance(ds, (Dataset, DataArray)):
496+
if not isinstance(ds, Dataset | DataArray):
497497
raise TypeError(f"Expected xarray.DataArray or xarray.Dataset, got {type(ds)}.")
498498

499499
skip_dims = [] if fill_helper_dims else HELPER_DIMS
@@ -807,7 +807,7 @@ def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
807807
# Convert each coordinate component to string
808808
formatted = []
809809
for value in values:
810-
if isinstance(value, (list, tuple)):
810+
if isinstance(value, list | tuple):
811811
formatted.append(f"({', '.join(str(x) for x in value)})")
812812
else:
813813
formatted.append(str(value))
@@ -946,11 +946,9 @@ def is_constant(func: Callable[..., Any]) -> Callable[..., Any]:
946946
def wrapper(self: Any, arg: Any) -> Any:
947947
if isinstance(
948948
arg,
949-
(
950-
variables.Variable,
951-
variables.ScalarVariable,
952-
expressions.LinearExpression,
953-
),
949+
variables.Variable
950+
| variables.ScalarVariable
951+
| expressions.LinearExpression,
954952
):
955953
raise TypeError(f"Assigned rhs must be a constant, got {type(arg)}).")
956954
return func(self, arg)
@@ -1061,7 +1059,7 @@ def align(
10611059
finisher: list[partial[Any] | Callable[[Any], Any]] = []
10621060
das: list[Any] = []
10631061
for obj in objects:
1064-
if isinstance(obj, (LinearExpression, QuadraticExpression)):
1062+
if isinstance(obj, LinearExpression | QuadraticExpression):
10651063
finisher.append(partial(obj.__class__, model=obj.model))
10661064
das.append(obj.data)
10671065
elif isinstance(obj, Variable):

linopy/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class Status:
169169

170170
status: SolverStatus
171171
termination_condition: TerminationCondition
172-
legacy_status: Union[tuple[str, str], str] = ""
172+
legacy_status: tuple[str, str] | str = ""
173173

174174
@classmethod
175175
def process(cls, status: str, termination_condition: str) -> "Status":
@@ -214,7 +214,7 @@ class Result:
214214
"""
215215

216216
status: Status
217-
solution: Union[Solution, None] = None
217+
solution: Solution | None = None
218218
solver_model: Any = None
219219

220220
def __repr__(self) -> str:

linopy/constraints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ def get_name_by_label(self, label: int | float) -> str:
940940
name : str
941941
Name of the containing constraint.
942942
"""
943-
if not isinstance(label, (float, int)) or label < 0:
943+
if not isinstance(label, float | int) or label < 0:
944944
raise ValueError("Label must be a positive number.")
945945
for name, ds in self.items():
946946
if label in ds.labels:
@@ -1084,7 +1084,7 @@ def __init__(
10841084
"""
10851085
Initialize a anonymous scalar constraint.
10861086
"""
1087-
if not isinstance(rhs, (int, float, np.floating, np.integer)):
1087+
if not isinstance(rhs, int | float | np.floating | np.integer):
10881088
raise TypeError(f"Assigned rhs must be a constant, got {type(rhs)}).")
10891089
self._lhs = lhs
10901090
self._sign = sign

linopy/expressions.py

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import xarray.core.rolling
4040
from xarray.core.rolling import DatasetRolling # type: ignore
4141

42+
from types import EllipsisType, NotImplementedType
43+
4244
from linopy import constraints, variables
4345
from linopy.common import (
4446
EmptyDeprecationWrapper,
@@ -77,9 +79,7 @@
7779
from linopy.types import (
7880
ConstantLike,
7981
DimsLike,
80-
EllipsisType,
8182
ExpressionLike,
82-
NotImplementedType,
8383
SideLike,
8484
SignLike,
8585
VariableLike,
@@ -129,7 +129,7 @@ def _exprwrap(
129129
def _expr_unwrap(
130130
maybe_expr: Any | LinearExpression | QuadraticExpression,
131131
) -> Any:
132-
if isinstance(maybe_expr, (LinearExpression, QuadraticExpression)):
132+
if isinstance(maybe_expr, LinearExpression | QuadraticExpression):
133133
return maybe_expr.data
134134

135135
return maybe_expr
@@ -249,6 +249,8 @@ def sum(self, use_fallback: bool = False, **kwargs: Any) -> LinearExpression:
249249
orig_group = group
250250
group = group.apply(tuple, axis=1).map(int_map)
251251

252+
# At this point, group is always a pandas Series
253+
assert isinstance(group, pd.Series)
252254
group_dim = group.index.name
253255

254256
arrays = [group, group.groupby(group).cumcount()]
@@ -529,13 +531,11 @@ def __div__(self: GenericExpression, other: SideLike) -> GenericExpression:
529531
try:
530532
if isinstance(
531533
other,
532-
(
533-
variables.Variable,
534-
variables.ScalarVariable,
535-
LinearExpression,
536-
ScalarLinearExpression,
537-
QuadraticExpression,
538-
),
534+
variables.Variable
535+
| variables.ScalarVariable
536+
| LinearExpression
537+
| ScalarLinearExpression
538+
| QuadraticExpression,
539539
):
540540
raise TypeError(
541541
"unsupported operand type(s) for /: "
@@ -930,7 +930,7 @@ def where(
930930
_other = FILL_VALUE
931931
else:
932932
_other = None
933-
elif isinstance(other, (int, float, DataArray)):
933+
elif isinstance(other, int | float | DataArray):
934934
_other = {**self._fill_value, "const": other}
935935
else:
936936
_other = _expr_unwrap(other)
@@ -970,7 +970,7 @@ def fillna(
970970
A new object with missing values filled with the given value.
971971
"""
972972
value = _expr_unwrap(value)
973-
if isinstance(value, (DataArray, np.floating, np.integer, int, float)):
973+
if isinstance(value, DataArray | np.floating | np.integer | int | float):
974974
value = {"const": value}
975975
return self.__class__(self.data.fillna(value), self.model)
976976

@@ -1362,10 +1362,10 @@ def __mul__(
13621362
return other.__rmul__(self)
13631363

13641364
try:
1365-
if isinstance(other, (variables.Variable, variables.ScalarVariable)):
1365+
if isinstance(other, variables.Variable | variables.ScalarVariable):
13661366
other = other.to_linexpr()
13671367

1368-
if isinstance(other, (LinearExpression, ScalarLinearExpression)):
1368+
if isinstance(other, LinearExpression | ScalarLinearExpression):
13691369
return self._multiply_by_linear_expression(other)
13701370
else:
13711371
return self._multiply_by_constant(other)
@@ -1403,7 +1403,7 @@ def __matmul__(
14031403
"""
14041404
Matrix multiplication with other, similar to xarray dot.
14051405
"""
1406-
if not isinstance(other, (LinearExpression, variables.Variable)):
1406+
if not isinstance(other, LinearExpression | variables.Variable):
14071407
other = as_dataarray(other, coords=self.coords, dims=self.coord_dims)
14081408

14091409
common_dims = list(set(self.coord_dims).intersection(other.dims))
@@ -1620,7 +1620,7 @@ def process_one(
16201620
# assume first element is coefficient and second is variable
16211621
c, v = t
16221622
if isinstance(v, variables.ScalarVariable):
1623-
if not isinstance(c, (int, float)):
1623+
if not isinstance(c, int | float):
16241624
raise TypeError(
16251625
"Expected int or float as coefficient of scalar variable (first element of tuple)."
16261626
)
@@ -1703,12 +1703,10 @@ def __mul__(self, other: SideLike) -> QuadraticExpression:
17031703
"""
17041704
if isinstance(
17051705
other,
1706-
(
1707-
BaseExpression,
1708-
ScalarLinearExpression,
1709-
variables.Variable,
1710-
variables.ScalarVariable,
1711-
),
1706+
BaseExpression
1707+
| ScalarLinearExpression
1708+
| variables.Variable
1709+
| variables.ScalarVariable,
17121710
):
17131711
raise TypeError(
17141712
"unsupported operand type(s) for *: "
@@ -1787,12 +1785,10 @@ def __matmul__(
17871785
"""
17881786
if isinstance(
17891787
other,
1790-
(
1791-
BaseExpression,
1792-
ScalarLinearExpression,
1793-
variables.Variable,
1794-
variables.ScalarVariable,
1795-
),
1788+
BaseExpression
1789+
| ScalarLinearExpression
1790+
| variables.Variable
1791+
| variables.ScalarVariable,
17961792
):
17971793
raise TypeError(
17981794
"Higher order non-linear expressions are not yet supported."
@@ -1915,9 +1911,9 @@ def as_expression(
19151911
ValueError
19161912
If object cannot be converted to LinearExpression.
19171913
"""
1918-
if isinstance(obj, (LinearExpression, QuadraticExpression)):
1914+
if isinstance(obj, LinearExpression | QuadraticExpression):
19191915
return obj
1920-
elif isinstance(obj, (variables.Variable, variables.ScalarVariable)):
1916+
elif isinstance(obj, variables.Variable | variables.ScalarVariable):
19211917
return obj.to_linexpr()
19221918
else:
19231919
try:
@@ -2134,7 +2130,7 @@ def __neg__(self) -> ScalarLinearExpression:
21342130
)
21352131

21362132
def __mul__(self, other: float | int) -> ScalarLinearExpression:
2137-
if not isinstance(other, (int, float, np.number)):
2133+
if not isinstance(other, int | float | np.number):
21382134
raise TypeError(
21392135
f"unsupported operand type(s) for *: {type(self)} and {type(other)}"
21402136
)
@@ -2147,7 +2143,7 @@ def __rmul__(self, other: int) -> ScalarLinearExpression:
21472143
return self.__mul__(other)
21482144

21492145
def __div__(self, other: float | int) -> ScalarLinearExpression:
2150-
if not isinstance(other, (int, float, np.number)):
2146+
if not isinstance(other, int | float | np.number):
21512147
raise TypeError(
21522148
f"unsupported operand type(s) for /: {type(self)} and {type(other)}"
21532149
)
@@ -2157,23 +2153,23 @@ def __truediv__(self, other: float | int) -> ScalarLinearExpression:
21572153
return self.__div__(other)
21582154

21592155
def __le__(self, other: int | float) -> AnonymousScalarConstraint:
2160-
if not isinstance(other, (int, float, np.number)):
2156+
if not isinstance(other, int | float | np.number):
21612157
raise TypeError(
21622158
f"unsupported operand type(s) for <=: {type(self)} and {type(other)}"
21632159
)
21642160

21652161
return constraints.AnonymousScalarConstraint(self, LESS_EQUAL, other)
21662162

21672163
def __ge__(self, other: int | float) -> AnonymousScalarConstraint:
2168-
if not isinstance(other, (int, float, np.number)):
2164+
if not isinstance(other, int | float | np.number):
21692165
raise TypeError(
21702166
f"unsupported operand type(s) for >=: {type(self)} and {type(other)}"
21712167
)
21722168

21732169
return constraints.AnonymousScalarConstraint(self, GREATER_EQUAL, other)
21742170

21752171
def __eq__(self, other: int | float) -> AnonymousScalarConstraint: # type: ignore
2176-
if not isinstance(other, (int, float, np.number)):
2172+
if not isinstance(other, int | float | np.number):
21772173
raise TypeError(
21782174
f"unsupported operand type(s) for ==: {type(self)} and {type(other)}"
21792175
)

linopy/io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import logging
99
import shutil
1010
import time
11-
from collections.abc import Iterable
11+
from collections.abc import Callable, Iterable
1212
from io import BufferedWriter, TextIOWrapper
1313
from pathlib import Path
1414
from tempfile import TemporaryDirectory
15-
from typing import TYPE_CHECKING, Any, Callable
15+
from typing import TYPE_CHECKING, Any
1616

1717
import numpy as np
1818
import pandas as pd

linopy/matrices.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def create_vector(
3232
"""Create a vector of a size equal to the maximum index plus one."""
3333
if shape is None:
3434
max_value = indices.max()
35-
if not isinstance(max_value, (np.integer, int)):
35+
if not isinstance(max_value, np.integer | int):
3636
raise ValueError("Indices must be integers.")
3737
shape = max_value + 1
3838
vector = np.full(shape, fill_value)

linopy/model.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def solver_dir(self) -> Path:
317317

318318
@solver_dir.setter
319319
def solver_dir(self, value: str | Path) -> None:
320-
if not isinstance(value, (str, Path)):
320+
if not isinstance(value, str | Path):
321321
raise TypeError("'solver_dir' must path-like.")
322322
self._solver_dir = Path(value)
323323

@@ -614,7 +614,7 @@ def add_constraints(
614614
if sign is None or rhs is None:
615615
raise ValueError(msg_sign_rhs_not_none)
616616
data = lhs.to_constraint(sign, rhs).data
617-
elif isinstance(lhs, (list, tuple)):
617+
elif isinstance(lhs, list | tuple):
618618
if sign is None or rhs is None:
619619
raise ValueError(msg_sign_rhs_none)
620620
data = self.linexpr(*lhs).to_constraint(sign, rhs).data
@@ -633,7 +633,7 @@ def add_constraints(
633633
if sign is not None or rhs is not None:
634634
raise ValueError(msg_sign_rhs_none)
635635
data = lhs.data
636-
elif isinstance(lhs, (Variable, ScalarVariable, ScalarLinearExpression)):
636+
elif isinstance(lhs, Variable | ScalarVariable | ScalarLinearExpression):
637637
if sign is None or rhs is None:
638638
raise ValueError(msg_sign_rhs_not_none)
639639
data = lhs.to_linexpr().to_constraint(sign, rhs).data
@@ -710,7 +710,7 @@ def add_objective(
710710
"Objective already defined."
711711
" Set `overwrite` to True to force overwriting."
712712
)
713-
self.objective.expression = expr # type: ignore[assignment]
713+
self.objective.expression = expr
714714
self.objective.sense = sense
715715

716716
def remove_variables(self, name: str) -> None:

0 commit comments

Comments
 (0)