Skip to content

Commit 7298df0

Browse files
authored
speedup attribute style access and tab completion (#4742)
* speedup attribute style access and tab completion * changes from code review, whats-new entry
1 parent a8bbaed commit 7298df0

File tree

6 files changed

+81
-64
lines changed

6 files changed

+81
-64
lines changed

doc/whats-new.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@ Internal Changes
7676
- Run the tests in parallel using pytest-xdist (:pull:`4694`).
7777

7878
By `Justus Magin <https://github.com/keewis>`_ and `Mathias Hauser <https://github.com/mathause>`_.
79-
8079
- Replace all usages of ``assert x.identical(y)`` with ``assert_identical(x, y)``
8180
for clearer error messages.
8281
(:pull:`4752`);
8382
By `Maximilian Roos <https://github.com/max-sixty>`_.
83+
- Speed up attribute style access (e.g. ``ds.somevar`` instead of ``ds["somevar"]``) and tab completion
84+
in ipython (:issue:`4741`, :pull:`4742`). By `Richard Kleijn <https://github.com/rhkleijn>`_.
8485

8586
.. _whats-new.0.16.2:
8687

xarray/core/common.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,14 @@ def __init_subclass__(cls):
209209
)
210210

211211
@property
212-
def _attr_sources(self) -> List[Mapping[Hashable, Any]]:
213-
"""List of places to look-up items for attribute-style access"""
214-
return []
212+
def _attr_sources(self) -> Iterable[Mapping[Hashable, Any]]:
213+
"""Places to look-up items for attribute-style access"""
214+
yield from ()
215215

216216
@property
217-
def _item_sources(self) -> List[Mapping[Hashable, Any]]:
218-
"""List of places to look-up items for key-autocompletion"""
219-
return []
217+
def _item_sources(self) -> Iterable[Mapping[Hashable, Any]]:
218+
"""Places to look-up items for key-autocompletion"""
219+
yield from ()
220220

221221
def __getattr__(self, name: str) -> Any:
222222
if name not in {"__dict__", "__setstate__"}:
@@ -272,26 +272,26 @@ def __dir__(self) -> List[str]:
272272
"""Provide method name lookup and completion. Only provide 'public'
273273
methods.
274274
"""
275-
extra_attrs = [
275+
extra_attrs = set(
276276
item
277-
for sublist in self._attr_sources
278-
for item in sublist
277+
for source in self._attr_sources
278+
for item in source
279279
if isinstance(item, str)
280-
]
281-
return sorted(set(dir(type(self)) + extra_attrs))
280+
)
281+
return sorted(set(dir(type(self))) | extra_attrs)
282282

283283
def _ipython_key_completions_(self) -> List[str]:
284284
"""Provide method for the key-autocompletions in IPython.
285285
See http://ipython.readthedocs.io/en/stable/config/integrating.html#tab-completion
286286
For the details.
287287
"""
288-
item_lists = [
288+
items = set(
289289
item
290-
for sublist in self._item_sources
291-
for item in sublist
290+
for source in self._item_sources
291+
for item in source
292292
if isinstance(item, str)
293-
]
294-
return list(set(item_lists))
293+
)
294+
return list(items)
295295

296296

