diff --git a/.github/workflows/cd-test-pypi.yml b/.github/workflows/cd-test-pypi.yml new file mode 100644 index 0000000..e784edf --- /dev/null +++ b/.github/workflows/cd-test-pypi.yml @@ -0,0 +1,10 @@ +name: test-cd +on: + pull_request: + branches: ["master"] +jobs: + pypi: + uses: ecmwf/reusable-workflows/.github/workflows/cd-pypi.yml@v2 + secrets: inherit + with: + testpypi: true diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f919087..5bc789d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -3,7 +3,7 @@ name: cd on: push: tags: - - '**' + - '**' jobs: pypi: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b4eb09..41365ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,16 +4,16 @@ on: # Trigger the workflow on push to master or develop, except tag creation push: branches: - - "master" - - "develop" + - "master" + - "develop" tags-ignore: - - "**" + - "**" # Trigger the workflow on pull request - pull_request: ~ + pull_request: # Trigger the workflow manually - workflow_dispatch: ~ + workflow_dispatch: # Trigger after public PR approved for CI pull_request_target: @@ -28,7 +28,6 @@ jobs: with: earthkit-time: ecmwf/earthkit-time@${{ github.event.pull_request.head.sha || github.sha }} codecov_upload: true - python_qa: true secrets: inherit # Build downstream packages on HPC diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..ede4c86 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,20 @@ +name: Quality assurance +on: + push: + branches: ["master", "develop"] + pull_request: + branches: ["master", "develop"] + types: [opened, synchronize, reopened] +jobs: + pre-commit: + uses: ecmwf/reusable-workflows/.github/workflows/qa-precommit-run.yml@v2 + with: + skip-hooks: "no-commit-to-branch" + tests: + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + uses: ecmwf/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2 + with: + python-version: ${{ matrix.python-version }} + optional-dependencies: "test" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8c5c1c7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +default_language_version: + python: python3 +default_stages: +- pre-commit +- pre-push +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + args: [--unsafe, --allow-multiple-documents] + - id: check-toml + - id: check-added-large-files + - id: check-merge-conflict + - id: debug-statements + - id: mixed-line-ending + - id: no-commit-to-branch + args: [--branch, master, --branch, develop] +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.4 + hooks: + - id: ruff-check + exclude: '(dev/.*|.*_)\.py$' + args: + - --line-length=120 + - --fix + - --exit-non-zero-on-fix + - --preview + - id: ruff-format +- repo: https://github.com/executablebooks/mdformat + rev: 1.0.0 + hooks: + - id: mdformat +- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.16.0 + hooks: + - id: pretty-format-yaml + args: [--autofix, --preserve-quotes] + - id: pretty-format-toml + args: [--autofix] +- repo: https://github.com/gitleaks/gitleaks + rev: v8.30.0 + hooks: + - id: gitleaks diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e43012d..860be78 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -31,4 +31,4 @@ python: - method: pip path: . extra_requirements: - - docs + - docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4114b58..77f84d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,44 +2,44 @@ ## Unreleased changes -* Add `earthkit.time.create_sequence` wrapper for easy sequence creation -* Fix misleading date/datetime parsing +- Add `earthkit.time.create_sequence` wrapper for easy sequence creation +- Fix misleading date/datetime parsing ## 0.1.8 - 2026-02-16 -* Add `earthkit-datetime` tool to compute datetime shifts and differences +- Add `earthkit-datetime` tool to compute datetime shifts and differences ## 0.1.7 - 2025-01-02 -* Export version as `earthkit.time.__version__` -* Add options for relative year range in climatology tools +- Export version as `earthkit.time.__version__` +- Add options for relative year range in climatology tools ## 0.1.6 - 2024-11-04 -* Update project metadata +- Update project metadata ## 0.1.5 - 2024-11-04 -* Fix `ImportError` with Python >= 3.13 +- Fix `ImportError` with Python >= 3.13 ## 0.1.4 - 2024-10-21 -* Add `--skip` options to `earthkit-dateseq previous/next` +- Add `--skip` options to `earthkit-dateseq previous/next` ## 0.1.3 - 2024-07-19 -* Add `Sequence.nearest` and `earthkit-dateseq nearest` +- Add `Sequence.nearest` and `earthkit-dateseq nearest` ## 0.1.2 - 2024-07-02 -* Change the default separator to a newline for command-line tools -* Add `--sep` options to the actions printing lists -* Add `earthkit-date` tool to compute date shifts and differences +- Change the default separator to a newline for command-line tools +- Add `--sep` options to the actions printing lists +- Add `earthkit-date` tool to compute date shifts and differences ## 0.1.1 - 2024-06-20 -* Compatibility with Python 3.8 +- Compatibility with Python 3.8 ## 0.1.0 - 2024-06-19 -* First published version +- First published version diff --git a/README.md b/README.md index a355f31..b3008ac 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Documentation

