nctoolkit 1.3.6__tar.gz → 1.3.7__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.
Files changed (87) hide show
  1. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/PKG-INFO +1 -1
  2. nctoolkit-1.3.7/nctoolkit/__init__.py +124 -0
  3. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/cleanup.py +32 -28
  4. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/nco_command.py +32 -24
  5. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/runners.py +40 -53
  6. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/runthis.py +14 -4
  7. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/session.py +43 -0
  8. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/setup.py +1 -1
  9. nctoolkit-1.3.6/nctoolkit/__init__.py +0 -50
  10. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/LICENSE +0 -0
  11. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/MANIFEST.in +0 -0
  12. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/README.md +0 -0
  13. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/add_etc.py +0 -0
  14. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/anomaly.py +0 -0
  15. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/api.py +0 -0
  16. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/append.py +0 -0
  17. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/assign.py +0 -0
  18. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/cdo_command.py +0 -0
  19. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/cellareas.py +0 -0
  20. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/centres.py +0 -0
  21. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/checks.py +0 -0
  22. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/clear.py +0 -0
  23. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/compare.py +0 -0
  24. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/compare_data.py +0 -0
  25. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/corr.py +0 -0
  26. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/create_ensemble.py +0 -0
  27. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/crop.py +0 -0
  28. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/deprecated.py +0 -0
  29. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/distgrid.py +0 -0
  30. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/drop.py +0 -0
  31. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/ensembles.py +0 -0
  32. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/esoteric.py +0 -0
  33. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/experimental.py +0 -0
  34. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/fill.py +0 -0
  35. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/flatten.py +0 -0
  36. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/fldstat.py +0 -0
  37. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/format.py +0 -0
  38. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/generate_grid.py +0 -0
  39. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/inttime.py +0 -0
  40. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/invert.py +0 -0
  41. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/masking.py +0 -0
  42. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/matchpoint.py +0 -0
  43. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/mergers.py +0 -0
  44. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/meridonials.py +0 -0
  45. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/mp_adders.py +0 -0
  46. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/mp_matchers.py +0 -0
  47. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/mp_matchups.py +0 -0
  48. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/phenology.py +0 -0
  49. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/plot.py +0 -0
  50. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/reduce.py +0 -0
  51. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/reduce_grid.py +0 -0
  52. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/regrid.py +0 -0
  53. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/remove.py +0 -0
  54. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/rename.py +0 -0
  55. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/resample.py +0 -0
  56. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/rollstat.py +0 -0
  57. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/run.py +0 -0
  58. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/set.py +0 -0
  59. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/setters.py +0 -0
  60. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/shape.py +0 -0
  61. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/shift.py +0 -0
  62. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/show.py +0 -0
  63. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/split.py +0 -0
  64. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/static_plot.py +0 -0
  65. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/strip_vars.py +0 -0
  66. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/subset.py +0 -0
  67. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/sumall.py +0 -0
  68. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/temp_file.py +0 -0
  69. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/temporal_stat.py +0 -0
  70. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/temporals.py +0 -0
  71. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/thresholds.py +0 -0
  72. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/to_lonlat.py +0 -0
  73. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/to_nc.py +0 -0
  74. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/toxarray.py +0 -0
  75. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/tozlev.py +0 -0
  76. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/transect.py +0 -0
  77. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/unify.py +0 -0
  78. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/utils.py +0 -0
  79. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/validator.py +0 -0
  80. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/validator_funs.py +0 -0
  81. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/verticals.py +0 -0
  82. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/zip.py +0 -0
  83. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit/zonals.py +0 -0
  84. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/nctoolkit.egg-info/SOURCES.txt +0 -0
  85. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/pyproject.toml +0 -0
  86. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/requirements.txt +0 -0
  87. {nctoolkit-1.3.6 → nctoolkit-1.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nctoolkit
3
- Version: 1.3.6
3
+ Version: 1.3.7
4
4
  Summary: A Python package for netCDF analysis and post-processing
5
5
  Home-page: https://github.com/pmlmodelling/nctoolkit
6
6
  Author: Robert Wilson
@@ -0,0 +1,124 @@
1
+ from nctoolkit.api import (
2
+ open_data,
3
+ open_geotiff,
4
+ from_xarray,
5
+ merge,
6
+ cor_time,
7
+ cor_space,
8
+ options,
9
+ DataSet,
10
+ open_thredds,
11
+ open_url,
12
+ )
13
+
14
+
15
+ from nctoolkit.unify import unify
16
+ from nctoolkit.shape import open_shape
17
+ from nctoolkit.static_plot import panel_plot
18
+
19
+ from nctoolkit.validator import validator
20
+ from nctoolkit.matchpoint import open_matchpoint
21
+
22
+
23
+ from nctoolkit.cleanup import cleanup, clean_all, deep_clean, temp_check
24
+ from nctoolkit.session import kill_active_cdo
25
+
26
+ import atexit
27
+ import os
28
+ import signal
29
+
30
+ # Forked children (multiprocessing workers, os.fork) inherit these handlers but
31
+ # share the session stamp; only the process that imported nctoolkit may clean
32
+ # up, otherwise a worker exiting would delete the parent's live temp files.
33
+ _owner_pid = os.getpid()
34
+
35
+
36
+ def _clean_all_if_owner():
37
+ if os.getpid() == _owner_pid:
38
+ clean_all()
39
+
40
+
41
+ def _stop_workers():
42
+ """
43
+ Terminate multiprocessing children (e.g. ensemble workers) so the cdo
44
+ processes they are running die with them, before temp files are removed
45
+ """
46
+ import multiprocessing
47
+
48
+ children = multiprocessing.active_children()
49
+ for child in children:
50
+ try:
51
+ child.terminate()
52
+ except Exception:
53
+ pass
54
+ for child in children:
55
+ try:
56
+ child.join(2)
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ atexit.register(_clean_all_if_owner)
62
+
63
+
64
+ def _install_termination_handlers():
65
+ """
66
+ Remove temp files when the process is terminated by SIGTERM or SIGHUP
67
+
68
+ Python only runs atexit handlers on a normal exit, so a plain `kill` would
69
+ otherwise leave temp files behind. The handler cleans up and then
70
+ re-delivers the signal with its previous disposition, so the process still
71
+ terminates as the sender intended. Handlers someone else has already
72
+ installed are chained to rather than replaced.
73
+ """
74
+
75
+ def make_handler(previous):
76
+ def handler(signum, frame):
77
+ kill_active_cdo()
78
+ if os.getpid() == _owner_pid:
79
+ _stop_workers()
80
+ _clean_all_if_owner()
81
+ if callable(previous):
82
+ previous(signum, frame)
83
+ return
84
+ signal.signal(signum, signal.SIG_DFL)
85
+ os.kill(os.getpid(), signum)
86
+
87
+ return handler
88
+
89
+ for name in ("SIGTERM", "SIGHUP"):
90
+ signum = getattr(signal, name, None)
91
+ if signum is None:
92
+ continue
93
+ try:
94
+ previous = signal.getsignal(signum)
95
+ if previous is None or previous == signal.SIG_IGN:
96
+ # not set from Python, or deliberately ignored (e.g. nohup)
97
+ continue
98
+ signal.signal(signum, make_handler(previous))
99
+ except ValueError:
100
+ # signal handlers can only be set from the main thread
101
+ pass
102
+
103
+
104
+ _install_termination_handlers()
105
+
106
+ from nctoolkit.create_ensemble import create_ensemble, glob
107
+ from nctoolkit.session import session_files
108
+ from nctoolkit.show import nc_variables, nc_years, nc_months, nc_times
109
+
110
+ from nctoolkit.utils import validate_version, cdo_version
111
+ from nctoolkit.session import session_info
112
+ from nctoolkit.mp_adders import match_points
113
+
114
+ session_info["cdo"] = cdo_version()
115
+
116
+ try:
117
+ from importlib.metadata import version as _version
118
+ except ImportError:
119
+ from importlib_metadata import version as _version
120
+
121
+ try:
122
+ __version__ = _version("nctoolkit")
123
+ except Exception:
124
+ __version__ = "999"
@@ -12,6 +12,7 @@ from nctoolkit.session import (
12
12
  append_safe,
13
13
  remove_safe,
14
14
  get_safe,
15
+ kill_active_cdo,
15
16
  )
16
17
 
17
18
 
@@ -78,40 +79,43 @@ def cleanup():
78
79
  session_info["temp_dir"] = "/tmp/"
79
80
 
80
81
 
81
- def clean_all(x = None, y = None):
82
+ def clean_all():
82
83
  """
83
84
  Remove all temporary files created by nctoolkit in the present session
84
- """
85
85
 
86
- # Step 1 is to find the files we potentially need to delete
87
- # These are files that we know nctoolkit has either created
88
- # or would attempt to create after
89
- # operation failure
90
- # It also finds temp files generated by ncea that are still on the system
91
- if x is not None:
92
- return None
93
- if y is not None:
94
- return None
95
-
96
- candidates = []
97
- for directory in get_tempdirs():
98
- mylist = [f for f in glob.glob(f"{directory}/*")]
99
- mylist = [f for f in mylist if session_info["stamp"] in f]
100
- for ff in mylist:
101
- candidates.append(ff)
86
+ This runs at interpreter exit and from the SIGTERM/SIGHUP handler, so it
87
+ must not depend on the multiprocessing Manager servers behind the
88
+ parallel-mode session lists: if the whole process group is being killed
89
+ those servers can already be gone. Candidates are therefore found by
90
+ globbing for the session stamp in the usual temp locations as well as in
91
+ any directory nctoolkit has recorded.
92
+ """
102
93
 
103
- candidates = list(set(candidates))
104
- candidates = [x for x in candidates if os.path.exists(x)]
105
- candidates
106
- delete_these = set(candidates)
107
- delete_these = list(delete_these)
94
+ # stop any cdo still writing to a temp file before deleting them
95
+ kill_active_cdo()
108
96
 
109
- # finally, to be ultra-safe, we will make sure all of the
110
- # files to be deleted are in a temporary folder
97
+ stamp = session_info["stamp"]
111
98
 
112
- for dd in delete_these:
113
- if os.path.exists(dd):
114
- nc_remove(dd, deep=True)
99
+ directories = {"/tmp", "/var/tmp", "/usr/tmp"}
100
+ directories.add(str(session_info["temp_dir"]).rstrip("/") or "/")
101
+ try:
102
+ directories.update(d.rstrip("/") for d in get_tempdirs())
103
+ except Exception:
104
+ # dead Manager server (e.g. the process group was killed)
105
+ pass
106
+
107
+ candidates = set()
108
+ for directory in directories:
109
+ candidates.update(glob.glob(f"{directory}/*{stamp}*"))
110
+
111
+ for dd in candidates:
112
+ # fail-safe: only ever delete files nctoolkit named for this session
113
+ if "nctoolkit" not in dd or stamp not in os.path.basename(dd):
114
+ continue
115
+ try:
116
+ os.remove(dd)
117
+ except OSError:
118
+ pass
115
119
 
116
120
 
117
121
  def deep_clean():
@@ -1,12 +1,17 @@
1
1
  import copy
2
- import multiprocessing
3
2
  import warnings
4
3
 
5
4
  from nctoolkit.cleanup import cleanup
6
5
  from nctoolkit.flatten import str_flatten
7
6
  from nctoolkit.runthis import run_nco
8
7
  from nctoolkit.temp_file import temp_file
9
- from nctoolkit.session import remove_safe, session_info, append_safe, get_safe
8
+ from nctoolkit.session import (
9
+ remove_safe,
10
+ session_info,
11
+ append_safe,
12
+ get_safe,
13
+ fork_pool,
14
+ )
10
15
 
11
16
 
12
17
  def nco_command(self, command=None, ensemble=False):
@@ -57,26 +62,34 @@ def nco_command(self, command=None, ensemble=False):
57
62
  new_files = []
58
63
  new_commands = []
59
64
 
60
- if cores > 1:
61
- pool = multiprocessing.get_context("fork").Pool(cores)
62
- target_list = []
63
- results = dict()
64
- else:
65
- target_list = []
65
+ target_list = []
66
66
 
67
67
  if (ensemble is False) or (len(self) == 1):
68
68
  if cores > 1:
69
- for ff in self:
70
- target = temp_file(".nc")
71
-
72
- append_safe(target)
73
-
74
- the_command = f"{command} {ff} {target}"
75
- the_command = the_command.replace(" ", " ")
76
-
77
- temp = pool.apply_async(run_nco, [the_command, target])
78
- results[ff] = temp
79
- new_commands.append(the_command)
69
+ targets = []
70
+ try:
71
+ with fork_pool(cores) as pool:
72
+ results = []
73
+ for ff in self:
74
+ target = temp_file(".nc")
75
+
76
+ append_safe(target)
77
+ targets.append(target)
78
+
79
+ the_command = f"{command} {ff} {target}"
80
+ the_command = the_command.replace(" ", " ")
81
+
82
+ results.append(
83
+ pool.apply_async(run_nco, [the_command, target])
84
+ )
85
+ new_commands.append(the_command)
86
+
87
+ for v in results:
88
+ target_list.append(v.get())
89
+ except BaseException:
90
+ for target in targets:
91
+ remove_safe(target)
92
+ raise
80
93
 
81
94
  else:
82
95
  for ff in self:
@@ -123,11 +136,6 @@ def nco_command(self, command=None, ensemble=False):
123
136
  self._hold_history = copy.deepcopy(self.history)
124
137
 
125
138
  if cores > 1 and ensemble is False:
126
- pool.close()
127
- pool.close()
128
- for k, v in results.items():
129
- target_list.append(v.get())
130
-
131
139
  self.current = copy.deepcopy(target_list)
132
140
  for cc in new_commands:
133
141
  self.history.append(cc)
@@ -15,7 +15,8 @@ from nctoolkit.session import (
15
15
  append_safe,
16
16
  remove_safe,
17
17
  get_protected,
18
- session_warnings
18
+ session_warnings,
19
+ active_cdo,
19
20
  )
20
21
  from nctoolkit.temp_file import temp_file
21
22
 
@@ -120,14 +121,7 @@ def run_nco(command, target, out_file=None, overwrite=False):
120
121
  target = new_target
121
122
  append_safe(target)
122
123
 
123
- out = subprocess.Popen(
124
- command,
125
- shell=True,
126
- stdin=subprocess.PIPE,
127
- stdout=subprocess.PIPE,
128
- stderr=subprocess.STDOUT,
129
- )
130
- result, ignore = out.communicate()
124
+ out, result = run_shell(command)
131
125
 
132
126
  if "(Abort)" in str(result):
133
127
  raise ValueError(
@@ -142,14 +136,7 @@ def run_nco(command, target, out_file=None, overwrite=False):
142
136
  remove_safe(target)
143
137
  target = new_target
144
138
 
145
- out = subprocess.Popen(
146
- command,
147
- shell=True,
148
- stdin=subprocess.PIPE,
149
- stdout=subprocess.PIPE,
150
- stderr=subprocess.STDOUT,
151
- )
152
- result1, ignore = out.communicate()
139
+ out, result1 = run_shell(command)
153
140
  if "ERROR" in str(result1):
154
141
  remove_safe(target)
155
142
  raise ValueError(
@@ -178,6 +165,38 @@ def run_nco(command, target, out_file=None, overwrite=False):
178
165
  return target
179
166
 
180
167
 
168
+ def run_shell(command):
169
+ """
170
+ Run a cdo command in its own process group and return (process, output)
171
+
172
+ The process group is registered in session.active_cdo while it runs. If
173
+ anything interrupts the wait (KeyboardInterrupt, an exception, or the
174
+ SIGTERM handler via clean_all) the whole group is killed, so cdo can't
175
+ keep running - and keep writing a temp file - after nctoolkit has stopped.
176
+ """
177
+ out = subprocess.Popen(
178
+ command,
179
+ shell=True,
180
+ stdin=subprocess.PIPE,
181
+ stdout=subprocess.PIPE,
182
+ stderr=subprocess.STDOUT,
183
+ start_new_session=True,
184
+ )
185
+ active_cdo.add(out)
186
+ try:
187
+ result, ignore = out.communicate()
188
+ except BaseException:
189
+ try:
190
+ os.killpg(out.pid, signal.SIGKILL)
191
+ except (OSError, ProcessLookupError):
192
+ pass
193
+ out.wait()
194
+ raise
195
+ finally:
196
+ active_cdo.discard(out)
197
+ return out, result
198
+
199
+
181
200
  def run_cdo(command=None, target=None, out_file=None, overwrite=False, precision=None):
182
201
  warned = False
183
202
 
@@ -221,15 +240,7 @@ def run_cdo(command=None, target=None, out_file=None, overwrite=False, precision
221
240
  raise ValueError("The command does not start with cdo!")
222
241
 
223
242
  append_safe(target)
224
- out = subprocess.Popen(
225
- command,
226
- shell=True,
227
- stdin=subprocess.PIPE,
228
- stdout=subprocess.PIPE,
229
- stderr=subprocess.STDOUT,
230
- )
231
- out.wait()
232
- result, ignore = out.communicate()
243
+ out, result = run_shell(command)
233
244
 
234
245
  # If it is a generic grid, it's better to not throw the CDO error which might be confusing.
235
246
  if "generic" in result.decode("utf-8").lower():
@@ -267,15 +278,7 @@ def run_cdo(command=None, target=None, out_file=None, overwrite=False, precision
267
278
  command = command.replace(target, new_target)
268
279
  target = new_target
269
280
 
270
- out = subprocess.Popen(
271
- command,
272
- shell=True,
273
- stdin=subprocess.PIPE,
274
- stdout=subprocess.PIPE,
275
- stderr=subprocess.STDOUT,
276
- )
277
- out.wait()
278
- result, ignore = out.communicate()
281
+ out, result = run_shell(command)
279
282
 
280
283
  if "Use the CDO option -b F32" in (result.decode("utf-8")):
281
284
  command_chunks = command.split(" ")
@@ -293,15 +296,7 @@ def run_cdo(command=None, target=None, out_file=None, overwrite=False, precision
293
296
  command = command.replace("cdo ", "cdo -b F64 ")
294
297
  command
295
298
 
296
- out = subprocess.Popen(
297
- command,
298
- shell=True,
299
- stdin=subprocess.PIPE,
300
- stdout=subprocess.PIPE,
301
- stderr=subprocess.STDOUT,
302
- )
303
- out.wait()
304
- result, ignore = out.communicate()
299
+ out, result = run_shell(command)
305
300
 
306
301
  if out_file is not None:
307
302
  if "HDF5 library version mismatched error" in str(result):
@@ -490,15 +485,7 @@ def run_cdo(command=None, target=None, out_file=None, overwrite=False, precision
490
485
  "HDF error when running CDO. Check if files are corrupt using the is_corrupt method, and consider running the check method"
491
486
  )
492
487
 
493
- out = subprocess.Popen(
494
- command,
495
- shell=True,
496
- stdin=subprocess.PIPE,
497
- stdout=subprocess.PIPE,
498
- stderr=subprocess.STDOUT,
499
- )
500
- out.wait()
501
- result1, ignore = out.communicate()
488
+ out, result1 = run_shell(command)
502
489
  if (
503
490
  (str(result1).startswith("b'Error"))
504
491
  or ("HDF error" in str(result1))
@@ -124,6 +124,7 @@ def run_this(os_command, self, output="one", out_file=None, suppress=False):
124
124
  self._ncommands += 1
125
125
 
126
126
  cores = session_info["cores"]
127
+ pool = None
127
128
 
128
129
  # if len(self) == 1:
129
130
  # if cores > 1:
@@ -154,11 +155,7 @@ def run_this(os_command, self, output="one", out_file=None, suppress=False):
154
155
  os_command = os_command.replace(" ", " ")
155
156
 
156
157
  if cores > 1:
157
- original_sigint_handler = signal.signal(
158
- signal.SIGTERM, signal.SIG_IGN
159
- )
160
158
  pool = mp.get_context('fork').Pool(cores)
161
- signal.signal(signal.SIGTERM, original_sigint_handler)
162
159
 
163
160
  target_list = []
164
161
  results = dict()
@@ -297,6 +294,9 @@ def run_this(os_command, self, output="one", out_file=None, suppress=False):
297
294
  if progress_bar:
298
295
  if not suppress:
299
296
  pbar.update(1)
297
+ pool.close()
298
+ pool.join()
299
+ pool = None
300
300
  for mm in get_warnings():
301
301
  warnings.warn(mm)
302
302
  if mm in session_warnings:
@@ -457,5 +457,15 @@ def run_this(os_command, self, output="one", out_file=None, suppress=False):
457
457
 
458
458
  self._format = None
459
459
  except Exception as e:
460
+ if pool is not None:
461
+ pool.terminate()
462
+ pool.join()
463
+ pool = None
460
464
  self.reset()
461
465
  raise ValueError(e)
466
+ finally:
467
+ # anything not handled above (e.g. KeyboardInterrupt) must not leave
468
+ # the pool to the garbage collector
469
+ if pool is not None:
470
+ pool.terminate()
471
+ pool.join()
@@ -15,6 +15,29 @@ import multiprocessing as mp
15
15
  _mp_fork_ctx = mp.get_context("fork")
16
16
  Manager = _mp_fork_ctx.Manager
17
17
 
18
+
19
+ from contextlib import contextmanager
20
+
21
+
22
+ @contextmanager
23
+ def fork_pool(cores):
24
+ """
25
+ Context manager giving a fork Pool that is always shut down and joined
26
+
27
+ A pool left to the garbage collector is terminated and then joined with no
28
+ timeout, which hangs for ever if a worker survives SIGTERM. The stdlib
29
+ `with Pool()` only terminates, so close/join explicitly here.
30
+ """
31
+ pool = _mp_fork_ctx.Pool(cores)
32
+ try:
33
+ yield pool
34
+ except BaseException:
35
+ pool.terminate()
36
+ pool.join()
37
+ raise
38
+ pool.close()
39
+ pool.join()
40
+
18
41
  nc_safe_par = Manager().list()
19
42
  temp_dirs_par = Manager().list()
20
43
  nc_protected_par = Manager().list()
@@ -129,6 +152,26 @@ def get_warnings():
129
152
  return session_warnings[:]
130
153
 
131
154
 
155
+ # cdo processes currently running on behalf of this process, so they can be
156
+ # killed if nctoolkit is terminated rather than left running (and writing to
157
+ # temp files) after Python has gone
158
+ active_cdo = set()
159
+
160
+
161
+ def kill_active_cdo():
162
+ """
163
+ Function to kill any cdo processes started by this process
164
+ """
165
+ import signal
166
+
167
+ for proc in list(active_cdo):
168
+ try:
169
+ os.killpg(proc.pid, signal.SIGKILL)
170
+ except (OSError, ProcessLookupError):
171
+ pass
172
+ active_cdo.clear()
173
+
174
+
132
175
  def session_files():
133
176
  """
134
177
  Function to return the session files
@@ -46,7 +46,7 @@ extras_require["complete"] = ["geoviews", "rioxarray", "cfchecker", "geocube", "
46
46
  REQUIREMENTS = [i.strip() for i in open("requirements.txt").readlines()]
47
47
 
48
48
  setup(name='nctoolkit',
49
- version='1.3.6',
49
+ version='1.3.7',
50
50
  description=DESCRIPTION,
51
51
  long_description=LONG_DESCRIPTION,
52
52
  long_description_content_type='text/plain',
@@ -1,50 +0,0 @@
1
- from nctoolkit.api import (
2
- open_data,
3
- open_geotiff,
4
- from_xarray,
5
- merge,
6
- cor_time,
7
- cor_space,
8
- options,
9
- DataSet,
10
- open_thredds,
11
- open_url,
12
- )
13
-
14
-
15
- from nctoolkit.unify import unify
16
- from nctoolkit.shape import open_shape
17
- from nctoolkit.static_plot import panel_plot
18
-
19
- from nctoolkit.validator import validator
20
- from nctoolkit.matchpoint import open_matchpoint
21
-
22
-
23
- from nctoolkit.cleanup import cleanup, clean_all, deep_clean, temp_check
24
-
25
- import atexit
26
- import signal
27
-
28
- atexit.register(clean_all)
29
-
30
- signal.signal(signal.SIGTERM, clean_all)
31
-
32
- from nctoolkit.create_ensemble import create_ensemble, glob
33
- from nctoolkit.session import session_files
34
- from nctoolkit.show import nc_variables, nc_years, nc_months, nc_times
35
-
36
- from nctoolkit.utils import validate_version, cdo_version
37
- from nctoolkit.session import session_info
38
- from nctoolkit.mp_adders import match_points
39
-
40
- session_info["cdo"] = cdo_version()
41
-
42
- try:
43
- from importlib.metadata import version as _version
44
- except ImportError:
45
- from importlib_metadata import version as _version
46
-
47
- try:
48
- __version__ = _version("nctoolkit")
49
- except Exception:
50
- __version__ = "999"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes