cf-xarray 0.11.0__tar.gz → 0.11.1__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 (92) hide show
  1. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/PKG-INFO +1 -1
  2. cf_xarray-0.11.1/cf_xarray/_version.py +1 -0
  3. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/accessor.py +143 -72
  4. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/utils.py +8 -5
  5. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray.egg-info/PKG-INFO +1 -1
  6. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/uv.lock +3 -3
  7. cf_xarray-0.11.0/cf_xarray/_version.py +0 -1
  8. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.binder/environment.yml +0 -0
  9. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.deepsource.toml +0 -0
  10. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/dependabot.yml +0 -0
  11. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/release.yml +0 -0
  12. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/workflows/ci.yaml +0 -0
  13. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/workflows/parse_logs.py +0 -0
  14. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/workflows/pypi.yaml +0 -0
  15. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/workflows/testpypi-release.yaml +0 -0
  16. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.github/workflows/upstream-dev-ci.yaml +0 -0
  17. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.gitignore +0 -0
  18. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.pre-commit-config.yaml +0 -0
  19. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.readthedocs.yml +0 -0
  20. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/.tributors +0 -0
  21. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/CITATION.cff +0 -0
  22. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/LICENSE +0 -0
  23. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/README.rst +0 -0
  24. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/__init__.py +0 -0
  25. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/coding.py +0 -0
  26. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/criteria.py +0 -0
  27. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/datasets.py +0 -0
  28. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/formatting.py +0 -0
  29. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/geometry.py +0 -0
  30. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/groupers.py +0 -0
  31. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/helpers.py +0 -0
  32. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/options.py +0 -0
  33. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/parametric.py +0 -0
  34. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/py.typed +0 -0
  35. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/scripts/make_doc.py +0 -0
  36. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/scripts/print_versions.py +0 -0
  37. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/sgrid.py +0 -0
  38. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/__init__.py +0 -0
  39. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/conftest.py +0 -0
  40. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_accessor.py +0 -0
  41. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_coding.py +0 -0
  42. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_geometry.py +0 -0
  43. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_groupers.py +0 -0
  44. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_helpers.py +0 -0
  45. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_options.py +0 -0
  46. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_parametric.py +0 -0
  47. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_scripts.py +0 -0
  48. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/tests/test_units.py +0 -0
  49. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray/units.py +0 -0
  50. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray.egg-info/SOURCES.txt +0 -0
  51. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray.egg-info/dependency_links.txt +0 -0
  52. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray.egg-info/requires.txt +0 -0
  53. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/cf_xarray.egg-info/top_level.txt +0 -0
  54. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/codecov.yml +0 -0
  55. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/2D_bounds_averaged.png +0 -0
  56. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/2D_bounds_error.png +0 -0
  57. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/2D_bounds_nonunique.png +0 -0
  58. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/Makefile +0 -0
  59. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/dataset-diagram-logo.tex +0 -0
  60. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/full-logo.png +0 -0
  61. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/logo.png +0 -0
  62. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/logo.svg +0 -0
  63. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/rich-repr-example.png +0 -0
  64. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/_static/style.css +0 -0
  65. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/api.rst +0 -0
  66. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/bounds.md +0 -0
  67. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/cartopy_rotated_pole.png +0 -0
  68. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/coding.md +0 -0
  69. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/conf.py +0 -0
  70. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/contributing.rst +0 -0
  71. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/coord_axes.md +0 -0
  72. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/custom-criteria.md +0 -0
  73. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/dsg.md +0 -0
  74. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/examples/introduction.ipynb +0 -0
  75. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/faq.md +0 -0
  76. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/flags.md +0 -0
  77. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/geometry.md +0 -0
  78. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/grid_mappings.md +0 -0
  79. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/howtouse.md +0 -0
  80. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/index.rst +0 -0
  81. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/make.bat +0 -0
  82. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/parametricz.md +0 -0
  83. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/plotting.md +0 -0
  84. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/provenance.md +0 -0
  85. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/quickstart.md +0 -0
  86. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/roadmap.rst +0 -0
  87. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/selecting.md +0 -0
  88. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/sgrid_ugrid.md +0 -0
  89. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/units.md +0 -0
  90. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/doc/whats-new.rst +0 -0
  91. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/pyproject.toml +0 -0
  92. {cf_xarray-0.11.0 → cf_xarray-0.11.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -0,0 +1 @@
1
+ __version__ = "0.11.1"
@@ -27,6 +27,7 @@ import xarray as xr
27
27
  from xarray import DataArray, Dataset
28
28
  from xarray.core.groupby import GroupBy
29
29
  from xarray.core.resample import Resample
30
+ from xarray.core.utils import Frozen
30
31
 
31
32
  try:
32
33
  from xarray.core.rolling import ( # type:ignore[import-not-found,no-redef,unused-ignore]
@@ -495,7 +496,10 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
495
496
  return list(results)
496
497
 
497
498
 
498
- def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hashable]]:
499
+ @functools.lru_cache(maxsize=256)
500
+ def _parse_grid_mapping_attribute(
501
+ grid_mapping_attr: str,
502
+ ) -> Mapping[str, list[Hashable]]:
499
503
  """
500
504
  Parse a grid_mapping attribute that may contain multiple grid mappings.
501
505
 
@@ -507,11 +511,12 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
507
511
  - Multiple: "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
508
512
  -> {"spatial_ref": [], "crs_4326": ["latitude", "longitude"], "crs_27700": ["x27700", "y27700"]}
509
513
 
510
- Returns a dictionary mapping grid mapping variable names to their associated coordinate variables.
514
+ Returns a read-only mapping from grid mapping variable name to its associated
515
+ coordinate variables. The result is memoized, so callers must not mutate it.
511
516
  """