297297
def get_squeeze_dims(

xarray/core/coordinates.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -325,29 +325,6 @@ def _ipython_key_completions_(self):
325325
return self._data._ipython_key_completions_()
326326

327327

328-
class LevelCoordinatesSource(Mapping[Hashable, Any]):
329-
"""Iterator for MultiIndex level coordinates.
330-
331-
Used for attribute style lookup with AttrAccessMixin. Not returned directly
332-
by any public methods.
333-
"""
334-
335-
__slots__ = ("_data",)
336-
337-
def __init__(self, data_object: "Union[DataArray, Dataset]"):
338-
self._data = data_object
339-
340-
def __getitem__(self, key):
341-
# not necessary -- everything here can already be found in coords.
342-
raise KeyError()
343-
344-
def __iter__(self) -> Iterator[Hashable]:
345-
return iter(self._data._level_coords)
346-
347-
def __len__(self) -> int:
348-
return len(self._data._level_coords)
349-
350-
351328
def assert_coordinate_consistent(
352329
obj: Union["DataArray", "Dataset"], coords: Mapping[Hashable, Variable]
353330
) -> None:

xarray/core/dataarray.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
from .common import AbstractArray, DataWithCoords
4747
from .coordinates import (
4848
DataArrayCoordinates,
49-
LevelCoordinatesSource,
5049
assert_coordinate_consistent,
5150
remap_label_indexers,
5251
)
@@ -56,7 +55,13 @@
5655
from .indexing import is_fancy_indexer
5756
from .merge import PANDAS_TYPES, MergeError, _extract_indexes_from_coords
5857
from .options import OPTIONS, _get_keep_attrs
59-
from .utils import Default, ReprObject, _default, either_dict_or_kwargs
58+
from .utils import (
59+
Default,
60+
HybridMappingProxy,
61+
ReprObject,
62+
_default,
63+
either_dict_or_kwargs,
64+
)
6065
from .variable import (
6166
IndexVariable,
6267
Variable,
@@ -721,18 +726,20 @@ def __delitem__(self, key: Any) -> None:
721726
del self.coords[key]
722727

723728
@property
724-
def _attr_sources(self) -> List[Mapping[Hashable, Any]]:
725-
"""List of places to look-up items for attribute-style access"""
726-
return self._item_sources + [self.attrs]
729+
def _attr_sources(self) -> Iterable[Mapping[Hashable, Any]]:
730+
"""Places to look-up items for attribute-style access"""
731+
yield from self._item_sources
732+
yield self.attrs
727733

728734
@property
729-
def _item_sources(self) -> List[Mapping[Hashable, Any]]:
730-
"""List of places to look-up items for key-completion"""
731-
return [
732-
self.coords,
733-
{d: self.coords[d] for d in self.dims},
734-
LevelCoordinatesSource(self),
735-
]
735+
def _item_sources(self) -> Iterable[Mapping[Hashable, Any]]:
736+
"""Places to look-up items for key-completion"""
737+
yield HybridMappingProxy(keys=self._coords, mapping=self.coords)
738+
739+
# virtual coordinates
740+
# uses empty dict -- everything here can already be found in self.coords.
741+
yield HybridMappingProxy(keys=self.dims, mapping={})
742+
yield HybridMappingProxy(keys=self._level_coords, mapping={})
736743

737744
def __contains__(self, key: Any) -> bool:
738745
return key in self.data

xarray/core/dataset.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
)
5959
from .coordinates import (
6060
DatasetCoordinates,
61-
LevelCoordinatesSource,
6261
assert_coordinate_consistent,
6362
remap_label_indexers,
6463
)
@@ -84,6 +83,7 @@
8483
from .utils import (
8584
Default,
8685
Frozen,
86+
HybridMappingProxy,
8787
SortedKeysDict,
8888
_default,
8989
decode_numpy_dict_values,
@@ -1341,19 +1341,22 @@ def __deepcopy__(self, memo=None) -> "Dataset":
13411341
return self.copy(deep=True)
13421342

13431343
@property
1344-
def _attr_sources(self) -> List[Mapping[Hashable, Any]]:
1345-
"""List of places to look-up items for attribute-style access"""
1346-
return self._item_sources + [self.attrs]
1344+
def _attr_sources(self) -> Iterable[Mapping[Hashable, Any]]:
1345+
"""Places to look-up items for attribute-style access"""
1346+
yield from self._item_sources
1347+
yield self.attrs
13471348

13481349
@property
1349-
def _item_sources(self) -> List[Mapping[Hashable, Any]]:
1350-
"""List of places to look-up items for key-completion"""
1351-
return [
1352-
self.data_vars,
1353-
self.coords,
1354-
{d: self[d] for d in self.dims},
1355-
LevelCoordinatesSource(self),
1356-
]
1350+
def _item_sources(self) -> Iterable[Mapping[Hashable, Any]]:
1351+
"""Places to look-up items for key-completion"""
1352+
yield self.data_vars
1353+
yield HybridMappingProxy(keys=self._coord_names, mapping=self.coords)
1354+
1355+
# virtual coordinates
1356+
yield HybridMappingProxy(keys=self.dims, mapping=self)
1357+
1358+
# uses empty dict -- everything here can already be found in self.coords.
1359+
yield HybridMappingProxy(keys=self._level_coords, mapping={})
13571360

13581361
def __contains__(self, key: object) -> bool:
13591362
"""The 'in' operator will return true or false depending on whether

xarray/core/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,35 @@ def FrozenDict(*args, **kwargs) -> Frozen:
435435
return Frozen(dict(*args, **kwargs))
436436

437437

438+
class HybridMappingProxy(Mapping[K, V]):
439+
"""Implements the Mapping interface. Uses the wrapped mapping for item lookup
440+
and a separate wrapped keys collection for iteration.
441+
442+
Can be used to construct a mapping object from another dict-like object without
443+
eagerly accessing its items or when a mapping object is expected but only
444+
iteration over keys is actually used.
445+
446+
Note: HybridMappingProxy does not validate consistency of the provided `keys`
447+
and `mapping`. It is the caller's responsibility to ensure that they are
448+
suitable for the task at hand.
449+
"""
450+
451+
__slots__ = ("_keys", "mapping")
452+
453+
def __init__(self, keys: Collection[K], mapping: Mapping[K, V]):
454+
self._keys = keys
455+
self.mapping = mapping
456+
457+
def __getitem__(self, key: K) -> V:
458+
return self.mapping[key]
459+
460+
def __iter__(self) -> Iterator[K]:
461+
return iter(self._keys)
462+
463+
def __len__(self) -> int:
464+
return len(self._keys)
465+
466+
438467
class SortedKeysDict(MutableMapping[K, V]):
439468
"""An wrapper for dictionary-like objects that always iterates over its
440469
items in sorted order by key but is otherwise equivalent to the underlying

0 commit comments

Comments
 (0)