Skip to content

Commit

Permalink
feat(api): add .tables accessor to BaseBackend
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrist authored and cpcloud committed Jul 19, 2022
1 parent 0cdf799 commit 7ad27f0
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 2 deletions.
75 changes: 74 additions & 1 deletion ibis/backends/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from __future__ import annotations

import abc
import collections.abc
import functools
import keyword
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Iterable,
Iterator,
Mapping,
)

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


class TablesAccessor(collections.abc.Mapping):
"""A mapping-like object for accessing tables off a backend.
Tables may be accessed by name using either index or attribute access:
Examples
--------
>>> con = ibis.sqlite.connect("example.db")
>>> people = con.tables['people'] # access via index
>>> people = con.tables.people # access via attribute
"""

def __init__(self, backend: BaseBackend):
self._backend = backend

def __getitem__(self, name) -> ir.Table:
try:
return self._backend.table(name)
except Exception as exc:
raise KeyError(name) from exc

def __getattr__(self, name) -> ir.Table:
if name.startswith("_"):
raise AttributeError(name)
try:
return self._backend.table(name)
except Exception as exc:
raise AttributeError(name) from exc

def __iter__(self) -> Iterator[str]:
return iter(sorted(self._backend.list_tables()))

def __len__(self) -> int:
return len(self._backend.list_tables())

def __dir__(self) -> list[str]:
o = set()
o.update(dir(type(self)))
o.update(
name
for name in self._backend.list_tables()
if name.isidentifier() and not keyword.iskeyword(name)
)
return list(o)

def _ipython_key_completions_(self) -> list[str]:
return self._backend.list_tables()


class BaseBackend(abc.ABC):
"""Base backend class.
Expand Down Expand Up @@ -368,6 +427,20 @@ def exists_table(self, name: str, database: str | None = None) -> bool:
def table(self, name: str, database: str | None = None) -> ir.Table:
"""Return a table expression from the database."""

@functools.cached_property
def tables(self):
"""An accessor for tables in the database.
Tables may be accessed by name using either index or attribute access:
Examples
--------
>>> con = ibis.sqlite.connect("example.db")
>>> people = con.tables['people'] # access via index
>>> people = con.tables.people # access via attribute
"""
return TablesAccessor(self)

@deprecated(version='2.0', instead='use `.table(name).schema()`')
def get_schema(self, table_name: str, database: str = None) -> sch.Schema:
"""Return the schema of `table_name`."""
Expand Down
37 changes: 36 additions & 1 deletion ibis/backends/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest

import ibis


def test_backend_name(backend):
# backend is the TestConf for the backend
Expand Down Expand Up @@ -41,6 +43,39 @@ def test_database_consistency(con):
def test_list_tables(con):
tables = con.list_tables()
assert isinstance(tables, list)
# only table that is garanteed to be in all backends
# only table that is guaranteed to be in all backends
assert 'functional_alltypes' in tables
assert all(isinstance(table, str) for table in tables)


def test_tables_accessor_mapping(con):
assert isinstance(con.tables["functional_alltypes"], ibis.ir.Table)

with pytest.raises(KeyError, match="doesnt_exist"):
con.tables["doesnt_exist"]

tables = con.list_tables()

assert len(con.tables) == len(tables)
assert sorted(con.tables) == sorted(tables)


def test_tables_accessor_getattr(con):
assert isinstance(con.tables.functional_alltypes, ibis.ir.Table)

with pytest.raises(AttributeError, match="doesnt_exist"):
getattr(con.tables, "doesnt_exist")

# Underscore/double-underscore attributes are never available, since many
# python apis expect checking for the absence of these to be cheap.
with pytest.raises(AttributeError, match="_private_attr"):
getattr(con.tables, "_private_attr")


def test_tables_accessor_tab_completion(con):
attrs = dir(con.tables)
assert 'functional_alltypes' in attrs
assert 'keys' in attrs # type methods also present

keys = con.tables._ipython_key_completions_()
assert 'functional_alltypes' in keys

0 comments on commit 7ad27f0

Please sign in to comment.