From d79e7e6cd4562db978d9bbeb3fac69e38a30f04d Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 18:47:50 -0700 Subject: [PATCH 01/21] add a nox file --- noxfile.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 noxfile.py diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..bcb4d20 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import os +import pathlib +import shutil + +import nox + +PROJECT = "heat" +ROOT = pathlib.Path(__file__).parent + + +@nox.session +def test(session: nox.Session) -> None: + """Run the tests.""" + session.install(".[testing]") + + args = ["--cov", PROJECT, "-vvv"] + session.posargs + + if "CI" in os.environ: + args.append(f"--cov-report=xml:{ROOT.absolute()!s}/coverage.xml") + session.run("pytest", *args) + + if "CI" not in os.environ: + session.run("coverage", "report", "--ignore-errors", "--show-missing") + + +@nox.session +def lint(session: nox.Session) -> None: + """Look for lint.""" + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files") + + +@nox.session +def build(session: nox.Session) -> None: + session.install("pip") + session.install("build") + session.run("python", "--version") + session.run("pip", "--version") + session.run("python", "-m", "build", "--outdir", "./build/wheelhouse") + + +@nox.session(name="publish-testpypi") +def publish_testpypi(session): + """Publish wheelhouse/* to TestPyPI.""" + session.run("twine", "check", "build/wheelhouse/*") + session.run( + "twine", + "upload", + "--skip-existing", + "--repository-url", + "https://test.pypi.org/legacy/", + "build/wheelhouse/*.tar.gz", + ) + + +@nox.session(name="publish-pypi") +def publish_pypi(session): + """Publish wheelhouse/* to PyPI.""" + session.run("twine", "check", "build/wheelhouse/*") + session.run( + "twine", + "upload", + "--skip-existing", + "build/wheelhouse/*.tar.gz", + ) + + +@nox.session(python=False) +def clean(session): + """Remove all .venv's, build files and caches in the directory.""" + folders = ( + (ROOT,) if not session.posargs else (pathlib.Path(f) for f in session.posargs) + ) + for folder in folders: + if not str(folder.resolve()).startswith(str(ROOT.resolve())): + session.log(f"skipping {folder}: folder is outside of repository") + continue + + with session.chdir(folder): + session.log(f"cleaning {folder}") + + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("dist", ignore_errors=True) + shutil.rmtree(f"src/{PROJECT}.egg-info", ignore_errors=True) + shutil.rmtree(".pytest_cache", ignore_errors=True) + shutil.rmtree(".venv", ignore_errors=True) + + for pattern in ["*.py[co]", "__pycache__"]: + _clean_rglob(pattern) + + +def _clean_rglob(pattern): + nox_dir = pathlib.Path(".nox") + + for p in pathlib.Path(".").rglob(pattern): + if nox_dir in p.parents: + continue + if p.is_dir(): + p.rmdir() + else: + p.unlink() From 16e18f62293fa535e47a83245e3ac64ab4710756 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 18:47:58 -0700 Subject: [PATCH 02/21] clean up pyproject.toml --- pyproject.toml | 129 ++++++++++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd4dac2..551a1af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,106 +1,123 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - [project] -name = "bmi-heat" +name = "bmi_heat" +requires-python = ">=3.10" description = "BMI Python example" +keywords = [ + "bmi", + "component modeling", + "csdms", + "earth science", +] authors = [ - {email = "eric.hutton@colorado.edu"}, - {name = "Eric Hutton"} + { email = "eric.hutton@colorado.edu" }, + { name = "Eric Hutton" }, ] maintainers = [ - {name = "Mark Piper", email = "mark.piper@colorado.edu"}, - {name = "Eric Hutton", email = "eric.hutton@colorado.edu"}, + { name = "Mark Piper", email = "mark.piper@colorado.edu" }, + { name = "Eric Hutton", email = "eric.hutton@colorado.edu" }, ] -keywords = [ - "bmi", - "component modeling", - "csdms", - "earth science", -] -license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering :: Physics", ] -requires-python = ">=3.9" dependencies = [ + "bmipy", "numpy", - "scipy", "pyyaml", - "bmipy", + "scipy", +] +dynamic = [ + "readme", + "version", ] -dynamic = ["readme", "version"] + +[project.license] +text = "MIT" [project.urls] -Homepage = "https://csdms.colorado.edu" +Changelog = "https://github.com/csdms/bmi-example-python/blob/master/CHANGES.rst" Documentation = "https://bmi.readthedocs.io" +Homepage = "https://csdms.colorado.edu" Repository = "https://github.com/csdms/bmi-example-python" -Changelog = "https://github.com/csdms/bmi-example-python/blob/master/CHANGES.rst" [project.optional-dependencies] +dev = [ + "nox", +] testing = [ + "bmi-tester", "coveralls", "pytest", "pytest-cov", - "black", - "isort", - "ruff", - "bmi-tester", ] -[tool.setuptools.dynamic] -readme = {file = ["README.rst", "AUTHORS.rst", "CHANGES.rst"]} -version = {attr = "heat._version.__version__"} +[build-system] +requires = [ + "setuptools", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic.readme] +file = "README.rst" +content-type = "text/x-rst" + +[tool.setuptools.dynamic.version] +attr = "heat._version.__version__" [tool.setuptools.packages.find] -where = ["."] +where = [ + ".", +] + +[tool.coverage.run] +relative_files = true + +[tool.isort] +profile = "black" +force_single_line = "true" [tool.pytest.ini_options] minversion = "6.0" -testpaths = ["heat", "tests"] -norecursedirs = [".*", "*.egg*", "build", "dist"] -addopts = """ - --ignore setup.py - --tb native - --strict-markers - --durations 16 - --doctest-modules - -vvv -""" +testpaths = [ + "heat", + "tests", +] +norecursedirs = [ + ".*", + "*.egg*", + "build", + "dist", +] +addopts = [ + "--ignore=setup.py", + "--tb=native", + "--strict-markers", + "--durations=16", + "--doctest-modules", + "-vvv", +] doctest_optionflags = [ - "NORMALIZE_WHITESPACE", - "IGNORE_EXCEPTION_DETAIL", - "ALLOW_UNICODE" + "NORMALIZE_WHITESPACE", + "IGNORE_EXCEPTION_DETAIL", + "ALLOW_UNICODE", ] -[tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -line_length = 88 - [tool.ruff] line-length = 88 ignore = [ - "E203", - "E501", + "E203", + "E501", ] -[tool.coverage.run] -relative_files = true - [tool.zest-releaser] tag-format = "v{version}" python-file-with-version = "heat/_version.py" From 838d0ce042d47c8467288029ce5de9e81fd825c5 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 18:49:45 -0700 Subject: [PATCH 03/21] remove the manifest --- MANIFEST.in | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 625a716..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -recursive-include main_directory * -include LICENSE -include *.rst -include *.txt -include Makefile -global-exclude *.pyc -recursive-exclude examples * From b1f8be8448c8f728bc38c36cd45012a4d1e8d034 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 18:50:06 -0700 Subject: [PATCH 04/21] remove the makefile --- Makefile | 92 -------------------------------------------------------- 1 file changed, 92 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 1510d24..0000000 --- a/Makefile +++ /dev/null @@ -1,92 +0,0 @@ -.PHONY: clean clean-test clean-pyc clean-build docs help -.DEFAULT_GOAL := help - -define BROWSER_PYSCRIPT -import os, webbrowser, sys - -try: - from urllib import pathname2url -except: - from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT - -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT - -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -f coverage.xml - rm -fr htmlcov/ - rm -fr .pytest_cache - -lint: ## check style with ruff - ruff check . --fix - -pretty: - find heat -name '*.py' | xargs isort - black tests heat - -test: ## run tests quickly with the default Python - pytest - -test-all: ## run tests on every Python version with tox - tox - -coverage: ## check code coverage quickly with the default Python - coverage run --source heat -m pytest - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs - rm -f docs/source/heat.rst - rm -f docs/source/modules.rst - sphinx-apidoc -o docs/source heat - $(MAKE) -C docs clean - $(MAKE) -C docs html - $(BROWSER) docs/build/html/index.html - -servedocs: docs ## compile the docs watching for changes - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python -m build - ls -l dist - -install: clean ## install the package to the active Python's site-packages - pip install -e . From e533d4b3ba9d40fdd1f48d34a513f37a8d9e075d Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 18:52:47 -0700 Subject: [PATCH 05/21] rename tests with _test suffix --- tests/{test_get_value.py => get_value_test.py} | 0 tests/{test_grid_info.py => grid_info_test.py} | 0 tests/{test_irf.py => irf_test.py} | 0 tests/{test_set_value.py => set_value_test.py} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/{test_get_value.py => get_value_test.py} (100%) rename tests/{test_grid_info.py => grid_info_test.py} (100%) rename tests/{test_irf.py => irf_test.py} (100%) rename tests/{test_set_value.py => set_value_test.py} (100%) diff --git a/tests/test_get_value.py b/tests/get_value_test.py similarity index 100% rename from tests/test_get_value.py rename to tests/get_value_test.py diff --git a/tests/test_grid_info.py b/tests/grid_info_test.py similarity index 100% rename from tests/test_grid_info.py rename to tests/grid_info_test.py diff --git a/tests/test_irf.py b/tests/irf_test.py similarity index 100% rename from tests/test_irf.py rename to tests/irf_test.py diff --git a/tests/test_set_value.py b/tests/set_value_test.py similarity index 100% rename from tests/test_set_value.py rename to tests/set_value_test.py From d79ff553eefda031bea7b5a4ec9e2297538646db Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 19:04:46 -0700 Subject: [PATCH 06/21] add pre-commit config file for linting --- .pre-commit-config.yaml | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ab8dce0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,64 @@ +repos: +- repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + name: black + description: "Black: The uncompromising Python code formatter" + entry: black + language: python + language_version: python3 + minimum_pre_commit_version: 2.9.2 + require_serial: true + types_or: [python, pyi] + +- repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify + args: [--max-line-length=88] + +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py310-plus] + +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-builtin-literals + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: forbid-new-submodules + - id: mixed-line-ending + - id: trailing-whitespace + - id: name-tests-test + - id: file-contents-sorter + files: | + (?x)^( + .*requirements(-\w+)?.(in|txt)| + requirements/.*\.txt| + .gitignore + ) + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + language_version: python3.12 + additional_dependencies: [types-all] + files: src/.*\.py$ From 265cd792ab8e9137f50606a71a39b7cc03467ee6 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 19:05:15 -0700 Subject: [PATCH 07/21] clean up the .gitignore file --- .gitignore | 55 ++++-------------------------------------------------- 1 file changed, 4 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index db4561e..3222cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,54 +1,7 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ +*.egg-info/ *.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ +.coverage +.ipynb_checkpoints/ +__pycache__/ build/ -develop-eggs/ dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ From c34c6bf559df7e7d90d194e0a9325714dd053396 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 19:51:46 -0700 Subject: [PATCH 08/21] remove some lint --- CHANGES.rst | 3 +-- LICENSE | 1 - heat/__init__.py | 3 ++- heat/bmi_heat.py | 2 +- heat/heat.py | 2 +- noxfile.py | 2 +- requirements-testing.txt | 5 +---- requirements.txt | 4 ++-- tests/get_value_test.py | 3 ++- tests/irf_test.py | 4 +++- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 94a262c..4ee2ffa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,7 +31,7 @@ Changelog for bmi-example-python - Match bmipy (#12) - Use GitHub Actions for continuous integration (#15) - Dimensionalize flattened values passed into set_value (#17) -- Update CI (#18) +- Update CI (#18) - Switch from versioneer to zest.releaser - Remove obsolete docs directory @@ -46,4 +46,3 @@ Changelog for bmi-example-python ------------------ - Initial release - diff --git a/LICENSE b/LICENSE index 514dd15..0f18cdf 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/heat/__init__.py b/heat/__init__.py index 20911f3..167c269 100644 --- a/heat/__init__.py +++ b/heat/__init__.py @@ -1,6 +1,7 @@ """Model the diffusion of heat over a 2D plate.""" from ._version import __version__ from .bmi_heat import BmiHeat -from .heat import Heat, solve_2d +from .heat import Heat +from .heat import solve_2d __all__ = ["__version__", "BmiHeat", "solve_2d", "Heat"] diff --git a/heat/bmi_heat.py b/heat/bmi_heat.py index 310855a..76daf23 100644 --- a/heat/bmi_heat.py +++ b/heat/bmi_heat.py @@ -39,7 +39,7 @@ def initialize(self, filename=None): if filename is None: self._model = Heat() elif isinstance(filename, str): - with open(filename, "r") as file_obj: + with open(filename) as file_obj: self._model = Heat.from_file_like(file_obj.read()) else: self._model = Heat.from_file_like(filename) diff --git a/heat/heat.py b/heat/heat.py index e940511..baa20d8 100644 --- a/heat/heat.py +++ b/heat/heat.py @@ -53,7 +53,7 @@ def solve_2d(temp, spacing, out=None, alpha=1.0, time_step=1.0): return np.add(temp, out, out=out) -class Heat(object): +class Heat: """Solve the Heat equation on a grid. diff --git a/noxfile.py b/noxfile.py index bcb4d20..d288524 100644 --- a/noxfile.py +++ b/noxfile.py @@ -83,7 +83,7 @@ def clean(session): shutil.rmtree("build", ignore_errors=True) shutil.rmtree("dist", ignore_errors=True) - shutil.rmtree(f"src/{PROJECT}.egg-info", ignore_errors=True) + shutil.rmtree(f"{PROJECT}.egg-info", ignore_errors=True) shutil.rmtree(".pytest_cache", ignore_errors=True) shutil.rmtree(".venv", ignore_errors=True) diff --git a/requirements-testing.txt b/requirements-testing.txt index fc11c86..6c606df 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,4 @@ +bmi-tester coveralls pytest pytest-cov -black -isort -ruff -bmi-tester diff --git a/requirements.txt b/requirements.txt index 7105f42..2fc64c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ +bmipy numpy -scipy pyyaml -bmipy +scipy diff --git a/tests/get_value_test.py b/tests/get_value_test.py index 117525a..de265a2 100644 --- a/tests/get_value_test.py +++ b/tests/get_value_test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_less +from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_array_less from heat import BmiHeat diff --git a/tests/irf_test.py b/tests/irf_test.py index bfe2dc3..869309f 100644 --- a/tests/irf_test.py +++ b/tests/irf_test.py @@ -3,7 +3,9 @@ import numpy as np import yaml -from numpy.testing import assert_almost_equal, assert_array_equal, assert_array_less +from numpy.testing import assert_almost_equal +from numpy.testing import assert_array_equal +from numpy.testing import assert_array_less from heat import BmiHeat From 2a007dcfdf92c9d18c7c5afd6d4cbdc491ee69ff Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 23:10:44 -0700 Subject: [PATCH 09/21] add type annotations and test with mypy --- .pre-commit-config.yaml | 2 +- heat/bmi_heat.py | 120 +++++++++++++++++++++++----------------- heat/heat.py | 40 +++++++++----- pyproject.toml | 8 +++ 4 files changed, 104 insertions(+), 66 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab8dce0..9771c19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,4 +61,4 @@ repos: - id: mypy language_version: python3.12 additional_dependencies: [types-all] - files: src/.*\.py$ + files: heat/.*\.py$ diff --git a/heat/bmi_heat.py b/heat/bmi_heat.py index 76daf23..51ddf95 100644 --- a/heat/bmi_heat.py +++ b/heat/bmi_heat.py @@ -1,8 +1,11 @@ #! /usr/bin/env python """Basic Model Interface implementation for the 2D heat model.""" +from typing import Any + import numpy as np from bmipy import Bmi +from numpy.typing import NDArray from .heat import Heat @@ -15,20 +18,21 @@ class BmiHeat(Bmi): _input_var_names = ("plate_surface__temperature",) _output_var_names = ("plate_surface__temperature",) - def __init__(self): + def __init__(self) -> None: """Create a BmiHeat model that is ready for initialization.""" - self._model = None - self._values = {} - self._var_units = {} - self._var_loc = {} - self._grids = {} - self._grid_type = {} + # self._model: Heat | None = None + self._model: Heat + self._values: dict[str, NDArray[Any]] = {} + self._var_units: dict[str, str] = {} + self._var_loc: dict[str, str] = {} + self._grids: dict[int, list[str]] = {} + self._grid_type: dict[int, str] = {} self._start_time = 0.0 - self._end_time = np.finfo("d").max + self._end_time = float(np.finfo("d").max) self._time_units = "s" - def initialize(self, filename=None): + def initialize(self, filename: str | None = None) -> None: """Initialize the Heat model. Parameters @@ -40,7 +44,7 @@ def initialize(self, filename=None): self._model = Heat() elif isinstance(filename, str): with open(filename) as file_obj: - self._model = Heat.from_file_like(file_obj.read()) + self._model = Heat.from_file_like(file_obj) else: self._model = Heat.from_file_like(filename) @@ -50,11 +54,11 @@ def initialize(self, filename=None): self._grids = {0: ["plate_surface__temperature"]} self._grid_type = {0: "uniform_rectilinear"} - def update(self): + def update(self) -> None: """Advance model by one time step.""" self._model.advance_in_time() - def update_frac(self, time_frac): + def update_frac(self, time_frac: float) -> None: """Update model by a fraction of a time step. Parameters @@ -67,7 +71,7 @@ def update_frac(self, time_frac): self.update() self._model.time_step = time_step - def update_until(self, then): + def update_until(self, then: float) -> None: """Update model until a particular time. Parameters @@ -81,11 +85,12 @@ def update_until(self, then): self.update() self.update_frac(n_steps - int(n_steps)) - def finalize(self): + def finalize(self) -> None: """Finalize model.""" - self._model = None + del self._model + # self._model = None - def get_var_type(self, var_name): + def get_var_type(self, var_name: str) -> str: """Data type of variable. Parameters @@ -100,7 +105,7 @@ def get_var_type(self, var_name): """ return str(self.get_value_ptr(var_name).dtype) - def get_var_units(self, var_name): + def get_var_units(self, var_name: str) -> str: """Get units of variable. Parameters @@ -115,7 +120,7 @@ def get_var_units(self, var_name): """ return self._var_units[var_name] - def get_var_nbytes(self, var_name): + def get_var_nbytes(self, var_name: str) -> int: """Get units of variable. Parameters @@ -130,13 +135,13 @@ def get_var_nbytes(self, var_name): """ return self.get_value_ptr(var_name).nbytes - def get_var_itemsize(self, name): + def get_var_itemsize(self, name: str) -> int: return np.dtype(self.get_var_type(name)).itemsize - def get_var_location(self, name): + def get_var_location(self, name: str) -> str: return self._var_loc[name] - def get_var_grid(self, var_name): + def get_var_grid(self, var_name: str) -> int | None: """Grid id for a variable. Parameters @@ -152,8 +157,9 @@ def get_var_grid(self, var_name): for grid_id, var_name_list in self._grids.items(): if var_name in var_name_list: return grid_id + return None - def get_grid_rank(self, grid_id): + def get_grid_rank(self, grid_id: int) -> int: """Rank of grid. Parameters @@ -168,7 +174,7 @@ def get_grid_rank(self, grid_id): """ return len(self._model.shape) - def get_grid_size(self, grid_id): + def get_grid_size(self, grid_id: int) -> int: """Size of grid. Parameters @@ -183,7 +189,7 @@ def get_grid_size(self, grid_id): """ return int(np.prod(self._model.shape)) - def get_value_ptr(self, var_name): + def get_value_ptr(self, var_name: str) -> NDArray[Any]: """Reference to values. Parameters @@ -198,7 +204,7 @@ def get_value_ptr(self, var_name): """ return self._values[var_name] - def get_value(self, var_name, dest): + def get_value(self, var_name: str, dest: NDArray[Any]) -> NDArray[Any]: """Copy of values. Parameters @@ -216,7 +222,9 @@ def get_value(self, var_name, dest): dest[:] = self.get_value_ptr(var_name).flatten() return dest - def get_value_at_indices(self, var_name, dest, indices): + def get_value_at_indices( + self, var_name: str, dest: NDArray[Any], indices: NDArray[np.int_] + ) -> NDArray[Any]: """Get values at particular indices. Parameters @@ -236,7 +244,7 @@ def get_value_at_indices(self, var_name, dest, indices): dest[:] = self.get_value_ptr(var_name).take(indices) return dest - def set_value(self, var_name, src): + def set_value(self, var_name: str, src: NDArray[Any]) -> None: """Set model values. Parameters @@ -249,7 +257,9 @@ def set_value(self, var_name, src): val = self.get_value_ptr(var_name) val[:] = src.reshape(val.shape) - def set_value_at_indices(self, name, inds, src): + def set_value_at_indices( + self, name: str, inds: NDArray[np.int_], src: NDArray[Any] + ) -> None: """Set model values at particular indices. Parameters @@ -264,76 +274,80 @@ def set_value_at_indices(self, name, inds, src): val = self.get_value_ptr(name) val.flat[inds] = src - def get_component_name(self): + def get_component_name(self) -> str: """Name of the component.""" return self._name - def get_input_item_count(self): + def get_input_item_count(self) -> int: """Get names of input variables.""" return len(self._input_var_names) - def get_output_item_count(self): + def get_output_item_count(self) -> int: """Get names of output variables.""" return len(self._output_var_names) - def get_input_var_names(self): + def get_input_var_names(self) -> tuple[str, ...]: """Get names of input variables.""" return self._input_var_names - def get_output_var_names(self): + def get_output_var_names(self) -> tuple[str, ...]: """Get names of output variables.""" return self._output_var_names - def get_grid_shape(self, grid_id, shape): + def get_grid_shape(self, grid_id: int, shape: NDArray[np.int_]) -> NDArray[np.int_]: """Number of rows and columns of uniform rectilinear grid.""" var_name = self._grids[grid_id][0] shape[:] = self.get_value_ptr(var_name).shape return shape - def get_grid_spacing(self, grid_id, spacing): + def get_grid_spacing( + self, grid_id: int, spacing: NDArray[np.float_] + ) -> NDArray[np.float_]: """Spacing of rows and columns of uniform rectilinear grid.""" spacing[:] = self._model.spacing return spacing - def get_grid_origin(self, grid_id, origin): + def get_grid_origin( + self, grid_id: int, origin: NDArray[np.float_] + ) -> NDArray[np.float_]: """Origin of uniform rectilinear grid.""" origin[:] = self._model.origin return origin - def get_grid_type(self, grid_id): + def get_grid_type(self, grid_id: int) -> str: """Type of grid.""" return self._grid_type[grid_id] - def get_start_time(self): + def get_start_time(self) -> float: """Start time of model.""" return self._start_time - def get_end_time(self): + def get_end_time(self) -> float: """End time of model.""" return self._end_time - def get_current_time(self): + def get_current_time(self) -> float: return self._model.time - def get_time_step(self): + def get_time_step(self) -> float: return self._model.time_step - def get_time_units(self): + def get_time_units(self) -> str: return self._time_units - def get_grid_edge_count(self, grid): + def get_grid_edge_count(self, grid: int) -> int: raise NotImplementedError("get_grid_edge_count") - def get_grid_edge_nodes(self, grid, edge_nodes): + def get_grid_edge_nodes(self, grid: int, edge_nodes: NDArray[np.int_]) -> None: raise NotImplementedError("get_grid_edge_nodes") - def get_grid_face_count(self, grid): + def get_grid_face_count(self, grid: int) -> None: raise NotImplementedError("get_grid_face_count") - def get_grid_face_nodes(self, grid, face_nodes): + def get_grid_face_nodes(self, grid: int, face_nodes: NDArray[np.int_]) -> None: raise NotImplementedError("get_grid_face_nodes") - def get_grid_node_count(self, grid): + def get_grid_node_count(self, grid: int) -> int: """Number of grid nodes. Parameters @@ -348,17 +362,19 @@ def get_grid_node_count(self, grid): """ return self.get_grid_size(grid) - def get_grid_nodes_per_face(self, grid, nodes_per_face): + def get_grid_nodes_per_face( + self, grid: int, nodes_per_face: NDArray[np.int_] + ) -> None: raise NotImplementedError("get_grid_nodes_per_face") - def get_grid_face_edges(self, grid, face_edges): + def get_grid_face_edges(self, grid: int, face_edges: NDArray[np.int_]) -> None: raise NotImplementedError("get_grid_face_edges") - def get_grid_x(self, grid, x): + def get_grid_x(self, grid: int, x: NDArray[np.float_]) -> None: raise NotImplementedError("get_grid_x") - def get_grid_y(self, grid, y): + def get_grid_y(self, grid: int, y: NDArray[np.float_]) -> None: raise NotImplementedError("get_grid_y") - def get_grid_z(self, grid, z): + def get_grid_z(self, grid: int, z: NDArray[np.float_]) -> None: raise NotImplementedError("get_grid_z") diff --git a/heat/heat.py b/heat/heat.py index baa20d8..fa31bc1 100644 --- a/heat/heat.py +++ b/heat/heat.py @@ -1,11 +1,21 @@ """The 2D heat model.""" +from __future__ import annotations + +from io import TextIOBase import numpy as np import yaml +from numpy.typing import NDArray from scipy import ndimage -def solve_2d(temp, spacing, out=None, alpha=1.0, time_step=1.0): +def solve_2d( + temp: NDArray[np.float_], + spacing: tuple[float, ...], + out: NDArray[np.float_] | None = None, + alpha: float = 1.0, + time_step: float = 1.0, +) -> NDArray[np.float_]: """Solve the 2D Heat Equation on a uniform mesh. Parameters @@ -82,8 +92,12 @@ class Heat: """ def __init__( - self, shape=(10, 20), spacing=(1.0, 1.0), origin=(0.0, 0.0), alpha=1.0 - ): + self, + shape: tuple[int, int] = (10, 20), + spacing: tuple[float, float] = (1.0, 1.0), + origin: tuple[float, float] = (0.0, 0.0), + alpha: float = 1.0, + ) -> None: """Create a new heat model. Parameters @@ -108,17 +122,17 @@ def __init__( self._next_temperature = np.empty_like(self._temperature) @property - def time(self): + def time(self) -> float: """Current model time.""" return self._time @property - def temperature(self): + def temperature(self) -> NDArray[np.float_]: """Temperature of the plate.""" return self._temperature @temperature.setter - def temperature(self, new_temp): + def temperature(self, new_temp: float) -> None: """Set the temperature of the plate. Parameters @@ -129,32 +143,32 @@ def temperature(self, new_temp): self._temperature[:] = new_temp @property - def time_step(self): + def time_step(self) -> float: """Model time step.""" return self._time_step @time_step.setter - def time_step(self, time_step): + def time_step(self, time_step: float) -> None: """Set model time step.""" self._time_step = time_step @property - def shape(self): + def shape(self) -> tuple[int, int]: """Shape of the model grid.""" return self._shape @property - def spacing(self): + def spacing(self) -> tuple[float, float]: """Spacing between nodes of the model grid.""" return self._spacing @property - def origin(self): + def origin(self) -> tuple[float, float]: """Origin coordinates of the model grid.""" return self._origin @classmethod - def from_file_like(cls, file_like): + def from_file_like(cls: type[Heat], file_like: TextIOBase) -> Heat: """Create a Heat object from a file-like object. Parameters @@ -170,7 +184,7 @@ def from_file_like(cls, file_like): config = yaml.safe_load(file_like) return cls(**config) - def advance_in_time(self): + def advance_in_time(self) -> None: """Calculate new temperatures for the next time step.""" solve_2d( self._temperature, diff --git a/pyproject.toml b/pyproject.toml index 551a1af..8328aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,14 @@ relative_files = true profile = "black" force_single_line = "true" +[tool.mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +warn_redundant_casts = true +warn_unused_ignores = true + [tool.pytest.ini_options] minversion = "6.0" testpaths = [ From b2eae0a4fffc4c711601d3c024d71d48e407fb31 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 29 Jan 2024 23:23:22 -0700 Subject: [PATCH 10/21] remove format workflow --- .github/workflows/format.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml deleted file mode 100644 index 2d7a6b9..0000000 --- a/.github/workflows/format.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Format - -on: [push, pull_request] - -jobs: - - format: - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. Without this if check, checks are duplicated since - # internal PRs match both the push and pull_request events. - if: - github.event_name == 'push' || github.event.pull_request.head.repo.full_name != - github.repository - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - - uses: psf/black@stable - with: - args: ". --check" From 2160e6b24879c163ec0d4770fdde23adf549d2a9 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 30 Jan 2024 09:57:47 -0700 Subject: [PATCH 11/21] require bmi-tester >=0.5.7 --- pyproject.toml | 2 +- requirements-testing.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8328aed..3544572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "nox", ] testing = [ - "bmi-tester", + "bmi-tester>=0.5.7", "coveralls", "pytest", "pytest-cov", diff --git a/requirements-testing.txt b/requirements-testing.txt index 6c606df..79db897 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,4 +1,4 @@ -bmi-tester +bmi-tester>=0.5.7 coveralls pytest pytest-cov From 50fa6bcbbd0696ec16b8c3611e1baf6e38f5e2eb Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 30 Jan 2024 10:23:38 -0700 Subject: [PATCH 12/21] drop python 3.9 from testing; install with pip --- .github/workflows/test.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6df638..7e14823 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -36,12 +36,8 @@ jobs: conda info conda list - - name: Install requirements - run: | - mamba install --file=requirements.txt --file=requirements-testing.txt - - name: Build and install package - run: pip install -e . + run: pip install -e .[testing] - name: Test run: | From 1e20f152dceb482ff842b8a7fa384cdcde3385cf Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 30 Jan 2024 10:28:10 -0700 Subject: [PATCH 13/21] need to install bmi-tester from conda --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e14823..8308b69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,11 +33,12 @@ jobs: - name: Show conda installation info run: | + const install --file=requirements-testing.txt conda info conda list - name: Build and install package - run: pip install -e .[testing] + run: pip install . - name: Test run: | From f73b95b882f1dc17ab8cb1a8d576892d22e2c831 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 30 Jan 2024 10:37:20 -0700 Subject: [PATCH 14/21] fix typo --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8308b69..18c5d0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Show conda installation info run: | - const install --file=requirements-testing.txt + conda install --file=requirements-testing.txt conda info conda list From 9b16cad0d001dbd9e21aadd2a0dafebae298186c Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 31 Jan 2024 09:50:45 -0700 Subject: [PATCH 15/21] run tests with nox, run bmi-test outside of virtualenv --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18c5d0f..f5548f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12"] + fail-fast: false steps: - uses: actions/checkout@v4 @@ -33,18 +34,36 @@ jobs: - name: Show conda installation info run: | - conda install --file=requirements-testing.txt conda info conda list - - name: Build and install package - run: pip install . - - name: Test run: | - pytest --cov=heat --cov-report=xml:./coverage.xml -vvv + pip install nox + nox -s test --force-pythons="${{ matrix.python-version }}" + + - name: Run bmi-test + run: | + pip install model-metadata + conda install gimli.units pymt_hydrotrend -c conda-forge + pip install . bmi-test heat:BmiHeat --config-file=./examples/heat.yaml --root-dir=./examples -vvv - name: Coveralls - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + if: matrix.os == 'ubuntu-latest' uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + flag-name: py${{ matrix.python-version }}-${{ matrix.os }} + + debug: true + + coveralls_finish: + needs: build-and-test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + debug: true From f8a4769cbf04ee15d60245d54f01c1a134a94618 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 31 Jan 2024 10:04:10 -0700 Subject: [PATCH 16/21] don't need to force a python version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5548f7..e6a6fc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Test run: | pip install nox - nox -s test --force-pythons="${{ matrix.python-version }}" + nox -s test - name: Run bmi-test run: | From 34ddac8ce9643e7d8d1ca39e3224395066ba5d3d Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 31 Jan 2024 10:12:39 -0700 Subject: [PATCH 17/21] remove bmi-tester as a testing requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3544572..9e15052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "nox", ] testing = [ - "bmi-tester>=0.5.7", + # "bmi-tester>=0.5.7", "coveralls", "pytest", "pytest-cov", From 4e3c58ceca22be5432de5e7b37ebae1b47bd357a Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 31 Jan 2024 10:16:09 -0700 Subject: [PATCH 18/21] pip install bmi-tester --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6a6fc3..d025fe7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,8 @@ jobs: - name: Run bmi-test run: | pip install model-metadata - conda install gimli.units pymt_hydrotrend -c conda-forge + conda install gimli.units -c conda-forge + pip install bmi-tester pip install . bmi-test heat:BmiHeat --config-file=./examples/heat.yaml --root-dir=./examples -vvv From 6bb5d14d362cbc77eb1462fdc9faaf09d6048b67 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 22 Feb 2024 12:57:51 -0700 Subject: [PATCH 19/21] pip install gimli.units; fix path to heat.yaml --- .github/workflows/test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d025fe7..001a87d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,11 +44,9 @@ jobs: - name: Run bmi-test run: | - pip install model-metadata - conda install gimli.units -c conda-forge - pip install bmi-tester + pip install model-metadata gimli.units bmi-tester pip install . - bmi-test heat:BmiHeat --config-file=./examples/heat.yaml --root-dir=./examples -vvv + bmi-test heat:BmiHeat --config-file=./heat.yaml --root-dir=./examples -vvv - name: Coveralls if: matrix.os == 'ubuntu-latest' From 068e532c410095e191ce1a6bf5f98c85e817f095 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 22 Feb 2024 13:13:22 -0700 Subject: [PATCH 20/21] cd into examples folder before running bmi-tester --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 001a87d..73b68aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: run: | pip install model-metadata gimli.units bmi-tester pip install . - bmi-test heat:BmiHeat --config-file=./heat.yaml --root-dir=./examples -vvv + cd examples && bmi-test heat:BmiHeat --config-file=./heat.yaml --root-dir=. -vvv - name: Coveralls if: matrix.os == 'ubuntu-latest' From 9c8ff989b74aa7a999e71580d287c8e3852f2cc7 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 22 Feb 2024 14:05:17 -0700 Subject: [PATCH 21/21] use setup-python instead of mambaforge --- .github/workflows/test.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 73b68aa..e736849 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,18 +25,11 @@ jobs: steps: - uses: actions/checkout@v4 - - - uses: conda-incubator/setup-miniconda@v3 + - name: Set up Python + uses: actions/setup-python@v5 with: - miniforge-variant: Mambaforge - miniforge-version: latest python-version: ${{ matrix.python-version }} - - name: Show conda installation info - run: | - conda info - conda list - - name: Test run: | pip install nox