Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ section.

If an attribute is listed with a missing value that attribute is deleted from the file.

Variable and dimension [renaming](#renaming-variables-and-dimensions) can also be described in these file.

For example the following is an example of an attribute file:
```yaml
global:
Expand All @@ -30,8 +32,13 @@ variables:
long_name: "latitude coordinate"
units: "degrees_north"
standard_name: "latitude"
rename:
variables:
T: time
dimensions:
T: time
```
It will create (or replace) two global attributes: `Conventions` and `license`.
`addmeta` will start by renaming the variable and dimension `T` to `time`. It will then create (or replace) two global attributes: `Conventions` and `license`.
It will also create (or replace) attributes for two variables, `yt_ocean` and
`geolat_t`, and delete the `_FillValue` attribute of `yt_ocean`.

Expand Down Expand Up @@ -208,6 +215,25 @@ netCDF applications are expected to update the history attribute when modifying
the files. This can be enabled in `addmeta` with the `--update-history`
commandline argument.

### Renaming Variables and Dimensions

`addmeta` supports the renaming of variables and dimensions which can be described in the metadata YAML files alongside global and variable attributes:
```yaml
rename:
variables:
old_var_name: new_var_name
old_var_name2: new_var_name2
dimensions:
old_dim_name: new_dim_name
old_dim_name2: new_dim_name2
```

The renaming operation occurs before variable metadata is applied, thus `addmeta` will attempt to add attributes using a variable's new name.
`addmeta` will not fail if the original name of a variable or dimension does not exist in the file.

> [!Note]
> The dynamic templating available to attributes is not supported for variable and dimension renaming.

### Sorting Attributes

Global and variable attributes can be sorted lexicographically ignoring-case by `addmeta` if needed.
Expand Down
33 changes: 31 additions & 2 deletions addmeta/addmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def read_metadata(fname):
# Check if this appears to be a plain key/value yaml file rather
# than a structured file with 'global' and 'variables' keywords
assume_global = True
for key in ["variables", "global"]:
for key in ["variables", "global", "rename"]:
if key in metadict and isinstance(metadict[key], dict):
assume_global = False

Expand Down Expand Up @@ -111,6 +111,18 @@ def add_meta(ncfile, metadict, template_vars, sort_attrs=False, history=None, ve
Add meta data from a dictionary to a netCDF file
"""
rootgrp = nc.Dataset(ncfile, "r+")

# Rename variables and dimensions
if "rename" in metadict:
rename_dict = metadict["rename"]
if "variables" in rename_dict:
for old_name, new_name in rename_dict["variables"].items():
rename_var_or_dim(rootgrp, old_name, new_name, is_var=True, verbose=verbose)

if "dimensions" in rename_dict:
for old_name, new_name in rename_dict["dimensions"].items():
rename_var_or_dim(rootgrp, old_name, new_name, is_var=False, verbose=verbose)
Comment thread
aidanheerdegen marked this conversation as resolved.

# Add metadata to matching variables
if "variables" in metadict:
for var, attr_dict in metadict["variables"].items():
Expand Down Expand Up @@ -166,7 +178,24 @@ def array_to_csv(array):
else:
return f.getvalue()

def set_attribute(group, attribute, value, template_vars, verbose=False, var=None,):
def rename_var_or_dim(group, old_name, new_name, is_var=True, verbose=False):
"""
Rename a variable or dimensions in group from old_name to new_name.
Will try to rename a variable if is_var=True, otherwise will try to rename a
dimension
"""
s = "variable" if is_var else "dimension"
try:
if is_var:
group.renameVariable(old_name, new_name)
else:
group.renameDimension(old_name, new_name)

if verbose: print(f" ~ renamed {s} \"{old_name}\" to \"{new_name}\".")
except KeyError:
if verbose: print(f" ~ {s} \"{old_name}\" not found, can't rename to \"{new_name}\"")

def set_attribute(group, attribute, value, template_vars, verbose=False, var=None):
"""
Small wrapper to select, delete, or set attribute depending
on value passed and expand jinja template variables
Expand Down
132 changes: 132 additions & 0 deletions test/test_rename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env python

"""
Copyright 2025 ACCESS-NRI

author: Joshua Torrance <[email protected]>

Licensed under the Apache License, Version 2.0 (the "License"),
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import pytest
import netCDF4
import yaml

from common import runcmd, make_nc

def get_var_names(ncfile):
with netCDF4.Dataset(ncfile, 'r') as ds:
return ds.variables.keys()

def get_dim_names(ncfile):
with netCDF4.Dataset(ncfile, 'r') as ds:
return ds.dimensions.keys()

def get_names(ncfile, var_or_dim):
if var_or_dim == "variable":
return get_var_names(ncfile)
else:
return get_dim_names(ncfile)

@pytest.mark.parametrize(
"name_tuples",
[
[("Times", "time")],
[("Times", "times")],
[("Times", "quiteadifferentname99")],
[("Times", "_underscore")],
[("Times", "time"), ("temp", "temperature")],
]
)
def test_rename_vars(tmp_path, make_nc, name_tuples):
"""
Rename a list of variables given as a list of (old_name, new_name)
"""
d = {
"rename": {
"variables": {k: v for k, v in name_tuples},
}
}

meta_path = tmp_path / "meta.yaml"
with open(meta_path, "w") as f:
yaml.dump(d, f)

runcmd(f"addmeta -m {meta_path} {make_nc}")

var_names = get_var_names(make_nc)

for old_name, new_name in name_tuples:
assert old_name not in var_names
assert new_name in var_names

@pytest.mark.parametrize(
"name_tuples",
[
[("x", "ex")],
[("y", "thisisaverylongdimname")],
[("Times", "times")],
[("x", "_underscore")],
[("x", "ex"), ("Times", "time"), ("y", "why")],
]
)
def test_rename_dims(tmp_path, make_nc, name_tuples):
"""
Rename a list of dims given as a list of (old_name, new_name)
"""
d = {
"rename": {
"dimensions": {k: v for k, v in name_tuples},
}
}

meta_path = tmp_path / "meta.yaml"
with open(meta_path, "w") as f:
yaml.dump(d, f)

runcmd(f"addmeta -m {meta_path} {make_nc}")

dim_names = get_dim_names(make_nc)

for old_name, new_name in name_tuples:
assert old_name not in dim_names
assert new_name in dim_names

@pytest.mark.parametrize(
"var_or_dim", ["variable", "dimension"]
)
def test_rename_var_dim_missing(tmp_path, make_nc, var_or_dim):
"""
Confirm that trying to rename a variable or dimension that doesn't exist
does not fail.
"""
old_name = "thisdoesntexist"
new_name = "newname"
d = {
"rename": {
f"{var_or_dim}s": {
old_name: new_name
},
}
}

meta_path = tmp_path / "meta.yaml"
with open(meta_path, "w") as f:
yaml.dump(d, f)

runcmd(f"addmeta -m {meta_path} {make_nc}")

names = get_names(make_nc, var_or_dim)

assert old_name not in names
assert new_name not in names
Loading