Skip to content

Commit 76c98f3

Browse files
authored
Merge pull request #905 from davidhassell/import-speed-up
Reduce the time taken to import `cf`
2 parents 9a03b76 + 727bb79 commit 76c98f3

24 files changed

+387
-399
lines changed

Changelog.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
Version NEXTVERSION
2+
-------------------
3+
4+
**2025-12-??**
5+
6+
* Reduce the time taken to import `cf`
7+
(https://github.com/NCAS-CMS/cf-python/issues/902)
8+
9+
----
10+
111
Version 3.18.2
212
--------------
313

cf/__init__.py

Lines changed: 14 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -80,167 +80,26 @@
8080
8181
"""
8282

83-
__date__ = "2025-10-16"
84-
__version__ = "3.18.2"
83+
import cfdm
8584

86-
_requires = (
87-
"numpy",
88-
"netCDF4",
89-
"cftime",
90-
"cfunits",
91-
"cfdm",
92-
"psutil",
93-
"dask",
94-
"packaging",
95-
"scipy",
96-
"distributed",
97-
)
98-
x = ", ".join(_requires)
99-
_error0 = f"cf v{__version__} requires the modules {x}. "
100-
101-
import importlib.util
102-
from platform import python_version
103-
104-
_found_esmpy = bool(importlib.util.find_spec("esmpy"))
105-
106-
try:
107-
import packaging
108-
from packaging.version import Version
109-
except ImportError as error1:
110-
raise ImportError(_error0 + str(error1))
111-
else:
112-
_minimum_vn = "20.0"
113-
if Version(packaging.__version__) < Version(_minimum_vn):
114-
raise RuntimeError(
115-
f"Bad packaging version: cf requires packaging>={_minimum_vn}. "
116-
f"Got {packaging.__version__} at {packaging.__file__}"
117-
)
118-
119-
try:
120-
import cfdm
121-
except ImportError as error1:
122-
raise ImportError(_error0 + str(error1))
123-
else:
124-
# Check the version of cfdm
125-
_minimum_vn = "1.12.3.1"
126-
_maximum_vn = "1.12.4.0"
127-
_cfdm_version = Version(cfdm.__version__)
128-
if _cfdm_version < Version(_minimum_vn) or _cfdm_version >= Version(
129-
_maximum_vn
130-
):
131-
raise RuntimeError(
132-
"Bad cfdm version: cf requires "
133-
f"{_minimum_vn}<=cfdm<{_maximum_vn}. "
134-
f"Got {_cfdm_version} at {cfdm.__file__}"
135-
)
85+
from packaging.version import Version
13686

87+
88+
__date__ = "2025-10-16"
89+
__version__ = "3.18.2"
13790
__cf_version__ = cfdm.__cf_version__
13891
__Conventions__ = f"CF-{__cf_version__}"
13992

140-
try:
141-
import netCDF4
142-
except ImportError as error1:
143-
raise ImportError(_error0 + str(error1))
144-
else:
145-
_set_vn = "1.7.2"
146-
if Version(netCDF4.__version__) != Version(_set_vn):
147-
raise RuntimeError(
148-
"Bad netCDF4 version: cf requires "
149-
f"netCDF4=={_set_vn}. "
150-
f"Got {netCDF4.__version__} at {netCDF4.__file__}"
151-
)
152-
153-
try:
154-
import numpy as np
155-
except ImportError as error1:
156-
raise ImportError(_error0 + str(error1))
157-
else:
158-
_minimum_vn = "2.0.0"
159-
if Version(np.__version__) < Version(_minimum_vn):
160-
raise RuntimeError(
161-
f"Bad numpy version: cf requires numpy>={_minimum_vn} "
162-
f"Got {np.__version__} at {np.__file__}"
163-
)
164-
165-
try:
166-
import cftime
167-
except ImportError as error1:
168-
raise ImportError(_error0 + str(error1))
169-
else:
170-
_minimum_vn = "1.6.4"
171-
if Version(cftime.__version__) < Version(_minimum_vn):
172-
raise RuntimeError(
173-
f"Bad cftime version: cf requires cftime>={_minimum_vn}. "
174-
f"Got {cftime.__version__} at {cftime.__file__}"
175-
)
176-
177-
try:
178-
import cfunits
179-
except ImportError as error1:
180-
raise ImportError(_error0 + str(error1))
181-
else:
182-
_minimum_vn = "3.3.7"
183-
if Version(cfunits.__version__) < Version(_minimum_vn):
184-
raise RuntimeError(
185-
f"Bad cfunits version: cf requires cfunits>={_minimum_vn}. "
186-
f"Got {cfunits.__version__} at {cfunits.__file__}"
187-
)
188-
189-
try:
190-
import psutil
191-
except ImportError as error1:
192-
raise ImportError(_error0 + str(error1))
193-
else:
194-
_minimum_vn = "0.6.0"
195-
if Version(psutil.__version__) < Version(_minimum_vn):
196-
raise RuntimeError(
197-
f"Bad psutil version: cf requires psutil>={_minimum_vn}. "
198-
f"Got {psutil.__version__} at {psutil.__file__}"
199-
)
200-
201-
# Check the version of dask
202-
try:
203-
import dask
204-
except ImportError as error1:
205-
raise ImportError(_error0 + str(error1))
206-
else:
207-
_minimum_vn = "2025.5.1"
208-
if Version(dask.__version__) < Version(_minimum_vn):
209-
raise RuntimeError(
210-
f"Bad dask version: cf requires dask>={_minimum_vn}. "
211-
f"Got {dask.__version__} at {dask.__file__}"
212-
)
213-
214-
try:
215-
import distributed
216-
except ImportError as error1:
217-
raise ImportError(_error0 + str(error1))
218-
else:
219-
_minimum_vn = "2025.5.1"
220-
if Version(distributed.__version__) < Version(_minimum_vn):
221-
raise RuntimeError(
222-
"Bad distributed version: cf requires "
223-
f"distributed>={_minimum_vn}. "
224-
f"Got {distributed.__version__} at {distributed.__file__}"
225-
)
226-
227-
try:
228-
import scipy
229-
except ImportError as error1:
230-
raise ImportError(_error0 + str(error1))
231-
else:
232-
_minimum_vn = "1.10.0"
233-
if Version(scipy.__version__) < Version(_minimum_vn):
234-
raise RuntimeError(
235-
f"Bad scipy version: cf requires scipy>={_minimum_vn}. "
236-
f"Got {scipy.__version__} at {scipy.__file__}"
237-
)
238-
239-
_minimum_vn = "3.10.0"
240-
if Version(python_version()) < Version(_minimum_vn):
93+
# Check the version of cfdm (this is worth doing because of the very
94+
# tight coupling between cf and cfdm, and the risk of bad things
95+
# happening at run time if the versions are mismatched).
96+
_minimum_vn = "1.12.3.1"
97+
_maximum_vn = "1.12.4.0"
98+
_cfdm_vn = Version(cfdm.__version__)
99+
if _cfdm_vn < Version(_minimum_vn) or _cfdm_vn >= Version(_maximum_vn):
241100
raise RuntimeError(
242-
f"Bad python version: cf requires python>={_minimum_vn}. "
243-
f"Got {python_version()}"
101+
f"cf v{__version__} requires {_minimum_vn}<=cfdm<{_maximum_vn}. "
102+
f"Got {_cfdm_vn} at {cfdm.__file__}"
244103
)
245104

246105
del _minimum_vn, _maximum_vn

cf/aggregate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import numpy as np
88
from cfdm import is_log_level_debug, is_log_level_detail, is_log_level_info
9-
from dask.base import tokenize
109

1110
from .auxiliarycoordinate import AuxiliaryCoordinate
1211
from .data import Data
@@ -1986,6 +1985,8 @@ def tokenise_cell_conditions(self, cell_conditions):
19861985
('ce9a05dd6ec76c6a6d171b0c055f3127', '8e0216a9a17a20b6620c6502bb45dec9')
19871986
19881987
"""
1988+
from dask.base import tokenize
1989+
19891990
out = []
19901991
for x in cell_conditions:
19911992
if x is None:
@@ -4056,6 +4057,8 @@ def _get_hfl(
40564057
hash_value = d.get_deterministic_name()
40574058
except ValueError:
40584059
# Slow
4060+
from dask.base import tokenize
4061+
40594062
hash_value = tokenize(d.compute())
40604063

40614064
if hash_value in hash_map:

cf/cellmethod.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import re
32
from ast import literal_eval as ast_literal_eval
43

54
import cfdm
@@ -89,6 +88,8 @@ def create(cls, cell_methods_string=None):
8988
>>> c = CellMethod.create('lat: mean (interval: 1 hour)')
9089
9190
"""
91+
import re
92+
9293
incorrect_interval = "Cell method interval is incorrectly formatted"
9394

9495
out = []

cf/cfdatetime.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import datetime
22
from functools import partial
33

4-
import cftime
54
import numpy as np
65

76
from .functions import _DEPRECATION_ERROR_CLASS
@@ -10,17 +9,10 @@
109
default_calendar = "gregorian"
1110

1211
# --------------------------------------------------------------------
13-
# Mapping of CF calendars to cftime date-time objects
12+
# Mapping of CF calendars to cftime date-time objects (that gets
13+
# populated in the `dt` function).
1414
# --------------------------------------------------------------------
15-
_datetime_object = {
16-
("",): partial(cftime.datetime, calendar=""),
17-
(None, "gregorian", "standard", "none"): cftime.DatetimeGregorian,
18-
("proleptic_gregorian",): cftime.DatetimeProlepticGregorian,
19-
("360_day",): cftime.Datetime360Day,
20-
("noleap", "365_day"): cftime.DatetimeNoLeap,
21-
("all_leap", "366_day"): cftime.DatetimeAllLeap,
22-
("julian",): cftime.DatetimeJulian,
23-
}
15+
_datetime_object = {}
2416

2517
canonical_calendar = {
2618
None: "standard",
@@ -40,7 +32,7 @@
4032
_calendar_map = {None: "gregorian"}
4133

4234

43-
class Datetime(cftime.datetime):
35+
class Datetime:
4436
"""A date-time object which supports CF calendars.
4537
4638
Deprecated at version 3.0.0. Use function 'cf.dt' to create date-
@@ -134,6 +126,26 @@ def dt(
134126
(2003, 4, 5, 12, 30, 15)
135127
136128
"""
129+
import cftime
130+
131+
if not _datetime_object:
132+
_datetime_object.update(
133+
{
134+
("",): partial(cftime.datetime, calendar=""),
135+
(
136+
None,
137+
"gregorian",
138+
"standard",
139+
"none",
140+
): cftime.DatetimeGregorian,
141+
("proleptic_gregorian",): cftime.DatetimeProlepticGregorian,
142+
("360_day",): cftime.Datetime360Day,
143+
("noleap", "365_day"): cftime.DatetimeNoLeap,
144+
("all_leap", "366_day"): cftime.DatetimeAllLeap,
145+
("julian",): cftime.DatetimeJulian,
146+
}
147+
)
148+
137149
if isinstance(arg, str):
138150
(year, month, day, hour, minute, second, microsecond) = st2elements(
139151
arg
@@ -161,11 +173,6 @@ def dt(
161173
else:
162174
year = arg
163175

164-
# calendar=_calendar_map.get(calendar, calendar)
165-
#
166-
# return cftime.datetime(year, month, day, hour, minute, second,
167-
# microsecond, calendar=calendar)
168-
169176
for calendars, datetime_cls in _datetime_object.items():
170177
if calendar in calendars:
171178
return datetime_cls(
@@ -354,6 +361,8 @@ def st2datetime(date_string, calendar=None):
354361
`cftime.datetime`
355362
356363
"""
364+
import cftime
365+
357366
if date_string.count("-") != 2:
358367
raise ValueError(
359368
"Input date-time string must contain at least a year, a month "
@@ -388,6 +397,8 @@ def st2elements(date_string):
388397
`tuple`
389398
390399
"""
400+
import cftime
401+
391402
if date_string.count("-") != 2:
392403
raise ValueError(
393404
"Input date-time string must contain at least a year, a month "
@@ -450,6 +461,8 @@ def rt2dt(array, units_in, units_out=None, dummy1=None):
450461
# mask
451462
return np.ma.masked_all((), dtype=object)
452463

464+
import cftime
465+
453466
units = units_in.units
454467
calendar = getattr(units_in, "calendar", "standard")
455468

@@ -510,6 +523,8 @@ def dt2rt(array, units_in, units_out, dummy1=None):
510523
[-- 685.5]
511524
512525
"""
526+
import cftime
527+
513528
isscalar = not np.ndim(array)
514529

515530
array = cftime.date2num(
@@ -545,8 +560,9 @@ def st2rt(array, units_in, units_out, dummy1=None):
545560
An array of floats with the same shape as *array*.
546561
547562
"""
563+
import cftime
564+
548565
array = st2dt(array, units_in)
549-
# array = units_out._utime.date2num(array)
550566
array = cftime.date2num(
551567
array, units=units_out.units, calendar=units_out._utime.calendar
552568
)

0 commit comments

Comments
 (0)