Skip to content
10 changes: 10 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
Version NEXTVERSION
-------------------

**2025-12-??**

* Reduce the time taken to import `cf`
(https://github.com/NCAS-CMS/cf-python/issues/902)

----

Version 3.18.2
--------------

Expand Down
169 changes: 14 additions & 155 deletions cf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,167 +80,26 @@
"""

__date__ = "2025-10-16"
__version__ = "3.18.2"
import cfdm

_requires = (
"numpy",
"netCDF4",
"cftime",
"cfunits",
"cfdm",
"psutil",
"dask",
"packaging",
"scipy",
"distributed",
)
x = ", ".join(_requires)
_error0 = f"cf v{__version__} requires the modules {x}. "

import importlib.util
from platform import python_version

_found_esmpy = bool(importlib.util.find_spec("esmpy"))

try:
import packaging
from packaging.version import Version
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "20.0"
if Version(packaging.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad packaging version: cf requires packaging>={_minimum_vn}. "
f"Got {packaging.__version__} at {packaging.__file__}"
)

try:
import cfdm
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
# Check the version of cfdm
_minimum_vn = "1.12.3.1"
_maximum_vn = "1.12.4.0"
_cfdm_version = Version(cfdm.__version__)
if _cfdm_version < Version(_minimum_vn) or _cfdm_version >= Version(
_maximum_vn
):
raise RuntimeError(
"Bad cfdm version: cf requires "
f"{_minimum_vn}<=cfdm<{_maximum_vn}. "
f"Got {_cfdm_version} at {cfdm.__file__}"
)
from packaging.version import Version


__date__ = "2025-10-16"
__version__ = "3.18.2"
__cf_version__ = cfdm.__cf_version__
__Conventions__ = f"CF-{__cf_version__}"

try:
import netCDF4
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_set_vn = "1.7.2"
if Version(netCDF4.__version__) != Version(_set_vn):
raise RuntimeError(
"Bad netCDF4 version: cf requires "
f"netCDF4=={_set_vn}. "
f"Got {netCDF4.__version__} at {netCDF4.__file__}"
)

try:
import numpy as np
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "2.0.0"
if Version(np.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad numpy version: cf requires numpy>={_minimum_vn} "
f"Got {np.__version__} at {np.__file__}"
)

try:
import cftime
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "1.6.4"
if Version(cftime.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad cftime version: cf requires cftime>={_minimum_vn}. "
f"Got {cftime.__version__} at {cftime.__file__}"
)

try:
import cfunits
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "3.3.7"
if Version(cfunits.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad cfunits version: cf requires cfunits>={_minimum_vn}. "
f"Got {cfunits.__version__} at {cfunits.__file__}"
)

try:
import psutil
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "0.6.0"
if Version(psutil.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad psutil version: cf requires psutil>={_minimum_vn}. "
f"Got {psutil.__version__} at {psutil.__file__}"
)

# Check the version of dask
try:
import dask
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "2025.5.1"
if Version(dask.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad dask version: cf requires dask>={_minimum_vn}. "
f"Got {dask.__version__} at {dask.__file__}"
)

try:
import distributed
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "2025.5.1"
if Version(distributed.__version__) < Version(_minimum_vn):
raise RuntimeError(
"Bad distributed version: cf requires "
f"distributed>={_minimum_vn}. "
f"Got {distributed.__version__} at {distributed.__file__}"
)

try:
import scipy
except ImportError as error1:
raise ImportError(_error0 + str(error1))
else:
_minimum_vn = "1.10.0"
if Version(scipy.__version__) < Version(_minimum_vn):
raise RuntimeError(
f"Bad scipy version: cf requires scipy>={_minimum_vn}. "
f"Got {scipy.__version__} at {scipy.__file__}"
)

_minimum_vn = "3.10.0"
if Version(python_version()) < Version(_minimum_vn):
# Check the version of cfdm (this is worth doing because of the very
# tight coupling between cf and cfdm, and the risk of bad things
# happening at run time if the versions are mismatched).
_minimum_vn = "1.12.3.1"
_maximum_vn = "1.12.4.0"
_cfdm_vn = Version(cfdm.__version__)
if _cfdm_vn < Version(_minimum_vn) or _cfdm_vn >= Version(_maximum_vn):
raise RuntimeError(
f"Bad python version: cf requires python>={_minimum_vn}. "
f"Got {python_version()}"
f"cf v{__version__} requires {_minimum_vn}<=cfdm<{_maximum_vn}. "
f"Got {_cfdm_vn} at {cfdm.__file__}"
)

del _minimum_vn, _maximum_vn
Expand Down
5 changes: 4 additions & 1 deletion cf/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import numpy as np
from cfdm import is_log_level_debug, is_log_level_detail, is_log_level_info
from dask.base import tokenize

from .auxiliarycoordinate import AuxiliaryCoordinate
from .data import Data
Expand Down Expand Up @@ -1986,6 +1985,8 @@ def tokenise_cell_conditions(self, cell_conditions):
('ce9a05dd6ec76c6a6d171b0c055f3127', '8e0216a9a17a20b6620c6502bb45dec9')

"""
from dask.base import tokenize

out = []
for x in cell_conditions:
if x is None:
Expand Down Expand Up @@ -4056,6 +4057,8 @@ def _get_hfl(
hash_value = d.get_deterministic_name()
except ValueError:
# Slow
from dask.base import tokenize

hash_value = tokenize(d.compute())

if hash_value in hash_map:
Expand Down
3 changes: 2 additions & 1 deletion cf/cellmethod.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import re
from ast import literal_eval as ast_literal_eval

import cfdm
Expand Down Expand Up @@ -89,6 +88,8 @@ def create(cls, cell_methods_string=None):
>>> c = CellMethod.create('lat: mean (interval: 1 hour)')

"""
import re

incorrect_interval = "Cell method interval is incorrectly formatted"

out = []
Expand Down
52 changes: 34 additions & 18 deletions cf/cfdatetime.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
from functools import partial

import cftime
import numpy as np

from .functions import _DEPRECATION_ERROR_CLASS
Expand All @@ -10,17 +9,10 @@
default_calendar = "gregorian"

# --------------------------------------------------------------------
# Mapping of CF calendars to cftime date-time objects
# Mapping of CF calendars to cftime date-time objects (that gets
# populated in the `dt` function).
# --------------------------------------------------------------------
_datetime_object = {
("",): partial(cftime.datetime, calendar=""),
(None, "gregorian", "standard", "none"): cftime.DatetimeGregorian,
("proleptic_gregorian",): cftime.DatetimeProlepticGregorian,
("360_day",): cftime.Datetime360Day,
("noleap", "365_day"): cftime.DatetimeNoLeap,
("all_leap", "366_day"): cftime.DatetimeAllLeap,
("julian",): cftime.DatetimeJulian,
}
_datetime_object = {}

canonical_calendar = {
None: "standard",
Expand All @@ -40,7 +32,7 @@
_calendar_map = {None: "gregorian"}


class Datetime(cftime.datetime):
class Datetime:
"""A date-time object which supports CF calendars.

Deprecated at version 3.0.0. Use function 'cf.dt' to create date-
Expand Down Expand Up @@ -134,6 +126,26 @@ def dt(
(2003, 4, 5, 12, 30, 15)

"""
import cftime

if not _datetime_object:
_datetime_object.update(
{
("",): partial(cftime.datetime, calendar=""),
(
None,
"gregorian",
"standard",
"none",
): cftime.DatetimeGregorian,
("proleptic_gregorian",): cftime.DatetimeProlepticGregorian,
("360_day",): cftime.Datetime360Day,
("noleap", "365_day"): cftime.DatetimeNoLeap,
("all_leap", "366_day"): cftime.DatetimeAllLeap,
("julian",): cftime.DatetimeJulian,
}
)

if isinstance(arg, str):
(year, month, day, hour, minute, second, microsecond) = st2elements(
arg
Expand Down Expand Up @@ -161,11 +173,6 @@ def dt(
else:
year = arg

# calendar=_calendar_map.get(calendar, calendar)
#
# return cftime.datetime(year, month, day, hour, minute, second,
# microsecond, calendar=calendar)

for calendars, datetime_cls in _datetime_object.items():
if calendar in calendars:
return datetime_cls(
Expand Down Expand Up @@ -354,6 +361,8 @@ def st2datetime(date_string, calendar=None):
`cftime.datetime`

"""
import cftime

if date_string.count("-") != 2:
raise ValueError(
"Input date-time string must contain at least a year, a month "
Expand Down Expand Up @@ -388,6 +397,8 @@ def st2elements(date_string):
`tuple`

"""
import cftime

if date_string.count("-") != 2:
raise ValueError(
"Input date-time string must contain at least a year, a month "
Expand Down Expand Up @@ -450,6 +461,8 @@ def rt2dt(array, units_in, units_out=None, dummy1=None):
# mask
return np.ma.masked_all((), dtype=object)

import cftime

units = units_in.units
calendar = getattr(units_in, "calendar", "standard")

Expand Down Expand Up @@ -510,6 +523,8 @@ def dt2rt(array, units_in, units_out, dummy1=None):
[-- 685.5]

"""
import cftime

isscalar = not np.ndim(array)

array = cftime.date2num(
Expand Down Expand Up @@ -545,8 +560,9 @@ def st2rt(array, units_in, units_out, dummy1=None):
An array of floats with the same shape as *array*.

"""
import cftime

array = st2dt(array, units_in)
# array = units_out._utime.date2num(array)
array = cftime.date2num(
array, units=units_out.units, calendar=units_out._utime.calendar
)
Expand Down
Loading