Skip to content

Commit

Permalink
feat(api): support tab completion for backends
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrist authored and cpcloud committed Jul 21, 2022
1 parent 949dbcd commit eb75fc5
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 68 deletions.
34 changes: 22 additions & 12 deletions ibis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Initialize Ibis module."""
from __future__ import annotations

import importlib.metadata as _importlib_metadata

# Converting an Ibis schema to a pandas DataFrame requires registering
# some type conversions that are currently registered in the pandas backend
Expand All @@ -12,18 +15,29 @@
from ibis.expr import api
from ibis.expr.api import * # noqa: F401,F403

try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove this when Python 3.9 support is dropped
import importlib_metadata

__all__ = ['api', 'ir', 'util', 'IbisError', 'options']
__all__ = ['api', 'ir', 'util', 'BaseBackend', 'IbisError', 'options']
__all__ += api.__all__

__version__ = "3.0.2"


def _get_backend_entrypoints() -> list[_importlib_metadata.EntryPoint]:
"""Get the list of installed `ibis.backend` entrypoints"""
import sys

if sys.version_info < (3, 10):
return list(_importlib_metadata.entry_points()['ibis.backends'])
else:
return list(_importlib_metadata.entry_points(group="ibis.backends"))


def __dir__() -> list[str]:
"""Adds tab completion for ibis backends to the top-level module"""
out = set(__all__)
out.update(ep.name for ep in _get_backend_entrypoints())
return sorted(out)


def __getattr__(name: str) -> BaseBackend:
"""Load backends in a lazy way with `ibis.<backend-name>`.
Expand All @@ -39,11 +53,7 @@ def __getattr__(name: str) -> BaseBackend:
the `ibis.backends` entrypoints. If successful, the `ibis.sqlite`
attribute is "cached", so this function is only called the first time.
"""
entry_points = {
entry_point
for entry_point in importlib_metadata.entry_points()["ibis.backends"]
if name == entry_point.name
}
entry_points = {ep for ep in _get_backend_entrypoints() if ep.name == name}

if not entry_points:
raise AttributeError(
Expand Down
21 changes: 10 additions & 11 deletions ibis/backends/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

import importlib
import importlib.metadata
import os
import platform
from functools import lru_cache
Expand All @@ -19,12 +19,6 @@
import ibis
import ibis.util as util

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata


TEST_TABLES = {
"functional_alltypes": ibis.schema(
{
Expand Down Expand Up @@ -236,10 +230,15 @@ def _get_backend_names() -> frozenset[str]:
If a `set` is used, then any in-place modifications to the set
are visible to every caller of this function.
"""
return frozenset(
entry_point.name
for entry_point in importlib_metadata.entry_points()["ibis.backends"]
)
import sys

if sys.version_info < (3, 10):
entrypoints = list(importlib.metadata.entry_points()['ibis.backends'])
else:
entrypoints = list(
importlib.metadata.entry_points(group="ibis.backends")
)
return frozenset(ep.name for ep in entrypoints)


def _get_backend_conf(backend_str: str):
Expand Down
9 changes: 3 additions & 6 deletions ibis/backends/datafusion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ class Backend(BaseBackend):

@property
def version(self):
try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove this when Python 3.9 support is dropped
import importlib_metadata
return importlib_metadata.version("datafusion")
import importlib.metadata

return importlib.metadata.version("datafusion")

def do_connect(
self,
Expand Down
9 changes: 3 additions & 6 deletions ibis/backends/duckdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,9 @@ def current_database(self) -> str:
@property
def version(self) -> str:
# TODO: there is a `PRAGMA version` we could use instead
try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove this when Python 3.9 support is dropped
import importlib_metadata
return importlib_metadata.version("duckdb")
import importlib.metadata

return importlib.metadata.version("duckdb")

def do_connect(
self,
Expand Down
62 changes: 29 additions & 33 deletions ibis/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
from __future__ import annotations

import sys
from importlib.metadata import EntryPoint
from typing import NamedTuple

import pytest

import ibis
from ibis.backends.base import BaseBackend

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata

EntryPoint = importlib_metadata.EntryPoint
def test_backends_are_cached():
assert isinstance(ibis.sqlite, BaseBackend)
del ibis.sqlite # delete to force recreation
assert isinstance(ibis.sqlite, BaseBackend)
assert ibis.sqlite is ibis.sqlite


def test_backends_are_cached():
# can't use `hasattr` since it calls `__getattr__`
if 'sqlite' in dir(ibis):
del ibis.sqlite
def test_backends_tab_completion():
assert isinstance(ibis.sqlite, BaseBackend)
del ibis.sqlite # delete to ensure not real attr
assert "sqlite" in dir(ibis)
assert isinstance(ibis.sqlite, BaseBackend)
assert 'sqlite' in dir(ibis)
assert "sqlite" in dir(ibis) # in dir even if already created


def test_missing_backend():
Expand All @@ -31,32 +32,27 @@ def test_missing_backend():


def test_multiple_backends(mocker):
if sys.version_info[:2] < (3, 8):
module = 'importlib_metadata'
else:
module = 'importlib.metadata'

api = f"{module}.entry_points"

class Distribution(NamedTuple):
entry_points: list[EntryPoint]

return_value = {
"ibis.backends": [
EntryPoint(
name="foo",
value='ibis.backends.backend1',
group="ibis.backends",
),
EntryPoint(
name="foo",
value='ibis.backends.backend2',
group="ibis.backends",
),
],
}

mocker.patch(api, return_value=return_value)
entrypoints = [
EntryPoint(
name="foo",
value='ibis.backends.backend1',
group="ibis.backends",
),
EntryPoint(
name="foo",
value='ibis.backends.backend2',
group="ibis.backends",
),
]
if sys.version_info < (3, 10):
return_value = {"ibis.backends": entrypoints}
else:
return_value = entrypoints

mocker.patch("importlib.metadata.entry_points", return_value=return_value)

msg = r"\d+ packages found for backend 'foo'"
with pytest.raises(RuntimeError, match=msg):
Expand Down

0 comments on commit eb75fc5

Please sign in to comment.