Skip to content

Commit 220b686

Browse files
jkretzatmodatcode
andauthored
Experimental PR that aims at including pytest workflows (#45)
* Check if version of CF Convention and AtMoDat Standard are in predefined range. Further check if string provided in global attribute is empty. * Fix small issues related to summary creation and update README.md * Remove ess_vocabs_utils.py as we do not really need it * Remove not needed "if" statement in check_conventions_version_number method * Do not perform "GlobalAttrTypeCheck" for "Conventions" global attribute * Add first test * Resolve issue in "check_global_attr_type" when checking for empty global attributes * Add tests for "check_global_attr_type" method * Replace "datetime.datetime.fromisoformat" with "dateutil.parser.isoparse" in "check_global_attr_iso8601" * Add tests for "DateISO8601Check" class * Add pytest to GitHub Actions * Update pytest.yml * Update pytest.yml * Update pytest.yml * Update pytest.yml * Update pytest.yml * Update pytest.yml * Update pytest.yml * Fix linter issues, clean-up "environment.yml" and "requirements.txt" * Fix creation of "message" list in "ConventionsVersionCheck" * Remove ess_vocabs_utils.py as we do not really need it * Remove not needed "if" statement in check_conventions_version_number method * Do not perform "GlobalAttrTypeCheck" for "Conventions" global attribute * Resolve issue in "check_global_attr_type" when checking for empty global attributes * Check for ATMODAT/CF information missing in "Conventions" global attribute * Revise FileIsNetCDF to work with "status" argument * Add first tests for "GobalAttrResolutionFormatCheck" class * Remove format_checks_atmodat_register.py as this check is performed by "_check_primary_arg" for each test * Fix test for "ConventionsVersionCheck" * Assert that message lenght after splitting is 3 and adapt "GlobalAttrTypeCheck" accordingly * Fix linter issues * Update README.md added description of new optional arguments Co-authored-by: atmodatcode <79904507+atmodatcode@users.noreply.github.com>
1 parent 7e2c95b commit 220b686

File tree

9 files changed

+366
-47
lines changed

9 files changed

+366
-47
lines changed

.github/workflows/pytest.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
on:
2+
push:
3+
branches: [master]
4+
pull_request:
5+
branches: [master]
6+
7+
jobs:
8+
build:
9+
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: [3.6]
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Checkout submodules
18+
run: git submodule update --init --recursive
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v2
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
- name: Add conda to system path
24+
run: |
25+
# $CONDA is an environment variable pointing to the root of the miniconda directory
26+
echo $CONDA/bin >> $GITHUB_PATH
27+
- name: Install dependencies
28+
run: |
29+
conda env update --file environment.yml --name base
30+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31+
- name: Test with pytest
32+
run:
33+
pytest
34+
env:
35+
PYESSV_ARCHIVE_HOME: ${{ github.workspace }}/AtMoDat_CVs/pyessv-archive

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,35 @@ To run checkers on a single file, use:
5252
```bash
5353
run_checks.py -f file_to_check.nc
5454
```
55-
To run the checker on all *.nc files of a directory, use:
55+
To run the checker on all *.nc files of a directory (including all sub-directories), use:
5656
```bash
5757
run_checks.py -p file_path
5858
```
5959
To create summary of checker ouput add the ````-s```` flag, e.g.:
6060
```bash
6161
run_checks.py -s -p file_path
6262
```
63+
To define a custom path where the checker output shall be written, use:
64+
```bash
65+
run_checks.py -op output_path -p file_path
66+
```
67+
To define a CF version against which the file(s) shall be checked, use:
68+
```bash
69+
run_checks.py -cfv 1.6 -p file_path
70+
```
71+
Valid are versions from 1.3 to 1.8. Default is ````-cfv auto````.
72+
73+
To define if the file(s) shall be checked only against the ATMODAT Standard (AT) or the CF Conventions (CF), specify either ````-check AT```` or ````-check CF````.
74+
Default is ````-check both````.
75+
```bash
76+
run_checks.py -check AT -p file_path
77+
```
78+
You can combine different optional arguments, for example:
79+
```bash
80+
run_checks.py -s -op mychecks -check both -cfv 1.4 -p file_path
81+
```
82+
83+
6384
For more information use `python run_checks.py --help`
6485

6586
## Contributors

atmodat_checklib/register/nc_file_checks_atmodat_register.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ class GlobalAttrTypeCheck(NCFileCheckBase):
100100
defaults = {}
101101
required_args = ['attribute', 'type', 'status']
102102
message_templates = ["'{attribute}' global attribute is not present",
103+
"'{attribute}' global attribute check againt unsupported type {type} "
104+
"(allowed types are: str, int, float).",
103105
"'{attribute}' global attribute is empty",
104-
"'{attribute}' global attribute value does not match type '{type}'"]
106+
"'{attribute}' global attribute value does not match type {type}"]
105107

