Skip to content

Commit bd4ac95

Browse files
authored
Add alpha versions of diag_tool and libdiagtable (NOAA-GFDL#68)
Add beta versions of `libdiagtable` and `diag_tool`
1 parent 1223851 commit bd4ac95

File tree

4 files changed

+953
-0
lines changed

4 files changed

+953
-0
lines changed

fms_yaml_tools/diag_table/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from fms_yaml_tools.diag_table.libdiagtable import (DiagTable, DiagTableFile, DiagTableVar, DiagTableSubRegion,
2+
DiagTableError, abstract_dict)
+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python
2+
# ***********************************************************************
3+
# * GNU Lesser General Public License
4+
# *
5+
# * This file is part of the GFDL Flexible Modeling System (FMS) YAML
6+
# * tools.
7+
# *
8+
# * FMS_yaml_tools is free software: you can redistribute it and/or
9+
# * modify it under the terms of the GNU Lesser General Public License
10+
# * as published by the Free Software Foundation, either version 3 of the
11+
# * License, or (at your option) any later version.
12+
# *
13+
# * FMS_yaml_tools is distributed in the hope that it will be useful, but
14+
# * WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
# * General Public License for more details.
17+
# *
18+
# * You should have received a copy of the GNU Lesser General Public
19+
# * License along with FMS. If not, see <http://www.gnu.org/licenses/>.
20+
# ***********************************************************************
21+
22+
import click
23+
from fms_yaml_tools.diag_table import DiagTable, DiagTableFile, DiagTableVar, DiagTableError, abstract_dict
24+
25+
26+
def echo(msg):
27+
"""Print a message to STDERR"""
28+
click.echo(msg, err=True)
29+
30+
31+
def get_editor():
32+
"""Get the user's preferred editor as per the VISUAL or EDITOR env variable. Redirect the editor's STDOUT to STDERR,
33+
so that diag-tool's own STDOUT can be redirected or piped by the user"""
34+
from click._termui_impl import Editor
35+
return "1>&2 " + Editor().get_editor()
36+
37+
38+
def apply_filters(obj):
39+
"""Apply file and var filters, then prune empty files if desired"""
40+
if type(obj) is DiagTable:
41+
obj = obj.filter_files(options["file"])
42+
43+
if type(obj) in (DiagTable, DiagTableFile):
44+
obj = obj.filter_vars(options["var"])
45+
46+
if type(obj) is DiagTable and options["prune"]:
47+
obj = obj.prune()
48+
49+
return obj
50+
51+
52+
def get_filtered_table_obj(yaml):
53+
"""Load a DiagTable object from a filename, then apply file and var filters to it"""
54+
return apply_filters(DiagTable.from_file(yaml))
55+
56+
57+
def write_out(yaml, obj):
58+
"""Write obj to standard output or to the YAML file provided, depending on the `in_place` option"""
59+
if options["in_place"]:
60+
if yaml == "-":
61+
echo("Warning: Ignoring --in-place option, because the original table was read from standard input")
62+
else:
63+
if not (options["force"] or click.confirm("Overwrite {:s}?".format(click.format_filename(yaml)))):
64+
echo("Changes have been discarded")
65+
return
66+
else:
67+
yaml = "-"
68+
69+
obj.write(yaml, options["abstract"])
70+
71+
72+
def get_filtered_files(table_obj):
73+
"""Get an iterator over the table's files after file filters are applied"""
74+
return table_obj.get_filtered_files(options["file"])
75+
76+
77+
def get_filtered_vars(table_obj):
78+
"""Get an iterator over the table's variables after file and variable filters are applied"""
79+
return table_obj.filter_files(options["file"]).get_filtered_vars(options["var"])
80+
81+
82+
def merge_generic(yamls, combine_func):
83+
"""Perform either a symmetric or asymmetric merge according to `combine_func`"""
84+
if len(yamls) == 0:
85+
echo("At least one YAML argument is required")
86+
return
87+
elif len(yamls) == 1:
88+
yamls = ["-", yamls[0]]
89+
else:
90+
yamls = list(yamls)
91+
92+
if len(options["var"]) > 0:
93+
cls = DiagTableVar
94+
lhs_iter = get_filtered_vars
95+
elif len(options["file"]) > 0:
96+
cls = DiagTableFile
97+
lhs_iter = get_filtered_files
98+
else:
99+
cls = DiagTable
100+
lhs_iter = lambda table_obj: (yield table_obj)
101+
102+
try:
103+
yaml = yamls.pop()
104+
diag_table_obj = DiagTable.from_file(yaml)
105+
106+
while len(yamls) > 0:
107+
rhs = cls.from_file(yamls.pop())
108+
for lhs in lhs_iter(diag_table_obj):
109+
combine_func(lhs, rhs)
110+
111+
write_out(yaml, diag_table_obj)
112+
except DiagTableError as err:
113+
echo(err)
114+
115+
116+
def yaml_str_from_file(yaml):
117+
"""Read a YAML string from a file"""
118+
with click.open_file(yaml, "r") as fh:
119+
return fh.read()
120+
121+
122+
def get_obj_generic(yaml):
123+
"""Get a DiagTable, DiagTableFile, or DiagTableVar object from a YAML file"""
124+
yaml_str = yaml_str_from_file(yaml)
125+
for cls in (DiagTable, DiagTableFile, DiagTableVar):
126+
try:
127+
return cls.from_yaml_str(yaml_str)
128+
except DiagTableError:
129+
pass
130+
131+
132+
@click.group()
133+
@click.version_option("alpha1", "-V", "--version")
134+
@click.help_option("-h", "--help")
135+
@click.option("-i", "--in-place", is_flag=True, default=False,
136+
help="Overwrite the existing table, rather than writing to standard output")
137+
@click.option("-F", "--force", is_flag=True, default=False,
138+
help="Skip the confirmation prompt when overwriting an existing table")
139+
@click.option("-f", "--file", type=click.STRING, multiple=True,
140+
help="Apply a file filter, of the form `[~]FILE`, where FILE may be an individual file name or a"
141+
+ " comma-separated list thereof")
142+
@click.option("-v", "--var", type=click.STRING, multiple=True,
143+
help="Apply a variable filter, of the form `[~][FILE:[MODULE:]]VAR`, where FILE, MODULE, and VAR may be"
144+
+ " individual names or comma-separated lists thereof")
145+
@click.option("-p", "--prune", is_flag=True, default=False,
146+
help="Prune files which have no variables after filters are applied")
147+
@click.option("-a", "--abstract", type=click.Choice(("table", "file", "var"), case_sensitive=True), multiple=True,
148+
help="Exclude table, file, or variable attributes from the output")
149+
def diag_tool(in_place, force, file, var, prune, abstract):
150+
"""Utility to update, merge, subset, or summarize diag YAMLs"""
151+
global options
152+
options = {
153+
"in_place": in_place,
154+
"force": force,
155+
"file": file,
156+
"var": var,
157+
"prune": prune,
158+
"abstract": abstract_dict(abstract)
159+
}
160+
161+
162+
@diag_tool.command(name="edit")
163+
@click.help_option("-h", "--help")
164+
@click.argument("yaml", type=click.Path(), default="-")
165+
def edit_cmd(yaml):
166+
"""Edit a table interactively, then merge the changes back in"""
167+
try:
168+
obj = get_obj_generic(yaml)
169+
170+
if obj is None:
171+
echo("{:s} was not a valid table, file, or variable... exiting".format(yaml))
172+
return
173+
174+
yaml_str_0 = apply_filters(obj).dump_yaml(options["abstract"])
175+
yaml_str_1 = click.edit(yaml_str_0, editor=get_editor(), extension=".yaml")
176+
177+
if yaml_str_1:
178+
cls = obj.__class__
179+
obj |= cls.from_yaml_str(yaml_str_1)
180+
else:
181+
if options["in_place"]:
182+
echo("No changes were made... exiting without modifying '{}'".format(yaml))
183+
return
184+
else:
185+
echo("No changes were made... passing original table through")
186+
187+
options["abstract"] = {}
188+
write_out(yaml, obj)
189+
except DiagTableError as err:
190+
echo(err)
191+
192+
193+
@diag_tool.command(name="update")
194+
@click.help_option("-h", "--help")
195+
@click.argument("yamls", type=click.Path(), nargs=-1)
196+
def update_cmd(yamls):
197+
"""Update a table or its files/variables"""
198+
def combine_func(lhs, rhs):
199+
lhs |= rhs
200+
merge_generic(yamls, combine_func)
201+
202+
203+
@diag_tool.command(name="merge")
204+
@click.help_option("-h", "--help")
205+
@click.argument("yamls", type=click.Path(), nargs=-1)
206+
def merge_cmd(yamls):
207+
"""Symmetrically merge tables, failing if any conflicts occur"""
208+
def combine_func(lhs, rhs):
209+
lhs += rhs
210+
merge_generic(yamls, combine_func)
211+
212+
213+
@diag_tool.command(name="filter")
214+
@click.help_option("-h", "--help")
215+
@click.argument("yaml", type=click.Path(), default="-")
216+
def filter_cmd(yaml):
217+
"""Apply file or variable filters to a table"""
218+
try:
219+
diag_table_obj = get_filtered_table_obj(yaml)
220+
write_out(yaml, diag_table_obj)
221+
except DiagTableError as err:
222+
echo(err)
223+
224+
225+
@diag_tool.command(name="list")
226+
@click.help_option("-h", "--help")
227+
@click.argument("yaml", type=click.Path(), default="-")
228+
def list_cmd(yaml):
229+
"""List the files and variables in a table"""
230+
options["abstract"] = abstract_dict(("table", "file", "var")) | options["abstract"]
231+
232+
try:
233+
diag_table_obj = get_filtered_table_obj(yaml)
234+
write_out(yaml, diag_table_obj)
235+
except DiagTableError as err:
236+
echo(err)
237+
238+
239+
@diag_tool.command(name="pick")
240+
@click.help_option("-h", "--help")
241+
@click.argument("yaml", type=click.Path(), default="-")
242+
def pick_cmd(yaml):
243+
"""Pick a single file or variable from a table"""
244+
if len(options["var"]) > 0:
245+
noun = "variable"
246+
pick_func = get_filtered_vars
247+
elif len(options["file"]) > 0:
248+
noun = "file"
249+
pick_func = get_filtered_files
250+
else:
251+
echo("`diag-tool pick` requires a file or variable filter to be provided")
252+
return
253+
254+
try:
255+
diag_table_obj = DiagTable.from_file(yaml)
256+
picked_objs = tuple(pick_func(diag_table_obj))
257+
n = len(picked_objs)
258+
259+
if n == 0:
260+
echo("No {:s} was found matching the filter criteria".format(noun))
261+
return
262+
elif n > 1:
263+
echo("Selecting the first out of {:d} {:s}s which satisfy the filter criteria".format(n, noun))
264+
265+
write_out(yaml, picked_objs[0])
266+
except DiagTableError as err:
267+
echo(err)
268+
269+
270+
if __name__ == "__main__":
271+
diag_tool()

0 commit comments

Comments
 (0)