Skip to content

Commit 7ad27f0

Browse files
jcristcpcloud
authored andcommitted
feat(api): add .tables accessor to BaseBackend
1 parent 0cdf799 commit 7ad27f0

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

ibis/backends/base/__init__.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
from __future__ import annotations
22

33
import abc
4+
import collections.abc
45
import functools
6+
import keyword
57
import re
68
from pathlib import Path
7-
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping
9+
from typing import (
10+
TYPE_CHECKING,
11+
Any,
12+
Callable,
13+
ClassVar,
14+
Iterable,
15+
Iterator,
16+
Mapping,
17+
)
818

919
if TYPE_CHECKING:
1020
import pandas as pd
@@ -145,6 +155,55 @@ def list_tables(self, like=None):
145155
return self.client.list_tables(like, database=self.name)
146156

147157

158+
class TablesAccessor(collections.abc.Mapping):
159+
"""A mapping-like object for accessing tables off a backend.
160+
161+
Tables may be accessed by name using either index or attribute access:
162+
163+
Examples
164+
--------
165+
>>> con = ibis.sqlite.connect("example.db")
166+
>>> people = con.tables['people'] # access via index
167+
>>> people = con.tables.people # access via attribute
168+
"""
169+
170+
def __init__(self, backend: BaseBackend):
171+
self._backend = backend
172+
173+
def __getitem__(self, name) -> ir.Table:
174+
try:
175+
return self._backend.table(name)
176+
except Exception as exc:
177+
raise KeyError(name) from exc
178+
179+
def __getattr__(self, name) -> ir.Table:
180+
if name.startswith("_"):
181+
raise AttributeError(name)
182+
try:
183+
return self._backend.table(name)
184+
except Exception as exc:
185+
raise AttributeError(name) from exc
186+
187+
def __iter__(self) -> Iterator[str]:
188+
return iter(sorted(self._backend.list_tables()))
189+
190+
def __len__(self) -> int:
191+
return len(self._backend.list_tables())
192+
193+
def __dir__(self) -> list[str]:
194+
o = set()
195+
o.update(dir(type(self)))
196+
o.update(
197+
name
198+
for name in self._backend.list_tables()
199+
if name.isidentifier() and not keyword.iskeyword(name)
200+
)
201+
return list(o)
202+
203+
def _ipython_key_completions_(self) -> list[str]:
204+
return self._backend.list_tables()
205+
206+
148207
class BaseBackend(abc.ABC):
149208
"""Base backend class.
150209
@@ -368,6 +427,20 @@ def exists_table(self, name: str, database: str | None = None) -> bool:
368427
def table(self, name: str, database: str | None = None) -> ir.Table:
369428
"""Return a table expression from the database."""
370429

430+
@functools.cached_property
431+
def tables(self):
432+
"""An accessor for tables in the database.
433+
434+
Tables may be accessed by name using either index or attribute access:
435+
436+
Examples
437+
--------
438+
>>> con = ibis.sqlite.connect("example.db")
439+
>>> people = con.tables['people'] # access via index
440+
>>> people = con.tables.people # access via attribute
441+
"""
442+
return TablesAccessor(self)
443+
371444
@deprecated(version='2.0', instead='use `.table(name).schema()`')
372445
def get_schema(self, table_name: str, database: str = None) -> sch.Schema:
373446
"""Return the schema of `table_name`."""

ibis/backends/tests/test_api.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22

3+
import ibis
4+
35

46
def test_backend_name(backend):
57
# backend is the TestConf for the backend
@@ -41,6 +43,39 @@ def test_database_consistency(con):
4143
def test_list_tables(con):
4244
tables = con.list_tables()
4345
assert isinstance(tables, list)
44-
# only table that is garanteed to be in all backends
46+
# only table that is guaranteed to be in all backends
4547
assert 'functional_alltypes' in tables
4648
assert all(isinstance(table, str) for table in tables)
49+
50+
51+
def test_tables_accessor_mapping(con):
52+
assert isinstance(con.tables["functional_alltypes"], ibis.ir.Table)
53+
54+
with pytest.raises(KeyError, match="doesnt_exist"):
55+
con.tables["doesnt_exist"]
56+
57+
tables = con.list_tables()
58+
59+
assert len(con.tables) == len(tables)
60+
assert sorted(con.tables) == sorted(tables)
61+
62+
63+
def test_tables_accessor_getattr(con):
64+
assert isinstance(con.tables.functional_alltypes, ibis.ir.Table)
65+
66+
with pytest.raises(AttributeError, match="doesnt_exist"):
67+
getattr(con.tables, "doesnt_exist")
68+
69+
# Underscore/double-underscore attributes are never available, since many
70+
# python apis expect checking for the absence of these to be cheap.
71+
with pytest.raises(AttributeError, match="_private_attr"):
72+
getattr(con.tables, "_private_attr")
73+
74+
75+
def test_tables_accessor_tab_completion(con):
76+
attrs = dir(con.tables)
77+
assert 'functional_alltypes' in attrs
78+
assert 'keys' in attrs # type methods also present
79+
80+
keys = con.tables._ipython_key_completions_()
81+
assert 'functional_alltypes' in keys

0 commit comments

Comments
 (0)