106108
def _get_result(self, primary_arg):
107109
self._atmodat_status_to_level(self.kwargs["status"])

atmodat_checklib/test/__init__.py

Whitespace-only changes.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
"""
2+
test_nc_file_checks_atmodat_register.py
3+
======================
4+
Unit tests for the contents of the atmodat_checklib.register.nc_file_checks_atmodat_register module.
5+
"""
6+
import numpy as np
7+
import pytest
8+
from atmodat_checklib.register.nc_file_checks_atmodat_register import ConventionsVersionCheck, \
9+
GlobalAttrTypeCheck, DateISO8601Check, GobalAttrResolutionFormatCheck
10+
import datetime
11+
import pytz
12+
from netCDF4 import Dataset
13+
14+
msgs_incorrect = "Incorrect output message"
15+
16+
17+
@pytest.fixture(scope="session")
18+
def empty_netcdf(tmpdir_factory):
19+
nc_file = tmpdir_factory.mktemp("netcdf").join("tmp.nc")
20+
Dataset(nc_file, 'w')
21+
return nc_file
22+
23+
24+
def write_global_attribute(empty_netcdf, **kwargs):
25+
f = Dataset(empty_netcdf, 'w')
26+
for key, value in kwargs.items():
27+
f.setncattr(key, value)
28+
return f
29+
30+
31+
def assert_output_msgs_len(resp_in):
32+
msgs = resp_in.msgs
33+
if resp_in.value[0] == resp_in.value[1]:
34+
assert(len(msgs) == 0)
35+
else:
36+
assert(len(msgs[0].split("'")) == 3), "Incorrect format of error message"
37+
38+
39+
def test_cf_conventions_in_range(empty_netcdf):
40+
min_range, max_range = 1.4, 1.8
41+
for val in np.linspace(min_range, max_range, 5):
42+
ds = write_global_attribute(empty_netcdf, Conventions='CF-' + str(val) + 'ATMODAT-1.0')
43+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "CF",
44+
"min_version": min_range, "max_version": max_range})
45+
resp = x(ds)
46+
assert_output_msgs_len(resp)
47+
assert(resp.value == (3, 3)), resp.msgs
48+
ds.close()
49+
50+
51+
def test_cf_conventions_greater_than_range(empty_netcdf):
52+
min_range, max_range = 1.4, 1.8
53+
val = 1.9
54+
ds = write_global_attribute(empty_netcdf, Conventions='CF-' + str(val) + 'ATMODAT-1.0')
55+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "CF",
56+
"min_version": min_range, "max_version": max_range})
57+
resp = x(ds)
58+
assert_output_msgs_len(resp)
59+
assert(resp.value == (2, 3)), resp.msgs
60+
ds.close()
61+
62+
63+
def test_cf_conventions_less_than_range(empty_netcdf):
64+
min_range, max_range = 1.4, 1.8
65+
val = 1.3
66+
ds = write_global_attribute(empty_netcdf, Conventions='CF-' + str(val) + 'ATMODAT-1.0')
67+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "CF",
68+
"min_version": min_range, "max_version": max_range})
69+
resp = x(ds)
70+
assert_output_msgs_len(resp)
71+
assert(resp.value == (2, 3)), resp.msgs
72+
ds.close()
73+
74+
75+
def test_cf_conventions_conventions_missing(empty_netcdf):
76+
min_range, max_range = 1.4, 1.8
77+
ds = write_global_attribute(empty_netcdf)
78+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "CF",
79+
"min_version": min_range, "max_version": max_range})
80+
resp = x(ds)
81+
assert_output_msgs_len(resp)
82+
assert(resp.value == (0, 3)), resp.msgs
83+
ds.close()
84+
85+
86+
def test_atmodat_conventions_not_present(empty_netcdf):
87+
min_range, max_range = 3.0, 3.0
88+
ds = write_global_attribute(empty_netcdf)
89+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "ATMODAT",
90+
"min_version": min_range, "max_version": max_range})
91+
resp = x(ds)
92+
assert_output_msgs_len(resp)
93+
assert(resp.value == (0, 3)), resp.msgs
94+
ds.close()
95+
96+
97+
def test_atmodat_conventions_version_match(empty_netcdf):
98+
min_range, max_range = 3.0, 3.0
99+
val = 3.0
100+
ds = write_global_attribute(empty_netcdf, Conventions='ATMODAT-' + str(val) + 'CF-1.0')
101+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "ATMODAT",
102+
"min_version": min_range, "max_version": max_range})
103+
resp = x(ds)
104+
assert_output_msgs_len(resp)
105+
assert(resp.value == (3, 3)), resp.msgs
106+
ds.close()
107+
108+
109+
def test_atmodat_conventions_version_no_match(empty_netcdf):
110+
min_range, max_range = 3.0, 3.0
111+
val = 2.0
112+
ds = write_global_attribute(empty_netcdf, Conventions='ATMODAT-' + str(val) + 'CF-1.0')
113+
x = ConventionsVersionCheck(kwargs={"status": "mandatory", "attribute": "Conventions", "convention_type": "ATMODAT",
114+
"min_version": min_range, "max_version": max_range})
115+
resp = x(ds)
116+
assert_output_msgs_len(resp)
117+
assert(resp.value == (2, 3)), resp.msgs
118+
ds.close()
119+
120+
121+
def test_global_attr_type_check_missing(empty_netcdf):
122+
ds = write_global_attribute(empty_netcdf)
123+
x = GlobalAttrTypeCheck(kwargs={"status": "mandatory", "attribute": "foo", "type": "str"})
124+
resp = x(ds)
125+
assert_output_msgs_len(resp)
126+
assert(resp.value == (0, 4)), resp.msgs
127+
ds.close()
128+
129+
130+
def test_global_attr_type_unsupported_type(empty_netcdf):
131+
ds = write_global_attribute(empty_netcdf, foo='bar')
132+
x = GlobalAttrTypeCheck(kwargs={"status": "mandatory", "attribute": "foo", "type": "foobar"})
133+
resp = x(ds)
134+
assert_output_msgs_len(resp)
135+
assert(resp.value == (1, 4)), resp.msgs
136+
ds.close()
137+
138+
139+
def test_global_attr_type_check_empty(empty_netcdf):
140+
ds = write_global_attribute(empty_netcdf, foo='')
141+
x = GlobalAttrTypeCheck(kwargs={"status": "mandatory", "attribute": "foo", "type": "str"})
142+
resp = x(ds)
143+
assert_output_msgs_len(resp)
144+
assert(resp.value == (2, 4)), resp.msgs
145+
ds.close()
146+
147+
148+
def test_global_attr_type_check_wrong_type(empty_netcdf):
149+
type_dict = {"int": ['foo', 1.0], "str": [1, 1.0], "float": ['foo', 1]}
150+
for key, values in type_dict.items():
151+
for value in values:
152+
ds = write_global_attribute(empty_netcdf, foo=value)
153+
x = GlobalAttrTypeCheck(kwargs={"status": "mandatory", "attribute": "foo", "type": key})
154+
resp = x(ds)
155+
assert_output_msgs_len(resp)
156+
assert(resp.value == (3, 4)), resp.msgs
157+
ds.close()
158+
159+
160+
def test_global_attr_type_check_correct_type(empty_netcdf):
161+
type_dict = {"str": 'bar', "int": 1, "float": 1.0}
162+
for key, value in type_dict.items():
163+
ds = write_global_attribute(empty_netcdf, foo=value)
164+
x = GlobalAttrTypeCheck(kwargs={"status": "mandatory", "attribute": "foo", "type": key})
165+
resp = x(ds)
166+
assert_output_msgs_len(resp)
167+
assert(resp.value == (4, 4)), resp.msgs
168+
ds.close()
169+
170+
171+
def test_date_iso8601_check_missing(empty_netcdf):
172+
ds = write_global_attribute(empty_netcdf)
173+
x = DateISO8601Check(kwargs={"status": "recommended", "attribute": "creation_date"})
174+
resp = x(ds)
175+
assert_output_msgs_len(resp)
176+
assert(resp.value == (0, 2)), resp.msgs
177+
ds.close()
178+
179+
180+
def test_date_iso8601_check_valid_timestring(empty_netcdf):
181+
timestring_list = [datetime.datetime.now().isoformat(),
182+
datetime.datetime.now().replace(microsecond=0).isoformat(),
183+
datetime.datetime.now().now().strftime('%Y-%m-%d'),
184+
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat(),
185+
datetime.datetime.now().astimezone(pytz.timezone("US/Eastern")).isoformat(),
186+
datetime.datetime.now().astimezone(pytz.timezone("Europe/Berlin")).isoformat()]
187+
for timestring in timestring_list:
188+
ds = write_global_attribute(empty_netcdf, creation_date=timestring)
189+
x = DateISO8601Check(kwargs={"status": "recommended", "attribute": "creation_date"})
190+
resp = x(ds)
191+
assert_output_msgs_len(resp)
192+
assert(resp.value == (2, 2)), resp.msgs
193+
ds.close()
194+
195+
196+
def test_date_iso8601_check_invalid_timestring(empty_netcdf):
197+
# More example can be added here
198+
timestring_list = ['01.01.2021', '01/01/2021', 'Monday, June 15, 2009 1:45', '2009/6/15 13:45:30',
199+
'Mon, 15 Jun 2009 20:45:30 GMT']
200+
for timestring in timestring_list:
201+
ds = write_global_attribute(empty_netcdf, creation_date=timestring)
202+
x = DateISO8601Check(kwargs={"status": "recommended", "attribute": "creation_date"})
203+
resp = x(ds)
204+
assert_output_msgs_len(resp)
205+
assert(resp.value == (1, 2)), resp.msgs
206+
ds.close()
207+
208+
209+
def test_gobal_attr_resolution_format_check_missing(empty_netcdf):
210+
for attr in ['geospatial_lon_resolution', 'geospatial_lat_resolution', 'geospatial_vertical_resolution']:
211+
ds = write_global_attribute(empty_netcdf)
212+
x = GobalAttrResolutionFormatCheck(kwargs={"status": "recommended", "attribute": attr})
213+
resp = x(ds)
214+
assert_output_msgs_len(resp)
215+
assert(resp.value == (0, 4)), resp.msgs
216+
ds.close()
217+
218+
219+
def test_gobal_attr_resolution_format_check_no_value(empty_netcdf):
220+
for attr in ['geospatial_lon_resolution', 'geospatial_lat_resolution', 'geospatial_vertical_resolution']:
221+
for unit in ['W', ' W', 'W ']:
222+
attr_dict = {attr: unit}
223+
ds = write_global_attribute(empty_netcdf, **attr_dict)
224+
x = GobalAttrResolutionFormatCheck(kwargs={"status": "recommended", "attribute": attr})
225+
resp = x(ds)
226+
assert_output_msgs_len(resp)
227+
assert(resp.value == (1, 4)), resp.msgs
228+
ds.close()
229+
230+
231+
def test_gobal_attr_resolution_format_check_no_unit(empty_netcdf):
232+
for val in [1, 1.0]:
233+
for attr in ['geospatial_lon_resolution', 'geospatial_lat_resolution', 'geospatial_vertical_resolution']:
234+
attr_dict = {attr: str(val)}
235+
ds = write_global_attribute(empty_netcdf, **attr_dict)
236+
x = GobalAttrResolutionFormatCheck(kwargs={"status": "recommended", "attribute": attr})
237+
resp = x(ds)
238+
assert_output_msgs_len(resp)
239+
assert(resp.value == (2, 4)), resp.msgs
240+
ds.close()
241+
242+
243+
def test_gobal_attr_resolution_format_check_invalid_unit(empty_netcdf):
244+
for val in [1, 1.0]:
245+
for unit in ['m/ss', 'Ai']:
246+
for attr in ['geospatial_lon_resolution', 'geospatial_lat_resolution', 'geospatial_vertical_resolution']:
247+
attr_dict = {attr: str(val) + ' ' + unit}
248+
ds = write_global_attribute(empty_netcdf, **attr_dict)
249+
x = GobalAttrResolutionFormatCheck(kwargs={"status": "recommended", "attribute": attr})
250+
resp = x(ds)
251+
assert_output_msgs_len(resp)
252+
assert(resp.value == (3, 4)), resp.msgs
253+
ds.close()
254+
255+
256+
def test_gobal_attr_resolution_format_check_correct(empty_netcdf):
257+
for val in [1, 1.0]:
258+
for unit in ['W', 'J', 'm/s']:
259+
for attr in ['geospatial_lon_resolution', 'geospatial_lat_resolution', 'geospatial_vertical_resolution']:
260+
attr_dict = {attr: str(val) + ' ' + unit}
261+
ds = write_global_attribute(empty_netcdf, **attr_dict)
262+
x = GobalAttrResolutionFormatCheck(kwargs={"status": "recommended", "attribute": attr})
263+
resp = x(ds)
264+
assert_output_msgs_len(resp)
265+
assert(resp.value == (4, 4)), resp.msgs
266+
ds.close()

0 commit comments

Comments
 (0)