xconv2 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xconv2-0.1.0a1/LICENSE +21 -0
- xconv2-0.1.0a1/PKG-INFO +16 -0
- xconv2-0.1.0a1/README.md +46 -0
- xconv2-0.1.0a1/pyproject.toml +41 -0
- xconv2-0.1.0a1/setup.cfg +4 -0
- xconv2-0.1.0a1/tests/test_cf_interface.py +352 -0
- xconv2-0.1.0a1/tests/test_contour.py +248 -0
- xconv2-0.1.0a1/tests/test_coordinate_flow.py +309 -0
- xconv2-0.1.0a1/tests/test_lineplot.py +223 -0
- xconv2-0.1.0a1/tests/test_load_selected_file.py +114 -0
- xconv2-0.1.0a1/tests/test_worker_save_script.py +126 -0
- xconv2-0.1.0a1/xconv2/__init__.py +24 -0
- xconv2-0.1.0a1/xconv2/assets/Pyfive-logo.png +0 -0
- xconv2-0.1.0a1/xconv2/assets/UoR-logo.png +0 -0
- xconv2-0.1.0a1/xconv2/assets/cf-logo-t.svg +30367 -0
- xconv2-0.1.0a1/xconv2/assets/cf-logo.svg +30375 -0
- xconv2-0.1.0a1/xconv2/assets/dask-logo.svg +13 -0
- xconv2-0.1.0a1/xconv2/assets/ncas-logo.png +0 -0
- xconv2-0.1.0a1/xconv2/assets/under-construction.svg +9 -0
- xconv2-0.1.0a1/xconv2/cell_method_handler.py +92 -0
- xconv2-0.1.0a1/xconv2/cf_templates.py +154 -0
- xconv2-0.1.0a1/xconv2/colour_scales.py +43 -0
- xconv2-0.1.0a1/xconv2/core_window.py +1059 -0
- xconv2-0.1.0a1/xconv2/gui.py +43 -0
- xconv2-0.1.0a1/xconv2/lineplot.py +175 -0
- xconv2-0.1.0a1/xconv2/main_window.py +482 -0
- xconv2-0.1.0a1/xconv2/ui/__init__.py +5 -0
- xconv2-0.1.0a1/xconv2/ui/contour_options_controller.py +671 -0
- xconv2-0.1.0a1/xconv2/ui/dialogs.py +154 -0
- xconv2-0.1.0a1/xconv2/ui/field_metadata_controller.py +277 -0
- xconv2-0.1.0a1/xconv2/ui/menu_controller.py +146 -0
- xconv2-0.1.0a1/xconv2/ui/plot_view_controller.py +390 -0
- xconv2-0.1.0a1/xconv2/ui/selection_controller.py +414 -0
- xconv2-0.1.0a1/xconv2/ui/settings_store.py +204 -0
- xconv2-0.1.0a1/xconv2/worker.py +309 -0
- xconv2-0.1.0a1/xconv2/xconv_cf_interface.py +538 -0
- xconv2-0.1.0a1/xconv2.egg-info/PKG-INFO +16 -0
- xconv2-0.1.0a1/xconv2.egg-info/SOURCES.txt +40 -0
- xconv2-0.1.0a1/xconv2.egg-info/dependency_links.txt +1 -0
- xconv2-0.1.0a1/xconv2.egg-info/entry_points.txt +5 -0
- xconv2-0.1.0a1/xconv2.egg-info/requires.txt +10 -0
- xconv2-0.1.0a1/xconv2.egg-info/top_level.txt +1 -0
xconv2-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NCAS University of Reading
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xconv2-0.1.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xconv2
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: High-performance data viewer and simple data converter
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: PySide6
|
|
8
|
+
Requires-Dist: superqt
|
|
9
|
+
Requires-Dist: cf-python
|
|
10
|
+
Requires-Dist: cf-plot
|
|
11
|
+
Requires-Dist: zarr
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: setuptools>80
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
xconv2-0.1.0a1/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# xconv2
|
|
2
|
+
|
|
3
|
+
xconv2 provides (or will provide) a graphical interface to local and remote weather and climate data. It is a replacement for the venerable [xconv](https://ncas-cms.github.io/xconv-doc/html/index.html) package that has been supported by NCAS CMS for decades.
|
|
4
|
+
|
|
5
|
+
<table>
|
|
6
|
+
<tr>
|
|
7
|
+
<td style="vertical-align: middle; padding: 2px;">
|
|
8
|
+
<img src="xconv2/assets/under-construction.svg" alt="Under construction icon" height="128px" />
|
|
9
|
+
</td>
|
|
10
|
+
<td style="vertical-align: middle;">
|
|
11
|
+
This version is under development. If you encounter something that looks like it should work, but doesn't, please
|
|
12
|
+
raise a bug issue at the address found in the About menu. If there is function that you would like to see, feel
|
|
13
|
+
free to raise an enhancement request at the same location.
|
|
14
|
+
</td>
|
|
15
|
+
</tr>
|
|
16
|
+
</table>
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
We intend to release this as a standalone executable for linux and macos, but for the moment you should use a Python 3.12 (or later) environment with
|
|
21
|
+
udunits2 installed, then you can pip install from the source using one of these:
|
|
22
|
+
|
|
23
|
+
`pip install "git+https://github.com/ncas-cms/xconv2.git"`
|
|
24
|
+
`pip install "git+ssh://github.com/ncas-cms/xconv2.git"`
|
|
25
|
+
|
|
26
|
+
and then you should have `xconv2` available on your command line.
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
No user documentation is yet available, but it is designed to be intuitive
|
|
31
|
+
and basic usage should be possible without documentation once it is installed.
|
|
32
|
+
|
|
33
|
+
### Developer Documentation
|
|
34
|
+
|
|
35
|
+
Limited developer documentation is available. For the moment it is
|
|
36
|
+
limited to the following:
|
|
37
|
+
|
|
38
|
+
#### UML Diagrams
|
|
39
|
+
|
|
40
|
+
- `docs/uml/alpha_core_window.puml`
|
|
41
|
+
- `docs/uml/core_window_gui_worker_signals.puml`
|
|
42
|
+
- `docs/uml/core_window_options_sequence.puml`
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
This project is licensed under the MIT License. See `LICENSE`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xconv2"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "High-performance data viewer and simple data converter"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"PySide6",
|
|
12
|
+
"superqt",
|
|
13
|
+
"cf-python",
|
|
14
|
+
"cf-plot",
|
|
15
|
+
"zarr",
|
|
16
|
+
"pandas",
|
|
17
|
+
"setuptools>80",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
test = [
|
|
22
|
+
"pytest",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.gui-scripts]
|
|
26
|
+
# This creates an 'xconv2' command that launches your GUI
|
|
27
|
+
xconv2 = "xconv2.gui:main"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
# This creates a 'cf-worker' command for the backend process
|
|
31
|
+
cf-worker = "xconv2.worker:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["."]
|
|
35
|
+
include = ["xconv2*"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
xconv2 = ["assets/*.svg", "assets/*.png"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
xconv2-0.1.0a1/setup.cfg
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import xconv2.xconv_cf_interface as cf_interface
|
|
5
|
+
|
|
6
|
+
from xconv2.xconv_cf_interface import (
|
|
7
|
+
auto_contour_title,
|
|
8
|
+
coordinate_info,
|
|
9
|
+
field_info,
|
|
10
|
+
get_data_for_plotting,
|
|
11
|
+
run_contour_plot,
|
|
12
|
+
run_line_plot,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
cf = pytest.importorskip("cf")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _MockCellMeasures:
|
|
22
|
+
def __call__(self) -> str:
|
|
23
|
+
return "cell_measures: area: areacella"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _MockField:
|
|
27
|
+
shape = (2, 3)
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return "mock-field-summary"
|
|
31
|
+
|
|
32
|
+
def identity(self) -> str:
|
|
33
|
+
return "air_temperature"
|
|
34
|
+
|
|
35
|
+
def properties(self) -> dict[str, str]:
|
|
36
|
+
return {"units": "K", "standard_name": "air_temperature"}
|
|
37
|
+
|
|
38
|
+
def coordinates(self) -> dict[str, str]:
|
|
39
|
+
return {
|
|
40
|
+
"dimensioncoordinate0": "Dimension coordinate: latitude(2) degrees_north",
|
|
41
|
+
"dimensioncoordinate1": "Dimension coordinate: longitude(3) degrees_east",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def cell_methods(self) -> str:
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
def cell_measures(self) -> str:
|
|
48
|
+
return _MockCellMeasures()()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_field_info_returns_serialized_rows() -> None:
|
|
52
|
+
payload = field_info([_MockField()])
|
|
53
|
+
|
|
54
|
+
assert isinstance(payload, list)
|
|
55
|
+
assert len(payload) == 1
|
|
56
|
+
assert isinstance(payload[0], str)
|
|
57
|
+
|
|
58
|
+
parts = payload[0].split("\x1f", 2)
|
|
59
|
+
assert len(parts) == 3
|
|
60
|
+
assert parts[0].startswith("air_temperature")
|
|
61
|
+
assert parts[1] == "mock-field-summary"
|
|
62
|
+
assert "units" in parts[2]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _MockCoord:
|
|
66
|
+
def __init__(self, name: str, values: list[object]) -> None:
|
|
67
|
+
self._name = name
|
|
68
|
+
self.array = values
|
|
69
|
+
|
|
70
|
+
def identity(self, default: str = "unknown") -> str:
|
|
71
|
+
return self._name or default
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _MockCoordField:
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self._coords = {
|
|
77
|
+
"time": _MockCoord("time", [1, 2, 3]),
|
|
78
|
+
"height": _MockCoord("height", [10]),
|
|
79
|
+
"lat": _MockCoord("latitude", [-90, 0, 90]),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def dimension_coordinates(self, **kwargs) -> list[str]:
|
|
83
|
+
return self._coords
|
|
84
|
+
|
|
85
|
+
def coordinate(self, key: str) -> _MockCoord:
|
|
86
|
+
return self._coords[key]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_coordinate_info_filters_singletons_and_serializes_values() -> None:
|
|
90
|
+
payload = coordinate_info(_MockCoordField())
|
|
91
|
+
|
|
92
|
+
assert payload == [
|
|
93
|
+
("time", ["1", "2", "3"], ""),
|
|
94
|
+
("latitude", ["-90", "0", "90"], ""),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _FakePlotField:
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
self.kwargs: dict[str, object] | None = None
|
|
101
|
+
self.collapse_calls: list[tuple[str, str, bool]] = []
|
|
102
|
+
|
|
103
|
+
def subspace(self, **kwargs: object) -> "_FakePlotField":
|
|
104
|
+
self.kwargs = kwargs
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def dimension_coordinate(self, coord_name) -> None:
|
|
108
|
+
return np.array([0.0, 1.0])
|
|
109
|
+
|
|
110
|
+
def collapse(self, instruction: str, weights: bool = False) -> "_FakePlotField":
|
|
111
|
+
self.collapse_calls.append((instruction, weights))
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_get_data_for_plotting_builds_subspace_kwargs() -> None:
|
|
116
|
+
fld = _FakePlotField()
|
|
117
|
+
|
|
118
|
+
pfld = get_data_for_plotting(
|
|
119
|
+
fld,
|
|
120
|
+
{
|
|
121
|
+
"time": ("3", "1"),
|
|
122
|
+
"level": ("850", "850"),
|
|
123
|
+
"name": ("foo", "foo"),
|
|
124
|
+
},
|
|
125
|
+
{"time": "mean", "name": "max"},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert pfld is fld
|
|
129
|
+
assert fld.kwargs is not None
|
|
130
|
+
assert fld.kwargs["level"] == 850
|
|
131
|
+
assert fld.kwargs["name"] == "foo"
|
|
132
|
+
assert str(fld.kwargs["time"]) == str(cf.wi(1, 3))
|
|
133
|
+
assert fld.collapse_calls == [
|
|
134
|
+
("time: mean name: max", True),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _FakeCFPlot:
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
self.levs_calls: list[dict[str, object]] = []
|
|
141
|
+
self.con_calls: list[dict[str, object]] = []
|
|
142
|
+
self.lineplot_calls: list[dict[str, object]] = []
|
|
143
|
+
self.cscale_calls: list[dict[str, object]] = []
|
|
144
|
+
self.gopen_calls: list[dict[str, object]] = []
|
|
145
|
+
self.setvars_calls: list[dict[str, object]] = []
|
|
146
|
+
self.gclose_calls = 0
|
|
147
|
+
|
|
148
|
+
def levs(self, **kwargs: object) -> None:
|
|
149
|
+
self.levs_calls.append(kwargs)
|
|
150
|
+
|
|
151
|
+
def con(self, _field: object, **kwargs: object) -> None:
|
|
152
|
+
self.con_calls.append(kwargs)
|
|
153
|
+
|
|
154
|
+
def lineplot(self, _field: object, **kwargs: object) -> None:
|
|
155
|
+
self.lineplot_calls.append(kwargs)
|
|
156
|
+
|
|
157
|
+
def cscale(self, **kwargs: object) -> None:
|
|
158
|
+
self.cscale_calls.append(kwargs)
|
|
159
|
+
|
|
160
|
+
def gopen(self, file: str = "cfplot.png", **kwargs: object) -> None:
|
|
161
|
+
payload: dict[str, object] = {"file": file}
|
|
162
|
+
payload.update(kwargs)
|
|
163
|
+
self.gopen_calls.append(payload)
|
|
164
|
+
|
|
165
|
+
def setvars(self, **kwargs: object) -> None:
|
|
166
|
+
self.setvars_calls.append(kwargs)
|
|
167
|
+
|
|
168
|
+
def gclose(self) -> None:
|
|
169
|
+
self.gclose_calls += 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _FakeFigure:
|
|
173
|
+
def __init__(self) -> None:
|
|
174
|
+
self.text_calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
|
|
175
|
+
self.suptitle_calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
|
|
176
|
+
|
|
177
|
+
def text(self, *args: object, **kwargs: object) -> None:
|
|
178
|
+
self.text_calls.append((args, kwargs))
|
|
179
|
+
|
|
180
|
+
def suptitle(self, *args: object, **kwargs: object) -> None:
|
|
181
|
+
self.suptitle_calls.append((args, kwargs))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _FakePlt:
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
self.figure = _FakeFigure()
|
|
187
|
+
|
|
188
|
+
def gcf(self) -> _FakeFigure:
|
|
189
|
+
return self.figure
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_run_contour_plot_applies_levels_annotations_and_save(
|
|
193
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
194
|
+
) -> None:
|
|
195
|
+
cfp = _FakeCFPlot()
|
|
196
|
+
plt_obj = _FakePlt()
|
|
197
|
+
|
|
198
|
+
monkeypatch.setattr(cf_interface, "cfp", cfp)
|
|
199
|
+
monkeypatch.setattr(cf_interface, "plt", plt_obj)
|
|
200
|
+
|
|
201
|
+
run_contour_plot(
|
|
202
|
+
pfld=object(),
|
|
203
|
+
options={
|
|
204
|
+
"mode": "explicit",
|
|
205
|
+
"levels": [-1.0, 0.0, 1.0],
|
|
206
|
+
"cscale": "magma",
|
|
207
|
+
"filename": "/tmp/mock.png",
|
|
208
|
+
"annotation_display": True,
|
|
209
|
+
"annotation_properties": [("units", "K")],
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
assert cfp.cscale_calls == [{"scale": "magma"}]
|
|
214
|
+
assert cfp.gopen_calls == [{"file": "/tmp/mock.png"}]
|
|
215
|
+
assert cfp.levs_calls == [{"manual": [-1.0, 0.0, 1.0]}]
|
|
216
|
+
assert cfp.setvars_calls == [{"title_fontsize": 10.5}]
|
|
217
|
+
assert cfp.con_calls
|
|
218
|
+
assert cfp.gclose_calls == 1
|
|
219
|
+
assert plt_obj.figure.text_calls
|
|
220
|
+
assert plt_obj.figure.text_calls[-1][1]["fontsize"] == 8.0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_run_contour_plot_uses_configured_title_font_sizes(
|
|
224
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
225
|
+
) -> None:
|
|
226
|
+
cfp = _FakeCFPlot()
|
|
227
|
+
plt_obj = _FakePlt()
|
|
228
|
+
|
|
229
|
+
monkeypatch.setattr(cf_interface, "cfp", cfp)
|
|
230
|
+
monkeypatch.setattr(cf_interface, "plt", plt_obj)
|
|
231
|
+
|
|
232
|
+
run_contour_plot(
|
|
233
|
+
pfld=object(),
|
|
234
|
+
options={
|
|
235
|
+
"mode": "default",
|
|
236
|
+
"page_title": "Overview",
|
|
237
|
+
"page_title_display": True,
|
|
238
|
+
"annotation_display": True,
|
|
239
|
+
"annotation_properties": [("units", "K")],
|
|
240
|
+
"contour_title_fontsize": 12.5,
|
|
241
|
+
"page_title_fontsize": 14.0,
|
|
242
|
+
"annotation_fontsize": 9.5,
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert cfp.setvars_calls == [{"title_fontsize": 12.5}]
|
|
247
|
+
assert plt_obj.figure.suptitle_calls == [
|
|
248
|
+
(("Overview",), {"y": 0.995, "fontsize": 14.0})
|
|
249
|
+
]
|
|
250
|
+
assert plt_obj.figure.text_calls == [
|
|
251
|
+
((0.5, 0.02, "units: K"), {"ha": "center", "va": "bottom", "fontsize": 9.5})
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_run_contour_plot_sets_title_from_singleton_selection(
|
|
256
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
257
|
+
) -> None:
|
|
258
|
+
cfp = _FakeCFPlot()
|
|
259
|
+
plt_obj = _FakePlt()
|
|
260
|
+
|
|
261
|
+
monkeypatch.setattr(cf_interface, "cfp", cfp)
|
|
262
|
+
monkeypatch.setattr(cf_interface, "plt", plt_obj)
|
|
263
|
+
|
|
264
|
+
run_contour_plot(
|
|
265
|
+
pfld=object(),
|
|
266
|
+
options={"mode": "default"},
|
|
267
|
+
selection_spec={"time": ("2000-01-01", "2000-01-01"), "lat": ("-90", "90")},
|
|
268
|
+
collapse_by_coord={},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
assert cfp.gopen_calls == [{"file": "cfplot.png", "user_plot": 1}]
|
|
272
|
+
assert cfp.con_calls
|
|
273
|
+
assert cfp.con_calls[-1]["title"] == "time=2000-01-01"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_run_contour_plot_prefers_cell_method_title_for_collapses(
|
|
277
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
278
|
+
) -> None:
|
|
279
|
+
cfp = _FakeCFPlot()
|
|
280
|
+
plt_obj = _FakePlt()
|
|
281
|
+
|
|
282
|
+
monkeypatch.setattr(cf_interface, "cfp", cfp)
|
|
283
|
+
monkeypatch.setattr(cf_interface, "plt", plt_obj)
|
|
284
|
+
monkeypatch.setattr(cf_interface, "cell_methods_string_from_field", lambda _field, *args: "time: mean")
|
|
285
|
+
|
|
286
|
+
run_contour_plot(
|
|
287
|
+
pfld=object(),
|
|
288
|
+
options={"mode": "default"},
|
|
289
|
+
selection_spec={"time": ("2000-01-01", "2000-12-31")},
|
|
290
|
+
collapse_by_coord={"time": "mean"},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
assert cfp.con_calls
|
|
294
|
+
assert cfp.con_calls[-1]["title"] == "time: mean"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_auto_contour_title_from_singleton_selection() -> None:
|
|
298
|
+
title = auto_contour_title(
|
|
299
|
+
pfld=object(),
|
|
300
|
+
selection_spec={"time": ("2001-01-01", "2001-01-01"), "lat": ("-90", "90")},
|
|
301
|
+
collapse_by_coord={},
|
|
302
|
+
)
|
|
303
|
+
assert title == "time=2001-01-01"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_auto_contour_title_prefers_cell_method_for_collapse(
|
|
307
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
308
|
+
) -> None:
|
|
309
|
+
monkeypatch.setattr(cf_interface, "cell_methods_string_from_field", lambda _field, *args: "time: mean")
|
|
310
|
+
title = auto_contour_title(
|
|
311
|
+
pfld=object(),
|
|
312
|
+
selection_spec={"time": ("2001-01-01", "2001-12-31")},
|
|
313
|
+
collapse_by_coord={"time": "mean"},
|
|
314
|
+
)
|
|
315
|
+
assert title == "time: mean"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_run_line_plot_uses_canonical_axes_and_wraps_file_output() -> None:
|
|
319
|
+
field_eg = object()
|
|
320
|
+
monkeypatch = pytest.MonkeyPatch()
|
|
321
|
+
|
|
322
|
+
captured: dict[str, object] = {}
|
|
323
|
+
|
|
324
|
+
class _FakeLinePlot:
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
pfld: object,
|
|
328
|
+
options: dict[str, object] | None,
|
|
329
|
+
collapse_by_coord: dict[str, str] | None
|
|
330
|
+
) -> None:
|
|
331
|
+
captured["pfld"] = pfld
|
|
332
|
+
captured["options"] = options
|
|
333
|
+
|
|
334
|
+
def render(self) -> None:
|
|
335
|
+
captured["rendered"] = True
|
|
336
|
+
|
|
337
|
+
monkeypatch.setattr(cf_interface, "LinePlot", _FakeLinePlot)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
run_line_plot(
|
|
341
|
+
pfld=field_eg,
|
|
342
|
+
options={"filename": "/tmp/line.png", "title": "line"},
|
|
343
|
+
selection_spec={"time": ("1", "2")},
|
|
344
|
+
collapse_by_coord={},
|
|
345
|
+
)
|
|
346
|
+
finally:
|
|
347
|
+
monkeypatch.undo()
|
|
348
|
+
|
|
349
|
+
assert captured["pfld"] is field_eg
|
|
350
|
+
assert captured["options"] == {"filename": "/tmp/line.png", "title": "line"}
|
|
351
|
+
assert captured["rendered"] is True
|
|
352
|
+
|