512
517
  # Check if there are colons indicating multiple mappings
513
518
  if ":" not in grid_mapping_attr:
514
- return {grid_mapping_attr.strip(): []}
519
+ return Frozen({grid_mapping_attr.strip(): []})
515
520
 
516
521
  # Use regex to parse the format
517
522
  # First, find all grid mapping variables (words before colons)
@@ -519,7 +524,7 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
519
524
  grid_mappings = re.findall(grid_pattern, grid_mapping_attr)
520
525
 
521
526
  if not grid_mappings:
522
- return {grid_mapping_attr.strip(): []}
527
+ return Frozen({grid_mapping_attr.strip(): []})
523
528
 
524
529
  result: dict[str, list[Hashable]] = {}
525
530
 
@@ -548,13 +553,13 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
548
553
  else:
549
554
  result[gm] = []
550
555
 
551
- return result
556
+ return Frozen(result)
552
557
 
553
558
 
554
559
  def _create_grid_mapping(
555
560
  var_name: str,
556
561
  ds: Dataset,
557
- grid_mapping_dict: dict[str, list[Hashable]],
562
+ grid_mapping_dict: Mapping[str, list[Hashable]],
558
563
  ) -> GridMapping:
559
564
  """
560
565
  Create a GridMapping dataclass instance from a grid mapping variable.
@@ -1007,7 +1012,11 @@ def _getattr(
1007
1012
  newmap.update(dict.fromkeys(inverted[key], value))
1008
1013
  newmap.update({key: attribute[key] for key in unused_keys})
1009
1014
 
1010
- skip: dict[str, list[Literal["coords", "measures"]] | None] = {
1015
+ skip: dict[
1016
+ str,
1017
+ list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
1018
+ | None,
1019
+ ] = {
1011
1020
  "data_vars": ["coords"],
1012
1021
  "coords": None,
1013
1022
  }
@@ -1048,7 +1057,8 @@ def _getattr(
1048
1057
  def _getitem(
1049
1058
  accessor: CFAccessor,
1050
1059
  key: Hashable,
1051
- skip: list[Literal["coords", "measures"]] | None = None,
1060
+ skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
1061
+ | None = None,
1052
1062
  ) -> DataArray: ...
1053
1063
 
1054
1064
 
@@ -1056,14 +1066,16 @@ def _getitem(
1056
1066
  def _getitem(
1057
1067
  accessor: CFAccessor,
1058
1068
  key: Iterable[Hashable],
1059
- skip: list[Literal["coords", "measures"]] | None = None,
1069
+ skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
1070
+ | None = None,
1060
1071
  ) -> Dataset: ...
1061
1072
 
1062
1073
 
1063
1074
  def _getitem(
1064
1075
  accessor: CFAccessor,
1065
1076
  key: Hashable | Iterable[Hashable],
1066
- skip: list[Literal["coords", "measures"]] | None = None,
1077
+ skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
1078
+ | None = None,
1067
1079
  ):
1068
1080
  """
