diff --git a/.github/workflows/type_checks.yml b/.github/workflows/type_checks.yml new file mode 100644 index 00000000000..f8f7c0e8304 --- /dev/null +++ b/.github/workflows/type_checks.yml @@ -0,0 +1,53 @@ +# Static type checks +# +# This workflow runs static type checks using mypy. +# +# It is run on every commit to the main and pull request branches. It is also +# scheduled to run daily on the main branch. +# +name: Static Type Checks + +on: + push: + branches: [ main ] + pull_request: + # Schedule daily tests + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + static_check: + name: Static Type Check + runs-on: ubuntu-latest + + steps: + # Checkout current git repository + - name: Checkout + uses: actions/checkout@v4.1.1 + + # Setup Python + - name: Set up Python + uses: actions/setup-python@v4.7.1 + with: + python-version: '3.12' + + - name: Install packages + run: | + # Need to install four groups of packages: + # 1. required packages + # 2. optional packages + # 3. type checker and stub packages + # 4. other packages that are used somewhere in PyGMT + python -m pip install \ + numpy pandas xarray netcdf4 packaging \ + contextily geopandas ipython rioxarray \ + mypy pandas-stubs \ + matplotlib pytest + python -m pip list + + - name: Static type check + run: make typecheck diff --git a/.gitignore b/.gitignore index c89f876a3ca..7d46956d921 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ MANIFEST .coverage coverage.xml htmlcov/ +.mypy_cache/ .pytest_cache/ .ruff_cache/ results/ diff --git a/Makefile b/Makefile index 3fdf06b911c..8e73587bea6 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ help: @echo " format run blackdoc, docformatter and ruff to automatically format the code" @echo " check run code style and quality checks (blackdoc, docformatter, ruff)" @echo " codespell run codespell to check common misspellings" + @echo " typecheck run mypy for static type check" @echo " clean clean up build and generated files" @echo " distclean clean up build and generated files, including project metadata files" @echo "" @@ -73,12 +74,15 @@ check: codespell: @codespell +typecheck: + mypy ${PROJECT} + clean: find . -name "*.pyc" -exec rm -v {} + find . -name "*~" -exec rm -v {} + find . -type d -name "__pycache__" -exec rm -rv {} + rm -rvf build dist .eggs MANIFEST .coverage htmlcov coverage.xml - rm -rvf .cache .pytest_cache .ruff_cache + rm -rvf .cache .mypy_cache .pytest_cache .ruff_cache rm -rvf $(TESTDIR) rm -rvf baseline rm -rvf result_images diff --git a/environment.yml b/environment.yml index a637627b378..8c67d425f7f 100644 --- a/environment.yml +++ b/environment.yml @@ -41,3 +41,6 @@ dependencies: - sphinx-design - sphinx-gallery - sphinx_rtd_theme + # Dev dependencies (type hints) + - mypy + - pandas-stubs diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index 6c2a10af192..da47c1f71af 100644 --- a/pygmt/datasets/load_remote_dataset.py +++ b/pygmt/datasets/load_remote_dataset.py @@ -1,7 +1,7 @@ """ Internal function to load GMT remote datasets. """ -from typing import NamedTuple +from typing import NamedTuple, Union from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import kwargs_to_strings @@ -58,7 +58,7 @@ class GMTRemoteDataset(NamedTuple): title: str name: str long_name: str - units: str + units: Union[str, None] resolutions: dict[str, Resolution] extra_attributes: dict diff --git a/pygmt/figure.py b/pygmt/figure.py index c586d96933a..a54f8c89da0 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -517,7 +517,7 @@ def _repr_html_(self): html = '' return html.format(image=base64_png.decode("utf-8"), width=500) - from pygmt.src import ( + from pygmt.src import ( # type: ignore [misc] basemap, coast, colorbar, diff --git a/pyproject.toml b/pyproject.toml index 8c6f2489acf..517c5c1399e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,10 @@ make-summary-multi-line = true wrap-summaries = 79 wrap-descriptions = 79 +[tool.mypy] +exclude = ["pygmt/tests/"] +ignore_missing_imports = true + [tool.ruff] line-length = 88 # E501 (line-too-long) show-source = true