diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 176d079..b5cebd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,17 +4,6 @@ repos: hooks: - id: absolufy-imports name: absolufy-imports - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - language_version: python3 - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 - hooks: - - id: pyupgrade - args: - - --py38-plus - repo: https://github.com/psf/black rev: 23.12.1 hooks: @@ -22,11 +11,11 @@ repos: language_version: python3 args: - --target-version=py38 - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.14 hooks: - - id: flake8 - language_version: python3 + - id: ruff + args: ["--fix", "--show-fixes"] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: diff --git a/HOW_TO_RELEASE b/HOW_TO_RELEASE index f5586c8..be04408 100644 --- a/HOW_TO_RELEASE +++ b/HOW_TO_RELEASE @@ -13,17 +13,10 @@ Time required: about an hour. git commit -a -m 'Release v0.X.Y' 5. Tag the release: git tag -a v0.X.Y -m 'v0.X.Y' - 6. Build source for pypi: - python setup.py sdist - 7. Use twine to register and upload the release on pypi. Be careful, you can't - take this back! - twine upload dist/xarray_extras-0.X.Y* - You will need to be listed as a package owner at - https://pypi.python.org/pypi/xarray_extras for this to work. - 8. Push your changes to main: + 6. Push your changes to main: git push origin main git push origin --tags - 9. Update the stable branch (used by ReadTheDocs) and switch back to main: + 7. Update the stable branch (used by ReadTheDocs) and switch back to main: git checkout stable git rebase main git push origin stable @@ -31,14 +24,20 @@ Time required: about an hour. It's OK to force push to 'stable' if necessary. We also update the stable branch with `git cherrypick` for documentation only fixes that apply the current released version. -10. Add a section for the next release (v.X.(Y+1)) to doc/whats-new.rst. -11. Commit your changes and push to main again: + 8. Add a section for the next release (v.X.(Y+1)) to doc/whats-new.rst. + 9. Commit your changes and push to main again: git commit -a -m 'Revert to dev version' git push origin main You're done pushing to main! -12. Issue the release on GitHub. Open https://github.com/crusaderky/xarray_extras/releases; +10. Issue the release on GitHub. Open https://github.com/crusaderky/xarray_extras/releases; the new release should have automatically appeared. Otherwise, click on "Draft a new release" and paste in the latest from whats-new.rst. +11. Download the .tar.gz package for the release +12. Use twine to register and upload the release on pypi. Be careful, you can't + take this back! + twine upload xarray_extras-*.tar.gz + You will need to be listed as a package owner at + https://pypi.python.org/pypi/xarray_extras for this to work. 13. Update the docs. Login to https://readthedocs.org/projects/xarray_extras/versions/ and switch your new release tag (at the bottom) from "Inactive" to "Active". It should now build automatically. diff --git a/doc/develop.rst b/doc/develop.rst index e24bacf..42577a1 100644 --- a/doc/develop.rst +++ b/doc/develop.rst @@ -59,9 +59,8 @@ Test using ``py.test``: Code Formatting --------------- -xarray_extras uses several code linters (flake8, black, isort, pyupgrade, mypy), -which are enforced by CI. Developers should run them locally before they submit a PR, -through the single command +xarray_extras uses several code linters (black, ruff, mypy), which are enforced by CI. +Developers should run them locally before they submit a PR, through the single command .. code-block:: bash diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 6539b45..29d95a4 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -24,6 +24,8 @@ v0.6.0 (Unreleased) - Added support for Python 3.10 and 3.11 - Added support for recent versions of Pandas (tested up to 2.2) and xarray - Added support for :cls:`pathlib.Path` in function arguments +- Migrated from setup.cfg to pyproject.toml +- Migrated from flake8+isort+pyupgrade to ruff .. _whats-new.0.5.0: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9b50901 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[project] +name = "xarray_extras" +authors = [{name = "Guido Imperiale", email = "crusaderky@gmail.com"}] +license = {text = "Apache"} +description = "Advanced / experimental algorithms for xarray" +keywords = ["xarray"] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +requires-python = ">=3.8" +dependencies = [ + "dask >= 2022.6", + "numba >= 0.56", + "numpy >= 1.23", + "pandas >= 1.5", + "scipy >= 1.9", + "xarray >= 2022.6.0", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/crusaderky/xarray_extras" + +[tool.setuptools] +packages = ["xarray_extras"] +zip-safe = false # https://mypy.readthedocs.io/en/latest/installed_packages.html +include-package-data = true + +[tool.setuptools_scm] +# Use hardcoded version when .git has been removed and this is not a package created +# by sdist. This is the case e.g. of a remote deployment with PyCharm. +fallback_version = "9999" + +[tool.setuptools.package-data] +xarray_extras = [ + "py.typed", + "tests/data/*", +] + +[build-system] +requires = [ + "setuptools>=66", + "setuptools_scm[toml]", +] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +addopts = "--strict-markers --strict-config -v -r sxfE --color=yes" +xfail_strict = true +python_files = ["test_*.py"] +testpaths = ["xarray_extras/tests"] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: nocover", + "pragma: no cover", + "TYPE_CHECKING", + "except ImportError", + "@overload", + '@(abc\.)?abstractmethod', + '@(numba\.)?jit', + '@(numba\.)?vectorize', + '@(numba\.)?guvectorize', +] + +[tool.ruff] +builtins = ["ellipsis"] +exclude = [".eggs"] +target-version = "py38" + +[tool.ruff.lint] +ignore = [ + "E402", # module level import not at top of file + "SIM108", # use ternary operator instead of if-else block +] +select = [ + "F", # Pyflakes + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "SIM", # flake8-simplify + "E", # Pycodestyle + "W", # Pycodestyle + "I", # isort + "N", # pep8-naming + "UP", # Pyupgrade + "RUF", # unused-noqa + "EXE001", # Shebang is present but file is not executable +] + +[tool.ruff.lint.isort] +known-first-party = ["xarray_extras"] + +[tool.mypy] +allow_incomplete_defs = false +allow_untyped_decorators = false +allow_untyped_defs = false +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unreachable = true + +[[tool.mypy.overrides]] +module = ["*.tests.*"] +allow_untyped_defs = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3ccb7c1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,98 +0,0 @@ -[metadata] -name = xarray_extras -author = Guido Imperiale -author_email = crusaderky@gmail.com -license = Apache -description = Advanced / experimental algorithms for xarray -description_content_type=text/plain -keywords = xarray -url = https://github.com/crusaderky/xarray_extras -classifiers = - Development Status :: 3 - Alpha - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Intended Audience :: Science/Research - Topic :: Scientific/Engineering - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - -[options] -packages = xarray_extras -zip_safe = False # https://mypy.readthedocs.io/en/latest/installed_packages.html -include_package_data = True -python_requires = >=3.8 -install_requires = - dask >= 2022.6 - numba >= 0.56 - numpy >= 1.23 - pandas >= 1.5 - scipy >= 1.9 - xarray >= 2022.6.0 - -setup_requires = setuptools_scm - -[options.package_data] -xarray_extras = - py.typed - tests/data/* - -[bdist_wheel] -universal = 1 - -[wheel] -universal = 1 - -[tool:pytest] -addopts = --strict-markers --strict-config -v -r sxfE --color=yes -xfail_strict = true -python_files = test_*.py -testpaths = xarray_extras/tests - -[coverage:report] -show_missing = true -exclude_lines = - pragma: nocover - pragma: no cover - TYPE_CHECKING - except ImportError - @overload - @(abc\.)?abstractmethod - -[flake8] -# https://github.com/python/black#line-length -max-line-length = 88 -# E203: PEP8-compliant slice operators -# https://github.com/python/black#slices -# W503: Allow for breaks before binary operator (Knuth's convention) - see -# https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator -ignore = E203, W503 -exclude = - .eggs - doc/ - -[isort] -default_section = THIRDPARTY -known_first_party = xarray_extras -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -line_length = 88 - -[mypy] -allow_incomplete_defs = false -allow_untyped_decorators = false -allow_untyped_defs = false -ignore_missing_imports = true -no_implicit_optional = true -show_error_codes = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_unreachable = true - -[mypy-*.tests.*] -allow_untyped_defs = true diff --git a/setup.py b/setup.py index f7dea83..c57fa5c 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,7 @@ -#!/usr/bin/env python from setuptools import Extension, setup setup( - # Use hardcoded version when .git has been removed and this is not a package created - # by sdist. This is the case e.g. of a remote deployment with PyCharm. - use_scm_version={"fallback_version": "999"}, + use_scm_version=True, # Compile CPython extensions ext_modules=[ Extension( diff --git a/xarray_extras/csv.py b/xarray_extras/csv.py index 3478fde..0e481d5 100644 --- a/xarray_extras/csv.py +++ b/xarray_extras/csv.py @@ -18,7 +18,7 @@ __all__ = ("to_csv",) if TYPE_CHECKING: - # TODO remove TYPE_CHECKING (requires dask >=2023.9.1) + # TODO: remove TYPE_CHECKING (requires dask >=2023.9.1) from dask.typing import DaskCollection, Key @@ -76,7 +76,7 @@ def to_csv( and concatenate the partial outputs. """ if not isinstance(x, xarray.DataArray): - raise ValueError("first argument must be a DataArray") + raise TypeError("first argument must be a DataArray") # Health checks if not isinstance(path, (str, Path)): @@ -86,7 +86,7 @@ def to_csv( if x.ndim not in (1, 2): raise ValueError( - "cannot convert arrays with %d dimensions into " "pandas objects" % x.ndim + "cannot convert arrays with %d dimensions into pandas objects" % x.ndim ) if nogil and x.dtype.kind not in "if": diff --git a/xarray_extras/interpolate.py b/xarray_extras/interpolate.py index 2ade662..548d691 100644 --- a/xarray_extras/interpolate.py +++ b/xarray_extras/interpolate.py @@ -169,7 +169,7 @@ def splev( dims = [tck.spline_dim] else: raise ValueError( - "N-dimensional x_new is only supported if " "x_new is a DataArray" + "N-dimensional x_new is only supported if x_new is a DataArray" ) x_new = xarray.DataArray(x_new, dims=dims, coords={tck.spline_dim: x_new}) diff --git a/xarray_extras/kernels/csv.py b/xarray_extras/kernels/csv.py index bc2a726..04294c1 100755 --- a/xarray_extras/kernels/csv.py +++ b/xarray_extras/kernels/csv.py @@ -44,7 +44,7 @@ def to_csv( x_pd = pd.DataFrame(x, index, columns) else: # proper ValueError already raised in wrapper - assert False, "unreachable" # pragma: nocover + raise AssertionError("unreachable") # pragma: nocover encoding = kwargs.pop("encoding", "utf-8") header = kwargs.pop("header", True) @@ -83,7 +83,7 @@ def to_csv( elif x.dtype.kind == "f": body_bytes = snprintcsvd(x, index_csv, sep, fmt, na_rep) else: - raise NotImplementedError("only int and float are supported when " "nogil=True") + raise NotImplementedError("only int and float are supported when nogil=True") if header is not False: header_bytes = ( diff --git a/xarray_extras/kernels/cumulatives.py b/xarray_extras/kernels/cumulatives.py index f787dc5..2051894 100644 --- a/xarray_extras/kernels/cumulatives.py +++ b/xarray_extras/kernels/cumulatives.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Numba kernels for :mod:`cumulatives` """ import numpy as np @@ -39,7 +38,7 @@ def compound_mean(x: np.ndarray, c: np.ndarray, y: np.ndarray) -> None: """ acc = 0 j = 0 # Initialise j explicitly for when x.shape == (0, ) - for j, i in enumerate(c): + for j, i in enumerate(c): # noqa: B007 if i == -1: break acc += x[i] diff --git a/xarray_extras/kernels/np_to_csv_py.py b/xarray_extras/kernels/np_to_csv_py.py index c1ea6d6..ff96d99 100755 --- a/xarray_extras/kernels/np_to_csv_py.py +++ b/xarray_extras/kernels/np_to_csv_py.py @@ -79,7 +79,7 @@ def snprintcsvd( # Test fmt while in Python - much better to get # an Exception here than a segfault in C! if fmt is not None: - fmt % 1.23 # noqa + fmt % 1.23 bfmt = fmt.encode("ascii") + bsep trim_zeros = False else: @@ -135,4 +135,4 @@ def snprintcsvi(a: np.ndarray, index: str, sep: str = ",") -> bytes: buf = ctypes.create_string_buffer(bufsize) nchar = np_to_csv.snprintcsvi(buf, bufsize, a, a.shape[0], a.shape[1], bindex, bsep) assert nchar < bufsize - return bytes(buf[:nchar]) # type: ignore + return bytes(buf[:nchar]) # type: ignore[arg-type] diff --git a/xarray_extras/numba_extras.py b/xarray_extras/numba_extras.py index b89f8b3..855c67e 100644 --- a/xarray_extras/numba_extras.py +++ b/xarray_extras/numba_extras.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Extensions to numba """ from __future__ import annotations @@ -8,6 +7,23 @@ import numba +_DTYPES = ( + # uint needs to appear before signed int: + # https://github.com/numba/numba/issues/2934 + "uint8", + "uint16", + "uint32", + "uint64", + "int8", + "int16", + "int32", + "int64", + "float32", + "float64", + "complex64", + "complex128", +) + def guvectorize( signature: str, layout: str, **kwargs: Any @@ -40,24 +56,8 @@ def guvectorize( Discussing upstream fix; see ``_. """ - DTYPES = [ - # uint needs to appear before signed int: - # https://github.com/numba/numba/issues/2934 - "uint8", - "uint16", - "uint32", - "uint64", - "int8", - "int16", - "int32", - "int64", - "float32", - "float64", - "complex64", - "complex128", - ] if "{T}" in signature: - signatures = [signature.format(T=dtype) for dtype in DTYPES] + signatures = [signature.format(T=dtype) for dtype in _DTYPES] else: signatures = [signature] kwargs.setdefault("cache", True) diff --git a/xarray_extras/stack.py b/xarray_extras/stack.py index 0fa7e6f..310a7d3 100644 --- a/xarray_extras/stack.py +++ b/xarray_extras/stack.py @@ -42,7 +42,7 @@ def proper_unstack(array: T, dim: Hashable) -> T: if code not in level_map: level_map[code] = len(level_map) - levels.append([levels_i[k] for k in level_map.keys()]) + levels.append([levels_i[k] for k in level_map]) codes.append([level_map[k] for k in codes_i]) mindex = pandas.MultiIndex(levels, codes, names=mindex.names) diff --git a/xarray_extras/tests/test_csv.py b/xarray_extras/tests/test_csv.py index fb4dd25..a94cc85 100644 --- a/xarray_extras/tests/test_csv.py +++ b/xarray_extras/tests/test_csv.py @@ -79,7 +79,6 @@ def test_series(chunks, nogil, dtype, header, lineterminator): @pytest.mark.parametrize("lineterminator", ["\n", "\r\n"]) def test_series_with_path(path_type, chunks, nogil, dtype, header, lineterminator): x = xarray.DataArray([1, 2, 3, 4], dims=["x"], coords={"x": [10, 20, 30, 40]}) - print(f"Path type = '{path_type}' of type {type(path_type)}") assert_to_csv_with_path_type( x, path_type, chunks, nogil, dtype, header=header, lineterminator=lineterminator ) @@ -231,7 +230,7 @@ def test_buffer_overflow_float(chunks, nogil, float_format, na_rep, index, coord if nogil and not index and np.isnan(x) and na_rep == "": # Expected: b'""\n' # Actual: b'\n' - pytest.xfail("pandas prints useless " " for empty lines") + pytest.xfail("pandas prints useless for empty lines") a = xarray.DataArray([x], dims=["x"], coords={"x": [coord]}) assert_to_csv( diff --git a/xarray_extras/tests/test_interpolate.py b/xarray_extras/tests/test_interpolate.py index ad0335c..dea9b6c 100644 --- a/xarray_extras/tests/test_interpolate.py +++ b/xarray_extras/tests/test_interpolate.py @@ -264,7 +264,7 @@ def test_chunked_x(): with pytest.raises(NotImplementedError) as excinfo: splrep(y, "x", 1) - assert str(excinfo.value) == "Unsupported: multiple chunks on " "interpolation dim" + assert str(excinfo.value) == "Unsupported: multiple chunks on interpolation dim" def test_distributed():