-> \[!IMPORTANT\] +> [!IMPORTANT] > This software is **Emerging** and subject to ECMWF's guidelines on [Software Maturity](https://github.com/ecmwf/codex/raw/refs/heads/main/Project%20Maturity). **earthkit-time** is a package containing date and time manipulation routines for the use of weather data. It is a component of [earthkit](https://github.com/ecmwf/earthkit). @@ -41,6 +41,7 @@ The documentation can be found at https://earthkit-time.readthedocs.io. ## Installation + Install from PyPI: ``` diff --git a/docs/api/sequence.rst b/docs/api/sequence.rst index 39fcc42..ed3d519 100644 --- a/docs/api/sequence.rst +++ b/docs/api/sequence.rst @@ -31,4 +31,3 @@ Built-in Sequences .. autoclass:: MonthlySequence .. autoclass:: YearlySequence - diff --git a/pyproject.toml b/pyproject.toml index 1db3df5..3600f93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,69 +1,89 @@ [build-system] -requires = ["setuptools>=65", "wheel"] build-backend = "setuptools.build_meta" +requires = ["setuptools>=65", "wheel"] [project] -dynamic = ["version"] -name = "earthkit-time" -requires-python = ">= 3.8" -description = "Date and time manipulation routines for the use of weather data" -license = {file = "LICENSE"} -readme = "README.md" classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Scientific/Engineering", + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering" ] dependencies = ["pyyaml"] +description = "Date and time manipulation routines for the use of weather data" +dynamic = ["version"] +license = {file = "LICENSE"} +name = "earthkit-time" +readme = "README.md" +requires-python = ">= 3.8" [project.optional-dependencies] docs = [ - "myst-parser", - "Sphinx", - "sphinx-rtd-theme", + "myst-parser", + "Sphinx", + "sphinx-rtd-theme" ] test = [ - "pytest >= 8.1", + "pytest >= 8.1" ] [project.scripts] earthkit-climdates = "earthkit.time.cli.climatology:main" -earthkit-dateseq = "earthkit.time.cli.sequence:main" earthkit-date = "earthkit.time.cli.date:main" +earthkit-dateseq = "earthkit.time.cli.sequence:main" earthkit-datetime = "earthkit.time.cli.datetime:main" [project.urls] -Homepage = "https://github.com/ecmwf/earthkit-time/" Documentation = "https://earthkit-time.readthedocs.io" - -[tool.isort] -profile = "black" +Homepage = "https://github.com/ecmwf/earthkit-time/" [tool.pytest.ini_options] -minversion = "8.1" addopts = "--doctest-modules" +minversion = "8.1" testpaths = [ - "src", - "tests", + "tests" +] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +ignore = [ + "D1", # pydocstyle: Missing Docstrings + "D203", + "D205", + "D212", + "D213", + "D401", + "D402", + "D413", + "D415", + "D416", + "D417" +] +select = [ + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle warnings + "I", # isort + "D" # pydocstyle ] -consider_namespace_packages = true [tool.setuptools.dynamic] version = {attr = "earthkit.time.__version__"} -[tool.setuptools.packages.find] -where = ["src"] - [tool.setuptools.package-data] "earthkit.time.data" = ["*/*.yaml"] + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/earthkit/time/calendar.py b/src/earthkit/time/calendar.py index 76784d3..0c89452 100644 --- a/src/earthkit/time/calendar.py +++ b/src/earthkit/time/calendar.py @@ -5,7 +5,7 @@ class Weekday(IntEnum): - """:class:`enum.IntEnum` representing week days""" + """:class:`enum.IntEnum` representing week days.""" MONDAY = 0 TUESDAY = 1 @@ -26,7 +26,7 @@ class Weekday(IntEnum): def to_weekday(arg: Union[int, str]) -> Weekday: - """Convert integers and strings to weekdays + """Convert integers and strings to weekdays. Any unambiguous prefix of a weekday name will be accepted, and case is ignored. @@ -65,7 +65,7 @@ def to_weekday(arg: Union[int, str]) -> Weekday: def month_length(year: int, month: int) -> int: - """Return the number of days of a given month""" + """Return the number of days of a given month.""" if month < 1 or month > 12: raise ValueError(f"Invalid month: {month}") mlen = _MONTH_LENGTHS[month] @@ -75,7 +75,7 @@ def month_length(year: int, month: int) -> int: def day_exists(year: int, month: int, day: int) -> bool: - """Check whether a given day exists in the calendar""" + """Check whether a given day exists in the calendar.""" if month < 1 or month > 12: return False if day < 1 or day > month_length(year, month): @@ -84,7 +84,7 @@ def day_exists(year: int, month: int, day: int) -> bool: class MonthInYear: - """Represent a given month in a year""" + """Represent a given month in a year.""" year: int month: int @@ -107,24 +107,24 @@ def __contains__(self, day: Union[int, date]) -> bool: return True def length(self) -> int: - """Returns the number of days in the given month""" + """Returns the number of days in the given month.""" return month_length(self.year, self.month) def next(self) -> "MonthInYear": - """Return the following month""" + """Return the following month.""" d, m = divmod(self.month, 12) m += 1 return MonthInYear(self.year + d, m) def previous(self) -> "MonthInYear": - """Return the previous month""" + """Return the previous month.""" d, m = divmod(self.month - 2, 12) m += 1 return MonthInYear(self.year + d, m) def parse_mmdd(arg: Union[Tuple[int, int], str]) -> Tuple[int, int]: - """Convert pairs of ints or MMDD strings into (month, day) pairs""" + """Convert pairs of ints or MMDD strings into (month, day) pairs.""" if not isinstance(arg, str): m, d = arg if not day_exists(2000, m, d): @@ -146,7 +146,7 @@ def parse_mmdd(arg: Union[Tuple[int, int], str]) -> Tuple[int, int]: def parse_date(arg: Union[str, Tuple[int, int, int]]) -> date: - """Convert triples of ints or YYYYMMDD strings into date objects""" + """Convert triples of ints or YYYYMMDD strings into date objects.""" if not isinstance(arg, str): y, m, d = arg if not day_exists(y, m, d): @@ -169,7 +169,7 @@ def parse_date(arg: Union[str, Tuple[int, int, int]]) -> date: def parse_datetime(arg: Union[str, Tuple[int, int, int, int]]) -> datetime: - """Convert quadruplets of ints or YYYYMMDDHH strings into datetime objects""" + """Convert quadruplets of ints or YYYYMMDDHH strings into datetime objects.""" if not isinstance(arg, str): y, m, d, h = arg if not day_exists(y, m, d) or not 0 <= h <= 23: diff --git a/src/earthkit/time/cli/cliargs.py b/src/earthkit/time/cli/cliargs.py index 0c1a8c5..cb4b767 100644 --- a/src/earthkit/time/cli/cliargs.py +++ b/src/earthkit/time/cli/cliargs.py @@ -2,9 +2,9 @@ import re from typing import List, Tuple -from ..calendar import Weekday, parse_date, parse_mmdd, to_weekday -from ..climatology import RelativeYear -from ..sequence import ( +from earthkit.time.calendar import Weekday, parse_date, parse_mmdd, to_weekday +from earthkit.time.climatology import RelativeYear +from earthkit.time.sequence import ( DailySequence, MonthlySequence, Sequence, @@ -88,18 +88,14 @@ def add_sequence_args(parser: argparse.ArgumentParser): ) -def create_sequence( - parser: argparse.ArgumentParser, args: argparse.Namespace -) -> Sequence: +def create_sequence(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Sequence: seq = None try: if args.daily: try: excludes = [int(elem) for elem in args.exclude] except ValueError as e: - raise ValueError( - "Invalid excludes, must be a slash-separated list of days" - ) from e + raise ValueError("Invalid excludes, must be a slash-separated list of days") from e seq = DailySequence(excludes=excludes) elif args.weekly is not None: seq = WeeklySequence(args.weekly) @@ -138,9 +134,7 @@ def _eval_escape(m: re.Match) -> str: def escaped_str(arg: str) -> str: - return re.sub( - r"\\([\\0abfnrtv]|x[0-9a-f]{2}|[0-3][0-7]{2})", _eval_escape, arg, flags=re.I - ) + return re.sub(r"\\([\\0abfnrtv]|x[0-9a-f]{2}|[0-3][0-7]{2})", _eval_escape, arg, flags=re.I) SEP_EPILOG = """ diff --git a/src/earthkit/time/cli/climatology.py b/src/earthkit/time/cli/climatology.py index 11d60fb..1b3d51b 100644 --- a/src/earthkit/time/cli/climatology.py +++ b/src/earthkit/time/cli/climatology.py @@ -2,10 +2,9 @@ import textwrap from typing import List, Optional -from ..calendar import parse_date -from ..climatology import date_range, model_climate_dates -from .actions import ActionParser -from .cliargs import ( +from earthkit.time.calendar import parse_date +from earthkit.time.cli.actions import ActionParser +from earthkit.time.cli.cliargs import ( SEP_EPILOG, SEQ_EPILOG, add_sep_arg, @@ -13,7 +12,8 @@ create_sequence, relative_year, ) -from .cliout import format_date_list +from earthkit.time.cli.cliout import format_date_list +from earthkit.time.climatology import date_range, model_climate_dates def date_range_action(parser: argparse.ArgumentParser, args: argparse.Namespace): @@ -33,9 +33,7 @@ def model_climate_action(parser: argparse.ArgumentParser, args: argparse.Namespa def get_parser() -> argparse.ArgumentParser: - parser = ActionParser( - description="Tools to compute model climate dates", fromfile_prefix_chars="@" - ) + parser = ActionParser(description="Tools to compute model climate dates", fromfile_prefix_chars="@") range_action = parser.add_action( "range", @@ -48,12 +46,8 @@ def get_parser() -> argparse.ArgumentParser: range_action.add_argument("date", type=parse_date, help="reference date") range_start_group = range_action.add_mutually_exclusive_group(required=True) - range_start_group.add_argument( - "--from-date", type=parse_date, dest="start", help="starting date" - ) - range_start_group.add_argument( - "--from-year", type=int, dest="start", help="starting year" - ) + range_start_group.add_argument("--from-date", type=parse_date, dest="start", help="starting date") + range_start_group.add_argument("--from-year", type=int, dest="start", help="starting year") range_start_group.add_argument( "--from-rel-year", type=relative_year, @@ -62,9 +56,7 @@ def get_parser() -> argparse.ArgumentParser: ) range_end_group = range_action.add_mutually_exclusive_group(required=True) - range_end_group.add_argument( - "--to-date", type=parse_date, dest="end", help="ending date" - ) + range_end_group.add_argument("--to-date", type=parse_date, dest="end", help="ending date") range_end_group.add_argument("--to-year", type=int, dest="end", help="ending year") range_end_group.add_argument( "--to-rel-year", @@ -91,12 +83,8 @@ def get_parser() -> argparse.ArgumentParser: mclim_action.add_argument("date", type=parse_date, help="reference date") mclim_start_group = mclim_action.add_mutually_exclusive_group(required=True) - mclim_start_group.add_argument( - "--from-date", type=parse_date, dest="start", help="starting date" - ) - mclim_start_group.add_argument( - "--from-year", type=int, dest="start", help="starting year" - ) + mclim_start_group.add_argument("--from-date", type=parse_date, dest="start", help="starting date") + mclim_start_group.add_argument("--from-year", type=int, dest="start", help="starting year") mclim_start_group.add_argument( "--from-rel-year", type=relative_year, @@ -105,9 +93,7 @@ def get_parser() -> argparse.ArgumentParser: ) mclim_end_group = mclim_action.add_mutually_exclusive_group(required=True) - mclim_end_group.add_argument( - "--to-date", type=parse_date, dest="end", help="ending date" - ) + mclim_end_group.add_argument("--to-date", type=parse_date, dest="end", help="ending date") mclim_end_group.add_argument("--to-year", type=int, dest="end", help="ending year") mclim_end_group.add_argument( "--to-rel-year", diff --git a/src/earthkit/time/cli/date.py b/src/earthkit/time/cli/date.py index 86e6507..885f215 100644 --- a/src/earthkit/time/cli/date.py +++ b/src/earthkit/time/cli/date.py @@ -2,9 +2,9 @@ from datetime import timedelta from typing import List, Optional -from ..calendar import parse_date -from .actions import ActionParser -from .cliout import format_date +from earthkit.time.calendar import parse_date +from earthkit.time.cli.actions import ActionParser +from earthkit.time.cli.cliout import format_date def date_shift_action(parser: argparse.ArgumentParser, args: argparse.Namespace): diff --git a/src/earthkit/time/cli/datetime.py b/src/earthkit/time/cli/datetime.py index 61a144e..a0dd944 100644 --- a/src/earthkit/time/cli/datetime.py +++ b/src/earthkit/time/cli/datetime.py @@ -2,9 +2,9 @@ from datetime import timedelta from typing import List, Optional -from ..calendar import parse_datetime -from .actions import ActionParser -from .cliout import format_datetime +from earthkit.time.calendar import parse_datetime +from earthkit.time.cli.actions import ActionParser +from earthkit.time.cli.cliout import format_datetime def datetime_shift_action(parser: argparse.ArgumentParser, args: argparse.Namespace): @@ -26,12 +26,8 @@ def get_parser() -> argparse.ArgumentParser: help="shift a datetime", description="Shift a datetime by the given number of hours", ) - shift_action.add_argument( - "datetime", type=parse_datetime, help="reference datetime" - ) - shift_action.add_argument( - "hours", type=int, help="number of hours (can be negative)" - ) + shift_action.add_argument("datetime", type=parse_datetime, help="reference datetime") + shift_action.add_argument("hours", type=int, help="number of hours (can be negative)") diff_action = parser.add_action( "diff", @@ -39,12 +35,8 @@ def get_parser() -> argparse.ArgumentParser: help="subtract two datetimes", description="Subtract DATETIME2 from DATETIME1, returning the number of hours", ) - diff_action.add_argument( - "datetime1", type=parse_datetime, help="first datetime (+)" - ) - diff_action.add_argument( - "datetime2", type=parse_datetime, help="second datetime (-)" - ) + diff_action.add_argument("datetime1", type=parse_datetime, help="first datetime (+)") + diff_action.add_argument("datetime2", type=parse_datetime, help="second datetime (-)") return parser diff --git a/src/earthkit/time/cli/sequence.py b/src/earthkit/time/cli/sequence.py index 950eadc..ec24c84 100644 --- a/src/earthkit/time/cli/sequence.py +++ b/src/earthkit/time/cli/sequence.py @@ -1,16 +1,16 @@ import argparse from typing import List, Optional -from ..calendar import parse_date -from .actions import ActionParser -from .cliargs import ( +from earthkit.time.calendar import parse_date +from earthkit.time.cli.actions import ActionParser +from earthkit.time.cli.cliargs import ( SEP_EPILOG, SEQ_EPILOG, add_sep_arg, add_sequence_args, create_sequence, ) -from .cliout import format_date, format_date_list +from earthkit.time.cli.cliout import format_date, format_date_list def seq_next_action(parser: argparse.ArgumentParser, args: argparse.Namespace): @@ -54,17 +54,11 @@ def seq_bracket_action(parser: argparse.ArgumentParser, args: argparse.Namespace if args.after is not None: num = (args.before, args.after) seq = create_sequence(parser, args) - print( - format_date_list( - seq.bracket(args.date, num, strict=(not args.inclusive)), sep=args.sep - ) - ) + print(format_date_list(seq.bracket(args.date, num, strict=(not args.inclusive)), sep=args.sep)) def get_parser() -> argparse.ArgumentParser: - parser = ActionParser( - description="Manipulate sequences of dates", fromfile_prefix_chars="@" - ) + parser = ActionParser(description="Manipulate sequences of dates", fromfile_prefix_chars="@") next_action = parser.add_action( "next", @@ -139,12 +133,8 @@ def get_parser() -> argparse.ArgumentParser: add_sep_arg(range_action) range_action.add_argument("from", type=parse_date, help="starting date") range_action.add_argument("to", type=parse_date, help="ending date") - range_action.add_argument( - "--exclude-start", action="store_true", help="exclude starting date" - ) - range_action.add_argument( - "--exclude-end", action="store_true", help="exclude ending date" - ) + range_action.add_argument("--exclude-start", action="store_true", help="exclude starting date") + range_action.add_argument("--exclude-end", action="store_true", help="exclude ending date") bracket_action = parser.add_action( "bracket", diff --git a/src/earthkit/time/climatology.py b/src/earthkit/time/climatology.py index b26e38c..b1be535 100644 --- a/src/earthkit/time/climatology.py +++ b/src/earthkit/time/climatology.py @@ -1,16 +1,16 @@ -"""Date utilities to build a climatology""" +"""Date utilities to build a climatology.""" from dataclasses import dataclass from datetime import date, timedelta from typing import Iterator, Union -from .sequence import Sequence, YearlySequence -from .utilities import merge_sorted +from earthkit.time.sequence import Sequence, YearlySequence +from earthkit.time.utilities import merge_sorted @dataclass class RelativeYear: - """Wrapper for a year intended to be relative to a reference""" + """Wrapper for a year intended to be relative to a reference.""" value: int @@ -27,7 +27,7 @@ def date_range( recurrence: str = "yearly", include_endpoint: bool = True, ) -> Iterator[date]: - """Generate a sequence of dates following a recurrence pattern + """Generate a sequence of dates following a recurrence pattern. If the reference date is February 29th on a leap year, it will be replaced by February 28th for every year in the output. @@ -64,7 +64,6 @@ def date_range( >>> list(date_range(date(2014, 8, 23), RelativeYear(-3), RelativeYear(-1))) [datetime.date(2011, 8, 23), datetime.date(2012, 8, 23), datetime.date(2013, 8, 23)] """ - _known_recurrences = ["yearly"] if recurrence not in _known_recurrences: known = ", ".join(_known_recurrences) @@ -96,7 +95,7 @@ def model_climate_dates( after: Union[timedelta, int], sequence: Sequence, ) -> Iterator[date]: - """Generate a set of dates for a model climate + """Generate a set of dates for a model climate. The set is created by combining yearly dates between ``start`` and ``end``, for each date between ``reference - before`` and ``reference + after``. If @@ -152,7 +151,4 @@ def model_climate_dates( before = timedelta(days=before) if not isinstance(after, timedelta): after = timedelta(days=after) - yield from merge_sorted( - date_range(d, start, end) - for d in sequence.range(reference - before, reference + after) - ) + yield from merge_sorted(date_range(d, start, end) for d in sequence.range(reference - before, reference + after)) diff --git a/src/earthkit/time/data/__init__.py b/src/earthkit/time/data/__init__.py index e385123..6db140a 100644 --- a/src/earthkit/time/data/__init__.py +++ b/src/earthkit/time/data/__init__.py @@ -35,11 +35,7 @@ def _open_text( errors: str = "strict", ) -> TextIO: if hasattr(resources, "files"): # Python >= 3.9 - return ( - resources.files(package) - .joinpath(resource) - .open("r", encoding=encoding, errors=errors) - ) + return resources.files(package).joinpath(resource).open("r", encoding=encoding, errors=errors) else: return resources.open_text(package, resource, encoding, errors) @@ -56,7 +52,7 @@ def find_resource( env_file: Optional[str] = None, env_path: Optional[str] = None, ) -> Tuple[ResourceType, str]: - """Find a given resource file using various methods + """Find a given resource file using various methods. The search order is: ``path``, ``env_file``, `env_path``, packaged date. @@ -78,7 +74,6 @@ def find_resource( str Path to the resource, if found """ - if path is not None: if pathlib.Path(path).is_file(): return ResourceType.FILE, path @@ -107,7 +102,7 @@ def load_yaml( env_file: Optional[str] = None, env_path: Optional[str] = None, ) -> object: - """Load a YAML resource + """Load a YAML resource. See `find_resource` for the signification of the parameters diff --git a/src/earthkit/time/data/sequences/ecmwf-2days.yaml b/src/earthkit/time/data/sequences/ecmwf-2days.yaml index 3acc29a..200298d 100644 --- a/src/earthkit/time/data/sequences/ecmwf-2days.yaml +++ b/src/earthkit/time/data/sequences/ecmwf-2days.yaml @@ -17,4 +17,4 @@ days: - 29 - 31 excludes: -- "0229" \ No newline at end of file +- "0229" diff --git a/src/earthkit/time/data/sequences/ecmwf-4days.yaml b/src/earthkit/time/data/sequences/ecmwf-4days.yaml index 00d7a24..b539b17 100644 --- a/src/earthkit/time/data/sequences/ecmwf-4days.yaml +++ b/src/earthkit/time/data/sequences/ecmwf-4days.yaml @@ -9,4 +9,4 @@ days: - 25 - 29 excludes: -- "0229" \ No newline at end of file +- "0229" diff --git a/src/earthkit/time/sequence.py b/src/earthkit/time/sequence.py index 0220919..c8e3ca3 100644 --- a/src/earthkit/time/sequence.py +++ b/src/earthkit/time/sequence.py @@ -3,7 +3,7 @@ from datetime import date, timedelta from typing import Container, Dict, Iterable, Iterator, Optional, Tuple, Type, Union -from .calendar import ( +from earthkit.time.calendar import ( MonthInYear, Weekday, day_exists, @@ -11,11 +11,12 @@ parse_mmdd, to_weekday, ) + from .data import load_yaml class Sequence(ABC): - """Abstract representation of a sequence of dates + """Abstract representation of a sequence of dates. Minimal implementation requirement: ``__contains__``. Implementing ``next`` and ``previous`` is highly recommended for efficiency. @@ -91,10 +92,8 @@ def range( yield current current = self.next(current) - def bracket( - self, reference: date, num: Union[int, Tuple[int, int]] = 1, strict: bool = True - ) -> Iterator[date]: - """Return matching dates around ``reference`` + def bracket(self, reference: date, num: Union[int, Tuple[int, int]] = 1, strict: bool = True) -> Iterator[date]: + """Return matching dates around ``reference``. Parameters ---------- @@ -131,7 +130,7 @@ def bracket( @classmethod @abstractmethod def _from_dict(cls, seq_dict: dict) -> "Sequence": - """Create a specific sequence from the given dictionary + """Create a specific sequence from the given dictionary. Dictionary contents can vary depending on the sequence. Frequent items are: * ``days``: list of recurring days @@ -154,7 +153,7 @@ def __init_subclass__(cls, /, seqname: Optional[str] = None, **kwargs): @classmethod def from_dict(cls, seq_dict: dict) -> "Sequence": - """Create a sequence from the given dictionary + """Create a sequence from the given dictionary. The type of sequence is specified by the ``type`` key, and must match one of the known sequences, e.g. ``daily``, ``weekly``, ``monthly``, @@ -176,7 +175,7 @@ def from_dict(cls, seq_dict: dict) -> "Sequence": @classmethod def from_resource(cls, name: str) -> "Sequence": - """Load a sequence from a resource file + """Load a sequence from a resource file. ``name`` should be either the name of a known sequence (in ``earthkit.time.data.sequences`` or ``EARTHKIT_TIME_SEQ_PATH``, @@ -185,16 +184,14 @@ def from_resource(cls, name: str) -> "Sequence": Raises :class:`FileNotFoundError` if no corresponding resource is found """ path = name if os.path.isfile(name) else None - seq_dict = load_yaml( - f"sequences/{name}.yaml", path, env_path="EARTHKIT_TIME_SEQ_PATH" - ) + seq_dict = load_yaml(f"sequences/{name}.yaml", path, env_path="EARTHKIT_TIME_SEQ_PATH") if not isinstance(seq_dict, dict): raise ValueError("Invalid resource file") return cls.from_dict(seq_dict) class DailySequence(Sequence, seqname="daily"): - """Sequence of consecutive dates + """Sequence of consecutive dates. Any day number (in the month) present in ``excludes`` will be skipped @@ -219,7 +216,7 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: class WeeklySequence(Sequence, seqname="weekly"): - """Sequence of dates happening on given days of each week + """Sequence of dates happening on given days of each week. Can be created from a :class:`dict` with items: @@ -282,7 +279,7 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: class MonthlySequence(Sequence, seqname="monthly"): - """Sequence of dates happening on given days of each month + """Sequence of dates happening on given days of each month. Any ``(month, day)`` tuple present in ``excludes`` will be skipped @@ -310,10 +307,7 @@ def __init__( self.excludes = excludes def __contains__(self, reference: date) -> bool: - return ( - reference.day in self.days - and (reference.month, reference.day) not in self.excludes - ) + return reference.day in self.days and (reference.month, reference.day) not in self.excludes def __repr__(self) -> str: return f"MonthlySequence(days={self.days!r}, excludes={self.excludes!r})" @@ -326,9 +320,7 @@ def next(self, reference: date, strict: bool = True) -> date: ( day for day in self.days - if day > reference.day - and day in ymonth - and (ymonth.month, day) not in self.excludes + if day > reference.day and day in ymonth and (ymonth.month, day) not in self.excludes ), None, ) @@ -348,9 +340,7 @@ def previous(self, reference: date, strict: bool = True) -> date: ( day for day in self.days[::-1] - if day < reference.day - and day in ymonth - and (ymonth.month, day) not in self.excludes + if day < reference.day and day in ymonth and (ymonth.month, day) not in self.excludes ), None, ) @@ -371,7 +361,7 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: class YearlySequence(Sequence, seqname="yearly"): - """Sequence of dates happening on given days of each year (in (month, day) format) + """Sequence of dates happening on given days of each year (in (month, day) format). Can be created from a :class:`dict` with items: @@ -387,11 +377,7 @@ def __init__( days: Union[Tuple[int, int], Iterable[Tuple[int, int]]], excludes: Container[date] = set(), ): - if ( - isinstance(days, tuple) - and len(days) == 2 - and all(isinstance(day, int) for day in days) - ): + if isinstance(days, tuple) and len(days) == 2 and all(isinstance(day, int) for day in days): self.days = [days] else: self.days = sorted(days) @@ -424,10 +410,7 @@ def next(self, reference: date, strict: bool = True) -> date: while new_day is None: year += 1 for month, day in self.days: - if ( - day_exists(year, month, day) - and date(year, month, day) not in self.excludes - ): + if day_exists(year, month, day) and date(year, month, day) not in self.excludes: new_month = month new_day = day break @@ -451,10 +434,7 @@ def previous(self, reference: date, strict: bool = True) -> date: while new_day is None: year -= 1 for month, day in self.days[::-1]: - if ( - day_exists(year, month, day) - and date(year, month, day) not in self.excludes - ): + if day_exists(year, month, day) and date(year, month, day) not in self.excludes: new_month = month new_day = day break @@ -474,7 +454,7 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: def create_sequence(type_: str, *args, **kwargs) -> Sequence: - """Create a sequence + """Create a sequence. This is a wrapper around the following constructors and factory methods. Any extra arguments (positional or keyword) are passed to the corresponding diff --git a/tests/cli/test_climatology_cli.py b/tests/cli/test_climatology_cli.py index 00e4c44..c230831 100644 --- a/tests/cli/test_climatology_cli.py +++ b/tests/cli/test_climatology_cli.py @@ -74,11 +74,7 @@ def test_date_range_action( "after": 7, "weekly": [Weekday.MONDAY, Weekday.THURSDAY], }, - "\n".join( - f"{y}{m:02d}{d:02d}" - for y in range(2020, 2024) - for m, d in [(5, 16), (5, 20), (5, 23), (5, 27)] - ), + "\n".join(f"{y}{m:02d}{d:02d}" for y in range(2020, 2024) for m, d in [(5, 16), (5, 20), (5, 23), (5, 27)]), id="weekly", ), pytest.param( @@ -91,9 +87,7 @@ def test_date_range_action( "monthly": [1, 5, 9, 13, 17, 21, 25, 29], }, "\n".join( - f"{y}{m:02d}{d:02d}" - for y in range(2016, 2018) - for m, d in [(6, 25), (6, 29), (7, 1), (7, 5), (7, 9)] + f"{y}{m:02d}{d:02d}" for y in range(2016, 2018) for m, d in [(6, 25), (6, 29), (7, 1), (7, 5), (7, 9)] ), id="monthly", ), @@ -107,11 +101,7 @@ def test_date_range_action( "monthly": [1, 5, 9, 13, 17, 21, 25, 29], "exclude": ["0229"], }, - "\n".join( - f"{y}{m:02d}{d:02d}" - for y in range(2012, 2015) - for m, d in [(2, 25), (3, 1), (3, 5)] - ), + "\n".join(f"{y}{m:02d}{d:02d}" for y in range(2012, 2015) for m, d in [(2, 25), (3, 1), (3, 5)]), id="monthly-exclude", ), pytest.param( @@ -155,9 +145,7 @@ def test_date_range_action( ), ], ) -def test_model_climate_action( - args: dict, expected: str, capsys: pytest.CaptureFixture[str] -): +def test_model_climate_action(args: dict, expected: str, capsys: pytest.CaptureFixture[str]): parser = argparse.ArgumentParser() args.setdefault("daily", False) args.setdefault("weekly", None) diff --git a/tests/cli/test_cliout.py b/tests/cli/test_cliout.py index c1827c2..35cdd90 100644 --- a/tests/cli/test_cliout.py +++ b/tests/cli/test_cliout.py @@ -6,9 +6,7 @@ from earthkit.time.cli.cliout import format_date, format_date_list -@pytest.mark.parametrize( - "ymd, expected", [((1999, 11, 22), "19991122"), ((2000, 1, 3), "20000103")] -) +@pytest.mark.parametrize("ymd, expected", [((1999, 11, 22), "19991122"), ((2000, 1, 3), "20000103")]) def test_format_date(ymd: Tuple[int, int, int], expected: str): assert format_date(date(*ymd)) == expected @@ -32,9 +30,7 @@ def test_format_date(ymd: Tuple[int, int, int], expected: str): ([(1999, 5, 8), (2003, 2, 4)], ", ", "19990508, 20030204"), ], ) -def test_format_date_list( - dates: List[Tuple[int, int, int]], sep: Optional[str], expected: str -): +def test_format_date_list(dates: List[Tuple[int, int, int]], sep: Optional[str], expected: str): if sep is None: assert format_date_list([date(*ymd) for ymd in dates]) == expected else: diff --git a/tests/cli/test_sequence_cli.py b/tests/cli/test_sequence_cli.py index 243d07a..1ed71b4 100644 --- a/tests/cli/test_sequence_cli.py +++ b/tests/cli/test_sequence_cli.py @@ -16,9 +16,7 @@ @pytest.mark.parametrize( "args, expected", [ - pytest.param( - {"daily": True, "date": date(1999, 12, 31)}, "20000101", id="daily" - ), + pytest.param({"daily": True, "date": date(1999, 12, 31)}, "20000101", id="daily"), pytest.param( {"daily": True, "date": date(1999, 12, 31), "inclusive": True}, "19991231", @@ -150,9 +148,7 @@ def test_seq_prev(args: dict, expected: str, capsys: pytest.CaptureFixture[str]) @pytest.mark.parametrize( "args, expected", [ - pytest.param( - {"daily": True, "date": date(2006, 7, 26)}, "20060726", id="daily" - ), + pytest.param({"daily": True, "date": date(2006, 7, 26)}, "20060726", id="daily"), pytest.param( {"daily": True, "date": date(2017, 3, 30), "exclude": ["30", "31"]}, "20170329", @@ -212,10 +208,7 @@ def test_seq_nearest(args: dict, expected: str, capsys: pytest.CaptureFixture[st "to": date(2010, 10, 17), "exclude_start": True, }, - "\n".join( - f"2010{m:02d}{d:02d}" - for m, d in [(9, 12), (9, 19), (9, 26), (10, 3), (10, 10), (10, 17)] - ), + "\n".join(f"2010{m:02d}{d:02d}" for m, d in [(9, 12), (9, 19), (9, 26), (10, 3), (10, 10), (10, 17)]), id="weekly-nostart", ), pytest.param( @@ -247,9 +240,7 @@ def test_seq_nearest(args: dict, expected: str, capsys: pytest.CaptureFixture[st "exclude_start": True, "exclude_end": True, }, - "\n".join( - f"{y:04d}{m:02d}{d:02d}" for y, m, d in [(2014, 1, 15), (2014, 7, 20)] - ), + "\n".join(f"{y:04d}{m:02d}{d:02d}" for y, m, d in [(2014, 1, 15), (2014, 7, 20)]), id="yearly-nostart-noend", ), pytest.param( @@ -292,18 +283,14 @@ def test_seq_range(args: dict, expected: str, capsys: pytest.CaptureFixture[str] @pytest.mark.parametrize( "args, expected", [ - pytest.param( - {"daily": True, "date": date(2015, 3, 26)}, "20150325\n20150327", id="daily" - ), + pytest.param({"daily": True, "date": date(2015, 3, 26)}, "20150325\n20150327", id="daily"), pytest.param( { "weekly": [Weekday.WEDNESDAY, Weekday.SATURDAY], "date": date(2016, 10, 4), "before": 2, }, - "\n".join( - f"2016{m:02d}{d:02d}" for m, d in [(9, 28), (10, 1), (10, 5), (10, 8)] - ), + "\n".join(f"2016{m:02d}{d:02d}" for m, d in [(9, 28), (10, 1), (10, 5), (10, 8)]), id="weekly-2", ), pytest.param( @@ -319,10 +306,7 @@ def test_seq_range(args: dict, expected: str, capsys: pytest.CaptureFixture[str] "after": 2, "inclusive": True, }, - "\n".join( - f"{y:04d}{m:02d}{d:02d}" - for y, m, d in [(2019, 2, 2), (2019, 3, 3), (2019, 4, 4), (2020, 1, 1)] - ), + "\n".join(f"{y:04d}{m:02d}{d:02d}" for y, m, d in [(2019, 2, 2), (2019, 3, 3), (2019, 4, 4), (2020, 1, 1)]), id="yearly-1-2-inc", ), pytest.param( diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 74e3e68..0e2c109 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -86,13 +86,9 @@ def test_day_exists(year: int, month: int, day: int, expected: bool): assert day_exists(year, month, day) == expected -@pytest.mark.parametrize( - "year, month, ok", [(y, y - 2009, y > 2009 and y < 2022) for y in range(2005, 2025)] -) +@pytest.mark.parametrize("year, month, ok", [(y, y - 2009, y > 2009 and y < 2022) for y in range(2005, 2025)]) def test_monthinyear_create(year: int, month: int, ok: bool): - context = ( - nullcontext() if ok else pytest.raises(ValueError, match="^Invalid month:") - ) + context = nullcontext() if ok else pytest.raises(ValueError, match="^Invalid month:") with context: ymonth = MonthInYear(year, month) if ok: @@ -114,9 +110,7 @@ def test_monthinyear_create(year: int, month: int, ok: bool): (2018, 8, 0, False), ], ) -def test_monthinyear_contains( - year: int, month: int, day: Union[int, date], expected: bool -): +def test_monthinyear_contains(year: int, month: int, day: Union[int, date], expected: bool): ymonth = MonthInYear(year, month) assert (day in ymonth) == expected @@ -180,9 +174,7 @@ def test_monthinyear_previous(year: int, month: int, eyear: int, emonth: int): ("1213", (12, 13)), ], ) -def test_parse_mmdd( - arg: Union[Tuple[int, int], str], expected: Union[Tuple[int, int], str] -): +def test_parse_mmdd(arg: Union[Tuple[int, int], str], expected: Union[Tuple[int, int], str]): context = nullcontext() if isinstance(expected, str): context = pytest.raises(ValueError, match=expected) diff --git a/tests/test_climatology.py b/tests/test_climatology.py index f2a6175..a1cb115 100644 --- a/tests/test_climatology.py +++ b/tests/test_climatology.py @@ -47,9 +47,7 @@ def test_date_range_leapyear(): ] # end is leap (2) - assert list( - date_range(date(2022, 2, 28), date(2020, 2, 28), date(2024, 2, 29)) - ) == [ + assert list(date_range(date(2022, 2, 28), date(2020, 2, 28), date(2024, 2, 29))) == [ date(2020, 2, 28), date(2021, 2, 28), date(2022, 2, 28), @@ -66,9 +64,7 @@ def test_date_range_leapyear(): ] # start and end are leap (2) - assert list( - date_range(date(2022, 2, 28), date(2020, 2, 29), date(2024, 2, 29)) - ) == [ + assert list(date_range(date(2022, 2, 28), date(2020, 2, 29), date(2024, 2, 29))) == [ date(2021, 2, 28), date(2022, 2, 28), date(2023, 2, 28), @@ -83,9 +79,7 @@ def test_date_range_leapyear(): ] # reference and end are leap - assert list( - date_range(date(2020, 2, 29), date(2017, 2, 28), date(2020, 2, 29)) - ) == [ + assert list(date_range(date(2020, 2, 29), date(2017, 2, 28), date(2020, 2, 29))) == [ date(2017, 2, 28), date(2018, 2, 28), date(2019, 2, 28), @@ -93,9 +87,7 @@ def test_date_range_leapyear(): ] # all dates are leap - assert list( - date_range(date(2020, 2, 29), date(2016, 2, 29), date(2020, 2, 29)) - ) == [ + assert list(date_range(date(2020, 2, 29), date(2016, 2, 29), date(2020, 2, 29))) == [ date(2017, 2, 28), date(2018, 2, 28), date(2019, 2, 28), @@ -116,11 +108,7 @@ def test_model_climate_dates(): timedelta(days=7), WeeklySequence([MONDAY, THURSDAY]), ) - ) == [ - date(y, m, d) - for y in range(2020, 2024) - for m, d in [(2, 22), (2, 26), (2, 28), (3, 4), (3, 7)] - ] + ) == [date(y, m, d) for y in range(2020, 2024) for m, d in [(2, 22), (2, 26), (2, 28), (3, 4), (3, 7)]] assert list( model_climate_dates( @@ -131,11 +119,7 @@ def test_model_climate_dates(): timedelta(days=10), MonthlySequence(range(1, 32, 4), excludes=[(2, 29)]), ) - ) == [ - date(y, m, d) - for y in range(2020, 2024) - for m, d in [(2, 21), (2, 25), (3, 1), (3, 5), (3, 9)] - ] + ) == [date(y, m, d) for y in range(2020, 2024) for m, d in [(2, 21), (2, 25), (3, 1), (3, 5), (3, 9)]] assert list( model_climate_dates( diff --git a/tests/test_data.py b/tests/test_data.py index 3f0ca7b..cf261cf 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -5,9 +5,7 @@ from earthkit.time.data import ResourceType, find_resource -def test_find_resource( - monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory -): +def test_find_resource(monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory): # Packaged resources only assert find_resource("sequences/ecmwf-mon-thu.yaml")[0] == ResourceType.PACKAGED assert find_resource("sequences/nonexistent")[0] == ResourceType.NOTFOUND @@ -32,10 +30,7 @@ def test_find_resource( # Non-existent path does not override assert ( - find_resource( - "sequences/ecmwf-mon-thu.yaml", path=str(custom / "nonexistent.txt") - )[0] - == ResourceType.PACKAGED + find_resource("sequences/ecmwf-mon-thu.yaml", path=str(custom / "nonexistent.txt"))[0] == ResourceType.PACKAGED ) # Resource by env file @@ -49,17 +44,12 @@ def test_find_resource( # Non-existent env file with monkeypatch.context() as m: m.setenv("TEST_RES_FILE", str(custom / "nonexistent.txt")) - assert ( - find_resource("sequences/test-hello", env_file="TEST_RES_FILE")[0] - == ResourceType.NOTFOUND - ) + assert find_resource("sequences/test-hello", env_file="TEST_RES_FILE")[0] == ResourceType.NOTFOUND # Env file overrides packaged with monkeypatch.context() as m: m.setenv("TEST_RES_FILE", str(custom_seq)) - assert find_resource( - "sequences/ecmwf-mon-thu.yaml", env_file="TEST_RES_FILE" - ) == ( + assert find_resource("sequences/ecmwf-mon-thu.yaml", env_file="TEST_RES_FILE") == ( ResourceType.FILE, str(custom_seq), ) @@ -67,10 +57,7 @@ def test_find_resource( # Non-existent env file does not override with monkeypatch.context() as m: m.setenv("TEST_RES_FILE", str(custom / "nonexistent.txt")) - assert ( - find_resource("sequences/ecmwf-mon-thu.yaml", env_file="TEST_RES_FILE")[0] - == ResourceType.PACKAGED - ) + assert find_resource("sequences/ecmwf-mon-thu.yaml", env_file="TEST_RES_FILE")[0] == ResourceType.PACKAGED other = tmp_path_factory.mktemp("other") other_foo = other / "foo.txt" @@ -81,9 +68,7 @@ def test_find_resource( # Path overrides env file with monkeypatch.context() as m: m.setenv("TEST_RES_FILE", str(custom_foo)) - assert find_resource( - "sequences/test-hello", path=str(other_foo), env_file="TEST_RES_FILE" - ) == ( + assert find_resource("sequences/test-hello", path=str(other_foo), env_file="TEST_RES_FILE") == ( ResourceType.FILE, str(other_foo), ) @@ -107,17 +92,12 @@ def test_find_resource( # Resource by env path, non-existent with monkeypatch.context() as m: m.setenv("TEST_RES_DIR", os.pathsep.join([str(custom), str(other)])) - assert ( - find_resource("sequences/nonexistent", env_path="TEST_RES_DIR")[0] - == ResourceType.NOTFOUND - ) + assert find_resource("sequences/nonexistent", env_path="TEST_RES_DIR")[0] == ResourceType.NOTFOUND # Env path overrides packaged with monkeypatch.context() as m: m.setenv("TEST_RES_DIR", os.pathsep.join([str(other), str(custom)])) - assert find_resource( - "sequences/ecmwf-mon-thu.yaml", env_path="TEST_RES_DIR" - ) == ( + assert find_resource("sequences/ecmwf-mon-thu.yaml", env_path="TEST_RES_DIR") == ( ResourceType.FILE, str(custom_seq), ) @@ -125,18 +105,13 @@ def test_find_resource( # Non-existent env path does not override with monkeypatch.context() as m: m.setenv("TEST_RES_DIR", os.pathsep.join([str(other / "nonexistent")])) - assert ( - find_resource("sequences/ecmwf-mon-thu.yaml", env_path="TEST_RES_DIR")[0] - == ResourceType.PACKAGED - ) + assert find_resource("sequences/ecmwf-mon-thu.yaml", env_path="TEST_RES_DIR")[0] == ResourceType.PACKAGED # Env file overrides env path with monkeypatch.context() as m: m.setenv("TEST_RES_FILE", str(other_bar)) m.setenv("TEST_RES_DIR", os.pathsep.join([str(other), str(custom)])) - assert find_resource( - "sequences/foo.txt", env_file="TEST_RES_FILE", env_path="TEST_RES_DIR" - ) == ( + assert find_resource("sequences/foo.txt", env_file="TEST_RES_FILE", env_path="TEST_RES_DIR") == ( ResourceType.FILE, str(other_bar), ) @@ -144,9 +119,7 @@ def test_find_resource( # Path overrides env path with monkeypatch.context() as m: m.setenv("TEST_RES_DIR", os.pathsep.join([str(other), str(custom)])) - assert find_resource( - "sequences/foo.txt", path=str(other_bar), env_path="TEST_RES_DIR" - ) == ( + assert find_resource("sequences/foo.txt", path=str(other_bar), env_path="TEST_RES_DIR") == ( ResourceType.FILE, str(other_bar), ) diff --git a/tests/test_sequence.py b/tests/test_sequence.py index dff2cef..e73cb10 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -263,10 +263,7 @@ def test_sequence( assert list(seq.range(dates[0], dates[-1])) == dates assert list(seq.range(dates[0], dates[-1], include_start=False)) == dates[1:] - assert ( - list(seq.range(dates[0], dates[-1], include_start=False, include_end=False)) - == dates[1:-1] - ) + assert list(seq.range(dates[0], dates[-1], include_start=False, include_end=False)) == dates[1:-1] assert list(seq.range(dates[0], dates[-1], include_end=False)) == dates[:-1] assert list(seq.bracket(dates[2])) == [dates[1], dates[3]] @@ -296,22 +293,14 @@ def test_sequence( assert seq.nearest(out_date, resolve="next") == dates[out_i] assert list(seq.range(out_date, dates[-1])) == dates[out_i:] - assert ( - list(seq.range(out_date, dates[-1], include_start=False)) == dates[out_i:] - ) + assert list(seq.range(out_date, dates[-1], include_start=False)) == dates[out_i:] assert list(seq.range(dates[0], out_date)) == dates[:out_i] assert list(seq.range(dates[0], out_date, include_end=False)) == dates[:out_i] assert list(seq.bracket(out_date)) == [dates[out_i - 1], dates[out_i]] assert list(seq.bracket(out_date, (before, after))) == dates - assert ( - list(seq.bracket(out_date, (min(2, before), after))) - == dates[max(0, out_i - 2) :] - ) - assert ( - list(seq.bracket(out_date, (before, min(2, after)))) - == dates[: min(len(dates), out_i + 2)] - ) + assert list(seq.bracket(out_date, (min(2, before), after))) == dates[max(0, out_i - 2) :] + assert list(seq.bracket(out_date, (before, min(2, after)))) == dates[: min(len(dates), out_i + 2)] @pytest.mark.parametrize( @@ -353,9 +342,7 @@ def test_sequence( None, id="weekly-strlist", ), - pytest.param( - {"type": "monthly", "days": 13}, MonthlySequence, [13], set(), id="monthly" - ), + pytest.param({"type": "monthly", "days": 13}, MonthlySequence, [13], set(), id="monthly"), pytest.param( {"type": "monthly", "days": [7, 21]}, MonthlySequence, @@ -447,9 +434,7 @@ def test_sequence_from_dict( "seq_dict, expect_msg", [ pytest.param({}, "^Sequence dictionary must contain `type` key$", id="notype"), - pytest.param( - {"type": "sesquiannual"}, "^Unknown type 'sesquiannual'$", id="unknowntype" - ), + pytest.param({"type": "sesquiannual"}, "^Unknown type 'sesquiannual'$", id="unknowntype"), pytest.param( {"type": "weekly"}, "^Weekly sequence must provide `days`$", @@ -472,9 +457,7 @@ def test_create_sequence_invalid(seq_dict: dict, expect_msg: str): Sequence.from_dict(seq_dict) -def test_sequence_from_resource( - monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory -): +def test_sequence_from_resource(monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory): seq = Sequence.from_resource("ecmwf-mon-thu") assert type(seq) is WeeklySequence assert seq.days == [MONDAY, THURSDAY] @@ -483,12 +466,8 @@ def test_sequence_from_resource( Sequence.from_resource("invalid-sequence") seq_path1 = tmp_path_factory.mktemp("seqs1") - (seq_path1 / "wednesdays.yaml").write_text( - yaml.safe_dump({"type": "weekly", "days": ["Wednesday"]}) - ) - (seq_path1 / "foo.yaml").write_text( - yaml.safe_dump({"type": "monthly", "days": [2, 4, 6, 8]}) - ) + (seq_path1 / "wednesdays.yaml").write_text(yaml.safe_dump({"type": "weekly", "days": ["Wednesday"]})) + (seq_path1 / "foo.yaml").write_text(yaml.safe_dump({"type": "monthly", "days": [2, 4, 6, 8]})) seq_path2 = tmp_path_factory.mktemp("seqs2") (seq_path2 / "foo.yaml").write_text(yaml.safe_dump({"type": "daily"})) @@ -496,11 +475,7 @@ def test_sequence_from_resource( yaml.safe_dump( { "type": "yearly", - "days": [ - (m, d) - for m in range(1, 13) - for d in range(1, month_length(1999, m) + 1, 4) - ], + "days": [(m, d) for m in range(1, 13) for d in range(1, month_length(1999, m) + 1, 4)], } ) ) @@ -526,9 +501,7 @@ def test_sequence_from_resource( "name, args, kwargs, expect_type, expect_days, expect_excludes", [ pytest.param("daily", (), {}, DailySequence, None, set(), id="daily"), - pytest.param( - "daily", ({31},), {}, DailySequence, None, {31}, id="daily-excludes" - ), + pytest.param("daily", ({31},), {}, DailySequence, None, {31}, id="daily-excludes"), pytest.param( "daily", (), @@ -538,9 +511,7 @@ def test_sequence_from_resource( {31}, id="daily-excludes-kw", ), - pytest.param( - "weekly", (2,), {}, WeeklySequence, [WEDNESDAY], None, id="weekly" - ), + pytest.param("weekly", (2,), {}, WeeklySequence, [WEDNESDAY], None, id="weekly"), pytest.param( "weekly", (), @@ -587,9 +558,7 @@ def test_sequence_from_resource( {(2, 29)}, id="monthly-excludes-mix", ), - pytest.param( - "yearly", ((7, 1),), {}, YearlySequence, [(7, 1)], set(), id="yearly" - ), + pytest.param("yearly", ((7, 1),), {}, YearlySequence, [(7, 1)], set(), id="yearly"), pytest.param( "yearly", (), @@ -626,9 +595,7 @@ def test_sequence_from_resource( {(2020, 4, 1)}, id="yearly-excludes-mix", ), - pytest.param( - "dict", ({"type": "daily"},), {}, DailySequence, None, set(), id="dict" - ), + pytest.param("dict", ({"type": "daily"},), {}, DailySequence, None, set(), id="dict"), pytest.param( "dict", (), @@ -656,9 +623,7 @@ def test_sequence_from_resource( None, id="resource-kw", ), - pytest.param( - "file", ("seqs/foo.yaml",), {}, DailySequence, None, {13}, id="file" - ), + pytest.param("file", ("seqs/foo.yaml",), {}, DailySequence, None, {13}, id="file"), pytest.param( "file", (), @@ -683,12 +648,8 @@ def test_create_sequence( if name == "file": seq_dir = tmp_path / "seqs" seq_dir.mkdir(parents=True, exist_ok=True) - (seq_dir / "foo.yaml").write_text( - yaml.safe_dump({"type": "daily", "excludes": [13]}) - ) - (seq_dir / "bar.yaml").write_text( - yaml.safe_dump({"type": "yearly", "days": (1, 1)}) - ) + (seq_dir / "foo.yaml").write_text(yaml.safe_dump({"type": "daily", "excludes": [13]})) + (seq_dir / "bar.yaml").write_text(yaml.safe_dump({"type": "yearly", "days": (1, 1)})) monkeypatch.chdir(tmp_path) seq = create_sequence(name, *args, **kwargs)