Compare commits

..

2 Commits

Author SHA1 Message Date
Gatefixer 600ea78cac fix(python): preserve pylance distribution variant 2026-08-05 23:29:37 +00:00
Gatefixer 876fab2422 fix(python): document optional pylance installation 2026-08-05 22:54:28 +00:00
7 changed files with 111 additions and 120 deletions
+19
View File
@@ -17,6 +17,25 @@ The general flow of using the API is:
pip install lancedb
```
The core package does not require PyLance. When you need access to the underlying
Lance dataset or GPU-accelerated indexing, add the `pylance` extra to the
distribution you already installed.
For the standard distribution:
```shell
pip install "lancedb[pylance]"
```
For the pre-Haswell compatibility distribution:
```shell
pip install "lancedb-compat[pylance]"
```
Use only the extra matching your installed distribution. Do not install both
distributions because they share the `lancedb` namespace.
The following methods describe the synchronous API client. There
is also an [asynchronous API client](#connections-asynchronous).
+19
View File
@@ -8,6 +8,25 @@ A Python library for [LanceDB](https://github.com/lancedb/lancedb).
pip install lancedb
```
The core package does not require PyLance. When you need access to the underlying
Lance dataset or GPU-accelerated indexing, add the `pylance` extra to the
distribution you already installed.
For the standard distribution:
```bash
pip install "lancedb[pylance]"
```
For the pre-Haswell compatibility distribution:
```bash
pip install "lancedb-compat[pylance]"
```
Use only the extra matching your installed distribution. Do not install both
distributions because they share the `lancedb` namespace.
### Pre-Haswell x86_64 hosts: `lancedb-compat`
The default `lancedb` wheel targets `x86-64-haswell` (AVX2 + FMA + F16C) for full performance on modern hardware. Pre-Haswell hosts — Intel Sandy Bridge / Ivy Bridge / Westmere; AMD Bulldozer / Piledriver / Steamroller — don't have AVX2 and crash with `Illegal instruction` at `import lancedb`.
+3 -56
View File
@@ -3,7 +3,6 @@
from typing import List
from urllib.parse import unquote, urlparse
import numpy as np
@@ -126,20 +125,9 @@ class InstructorEmbeddingFunction(TextEmbeddingFunction):
@weak_lru(maxsize=1)
def get_model(self):
huggingface_hub = attempt_import_or_raise("huggingface_hub", "huggingface-hub")
missing = object()
original_cached_download = getattr(huggingface_hub, "cached_download", missing)
if original_cached_download is missing:
huggingface_hub.cached_download = _cached_download(huggingface_hub)
try:
instructor_embedding = attempt_import_or_raise(
"InstructorEmbedding", "InstructorEmbedding"
)
finally:
if original_cached_download is missing:
del huggingface_hub.cached_download
instructor_embedding = attempt_import_or_raise(
"InstructorEmbedding", "InstructorEmbedding"
)
torch = attempt_import_or_raise("torch", "torch")
model = instructor_embedding.INSTRUCTOR(self.name)
@@ -152,44 +140,3 @@ class InstructorEmbeddingFunction(TextEmbeddingFunction):
model, {torch.nn.Linear}, dtype=torch.qint8
)
return model
def _cached_download(huggingface_hub):
"""Provide the legacy download API used by sentence-transformers 2.2.x."""
def cached_download(
*,
url,
cache_dir=None,
force_filename=None,
library_name=None,
library_version=None,
user_agent=None,
use_auth_token=None,
**_,
):
path = urlparse(url).path.lstrip("/")
try:
repo_id, resolved_path = path.split("/resolve/", maxsplit=1)
revision, filename = resolved_path.split("/", maxsplit=1)
except ValueError as err:
raise ValueError(f"Unsupported Hugging Face Hub URL: {url}") from err
repo_id = unquote(repo_id)
revision = unquote(revision)
filename = unquote(filename)
# sentence-transformers derives force_filename from this Hub path with
# os.path.join. Using the URL path beneath local_dir produces the same
# local destination without sending Windows separators to the Hub.
return huggingface_hub.hf_hub_download(
repo_id=repo_id,
filename=filename,
revision=revision,
local_dir=cache_dir,
library_name=library_name,
library_version=library_version,
user_agent=user_agent,
token=use_auth_token,
)
return cached_download
+10 -8
View File
@@ -117,6 +117,14 @@ _MODEL_BACKED_TOKENIZER_ERRORS = (
"Failed to initialize default tokenizer",
)
_PYLANCE_INSTALL_ERROR = (
"The lance library is required to use this function. Install the PyLance "
"extra for the distribution already installed: "
'`pip install "lancedb[pylance]"` for `lancedb`, or '
'`pip install "lancedb-compat[pylance]"` for `lancedb-compat`. '
"Do not install both distributions because they share the `lancedb` namespace."
)
def _add_unique_note(exception: BaseException, note: str) -> None:
existing_notes = getattr(exception, "__notes__", ()) or ()
@@ -2249,10 +2257,7 @@ class LanceTable(Table):
try:
import lance
except ImportError:
raise ImportError(
"The lance library is required to use this function. "
"Please install with `pip install pylance`."
)
raise ImportError(_PYLANCE_INSTALL_ERROR)
branch = self.current_branch()
version = None if branch is not None else self.version
@@ -4757,10 +4762,7 @@ class AsyncTable:
try:
import lance
except ImportError:
raise ImportError(
"The lance library is required to use this function. "
"Please install with `pip install pylance`."
)
raise ImportError(_PYLANCE_INSTALL_ERROR)
# lance.dataset() can't open a branch directly, so open the base table
# and check out the branch ref (a None branch resolves to main).
-56
View File
@@ -1,11 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import ntpath
import os
import pickle
import sys
from types import ModuleType
from typing import List, Optional, Union
from unittest.mock import MagicMock, patch
@@ -525,59 +522,6 @@ def test_embedding_function_safe_model_dump(embedding_type):
)
def test_instructor_embedding_supports_huggingface_hub_without_cached_download(
tmp_path, monkeypatch
):
from lancedb.embeddings.instructor import InstructorEmbeddingFunction
hub_download = MagicMock(return_value="/cache/1_Pooling/config.json")
huggingface_hub = ModuleType("huggingface_hub")
huggingface_hub.hf_hub_download = hub_download
torch = ModuleType("torch")
monkeypatch.setitem(sys.modules, "huggingface_hub", huggingface_hub)
monkeypatch.setitem(sys.modules, "torch", torch)
monkeypatch.delitem(sys.modules, "InstructorEmbedding", raising=False)
monkeypatch.syspath_prepend(str(tmp_path))
(tmp_path / "InstructorEmbedding.py").write_text(
"from huggingface_hub import cached_download\n\n"
"class INSTRUCTOR:\n"
" def __init__(self, name):\n"
" self.name = name\n"
)
embedding = InstructorEmbeddingFunction.create(show_progress_bar=False)
instructor_model = embedding.get_model()
assert instructor_model.name == "hkunlp/instructor-base"
assert not hasattr(huggingface_hub, "cached_download")
instructor_embedding = sys.modules["InstructorEmbedding"]
path = instructor_embedding.cached_download(
url=(
"https://huggingface.co/hkunlp/instructor-base/resolve/abc123/"
"1_Pooling/config.json"
),
cache_dir="/cache",
force_filename=ntpath.join("1_Pooling", "config.json"),
library_name="sentence-transformers",
library_version="2.2.2",
use_auth_token="token",
)
assert path == "/cache/1_Pooling/config.json"
hub_download.assert_called_once_with(
repo_id="hkunlp/instructor-base",
filename="1_Pooling/config.json",
revision="abc123",
local_dir="/cache",
library_name="sentence-transformers",
library_version="2.2.2",
user_agent=None,
token="token",
)
@patch("time.sleep")
def test_retry(mock_sleep):
test_function = MagicMock(side_effect=[Exception] * 9 + ["result"])
+30
View File
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import subprocess
import sys
def test_import_lancedb_without_pylance():
script = """
import sys
class BlockLanceImports:
def find_spec(self, fullname, path=None, target=None):
if fullname == "lance" or fullname.startswith("lance."):
raise ModuleNotFoundError(f"blocked optional dependency: {fullname}")
return None
sys.meta_path.insert(0, BlockLanceImports())
import lancedb
"""
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
+30
View File
@@ -1258,6 +1258,24 @@ def test_branch_to_lance_targets_branch(tmp_path):
assert table.to_lance().count_rows() == 1
def _assert_pylance_install_error(error: ImportError):
message = str(error)
assert 'pip install "lancedb[pylance]"' in message
assert 'pip install "lancedb-compat[pylance]"' in message
assert "distribution already installed" in message
assert "Do not install both distributions" in message
def test_to_lance_recommends_pylance_extra(tmp_db):
table = tmp_db.create_table("t", [{"i": 1}])
with patch("builtins.__import__", side_effect=ImportError):
with pytest.raises(ImportError) as exc_info:
table.to_lance()
_assert_pylance_install_error(exc_info.value)
@pytest.mark.asyncio
async def test_async_to_lance(tmp_path):
pytest.importorskip("lance")
@@ -1269,6 +1287,18 @@ async def test_async_to_lance(tmp_path):
assert dataset.count_rows() == 1
@pytest.mark.asyncio
async def test_async_to_lance_recommends_pylance_extra(tmp_path):
db = await lancedb.connect_async(tmp_path)
table = await db.create_table("t", [{"i": 1}])
with patch("builtins.__import__", side_effect=ImportError):
with pytest.raises(ImportError) as exc_info:
await table.to_lance()
_assert_pylance_install_error(exc_info.value)
@pytest.mark.asyncio
async def test_async_branch_to_lance_targets_branch(tmp_path):
pytest.importorskip("lance")