From 7b5f1f009ace29ee5a11108a2339ceb1b37eb760 Mon Sep 17 00:00:00 2001 From: beinan Date: Sat, 20 Dec 2025 22:48:12 +0000 Subject: [PATCH 1/2] Fix lance dependency and remove the implicit feather fallback --- python/pyproject.toml | 2 +- python/python/knowledge_graph/store.py | 58 ++-- python/python/tests/test_store.py | 356 +++++++++++++++++++++++++ python/uv.lock | 69 ++++- 4 files changed, 435 insertions(+), 50 deletions(-) create mode 100644 python/python/tests/test_store.py diff --git a/python/pyproject.toml b/python/pyproject.toml index 7ac193add..8d98371ca 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "uvicorn>=0.24.0", "pydantic>=2.0.0", "openai>=1.52.0", - "lance>=0.17.0", + "pylance", ] description = "Python bindings for the lance-graph Cypher engine" authors = [{ name = "Lance Devs", email = "dev@lancedb.com" }] diff --git a/python/python/knowledge_graph/store.py b/python/python/knowledge_graph/store.py index 49ea0487b..ec8202d62 100644 --- a/python/python/knowledge_graph/store.py +++ b/python/python/knowledge_graph/store.py @@ -1,4 +1,4 @@ -"""Persistent storage helpers built on Lance datasets (with Feather fallback).""" +"""Persistent storage helpers built on Lance datasets.""" from __future__ import annotations @@ -46,42 +46,35 @@ def list_datasets(self) -> Dict[str, "Path"]: datasets: Dict[str, Path] = {} if not self._root.exists(): return datasets - valid_suffixes = {".lance", ".arrow"} for child in self._root.iterdir(): if child.is_dir() and child.suffix == ".lance": datasets[child.stem] = child - elif child.is_file() and child.suffix in valid_suffixes: - datasets[child.stem] = child return datasets def _dataset_path(self, name: str) -> "Path": """Create the canonical path for a dataset.""" safe_name = name.replace("/", "_") - suffix = ".lance" if self._get_lance() else ".arrow" - return self._root / f"{safe_name}{suffix}" + return self._root / f"{safe_name}.lance" - def _get_lance(self) -> Optional[ModuleType]: + def _get_lance(self) -> ModuleType: if not self._lance_attempted: self._lance_attempted = True try: module = import_module("lance") - except ImportError: - module = None - else: - has_writer = hasattr(module, "write_dataset") - has_loader = hasattr(module, "dataset") - if not (has_writer and has_loader): - LOGGER.warning( - "Installed `lance` package missing dataset APIs; " - "falling back to Feather storage." - ) - module = None - self._lance = module - if module is None: - LOGGER.debug( - "Lance storage unavailable; using Feather files under %s.", - self._root, + except ImportError as e: + raise ImportError( + "Lance module is required but not installed. " + "Install it with: pip install pylance" + ) from e + + has_loader = hasattr(module, "dataset") + if not (has_loader): + raise ImportError( + "Installed `lance` package is missing required dataset APIs." ) + self._lance = module + if self._lance is None: + raise ImportError("Lance module failed to load") return self._lance def load_tables( @@ -90,7 +83,6 @@ def load_tables( ) -> Mapping[str, "pa.Table"]: """Load Lance datasets as PyArrow tables.""" lance = self._get_lance() - use_lance = lance is not None self.ensure_layout() available = self.list_datasets() @@ -101,13 +93,8 @@ def load_tables( path = available.get(name, self._dataset_path(name)) if not path.exists(): raise FileNotFoundError(f"Dataset '{name}' not found at {path}") - if path.suffix == ".lance" and use_lance: - dataset = lance.dataset(str(path)) # type: ignore[union-attr] - table = dataset.scanner().to_table() - else: - import pyarrow.feather as feather - - table = feather.read_table(str(path)) + dataset = lance.dataset(str(path)) + table = dataset.scanner().to_table() tables[name] = table return tables @@ -123,10 +110,5 @@ def write_tables(self, tables: Mapping[str, "pa.Table"]) -> None: f"Dataset '{name}' must be a pyarrow.Table (got {type(table)!r})" ) path = self._dataset_path(name) - if path.suffix == ".lance" and lance is not None: - mode = "overwrite" if path.exists() else "create" - lance.write_dataset(table, str(path), mode=mode) # type: ignore[union-attr] - else: - import pyarrow.feather as feather - - feather.write_feather(table, str(path)) + mode = "overwrite" if path.exists() else "create" + lance.write_dataset(table, str(path), mode=mode) diff --git a/python/python/tests/test_store.py b/python/python/tests/test_store.py new file mode 100644 index 000000000..a01944582 --- /dev/null +++ b/python/python/tests/test_store.py @@ -0,0 +1,356 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright The Lance Authors + +"""Unit tests for LanceGraphStore.""" + +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pyarrow as pa +import pytest +from knowledge_graph.config import KnowledgeGraphConfig +from knowledge_graph.store import LanceGraphStore + +import lance + + +@pytest.fixture +def config(tmp_path): + """Create a test configuration with temporary storage path.""" + return KnowledgeGraphConfig( + storage_path=tmp_path / "test_storage", + schema_path=tmp_path / "graph.yaml", + ) + + +@pytest.fixture +def store(config): + """Create a LanceGraphStore instance.""" + return LanceGraphStore(config) + + +class TestLanceModuleImport: + """Tests for Lance module import and validation.""" + + def test_get_lance_raises_import_error_when_module_not_found(self, store): + """Test that missing lance module raises ImportError with helpful message.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'lance'") + + with pytest.raises(ImportError) as exc_info: + store._get_lance() + + assert "Lance module is required but not installed" in str(exc_info.value) + assert "pip install pylance" in str(exc_info.value) + + def test_get_lance_raises_import_error_when_missing_dataset_method(self, store): + """Test that lance module without dataset() raises ImportError.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_lance = Mock() + mock_lance.write_dataset = Mock() + # Missing dataset method + delattr(mock_lance, "dataset") + mock_import.return_value = mock_lance + + with pytest.raises(ImportError) as exc_info: + store._get_lance() + + assert "missing required dataset APIs" in str(exc_info.value) + + def test_get_lance_only_checks_dataset_method(self, store): + """Test that lance module only requires dataset() method.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_lance = Mock() + mock_lance.dataset = Mock() + mock_lance.write_dataset = Mock() + mock_import.return_value = mock_lance + + result = store._get_lance() + + assert result == mock_lance + + def test_get_lance_succeeds_with_valid_module(self, store): + """Test that valid lance module loads successfully.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_lance = Mock() + mock_lance.write_dataset = Mock() + mock_lance.dataset = Mock() + mock_import.return_value = mock_lance + + result = store._get_lance() + + assert result == mock_lance + mock_import.assert_called_once_with("lance") + + def test_get_lance_caches_result(self, store): + """Test that _get_lance() caches the module and doesn't re-import.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_lance = Mock() + mock_lance.write_dataset = Mock() + mock_lance.dataset = Mock() + mock_import.return_value = mock_lance + + # First call + result1 = store._get_lance() + # Second call + result2 = store._get_lance() + + assert result1 == result2 == mock_lance + # import_module should only be called once due to caching + mock_import.assert_called_once_with("lance") + + +class TestBasicOperations: + """Tests for basic store operations.""" + + def test_config_property_returns_config(self, store, config): + """Test that config property returns the stored configuration.""" + assert store.config == config + + def test_root_property_returns_storage_path(self, store, config): + """Test that root property returns the storage path.""" + assert store.root == config.storage_path + + def test_ensure_layout_creates_directory(self, store, tmp_path): + """Test that ensure_layout creates the storage directory.""" + storage_path = tmp_path / "test_storage" + assert not storage_path.exists() + + store.ensure_layout() + + assert storage_path.exists() + assert storage_path.is_dir() + + def test_ensure_layout_succeeds_when_directory_exists(self, store): + """Test that ensure_layout is idempotent.""" + store.ensure_layout() + store.ensure_layout() # Should not raise + + assert store.root.exists() + + def test_dataset_path_generates_correct_path(self, store): + """Test that _dataset_path generates paths with .lance extension.""" + path = store._dataset_path("Person") + + assert path == store.root / "Person.lance" + assert path.suffix == ".lance" + + def test_dataset_path_sanitizes_slashes(self, store): + """Test that _dataset_path replaces slashes with underscores.""" + path = store._dataset_path("some/nested/name") + + assert path == store.root / "some_nested_name.lance" + assert "/" not in path.name + + def test_list_datasets_returns_empty_when_root_not_exists(self, store): + """Test that list_datasets returns empty dict when storage path doesn't exist.""" + datasets = store.list_datasets() + + assert datasets == {} + + def test_list_datasets_finds_lance_directories(self, store): + """Test that list_datasets finds .lance directories.""" + store.ensure_layout() + (store.root / "Person.lance").mkdir() + (store.root / "Company.lance").mkdir() + (store.root / "other.txt").touch() # Should be ignored + + datasets = store.list_datasets() + + assert len(datasets) == 2 + assert "Person" in datasets + assert "Company" in datasets + assert datasets["Person"] == store.root / "Person.lance" + assert datasets["Company"] == store.root / "Company.lance" + + +class TestWriteTables: + """Tests for write_tables method.""" + + def test_write_tables_creates_storage_directory(self, store): + """Test that write_tables creates storage directory if it doesn't exist.""" + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + + store.write_tables({"Person": table}) + + assert store.root.exists() + assert (store.root / "Person.lance").exists() + + def test_write_tables_writes_real_lance_dataset(self, store): + """Test that write_tables writes a real Lance dataset.""" + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + store.ensure_layout() + + store.write_tables({"Person": table}) + + # Verify the dataset was written + dataset_path = store.root / "Person.lance" + assert dataset_path.exists() + assert dataset_path.is_dir() + + def test_write_tables_uses_overwrite_mode_when_path_exists(self, store): + """Test that write_tables uses 'overwrite' mode for existing datasets.""" + table1 = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + table2 = pa.table({"id": [3, 4, 5], "name": ["Carol", "David", "Eve"]}) + store.ensure_layout() + + # First write + store.write_tables({"Person": table1}) + # Second write should overwrite + store.write_tables({"Person": table2}) + + # Verify overwrite worked - should have 3 rows, not 2 + dataset = lance.dataset(str(store.root / "Person.lance")) + result = dataset.scanner().to_table() + assert len(result) == 3 + + def test_write_tables_handles_multiple_tables(self, store): + """Test that write_tables can write multiple tables.""" + person_table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + company_table = pa.table({"id": [101, 102], "name": ["TechCorp", "DataInc"]}) + + store.write_tables({"Person": person_table, "Company": company_table}) + + assert (store.root / "Person.lance").exists() + assert (store.root / "Company.lance").exists() + + def test_write_tables_raises_type_error_for_non_table(self, store): + """Test that write_tables raises TypeError for non-PyArrow tables.""" + with pytest.raises(TypeError) as exc_info: + store.write_tables({"Person": {"not": "a table"}}) + + assert "must be a pyarrow.Table" in str(exc_info.value) + + +class TestLoadTables: + """Tests for load_tables method.""" + + def test_load_tables_reads_real_lance_dataset(self, store): + """Test that load_tables reads a real Lance dataset.""" + # First write a dataset + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + store.ensure_layout() + store.write_tables({"Person": table}) + + # Now load it + result = store.load_tables(["Person"]) + + assert "Person" in result + loaded_table = result["Person"] + assert len(loaded_table) == 2 + assert loaded_table.column_names == ["id", "name"] + assert loaded_table.to_pydict() == {"id": [1, 2], "name": ["Alice", "Bob"]} + + def test_load_tables_returns_pyarrow_table(self, store): + """Test that load_tables returns PyArrow table instances.""" + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + store.ensure_layout() + store.write_tables({"Person": table}) + + result = store.load_tables(["Person"]) + + assert isinstance(result["Person"], pa.Table) + + def test_load_tables_loads_all_datasets_when_names_not_provided(self, store): + """Test that load_tables loads all datasets when names is None.""" + person_table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + company_table = pa.table({"id": [101, 102], "name": ["TechCorp", "DataInc"]}) + store.ensure_layout() + store.write_tables({"Person": person_table, "Company": company_table}) + + result = store.load_tables() # No names provided + + assert len(result) == 2 + assert "Person" in result + assert "Company" in result + assert len(result["Person"]) == 2 + assert len(result["Company"]) == 2 + + def test_load_tables_raises_file_not_found_for_missing_dataset(self, store): + """Test that load_tables raises FileNotFoundError for non-existent dataset.""" + store.ensure_layout() + + with pytest.raises(FileNotFoundError) as exc_info: + store.load_tables(["NonExistent"]) + + assert "Dataset 'NonExistent' not found" in str(exc_info.value) + + def test_load_tables_loads_multiple_datasets(self, store): + """Test that load_tables can load multiple datasets.""" + person_table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + company_table = pa.table({"id": [101, 102], "name": ["TechCorp", "DataInc"]}) + store.ensure_layout() + store.write_tables({"Person": person_table, "Company": company_table}) + + result = store.load_tables(["Person", "Company"]) + + assert len(result) == 2 + assert "Person" in result + assert "Company" in result + assert result["Person"].to_pydict() == person_table.to_pydict() + assert result["Company"].to_pydict() == company_table.to_pydict() + + +class TestIntegration: + """Integration-style tests combining multiple operations.""" + + def test_write_and_list_workflow(self, store): + """Test write_tables followed by list_datasets.""" + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + store.write_tables({"Person": table}) + + datasets = store.list_datasets() + + assert "Person" in datasets + assert datasets["Person"].exists() + + def test_write_then_load_roundtrip(self, store): + """Test full write->load roundtrip with real Lance datasets.""" + original_person = pa.table( + { + "id": [1, 2, 3], + "name": ["Alice", "Bob", "Carol"], + "age": [28, 34, 29], + } + ) + original_company = pa.table( + { + "id": [101, 102], + "name": ["TechCorp", "DataInc"], + } + ) + + # Write + store.write_tables({"Person": original_person, "Company": original_company}) + + # Load back + loaded = store.load_tables() + + assert len(loaded) == 2 + assert loaded["Person"].to_pydict() == original_person.to_pydict() + assert loaded["Company"].to_pydict() == original_company.to_pydict() + + def test_lance_module_failure_propagates_to_write_tables(self, store): + """Test that lance import failure in write_tables raises ImportError.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'lance'") + + table = pa.table({"id": [1, 2], "name": ["Alice", "Bob"]}) + + with pytest.raises(ImportError) as exc_info: + store.write_tables({"Person": table}) + + assert "Lance module is required" in str(exc_info.value) + + def test_lance_module_failure_propagates_to_load_tables(self, store): + """Test that lance import failure in load_tables raises ImportError.""" + with patch("knowledge_graph.store.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'lance'") + + store.ensure_layout() + (store.root / "Person.lance").mkdir() + + with pytest.raises(ImportError) as exc_info: + store.load_tables(["Person"]) + + assert "Lance module is required" in str(exc_info.value) diff --git a/python/uv.lock b/python/uv.lock index 0358bb50a..4df2e8bc8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -281,25 +281,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/71/71408b02c6133153336d29fa3ba53000f1e1a3f78bb2fc2d1a1865d2e743/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960", size = 343697, upload-time = "2025-10-17T11:31:13.773Z" }, ] -[[package]] -name = "lance" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/2f/19bab0b1d8d5a4917581db6a7d2e132cca11323192f476cd27ed996192bc/lance-1.2.1.tar.gz", hash = "sha256:2849817a5eb7f5e610a4cf766bc3bd096d7f9e230bcc5a60c8dd3986510705e4", size = 16240, upload-time = "2020-11-04T17:18:10.806Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/1e/d47c5ff992a79d3987b4ac0a4d8a905457099f7def942dc6e1bd9c2216e2/lance-1.2.1-py3-none-any.whl", hash = "sha256:f9ac09055c7935c8d351bccd5f0deda9ecfce6b31fb1acdf650ef97140114137", size = 22548, upload-time = "2020-11-04T17:18:09.354Z" }, -] - [[package]] name = "lance-graph" version = "0.3.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, - { name = "lance" }, { name = "openai" }, { name = "pyarrow" }, { name = "pydantic" }, + { name = "pylance" }, { name = "pyyaml" }, { name = "uvicorn" }, ] @@ -319,12 +310,12 @@ tests = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.104.0" }, - { name = "lance", specifier = ">=0.17.0" }, { name = "openai", specifier = ">=1.52.0" }, { name = "pandas", marker = "extra == 'tests'" }, { name = "pyarrow", specifier = ">=14" }, { name = "pyarrow", marker = "extra == 'tests'", specifier = ">=14" }, { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pylance" }, { name = "pyright", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'tests'" }, { name = "pyyaml", specifier = ">=6.0" }, @@ -334,6 +325,33 @@ requires-dist = [ ] provides-extras = ["tests", "dev"] +[[package]] +name = "lance-namespace" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace-urllib3-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/44/946ca6033997820623906d84cb9830af89768940bbc9f824aadec6136254/lance_namespace-0.3.2.tar.gz", hash = "sha256:51eb30f8a9f073bba15d1824460bf6e9fa7f867e224e73ee64520ed254f0c140", size = 6833, upload-time = "2025-12-15T18:28:23.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/d2/947eedf16c59e1269c9cf7a2dc3c4522a3915cec664a9ffe8a7d1a0e2fcd/lance_namespace-0.3.2-py3-none-any.whl", hash = "sha256:794249bec15fb6e34d2b8d9f9698f11ae191179eccd9cd879743d8fb3c666ca0", size = 8335, upload-time = "2025-12-15T18:28:24.701Z" }, +] + +[[package]] +name = "lance-namespace-urllib3-client" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/17/56d98ad4a969e59d08d6e7157f9a680383f1fe5fd2916b75a42826ad0b52/lance_namespace_urllib3_client-0.3.2.tar.gz", hash = "sha256:1474e8a16a3547faeb5be56270b8903bd2c9ce10ae04d09245f3870ede3a5c4d", size = 151790, upload-time = "2025-12-15T18:28:23.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/8c/40ac725fb6fb7a4a13295fa2bc3b6ff877be1538d0a95ecf939ef0ceb562/lance_namespace_urllib3_client-0.3.2-py3-none-any.whl", hash = "sha256:bc73668b1086ef96c279870b019902bb293d15a6271ea8cf8eb429a57ab6a6ab", size = 256823, upload-time = "2025-12-15T18:28:25.603Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -856,6 +874,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pylance" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyarrow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5c/501e3a5d73b8ef1247045ce959fa6f8932753eacf192b7a122f394a063a0/pylance-1.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f1d70a59868dcee62862545f9f0846b328ee013f845bec536ff6d8aac23e3bfb", size = 49829642, upload-time = "2025-12-12T21:42:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/a30ad89ce6bf818c9551224ce0d2bfe4f67d7d99b3f8298f8860b12e3de6/pylance-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29f2af7d4eed932334b98c991b1d0c105de89a706f95ae40cce48385c6f5589e", size = 52193853, upload-time = "2025-12-12T21:51:49.609Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4d/160ca42beb5e903dd1dc6526fb8b0b3a0fe4750e9f04d3f16531ef23b158/pylance-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05196823a7698571c122f861038193a591fe55d42a0532c1183756a9f1602cf3", size = 55557899, upload-time = "2025-12-12T21:58:02.104Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/6fd71a0e0ba8560061d3222773c9d9406beb4d9f12dc8dcdce36964d6884/pylance-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:78db3a4270f0171870cfbfc13abe6af16e50565f111a8fe57b551600cfa27566", size = 52217155, upload-time = "2025-12-12T21:51:13.615Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a5/5c3c0605fb93d38d889e4219a8987e46863ab42e4ac46b8922afea0a5263/pylance-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4564edbe124052272c802bfc7d43de9a7448fe8ee25d10376dcfeed2f3c42ff8", size = 55530328, upload-time = "2025-12-12T21:58:36.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/05/2fd1188e0ccb419e45e30788c033ff6fd98fc3b8ccc204ef7c67bcc82146/pylance-1.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:cfc3e03709e64f255fc5c9dd9ac8847d8c24cce971cf290cffe92068b320188d", size = 59355812, upload-time = "2025-12-12T22:18:08.83Z" }, +] + [[package]] name = "pyright" version = "1.1.405" @@ -1119,6 +1157,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + [[package]] name = "uvicorn" version = "0.38.0" From 195358abceb9457f40f02a0a2c331fd23780002b Mon Sep 17 00:00:00 2001 From: beinan Date: Sat, 20 Dec 2025 23:06:49 +0000 Subject: [PATCH 2/2] Fix lint --- python/python/tests/test_store.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/python/tests/test_store.py b/python/python/tests/test_store.py index a01944582..62923d64e 100644 --- a/python/python/tests/test_store.py +++ b/python/python/tests/test_store.py @@ -3,16 +3,14 @@ """Unit tests for LanceGraphStore.""" -from pathlib import Path -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch +import lance import pyarrow as pa import pytest from knowledge_graph.config import KnowledgeGraphConfig from knowledge_graph.store import LanceGraphStore -import lance - @pytest.fixture def config(tmp_path): @@ -143,7 +141,7 @@ def test_dataset_path_sanitizes_slashes(self, store): assert "/" not in path.name def test_list_datasets_returns_empty_when_root_not_exists(self, store): - """Test that list_datasets returns empty dict when storage path doesn't exist.""" + """Test that list_datasets returns empty dict when root doesn't exist.""" datasets = store.list_datasets() assert datasets == {}