-
Notifications
You must be signed in to change notification settings - Fork 296
Support Standard Name Modifers (not units!) #3393
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
Conversation
Ping @bjlittle ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lbdreyer Nice one, over to you... 😄
lib/iris/fileformats/cf.py
Outdated
# Supported standard name modifiers. Ref: [CF] Appendix C. | ||
STD_NAME_MODIFIERS = ['detection_minimum', 'number_of_observations', | ||
'standard_error', 'status_flag'] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lbdreyer It's kinda weird to define constants here that aren't used in this module. Rather they're used in _cube_coord_common.py
, and specifically in only one place...
I see the intent, but would you consider binding their use closer to their definition? i.e., actually define STD_NAME_MODIFIERS
as a variable within the iris._cube_coord_common.get_valid_standard_name
function.
lib/iris/_cube_coord_common.py
Outdated
self._standard_name = name | ||
else: | ||
raise ValueError('%r is not a valid standard_name' % name) | ||
self._standard_name = get_valid_standard_name(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could change get_valid_standard_name
so that if None
is passed in, then None
is passed out. That way any code that calls get_valid_standard_name
doesn't need to explicitly handle the None
case (as above).
So here we would simply have:
@standard_name.setter
def standard_name(self, name):
self._standard_name = get_valid_standard_name(name)
lib/iris/_cube_coord_common.py
Outdated
standard_name_modifier not in | ||
iris.fileformats.cf.STD_NAME_MODIFIERS): | ||
raise ValueError(error_msg) | ||
return name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider pulling the return name
out from within the embedded if else
and make it the last statement of the function i.e.,
def get_valid_standard_name(name):
...
if ...
...
else:
...
return name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree, it would be nicer. I just couldn't quite think through how to do that, but I will give it another look!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lbdreyer I think that all you need to do is simply move the return name
outside the if ... else ...
, as your not changing the contents of name
, the output of the function is just the same as the input.
If the input std_name
(from name
) is valid, then it will pass through the if ...
, and then if the standard_name_modifier
is valid it will simply drop through and return name
. On the otherhand, if the modifier is not valid an exception is raised and the return name
is never be reached.
If the input name
is invalid, then it will pass through to the else ...
where an exception is raised and the return name
will never be reached.
Should just work, right? Check my logic is right, though...
lib/iris/_cube_coord_common.py
Outdated
# Standard names are optionally followed by a standard name | ||
# modifier, separated by one or more blank spaces | ||
name_parts = name.split(' ') | ||
std_name, std_name_extension = name_parts[0], name_parts[1:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a (compiled) regular expression would allow you to easily pluck out the standard name and modifier, and avoid dealing with a std_name_extension
that is a list.
with self.temp_filename(suffix='.nc') as fout: | ||
iris.save(cube, fout) | ||
detection_limit_cube = iris.load_cube(fout) | ||
self.assertEqual(detection_limit_cube.standard_name, standard_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 👍
class Test_standard_name__setter(tests.IrisTest): | ||
def test_valid_standard_name(self): | ||
cf_var = CFVariableMixin() | ||
cf_var.standard_name = 'air_temperature' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you then self.assertEqual(cf_var.standard_name, 'air_temperature')
to ensure that we get back what we set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my concern with that was that we are then testing the standard_name getter and setter at the same time, which doesn't feel very unit-testy but I guess we should test not just that setting the standard doesn't raise errors, but also that it is done correctly and it's hard to do that in isolation from testing the getter.
|
||
def test_none_standard_name(self): | ||
cf_var = CFVariableMixin() | ||
cf_var.standard_name = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, include a self.assertIsNone(cf_var.standard_name)
?
engine = mock.Mock( | ||
cube=Cube([23]), | ||
cf_var=cf_var) | ||
return engine |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondered why you pulled this out of TestInvalidGlobalAttributes
, but you're also using it in the new test class TestCubeName
... nice 👍
@lbdreyer What's the strategy for checking the Separate PR? |
inp = ('latitude_coord', 'lat_long_name', 'lat_var_name', 'latitude') | ||
exp = ('latitude', 'lat_long_name', 'lat_var_name', | ||
{'invalid_standard_name': 'latitude_coord'}) | ||
self.check_names(inp, exp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lbdreyer Whoa... nice test coverage 🎉
b677563
to
8e30737
Compare
8e30737
to
101dc5c
Compare
@bjlittle I believe I have addressed your review comments in the third commit "review actions" Note: I had to rebase the first two commits due a conflict that was caused when #3399 was merged, but those commits should be the same as they were before. |
@lbdreyer Awesome, thanks! 🎉 |
CF conventions support a set of standard_name modifiers. See 3.3. Standard Name for more details, specifically:
This PR modifies the CFVariableMixin standard_name setter (and the pyke rules) to support standard name modifiers.
I found there wasn't much testing of cube/coord names so I have split this PR into two commits:
Note that standard name modifiers have associated units (See CF Appendix C) however I have not included that in this PR as I am only modifying the standard_name setter which at the moment doesn't have any requirement on the unit