1069
1081
  Index into obj using key. Attaches CF associated variables.
@@ -1077,9 +1089,14 @@ def _getitem(
1077
1089
  """
1078
1090
 
1079
1091
  obj = accessor._obj
1080
- all_bounds = obj.cf.bounds if isinstance(obj, Dataset) else {}
1081
1092
  kind = str(type(obj).__name__)
1082
1093
  scalar_key = isinstance(key, Hashable)
1094
+ # obj.cf.bounds is expensive; only compute it when scalar lookup on a
1095
+ # Dataset actually needs to drop bounds variables.
1096
+ if not isinstance(obj, DataArray) and scalar_key:
1097
+ all_bounds = obj.cf.bounds
1098
+ else:
1099
+ all_bounds = {}
1083
1100
 
1084
1101
  key_iter: Iterable[Hashable]
1085
1102
  if isinstance(key, Hashable): # using scalar_key breaks mypy type narrowing
@@ -1127,60 +1144,90 @@ def _getitem(
1127
1144
 
1128
1145
  custom_criteria = ChainMap(*OPTIONS["custom_criteria"])
1129
1146
 
1130
- varnames: list[Hashable] = []
1131
- coords: list[Hashable] = []
1132
- successful = dict.fromkeys(key_iter, False)
1133
- for k in key_iter:
1134
- if "coords" not in skip and k in _AXIS_NAMES + _COORD_NAMES:
1135
- names = _get_all(obj, k)
1136
- names = drop_bounds(names)
1137
- check_results(names, k)
1138
- successful[k] = bool(names)
1139
- coords.extend(names)
1140
- elif "measures" not in skip and k in measures:
1141
- measure = _get_all(obj, k)
1142
- check_results(measure, k)
1143
- successful[k] = bool(measure)
1144
- if measure:
1145
- varnames.extend(measure)
1146
- elif "grid_mapping_names" not in skip and k in grid_mapping_names:
1147
- grid_mapping = _get_all(obj, k)
1148
- check_results(grid_mapping, k)
1149
- successful[k] = bool(grid_mapping)
1150
- if grid_mapping:
1151
- varnames.extend(grid_mapping)
1152
- elif "geometries" not in skip and (k == "geometry" or k in _GEOMETRY_TYPES):
1153
- geometries = _get_all(obj, k)
1154
- if geometries and k in _GEOMETRY_TYPES:
1155
- new = itertools.chain(
1156
- _parse_related_geometry_vars(
1157
- ChainMap(obj[g].attrs, obj[g].encoding)
1147
+ # Fast path: when every key is just a plain variable in the Dataset (no CF
1148
+ # special meaning), skip the per-key classification loop below. Test the
1149
+ # cheap predicates first and only build the reserved-name set / consult
1150
+ # accessor.standard_names (a full attrs scan) if those could pass — the
1151
+ # common ds.cf["X"] / ds.cf["longitude"] paths must not pay that cost.
1152
+ fast_path = (
1153
+ isinstance(obj, Dataset)
1154
+ and not skip
1155
+ and all(k in obj._variables for k in key_iter)
1156
+ )
1157
+ if fast_path:
1158
+ reserved: set[Hashable] = set(_AXIS_NAMES).union(
1159
+ _COORD_NAMES,
1160
+ _GEOMETRY_TYPES,
1161
+ ("geometry",),
1162
+ measures,
1163
+ grid_mapping_names,
1164
+ custom_criteria,
1165
+ cf_role_criteria,
1166
+ )
1167
+ standard_names = accessor.standard_names
1168
+ fast_path = all(k not in reserved and k not in standard_names for k in key_iter)
1169
+
1170
+ varnames: list[Hashable]
1171
+ coords: list[Hashable]
1172
+ if fast_path:
1173
+ varnames = list(key_iter)
1174
+ coords = []
1175
+ successful = dict.fromkeys(key_iter, True)
1176
+ else:
1177
+ varnames = []
1178
+ coords = []
1179
+ successful = dict.fromkeys(key_iter, False)
1180
+ for k in key_iter:
1181
+ if "coords" not in skip and k in _AXIS_NAMES + _COORD_NAMES:
1182
+ names = _get_all(obj, k)
1183
+ names = drop_bounds(names)
1184
+ check_results(names, k)
1185
+ successful[k] = bool(names)
1186
+ coords.extend(names)
1187
+ elif "measures" not in skip and k in measures:
1188
+ measure = _get_all(obj, k)
1189
+ check_results(measure, k)
1190
+ successful[k] = bool(measure)
1191
+ if measure:
1192
+ varnames.extend(measure)
1193
+ elif "grid_mapping_names" not in skip and k in grid_mapping_names:
1194
+ grid_mapping = _get_all(obj, k)
1195
+ check_results(grid_mapping, k)
1196
+ successful[k] = bool(grid_mapping)
1197
+ if grid_mapping:
1198
+ varnames.extend(grid_mapping)
1199
+ elif "geometries" not in skip and (k == "geometry" or k in _GEOMETRY_TYPES):
1200
+ geometries = _get_all(obj, k)
1201
+ if geometries and k in _GEOMETRY_TYPES:
1202
+ new = itertools.chain(
1203
+ _parse_related_geometry_vars(
1204
+ ChainMap(obj[g].attrs, obj[g].encoding)
1205
+ )
1206
+ for g in geometries
1158
1207
  )
1159
- for g in geometries
1160
- )
1161
- geometries.extend(*new)
1162
- if len(geometries) > 1 and scalar_key:
1163
- raise ValueError(
1164
- f"CF geometries must be represented by an Xarray Dataset. To request a Dataset in return please pass `[{k!r}]` instead."
1165
- )
1166
- successful[k] = bool(geometries)
1167
- if geometries:
1168
- varnames.extend(geometries)
1169
- elif k in custom_criteria or k in cf_role_criteria:
1170
- names = _get_all(obj, k)
1171
- check_results(names, k)
1172
- successful[k] = bool(names)
1173
- varnames.extend(names)
1174
- else:
1175
- stdnames = set(_get_with_standard_name(obj, k))
1176
- objcoords = set(obj.coords)
1177
- stdnames = drop_bounds(stdnames)
1178
- if "coords" in skip:
1179
- stdnames -= objcoords
1180
- check_results(stdnames, k)
1181
- successful[k] = bool(stdnames)
1182
- varnames.extend(stdnames - objcoords)
1183
- coords.extend(stdnames & objcoords)
1208
+ geometries.extend(*new)
1209
+ if len(geometries) > 1 and scalar_key:
1210
+ raise ValueError(
1211
+ f"CF geometries must be represented by an Xarray Dataset. To request a Dataset in return please pass `[{k!r}]` instead."
1212
+ )
1213
+ successful[k] = bool(geometries)
1214
+ if geometries:
1215
+ varnames.extend(geometries)
1216
+ elif k in custom_criteria or k in cf_role_criteria:
1217
+ names = _get_all(obj, k)
1218
+ check_results(names, k)
1219
+ successful[k] = bool(names)
1220
+ varnames.extend(names)
1221
+ else:
1222
+ stdnames = set(_get_with_standard_name(obj, k))
1223
+ objcoords = set(obj.coords)
1224
+ stdnames = drop_bounds(stdnames)
1225
+ if "coords" in skip:
1226
+ stdnames -= objcoords
1227
+ check_results(stdnames, k)
1228
+ successful[k] = bool(stdnames)
1229
+ varnames.extend(stdnames - objcoords)
1230
+ coords.extend(stdnames & objcoords)
1184
1231
 
1185
1232
  # these are not special names but could be variable names in underlying object
1186
1233
  # we allow this so that we can return variables with appropriate CF auxiliary variables
@@ -2042,7 +2089,7 @@ class CFAccessor:
2042
2089
  ]
2043
2090
  as_dataset = self._maybe_to_dataset().reset_coords()
2044
2091
 
2045
- keys = {}
2092
+ keys: dict[str, str] = {}
2046
2093
  for attr in set(all_attrs):
2047
2094
  try:
2048
2095
  keys.update(parse_cell_methods_attr(attr))
@@ -2835,14 +2882,38 @@ class CFDatasetAccessor(CFAccessor):
2835
2882
  """
2836
2883
 
2837
2884
  obj = self._obj
2838
- keys = self.keys() | set(obj.variables)
2885
+ variables = obj._variables
2886
+
2887
+ # Single linear scan to discover which variables in the dataset have
2888
+ # a ``bounds`` attribute pointing at another existing variable. The
2889
+ # naive implementation called ``apply_mapper(_get_bounds, ...)`` for
2890
+ # every key in ``self.keys() | set(variables)``, which itself fans
2891
+ # out through ``_get_all`` and runs the regex criteria against every
2892
+ # variable's attrs. That is O(num_keys × num_vars × num_criteria)
2893
+ # for an answer that is structurally a single attribute lookup.
2894
+ var_to_bounds: dict[Hashable, Hashable] = {}
2895
+ for name, var in variables.items():
2896
+ attrs_or_encoding = ChainMap(var.attrs, var.encoding)
2897
+ bounds_name = attrs_or_encoding.get("bounds")
2898
+ if bounds_name is not None and bounds_name in variables:
2899
+ var_to_bounds[name] = bounds_name
2900
+
2901
+ if not var_to_bounds:
2902
+ return {}
2839
2903
 
2840
- vardict = {
2841
- key: self._drop_missing_variables(
2842
- apply_mapper(_get_bounds, obj, key, error=False)
2843
- )
2844
- for key in keys
2845
- }
2904
+ vardict: dict[Hashable, set[Hashable]] = {}
2905
+ # Direct variable-name keys: O(1) lookup, no mapper machinery.
2906
+ for name, bounds_name in var_to_bounds.items():
2907
+ vardict[name] = {bounds_name}
2908
+
2909
+ # CF keys (axes, coordinate names, custom criteria, ...): resolve to
2910
+ # their target variable names once, then read the precomputed
2911
+ # ``var_to_bounds`` map. ``_get_bounds`` is no longer invoked.
2912
+ for key in self.keys() - set(variables):
2913
+ target_vars = apply_mapper(_get_all, obj, key, error=False, default=[])
2914
+ bounds_vars = {var_to_bounds[v] for v in target_vars if v in var_to_bounds}
2915
+ if bounds_vars:
2916
+ vardict[key] = bounds_vars
2846
2917
 
2847
2918
  return {k: sort_maybe_hashable(v) for k, v in vardict.items() if v}
2848
2919
 
@@ -1,13 +1,15 @@
1
+ import functools
1
2
  import inspect
2
3
  import os
3
4
  import warnings
4
5
  from collections import defaultdict
5
- from collections.abc import Iterable
6
+ from collections.abc import Iterable, Mapping
6
7
  from typing import Any
7
8
  from xml.etree import ElementTree
8
9
 
9
10
  import numpy as np
10
11
  from xarray import DataArray
12
+ from xarray.core.utils import Frozen
11
13
 
12
14
  try:
13
15
  import cftime
@@ -64,7 +66,8 @@ def _is_datetime_like(da: DataArray) -> bool:
64
66
  return False
65
67
 
66
68
 
67
- def parse_cell_methods_attr(attr: str) -> dict[str, str]:
69
+ @functools.lru_cache(maxsize=256)
70
+ def parse_cell_methods_attr(attr: str) -> Mapping[str, str]:
68
71
  """
69
72
  Parse cell_methods attributes (format is 'measure: name').
70
73
 
@@ -75,14 +78,14 @@ def parse_cell_methods_attr(attr: str) -> dict[str, str]:
75
78
 
76
79
  Returns
77
80
  -------
78
- Dictionary mapping measure to name
81
+ Read-only mapping from measure to name.
79
82
  """
80
83
  strings = [s for scolons in attr.split(":") for s in scolons.split()]
81
84
  if len(strings) % 2 != 0:
82
85
  raise ValueError(f"attrs['cell_measures'] = {attr!r} is malformed.")
83
86
 
84
- return dict(
85
- zip(strings[slice(0, None, 2)], strings[slice(1, None, 2)], strict=False)
87
+ return Frozen(
88
+ dict(zip(strings[slice(0, None, 2)], strings[slice(1, None, 2)], strict=False))
86
89
  )
87
90
 
88
91
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -2983,11 +2983,11 @@ wheels = [
2983
2983
 
2984
2984
  [[package]]
2985
2985
  name = "urllib3"
2986
- version = "2.6.3"
2986
+ version = "2.7.0"
2987
2987
  source = { registry = "https://pypi.org/simple" }
2988
- sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
2988
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
2989
2989
  wheels = [
2990
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
2990
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
2991
2991
  ]
2992
2992
 
2993
2993
  [[package]]
@@ -1 +0,0 @@
1
- __version__ = "0.11.0"
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