Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update DevOps to cache conda and fix attributes not being preserved with xarray > 2023.3.0 #465

Merged
merged 7 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions .github/workflows/build_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,32 +62,44 @@ jobs:
- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
uses: actions/checkout@v3

- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
name: Cache Conda
uses: actions/cache@v3
env:
# Increase this value to reset cache if conda-env/ci.yml has not changed in the workflow
CACHE_NUMBER: 0
with:
path: ~/conda_pkgs_dir
key: ${{ runner.os }}-${{ matrix.python-version }}-conda-${{ env.CACHE_NUMBER }}

- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
name: Set up Conda Environment
uses: conda-incubator/setup-miniconda@v2
with:
activate-environment: "xcdat_ci"
miniforge-variant: Mambaforge
miniforge-version: latest
activate-environment: "xcdat_ci"
use-mamba: true
mamba-version: "*"
environment-file: conda-env/ci.yml
channel-priority: strict
auto-update-conda: true
# IMPORTANT: This needs to be set for caching to work properly!
use-only-tar-bz2: true
python-version: ${{ matrix.python-version }}

# Refresh the cache every 24 hours to avoid inconsistencies of package versions
# between the CI pipeline and local installations.
- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
id: get-date
name: Get Date
run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT
shell: bash

- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
id: cache
name: Cache Conda env
uses: actions/cache@v3
with:
path: ${{ env.CONDA }}/envs
key:
conda-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{
steps.get-date.outputs.today }}-${{hashFiles('conda-env/ci.yml') }}-${{ env.CACHE_NUMBER}}
env:
# Increase this value to reset cache if conda-env/ci.yml has not changed in the workflow
CACHE_NUMBER: 0

- if: $${{ steps.skip_check.outputs.should_skip != 'true' && steps.cache.outputs.cache-hit != 'true' }}
name: Update environment
run: mamba env update -n xcdat_ci -f conda-env/ci.yml

- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
name: Install xcdat
# Source: https://github.com/conda/conda-build/issues/4251#issuecomment-1053460542
Expand Down
4 changes: 3 additions & 1 deletion conda-env/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ dependencies:
- pandas
- python-dateutil
- xarray
- xesmf
# Constrained because 0.6.3 breaks with import ESMF
# Source: https://github.com/pangeo-data/xESMF/issues/212
- xesmf >0.6.3
# Quality Assurance
# ==================
- types-python-dateutil
Expand Down
33 changes: 13 additions & 20 deletions tests/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
from lxml import etree

from tests.fixtures import generate_dataset
from xcdat._logger import _setup_custom_logger
from xcdat.dataset import (
_keep_single_var,
_postprocess_dataset,
decode_time,
open_dataset,
open_mfdataset,
)
from xcdat.logger import setup_custom_logger

logger = setup_custom_logger("xcdat.dataset", propagate=True)
logger = _setup_custom_logger("xcdat.dataset", propagate=True)


class TestOpenDataset:
Expand All @@ -29,6 +29,9 @@ def setup(self, tmp_path):
self.file_path = f"{dir}/file.nc"

