From 16f8bf1ed18d21642840998f89af4e5d1cfc9b08 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Wed, 28 Aug 2024 18:13:35 +0200 Subject: [PATCH] ci: move mypy to gh raction and add more rules (#344) * read mypy config correctly * move mypy to gh action and add more rules * fix --- .github/workflows/test.yml | 30 ++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 6 ------ linopy/common.py | 4 ++-- linopy/constraints.py | 4 ++-- linopy/expressions.py | 10 +++++----- linopy/io.py | 11 ++++++----- linopy/model.py | 6 +++--- linopy/solvers.py | 28 ++++++++++++++-------------- linopy/variables.py | 4 +--- pyproject.toml | 13 +++++++++++++ test/test_common.py | 2 +- test/test_repr.py | 8 ++++---- test/test_variable.py | 2 +- 13 files changed, 82 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4f3b324..62d45363 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -95,3 +95,33 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + + check-types: + name: Check types + needs: [build] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for setuptools_scm + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Download package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Install package and dependencies + run: | + python -m pip install uv + uv pip install --system "$(ls dist/*.whl)[dev]" + + - name: Run type checker (mypy) + run: | + mypy . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c72964d8..e6111467 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,9 +35,3 @@ repos: hooks: - id: jupyter-notebook-cleanup exclude: examples/solve-on-remote.ipynb -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 - hooks: - - id: mypy - files: ^(linopy|test)/ - additional_dependencies: [numpy, pandas, xarray, types-paramiko] diff --git a/linopy/common.py b/linopy/common.py index c857eded..c9532a62 100644 --- a/linopy/common.py +++ b/linopy/common.py @@ -242,7 +242,7 @@ def as_dataarray( ) arr = fill_missing_coords(arr) - return arr # type: ignore + return arr # TODO: rename to to_pandas_dataframe @@ -496,7 +496,7 @@ def best_int(max_value: int) -> type: Get the minimal int dtype for storing values <= max_value. """ for t in (np.int8, np.int16, np.int32, np.int64): - if max_value <= np.iinfo(t).max: # type: ignore + if max_value <= np.iinfo(t).max: return t raise ValueError(f"Value {max_value} is too large for int64.") diff --git a/linopy/constraints.py b/linopy/constraints.py index 4d7fa640..fabe74d2 100644 --- a/linopy/constraints.py +++ b/linopy/constraints.py @@ -184,7 +184,7 @@ def values(self) -> Union[DataArray, None]: "The `.values` attribute is deprecated. Use `.labels.values` instead.", DeprecationWarning, ) - return self.labels.values if self.is_assigned else None # type: ignore + return self.labels.values if self.is_assigned else None @property def nterm(self): @@ -767,7 +767,7 @@ def labels(self) -> Dataset: """ return save_join( *[v.labels.rename(k) for k, v in self.items()], - integer_dtype=True, # type: ignore + integer_dtype=True, ) @property diff --git a/linopy/expressions.py b/linopy/expressions.py index 0ad210a3..b75f66f4 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -153,7 +153,7 @@ def groupby(self) -> xarray.core.groupby.DatasetGroupBy: "Grouping by pandas objects is only supported in sum function." ) - return self.data.groupby(group=self.group, **self.kwargs) # type: ignore + return self.data.groupby(group=self.group, **self.kwargs) def map( self, func: Callable, shortcut: bool = False, args: tuple[()] = (), **kwargs @@ -226,7 +226,7 @@ def sum(self, use_fallback: bool = False, **kwargs) -> LinearExpression: if group_name == group_dim: raise ValueError("Group name cannot be the same as group dimension") - arrays = [group, group.groupby(group).cumcount()] # type: ignore + arrays = [group, group.groupby(group).cumcount()] idx = pd.MultiIndex.from_arrays( arrays, names=[group_name, GROUPED_TERM_DIM] ) @@ -239,7 +239,7 @@ def sum(self, use_fallback: bool = False, **kwargs) -> LinearExpression: index = ds.indexes["group"].map({v: k for k, v in int_map.items()}) index.names = [str(col) for col in orig_group.columns] index.name = group_name - coords = Coordinates.from_pandas_multiindex(index, group_name) # type: ignore + coords = Coordinates.from_pandas_multiindex(index, group_name) ds = xr.Dataset(ds.assign_coords(coords)) return LinearExpression(ds, self.model) @@ -549,7 +549,7 @@ def __pow__(self, other: int) -> QuadraticExpression: raise ValueError("Power must be 2.") return self * self # type: ignore - def __rmul__( # type: ignore + def __rmul__( self, other: float | int | DataArray ) -> LinearExpression | QuadraticExpression: """ @@ -590,7 +590,7 @@ def __truediv__( def __le__(self, rhs: int) -> Constraint: return self.to_constraint(LESS_EQUAL, rhs) - def __ge__(self, rhs: int | ndarray | DataArray) -> Constraint: # type: ignore + def __ge__(self, rhs: int | ndarray | DataArray) -> Constraint: return self.to_constraint(GREATER_EQUAL, rhs) def __eq__(self, rhs: LinearExpression | float | Variable | int) -> Constraint: # type: ignore diff --git a/linopy/io.py b/linopy/io.py index 517d662f..93120a97 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -8,6 +8,7 @@ import logging import shutil import time +from collections.abc import Iterable from io import TextIOWrapper from pathlib import Path from tempfile import TemporaryDirectory @@ -132,7 +133,7 @@ def constraints_to_file( return f.write("\n\ns.t.\n\n") - names = m.constraints + names: Iterable = m.constraints if log: names = tqdm( list(names), @@ -194,7 +195,7 @@ def bounds_to_file( """ Write out variables of a model to a lp file. """ - names = list(m.variables.continuous) + list(m.variables.integers) + names: Iterable = list(m.variables.continuous) + list(m.variables.integers) if not len(list(names)): return @@ -231,7 +232,7 @@ def binaries_to_file( """ Write out binaries of a model to a lp file. """ - names = m.variables.binaries + names: Iterable = m.variables.binaries if not len(list(names)): return @@ -265,7 +266,7 @@ def integers_to_file( """ Write out integers of a model to a lp file. """ - names = m.variables.integers + names: Iterable = m.variables.integers if not len(list(names)): return @@ -592,7 +593,7 @@ def to_mosek(m: Model, task: Any | None = None) -> Any: task : MOSEK Task object """ - import mosek # type: ignore + import mosek if task is None: task = mosek.Task() diff --git a/linopy/model.py b/linopy/model.py index 93d269d3..5800c047 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -551,7 +551,7 @@ def add_constraints( if isinstance(lhs, LinearExpression): if sign is None or rhs is None: raise ValueError(msg_sign_rhs_not_none) - data = lhs.to_constraint(sign, rhs).data # type: ignore + data = lhs.to_constraint(sign, rhs).data elif isinstance(lhs, (list, tuple)): if sign is None or rhs is None: raise ValueError(msg_sign_rhs_none) @@ -803,7 +803,7 @@ def calculate_block_maps(self) -> None: dtype = self.blocks.dtype self.variables.set_blocks(self.blocks) - block_map = self.variables.get_blockmap(dtype) # type: ignore + block_map = self.variables.get_blockmap(dtype) self.constraints.set_blocks(block_map) blocks = replace_by_map(self.objective.vars, block_map) @@ -1046,7 +1046,7 @@ def solve( if solver_name is None: solver_name = available_solvers[0] - logger.info(f" Solve problem using {solver_name.title()} solver") # type: ignore + logger.info(f" Solve problem using {solver_name.title()} solver") assert solver_name in available_solvers, f"Solver {solver_name} not installed" # reset result diff --git a/linopy/solvers.py b/linopy/solvers.py index 8a0ce597..f5a9a171 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -77,15 +77,15 @@ available_solvers.append("scip") with contextlib.suppress(ImportError): - import cplex # type: ignore + import cplex available_solvers.append("cplex") with contextlib.suppress(ImportError): - import xpress # type: ignore + import xpress available_solvers.append("xpress") with contextlib.suppress(ImportError): - import mosek # type: ignore + import mosek with contextlib.suppress(mosek.Error): with mosek.Env() as m: @@ -100,7 +100,7 @@ available_solvers.append("mindopt") with contextlib.suppress(ImportError): - import coptpy # type: ignore + import coptpy with contextlib.suppress(coptpy.CoptError): coptpy.Envr() @@ -962,7 +962,7 @@ def run_mosek( if env is None: env = stack.enter_context(mosek.Env()) - with env.Task() as m: # type: ignore + with env.Task() as m: if io_api == "direct": model.to_mosek(m) elif io_api is None or io_api in FILE_IO_APIS: @@ -1241,7 +1241,7 @@ def run_mindopt( warmstart_fn: Path | None = None, basis_fn: Path | None = None, keep_files: bool = False, - env: mindoptpy.Env | None = None, # type: ignore + env: mindoptpy.Env | None = None, **solver_options, ) -> Result: """ @@ -1274,10 +1274,10 @@ def run_mindopt( problem_fn = model.to_file(problem_fn, io_api) if env is None: - env = mindoptpy.Env(path_to_string(log_fn) if log_fn else "") # type: ignore - env.start() # type: ignore + env = mindoptpy.Env(path_to_string(log_fn) if log_fn else "") + env.start() - m = mindoptpy.read(path_to_string(problem_fn), env) # type: ignore + m = mindoptpy.read(path_to_string(problem_fn), env) for k, v in solver_options.items(): m.setParam(k, v) @@ -1285,7 +1285,7 @@ def run_mindopt( if warmstart_fn: try: m.read(path_to_string(warmstart_fn)) - except mindoptpy.MindoptError as err: # type: ignore + except mindoptpy.MindoptError as err: logger.info("Model basis could not be read. Raised error: %s", err) m.optimize() @@ -1293,13 +1293,13 @@ def run_mindopt( if basis_fn: try: m.write(path_to_string(basis_fn)) - except mindoptpy.MindoptError as err: # type: ignore + except mindoptpy.MindoptError as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn: try: m.write(path_to_string(solution_fn)) - except mindoptpy.MindoptError as err: # type: ignore + except mindoptpy.MindoptError as err: logger.info("No model solution stored. Raised error: %s", err) condition = m.status @@ -1316,7 +1316,7 @@ def get_solver_solution() -> Solution: try: dual = pd.Series({c.constrname: c.DualSoln for c in m.getConstrs()}) dual = set_int_index(dual) - except (mindoptpy.MindoptError, AttributeError): # type: ignore + except (mindoptpy.MindoptError, AttributeError): logger.warning("Dual values of MILP couldn't be parsed") dual = pd.Series(dtype=float) @@ -1325,7 +1325,7 @@ def get_solver_solution() -> Solution: solution = safe_get_solution(status, get_solver_solution) maybe_adjust_objective_sign(solution, model.objective.sense, io_api) - env.dispose() # type: ignore + env.dispose() return Result(status, solution, m) diff --git a/linopy/variables.py b/linopy/variables.py index c589d3a0..fe0a214d 100644 --- a/linopy/variables.py +++ b/linopy/variables.py @@ -396,9 +396,7 @@ def __pow__(self, other: int) -> QuadraticExpression: expr = self.to_linexpr() return expr._multiply_by_linear_expression(expr) - def __rmul__( # type: ignore - self, other: float | DataArray | int | ndarray - ) -> LinearExpression: + def __rmul__(self, other: float | DataArray | int | ndarray) -> LinearExpression: """ Right-multiply variables with a coefficient. """ diff --git a/pyproject.toml b/pyproject.toml index 72776365..b493ad6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,10 +91,23 @@ version_scheme = "no-guess-dev" branch = true source = ["linopy"] omit = ["test/*"] +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", +] [tool.mypy] exclude = ['dev/*', 'examples/*', 'benchmark/*', 'doc/*'] ignore_missing_imports = true +no_implicit_optional = true +warn_unused_ignores = true +show_error_code_links = true +# disallow_any_generics = true +# warn_return_any = true + +# [[tool.mypy.overrides]] +# module = "linopy.*" +# disallow_untyped_defs = true [tool.ruff] extend-include = ['*.ipynb'] diff --git a/test/test_common.py b/test/test_common.py index d82d55bf..c5937c06 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -441,7 +441,7 @@ def test_assign_multiindex_safe(): assert result["humidity"].equals(data) # Case 2: Assigning a Dataset - result = assign_multiindex_safe(ds, **xr.Dataset({"humidity": data})) # type: ignore + result = assign_multiindex_safe(ds, **xr.Dataset({"humidity": data})) assert "humidity" in result assert "value" in result assert result["humidity"].equals(data) diff --git a/test/test_repr.py b/test/test_repr.py index 5fbeaa99..66f0e538 100644 --- a/test/test_repr.py +++ b/test/test_repr.py @@ -13,15 +13,15 @@ u = m.add_variables(0, upper, name="u") v = m.add_variables(lower, upper, name="v") -x = m.add_variables(lower, 10, coords=[lower.index], name="x") # type: ignore +x = m.add_variables(lower, 10, coords=[lower.index], name="x") y = m.add_variables(0, 10, name="y") z = m.add_variables(name="z", binary=True) -a = m.add_variables(coords=[lower.index], name="a", binary=True) # type: ignore -b = m.add_variables(coords=[lower.index], name="b", integer=True) # type: ignore +a = m.add_variables(coords=[lower.index], name="a", binary=True) +b = m.add_variables(coords=[lower.index], name="b", integer=True) c_mask = xr.DataArray(False, coords=upper.axes) c_mask[:, 5:] = True c = m.add_variables(lower, upper, name="c", mask=c_mask) -d = m.add_variables(0, 10, coords=[types], name="d") # type: ignore +d = m.add_variables(0, 10, coords=[types], name="d") # new behavior in v0.2, variable with dimension name and other # coordinates are added without a warning diff --git a/test/test_variable.py b/test/test_variable.py index 84655529..bf718558 100644 --- a/test/test_variable.py +++ b/test/test_variable.py @@ -198,7 +198,7 @@ def test_variable_where(x): assert x.labels[9] == x.at[0].label with pytest.raises(ValueError): - x.where([True] * 4 + [False] * 6, 0) # type: ignore + x.where([True] * 4 + [False] * 6, 0) def test_variable_shift(x):