diff --git a/.flake8 b/.flake8 index 9e60cc1..16eb6cf 100644 --- a/.flake8 +++ b/.flake8 @@ -23,7 +23,7 @@ ignore = W503, # W504: line break after binary operator W504 -exclude = +exclude = .eggs build docs/conf.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 8c55561..81e5843 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -13,14 +13,14 @@ assignees: '' ## How to Reproduce Steps to reproduce the behaviour: -1. -2. -3. +1. +2. +3. ## Expected Behaviour -## Environment +## Environment - OS & Version: [e.g., Ubuntu 20.04 LTS] - nc-time-axis Version: [e.g., From the command line run `python -c "import nc_time_axis; print(nc_time_axis.__version__)"`] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04bf5f2..f9486e3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,10 @@ updates: day: "thursday" time: "01:00" timezone: "Europe/London" + groups: + dependencies: + patterns: + - "*" labels: - "New: Pull Request" - "Bot" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ec16cd..a180e9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,26 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.ci/#configuration +# See https://github.com/scientific-python/cookie#sp-repo-review +# See https://github.com/SciTools/.github/wiki/Linting for common linter rules + +ci: + autofix_prs: false + autoupdate_commit_msg: "chore: update pre-commit hooks" + +# Alphabetised, for lack of a better order. +files: | + (?x)( + docs\/.+\.(py|rst)| + pyproject\.toml| + setup\.py| + src\/.+\.py + ) +minimum_pre_commit_version: 1.21.0 repos: + +# Hook for pre-commit's built-in checks: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v5.0.0' hooks: @@ -19,37 +38,69 @@ repos: - id: check-toml # Check YAML file syntax. - id: check-yaml - # Makes sure files end in a newline and only a newline + # Makes sure files end in a newline and only a newline. + # Duplicates Ruff W292 but also works on non-Python files. - id: end-of-file-fixer # Replaces or checks mixed line ending - id: mixed-line-ending # Don't commit to main branch. - id: no-commit-to-branch + # Trims trailing whitespace. + # Duplicates Ruff W291 but also works on non-Python files. + - id: trailing-whitespace -- repo: https://github.com/psf/black - rev: '24.10.0' +# Hooks for all other repos. +# Keep these alphabetised by hook (aka 'id') order. + +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.19.1 hooks: - - id: black + - id: blacken-docs + types: [file, rst] + +- repo: https://github.com/codespell-project/codespell + rev: 'v2.3.0' + hooks: + - id: codespell + types_or: [asciidoc, python, markdown, rst] + additional_dependencies: [tomli] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.13.0' + hooks: + - id: mypy + exclude: 'noxfile\.py|docs/conf\.py' + +- repo: https://github.com/numpy/numpydoc + rev: v1.8.0 + hooks: + - id: numpydoc-validation types: [file, python] - args: [--config=./pyproject.toml, .] -- repo: https://github.com/PyCQA/flake8 - rev: '7.1.1' +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.7.4" hooks: - - id: flake8 + - id: ruff + types: [file, python] + args: [--fix, --show-fixes] + - id: ruff-format types: [file, python] - args: [--config=./.flake8] -- repo: https://github.com/pycqa/isort - rev: '5.13.2' +- repo: https://github.com/aio-libs/sort-all + rev: v1.3.0 hooks: - - id: isort + - id: sort-all types: [file, python] - args: ["--profile", "black", "--filter-files"] -- repo: https://github.com/codespell-project/codespell - rev: 'v2.3.0' +- repo: https://github.com/scientific-python/cookie + rev: 2024.08.19 hooks: - - id: codespell - types_or: [python, markdown, rst] - additional_dependencies: [tomli] + - id: sp-repo-review + additional_dependencies: ["repo-review[cli]"] + args: ["--show=errskip"] + +- repo: https://github.com/abravalheri/validate-pyproject + # More exhaustive than Ruff RUF200. + rev: "v0.23" + hooks: + - id: validate-pyproject diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b24bcfb..bfca220 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,6 @@ We would love to hear from you! - [View/discuss existing pull requests](https://github.com/SciTools/nc-time-axis/pulls) - [Raise a new pull request](https://github.com/SciTools/nc-time-axis/compare) -Note that all authors on pull requests will automatically be asked to sign the +Note that all authors on pull requests will automatically be asked to sign the [SciTools Contributor Licence Agreement](https://cla-assistant.io/SciTools/) -(CLA), if they have not already done so. +(CLA), if they have not already done so. diff --git a/LICENSE b/LICENSE index 1f2fc0e..40038b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ BSD 3-Clause License -Copyright (c) 2016, Met Office. +Copyright (c) 2016, Met Office. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/examples.rst b/docs/examples.rst index 0f52975..1354418 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -13,6 +13,11 @@ Basic Usage `matplotlib`_. To register its converters, simply ``import nc_time_axis``. Then you will be able to make plots with :py:class:`cftime.datetime` axes. +.. + comment: @savefig causes blacken-docs to fail + +.. blacken-docs:off + .. ipython:: python :okwarning: @@ -30,6 +35,8 @@ will be able to make plots with :py:class:`cftime.datetime` axes. @savefig basic.png fig.show() +.. blacken-docs:on + Setting the Axes Ticks and Tick Format -------------------------------------- @@ -43,14 +50,22 @@ documentation for acceptable format codes) and the calendar type of the axis (see the :py:class:`cftime.datetime` documentation for valid calendar strings). +.. + @savefig causes blacken-docs to fail + +.. blacken-docs:off + .. ipython:: python :okwarning: fig, ax = plt.subplots(1, 1) - ax.plot(times, y); - ax.set_xticks([cftime.datetime(2000, 1, day, calendar="noleap") for day in range(2, 19, 4)]); + ax.plot(times, y) + ax.set_xticks( + [cftime.datetime(2000, 1, day, calendar="noleap") for day in range(2, 19, 4)] + ) formatter = nc_time_axis.CFTimeFormatter("%m-%d %H:%M", "noleap") ax.xaxis.set_major_formatter(formatter) - @savefig set_ticks.png fig.show() + +.. blacken-docs:on diff --git a/docs/index.rst b/docs/index.rst index 02f5335..8b33e61 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ nc-time-axis ============ ``nc-time-axis`` is a package that enables making plots in `matplotlib`_ with axes made -up of :py:class:`cftime.datetime` dates with any calendar type. +up of :py:class:`cftime.datetime` dates with any calendar type. .. toctree:: :caption: Getting started diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 02de284..3043e3f 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -94,7 +94,7 @@ v1.4.0 (October 23rd, 2021) --------------------------- Deprecations -~~~~~~~~~~~~ +~~~~~~~~~~~~ * The :py:class:`CalendarDateTime` class has been deprecated and will be removed in ``nc-time-axis`` version 1.5.0. Please switch to plotting instances or @@ -122,10 +122,10 @@ Bug Fixes :py:meth:`matplotlib.axes.Axes.fill_between` to work properly with `cftime`_ values (:issue:`47`, :issue:`74`, :pull:`78`). By `Pascal Bourgault`_. -* Fixed a bug that resulted in the resolution of tick labels being inconsistent +* Fixed a bug that resulted in the resolution of tick labels being inconsistent with the resolution of tick values (:issue:`48`, :pull:`79`). By `Spencer Clark`_. -* Fixed a bug that prevented users from being able to explicitly set the ticks +* Fixed a bug that prevented users from being able to explicitly set the ticks along axes using :py:meth:`matplotlib.axes.Axes.set_xticks` or :py:meth:`matplotlib.axes.Axes.set_yticks` (:issue:`41`, :pull:`84`). By `Spencer Clark`_. diff --git a/pyproject.toml b/pyproject.toml index 37debe7..8803d74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ +# See https://github.com/SciTools/.github/wiki/Linting for common linter rules + [build-system] # Defined by PEP 518 requires = [ "setuptools>=61", "setuptools_scm[toml]>=7", - "wheel", ] # Defined by PEP 517 build-backend = "setuptools.build_meta" @@ -48,6 +49,240 @@ Code = "https://github.com/SciTools/nc-time-axis" Discussions = "https://github.com/SciTools/nc-time-axis/discussions" Issues = "https://github.com/SciTools/nc-time-axis/issues" +[tool.check-manifest] +ignore = [ + "src/nc_time_axis/_version.py", +] + +[tool.codespell] +ignore-words-list = "assertIn" +skip = ".git,./docs/_build" + +[tool.mypy] +disable_error_code = [ + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "attr-defined", + "misc", + "no-untyped-call", + "no-untyped-def", + "unreachable", +] +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +strict = true +warn_unreachable = true + +[tool.numpydoc_validation] +checks = [ + "all", # Enable all numpydoc validation rules, apart from the following: + + # -> Docstring text (summary) should start in the line immediately + # after the opening quotes (not in the same line, or leaving a + # blank line in between) + "GL01", # Permit summary line on same line as docstring opening quotes. + + # -> Closing quotes should be placed in the line after the last text + # in the docstring (do not close the quotes in the same line as + # the text, or leave a blank line between the last text and the + # quotes) + "GL02", # Permit a blank line before docstring closing quotes. + + # -> Double line break found; please use only one blank line to + # separate sections or paragraphs, and do not leave blank lines + # at the end of docstrings + "GL03", # Ignoring. + + # -> See Also section not found + "SA01", # Not all docstrings require a "See Also" section. + + # -> No extended summary found + "ES01", # Not all docstrings require an "Extended Summary" section. + + # -> No examples section found + "EX01", # Not all docstrings require an "Examples" section. + + # -> No Yields section found + "YD01", # Not all docstrings require a "Yields" section. + + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "GL08", # No docstring + "SS05", # Summary must start with infinitive verb + "SS06", # Summary should fit on one line + "PR01", # Parameters not documented + "PR06", # Wrong type used + "PR08", # Description should start with capitol letter + "RT01", # No Returns section found +] +exclude = [ + '\.__eq__$', + '\.__ne__$', + '\.__repr__$', +] + +[tool.pytest.ini_options] +addopts = "-ra -v --doctest-modules --strict-config --strict-markers" +doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS NUMBER" +#filterwarnings = ["error"] # TODO - PP309; enable once all warnings are fixed +log_cli_level = "INFO" +minversion = "6.0" +testpaths = ["src/nc_time_axis"] +xfail_strict = true + +[tool.repo-review] +ignore = [ + # https://learn.scientific-python.org/development/guides/style/#PC180 + "PC180", # Uses prettier + + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "PY007", # Supports an easy task runner (nox or tox) + "PP309", # Filter warnings specified + "PC170", # Uses PyGrep hooks (only needed if rST present) +] + +[tool.ruff] +line-length = 88 + +[tool.ruff.format] +preview = false + +[tool.ruff.lint] +ignore = [ + # flake8-commas (COM) + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing. + "COM819", # Trailing comma prohibited. + + # flake8-implicit-str-concat (ISC) + # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ + # NOTE: This rule may cause conflicts when used with "ruff format". + "ISC001", # Implicitly concatenate string literals on one line. + + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "PLR5501", # collapsible-else-if; Preserve readability of TODO block in + # `convert` method + + # Flake8-builtins + "A001", # builtin-variable-shadowing + "A002", # undocumented-public-method + + # Flake8-annotations + "ANN001", # missing-type-function-argument + "ANN002", # missing-type-args + "ANN003", # missing type annotation for `**kwargs` + "ANN201", # missing-return-type-undocumented-public-function + "ANN202", # missing-return-type-private-function + "ANN204", # missing-return-type-special-method + "ANN205", # missing-return-type-static-method + "ANN206", # missing-return-type-class-method + + # Flake8-unused-arguments + "ARG001", # unused-function-argument + "ARG002", # Unused method argument + "ARG003", # unused-class-method-argument + "ARG004", # unused-static-method-argument + + # Flake8-bugbear + "B028", # no-explicit-stacklevel + + # Flake8-comprehensions + "C408", # unnecessary-collection-call + "C901", # complex-structure + + # pydocstyle + "D100", # undocumented-public-module + "D101", # undocumented-public-class + "D102", # undocumented-public-method + "D103", # undocumented-public-function + "D105", # undocumented-magic-method + "D200", # fits-on-one-line + "D205", # blank-line-after-summary + "D401", # non-imperative-mood + + # Flake8-datetimez + "DTZ002", # call-datetime-today + + # pycodestyle + "E501", # line-too-long + + # Flake8-errmsg + "EM101", # raw-string-in-exception + + # Eradicate + "ERA001", # commented-out-code + + # Flake8-fixme + "FIX002", # Line contains TODO, consider resolving the issue + + # flake8-import-conventions + "ICN001", # unconventional-import-alias + + # pep8-naming + "N801", # invalid-class-name + "N802", # invalid-function-name + + # numpy + "NPY002", # numpy-legacy-random + + # pylint + "PLR0912", # Too many branches + + # flake8-pytest-style + "PT009", # pytest-unittest-assertion + "PT027", # pytest-unittest-raises-assertion + + # flake8-return + "RET503", # implicit-return + + # flake8-bandit + "S101", # Assert used + + # flake8-simplify + "SIM102", # collapsible-if + "SIM108", # if-else-block-instead-of-if-exp + + # flake8-todos + "TD002", # Missing author in TODO; try + "TD003", # Missing issue link on the line following this TODO + "TD004", # Missing colon in TODO + + # tryceratops + "TRY003", # raise-vanilla-args + "TRY004", # type-check-without-type-error + + # pyupgrade + "UP008", # super-call-with-parameter +] + +preview = false +select = [ + "ALL", + # Note: the above "all" disables conflicting rules; if you want a specific + # rule that is skipped then it needs to be enabled explicitly below: + "D212", # Multi-line docstring summary should start at the first line +] + +[tool.ruff.lint.isort] +force-sort-within-sections = true +# Change to match specific package name: +known-first-party = ["nc_time_axis"] + +[tool.ruff.lint.per-file-ignores] +# All test scripts +"src/nc_time_axis/tests/*.py" = [ + "D104", # Missing docstring in public package + "N999", # Invalid module name +] + +"docs/conf.py" = [ + "INP001", # implicit namespace +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + [tool.setuptools] license-files = ["LICENSE"] zip-safe = false @@ -64,33 +299,6 @@ test = {file = "requirements/pypi-optional-test.txt"} include = ["nc_time_axis*"] where = ["src"] -[tool.black] -line-length = 79 -target-version = ['py39', 'py310', 'py311'] -include = '\.pyi?$' - -[tool.isort] -known_first_party = "nc_time_axis" -line_length = 79 -profile = "black" -skip_gitignore = "True" -verbose = "False" - -[tool.pytest.ini_options] -addopts = "-ra -v --doctest-modules" -doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS NUMBER" -minversion = "6.0" -testpaths = ["src/nc_time_axis"] - [tool.setuptools_scm] write_to = "src/nc_time_axis/_version.py" local_scheme = "dirty-tag" - -[tool.check-manifest] -ignore = [ - "src/nc_time_axis/_version.py", -] - -[tool.codespell] -ignore-words-list = "assertIn" -skip = ".git,./docs/_build" diff --git a/src/nc_time_axis/__init__.py b/src/nc_time_axis/__init__.py index 11a80a4..813be9b 100644 --- a/src/nc_time_axis/__init__.py +++ b/src/nc_time_axis/__init__.py @@ -1,7 +1,4 @@ -""" -Support for cftime axis in matplotlib. - -""" +"""Support for cftime axis in matplotlib.""" # nc-time-axis provides datetime locator and formatter objects which are # analogous to matplotlib's, but are compatible with cftime.datetime objects @@ -110,8 +107,8 @@ # Licensee agrees to be bound by the terms and conditions of this License # Agreement. -import warnings from numbers import Number +import warnings import cftime import matplotlib.dates as mdates @@ -128,8 +125,7 @@ class CalendarDateTime: - """ - Container for a :py:class:`cftime.datetime` object and calendar. + """Container for a :py:class:`cftime.datetime` object and calendar. Parameters ---------- @@ -185,8 +181,7 @@ def __repr__(self): class AutoCFTimeFormatter(mticker.Formatter): - """ - Automatic formatter for :py:class:`cftime.datetime` data. + """Automatic formatter for :py:class:`cftime.datetime` data. Automatically chooses a date format based on the resolution set by the :py:class:`NetCDFDateTimeLocator`. If no resolution is set, a default @@ -240,8 +235,7 @@ def __init__(self, *args, **kwargs): class CFTimeFormatter(mticker.Formatter): - """ - A formatter for explicitly setting the format of a + """A formatter for explicitly setting the format of a :py:class:`cftime.datetime` axis. Parameters @@ -266,8 +260,7 @@ def __call__(self, x, pos=0): class NetCDFTimeDateLocator(mticker.Locator): - """ - Determines tick locations when plotting :py:class:`cftime.datetime` data. + """Determines tick locations when plotting :py:class:`cftime.datetime` data. Parameters ---------- @@ -305,25 +298,20 @@ def __init__(self, max_n_ticks, calendar, date_unit=None, min_n_ticks=3): self.calendar = calendar if date_unit is not None: warnings.warn( - "The date_unit argument will be removed in " - "nc_time_axis version 1.5", + "The date_unit argument will be removed in " "nc_time_axis version 1.5", DeprecationWarning, ) self.date_unit = date_unit else: self.date_unit = _TIME_UNITS if not self.date_unit.lower().startswith("days since"): - emsg = ( - "The date unit must be days since for a NetCDF " - "time locator." - ) + emsg = "The date unit must be days since for a NetCDF " "time locator." raise ValueError(emsg) self.resolution = _DEFAULT_RESOLUTION self._cached_resolution = {} def compute_resolution(self, num1, num2, date1, date2): - """ - Returns the resolution of the dates (hourly, minutely, yearly), and + """Returns the resolution of the dates (hourly, minutely, yearly), and an **approximate** number of those units. """ @@ -353,9 +341,7 @@ def __call__(self): return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): - vmin, vmax = mtransforms.nonsingular( - vmin, vmax, expander=1e-7, tiny=1e-13 - ) + vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=1e-7, tiny=1e-13) lower = cftime.num2date(vmin, self.date_unit, calendar=self.calendar) upper = cftime.num2date(vmax, self.date_unit, calendar=self.calendar) @@ -412,17 +398,14 @@ def has_year_zero(year): ) hours = self._max_n_locator.tick_values(in_hours[0], in_hours[1]) ticks = [ - cftime.num2date(dt, hour_unit, calendar=self.calendar) - for dt in hours + cftime.num2date(dt, hour_unit, calendar=self.calendar) for dt in hours ] elif resolution == "MINUTELY": minute_unit = "minutes since 2000-01-01" in_minutes = cftime.date2num( [lower, upper], minute_unit, calendar=self.calendar ) - minutes = self._max_n_locator.tick_values( - in_minutes[0], in_minutes[1] - ) + minutes = self._max_n_locator.tick_values(in_minutes[0], in_minutes[1]) ticks = [ cftime.num2date(dt, minute_unit, calendar=self.calendar) for dt in minutes @@ -432,9 +415,7 @@ def has_year_zero(year): in_seconds = cftime.date2num( [lower, upper], second_unit, calendar=self.calendar ) - seconds = self._max_n_locator.tick_values( - in_seconds[0], in_seconds[1] - ) + seconds = self._max_n_locator.tick_values(in_seconds[0], in_seconds[1]) ticks = [ cftime.num2date(dt, second_unit, calendar=self.calendar) for dt in seconds @@ -455,17 +436,13 @@ def has_year_zero(year): class NetCDFTimeConverter(mdates.DateConverter): - """ - Converter for :py:class:`cftime.datetime` data. - - """ + """Converter for :py:class:`cftime.datetime` data.""" standard_unit = "days since 2000-01-01" @staticmethod def axisinfo(unit, axis): - """ - Returns the :class:`~matplotlib.units.AxisInfo` for *unit*. + """Returns the :class:`~matplotlib.units.AxisInfo` for *unit*. *unit* is a tzinfo instance or None. The *axis* argument is required but not used. @@ -475,12 +452,8 @@ def axisinfo(unit, axis): majloc = NetCDFTimeDateLocator(4, calendar=calendar) majfmt = AutoCFTimeFormatter(majloc, calendar=calendar) if date_type is CalendarDateTime: - datemin = CalendarDateTime( - cftime.datetime(2000, 1, 1), calendar=calendar - ) - datemax = CalendarDateTime( - cftime.datetime(2010, 1, 1), calendar=calendar - ) + datemin = CalendarDateTime(cftime.datetime(2000, 1, 1), calendar=calendar) + datemax = CalendarDateTime(cftime.datetime(2010, 1, 1), calendar=calendar) else: datemin = date_type(2000, 1, 1) datemax = date_type(2010, 1, 1) @@ -493,10 +466,7 @@ def axisinfo(unit, axis): @classmethod def default_units(cls, sample_point, axis): - """ - Computes some units for the given data point. - - """ + """Computes some units for the given data point.""" if hasattr(sample_point, "__iter__"): # Deal with n-D `sample_point` arrays. if isinstance(sample_point, np.ndarray): @@ -510,12 +480,9 @@ def default_units(cls, sample_point, axis): else: # Deal with a single `sample_point` value. if not hasattr(sample_point, "calendar"): - msg = ( - "Expecting cftimes with an extra " '"calendar" attribute.' - ) + msg = "Expecting cftimes with an extra " '"calendar" attribute.' raise ValueError(msg) - else: - calendar = sample_point.calendar + calendar = sample_point.calendar date_type = type(sample_point) if calendar == "": raise ValueError( @@ -525,8 +492,7 @@ def default_units(cls, sample_point, axis): @classmethod def convert(cls, value, unit, axis): - """ - Converts value, if it is not already a number or sequence of numbers, + """Converts value, if it is not already a number or sequence of numbers, with :py:func:`cftime.date2num`. """ @@ -546,7 +512,7 @@ def convert(cls, value, unit, axis): if is_numlike(value): return value # Not an array but a list of non-numerical types (thus assuming datetime types) - elif isinstance(value, (list, tuple)): + if isinstance(value, (list, tuple)): first_value = value[0] else: # Neither numerical, list or ndarray : must be a datetime scalar. @@ -573,9 +539,7 @@ def convert(cls, value, unit, axis): else: value = value.datetime - result = cftime.date2num( - value, _TIME_UNITS, calendar=first_value.calendar - ) + result = cftime.date2num(value, _TIME_UNITS, calendar=first_value.calendar) if shape is not None: result = result.reshape(shape) @@ -584,8 +548,7 @@ def convert(cls, value, unit, axis): def is_numlike(x): - """ - The Matplotlib datalim, autoscaling, locators etc work with scalars which + """The Matplotlib datalim, autoscaling, locators etc work with scalars which are the units converted to floats given the current unit. The converter may be passed these floats, or arrays of them, even when units are set. diff --git a/src/nc_time_axis/tests/integration/test_plot.py b/src/nc_time_axis/tests/integration/test_plot.py index 7698e91..7502148 100644 --- a/src/nc_time_axis/tests/integration/test_plot.py +++ b/src/nc_time_axis/tests/integration/test_plot.py @@ -29,8 +29,7 @@ def tearDown(self): def test_360_day_calendar_CalendarDateTime(self): calendar = "360_day" datetimes = [ - cftime.datetime(1986, month, 30, calendar=calendar) - for month in range(1, 6) + cftime.datetime(1986, month, 30, calendar=calendar) for month in range(1, 6) ] cal_datetimes = [ nc_time_axis.CalendarDateTime(dt, calendar) for dt in datetimes @@ -40,9 +39,7 @@ def test_360_day_calendar_CalendarDateTime(self): np.testing.assert_array_equal(result_ydata, cal_datetimes) def test_360_day_calendar_raw_dates(self): - datetimes = [ - cftime.Datetime360Day(1986, month, 30) for month in range(1, 6) - ] + datetimes = [cftime.Datetime360Day(1986, month, 30) for month in range(1, 6)] (line1,) = plt.plot(datetimes) result_ydata = line1.get_ydata() np.testing.assert_array_equal(result_ydata, datetimes) @@ -58,8 +55,7 @@ def test_360_day_calendar_raw_universal_dates(self): def test_no_calendar_raw_universal_dates(self): datetimes = [ - cftime.datetime(1986, month, 30, calendar=None) - for month in range(1, 6) + cftime.datetime(1986, month, 30, calendar=None) for month in range(1, 6) ] with self.assertRaisesRegex(ValueError, "defined"): plt.plot(datetimes) @@ -72,9 +68,7 @@ def test_fill_between(self): for day in range(1, 31) ] cdt = [nc_time_axis.CalendarDateTime(item, calendar) for item in dt] - temperatures = [ - np.round(np.random.uniform(0, 12), 3) for _ in range(len(cdt)) - ] + temperatures = [np.round(np.random.uniform(0, 12), 3) for _ in range(len(cdt))] plt.fill_between(cdt, temperatures, 0) @@ -92,9 +86,7 @@ def teardown_function(function): TICKS = { "List[cftime.datetime]": [cftime.Datetime360Day(1986, 2, 1)], "List[CalendarDateTime]": [ - nc_time_axis.CalendarDateTime( - cftime.Datetime360Day(1986, 2, 1), "360_day" - ) + nc_time_axis.CalendarDateTime(cftime.Datetime360Day(1986, 2, 1), "360_day") ], } diff --git a/src/nc_time_axis/tests/unit/test_AutoCFTimeFormatter.py b/src/nc_time_axis/tests/unit/test_AutoCFTimeFormatter.py index ab68258..7389f42 100644 --- a/src/nc_time_axis/tests/unit/test_AutoCFTimeFormatter.py +++ b/src/nc_time_axis/tests/unit/test_AutoCFTimeFormatter.py @@ -1,7 +1,7 @@ """Unit tests for the `nc_time_axis.AutoCFTimeFormatter` class.""" import unittest -import unittest.mock as mock +from unittest import mock import pytest diff --git a/src/nc_time_axis/tests/unit/test_CalendarDateTime.py b/src/nc_time_axis/tests/unit/test_CalendarDateTime.py index af2f66e..c0c41ba 100644 --- a/src/nc_time_axis/tests/unit/test_CalendarDateTime.py +++ b/src/nc_time_axis/tests/unit/test_CalendarDateTime.py @@ -11,23 +11,17 @@ @pytest.mark.filterwarnings("ignore::DeprecationWarning") class Test___eq__(unittest.TestCase): def setUp(self): - self.cdt = CalendarDateTime( - cftime.datetime(1967, 7, 22, 3, 6), "360_day" - ) + self.cdt = CalendarDateTime(cftime.datetime(1967, 7, 22, 3, 6), "360_day") def test_equal(self): self.assertTrue(self.cdt == self.cdt) def test_diff_cal(self): - other_cdt = CalendarDateTime( - cftime.datetime(1967, 7, 22, 3, 6), "365_day" - ) + other_cdt = CalendarDateTime(cftime.datetime(1967, 7, 22, 3, 6), "365_day") self.assertFalse(self.cdt == other_cdt) def test_diff_datetime(self): - other_cdt = CalendarDateTime( - cftime.datetime(1992, 11, 23, 3, 6), "360_day" - ) + other_cdt = CalendarDateTime(cftime.datetime(1992, 11, 23, 3, 6), "360_day") self.assertFalse(self.cdt == other_cdt) def test_diff_type(self): @@ -37,23 +31,17 @@ def test_diff_type(self): @pytest.mark.filterwarnings("ignore::DeprecationWarning") class Test__ne__(unittest.TestCase): def setUp(self): - self.cdt = CalendarDateTime( - cftime.datetime(1967, 7, 22, 3, 6), "360_day" - ) + self.cdt = CalendarDateTime(cftime.datetime(1967, 7, 22, 3, 6), "360_day") def test_equal(self): self.assertFalse(self.cdt != self.cdt) def test_diff_cal(self): - other_cdt = CalendarDateTime( - cftime.datetime(1967, 7, 22, 3, 6), "365_day" - ) + other_cdt = CalendarDateTime(cftime.datetime(1967, 7, 22, 3, 6), "365_day") self.assertTrue(self.cdt != other_cdt) def test_diff_datetime(self): - other_cdt = CalendarDateTime( - cftime.datetime(1992, 11, 23, 3, 6), "360_day" - ) + other_cdt = CalendarDateTime(cftime.datetime(1992, 11, 23, 3, 6), "360_day") self.assertTrue(self.cdt != other_cdt) def test_diff_type(self): diff --git a/src/nc_time_axis/tests/unit/test_NetCDFTimeConverter.py b/src/nc_time_axis/tests/unit/test_NetCDFTimeConverter.py index 6be1909..5a0310b 100644 --- a/src/nc_time_axis/tests/unit/test_NetCDFTimeConverter.py +++ b/src/nc_time_axis/tests/unit/test_NetCDFTimeConverter.py @@ -221,9 +221,7 @@ def test_cftime_np_array_raw_date(self): self.assertEqual(result, np.array([4473.0])) def test_cftime_np_array_raw_universal_date(self): - val = np.array( - [cftime.datetime(2012, 6, 4, calendar="360_day")], dtype=object - ) + val = np.array([cftime.datetime(2012, 6, 4, calendar="360_day")], dtype=object) result = NetCDFTimeConverter().convert(val, None, None) self.assertEqual(result, np.array([4473.0])) diff --git a/src/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py b/src/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py index 8de09bd..c7d9645 100644 --- a/src/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py +++ b/src/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py @@ -18,9 +18,7 @@ def setUp(self): self.calendar = "365_day" def check(self, max_n_ticks, num1, num2): - locator = NetCDFTimeDateLocator( - max_n_ticks=max_n_ticks, calendar=self.calendar - ) + locator = NetCDFTimeDateLocator(max_n_ticks=max_n_ticks, calendar=self.calendar) return locator.compute_resolution( num1, num2, @@ -29,12 +27,8 @@ def check(self, max_n_ticks, num1, num2): ) def test_one_minute(self): - self.assertEqual( - self.check(20, 0, 0.0003), ("SECONDLY", mdates.SEC_PER_DAY) - ) - self.assertEqual( - self.check(10, 0.0003, 0), ("SECONDLY", mdates.SEC_PER_DAY) - ) + self.assertEqual(self.check(20, 0, 0.0003), ("SECONDLY", mdates.SEC_PER_DAY)) + self.assertEqual(self.check(10, 0.0003, 0), ("SECONDLY", mdates.SEC_PER_DAY)) def test_one_hour(self): self.assertEqual(self.check(1, 0, 0.02), ("MINUTELY", 0)) @@ -45,9 +39,7 @@ def test_one_hour(self): def test_one_day(self): self.assertEqual(self.check(1, 0, 1), ("HOURLY", 0)) self.assertEqual(self.check(24, 0, 1), ("MINUTELY", 0)) - self.assertEqual( - self.check(86400, 0, 1), ("SECONDLY", mdates.SEC_PER_DAY) - ) + self.assertEqual(self.check(86400, 0, 1), ("SECONDLY", mdates.SEC_PER_DAY)) def test_30_days(self): self.assertEqual(self.check(1, 0, 30), ("DAILY", 30)) @@ -71,9 +63,7 @@ def test_10_years(self): self.assertEqual(self.check(10, 0, 10 * 365), ("MONTHLY", 121)) self.assertEqual(self.check(122, 0, 10 * 365), ("DAILY", 10 * 365)) self.assertEqual(self.check(10 * 365, 0, 10 * 365), ("HOURLY", 152)) - self.assertEqual( - self.check(10 * 365 * 24, 0, 10 * 365), ("MINUTELY", 2) - ) + self.assertEqual(self.check(10 * 365 * 24, 0, 10 * 365), ("MINUTELY", 2)) self.assertEqual( self.check(10 * 365 * 86400, 0, 10 * 365), ("SECONDLY", mdates.SEC_PER_DAY), @@ -102,9 +92,7 @@ def setUp(self): self.calendar = "365_day" def check(self, max_n_ticks, num1, num2): - locator = NetCDFTimeDateLocator( - max_n_ticks=max_n_ticks, calendar=self.calendar - ) + locator = NetCDFTimeDateLocator(max_n_ticks=max_n_ticks, calendar=self.calendar) return locator.tick_values(num1, num2) def test_secondly(self): @@ -160,9 +148,7 @@ def setUp(self): ] def check(self, max_n_ticks, num1, num2, calendar): - locator = NetCDFTimeDateLocator( - max_n_ticks=max_n_ticks, calendar=calendar - ) + locator = NetCDFTimeDateLocator(max_n_ticks=max_n_ticks, calendar=calendar) return locator.tick_values(num1, num2) def test_yearly_yr0_remove(self): @@ -170,8 +156,7 @@ def test_yearly_yr0_remove(self): # convert values to dates, check that none of them has year 0 ticks = self.check(5, -2001 * 365, -1901 * 365, calendar) year_ticks = [ - cftime.num2date(t, _TIME_UNITS, calendar=calendar).year - for t in ticks + cftime.num2date(t, _TIME_UNITS, calendar=calendar).year for t in ticks ] if calendar in self.yr0_remove_calendars: self.assertNotIn(0, year_ticks)