def test_raises_warning_if_decode_times_but_no_time_coords_found(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds = generate_dataset(decode_times=False, cf_compliant=True, has_bounds=True)
ds = ds.drop_dims("time")
ds.to_netcdf(self.file_path)
Expand All @@ -42,16 +45,10 @@ def test_raises_warning_if_decode_times_but_no_time_coords_found(self, caplog):
expected = expected.drop_dims("time")

assert result.identical(expected)
assert (
"No time coordinates were found in this dataset to decode. If time "
"coordinates were expected to exist, make sure they are detectable by "
"setting the CF 'axis' or 'standard_name' attribute (e.g., "
"ds['time'].attrs['axis'] = 'T' or "
"ds['time'].attrs['standard_name'] = 'time'). Afterwards, try decoding "
"again with `xcdat.decode_time`."
) in caplog.text

def test_skip_decoding_time_explicitly(self):

def test_skip_decoding_time_explicitly(
self,
):
tomvothecoder marked this conversation as resolved.
Show resolved Hide resolved
ds = generate_dataset(decode_times=False, cf_compliant=True, has_bounds=True)
ds.to_netcdf(self.file_path)

Expand Down Expand Up @@ -602,6 +599,9 @@ def setUp(self, tmp_path):
self.file_path2 = f"{self.dir}/file2.nc"

def test_raises_warning_if_decode_times_but_no_time_coords_found(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds = generate_dataset(decode_times=False, cf_compliant=True, has_bounds=True)
ds = ds.drop_dims("time")
ds.to_netcdf(self.file_path1)
Expand All @@ -615,14 +615,6 @@ def test_raises_warning_if_decode_times_but_no_time_coords_found(self, caplog):
expected = expected.drop_dims("time")

assert result.identical(expected)
assert (
"No time coordinates were found in this dataset to decode. If time "
"coordinates were expected to exist, make sure they are detectable by "
"setting the CF 'axis' or 'standard_name' attribute (e.g., "
"ds['time'].attrs['axis'] = 'T' or "
"ds['time'].attrs['standard_name'] = 'time'). "
"Afterwards, try decoding again with `xcdat.decode_time`."
) in caplog.text

def test_skip_decoding_times_explicitly(self):
ds1 = generate_dataset(decode_times=False, cf_compliant=False, has_bounds=True)
Expand Down Expand Up @@ -1024,6 +1016,7 @@ def setup(self):
"axis": "T",
"long_name": "time",
"standard_name": "time",
"calendar": "standard",
},
)
time_bnds = xr.DataArray(
Expand Down
66 changes: 24 additions & 42 deletions tests/test_temporal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

import cftime
import numpy as np
import pytest
Expand All @@ -6,14 +8,14 @@
from xarray.tests import requires_dask

from tests.fixtures import generate_dataset
from xcdat.logger import setup_custom_logger
from xcdat._logger import _setup_custom_logger
from xcdat.temporal import (
TemporalAccessor,
_contains_datetime_like_objects,
_get_datetime_like_type,
)

logger = setup_custom_logger("xcdat.temporal", propagate=True)
logger = _setup_custom_logger("xcdat.temporal", propagate=True)


class TestTemporalAccessor:
Expand Down Expand Up @@ -41,23 +43,18 @@ def test_raises_error_if_time_coords_are_not_decoded(self):
with pytest.raises(TypeError):
ds.temporal.average("ts")

def test_raises_warning_if_calendar_encoding_attr_not_found_on_data_var_time_coords(
self, caplog
):
def test_defaults_calendar_attribute_to_standard_if_missing(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds: xr.Dataset = generate_dataset(
decode_times=True, cf_compliant=False, has_bounds=True
)
ds.ts.time.encoding = {}

ds.temporal.average("ts")

assert (
"'time' does not have a calendar encoding attribute set, "
"which is used to determine the `cftime.datetime` object type for the "
"output time coordinates. Defaulting to CF 'standard' calendar. "
"Otherwise, set the calendar type (e.g., "
"ds['time'].encoding['calendar'] = 'noleap') and try again."
) in caplog.text
assert ds.temporal.calendar == "standard"

def test_averages_for_yearly_time_series(self):
ds = xr.Dataset(
Expand Down Expand Up @@ -460,23 +457,18 @@ def test_raises_error_if_time_coords_are_not_decoded(self):
with pytest.raises(TypeError):
ds.temporal.group_average("ts", freq="year")

def test_raises_warning_if_calendar_encoding_attr_not_found_on_data_var_time_coords(
self, caplog
):
def test_defaults_calendar_attribute_to_standard_if_missing(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds: xr.Dataset = generate_dataset(
decode_times=True, cf_compliant=False, has_bounds=True
)
ds.ts.time.encoding = {}

ds.temporal.group_average("ts", freq="year")

assert (
"'time' does not have a calendar encoding attribute set, "
"which is used to determine the `cftime.datetime` object type for the "
"output time coordinates. Defaulting to CF 'standard' calendar. "
"Otherwise, set the calendar type (e.g., "
"ds['time'].encoding['calendar'] = 'noleap') and try again."
) in caplog.text
assert ds.temporal.calendar == "standard"

def test_weighted_annual_averages(self):
ds = self.ds.copy()
Expand Down Expand Up @@ -1039,23 +1031,18 @@ def test_raises_error_if_time_coords_are_not_decoded(self):
with pytest.raises(TypeError):
ds.temporal.climatology("ts", freq="year")

def test_raises_warning_if_calendar_encoding_attr_not_found_on_data_var_time_coords(
self, caplog
):
def test_defaults_calendar_attribute_to_standard_if_missing(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds: xr.Dataset = generate_dataset(
decode_times=True, cf_compliant=False, has_bounds=True
)
ds.ts.time.encoding = {}

ds.temporal.climatology("ts", freq="season")

assert (
"'time' does not have a calendar encoding attribute set, "
"which is used to determine the `cftime.datetime` object type for the "
"output time coordinates. Defaulting to CF 'standard' calendar. "
"Otherwise, set the calendar type (e.g., "
"ds['time'].encoding['calendar'] = 'noleap') and try again."
) in caplog.text
assert ds.temporal.calendar == "standard"

def test_raises_error_if_reference_period_arg_is_incorrect(self):
ds = self.ds.copy()
Expand Down Expand Up @@ -1742,23 +1729,18 @@ def test_raises_error_if_time_coords_are_not_decoded(self):
with pytest.raises(TypeError):
ds.temporal.departures("ts", freq="season")

def test_raises_warning_if_calendar_encoding_attr_not_found_on_data_var_time_coords(
self, caplog
):
def test_defaults_calendar_attribute_to_standard_if_missing(self, caplog):
# Silence warning to not pollute test suite output
caplog.set_level(logging.CRITICAL)

ds: xr.Dataset = generate_dataset(
decode_times=True, cf_compliant=False, has_bounds=True
)
ds.ts.time.encoding = {}

ds.temporal.departures("ts", freq="season")

assert (
"'time' does not have a calendar encoding attribute set, "
"which is used to determine the `cftime.datetime` object type for the "
"output time coordinates. Defaulting to CF 'standard' calendar. "
"Otherwise, set the calendar type (e.g., "
"ds['time'].encoding['calendar'] = 'noleap') and try again."
) in caplog.text
assert ds.temporal.calendar == "standard"

def test_raises_error_if_reference_period_arg_is_incorrect(self):
ds = self.ds.copy()
Expand Down
28 changes: 16 additions & 12 deletions xcdat/logger.py → xcdat/_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@
import logging
import logging.handlers

# Logging module setup
log_format = (
"%(asctime)s [%(levelname)s]: %(filename)s(%(funcName)s:%(lineno)s) >> %(message)s"
)
logging.basicConfig(format=log_format, filemode="w", level=logging.INFO)

def setup_custom_logger(name: str, propagate: bool = False) -> logging.Logger:
# Console handler setup
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
logFormatter = logging.Formatter(log_format)
console_handler.setFormatter(logFormatter)
logging.getLogger().addHandler(console_handler)


def _setup_custom_logger(name, propagate=True) -> logging.Logger:
"""Sets up a custom logger.

Documentation on logging: https://docs.python.org/3/library/logging.html

Parameters
----------
name : str
Expand Down Expand Up @@ -43,18 +58,7 @@ def setup_custom_logger(name: str, propagate: bool = False) -> logging.Logger:

>>> logger.critical("")
"""
log_format = "%(asctime)s [%(levelname)s]: %(filename)s(%(funcName)s:%(lineno)s) >> %(message)s"
log_filemode = "w" # w: overwrite; a: append

# Setup
logging.basicConfig(format=log_format, filemode=log_filemode, level=logging.INFO)
logger = logging.getLogger(name)
logger.propagate = propagate

# Console output
consoleHandler = logging.StreamHandler()
logFormatter = logging.Formatter(log_format)
consoleHandler.setFormatter(logFormatter)
logger.addHandler(consoleHandler)

return logger
4 changes: 2 additions & 2 deletions xcdat/bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
from xarray.coding.cftime_offsets import get_date_type
from xarray.core.common import contains_cftime_datetimes

from xcdat._logger import _setup_custom_logger
from xcdat.axis import CF_ATTR_MAP, CFAxisKey, get_dim_coords
from xcdat.dataset import _get_data_var
from xcdat.logger import setup_custom_logger
from xcdat.temporal import (
_contains_datetime_like_objects,
_get_datetime_like_type,
_infer_freq,
)

logger = setup_custom_logger(__name__)
logger = _setup_custom_logger(__name__)


@xr.register_dataset_accessor("bounds")
Expand Down
4 changes: 2 additions & 2 deletions xcdat/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
from xarray.core.variable import as_variable

from xcdat import bounds as bounds_accessor # noqa: F401
from xcdat._logger import _setup_custom_logger
from xcdat.axis import CFAxisKey, _get_all_coord_keys
from xcdat.axis import center_times as center_times_func
from xcdat.axis import swap_lon_axis
from xcdat.logger import setup_custom_logger

logger = setup_custom_logger(__name__)
logger = _setup_custom_logger(__name__)

#: List of non-CF compliant time units.
NON_CF_TIME_UNITS: List[str] = ["month", "months", "year", "years"]
Expand Down
4 changes: 2 additions & 2 deletions xcdat/regridder/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import xarray as xr

import xcdat.bounds # noqa: F401
from xcdat.logger import setup_custom_logger
from xcdat._logger import _setup_custom_logger

logger = setup_custom_logger(__name__)
logger = _setup_custom_logger(__name__)


def preserve_bounds(
Expand Down
Loading