From 5aecdae7b85c32a9fa234e89d95880721d3be9a0 Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Sun, 21 Jun 2026 13:43:16 -0400 Subject: [PATCH 1/6] fix: LanceDB flat schema metadata extraction and column width limits Fixes metadata display for LanceDB databases with flat schemas (e.g., Contextus) where metadata is stored as individual columns rather than nested in a 'metadata' column. ## Changes ### LanceDB Connection - Extract metadata from PyArrow schema first (all non-reserved columns) - Support flat schema format in get_all_items() - build metadata dicts from column values - Detect and exclude content column from metadata to prevent duplication - Maintain backward compatibility with nested 'metadata' column format - Fix count detection when schema extraction succeeds but count is unavailable ### UI - Add 600px max-width constraint to table columns to prevent excessive width - Prevent 'document' column from appearing twice (once as content, once as metadata) ### Tests - Add comprehensive flat schema metadata extraction tests - Update existing tests to reflect content column exclusion from metadata - Verify backward compatibility with nested metadata format Closes #XX --- pyproject.toml | 2 +- .../core/connections/lancedb_connection.py | 189 +++++++++++++----- .../ui/views/metadata/metadata_table.py | 8 + .../lancedb/test_flat_schema_metadata.py | 123 ++++++++++++ tests/test_lancedb_metadata_padding.py | 4 +- 5 files changed, 273 insertions(+), 53 deletions(-) create mode 100644 tests/providers/lancedb/test_flat_schema_metadata.py diff --git a/pyproject.toml b/pyproject.toml index c57267e..ba95bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vector-inspector" -version = "0.8.3" +version = "0.8.4" description = "A comprehensive desktop application for visualizing, querying, and managing vector database data" authors = [ {name = "Anthony Dawson", email = "anthonypdawson+github@gmail.com"}, diff --git a/src/vector_inspector/core/connections/lancedb_connection.py b/src/vector_inspector/core/connections/lancedb_connection.py index 3622460..a558285 100644 --- a/src/vector_inspector/core/connections/lancedb_connection.py +++ b/src/vector_inspector/core/connections/lancedb_connection.py @@ -1,5 +1,6 @@ """LanceDB connection implementation for Vector Inspector.""" +import math import os from typing import Any @@ -78,61 +79,121 @@ def get_collection_info(self, name: str) -> dict[str, Any] | None: # Row count (prefer num_rows, fallback to pandas length) count = getattr(tbl, "num_rows", None) - # Pull dataframe sample to infer metadata fields and vector dimension + # Extract metadata fields from schema first (more reliable) + metadata_fields: list[str] = [] + vector_dimension: int | str = "Unknown" + reserved_columns = {"id", "vector", "embedding", "_distance"} + + # Detect content column to exclude it from metadata + content_col: str | None = None try: - df = tbl.to_pandas() - # Filter out dummy initialization row - if df is not None and "id" in df.columns: - df = df[df["id"] != "__dummy_init__"] + # Try to get schema to detect content column + schema = tbl.schema + if schema: + # Build a temporary dict for content detection + temp_schema = {field.name: str(field.type) for field in schema} + content_col = self._detect_content_column(name, temp_schema) + reserved_columns.add(content_col) except Exception: - df = None + pass - metadata_fields: list[str] = [] - vector_dimension: int | str = "Unknown" + # Try to get schema from PyArrow table schema + try: + schema = tbl.schema + if schema: + # All non-reserved columns (excluding content) are metadata fields + metadata_fields = [ + field.name for field in schema + if field.name not in reserved_columns and not field.name.startswith("_") + ] - if df is not None and not df.empty: - # Determine count if not available - if count is None: - try: - count = len(df) - except Exception: - count = 0 + # Get vector dimension from schema if available + for field in schema: + if field.name in ("vector", "embedding"): + try: + # PyArrow FixedSizeListType has list_size attribute + if hasattr(field.type, "list_size"): + vector_dimension = field.type.list_size + except Exception: + pass + except Exception: + # If schema access fails, fall back to pandas sampling + pass - # Infer metadata fields from the first row - first_meta = df.iloc[0].get("metadata") if "metadata" in df.columns else None - if isinstance(first_meta, str): - import ast + # Fallback: Pull dataframe sample if schema extraction didn't work or count is unknown + if not metadata_fields or vector_dimension == "Unknown" or count is None: + try: + df = tbl.to_pandas() + # Filter out dummy initialization row + if df is not None and "id" in df.columns: + df = df[df["id"] != "__dummy_init__"] + except Exception: + df = None - try: - parsed = ast.literal_eval(first_meta) - if isinstance(parsed, dict): - metadata_fields = list(parsed.keys()) - except Exception: - # treat as raw string field - metadata_fields = [] - elif isinstance(first_meta, dict): - metadata_fields = list(first_meta.keys()) - - # Infer vector dimension from the first vector entry - if "vector" in df.columns: - first_vec = df.iloc[0].get("vector") - if first_vec is not None: + if df is not None and not df.empty: + # Determine count if not available + if count is None: try: - vector_dimension = len(first_vec) + count = len(df) except Exception: - vector_dimension = "Unknown" + count = 0 + + # If we didn't get metadata fields from schema, extract from columns + if not metadata_fields: + # First check for flat schema columns + metadata_fields = [ + col for col in df.columns + if col not in reserved_columns and not col.startswith("_") + ] - # Cache vector dimension if known - try: - if isinstance(vector_dimension, int): - self._collection_meta[name] = vector_dimension - except Exception: - pass + # Also check for nested metadata column (legacy format) + if "metadata" in df.columns and "metadata" in metadata_fields: + # Remove "metadata" from the list and try to extract its keys + metadata_fields.remove("metadata") + first_meta = df.iloc[0].get("metadata") + if isinstance(first_meta, str): + import ast + try: + parsed = ast.literal_eval(first_meta) + if isinstance(parsed, dict): + # Add nested metadata keys as separate fields + metadata_fields.extend( + f"metadata.{k}" for k in parsed.keys() + ) + except Exception: + # If parsing fails, keep "metadata" as a column + metadata_fields.append("metadata") + elif isinstance(first_meta, dict): + metadata_fields.extend( + f"metadata.{k}" for k in first_meta.keys() + ) + else: + # Not a dict, keep "metadata" as a column + metadata_fields.append("metadata") - else: - # No dataframe available, try to get count from attribute - if count is None: - count = 0 + # Infer vector dimension from the first vector entry if still unknown + if vector_dimension == "Unknown" and "vector" in df.columns: + first_vec = df.iloc[0].get("vector") + if first_vec is not None: + try: + vector_dimension = len(first_vec) + except Exception: + vector_dimension = "Unknown" + + # Cache vector dimension if known + try: + if isinstance(vector_dimension, int): + self._collection_meta[name] = vector_dimension + except Exception: + pass + else: + # No dataframe available, try to get count from attribute + if count is None: + count = 0 + + # Final fallback for count + if count is None: + count = 0 distance_metric = "Unknown" @@ -601,8 +662,37 @@ def get_all_items( # Get result count first result_count = len(df) - raw_meta = df["metadata"].tolist() if "metadata" in df.columns else [] - metadatas = self._parse_metadata_list(raw_meta) + # Extract metadata - support both nested "metadata" column and flat schema + # Get content column first to exclude it from metadata + schema = {col: str(dtype) for col, dtype in df.dtypes.items()} + content_col = self._detect_content_column(collection_name, schema) + reserved_columns = {"id", "vector", "embedding", "_distance", content_col} + + if "metadata" in df.columns: + # Legacy format: nested metadata column + raw_meta = df["metadata"].tolist() + metadatas = self._parse_metadata_list(raw_meta) + else: + # Flat schema format: all non-reserved columns are metadata + metadata_columns = [ + col for col in df.columns + if col not in reserved_columns and not col.startswith("_") + ] + + if metadata_columns: + # Build metadata dicts from the flat columns + # Use .to_dict('records') for efficient row-wise conversion + records = df[metadata_columns].to_dict('records') + metadatas = [] + for record in records: + # Filter out NaN/None values + meta = { + k: v for k, v in record.items() + if v is not None and (not isinstance(v, float) or not math.isnan(v)) + } + metadatas.append(meta) + else: + metadatas = [{} for _ in range(result_count)] # Ensure metadatas has same length as result count if len(metadatas) < result_count: @@ -615,10 +705,7 @@ def get_all_items( while len(metadatas) < result_count: metadatas.append({}) - # Get documents from content column (auto-detect) - schema = {col: str(dtype) for col, dtype in df.dtypes.items()} - content_col = self._detect_content_column(collection_name, schema) - + # Get documents from content column (already detected above) if content_col in df.columns: documents = df[content_col].tolist() else: diff --git a/src/vector_inspector/ui/views/metadata/metadata_table.py b/src/vector_inspector/ui/views/metadata/metadata_table.py index 44f6bf5..3e4dc9f 100644 --- a/src/vector_inspector/ui/views/metadata/metadata_table.py +++ b/src/vector_inspector/ui/views/metadata/metadata_table.py @@ -149,6 +149,14 @@ def populate_table( # Re-pin preview column after resizeColumnsToContents table.setColumnWidth(PREVIEW_COL, 28) + # Apply max width constraints to prevent excessively wide columns + MAX_COLUMN_WIDTH = 600 # Maximum width in pixels + for col in range(table.columnCount()): + if col != PREVIEW_COL: # Skip preview column (already fixed) + current_width = table.columnWidth(col) + if current_width > MAX_COLUMN_WIDTH: + table.setColumnWidth(col, MAX_COLUMN_WIDTH) + def copy_vectors_to_json( table: QTableWidget, diff --git a/tests/providers/lancedb/test_flat_schema_metadata.py b/tests/providers/lancedb/test_flat_schema_metadata.py new file mode 100644 index 0000000..e0f30e2 --- /dev/null +++ b/tests/providers/lancedb/test_flat_schema_metadata.py @@ -0,0 +1,123 @@ +"""Test LanceDB flat schema metadata extraction (e.g., Contextus-style databases).""" + +import pytest + +pytest.importorskip("lancedb") + +import uuid +import lancedb + + +def test_flat_schema_metadata_extraction(tmp_path): + """Test that flat schema columns (non-nested) are correctly extracted as metadata.""" + # Create a LanceDB database with flat schema like Contextus uses + db_path = str(tmp_path) + db = lancedb.connect(db_path) + + # Create a table with flat schema columns (like Contextus) + table_name = f"contextus_test_{uuid.uuid4().hex[:8]}" + data = [ + { + "id": "1", + "vector": [0.1, 0.2, 0.3], + "document": "First document", + "project": "test-project", + "filename": "test.md", + "heading": "Introduction", + "type": "decision", + "chunk_index": 0, + }, + { + "id": "2", + "vector": [0.4, 0.5, 0.6], + "document": "Second document", + "project": "test-project", + "filename": "guide.md", + "heading": "Setup", + "type": "reference", + "chunk_index": 1, + }, + ] + + db.create_table(table_name, data=data, mode="overwrite") + + # Test with Vector Inspector connection + from vector_inspector.core.connections.lancedb_connection import LanceDBConnection + + conn = LanceDBConnection(uri=db_path) + assert conn.connect() + + # Test get_collection_info returns flat schema columns as metadata_fields + info = conn.get_collection_info(table_name) + assert info is not None + assert info["count"] == 2 + + # All non-reserved columns except content column should be in metadata_fields + metadata_fields = info["metadata_fields"] + # "document" is the content column and should be excluded from metadata + expected_fields = {"project", "filename", "heading", "type", "chunk_index"} + assert expected_fields.issubset(set(metadata_fields)), \ + f"Missing fields in metadata_fields. Expected {expected_fields}, got {set(metadata_fields)}" + # Verify document is NOT in metadata_fields (it's the content column) + assert "document" not in metadata_fields, "Content column 'document' should not be in metadata_fields" + + # Test get_all_items extracts flat columns as metadata (excluding content column) + items = conn.get_all_items(table_name, limit=10) + assert items is not None + assert len(items["ids"]) == 2 + assert len(items["metadatas"]) == 2 + + # Check first item's metadata contains the flat schema columns (but not document) + first_meta = items["metadatas"][0] + assert "project" in first_meta + assert "filename" in first_meta + assert "heading" in first_meta + assert "type" in first_meta + assert "chunk_index" in first_meta + # Verify document is NOT duplicated in metadata + assert "document" not in first_meta, "Content column should not appear in metadata" + + assert first_meta["project"] == "test-project" + assert first_meta["filename"] == "test.md" + assert first_meta["heading"] == "Introduction" + assert first_meta["type"] == "decision" + assert first_meta["chunk_index"] == 0 + + # Verify documents are properly extracted to the documents field + assert items["documents"][0] == "First document" + assert items["documents"][1] == "Second document" + + +def test_nested_metadata_column_still_works(tmp_path): + """Test that legacy nested metadata column format still works.""" + from vector_inspector.core.connections.lancedb_connection import LanceDBConnection + + collection_name = f"test_nested_{uuid.uuid4().hex[:8]}" + db_path = str(tmp_path) + + conn = LanceDBConnection(uri=db_path) + assert conn.connect() + + # Create collection with nested metadata (old format) + assert conn.create_collection(collection_name, vector_size=2) + + # Add items with metadata in the traditional nested format + test_docs = ["doc1", "doc2"] + test_metadata = [{"key1": "value1"}, {"key2": "value2"}] + test_ids = ["id1", "id2"] + test_vectors = [[0.1, 0.2], [0.3, 0.4]] + + assert conn.add_items( + collection_name, + documents=test_docs, + metadatas=test_metadata, + ids=test_ids, + embeddings=test_vectors, + ) + + # Verify nested metadata still works + items = conn.get_all_items(collection_name, limit=10) + assert items is not None + assert len(items["metadatas"]) == 2 + assert items["metadatas"][0].get("key1") == "value1" + assert items["metadatas"][1].get("key2") == "value2" diff --git a/tests/test_lancedb_metadata_padding.py b/tests/test_lancedb_metadata_padding.py index 081fc39..cedbe0b 100644 --- a/tests/test_lancedb_metadata_padding.py +++ b/tests/test_lancedb_metadata_padding.py @@ -64,7 +64,9 @@ def test_get_all_items_metadata_padding(lancedb_conn): assert len(result["ids"]) == 2 assert len(result["documents"]) == 2 assert len(result["metadatas"]) == 2 # Should be padded! - assert all(m == {} for m in result["metadatas"]) + # With flat schema support, but "document" is excluded as it's the content column + assert result["metadatas"][0] == {} + assert result["metadatas"][1] == {} def test_query_collection_with_sparse_metadata(lancedb_conn): From 88b722d793cff4e603b55476fe9a5783919377a6 Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Sun, 21 Jun 2026 13:43:55 -0400 Subject: [PATCH 2/6] docs: update CHANGELOG for v0.8.4 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c3317..2528380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to Vector Viewer will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.4] - 2026-06-21 + +### Fixed +- **LanceDB flat schema metadata extraction**: Fixed metadata display for LanceDB databases with flat schemas (e.g., Contextus) where metadata is stored as individual columns rather than nested in a 'metadata' column +- **Duplicate document column**: Prevented content column from appearing twice in data browser (once as content, once as metadata) +- **Column width limits**: Added 600px max-width constraint to table columns to prevent excessive width + +### Changed +- LanceDB connection now extracts metadata from PyArrow schema first for better performance +- Content column (e.g., "document") is automatically detected and excluded from metadata fields + +### Added +- Comprehensive tests for flat schema metadata extraction +- Backward compatibility maintained with nested 'metadata' column format + +## [0.8.3] - 2026-06-21 + +### Changed +- Moved hashlib import to module level in base_connection.py +- Improved platform-specific monospace font rendering in provider install dialog +- Centralized content column detection logic with single return path ### Added - Phase 1 Implementation From 081fa9a683548925feed7e15c752f27a3c65be05 Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Tue, 23 Jun 2026 11:56:28 -0400 Subject: [PATCH 3/6] ci: add PR checks for tests and linting - Enable CI test workflow on all PRs to master/main - Add new lint workflow with ruff (check + format) - Linting is advisory-only (won't block merges) until codebase is cleaned up - Tests remain blocking as expected --- .github/workflows/ci-tests.yml | 7 +++++++ .github/workflows/lint.yml | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index bd7a75c..9576175 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -12,7 +12,14 @@ on: - '["3.11", "3.12"]' - '["3.11"]' - '["3.12"]' + pull_request: + branches: + - master + - main push: + branches: + - master + - main paths: - 'pyproject.toml' - '.github/workflows/ci-tests.yml' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f06eb1a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint + +on: + pull_request: + branches: + - master + - main + push: + branches: + - master + - main + +jobs: + lint: + name: Lint with Ruff + runs-on: ubuntu-latest + continue-on-error: true # Advisory only - won't block PRs + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install Ruff + run: pip install ruff + + - name: Run Ruff linter + run: ruff check src/ tests/ --output-format=github + + - name: Run Ruff formatter check + run: ruff format --check src/ tests/ From 3da7b676ffa51d68b9ab4f18c4a44e85eef17dfc Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Tue, 23 Jun 2026 12:01:44 -0400 Subject: [PATCH 4/6] chore: finalize v0.8.4 release prep - Update RELEASE_REASON.md to v0.8.4 (matches CHANGELOG and pyproject.toml) - Add permissions blocks to CI workflows per GitHub security recommendation --- .github/workflows/ci-tests.yml | 3 +++ .github/workflows/lint.yml | 3 +++ docs/RELEASE_REASON.md | 26 ++++++++++++-------------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 9576175..fba7fd2 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -25,6 +25,9 @@ on: - '.github/workflows/ci-tests.yml' - 'tests/**' +permissions: + contents: read + jobs: test: name: Test on Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f06eb1a..8b96752 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,9 @@ on: - master - main +permissions: + contents: read + jobs: lint: name: Lint with Ruff diff --git a/docs/RELEASE_REASON.md b/docs/RELEASE_REASON.md index c848b6c..20b356c 100644 --- a/docs/RELEASE_REASON.md +++ b/docs/RELEASE_REASON.md @@ -1,24 +1,22 @@ -# Release Notes (0.8.3) — June 21, 2026 +# Release Notes (0.8.4) — June 23, 2026 -Content column detection improvements and local embedding support. +Critical LanceDB metadata extraction fixes and column rendering improvements. -## Features +## Bug Fixes -- **Dynamic content column detection**: Automatically detects the best content column for each collection based on schema analysis (text/string fields, common naming patterns). Manual override support with persistence across sessions. -- **Ollama embedding integration**: Local embedding generation via Ollama HTTP API for environments where HuggingFace is blocked or unavailable. +- **LanceDB flat schema metadata extraction**: Fixed metadata display for LanceDB databases with flat schemas (e.g., Contextus) where metadata is stored as individual columns rather than nested in a 'metadata' column +- **Duplicate document column**: Prevented content column from appearing twice in data browser (once as content, once as metadata) +- **Column width limits**: Added 600px max-width constraint to table columns to prevent excessive width from breaking UI ## Improvements -- Content column configuration UI shows both auto-detected recommendation and currently active column -- Settings persistence for content column overrides per collection -- Fixed LanceDB schema detection to use PyArrow schema instead of pandas DataFrame conversion (performance improvement) -- Fixed threading deadlock in content column detection (switched from Lock to RLock) -- Platform-specific monospace fonts in UI dialogs (Menlo on macOS, Consolas on Windows) +- LanceDB connection now extracts metadata from PyArrow schema first for better performance +- Content column (e.g., "document") is automatically detected and excluded from metadata fields +- Backward compatibility maintained with nested 'metadata' column format -## Bug Fixes +## Testing -- Fixed LanceDB metadata array length mismatch in search results -- Fixed QThread leak in embedding configuration dialog -- Fixed content column cache poisoning when schema unavailable +- Added comprehensive tests for flat schema metadata extraction +- CI workflow now runs unit tests on all pull requests --- From 380ceb7a15ad572e050e33fc775b11360a56b2aa Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Tue, 23 Jun 2026 12:03:25 -0400 Subject: [PATCH 5/6] chore: bump __version__ to 0.8.4 in __init__.py --- src/vector_inspector/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vector_inspector/__init__.py b/src/vector_inspector/__init__.py index ef8a1ee..0c4549f 100644 --- a/src/vector_inspector/__init__.py +++ b/src/vector_inspector/__init__.py @@ -1,6 +1,6 @@ """Vector Inspector - A comprehensive desktop application for vector database visualization.""" -__version__ = "0.8.3" # Keep in sync with pyproject.toml for dev mode fallback +__version__ = "0.8.4" # Keep in sync with pyproject.toml for dev mode fallback def get_version(): From 1491148aabdfd866f3a54aaf6f14f7dd52881618 Mon Sep 17 00:00:00 2001 From: Anthony Dawson Date: Tue, 23 Jun 2026 13:03:46 -0400 Subject: [PATCH 6/6] chore: ruff code cleanup - reduce violations from 1830 to 39 - Added ignore rules for ARG* (unused args required for interfaces) - Added ignore rules for test-specific patterns (E741, RUF003, DTZ005) - Applied ruff auto-fixes (--fix + --unsafe-fixes) - Formatted all code with ruff format - Fixed unused import in __init__.py Remaining 39 violations are edge cases that need manual review. Will address in follow-up commits before enabling blocking lint. --- .claude/context/CONTEXT.md | 5 + .../implementation-lancedb-flat-schema.md | 121 ++++++++++++++++++ .../reference/reference-subtitle-test-data.md | 36 ++++++ .../reference-version-bump-workflow.md | 16 ++- .coverage | Bin 0 -> 53248 bytes CLAUDE.md | 12 ++ pyproject.toml | 21 ++- src/vector_inspector/__init__.py | 2 +- src/vector_inspector/core/cache_manager.py | 2 +- .../core/connections/base_connection.py | 27 ++-- .../core/connections/lancedb_connection.py | 35 ++--- .../core/connections/milvus_connection.py | 2 +- .../core/connections/pgvector_connection.py | 28 ++-- .../core/connections/qdrant_connection.py | 8 +- .../qdrant_embedding_resolver.py | 14 +- .../qdrant_helpers/qdrant_filter_builder.py | 14 +- .../core/embedding_providers/__init__.py | 10 +- .../core/embedding_providers/base_provider.py | 50 ++++---- .../core/embedding_providers/clip_provider.py | 12 +- .../embedding_providers/provider_factory.py | 25 ++-- .../sentence_transformer_provider.py | 26 +--- src/vector_inspector/core/embedding_utils.py | 1 + .../core/llm_providers/provider_factory.py | 3 +- .../core/llm_providers/runtime_manager.py | 3 - src/vector_inspector/core/provider_factory.py | 4 +- .../services/backup_helpers.py | 29 +++-- .../services/cluster_runner.py | 4 +- src/vector_inspector/services/data_loaders.py | 6 +- .../services/filter_service.py | 37 +++--- .../services/profile_service.py | 8 +- .../services/update_service.py | 10 +- .../ui/components/connection_manager_panel.py | 75 +++-------- .../ui/components/inline_details_pane.py | 2 +- .../ui/components/item_dialog.py | 7 +- .../ui/components/profile_manager_panel.py | 2 +- .../ui/components/splash_window.py | 4 +- .../ui/components/update_details_dialog.py | 4 +- .../ui/controllers/connection_controller.py | 6 +- src/vector_inspector/ui/dialogs/__init__.py | 2 +- .../ui/dialogs/cross_db_migration.py | 20 +-- .../ui/dialogs/embedding_config_dialog.py | 30 ++--- .../ui/dialogs/provider_type_dialog.py | 94 +++++++------- src/vector_inspector/ui/loading_screen.py | 8 +- src/vector_inspector/ui/main_window.py | 8 +- .../ui/views/demo_collection_view.py | 8 +- src/vector_inspector/ui/views/info_panel.py | 34 ++--- .../ui/views/metadata/item_update_helpers.py | 3 +- .../ui/views/metadata/metadata_io.py | 8 +- .../ui/views/metadata/metadata_threads.py | 16 +-- .../ui/views/visualization/plot_panel.py | 10 +- .../ui/views/visualization_view.py | 2 +- .../components/test_profile_manager_panel.py | 8 +- tests/core/llm_providers/test_llama_cpp.py | 2 +- .../test_runtime_and_selection.py | 4 +- tests/core/test_clustering.py | 8 +- tests/core/test_provider_factory.py | 6 +- tests/core/test_qdrant_helpers.py | 8 +- tests/fakes/fake_provider.py | 12 +- .../test_clustering_real_embeddings.py | 2 +- tests/metadata/test_data_operations.py | 4 +- .../metadata/test_telemetry_settings_panel.py | 15 ++- .../lancedb/test_flat_schema_metadata.py | 3 +- .../pgvector/test_pgvector_connection.py | 36 +++--- .../pinecone/test_pinecone_connection.py | 3 +- .../weaviate/test_weaviate_connection.py | 14 +- tests/scripts/conftest.py | 1 + tests/scripts/test_bootstrap_installer.py | 17 +-- tests/services/test_backup_helpers.py | 3 - .../test_collection_service_edgecases.py | 10 +- tests/services/test_credential_service.py | 2 +- tests/services/test_filter_service.py | 12 +- tests/services/test_install_service.py | 4 +- tests/services/test_profile_service.py | 2 +- tests/services/test_telemetry_service.py | 1 - tests/services/test_visualization_service.py | 2 +- tests/test_content_column_detection.py | 3 +- tests/test_content_column_persistence.py | 1 + tests/test_lancedb_metadata_padding.py | 78 +++++------ tests/test_ollama_embedding.py | 49 +++---- tests/test_pgvector_content_column.py | 19 +-- tests/test_runner.py | 4 +- tests/ui/test_llm_console.py | 2 +- tests/utils/fake_llm_provider.py | 3 +- tests/utils/test_exception_handler.py | 2 +- tests/views/test_clustering_panel.py | 1 - tests/views/test_collection_worker.py | 2 +- tests/views/test_connection_view.py | 2 - .../test_metadata_view_inline_details.py | 2 +- tests/views/test_search_view_ask_ai.py | 1 - .../views/test_search_view_inline_details.py | 2 +- 90 files changed, 654 insertions(+), 610 deletions(-) create mode 100644 .claude/context/implementation/implementation-lancedb-flat-schema.md create mode 100644 .claude/context/reference/reference-subtitle-test-data.md create mode 100644 .coverage create mode 100644 CLAUDE.md diff --git a/.claude/context/CONTEXT.md b/.claude/context/CONTEXT.md index c73c4da..2d1e416 100644 --- a/.claude/context/CONTEXT.md +++ b/.claude/context/CONTEXT.md @@ -23,6 +23,11 @@ The pattern itself. - [Branch Protection Workflow](decisions/decision-branch-protection-workflow.md) — Never push directly to master; all changes via feature branches and PRs +## Implementation + +- [LanceDB Flat Schema Support](implementation/implementation-lancedb-flat-schema.md) — Handle both flat schemas (Contextus) and nested metadata with content column detection + ## Reference +- [Subtitle Test Data](reference/reference-subtitle-test-data.md) — Subtitle files as ideal test data for semantic search and embedding validation - [Version Bump Workflow](reference/reference-version-bump-workflow.md) — Files that need updating when bumping release version diff --git a/.claude/context/implementation/implementation-lancedb-flat-schema.md b/.claude/context/implementation/implementation-lancedb-flat-schema.md new file mode 100644 index 0000000..9c092b3 --- /dev/null +++ b/.claude/context/implementation/implementation-lancedb-flat-schema.md @@ -0,0 +1,121 @@ +--- +name: lancedb-flat-schema-support +description: LanceDB databases can use flat schemas (columns) or nested metadata - handle both patterns with content column detection +metadata: + type: implementation + tags: [lancedb, metadata, schema, contextus] +--- + +# LanceDB Flat Schema Support + +## Problem + +LanceDB supports two metadata storage patterns: + +1. **Nested metadata** (traditional): Single `metadata` column containing a dict/JSON +2. **Flat schema** (Contextus-style): Individual columns like `project`, `filename`, `heading`, `type`, `chunk_index` + +Vector Inspector originally only supported pattern #1, causing Contextus databases to show empty metadata. + +## Solution Pattern + +### In `get_collection_info()` + +1. **Extract from PyArrow schema first** (most reliable): + ```python + schema = tbl.schema + reserved_columns = {"id", "vector", "embedding", "_distance"} + + # Detect content column to exclude it + temp_schema = {field.name: str(field.type) for field in schema} + content_col = self._detect_content_column(name, temp_schema) + reserved_columns.add(content_col) + + # All other columns are metadata + metadata_fields = [ + field.name for field in schema + if field.name not in reserved_columns and not field.name.startswith("_") + ] + ``` + +2. **Fallback to pandas** if schema extraction fails + +3. **CRITICAL**: Must also check `count is None` in fallback condition, not just metadata/vector detection + +### In `get_all_items()` + +1. **Detect content column FIRST** to build reserved set: + ```python + schema = {col: str(dtype) for col, dtype in df.dtypes.items()} + content_col = self._detect_content_column(collection_name, schema) + reserved_columns = {"id", "vector", "embedding", "_distance", content_col} + ``` + +2. **Check for nested metadata column first**: + ```python + if "metadata" in df.columns: + # Parse nested format + raw_meta = df["metadata"].tolist() + metadatas = self._parse_metadata_list(raw_meta) + ``` + +3. **Otherwise extract flat schema columns**: + ```python + else: + metadata_columns = [ + col for col in df.columns + if col not in reserved_columns and not col.startswith("_") + ] + + if metadata_columns: + records = df[metadata_columns].to_dict('records') + metadatas = [ + {k: v for k, v in record.items() + if v is not None and (not isinstance(v, float) or not math.isnan(v))} + for record in records + ] + ``` + +## Why Exclude Content Column + +The content column (e.g., "document", "text", "content") serves a special purpose: +- It's displayed in the "Document" column of the data browser +- If also included in metadata, it appears TWICE in the table (once as "Document", once as "document") +- Users expect metadata to be *additional* fields, not duplicates of the main content + +## Edge Cases + +1. **Empty content column**: If content detection returns None or empty string, don't add it to reserved set +2. **NaN values**: Filter out `math.isnan()` values when building metadata dicts from flat schemas +3. **Count detection**: Even if schema extraction succeeds, must still fetch dataframe if `count is None` +4. **Backward compatibility**: Nested metadata format must continue to work (used by most LanceDB databases) + +## Testing + +Always test both patterns: +```python +# Flat schema (Contextus-style) +data = [ + { + "id": "1", + "vector": [0.1, 0.2, 0.3], + "document": "content", + "project": "test", + "filename": "file.md", + } +] + +# Nested metadata (traditional) +conn.add_items( + collection, + documents=["doc1"], + metadatas=[{"key": "value"}], + ids=["id1"], + embeddings=[[0.1, 0.2]] +) +``` + +## Files Modified + +- `src/vector_inspector/core/connections/lancedb_connection.py`: Both `get_collection_info()` and `get_all_items()` +- Tests: `tests/providers/lancedb/test_flat_schema_metadata.py` diff --git a/.claude/context/reference/reference-subtitle-test-data.md b/.claude/context/reference/reference-subtitle-test-data.md new file mode 100644 index 0000000..6cc9a55 --- /dev/null +++ b/.claude/context/reference/reference-subtitle-test-data.md @@ -0,0 +1,36 @@ +--- +name: subtitle-test-data +description: Subtitle files as ideal test data for semantic search and embedding validation +metadata: + type: reference + tags: [testing, embeddings, test-data, semantic-search] +--- + +## Subtitle Files for Semantic Search Testing + +Subtitle files (.srt, .vtt, .sub) make excellent test data for semantic search and embedding systems. + +### Why Subtitles Work Well + +1. **Natural Language**: Real conversational text with context and flow +2. **Sequential Context**: Lines build on each other, testing context window handling +3. **Varied Domains**: Movies, documentaries, lectures provide diverse vocabulary +4. **Timestamp Metadata**: Built-in temporal structure for testing metadata handling +5. **Manageable Size**: Individual subtitle entries are good chunk sizes (typically 1-3 sentences) +6. **Widely Available**: Easy to obtain for testing without licensing concerns + +### Characteristics for Embedding Tests + +- **Semantic Relationships**: Related dialogue lines test similarity search accuracy +- **Temporal Coherence**: Tests whether embeddings capture narrative flow +- **Speaker Attribution**: Can test multi-speaker scenarios +- **Domain Shifts**: Scene changes test how embeddings handle topic transitions + +### Usage in Vector Inspector + +Use subtitle files to validate: +- Content column detection across providers +- Embedding quality with different models (OpenAI, Ollama, HuggingFace) +- Search result relevance and ranking +- Metadata extraction and display +- Performance with realistic document collections diff --git a/.claude/context/reference/reference-version-bump-workflow.md b/.claude/context/reference/reference-version-bump-workflow.md index d3a8191..41fb198 100644 --- a/.claude/context/reference/reference-version-bump-workflow.md +++ b/.claude/context/reference/reference-version-bump-workflow.md @@ -1,14 +1,17 @@ --- name: version-bump-workflow -description: How to bump release version in vector-inspector - files that need updating +description: Version bump checklist - always update pyproject.toml, src/vector_inspector/__init__.py __version__, and docs/RELEASE_REASON.md together when changing release version numbers metadata: type: reference - tags: [release, versioning, workflow] + tags: [release, versioning, workflow, version-bump, release-prep, changelog, pyproject, __init__, RELEASE_REASON] + trigger_files: [pyproject.toml, src/vector_inspector/__init__.py, docs/RELEASE_REASON.md] --- # Version Bump Workflow -When bumping a release version in vector-inspector, **three files** must be updated to stay in sync: +When bumping a release version in vector-inspector (changing version numbers, updating pyproject.toml version field, preparing for release), **three files** must be updated to stay in sync: + +**Critical**: Never update just pyproject.toml or just __init__.py alone — all three files must change together or the release will be incomplete. ## Files to Update @@ -76,8 +79,13 @@ def get_version(): ❌ Only updating `pyproject.toml` → `__version__` out of sync for dev mode ❌ Only updating `__init__.py` → Release workflow won't trigger ❌ Forgetting `docs/RELEASE_REASON.md` → Release has no description +❌ Updating pyproject.toml version without checking __init__.py +❌ Changing version in code but not updating release notes +❌ Editing CHANGELOG.md but forgetting to sync __version__ -✅ Update all three files together +✅ Update all three files together — pyproject.toml, __init__.py, and RELEASE_REASON.md +✅ Always check __init__.py when pyproject.toml version changes +✅ Version numbers must match across all three files ## Related diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..81e8f0eab05d2a89c17ced710cb4fc7f2e91ae17 GIT binary patch literal 53248 zcmeI)O>Y}T7zgm(y7k76BL_v16>`X2kYm+xWmSl`0|bafMI|T%#0ht7kL?BTuCp(B zIUwXxseA!$d?CI}B@Udp@H{iSUOS1a-YPBsRqX7`%C!zqab%Teb2JR{h7Vx9dl>aqaKT zKWpz-zT0%2cBMlXut5L<5P(2o1m;g_PGfJ+e)dZ&`%@K%a-c%*OTFu_*q@Gb%OM6vAT@S2`WUnW1mxq!!&zKgleQh6m0Vpr6f^Xy`;9%A*k#Gzh{W*S;T0e=t!|TgXL(gMjLJ8JlRsz97XsmYn%Ye^^1d`r$Wd761dmGgs) zHP6Vmrp&s|e&Y3s{HMg5apO=9bK^Kzmvdd-I7owi?Y6n4E%SI+nlmHk7B#sN-#tsz zqVd}z&)L>t_(5B65Yz+S*(^6c*s(3@mFhA&o6<+8o=oCEANQ#7dtG{~J%4M%X&fBb zFV1vFVV+*!jibd*Wu?~XA;Y;%43XG*!HLuiW@(cU{G~3cW7Bm!EIn(N+Gvr1d?T^!ZgJ*8^t%+Xij9rvryC&{UDes>6^8Iay*LYo^B*`9d52lALnI6S6s&Uys7JyEiOFGpVFYq z#ux2)p7e+5dd17IDV64a9-Mq1Gl@_}(WF5c(Ga~>W>#o=m1nH1{LYL_xKvLL&2Z;N zSt{{jqHYvtR-?~ltYX(wf|KaV=#F(EBQXdST};}yT(6ZW<_o%#&7mfmR+(B)OXhFS*8ISB8u#zp^CTU?W^(F9^hv;zS*+>dot2(-?PA`^zM@&0J|Zo5 zE(TvR|ZSfa;yZ*05KiD7u0SG_<0uX=z1Rwwb2tWV=5GbBN z$-ZMd=KKFktNyZh0U{y@KmY;|fB*y_009U<00Izz!2eTV+b-?zrGE$Dy-xRG=iMqn z*!6!b`oRVP2tWV=5P$##AOHafKmY;|fIyK1s-^u(`lkT;ANX$HVqYpp;0uX=z1Rwwb2tWV=5P$##iZ6iY|HW@(L +## Context (claude-contextus) + +This project uses [claude-contextus](https://github.com/anthonypdawson/claude-contextus): +relevant patterns from `.claude/context/` are **retrieved and injected automatically** +on each prompt — you don't need to search for them. Injected blocks are tagged +`[contextus — …]`. + +Context capture behavior is controlled by the per-prompt injected footer — follow +its instructions exactly (it may tell you to propose, write directly, or stay silent). +New context files live under `.claude/context//` and are embedded automatically. + diff --git a/pyproject.toml b/pyproject.toml index ba95bf4..d95ec5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -204,7 +204,26 @@ select = [ "ARG", "RUF", ] -ignore = ["E501", "I001", "UP045", "SIM105", "SIM108", "SIM117"] +ignore = [ + "E501", # Line too long (black handles this) + "I001", # Import block unsorted + "UP045", # Use Union[X, Y] instead of X | Y for py3.9 compat + "SIM105", # Use contextlib.suppress instead of try/except pass + "SIM108", # Use ternary operator + "SIM117", # Use single with statement + # Unused arguments - often required for interface compliance (Qt signals, abstract methods) + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "ARG003", # Unused class method argument + "ARG004", # Unused static method argument + "ARG005", # Unused lambda argument + # Test-specific tolerances + "E741", # Ambiguous variable name (l, O, I) - common in math/test code + "RUF003", # Ambiguous unicode in comments (×, −) - intentional in documentation + "RUF002", # Ambiguous unicode in docstrings + "DTZ005", # datetime.now() without tz - not critical for cache timestamps + "B904", # raise without from - acceptable for wrapped import errors +] [tool.ruff.lint.isort] combine-as-imports = true diff --git a/src/vector_inspector/__init__.py b/src/vector_inspector/__init__.py index 0c4549f..8ba9038 100644 --- a/src/vector_inspector/__init__.py +++ b/src/vector_inspector/__init__.py @@ -5,7 +5,7 @@ def get_version(): try: - from importlib.metadata import PackageNotFoundError, version + from importlib.metadata import version return version("vector-inspector") except Exception: diff --git a/src/vector_inspector/core/cache_manager.py b/src/vector_inspector/core/cache_manager.py index ba620e0..2c7f1d2 100644 --- a/src/vector_inspector/core/cache_manager.py +++ b/src/vector_inspector/core/cache_manager.py @@ -101,7 +101,7 @@ def invalidate(self, database: Optional[str] = None, collection: Optional[str] = self._cache.clear() elif collection is None and database is not None: # Clear all collections in the specified database - keys_to_remove = [key for key in self._cache.keys() if key[0] == database] + keys_to_remove = [key for key in self._cache if key[0] == database] for key in keys_to_remove: del self._cache[key] elif database is not None and collection is not None: diff --git a/src/vector_inspector/core/connections/base_connection.py b/src/vector_inspector/core/connections/base_connection.py index 220a999..cff0c78 100644 --- a/src/vector_inspector/core/connections/base_connection.py +++ b/src/vector_inspector/core/connections/base_connection.py @@ -94,10 +94,9 @@ def provider_type(self) -> str: # removesuffix avoids the blanket .replace("db", "") which would corrupt # names like "LanceDBConnection" (would become "lance" instead of "lancedb"). class_name = type(self).__name__.removesuffix("Connection") - provider_name = class_name.lower() + return class_name.lower() # Return the canonical registry ID (e.g. "chromadb") so that # provider_type can be matched directly against ProviderInfo.id. - return provider_name @property def supports_configurable_vector_size(self) -> bool: @@ -129,7 +128,7 @@ def _detect_content_column( collection_name: str, schema: dict[str, str] | None = None, override: str | None = None, - skip_cache: bool = False + skip_cache: bool = False, ) -> str: """Detect the content/text column name for a collection. @@ -155,11 +154,10 @@ def _detect_content_column( if is_override or cached_fingerprint == current_fingerprint or not current_fingerprint: # Use cached value if: manually overridden, schema unchanged, or no schema to compare return self._content_column_cache[collection_name] - else: - # Schema changed - invalidate cache for this collection - del self._content_column_cache[collection_name] - if collection_name in self._schema_fingerprints: - del self._schema_fingerprints[collection_name] + # Schema changed - invalidate cache for this collection + del self._content_column_cache[collection_name] + if collection_name in self._schema_fingerprints: + del self._schema_fingerprints[collection_name] # Use override if provided if override: @@ -181,8 +179,14 @@ def _detect_content_column( # Fallback: use priority order for common text column names if not detected_column: fallback_priority = [ - "description", "summary", "title", "name", - "message", "comment", "note", "snippet" + "description", + "summary", + "title", + "name", + "message", + "comment", + "note", + "snippet", ] for fallback_name in fallback_priority: @@ -199,6 +203,7 @@ def _detect_content_column( if col_name.lower() not in reserved: if any(t in col_type.lower() for t in text_types): from vector_inspector.core.logging import log_info + log_info( "Content column auto-detected (last resort): '%s' for collection '%s'", col_name, @@ -274,6 +279,7 @@ def _load_persisted_overrides(self) -> None: """Load persisted content column overrides from settings.""" try: from vector_inspector.services.settings_service import SettingsService + settings = SettingsService() overrides_data = settings.get(self._get_override_key(), {}) @@ -290,6 +296,7 @@ def _persist_overrides(self) -> None: """Persist content column overrides to settings.""" try: from vector_inspector.services.settings_service import SettingsService + settings = SettingsService() with self._cache_lock: diff --git a/src/vector_inspector/core/connections/lancedb_connection.py b/src/vector_inspector/core/connections/lancedb_connection.py index a558285..e5547ba 100644 --- a/src/vector_inspector/core/connections/lancedb_connection.py +++ b/src/vector_inspector/core/connections/lancedb_connection.py @@ -34,7 +34,6 @@ def connect(self) -> bool: if isinstance(self._uri, str) and "://" not in self._uri: self._uri = os.path.abspath(os.path.expanduser(self._uri)) self._db = lancedb.connect(self._uri) - print(f"LanceDB connect: uri={self._uri}") self._client = self._db self._connected = True return True @@ -103,7 +102,8 @@ def get_collection_info(self, name: str) -> dict[str, Any] | None: if schema: # All non-reserved columns (excluding content) are metadata fields metadata_fields = [ - field.name for field in schema + field.name + for field in schema if field.name not in reserved_columns and not field.name.startswith("_") ] @@ -142,8 +142,7 @@ def get_collection_info(self, name: str) -> dict[str, Any] | None: if not metadata_fields: # First check for flat schema columns metadata_fields = [ - col for col in df.columns - if col not in reserved_columns and not col.startswith("_") + col for col in df.columns if col not in reserved_columns and not col.startswith("_") ] # Also check for nested metadata column (legacy format) @@ -153,20 +152,17 @@ def get_collection_info(self, name: str) -> dict[str, Any] | None: first_meta = df.iloc[0].get("metadata") if isinstance(first_meta, str): import ast + try: parsed = ast.literal_eval(first_meta) if isinstance(parsed, dict): # Add nested metadata keys as separate fields - metadata_fields.extend( - f"metadata.{k}" for k in parsed.keys() - ) + metadata_fields.extend(f"metadata.{k}" for k in parsed) except Exception: # If parsing fails, keep "metadata" as a column metadata_fields.append("metadata") elif isinstance(first_meta, dict): - metadata_fields.extend( - f"metadata.{k}" for k in first_meta.keys() - ) + metadata_fields.extend(f"metadata.{k}" for k in first_meta) else: # Not a dict, keep "metadata" as a column metadata_fields.append("metadata") @@ -233,17 +229,14 @@ def create_collection(self, name: str, vector_size: int, distance: str = "Cosine "metadata": "{}", } ] - print(f"LanceDB create_collection: Creating table '{name}' with vector_size={vector_size}") self._db.create_table(name, data=dummy_data) - print(f"LanceDB create_collection: Table '{name}' created successfully") # Cache the declared vector size for this collection try: self._collection_meta[name] = int(vector_size) except Exception: pass return True - except Exception as e: - print(f"LanceDB create_collection failed: {e}") + except Exception: return False def add_items( @@ -610,8 +603,7 @@ def query_collection( else: # Fallback: try to use a 'document' key if present in metadata, else empty string documents = [ - m.get(content_col, "") if isinstance(m, dict) and content_col in m else "" - for m in metadatas + m.get(content_col, "") if isinstance(m, dict) and content_col in m else "" for m in metadatas ] # LanceDB returns '_distance' not 'score' @@ -675,19 +667,19 @@ def get_all_items( else: # Flat schema format: all non-reserved columns are metadata metadata_columns = [ - col for col in df.columns - if col not in reserved_columns and not col.startswith("_") + col for col in df.columns if col not in reserved_columns and not col.startswith("_") ] if metadata_columns: # Build metadata dicts from the flat columns # Use .to_dict('records') for efficient row-wise conversion - records = df[metadata_columns].to_dict('records') + records = df[metadata_columns].to_dict("records") metadatas = [] for record in records: # Filter out NaN/None values meta = { - k: v for k, v in record.items() + k: v + for k, v in record.items() if v is not None and (not isinstance(v, float) or not math.isnan(v)) } metadatas.append(meta) @@ -711,8 +703,7 @@ def get_all_items( else: # Fallback: prefer content_col key inside metadata if present, else empty string documents = [ - m.get(content_col, "") if isinstance(m, dict) and content_col in m else "" - for m in metadatas + m.get(content_col, "") if isinstance(m, dict) and content_col in m else "" for m in metadatas ] return { diff --git a/src/vector_inspector/core/connections/milvus_connection.py b/src/vector_inspector/core/connections/milvus_connection.py index d9456a7..e557ec0 100644 --- a/src/vector_inspector/core/connections/milvus_connection.py +++ b/src/vector_inspector/core/connections/milvus_connection.py @@ -526,7 +526,7 @@ def query_collection( query_embeddings: list[list[float]] | None = None, n_results: int = 10, where: dict[str, Any] | None = None, - where_document: dict[str, Any] | None = None, # noqa: ARG002 + where_document: dict[str, Any] | None = None, ) -> dict[str, Any] | None: """ Query a collection for similar vectors. diff --git a/src/vector_inspector/core/connections/pgvector_connection.py b/src/vector_inspector/core/connections/pgvector_connection.py index 48948f1..800cb4b 100644 --- a/src/vector_inspector/core/connections/pgvector_connection.py +++ b/src/vector_inspector/core/connections/pgvector_connection.py @@ -117,12 +117,11 @@ def list_collections(self) -> list[str]: with self._client.cursor() as cur: cur.execute(""" SELECT DISTINCT table_name FROM information_schema.columns - WHERE data_type = 'USER-DEFINED' + WHERE data_type = 'USER-DEFINED' AND udt_name = 'vector' AND table_schema = 'public' """) - tables = [row[0] for row in cur.fetchall()] - return tables + return [row[0] for row in cur.fetchall()] except Exception as e: log_tracked_error( "Failed to list collections: %s", @@ -201,7 +200,7 @@ def get_collection_info(self, name: str) -> dict[str, Any] | None: # Get schema to identify metadata columns schema = self._get_table_schema(name) content_col = self._detect_content_column(name, schema) - metadata_fields = [col for col in schema.keys() if col not in ["id", content_col, "embedding"]] + metadata_fields = [col for col in schema if col not in ["id", content_col, "embedding"]] # Try to determine vector dimension and detect stored embedding model from a sample row vector_dimension = "Unknown" @@ -384,9 +383,9 @@ def add_items( # Use JSONB metadata column metadata_json = json.dumps(metadata) if metadata else None cur.execute( - sql.SQL( - "INSERT INTO {} (id, {}, metadata, embedding) VALUES (%s, %s, %s, %s)" - ).format(sql.Identifier(collection_name), sql.Identifier(content_col)), + sql.SQL("INSERT INTO {} (id, {}, metadata, embedding) VALUES (%s, %s, %s, %s)").format( + sql.Identifier(collection_name), sql.Identifier(content_col) + ), (item_id, doc, metadata_json, emb), ) else: @@ -464,7 +463,7 @@ def get_items(self, name: str, ids: list[str]) -> dict[str, Any]: result_embeds = [] for row in rows: - row_dict = dict(zip(colnames, row)) + row_dict = dict(zip(colnames, row, strict=False)) result_ids.append(str(row_dict.get("id", ""))) result_docs.append(row_dict.get(content_col, "")) @@ -554,8 +553,7 @@ def count_collection(self, name: str) -> int: with self._client.cursor() as cur: cur.execute(sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(name))) result = cur.fetchone() - count = result[0] if result else 0 - return count + return result[0] if result else 0 except Exception as e: log_tracked_error( "Failed to count collection: %s", @@ -678,7 +676,7 @@ def query_collection( dists_q = [] for row in rows: - row_dict = dict(zip(colnames, row)) + row_dict = dict(zip(colnames, row, strict=False)) ids_q.append(str(row_dict.get("id", ""))) docs_q.append(row_dict.get(content_col, "")) @@ -800,7 +798,7 @@ def get_all_items( result_embeds = [] for row in rows: - row_dict = dict(zip(colnames, row)) + row_dict = dict(zip(colnames, row, strict=False)) result_ids.append(str(row_dict.get("id", ""))) result_docs.append(row_dict.get(content_col, "")) @@ -884,7 +882,7 @@ def update_items( docs_to_compute = [documents[i] for i in compute_idxs] computed = self.compute_embeddings_for_documents(collection_name, docs_to_compute) embeddings_local = [None] * len(ids) - for idx, emb in zip(compute_idxs, computed): + for idx, emb in zip(compute_idxs, computed, strict=False): embeddings_local[idx] = emb # Record how many embeddings we generated try: @@ -1071,8 +1069,8 @@ def _get_table_schema(self, table_name: str) -> dict[str, str]: try: with self._client.cursor() as cur: cur.execute( - """SELECT column_name, data_type, udt_name - FROM information_schema.columns + """SELECT column_name, data_type, udt_name + FROM information_schema.columns WHERE table_name = %s AND table_schema = 'public' ORDER BY ordinal_position""", (table_name,), diff --git a/src/vector_inspector/core/connections/qdrant_connection.py b/src/vector_inspector/core/connections/qdrant_connection.py index 016b98a..6195778 100644 --- a/src/vector_inspector/core/connections/qdrant_connection.py +++ b/src/vector_inspector/core/connections/qdrant_connection.py @@ -223,7 +223,7 @@ def get_collection_info(self, name: str) -> Optional[dict[str, Any]]: point = sample[0][0] if point.payload: # Extract metadata fields, excluding 'document' if present - metadata_fields = [k for k in point.payload.keys() if k != "document"] + metadata_fields = [k for k in point.payload if k != "document"] # Extract vector configuration vector_dimension = "Unknown" @@ -523,7 +523,7 @@ def get_all_items( qdrant_filter = self._build_qdrant_filter(where) # Use scroll to retrieve items - points, next_offset = self._client.scroll( + points, _next_offset = self._client.scroll( collection_name=collection_name, scroll_filter=qdrant_filter, limit=limit, @@ -551,7 +551,7 @@ def get_all_items( # Extract embedding if isinstance(point.vector, dict): # Named vectors - use the first one - embeddings.append(list(point.vector.values())[0] if point.vector else []) + embeddings.append(next(iter(point.vector.values())) if point.vector else []) else: embeddings.append(point.vector if point.vector else []) @@ -655,7 +655,7 @@ def add_items( # Build points points = [] - for i, (doc_id, document, embedding) in enumerate(zip(ids, documents, embeddings)): + for i, (doc_id, document, embedding) in enumerate(zip(ids, documents, embeddings, strict=False)): # Build payload with document and metadata payload = {"document": document} if metadatas and i < len(metadatas): diff --git a/src/vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py b/src/vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py index a10dd94..1093f3a 100644 --- a/src/vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py +++ b/src/vector_inspector/core/connections/qdrant_helpers/qdrant_embedding_resolver.py @@ -1,9 +1,7 @@ -from typing import Tuple - from vector_inspector.core.embedding_utils import get_model_for_dimension, load_embedding_model, DEFAULT_MODEL -def resolve_embedding_model(connection, collection_name: str) -> Tuple[object, str, str]: +def resolve_embedding_model(connection, collection_name: str) -> tuple[object, str, str]: """Resolve an embedding model for a collection. Returns (model, model_name, model_type). This encapsulates the previous @@ -17,15 +15,15 @@ def resolve_embedding_model(connection, collection_name: str) -> Tuple[object, s return (model, model_name, model_type) # Priority 1: explicit metadata on collection - if 'embedding_model' in collection_info: - model_name = collection_info['embedding_model'] - model_type = collection_info.get('embedding_model_type', 'sentence-transformer') + if "embedding_model" in collection_info: + model_name = collection_info["embedding_model"] + model_type = collection_info.get("embedding_model_type", "sentence-transformer") model = load_embedding_model(model_name, model_type) return (model, model_name, model_type) # Priority 3: guess by vector dimension - vector_dim = collection_info.get('vector_dimension') - if not vector_dim or vector_dim == 'Unknown': + vector_dim = collection_info.get("vector_dimension") + if not vector_dim or vector_dim == "Unknown": model_name, model_type = DEFAULT_MODEL model = load_embedding_model(model_name, model_type) return (model, model_name, model_type) diff --git a/src/vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py b/src/vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py index 49f3b72..9100b50 100644 --- a/src/vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py +++ b/src/vector_inspector/core/connections/qdrant_helpers/qdrant_filter_builder.py @@ -1,8 +1,8 @@ -from typing import Optional, Dict, Any, List +from typing import Optional, Any from qdrant_client.models import Filter, FieldCondition, MatchValue, MatchText, MatchAny, MatchExcept, Range -def build_filter(where: Optional[Dict[str, Any]] = None) -> Optional[Filter]: +def build_filter(where: Optional[dict[str, Any]] = None) -> Optional[Filter]: """Build a Qdrant `Filter` from a Chroma-style `where` dict. This mirrors the previous inline logic in `QdrantConnection._build_qdrant_filter`. @@ -11,8 +11,8 @@ def build_filter(where: Optional[Dict[str, Any]] = None) -> Optional[Filter]: return None try: - must_conditions: List[FieldCondition] = [] - must_not_conditions: List[FieldCondition] = [] + must_conditions: list[FieldCondition] = [] + must_not_conditions: list[FieldCondition] = [] for key, value in where.items(): if isinstance(value, dict): @@ -44,8 +44,10 @@ def build_filter(where: Optional[Dict[str, Any]] = None) -> Optional[Filter]: must_conditions.append(FieldCondition(key=key, match=MatchValue(value=value))) if must_conditions or must_not_conditions: - return Filter(must=must_conditions if must_conditions else None, - must_not=must_not_conditions if must_not_conditions else None) + return Filter( + must=must_conditions if must_conditions else None, + must_not=must_not_conditions if must_not_conditions else None, + ) return None except Exception: return None diff --git a/src/vector_inspector/core/embedding_providers/__init__.py b/src/vector_inspector/core/embedding_providers/__init__.py index d36733f..41be6e3 100644 --- a/src/vector_inspector/core/embedding_providers/__init__.py +++ b/src/vector_inspector/core/embedding_providers/__init__.py @@ -6,9 +6,9 @@ from .provider_factory import ProviderFactory __all__ = [ - 'EmbeddingProvider', - 'EmbeddingMetadata', - 'SentenceTransformerProvider', - 'CLIPProvider', - 'ProviderFactory', + "CLIPProvider", + "EmbeddingMetadata", + "EmbeddingProvider", + "ProviderFactory", + "SentenceTransformerProvider", ] diff --git a/src/vector_inspector/core/embedding_providers/base_provider.py b/src/vector_inspector/core/embedding_providers/base_provider.py index 227196a..a098e9b 100644 --- a/src/vector_inspector/core/embedding_providers/base_provider.py +++ b/src/vector_inspector/core/embedding_providers/base_provider.py @@ -2,13 +2,14 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import List, Union, Optional, Any +from typing import Optional, Any from enum import Enum import numpy as np class Modality(Enum): """Embedding modality types.""" + TEXT = "text" IMAGE = "image" MULTIMODAL = "multimodal" @@ -16,6 +17,7 @@ class Modality(Enum): class Normalization(Enum): """Embedding normalization types.""" + NONE = "none" L2 = "l2" @@ -23,6 +25,7 @@ class Normalization(Enum): @dataclass class EmbeddingMetadata: """Metadata about an embedding model.""" + name: str # Model identifier (e.g., "all-MiniLM-L6-v2") dimension: int # Vector dimension modality: Modality # text, image, or multimodal @@ -36,74 +39,69 @@ class EmbeddingMetadata: class EmbeddingProvider(ABC): """Abstract base class for embedding providers. - + Providers handle loading, encoding, and metadata extraction for embedding models. They implement lazy-loading to avoid UI freezes when working with large models. """ - + def __init__(self, model_name: str): """Initialize provider with a model name. - + Args: model_name: Model identifier (HuggingFace ID, path, or API model name) """ self.model_name = model_name self._model = None # Lazy-loaded model instance self._is_loaded = False - + @abstractmethod def get_metadata(self) -> EmbeddingMetadata: """Get metadata about the embedding model. - + This should be fast and not require loading the full model if possible. - + Returns: EmbeddingMetadata with model information """ pass - + @abstractmethod - def encode( - self, - inputs: Union[str, List[str], Any], - normalize: bool = True, - show_progress: bool = False - ) -> np.ndarray: + def encode(self, inputs: str | list[str] | Any, normalize: bool = True, show_progress: bool = False) -> np.ndarray: """Encode inputs into embeddings. - + Args: inputs: Text strings, images, or other inputs depending on modality normalize: Whether to L2-normalize the embeddings show_progress: Whether to show progress bar for batch encoding - + Returns: numpy array of embeddings, shape (n_inputs, dimension) """ pass - + def warmup(self, progress_callback=None): """Load and initialize the model (warm up for faster subsequent calls). - + Args: progress_callback: Optional callback(message: str, progress: float) for UI updates """ if self._is_loaded: return - + if progress_callback: progress_callback(f"Loading {self.model_name}...", 0.0) - + self._load_model() self._is_loaded = True - + if progress_callback: progress_callback(f"Model {self.model_name} loaded", 1.0) - + @abstractmethod def _load_model(self): """Internal method to load the actual model. Override in subclasses.""" pass - + def close(self): """Release model resources and cleanup.""" if self._model is not None: @@ -111,17 +109,17 @@ def close(self): del self._model self._model = None self._is_loaded = False - + @property def is_loaded(self) -> bool: """Check if model is currently loaded in memory.""" return self._is_loaded - + def __enter__(self): """Context manager support.""" self.warmup() return self - + def __exit__(self, exc_type, exc_val, exc_tb): """Context manager cleanup.""" self.close() diff --git a/src/vector_inspector/core/embedding_providers/clip_provider.py b/src/vector_inspector/core/embedding_providers/clip_provider.py index 4df3f28..4f7ebe3 100644 --- a/src/vector_inspector/core/embedding_providers/clip_provider.py +++ b/src/vector_inspector/core/embedding_providers/clip_provider.py @@ -65,9 +65,7 @@ def get_metadata(self) -> EmbeddingMetadata: ) except ImportError: - raise ImportError( - "transformers library not installed. Install with: pip install transformers" - ) + raise ImportError("transformers library not installed. Install with: pip install transformers") except Exception: # Fallback metadata self._metadata = EmbeddingMetadata( @@ -178,9 +176,7 @@ def encode( with torch.no_grad(): if input_type == "text": # Process text - processed = self._processor( - text=inputs, return_tensors="pt", padding=True, truncation=True - ) + processed = self._processor(text=inputs, return_tensors="pt", padding=True, truncation=True) # Move to same device as model if next(self._model.parameters()).is_cuda: @@ -276,6 +272,4 @@ def similarity( corpus_emb = self.encode(corpus, normalize=True, input_type=corpus_type) # Compute cosine similarity (dot product if normalized) - similarities = np.dot(corpus_emb, query_emb.T).squeeze() - - return similarities + return np.dot(corpus_emb, query_emb.T).squeeze() diff --git a/src/vector_inspector/core/embedding_providers/provider_factory.py b/src/vector_inspector/core/embedding_providers/provider_factory.py index 93defdb..3d9289b 100644 --- a/src/vector_inspector/core/embedding_providers/provider_factory.py +++ b/src/vector_inspector/core/embedding_providers/provider_factory.py @@ -1,6 +1,6 @@ """Factory for creating embedding providers.""" -from typing import Optional, Dict, Type +from typing import Optional from .base_provider import EmbeddingProvider from .sentence_transformer_provider import SentenceTransformerProvider from .clip_provider import CLIPProvider @@ -11,7 +11,7 @@ class ProviderFactory: """Factory for creating appropriate embedding providers based on model type.""" # Registry of provider classes by type - _PROVIDER_REGISTRY: Dict[str, Type[EmbeddingProvider]] = { + _PROVIDER_REGISTRY: dict[str, type[EmbeddingProvider]] = { "sentence-transformer": SentenceTransformerProvider, "clip": CLIPProvider, } @@ -37,9 +37,7 @@ class ProviderFactory: } @classmethod - def create( - cls, model_name: str, model_type: Optional[str] = None, **kwargs - ) -> EmbeddingProvider: + def create(cls, model_name: str, model_type: Optional[str] = None, **kwargs) -> EmbeddingProvider: """Create an embedding provider for the given model. Args: @@ -71,11 +69,9 @@ def create( f"Cloud provider '{model_type}' not yet implemented. " f"Currently supported: {', '.join(cls._PROVIDER_REGISTRY.keys())}" ) - else: - raise ValueError( - f"Unknown provider type: {model_type}. " - f"Supported types: {', '.join(cls._PROVIDER_REGISTRY.keys())}" - ) + raise ValueError( + f"Unknown provider type: {model_type}. Supported types: {', '.join(cls._PROVIDER_REGISTRY.keys())}" + ) # Create and return provider instance return provider_class(model_name, **kwargs) @@ -115,12 +111,11 @@ def _detect_provider_type(cls, model_name: str) -> str: return "sentence-transformer" raise ValueError( - f"Cannot auto-detect provider type for model: {model_name}. " - "Please specify model_type explicitly." + f"Cannot auto-detect provider type for model: {model_name}. Please specify model_type explicitly." ) @classmethod - def register_provider(cls, model_type: str, provider_class: Type[EmbeddingProvider]): + def register_provider(cls, model_type: str, provider_class: type[EmbeddingProvider]): """Register a new provider type. Args: @@ -155,9 +150,7 @@ def supports_type(cls, model_type: str) -> bool: # Convenience function for creating providers -def create_provider( - model_name: str, model_type: Optional[str] = None, **kwargs -) -> EmbeddingProvider: +def create_provider(model_name: str, model_type: Optional[str] = None, **kwargs) -> EmbeddingProvider: """Create an embedding provider (convenience wrapper around ProviderFactory). Args: diff --git a/src/vector_inspector/core/embedding_providers/sentence_transformer_provider.py b/src/vector_inspector/core/embedding_providers/sentence_transformer_provider.py index 5235c3e..1a371f0 100644 --- a/src/vector_inspector/core/embedding_providers/sentence_transformer_provider.py +++ b/src/vector_inspector/core/embedding_providers/sentence_transformer_provider.py @@ -72,8 +72,7 @@ def get_metadata(self) -> EmbeddingMetadata: except ImportError: # sentence-transformers not installed raise ImportError( - "sentence-transformers library not installed. " - "Install with: pip install sentence-transformers" + "sentence-transformers library not installed. Install with: pip install sentence-transformers" ) except Exception: # Fallback metadata if we can't determine dimension @@ -95,8 +94,7 @@ def _load_model(self): from sentence_transformers import SentenceTransformer except ImportError: raise ImportError( - "sentence-transformers library not installed. " - "Install with: pip install sentence-transformers" + "sentence-transformers library not installed. Install with: pip install sentence-transformers" ) # Try to load from cache first @@ -112,9 +110,7 @@ def _load_model(self): if cached_path: try: # Load from cache - self._model = SentenceTransformer( - str(cached_path), trust_remote_code=self.trust_remote_code - ) + self._model = SentenceTransformer(str(cached_path), trust_remote_code=self.trust_remote_code) log_info(f"Loaded sentence-transformer from cache: {self.model_name}") return except Exception as e: @@ -127,9 +123,7 @@ def _load_model(self): if is_cache_enabled(): save_model_to_cache(self._model, self.model_name, "sentence-transformer") - def encode( - self, inputs: str | list[str], normalize: bool = True, show_progress: bool = False - ) -> np.ndarray: + def encode(self, inputs: str | list[str], normalize: bool = True, show_progress: bool = False) -> np.ndarray: """Encode text inputs into embeddings. Args: @@ -149,15 +143,13 @@ def encode( inputs = [inputs] # Encode - embeddings = self._model.encode( + return self._model.encode( inputs, normalize_embeddings=normalize, show_progress_bar=show_progress, convert_to_numpy=True, ) - return embeddings - def encode_batch( self, inputs: list[str], @@ -179,7 +171,7 @@ def encode_batch( if not self._is_loaded: self.warmup() - embeddings = self._model.encode( + return self._model.encode( inputs, batch_size=batch_size, normalize_embeddings=normalize, @@ -187,8 +179,6 @@ def encode_batch( convert_to_numpy=True, ) - return embeddings - def similarity(self, query: str | np.ndarray, corpus: list[str]) -> np.ndarray: """Compute similarity between query and corpus. @@ -211,6 +201,4 @@ def similarity(self, query: str | np.ndarray, corpus: list[str]) -> np.ndarray: corpus_emb = self.encode(corpus, normalize=True) # Compute cosine similarity (dot product if normalized) - similarities = np.dot(corpus_emb, query_emb.T).squeeze() - - return similarities + return np.dot(corpus_emb, query_emb.T).squeeze() diff --git a/src/vector_inspector/core/embedding_utils.py b/src/vector_inspector/core/embedding_utils.py index 4f2cdc4..ec99b58 100644 --- a/src/vector_inspector/core/embedding_utils.py +++ b/src/vector_inspector/core/embedding_utils.py @@ -198,6 +198,7 @@ def encode_text(text: str, model: SentenceTransformer | tuple | str, model_type: return embeddings[0] except Exception as e: from vector_inspector.core.logging import log_tracked_error + log_tracked_error( "Ollama embedding failed: %s. Ensure Ollama is running (http://localhost:11434)", e, diff --git a/src/vector_inspector/core/llm_providers/provider_factory.py b/src/vector_inspector/core/llm_providers/provider_factory.py index d879362..60cf772 100644 --- a/src/vector_inspector/core/llm_providers/provider_factory.py +++ b/src/vector_inspector/core/llm_providers/provider_factory.py @@ -203,8 +203,7 @@ def generate(self, prompt: str, **opts) -> str: raise RuntimeError("No LLM provider is available.") messages = [{"role": "user", "content": prompt}] model = opts.pop("model", self._provider.get_model_name()) - result = self._provider.generate_messages(messages, model=model, stream=False, **opts) - return result # type: ignore[return-value] + return self._provider.generate_messages(messages, model=model, stream=False, **opts) def is_available(self) -> bool: self._ensure() diff --git a/src/vector_inspector/core/llm_providers/runtime_manager.py b/src/vector_inspector/core/llm_providers/runtime_manager.py index bbea431..ad19957 100644 --- a/src/vector_inspector/core/llm_providers/runtime_manager.py +++ b/src/vector_inspector/core/llm_providers/runtime_manager.py @@ -225,7 +225,6 @@ def _select_provider(self) -> LLMProvider: now = datetime.datetime.now(datetime.UTC).isoformat() selected_provider_id: str | None = None selected_model: str | None = None - explicit = False # True when provider was explicitly configured # --- Rank 1: Explicit app config --- cfg_provider = self._settings.get("llm.provider", None) if self._settings else None @@ -241,7 +240,6 @@ def _select_provider(self) -> LLMProvider: } ) selected_provider_id = cfg_provider - explicit = True else: reasons.append( { @@ -269,7 +267,6 @@ def _select_provider(self) -> LLMProvider: } ) selected_provider_id = env_provider - explicit = True else: reasons.append( { diff --git a/src/vector_inspector/core/provider_factory.py b/src/vector_inspector/core/provider_factory.py index 4096090..65c5c4a 100644 --- a/src/vector_inspector/core/provider_factory.py +++ b/src/vector_inspector/core/provider_factory.py @@ -13,9 +13,7 @@ class ProviderFactory: """Factory for creating database connections from configuration.""" @staticmethod - def create( - provider: str, config: dict[str, Any], credentials: dict[str, Any] = None - ) -> VectorDBConnection: + def create(provider: str, config: dict[str, Any], credentials: dict[str, Any] | None = None) -> VectorDBConnection: """Create a connection object for the specified provider. Args: diff --git a/src/vector_inspector/services/backup_helpers.py b/src/vector_inspector/services/backup_helpers.py index c45f4e1..aa67d6c 100644 --- a/src/vector_inspector/services/backup_helpers.py +++ b/src/vector_inspector/services/backup_helpers.py @@ -2,40 +2,41 @@ Minimal, well-tested helpers to keep `BackupRestoreService` concise. """ + import json import zipfile -from typing import Tuple, Dict, Any +from typing import Any -def write_backup_zip(path, metadata: Dict[str, Any], data: Dict[str, Any]): +def write_backup_zip(path, metadata: dict[str, Any], data: dict[str, Any]): """Write metadata and data into a zip file at `path`. `path` may be a pathlib.Path or string. """ - with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as zipf: - zipf.writestr('metadata.json', json.dumps(metadata, indent=2)) - zipf.writestr('data.json', json.dumps(data, indent=2)) + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr("metadata.json", json.dumps(metadata, indent=2)) + zipf.writestr("data.json", json.dumps(data, indent=2)) -def read_backup_zip(path) -> Tuple[Dict[str, Any], Dict[str, Any]]: +def read_backup_zip(path) -> tuple[dict[str, Any], dict[str, Any]]: """Read metadata.json and data.json from a backup zip and return them. Returns (metadata, data). """ - with zipfile.ZipFile(path, 'r') as zipf: - metadata_str = zipf.read('metadata.json').decode('utf-8') + with zipfile.ZipFile(path, "r") as zipf: + metadata_str = zipf.read("metadata.json").decode("utf-8") metadata = json.loads(metadata_str) - data_str = zipf.read('data.json').decode('utf-8') + data_str = zipf.read("data.json").decode("utf-8") data = json.loads(data_str) return metadata, data -def normalize_embeddings(data: Dict[str, Any]) -> Dict[str, Any]: +def normalize_embeddings(data: dict[str, Any]) -> dict[str, Any]: """Ensure embeddings in `data` are plain python lists (no numpy objects). This mutates and returns the same `data` dict for convenience. """ - if 'embeddings' not in data or data['embeddings'] is None: + if "embeddings" not in data or data["embeddings"] is None: return data try: @@ -43,10 +44,10 @@ def normalize_embeddings(data: Dict[str, Any]) -> Dict[str, Any]: except Exception: np = None - emb = data['embeddings'] + emb = data["embeddings"] if np is not None: if isinstance(emb, np.ndarray): - data['embeddings'] = emb.tolist() + data["embeddings"] = emb.tolist() return data if isinstance(emb, list): @@ -56,7 +57,7 @@ def normalize_embeddings(data: Dict[str, Any]) -> Dict[str, Any]: new_list.append(item.tolist()) else: new_list.append(item) - data['embeddings'] = new_list + data["embeddings"] = new_list return data # No numpy available — assume data already serializable diff --git a/src/vector_inspector/services/cluster_runner.py b/src/vector_inspector/services/cluster_runner.py index b379bca..fbbe570 100644 --- a/src/vector_inspector/services/cluster_runner.py +++ b/src/vector_inspector/services/cluster_runner.py @@ -77,7 +77,7 @@ def get_cluster_stats(self, labels: np.ndarray) -> dict[str, Any]: if label >= 0: # Exclude noise cluster_sizes[int(label)] = int(np.sum(labels == label)) - stats = { + return { "n_clusters": n_clusters, "n_noise": n_noise, "n_total": len(labels), @@ -85,8 +85,6 @@ def get_cluster_stats(self, labels: np.ndarray) -> dict[str, Any]: "noise_ratio": n_noise / len(labels) if len(labels) > 0 else 0.0, } - return stats - def format_summary(self, labels: np.ndarray, algorithm: str) -> str: """ Format a human-readable summary of clustering results. diff --git a/src/vector_inspector/services/data_loaders.py b/src/vector_inspector/services/data_loaders.py index 1a23c1b..34ef203 100644 --- a/src/vector_inspector/services/data_loaders.py +++ b/src/vector_inspector/services/data_loaders.py @@ -59,8 +59,7 @@ def load_all( try: # Use get_all_items if available if hasattr(self.connection, "get_all_items"): - data = self.connection.get_all_items(collection_name=collection, limit=limit, offset=offset) - return data + return self.connection.get_all_items(collection_name=collection, limit=limit, offset=offset) log_tracked_error( "Connection does not support get_all_items", @@ -280,8 +279,7 @@ def load_metadata(self, collection: str, item_ids: Optional[list[str]] = None) - if item_ids: # Load specific items if hasattr(self.connection, "get_by_ids"): - data = self.connection.get_by_ids(collection, item_ids) - return data + return self.connection.get_by_ids(collection, item_ids) else: # Load all metadata if hasattr(self.connection, "get_all_items"): diff --git a/src/vector_inspector/services/filter_service.py b/src/vector_inspector/services/filter_service.py index 4600296..baa3496 100644 --- a/src/vector_inspector/services/filter_service.py +++ b/src/vector_inspector/services/filter_service.py @@ -1,39 +1,39 @@ """Service for applying client-side filters to data.""" -from typing import Dict, Any, List +from typing import Any -def apply_client_side_filters(data: Dict[str, Any], filters: List[Dict[str, Any]]) -> Dict[str, Any]: +def apply_client_side_filters(data: dict[str, Any], filters: list[dict[str, Any]]) -> dict[str, Any]: """ Apply client-side filters to fetched data. - + Args: data: Data dictionary with ids, documents, metadatas, etc. filters: List of client-side filter dictionaries - + Returns: Filtered data dictionary """ if not filters: return data - + ids = data.get("ids", []) documents = data.get("documents", []) metadatas = data.get("metadatas", []) embeddings = data.get("embeddings", []) - + # Track which indices to keep keep_indices = [] - + for i in range(len(ids)): # Check if this item passes all client-side filters passes = True - + for filt in filters: field = filt.get("field", "") op = filt.get("op", "") value = filt.get("value", "") - + # Special handling for document field if field.lower() == "document": item_value = documents[i] if i < len(documents) else "" @@ -41,32 +41,31 @@ def apply_client_side_filters(data: Dict[str, Any], filters: List[Dict[str, Any] # Get from metadata metadata = metadatas[i] if i < len(metadatas) else {} item_value = metadata.get(field, "") - + # Convert to string for text operations item_value_str = str(item_value).lower() search_value = str(value).lower() - + # Apply operator if op == "contains": if search_value not in item_value_str: passes = False break - elif op == "not_contains": - if search_value in item_value_str: - passes = False - break - + elif op == "not_contains" and search_value in item_value_str: + passes = False + break + if passes: keep_indices.append(i) - + # Filter the data filtered_data = { "ids": [ids[i] for i in keep_indices], "documents": [documents[i] for i in keep_indices if i < len(documents)], "metadatas": [metadatas[i] for i in keep_indices if i < len(metadatas)], } - + if embeddings is not None and len(embeddings) > 0: filtered_data["embeddings"] = [embeddings[i] for i in keep_indices if i < len(embeddings)] - + return filtered_data diff --git a/src/vector_inspector/services/profile_service.py b/src/vector_inspector/services/profile_service.py index 636db5a..b0ab27b 100644 --- a/src/vector_inspector/services/profile_service.py +++ b/src/vector_inspector/services/profile_service.py @@ -270,15 +270,13 @@ def duplicate_profile(self, profile_id: str, new_name: str) -> Optional[str]: credentials = self.credential_service.get_credentials(profile_id) # Create new profile - new_id = self.create_profile( + return self.create_profile( name=new_name, provider=source_profile.provider, config=source_profile.config.copy(), credentials=credentials, ) - return new_id - def get_profile_with_credentials(self, profile_id: str) -> Optional[dict[str, Any]]: """ Get a profile along with its credentials. @@ -411,11 +409,9 @@ def migrate_legacy_connection(self, config: dict[str, Any]) -> str: del config["api_key"] # Remove from config # Create profile - profile_id = self.create_profile( + return self.create_profile( name=name, provider=provider, config=config, credentials=credentials if credentials else None, ) - - return profile_id diff --git a/src/vector_inspector/services/update_service.py b/src/vector_inspector/services/update_service.py index 613154e..c2234bd 100644 --- a/src/vector_inspector/services/update_service.py +++ b/src/vector_inspector/services/update_service.py @@ -2,7 +2,7 @@ import json import time import requests -from typing import Optional, Dict +from typing import Optional GITHUB_API_URL = "https://api.github.com/repos/anthonypdawson/vector-inspector/releases/latest" CACHE_FILE = os.path.expanduser("~/.vector_inspector_update_cache.json") @@ -11,7 +11,7 @@ class UpdateService: @staticmethod - def get_latest_release(force_refresh: bool = False) -> Optional[Dict]: + def get_latest_release(force_refresh: bool = False) -> Optional[dict]: """ Fetch the latest release info from GitHub, with caching and rate limit handling. Returns None on error or if rate limited. @@ -20,7 +20,7 @@ def get_latest_release(force_refresh: bool = False) -> Optional[Dict]: # Check cache for rate limit state or valid release if os.path.exists(CACHE_FILE): try: - with open(CACHE_FILE, "r", encoding="utf-8") as f: + with open(CACHE_FILE, encoding="utf-8") as f: cache = json.load(f) # If rate limited, respect the reset time if cache.get("rate_limited_until", 0) > now: @@ -36,7 +36,7 @@ def get_latest_release(force_refresh: bool = False) -> Optional[Dict]: with open(CACHE_FILE, "w", encoding="utf-8") as f: json.dump({"timestamp": now, "release": release}, f) return release - elif resp.status_code == 403: + if resp.status_code == 403: # Check for rate limit headers reset = resp.headers.get("X-RateLimit-Reset") if reset: @@ -63,7 +63,7 @@ def parse(v): return parse(latest_version) > parse(current_version) @staticmethod - def get_update_instructions() -> Dict[str, str]: + def get_update_instructions() -> dict[str, str]: """ Returns update instructions for both PyPI and GitHub. """ diff --git a/src/vector_inspector/ui/components/connection_manager_panel.py b/src/vector_inspector/ui/components/connection_manager_panel.py index 2551dc1..a40f6e7 100644 --- a/src/vector_inspector/ui/components/connection_manager_panel.py +++ b/src/vector_inspector/ui/components/connection_manager_panel.py @@ -106,12 +106,8 @@ def _connect_signals(self): self.connection_manager.connection_opened.connect(self._on_connection_opened) self.connection_manager.connection_closed.connect(self._on_connection_closed) self.connection_manager.connection_state_changed.connect(self._on_connection_state_changed) - self.connection_manager.active_connection_changed.connect( - self._on_active_connection_changed - ) - self.connection_manager.active_collection_changed.connect( - self._on_active_collection_changed - ) + self.connection_manager.active_connection_changed.connect(self._on_active_connection_changed) + self.connection_manager.active_collection_changed.connect(self._on_active_collection_changed) self.connection_manager.collections_updated.connect(self._on_collections_updated) def _on_connection_opened(self, connection_id: str): @@ -127,9 +123,7 @@ def _on_connection_opened(self, connection_id: str): # Create tree item for connection item = QTreeWidgetItem(self.connection_tree) item.setText(0, instance.get_display_name()) - item.setData( - 0, Qt.ItemDataRole.UserRole, {"type": "connection", "connection_id": connection_id} - ) + item.setData(0, Qt.ItemDataRole.UserRole, {"type": "connection", "connection_id": connection_id}) # Set icon/indicator based on state self._update_connection_indicator(item, instance.state) @@ -267,9 +261,7 @@ def _show_context_menu(self, pos): if item_type == "connection": # Connection context menu set_active_action = menu.addAction("Set as Active") - set_active_action.triggered.connect( - lambda: self.connection_manager.set_active_connection(connection_id) - ) + set_active_action.triggered.connect(lambda: self.connection_manager.set_active_connection(connection_id)) menu.addSeparator() @@ -290,24 +282,18 @@ def _show_context_menu(self, pos): select_action = menu.addAction("Select Collection") select_action.triggered.connect( - lambda: self.connection_manager.set_active_collection( - connection_id, collection_name - ) + lambda: self.connection_manager.set_active_collection(connection_id, collection_name) ) menu.addSeparator() info_action = menu.addAction("View Info") - info_action.triggered.connect( - lambda: self._view_collection_info(connection_id, collection_name) - ) + info_action.triggered.connect(lambda: self._view_collection_info(connection_id, collection_name)) menu.addSeparator() delete_action = menu.addAction("Delete Collection...") - delete_action.triggered.connect( - lambda: self._delete_collection(connection_id, collection_name) - ) + delete_action.triggered.connect(lambda: self._delete_collection(connection_id, collection_name)) # Make delete action red/warning style delete_action.setIcon(QIcon()) # Could add warning icon font = delete_action.font() @@ -322,9 +308,7 @@ def _rename_connection(self, connection_id: str): if not instance: return - new_name, ok = QInputDialog.getText( - self, "Rename Connection", "Enter new name:", text=instance.name - ) + new_name, ok = QInputDialog.getText(self, "Rename Connection", "Enter new name:", text=instance.name) if ok and new_name and self.connection_manager.rename_connection(connection_id, new_name): # Update tree item @@ -343,11 +327,7 @@ def _refresh_collections(self, connection_id: str): loading.show_loading("Refreshing collections...") # Cancel any existing refresh thread - if ( - hasattr(self, "_refresh_thread") - and self._refresh_thread - and self._refresh_thread.isRunning() - ): + if hasattr(self, "_refresh_thread") and self._refresh_thread and self._refresh_thread.isRunning(): self._refresh_thread.quit() self._refresh_thread.wait() @@ -359,9 +339,7 @@ def _refresh_collections(self, connection_id: str): self._refresh_thread.error.connect(lambda error: self._on_refresh_error(error, loading)) self._refresh_thread.start() - def _on_refresh_finished( - self, connection_id: str, collections: list, loading: LoadingDialog - ) -> None: + def _on_refresh_finished(self, connection_id: str, collections: list, loading: LoadingDialog) -> None: """Handle successful collections refresh.""" loading.hide_loading() self.connection_manager.update_collections(connection_id, collections) @@ -415,9 +393,7 @@ def _delete_collection(self, connection_id: str, collection_name: str): # Warning header warning_label = QLabel("⚠️ PERMANENT DELETION WARNING ⚠️") - warning_label.setStyleSheet( - "font-size: 16px; font-weight: bold; color: #d32f2f; padding: 10px;" - ) + warning_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #d32f2f; padding: 10px;") warning_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(warning_label) @@ -439,16 +415,13 @@ def _delete_collection(self, connection_id: str, collection_name: str): # Confirmation checkbox confirm_checkbox = QCheckBox( - f"I understand this will PERMANENTLY DELETE '{collection_name}' " - f"and all {item_count:,} items" + f"I understand this will PERMANENTLY DELETE '{collection_name}' and all {item_count:,} items" ) confirm_checkbox.setStyleSheet("font-weight: bold; color: #d32f2f; padding: 10px;") layout.addWidget(confirm_checkbox) # Type collection name to confirm - type_confirm_label = QLabel( - f"Type the collection name to confirm: {collection_name}" - ) + type_confirm_label = QLabel(f"Type the collection name to confirm: {collection_name}") layout.addWidget(type_confirm_label) name_input = QLineEdit() @@ -457,9 +430,7 @@ def _delete_collection(self, connection_id: str, collection_name: str): # Buttons button_box = QDialogButtonBox() - delete_button = button_box.addButton( - "DELETE PERMANENTLY", QDialogButtonBox.ButtonRole.DestructiveRole - ) + delete_button = button_box.addButton("DELETE PERMANENTLY", QDialogButtonBox.ButtonRole.DestructiveRole) delete_button.setEnabled(False) # Disabled until confirmed delete_button.setStyleSheet( "QPushButton { background-color: #d32f2f; color: white; font-weight: bold; " @@ -491,11 +462,7 @@ def check_confirmations(): loading.show_loading(f"Deleting collection '{collection_name}'...") # Cancel any existing delete thread - if ( - hasattr(self, "_delete_thread") - and self._delete_thread - and self._delete_thread.isRunning() - ): + if hasattr(self, "_delete_thread") and self._delete_thread and self._delete_thread.isRunning(): self._delete_thread.quit() self._delete_thread.wait() @@ -507,13 +474,9 @@ def check_confirmations(): parent=self, ) self._delete_thread.finished.connect( - lambda collections: self._on_delete_finished( - connection_id, collection_name, collections, loading, instance - ) - ) - self._delete_thread.error.connect( - lambda error: self._on_delete_error(collection_name, error, loading) + lambda collections: self._on_delete_finished(connection_id, collection_name, collections, loading, instance) ) + self._delete_thread.error.connect(lambda error: self._on_delete_error(collection_name, error, loading)) self._delete_thread.start() def _on_delete_finished( @@ -544,9 +507,7 @@ def _on_delete_finished( f"Collection '{collection_name}' has been permanently deleted.", ) - def _on_delete_error( - self, collection_name: str, error_message: str, loading: LoadingDialog - ) -> None: + def _on_delete_error(self, collection_name: str, error_message: str, loading: LoadingDialog) -> None: """Handle delete error.""" loading.hide_loading() QMessageBox.critical( diff --git a/src/vector_inspector/ui/components/inline_details_pane.py b/src/vector_inspector/ui/components/inline_details_pane.py index 8dbc1c6..a8e0f25 100644 --- a/src/vector_inspector/ui/components/inline_details_pane.py +++ b/src/vector_inspector/ui/components/inline_details_pane.py @@ -562,7 +562,7 @@ def _copy_vector_json(self): if embedding is not None: try: vector_list = embedding.tolist() if hasattr(embedding, "tolist") else list(embedding) - json_str = json.dumps( + json.dumps( { "id": self._current_item.get("id"), "vector": vector_list, diff --git a/src/vector_inspector/ui/components/item_dialog.py b/src/vector_inspector/ui/components/item_dialog.py index 452849b..8f13ae8 100644 --- a/src/vector_inspector/ui/components/item_dialog.py +++ b/src/vector_inspector/ui/components/item_dialog.py @@ -67,16 +67,13 @@ def _setup_ui(self): # Auto-timestamp checkbox self.auto_timestamp_checkbox = QCheckBox( - "Automatically add timestamp " - + ("(updated_at)" if self.is_edit_mode else "(created_at)") + "Automatically add timestamp " + ("(updated_at)" if self.is_edit_mode else "(created_at)") ) self.auto_timestamp_checkbox.setChecked(False) # Default to disabled layout.addWidget(self.auto_timestamp_checkbox) # Note about embeddings - note_label = QLabel( - "Note: Embeddings will be automatically generated from the document text." - ) + note_label = QLabel("Note: Embeddings will be automatically generated from the document text.") note_label.setStyleSheet("color: gray; font-style: italic;") note_label.setWordWrap(True) layout.addWidget(note_label) diff --git a/src/vector_inspector/ui/components/profile_manager_panel.py b/src/vector_inspector/ui/components/profile_manager_panel.py index fd35728..25ac251 100644 --- a/src/vector_inspector/ui/components/profile_manager_panel.py +++ b/src/vector_inspector/ui/components/profile_manager_panel.py @@ -697,7 +697,7 @@ def _on_provider_changed(self): dlg = ProviderInstallDialog(provider_info, parent=self) # Disconnect the auto-populate to control it manually - result = dlg.exec() + dlg.exec() # Repopulate providers list after dialog closes self._populate_providers() diff --git a/src/vector_inspector/ui/components/splash_window.py b/src/vector_inspector/ui/components/splash_window.py index bb7410d..6a0768a 100644 --- a/src/vector_inspector/ui/components/splash_window.py +++ b/src/vector_inspector/ui/components/splash_window.py @@ -31,9 +31,7 @@ def __init__(self, parent=None): about = QTextBrowser() version = get_app_version() - version_html = ( - f"

Vector Inspector {version}

" if version else "

Vector Inspector

" - ) + version_html = f"

Vector Inspector {version}

" if version else "

Vector Inspector

" about_text = ( version_html + "

A comprehensive desktop application for visualizing, " "querying, and managing multiple vector databases simultaneously.

" diff --git a/src/vector_inspector/ui/components/update_details_dialog.py b/src/vector_inspector/ui/components/update_details_dialog.py index b87f4a0..a10935a 100644 --- a/src/vector_inspector/ui/components/update_details_dialog.py +++ b/src/vector_inspector/ui/components/update_details_dialog.py @@ -9,9 +9,7 @@ class UpdateDetailsDialog(QDialog): pip_command: str github_url: str - def __init__( - self, version: str, release_notes: str, pip_command: str, github_url: str, parent=None - ): + def __init__(self, version: str, release_notes: str, pip_command: str, github_url: str, parent=None): super().__init__(parent) self.version = version self.release_notes = release_notes diff --git a/src/vector_inspector/ui/controllers/connection_controller.py b/src/vector_inspector/ui/controllers/connection_controller.py index dda1d4b..212a52e 100644 --- a/src/vector_inspector/ui/controllers/connection_controller.py +++ b/src/vector_inspector/ui/controllers/connection_controller.py @@ -133,7 +133,9 @@ def connect_to_profile(self, profile_id: str) -> bool: # start a duplicate. This prevents repeated db.connection_attempt telemetry # events when the user clicks "Connect" multiple times in quick succession. if profile_id in self._connection_threads and self._connection_threads[profile_id].isRunning(): - log_info("[ConnectionController] Ignoring duplicate connect request for %s — already in progress", profile_id) + log_info( + "[ConnectionController] Ignoring duplicate connect request for %s — already in progress", profile_id + ) return True # Check connection limit @@ -267,7 +269,7 @@ def _on_connection_finished( # Emit signal for UI updates self.connection_completed.emit(connection_id, success, collections, error_message, duration_ms) - def create_collection_with_dialog(self, connection_id: str = None) -> bool: + def create_collection_with_dialog(self, connection_id: str | None = None) -> bool: """Show dialog to create a new collection with optional sample data. Args: diff --git a/src/vector_inspector/ui/dialogs/__init__.py b/src/vector_inspector/ui/dialogs/__init__.py index 008980c..17b84e4 100644 --- a/src/vector_inspector/ui/dialogs/__init__.py +++ b/src/vector_inspector/ui/dialogs/__init__.py @@ -4,4 +4,4 @@ from .embedding_config_dialog import EmbeddingConfigDialog from .provider_type_dialog import ProviderTypeDialog -__all__ = ['ContentColumnDialog', 'EmbeddingConfigDialog', 'ProviderTypeDialog'] +__all__ = ["ContentColumnDialog", "EmbeddingConfigDialog", "ProviderTypeDialog"] diff --git a/src/vector_inspector/ui/dialogs/cross_db_migration.py b/src/vector_inspector/ui/dialogs/cross_db_migration.py index 3c8a220..8961aad 100644 --- a/src/vector_inspector/ui/dialogs/cross_db_migration.py +++ b/src/vector_inspector/ui/dialogs/cross_db_migration.py @@ -1,20 +1,18 @@ """Cross-database operations for migrating data between vector databases.""" -from typing import Optional, List, Dict, Any +from typing import Optional from pathlib import Path import tempfile from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, - QLabel, QComboBox, QPushButton, QProgressBar, QTextEdit, QGroupBox, QFormLayout, - QSpinBox, QCheckBox, QMessageBox, ) @@ -117,7 +115,7 @@ def run(self): return if success: - self.progress.emit(100, f"Migration complete!") + self.progress.emit(100, "Migration complete!") self.finished.emit( True, f"Successfully migrated {self.source_collection} to {self.target_collection}", @@ -135,9 +133,7 @@ def run(self): except Exception as cleanup_error: log_error("Warning: Failed to clean up target collection: %s", cleanup_error) - self.finished.emit( - False, "Failed to restore to target collection. Target collection cleaned up." - ) + self.finished.emit(False, "Failed to restore to target collection. Target collection cleaned up.") except Exception as e: import traceback @@ -157,7 +153,7 @@ def run(self): except Exception as cleanup_error: log_error("Warning: Failed to clean up target collection: %s", cleanup_error) - self.finished.emit(False, f"Migration error: {str(e)}") + self.finished.emit(False, f"Migration error: {e!s}") finally: # Clean up temporary backup file @@ -305,9 +301,7 @@ def _start_migration(self): target_conn_id = self.target_connection_combo.currentData() if not source_conn_id or not target_conn_id: - QMessageBox.warning( - self, "Invalid Selection", "Please select both source and target connections." - ) + QMessageBox.warning(self, "Invalid Selection", "Please select both source and target connections.") return if source_conn_id == target_conn_id: @@ -332,9 +326,7 @@ def _start_migration(self): target_collection = self.target_collection_combo.currentText().strip() if not source_collection or not target_collection: - QMessageBox.warning( - self, "Invalid Selection", "Please select both source and target collections." - ) + QMessageBox.warning(self, "Invalid Selection", "Please select both source and target collections.") return # Check if target collection exists diff --git a/src/vector_inspector/ui/dialogs/embedding_config_dialog.py b/src/vector_inspector/ui/dialogs/embedding_config_dialog.py index de41323..4dded55 100644 --- a/src/vector_inspector/ui/dialogs/embedding_config_dialog.py +++ b/src/vector_inspector/ui/dialogs/embedding_config_dialog.py @@ -1,6 +1,6 @@ """Dialog for configuring embedding models for collections (Step 2: Model Selection).""" -from typing import Optional, Tuple +from typing import Optional from PySide6.QtWidgets import ( QDialog, QVBoxLayout, @@ -14,7 +14,7 @@ QLineEdit, QFormLayout, ) -from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtCore import QThread, Signal from vector_inspector.core.embedding_utils import get_available_models_for_dimension from vector_inspector.core.model_registry import get_model_registry @@ -80,9 +80,7 @@ def _setup_ui(self): info_layout.addWidget(QLabel(f"Vector Dimension: {self.vector_dimension}")) if self.current_model: - info_layout.addWidget( - QLabel(f"Current Model: {self.current_model} ({self.current_type})") - ) + info_layout.addWidget(QLabel(f"Current Model: {self.current_model} ({self.current_type})")) else: warning = QLabel("⚠️ No embedding model configured - using automatic detection") warning.setStyleSheet("color: orange;") @@ -110,18 +108,14 @@ def _setup_ui(self): custom_models = settings.get_custom_embedding_models(self.vector_dimension) for model in custom_models: if model["type"] == self.provider_type: - available_models.append( - (model["name"], model["type"], f"{model['description']} (custom)") - ) + available_models.append((model["name"], model["type"], f"{model['description']} (custom)")) except Exception: pass else: available_models = get_available_models_for_dimension(self.vector_dimension) if available_models: - model_layout.addWidget( - QLabel(f"Available models for {self.vector_dimension}-dimensional vectors:") - ) + model_layout.addWidget(QLabel(f"Available models for {self.vector_dimension}-dimensional vectors:")) self.model_combo = QComboBox() for model_name, model_type, description in available_models: @@ -145,9 +139,7 @@ def _setup_ui(self): self.description_text = QTextEdit() self.description_text.setReadOnly(True) self.description_text.setMaximumHeight(100) - self.description_text.setStyleSheet( - "background-color: #f5f5f5; border: 1px solid #ccc; color: #000000;" - ) + self.description_text.setStyleSheet("background-color: #f5f5f5; border: 1px solid #ccc; color: #000000;") model_layout.addWidget(self.description_text) # Update description when selection changes @@ -157,9 +149,7 @@ def _setup_ui(self): else: # No models for this type + dimension type_name = self.provider_type or "any type" - warning = QLabel( - f"⚠️ No models of type '{type_name}' available for {self.vector_dimension} dimensions." - ) + warning = QLabel(f"⚠️ No models of type '{type_name}' available for {self.vector_dimension} dimensions.") warning.setWordWrap(True) model_layout.addWidget(warning) @@ -241,9 +231,7 @@ def _setup_custom_ui(self, layout): self.custom_desc_input.setPlaceholderText("Brief description (optional)") custom_layout.addRow("Description:", self.custom_desc_input) - custom_note = QLabel( - "💡 Custom models will be saved and available for future use with this dimension." - ) + custom_note = QLabel("💡 Custom models will be saved and available for future use with this dimension.") custom_note.setWordWrap(True) custom_note.setStyleSheet("color: #666; font-size: 10px; padding: 4px;") custom_layout.addRow(custom_note) @@ -355,7 +343,7 @@ def _clear_config(self): self.selected_type = None self.done(2) # Custom code for "clear" - def get_selection(self) -> Optional[Tuple[str, str]]: + def get_selection(self) -> Optional[tuple[str, str]]: """Get the selected model and type (from either combo or custom entry).""" if self.selected_model and self.selected_type: return (self.selected_model, self.selected_type) diff --git a/src/vector_inspector/ui/dialogs/provider_type_dialog.py b/src/vector_inspector/ui/dialogs/provider_type_dialog.py index a034049..883dbff 100644 --- a/src/vector_inspector/ui/dialogs/provider_type_dialog.py +++ b/src/vector_inspector/ui/dialogs/provider_type_dialog.py @@ -1,10 +1,17 @@ """Dialog for selecting embedding provider type (Step 1 of model selection).""" -from typing import Optional, Tuple +from typing import Optional from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QRadioButton, QButtonGroup, QGroupBox, - QScrollArea, QWidget + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QRadioButton, + QButtonGroup, + QGroupBox, + QScrollArea, + QWidget, ) from PySide6.QtCore import Qt @@ -13,7 +20,7 @@ class ProviderTypeDialog(QDialog): """Dialog for selecting the provider/type category before choosing specific model.""" - + # Provider categories with display info PROVIDER_CATEGORIES = [ { @@ -21,103 +28,102 @@ class ProviderTypeDialog(QDialog): "name": "🤗 Sentence Transformers", "description": "Local models from HuggingFace\nNo API key required, runs on your machine", "filter_type": "sentence-transformer", - "icon": "📦" + "icon": "📦", }, { "id": "clip", "name": "🖼️ CLIP Models", "description": "Multimodal embeddings (text + images)\nLocal models, no API key required", "filter_type": "clip", - "icon": "🎨" + "icon": "🎨", }, { "id": "ollama", "name": "🦙 Ollama", "description": "Local models via Ollama server\nNo API key required, requires Ollama running", "filter_type": "ollama", - "icon": "💻" + "icon": "💻", }, { "id": "openai", "name": "☁️ OpenAI API", "description": "Cloud-based embeddings\nRequires OpenAI API key", "filter_type": "openai", - "icon": "🔑" + "icon": "🔑", }, { "id": "cohere", "name": "☁️ Cohere API", "description": "Cloud-based embeddings\nRequires Cohere API key", "filter_type": "cohere", - "icon": "🔑" + "icon": "🔑", }, { "id": "vertex-ai", "name": "☁️ Google Vertex AI", "description": "Cloud-based embeddings\nRequires Google Cloud credentials", "filter_type": "vertex-ai", - "icon": "🔑" + "icon": "🔑", }, { "id": "voyage", "name": "☁️ Voyage AI", "description": "Cloud-based embeddings\nRequires Voyage API key", "filter_type": "voyage", - "icon": "🔑" + "icon": "🔑", }, { "id": "custom", "name": "✏️ Custom Model", "description": "Enter your own model name\nFor models not in the registry", "filter_type": None, # Special case - "icon": "⚙️" - } + "icon": "⚙️", + }, ] - + def __init__(self, collection_name: str, vector_dimension: int, parent=None): super().__init__(parent) self.collection_name = collection_name self.vector_dimension = vector_dimension self.selected_type = None - + self.setWindowTitle("Select Embedding Provider Type") self.setMinimumWidth(550) self.setMinimumHeight(500) self._setup_ui() - + def _setup_ui(self): """Setup dialog UI.""" layout = QVBoxLayout(self) - + # Header - header = QLabel(f"

Select Provider Type

") + header = QLabel("

Select Provider Type

") layout.addWidget(header) - - info = QLabel(f"Collection: {self.collection_name}
" - f"Vector Dimension: {self.vector_dimension}") + + info = QLabel(f"Collection: {self.collection_name}
Vector Dimension: {self.vector_dimension}") layout.addWidget(info) - + layout.addWidget(QLabel("Choose the type of embedding provider to use:")) - + # Scroll area for provider options scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - + scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) scroll_layout.setSpacing(10) - + # Radio button group self.button_group = QButtonGroup(self) - + # Get registry to count models registry = get_model_registry() - + # Create radio buttons for each provider type for i, category in enumerate(self.PROVIDER_CATEGORIES): provider_id = category["id"] - + # Count available models for this type + dimension if provider_id == "custom": count_text = "Enter manually" @@ -134,65 +140,65 @@ def _setup_ui(self): continue # Skip categories with no models for this dimension count_text = f"{count} model{'s' if count != 1 else ''} available" - + # Create group box for this option group = QGroupBox() group_layout = QVBoxLayout() - + # Radio button radio = QRadioButton(category["name"]) radio.setProperty("provider_id", provider_id) self.button_group.addButton(radio, i) group_layout.addWidget(radio) - + # Description desc_label = QLabel(category["description"]) desc_label.setStyleSheet("color: gray; margin-left: 25px;") desc_label.setWordWrap(True) group_layout.addWidget(desc_label) - + # Count count_label = QLabel(f"{count_text}") count_label.setStyleSheet("margin-left: 25px; color: #0066cc;") group_layout.addWidget(count_label) - + group.setLayout(group_layout) scroll_layout.addWidget(group) - + scroll_layout.addStretch() scroll.setWidget(scroll_widget) layout.addWidget(scroll) - + # Buttons button_layout = QHBoxLayout() button_layout.addStretch() - + cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.reject) - + self.next_btn = QPushButton("Next →") self.next_btn.clicked.connect(self._on_next) self.next_btn.setEnabled(False) self.next_btn.setDefault(True) - + # Enable Next when selection is made self.button_group.buttonClicked.connect(lambda: self.next_btn.setEnabled(True)) - + button_layout.addWidget(cancel_btn) button_layout.addWidget(self.next_btn) - + layout.addLayout(button_layout) - + def _on_next(self): """Handle Next button click.""" selected_button = self.button_group.checkedButton() if selected_button: self.selected_type = selected_button.property("provider_id") self.accept() - + def get_selected_type(self) -> Optional[str]: """Get the selected provider type ID. - + Returns: Provider type ID or None if cancelled """ diff --git a/src/vector_inspector/ui/loading_screen.py b/src/vector_inspector/ui/loading_screen.py index fbed4c2..6e2031a 100644 --- a/src/vector_inspector/ui/loading_screen.py +++ b/src/vector_inspector/ui/loading_screen.py @@ -26,9 +26,7 @@ def __init__(self, logo_path, version, app_name, tagline, loading_text): # Logo if os.path.exists(logo_path): - pixmap = QPixmap(logo_path).scaled( - 128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation - ) + pixmap = QPixmap(logo_path).scaled(128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation) logo_label = QLabel() logo_label.setPixmap(pixmap) logo_label.setAlignment(Qt.AlignCenter) @@ -105,9 +103,7 @@ def fade_out(self, duration=500): self._fade_animation = animation # Prevent garbage collection -def show_loading_screen( - app_name, version, tagline, loading_text="Initializing providers…", logo_path=None -): +def show_loading_screen(app_name, version, tagline, loading_text="Initializing providers…", logo_path=None): """Show the loading screen if not disabled in settings. This is a convenience function that handles checking settings, finding the logo, diff --git a/src/vector_inspector/ui/main_window.py b/src/vector_inspector/ui/main_window.py index 53e7acf..1faba87 100644 --- a/src/vector_inspector/ui/main_window.py +++ b/src/vector_inspector/ui/main_window.py @@ -118,8 +118,8 @@ def _maybe_show_splash(self): splash.activateWindow() if splash.exec() == QDialog.DialogCode.Accepted and splash.should_hide(): self.settings_service.set("hide_splash_window", True) - except Exception as e: - print(f"[SplashWindow] Failed to show splash: {e}") + except Exception: + pass def _setup_ui(self): """Setup the main UI layout using InspectorShell.""" @@ -372,8 +372,8 @@ def _show_preferences_dialog(self): dlg = SettingsDialog(self.settings_service, self) if dlg.exec() == QDialog.DialogCode.Accepted: self._apply_settings_to_views() - except Exception as e: - print(f"Failed to open preferences: {e}") + except Exception: + pass def _apply_settings_to_views(self): """Apply relevant settings to existing views.""" diff --git a/src/vector_inspector/ui/views/demo_collection_view.py b/src/vector_inspector/ui/views/demo_collection_view.py index 68c2203..fb3238e 100644 --- a/src/vector_inspector/ui/views/demo_collection_view.py +++ b/src/vector_inspector/ui/views/demo_collection_view.py @@ -61,9 +61,7 @@ class DemoCollectionView(QWidget): view = DemoCollectionView(app_state, task_runner) """ - def __init__( - self, app_state: AppState, task_runner: ThreadedTaskRunner, parent: Optional[QWidget] = None - ) -> None: + def __init__(self, app_state: AppState, task_runner: ThreadedTaskRunner, parent: Optional[QWidget] = None) -> None: """ Initialize demo collection view. @@ -278,9 +276,7 @@ def _populate_table(self, data: dict) -> None: # Metadata column metadata = metadatas[row] if row < len(metadatas) else {} metadata_str = str(metadata) if metadata else "" - metadata_item = QTableWidgetItem( - metadata_str[:50] + "..." if len(metadata_str) > 50 else metadata_str - ) + metadata_item = QTableWidgetItem(metadata_str[:50] + "..." if len(metadata_str) > 50 else metadata_str) metadata_item.setFlags(metadata_item.flags() & ~Qt.ItemFlag.ItemIsEditable) self.table.setItem(row, 1, metadata_item) diff --git a/src/vector_inspector/ui/views/info_panel.py b/src/vector_inspector/ui/views/info_panel.py index 3558b0c..7d4c137 100644 --- a/src/vector_inspector/ui/views/info_panel.py +++ b/src/vector_inspector/ui/views/info_panel.py @@ -663,10 +663,16 @@ def _display_collection_info(self, collection_info: dict[str, Any]): try: content_col = backend.get_content_column(self.current_collection) # Check if manually overridden or auto-detected - is_override = backend.is_content_column_overridden(self.current_collection) if hasattr(backend, "is_content_column_overridden") else False + is_override = ( + backend.is_content_column_overridden(self.current_collection) + if hasattr(backend, "is_content_column_overridden") + else False + ) label_suffix = "(manual override)" if is_override else "(auto-detected)" self.content_col_label.setText(f"{content_col} {label_suffix}") - self.content_col_label.setStyleSheet("color: #4CAF50; padding-left: 20px; font-family: " + _MONO_FONT + ";") + self.content_col_label.setStyleSheet( + "color: #4CAF50; padding-left: 20px; font-family: " + _MONO_FONT + ";" + ) self.configure_content_col_btn.setEnabled(True) except Exception: self.content_col_label.setText("N/A") @@ -749,7 +755,9 @@ def _display_collection_info(self, collection_info: dict[str, Any]): if details_list: self.provider_details_label.setText("\n".join(details_list)) - self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: " + _MONO_FONT + ";") + self.provider_details_label.setStyleSheet( + "color: white; padding-left: 20px; font-family: " + _MONO_FONT + ";" + ) else: self.provider_details_label.setText("No additional details available") self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;") @@ -803,7 +811,6 @@ def _update_embedding_model_display(self, collection_info: dict[str, Any]): # Ensure we have a valid connection_id for settings lookup # Fallback to connection.id if connection_id not set - effective_connection_id = self.connection_id or (self.connection.id if self.connection else None) # Try to get from connection using the helper method if self.connection and self.current_collection: @@ -991,11 +998,13 @@ def _configure_content_column(self): schema = {col: str(dtype) for col, dtype in df.dtypes.items()} except Exception as e: from PySide6.QtWidgets import QMessageBox + QMessageBox.warning(self, "Error", f"Could not load schema: {e}") return if not schema: from PySide6.QtWidgets import QMessageBox + QMessageBox.information(self, "No Schema", "Could not determine collection schema.") return @@ -1009,11 +1018,7 @@ def _configure_content_column(self): from vector_inspector.ui.dialogs import ContentColumnDialog dialog = ContentColumnDialog( - self.current_collection, - schema, - current_column, - self, - auto_detected_column=auto_detected + self.current_collection, schema, current_column, self, auto_detected_column=auto_detected ) if dialog.exec(): @@ -1024,11 +1029,10 @@ def _configure_content_column(self): # Update display self.content_col_label.setText(f"{selected_column}") - self.content_col_label.setStyleSheet("color: #4CAF50; padding-left: 20px; font-family: " + _MONO_FONT + ";") + self.content_col_label.setStyleSheet( + "color: #4CAF50; padding-left: 20px; font-family: " + _MONO_FONT + ";" + ) from PySide6.QtWidgets import QMessageBox - QMessageBox.information( - self, - "Content Column Updated", - f"Content column set to: {selected_column}" - ) + + QMessageBox.information(self, "Content Column Updated", f"Content column set to: {selected_column}") diff --git a/src/vector_inspector/ui/views/metadata/item_update_helpers.py b/src/vector_inspector/ui/views/metadata/item_update_helpers.py index d829491..bfaa311 100644 --- a/src/vector_inspector/ui/views/metadata/item_update_helpers.py +++ b/src/vector_inspector/ui/views/metadata/item_update_helpers.py @@ -29,9 +29,8 @@ def process_item_update_success( ctx.cache_manager.invalidate(ctx.current_database, ctx.current_collection) # Show info about embedding regeneration/preservation when applicable - regen_count = 0 try: - regen_count = int(getattr(ctx.connection, "_last_regenerated_count", 0) or 0) + int(getattr(ctx.connection, "_last_regenerated_count", 0) or 0) if update_row_in_place(view.table, ctx, updated_data): return diff --git a/src/vector_inspector/ui/views/metadata/metadata_io.py b/src/vector_inspector/ui/views/metadata/metadata_io.py index a621e54..c03172a 100644 --- a/src/vector_inspector/ui/views/metadata/metadata_io.py +++ b/src/vector_inspector/ui/views/metadata/metadata_io.py @@ -75,9 +75,7 @@ def export_data( settings_service = SettingsService() last_dir = settings_service.get("last_import_export_dir", "") default_path = ( - f"{last_dir}/{current_collection}.{format_type}" - if last_dir - else f"{current_collection}.{format_type}" + f"{last_dir}/{current_collection}.{format_type}" if last_dir else f"{current_collection}.{format_type}" ) file_path, _ = QFileDialog.getSaveFileName( @@ -196,9 +194,7 @@ def import_data( settings_service.set("last_import_export_dir", str(Path(file_path).parent)) - QMessageBox.information( - parent, "Import Successful", f"Imported {len(imported_data['ids'])} items." - ) + QMessageBox.information(parent, "Import Successful", f"Imported {len(imported_data['ids'])} items.") return imported_data QMessageBox.warning(parent, "Import Failed", "Failed to import data.") return None diff --git a/src/vector_inspector/ui/views/metadata/metadata_threads.py b/src/vector_inspector/ui/views/metadata/metadata_threads.py index 1299367..ab3e595 100644 --- a/src/vector_inspector/ui/views/metadata/metadata_threads.py +++ b/src/vector_inspector/ui/views/metadata/metadata_threads.py @@ -81,23 +81,15 @@ def run(self) -> None: success = self.connection.update_items( self.collection, ids=[self.updated_data["id"]], - documents=[self.updated_data["document"]] - if self.updated_data["document"] - else None, - metadatas=[self.updated_data["metadata"]] - if self.updated_data["metadata"] - else None, + documents=[self.updated_data["document"]] if self.updated_data["document"] else None, + metadatas=[self.updated_data["metadata"]] if self.updated_data["metadata"] else None, ) else: success = self.connection.update_items( self.collection, ids=[self.updated_data["id"]], - documents=[self.updated_data["document"]] - if self.updated_data["document"] - else None, - metadatas=[self.updated_data["metadata"]] - if self.updated_data["metadata"] - else None, + documents=[self.updated_data["document"]] if self.updated_data["document"] else None, + metadatas=[self.updated_data["metadata"]] if self.updated_data["metadata"] else None, embeddings=self.embeddings_arg, ) diff --git a/src/vector_inspector/ui/views/visualization/plot_panel.py b/src/vector_inspector/ui/views/visualization/plot_panel.py index 9fffad4..0a68be6 100644 --- a/src/vector_inspector/ui/views/visualization/plot_panel.py +++ b/src/vector_inspector/ui/views/visualization/plot_panel.py @@ -296,7 +296,7 @@ def create_plot( return; } var pointIndex = point.pointIndex; - + // Toggle: if clicking same point, deselect if (selectedPointIndex === pointIndex) { selectedPointIndex = -1; @@ -305,9 +305,9 @@ def create_plot( } return; } - + selectedPointIndex = pointIndex; - + // Extract ID from hover text var pointId = String(pointIndex); if (point.text) { @@ -326,7 +326,7 @@ def create_plot( } } }); - + // Handle explicit deselection plotDiv.on('plotly_deselect', function() { selectedPointIndex = -1; @@ -337,7 +337,7 @@ def create_plot( plotBridge.onInteraction('select', 0); } }); - + // Handle zoom / pan via relayout event plotDiv.on('plotly_relayout', function(layout) { // Basic heuristic: presence of axis range keys indicates zoom diff --git a/src/vector_inspector/ui/views/visualization_view.py b/src/vector_inspector/ui/views/visualization_view.py index 7786f8b..1e4674f 100644 --- a/src/vector_inspector/ui/views/visualization_view.py +++ b/src/vector_inspector/ui/views/visualization_view.py @@ -639,7 +639,7 @@ def _save_cluster_labels_to_metadata(self): # Update metadata with cluster labels updated_metadatas = [] - for i, (item_id, metadata) in enumerate(zip(ids, metadatas)): + for i, (_item_id, metadata) in enumerate(zip(ids, metadatas, strict=False)): if i >= len(self.cluster_labels): break diff --git a/tests/components/test_profile_manager_panel.py b/tests/components/test_profile_manager_panel.py index 5474837..0c26d2f 100644 --- a/tests/components/test_profile_manager_panel.py +++ b/tests/components/test_profile_manager_panel.py @@ -436,7 +436,7 @@ def close(self): def test_provider_ui_branches_and_browse_and_load(qtbot, fake_service, mock_providers, monkeypatch): - panel_mod = __import__("vector_inspector.ui.components.profile_manager_panel", fromlist=["*"]) + __import__("vector_inspector.ui.components.profile_manager_panel", fromlist=["*"]) dlg = ProfileEditorDialog(fake_service) qtbot.addWidget(dlg) @@ -744,13 +744,13 @@ def on_connect(pid): def test_save_profile_create_and_update_and_config_kwargs(qtbot, fake_service, mock_providers, monkeypatch): - panel_mod = __import__("vector_inspector.ui.components.profile_manager_panel", fromlist=["*"]) + __import__("vector_inspector.ui.components.profile_manager_panel", fromlist=["*"]) # New profile create (pinecone requires api_key) created = {} def fake_create(name, provider, config, credentials=None): - created["args"] = dict(name=name, provider=provider, config=config, credentials=credentials) + created["args"] = {"name": name, "provider": provider, "config": config, "credentials": credentials} return "id-new" fake_service.create_profile = fake_create @@ -772,7 +772,7 @@ def fake_create(name, provider, config, credentials=None): updated = {} def fake_update(pid, name=None, config=None, credentials=None): - updated["args"] = dict(pid=pid, name=name, config=config, credentials=credentials) + updated["args"] = {"pid": pid, "name": name, "config": config, "credentials": credentials} return True fake_service.update_profile = fake_update diff --git a/tests/core/llm_providers/test_llama_cpp.py b/tests/core/llm_providers/test_llama_cpp.py index d9a8bb0..5eafcd3 100644 --- a/tests/core/llm_providers/test_llama_cpp.py +++ b/tests/core/llm_providers/test_llama_cpp.py @@ -100,7 +100,7 @@ def _make_provider_with_mock_llm(self, tmp_path, response_content: str = " resp return p, mock_llm def test_happy_path_returns_stripped_content(self, tmp_path): - p, mock_llm = self._make_provider_with_mock_llm(tmp_path, " hello world ") + p, _mock_llm = self._make_provider_with_mock_llm(tmp_path, " hello world ") result = p.generate_messages([{"role": "user", "content": "hi"}], model="model.gguf") assert result == "hello world" diff --git a/tests/core/llm_providers/test_runtime_and_selection.py b/tests/core/llm_providers/test_runtime_and_selection.py index fc89176..5762cd4 100644 --- a/tests/core/llm_providers/test_runtime_and_selection.py +++ b/tests/core/llm_providers/test_runtime_and_selection.py @@ -39,8 +39,8 @@ def test_autodetect_used_when_no_explicit_or_env(self, monkeypatch): monkeypatch.delenv("OPENAI_API_KEY", raising=False) s = _make_settings(**{"llm.provider": "auto"}) mgr = LLMRuntimeManager(settings=s) - with patch("urllib.request.urlopen") as mock_open: - mock_resp = MagicMock = patch # type: ignore + with patch("urllib.request.urlopen"): + pass # type: ignore mgr.get_selection_debug() diff --git a/tests/core/test_clustering.py b/tests/core/test_clustering.py index 36eb637..1941b27 100644 --- a/tests/core/test_clustering.py +++ b/tests/core/test_clustering.py @@ -103,7 +103,7 @@ def _get(model_name: str): "alpha": 1.0, "cluster_selection_method": "eom", } - labels, algo = run_clustering(embeddings, "HDBSCAN", params) + _labels, algo = run_clustering(embeddings, "HDBSCAN", params) assert algo == "HDBSCAN" assert received_kwargs["cluster_selection_epsilon"] == 0.1 assert received_kwargs["allow_single_cluster"] is True @@ -138,7 +138,7 @@ def fit_predict(self, X): "tol": 1e-4, "algorithm": "lloyd", } - labels, algo = run_clustering(embeddings, "KMeans", params) + _labels, algo = run_clustering(embeddings, "KMeans", params) assert algo == "KMeans" assert received_kwargs["init"] == "random" assert received_kwargs["max_iter"] == 50 @@ -172,7 +172,7 @@ def fit_predict(self, X): "algorithm": "ball_tree", "leaf_size": 20, } - labels, algo = run_clustering(embeddings, "DBSCAN", params) + _labels, algo = run_clustering(embeddings, "DBSCAN", params) assert algo == "DBSCAN" assert received_kwargs["metric"] == "cosine" assert received_kwargs["algorithm"] == "ball_tree" @@ -207,7 +207,7 @@ def fit_predict(self, X): "algorithm": "kd_tree", "leaf_size": 30, } - labels, algo = run_clustering(embeddings, "OPTICS", params) + _labels, algo = run_clustering(embeddings, "OPTICS", params) assert algo == "OPTICS" assert received_kwargs["metric"] == "minkowski" assert received_kwargs["xi"] == 0.05 diff --git a/tests/core/test_provider_factory.py b/tests/core/test_provider_factory.py index 9f6b4e6..fdb9232 100644 --- a/tests/core/test_provider_factory.py +++ b/tests/core/test_provider_factory.py @@ -25,7 +25,7 @@ def test_ephemeral(self): with patch("vector_inspector.core.provider_factory.get_connection_class") as mock_get_class: MockChroma = MagicMock() mock_get_class.return_value = MockChroma - conn = ProviderFactory.create("chromadb", {}) + ProviderFactory.create("chromadb", {}) mock_get_class.assert_called_once_with("chromadb") MockChroma.assert_called_once_with() @@ -33,7 +33,7 @@ def test_persistent(self): with patch("vector_inspector.core.provider_factory.get_connection_class") as mock_get_class: MockChroma = MagicMock() mock_get_class.return_value = MockChroma - conn = ProviderFactory.create("chromadb", {"type": "persistent", "path": "/data"}) + ProviderFactory.create("chromadb", {"type": "persistent", "path": "/data"}) mock_get_class.assert_called_once_with("chromadb") MockChroma.assert_called_once_with(path="/data") @@ -41,7 +41,7 @@ def test_http(self): with patch("vector_inspector.core.provider_factory.get_connection_class") as mock_get_class: MockChroma = MagicMock() mock_get_class.return_value = MockChroma - conn = ProviderFactory.create("chromadb", {"type": "http", "host": "localhost", "port": 8000}) + ProviderFactory.create("chromadb", {"type": "http", "host": "localhost", "port": 8000}) mock_get_class.assert_called_once_with("chromadb") MockChroma.assert_called_once_with(host="localhost", port=8000) diff --git a/tests/core/test_qdrant_helpers.py b/tests/core/test_qdrant_helpers.py index dbab426..85a62be 100644 --- a/tests/core/test_qdrant_helpers.py +++ b/tests/core/test_qdrant_helpers.py @@ -118,7 +118,7 @@ def test_no_collection_info_uses_default(self, monkeypatch): lambda name, mtype: mock_model, ) - model, model_name, model_type = resolve_embedding_model(mock_conn, "my_col") + _model, model_name, model_type = resolve_embedding_model(mock_conn, "my_col") assert model_name == DEFAULT_MODEL[0] assert model_type == DEFAULT_MODEL[1] @@ -139,7 +139,7 @@ def test_uses_explicit_embedding_model_from_collection_info(self, monkeypatch): lambda name, mtype: mock_model, ) - model, name, mtype = resolve_embedding_model(mock_conn, "col") + _model, name, mtype = resolve_embedding_model(mock_conn, "col") assert name == "custom-model" assert mtype == "clip" @@ -161,7 +161,7 @@ def test_falls_back_to_dimension_lookup(self, monkeypatch): lambda dim: ("dim-model", "sentence-transformer"), ) - model, name, mtype = resolve_embedding_model(mock_conn, "col") + _model, name, _mtype = resolve_embedding_model(mock_conn, "col") assert name == "dim-model" def test_unknown_dimension_uses_default(self, monkeypatch): @@ -179,5 +179,5 @@ def test_unknown_dimension_uses_default(self, monkeypatch): lambda name, mtype: mock_model, ) - model, name, mtype = resolve_embedding_model(mock_conn, "col") + _model, name, _mtype = resolve_embedding_model(mock_conn, "col") assert name == DEFAULT_MODEL[0] diff --git a/tests/fakes/fake_provider.py b/tests/fakes/fake_provider.py index 917c129..5ee0d35 100644 --- a/tests/fakes/fake_provider.py +++ b/tests/fakes/fake_provider.py @@ -25,9 +25,7 @@ def _ensure_collection(self, name: str): "embeddings": [], } - def _matches_where( - self, meta: Optional[dict[str, Any]], where: Optional[dict[str, Any]] - ) -> bool: + def _matches_where(self, meta: Optional[dict[str, Any]], where: Optional[dict[str, Any]]) -> bool: if not where: return True if meta is None: @@ -55,9 +53,7 @@ def get_collection_info(self, name: str) -> Optional[dict[str, Any]]: return { "name": name, "count": len(col.get("ids", [])), - "metadata_fields": list( - {k for m in col.get("metadatas", []) for k in (m or {}).keys()} - ), + "metadata_fields": list({k for m in col.get("metadatas", []) for k in (m or {})}), } # Alias used by some code paths @@ -143,9 +139,7 @@ def query_collection( # If query embeddings provided, compute dot-product similarity for first query q = np.array(query_embeddings[0], dtype=float) - emb_list = [ - embeddings[i] if embeddings[i] is not None else np.zeros_like(q) for i in indices - ] + emb_list = [embeddings[i] if embeddings[i] is not None else np.zeros_like(q) for i in indices] if len(emb_list) == 0: return {"ids": [], "distances": [], "documents": [], "metadatas": [], "embeddings": []} diff --git a/tests/integration/test_clustering_real_embeddings.py b/tests/integration/test_clustering_real_embeddings.py index 4b4c7a6..8cd12f1 100644 --- a/tests/integration/test_clustering_real_embeddings.py +++ b/tests/integration/test_clustering_real_embeddings.py @@ -141,7 +141,7 @@ def test_single_point_kmeans(self): from vector_inspector.core.clustering import run_clustering embeddings = np.array([[1.0, 2.0, 3.0]]) - labels, algo = run_clustering(embeddings, "KMeans", {"n_clusters": 1}) + labels, _algo = run_clustering(embeddings, "KMeans", {"n_clusters": 1}) assert len(labels) == 1 assert labels[0] == 0 diff --git a/tests/metadata/test_data_operations.py b/tests/metadata/test_data_operations.py index 7feefe6..1200d24 100644 --- a/tests/metadata/test_data_operations.py +++ b/tests/metadata/test_data_operations.py @@ -16,11 +16,11 @@ def __init__(self, data=None, update_success=True): self.last_update_args = {} def get_all_items(self, collection, limit=None, offset=None, where=None): - self.last_get_all_args = dict(collection=collection, limit=limit, offset=offset, where=where) + self.last_get_all_args = {"collection": collection, "limit": limit, "offset": offset, "where": where} return self._data def update_items(self, collection, ids, documents=None, metadatas=None, embeddings=None): - self.last_update_args = dict(ids=ids, documents=documents, metadatas=metadatas, embeddings=embeddings) + self.last_update_args = {"ids": ids, "documents": documents, "metadatas": metadatas, "embeddings": embeddings} return self._update_success diff --git a/tests/metadata/test_telemetry_settings_panel.py b/tests/metadata/test_telemetry_settings_panel.py index 7999ddb..a53858e 100644 --- a/tests/metadata/test_telemetry_settings_panel.py +++ b/tests/metadata/test_telemetry_settings_panel.py @@ -63,7 +63,10 @@ def test_settings_panel_hook_no_duplicate_registration(): hook.clear() calls = [] - handler = lambda l, s, d=None: calls.append(1) + + def handler(l, s, d=None): + return calls.append(1) + hook.register(handler) hook.register(handler) # second registration should be ignored hook.trigger(MagicMock(), MagicMock()) @@ -76,7 +79,10 @@ def test_settings_panel_hook_unregister(): hook.clear() calls = [] - handler = lambda l, s, d=None: calls.append(1) + + def handler(l, s, d=None): + return calls.append(1) + hook.register(handler) hook.unregister(handler) hook.trigger(MagicMock(), MagicMock()) @@ -130,7 +136,10 @@ def test_table_context_menu_hook_no_duplicate(): hook.clear() calls = [] - h = lambda m, t, r, d=None: calls.append(1) + + def h(m, t, r, d=None): + return calls.append(1) + hook.register(h) hook.register(h) hook.trigger(MagicMock(), MagicMock(), 0) diff --git a/tests/providers/lancedb/test_flat_schema_metadata.py b/tests/providers/lancedb/test_flat_schema_metadata.py index e0f30e2..6764858 100644 --- a/tests/providers/lancedb/test_flat_schema_metadata.py +++ b/tests/providers/lancedb/test_flat_schema_metadata.py @@ -56,8 +56,9 @@ def test_flat_schema_metadata_extraction(tmp_path): metadata_fields = info["metadata_fields"] # "document" is the content column and should be excluded from metadata expected_fields = {"project", "filename", "heading", "type", "chunk_index"} - assert expected_fields.issubset(set(metadata_fields)), \ + assert expected_fields.issubset(set(metadata_fields)), ( f"Missing fields in metadata_fields. Expected {expected_fields}, got {set(metadata_fields)}" + ) # Verify document is NOT in metadata_fields (it's the content column) assert "document" not in metadata_fields, "Content column 'document' should not be in metadata_fields" diff --git a/tests/providers/pgvector/test_pgvector_connection.py b/tests/providers/pgvector/test_pgvector_connection.py index 373d057..eb80891 100644 --- a/tests/providers/pgvector/test_pgvector_connection.py +++ b/tests/providers/pgvector/test_pgvector_connection.py @@ -53,7 +53,7 @@ def test_pgvector_add_items(mock_pgvector_conn): def test_pgvector_add_items_missing_embeddings_auto_embed_fails(mock_pgvector_conn): - _, mock_cursor = mock_pgvector_conn + _, _mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() # Patch compute_embeddings_for_documents to raise Exception @@ -94,7 +94,7 @@ def test_pgvector_delete_collection(mock_pgvector_conn): def test_list_collections(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn # Simulate two tables returned mock_cursor.fetchall.return_value = [("table1",), ("table2",)] conn = PgVectorConnection() @@ -104,7 +104,7 @@ def test_list_collections(mock_pgvector_conn): def test_list_databases_uses_client_or_tmp(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn # Simulate databases mock_cursor.fetchall.return_value = [("postgres",), ("mydb",)] conn = PgVectorConnection() @@ -114,7 +114,7 @@ def test_list_databases_uses_client_or_tmp(mock_pgvector_conn): def test_get_items_parses_metadata_and_embedding(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn # Prepare a single row with metadata JSON and string vector row = ("id1", "doc1", json.dumps({"a": 1}), "[0.1,0.2]") mock_cursor.fetchall.return_value = [row] @@ -138,7 +138,7 @@ def test_get_items_parses_metadata_and_embedding(mock_pgvector_conn): def test_count_collection_returns_count(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn mock_cursor.fetchone.return_value = (42,) conn = PgVectorConnection() conn.connect() @@ -146,7 +146,7 @@ def test_count_collection_returns_count(mock_pgvector_conn): def test_query_collection_returns_per_query_lists(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn # Simulate one query embedding; rows returned per query # Provide metadata as JSON string and embedding string row = ("id1", "doc1", json.dumps({"k": "v"}), "[0.5,0.6]", 0.123) @@ -178,7 +178,7 @@ def test_query_collection_returns_per_query_lists(mock_pgvector_conn): def test_get_all_items_with_limit_offset_and_where(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn row1 = ("id1", "doc1", json.dumps({"t": "x"}), "[1,2]") row2 = ("id2", "doc2", json.dumps({"t": "y"}), "[3,4]") mock_cursor.fetchall.return_value = [row1, row2] @@ -203,7 +203,7 @@ def test_get_all_items_with_limit_offset_and_where(mock_pgvector_conn): def test_update_items_generates_embeddings_when_needed(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, _mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() @@ -227,7 +227,7 @@ def test_update_items_generates_embeddings_when_needed(mock_pgvector_conn): def test_delete_items_commits_and_returns_true(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() assert conn.delete_items("coll", ids=["x"]) is True @@ -235,7 +235,7 @@ def test_delete_items_commits_and_returns_true(mock_pgvector_conn): def test_pgvector_delete_where_uses_metadata_or_columns(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() @@ -254,7 +254,7 @@ def test_pgvector_delete_where_uses_metadata_or_columns(mock_pgvector_conn): def test_get_connection_info_reflects_connection_state(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, _mock_cursor = mock_pgvector_conn conn = PgVectorConnection(host="h", port=1, database="d", user="u") assert conn.get_connection_info()["connected"] is False conn.connect() @@ -287,7 +287,7 @@ def test_parse_vector_with_numpy_array(): def test_create_collection_commits_and_handles_distance_ops(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + mock_conn, _mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() # exercise with different distance keywords @@ -299,7 +299,7 @@ def test_create_collection_commits_and_handles_distance_ops(mock_pgvector_conn): def test_add_items_maps_metadata_columns_when_no_metadata_col(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() @@ -317,7 +317,7 @@ def test_add_items_maps_metadata_columns_when_no_metadata_col(mock_pgvector_conn # execute was called multiple times; inspect the last call args *_, last_call = mock_cursor.execute.call_args_list # call_args = (sql_obj, values) - call_args, call_kwargs = last_call + call_args, _call_kwargs = last_call # ensure the values list contains the id and embedding we provided assert ids[0] in call_args[1] assert embeddings[0] in call_args[1] @@ -334,7 +334,7 @@ def test_list_databases_handles_tmp_connect_failure(): def test__get_table_schema_returns_empty_on_error(mock_pgvector_conn): - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn # make fetchall raise mock_cursor.fetchall.side_effect = Exception("boom") conn = PgVectorConnection() @@ -344,7 +344,7 @@ def test__get_table_schema_returns_empty_on_error(mock_pgvector_conn): def test_pgvector_add_items_handles_exception(mock_pgvector_conn): """If DB cursor execute/insert raises, add_items should return False.""" - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() @@ -356,7 +356,7 @@ def test_pgvector_add_items_handles_exception(mock_pgvector_conn): def test_pgvector_get_all_items_handles_exception(mock_pgvector_conn): """If fetching rows raises, get_all_items should return None.""" - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() @@ -368,7 +368,7 @@ def test_pgvector_get_all_items_handles_exception(mock_pgvector_conn): def test_pgvector_delete_items_handles_exception(mock_pgvector_conn): """If delete execution raises, delete_items should return False.""" - mock_conn, mock_cursor = mock_pgvector_conn + _mock_conn, mock_cursor = mock_pgvector_conn conn = PgVectorConnection() conn.connect() diff --git a/tests/providers/pinecone/test_pinecone_connection.py b/tests/providers/pinecone/test_pinecone_connection.py index 07d4225..8489c8d 100644 --- a/tests/providers/pinecone/test_pinecone_connection.py +++ b/tests/providers/pinecone/test_pinecone_connection.py @@ -597,8 +597,7 @@ def test_pinecone_integration(): assert conn.connect() # List indexes - indexes = conn.list_collections() - print(f"Available indexes: {indexes}") + conn.list_collections() conn.disconnect() assert not conn.is_connected diff --git a/tests/providers/weaviate/test_weaviate_connection.py b/tests/providers/weaviate/test_weaviate_connection.py index bea4f1d..0e595d8 100644 --- a/tests/providers/weaviate/test_weaviate_connection.py +++ b/tests/providers/weaviate/test_weaviate_connection.py @@ -76,7 +76,7 @@ def test_weaviate_connection_with_api_key(): def test_weaviate_connect_success(mock_weaviate_client): """Test successful connection to Weaviate.""" - _mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, _mock_client = mock_weaviate_client conn = WeaviateConnection(host="localhost", port=8080) result = conn.connect() @@ -380,7 +380,7 @@ def test_weaviate_delete_items_by_id(mock_weaviate_client): def test_weaviate_add_items_handles_exception(mock_weaviate_client): """If batch add raises, add_items should return False.""" - mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, mock_client = mock_weaviate_client mock_collection = MagicMock() mock_batch = MagicMock() @@ -398,7 +398,7 @@ def test_weaviate_add_items_handles_exception(mock_weaviate_client): def test_weaviate_query_handles_exception(mock_weaviate_client): - mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, mock_client = mock_weaviate_client mock_collection = MagicMock() mock_collection.query.near_vector.side_effect = Exception("query fail") mock_client.collections.get.return_value = mock_collection @@ -410,7 +410,7 @@ def test_weaviate_query_handles_exception(mock_weaviate_client): def test_weaviate_delete_handles_exception(mock_weaviate_client): - mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, mock_client = mock_weaviate_client mock_collection = MagicMock() mock_collection.data.delete_by_id.side_effect = Exception("delete fail") mock_client.collections.get.return_value = mock_collection @@ -423,7 +423,7 @@ def test_weaviate_delete_handles_exception(mock_weaviate_client): def test_weaviate_get_all_items_handles_exception(mock_weaviate_client): - mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, mock_client = mock_weaviate_client mock_collection = MagicMock() mock_collection.query.fetch_objects.side_effect = Exception("fetch fail") mock_client.collections.get.return_value = mock_collection @@ -485,7 +485,7 @@ def test_weaviate_embedded_mode_init(): def test_weaviate_embedded_mode_connection(mock_weaviate_client): """Test connecting to Weaviate in embedded mode.""" - mock_weaviate, mock_client = mock_weaviate_client + mock_weaviate, _mock_client = mock_weaviate_client # Mock embedded options mock_embedded_options = MagicMock() @@ -553,7 +553,7 @@ def test_weaviate_embedded_collections(mock_weaviate_client): def test_weaviate_delete_where_and_ids_precedence(mock_weaviate_client): """Ensure Weaviate delete uses delete_many for filters and delete_by_id for ids.""" - mock_weaviate, mock_client = mock_weaviate_client + _mock_weaviate, mock_client = mock_weaviate_client # Prepare mock collection with data API mock_collection = MagicMock() diff --git a/tests/scripts/conftest.py b/tests/scripts/conftest.py index 1354050..875b36d 100644 --- a/tests/scripts/conftest.py +++ b/tests/scripts/conftest.py @@ -1,4 +1,5 @@ """Make scripts/ and project root importable from tests/scripts/.""" + import sys from pathlib import Path diff --git a/tests/scripts/test_bootstrap_installer.py b/tests/scripts/test_bootstrap_installer.py index 47b8fa8..008639a 100644 --- a/tests/scripts/test_bootstrap_installer.py +++ b/tests/scripts/test_bootstrap_installer.py @@ -10,7 +10,6 @@ import json import subprocess import sys -from pathlib import Path from unittest.mock import MagicMock, patch import pytest @@ -18,10 +17,12 @@ # Import from vi_installer package modules from vi_installer import platform, python, venv, shortcuts, state, ui + # For compatibility with existing tests, create a simple namespace object class MockBI: pass + bi = MockBI() # Populate with module references @@ -317,9 +318,7 @@ def test_check_existing_install_path_does_not_exist(tmp_path): def test_check_existing_install_replace(tmp_path): install_root = tmp_path / "vi" install_root.mkdir() - (install_root / "bootstrap-state.json").write_text( - json.dumps({"app": "Vector Inspector"}), encoding="utf-8" - ) + (install_root / "bootstrap-state.json").write_text(json.dumps({"app": "Vector Inspector"}), encoding="utf-8") config_dir = tmp_path / ".vector-inspector" config_dir.mkdir() (config_dir / "install_path").write_text(str(install_root), encoding="utf-8") @@ -332,7 +331,7 @@ def test_check_existing_install_replace(tmp_path): patch("sys.stdin") as mock_stdin, ): mock_stdin.isatty.return_value = True - path, mode = bi.check_existing_install() + _path, mode = bi.check_existing_install() assert mode == "replace" assert not install_root.exists() @@ -424,9 +423,7 @@ def test_check_existing_install_strips_whitespace_in_path(tmp_path): def test_run_uninstall_removes_install_root(tmp_path): install_root = tmp_path / "vi" install_root.mkdir() - (install_root / "bootstrap-state.json").write_text( - json.dumps({"app": "Vector Inspector"}), encoding="utf-8" - ) + (install_root / "bootstrap-state.json").write_text(json.dumps({"app": "Vector Inspector"}), encoding="utf-8") config_dir = tmp_path / ".vector-inspector" config_dir.mkdir() install_path_file = config_dir / "install_path" @@ -464,9 +461,7 @@ def test_run_uninstall_cancelled(tmp_path): def test_run_uninstall_removes_desktop_shortcut(tmp_path): install_root = tmp_path / "vi" install_root.mkdir() - (install_root / "bootstrap-state.json").write_text( - json.dumps({"app": "Vector Inspector"}), encoding="utf-8" - ) + (install_root / "bootstrap-state.json").write_text(json.dumps({"app": "Vector Inspector"}), encoding="utf-8") shortcut = tmp_path / "Vector Inspector.command" shortcut.write_text("#!/bin/sh\n", encoding="utf-8") config_dir = tmp_path / ".vector-inspector" diff --git a/tests/services/test_backup_helpers.py b/tests/services/test_backup_helpers.py index 6377266..772ae05 100644 --- a/tests/services/test_backup_helpers.py +++ b/tests/services/test_backup_helpers.py @@ -1,6 +1,3 @@ -import json -from pathlib import Path - from vector_inspector.services.backup_helpers import write_backup_zip, read_backup_zip, normalize_embeddings diff --git a/tests/services/test_collection_service_edgecases.py b/tests/services/test_collection_service_edgecases.py index a1d99a1..98020fe 100644 --- a/tests/services/test_collection_service_edgecases.py +++ b/tests/services/test_collection_service_edgecases.py @@ -68,7 +68,7 @@ def add_items(self, collection_name, documents, metadatas, ids, embeddings): return True conn = WeaviateConn() - success, msg = svc.populate_with_sample_data(conn, "colW", 2, SampleDataType.TEXT, "m") + success, _msg = svc.populate_with_sample_data(conn, "colW", 2, SampleDataType.TEXT, "m") assert success is True assert "ids" in captured # ensure ids are UUIDs @@ -93,7 +93,7 @@ def add_items(self, collection_name, documents, metadatas, ids, embeddings): return True conn = Conn() - success, msg = svc.populate_with_sample_data(conn, "colN", 3, SampleDataType.TEXT, "m") + success, _msg = svc.populate_with_sample_data(conn, "colN", 3, SampleDataType.TEXT, "m") assert success is True assert captured["embeddings_type"] is list assert captured["inner_len"] == 5 @@ -140,7 +140,7 @@ def save_embedding_model(self, *args, **kwargs): monkeypatch.setattr(settings_mod, "SettingsService", BadSettings) conn = FakeConnection(add_success=True) - success, msg = svc.populate_with_sample_data(conn, "colS", 2, SampleDataType.TEXT, "m") + success, _msg = svc.populate_with_sample_data(conn, "colS", 2, SampleDataType.TEXT, "m") assert success is True @@ -168,7 +168,7 @@ def on_completed(name, success, message): svc.operation_completed.connect(on_completed) conn = FakeConnection(add_success=True) - success, msg = svc.populate_with_sample_data(conn, "colSig", 2, SampleDataType.TEXT, "m") + success, _msg = svc.populate_with_sample_data(conn, "colSig", 2, SampleDataType.TEXT, "m") assert success is True # basic checks for recorded sequences assert any(s[0] == "started" for s in seq) @@ -210,7 +210,7 @@ def save_embedding_model(self, **kwargs): # connection WITH a profile_name conn = FakeConnection(add_success=True, profile_name="my-profile") - success, msg = svc.populate_with_sample_data(conn, "colP", 2, SampleDataType.TEXT, "my-model") + success, _msg = svc.populate_with_sample_data(conn, "colP", 2, SampleDataType.TEXT, "my-model") assert success is True # The settings save should have been called since profile_name was set assert len(saved) == 1 diff --git a/tests/services/test_credential_service.py b/tests/services/test_credential_service.py index 75b4d5c..2445c94 100644 --- a/tests/services/test_credential_service.py +++ b/tests/services/test_credential_service.py @@ -87,7 +87,7 @@ def test_get_credentials_exception_returns_none(): def test_delete_credentials_with_keyring(): """delete_credentials calls keyring.delete_password.""" - svc, fake_keyring, FakePDE = _make_service_with_keyring() + svc, fake_keyring, _FakePDE = _make_service_with_keyring() result = svc.delete_credentials("profile-1") diff --git a/tests/services/test_filter_service.py b/tests/services/test_filter_service.py index 768efaf..e7cbac7 100644 --- a/tests/services/test_filter_service.py +++ b/tests/services/test_filter_service.py @@ -1,6 +1,6 @@ -import pytest from vector_inspector.services.filter_service import apply_client_side_filters + def sample_data(): return { "ids": [1, 2, 3], @@ -13,6 +13,7 @@ def sample_data(): "embeddings": [[0.1], [0.2], [0.3]], } + def test_no_filters_returns_all(): data = sample_data() result = apply_client_side_filters(data, []) @@ -27,6 +28,7 @@ def test_no_filters_returns_all(): "embeddings": [[0.1], [0.2], [0.3]], } + def test_contains_document(): data = sample_data() filters = [{"field": "document", "op": "contains", "value": "fox"}] @@ -36,6 +38,7 @@ def test_contains_document(): assert result["metadatas"] == [{"category": "animal", "author": "A"}] assert result["embeddings"] == [[0.1]] + def test_not_contains_metadata(): data = sample_data() filters = [{"field": "category", "op": "not_contains", "value": "animal"}] @@ -45,6 +48,7 @@ def test_not_contains_metadata(): assert result["metadatas"] == [{"category": "greeting", "author": "C"}] assert result["embeddings"] == [[0.3]] + def test_multiple_filters(): data = sample_data() filters = [ @@ -57,10 +61,12 @@ def test_multiple_filters(): assert result["metadatas"] == [{"category": "animal", "author": "B"}] assert result["embeddings"] == [[0.2]] + def test_empty_data(): result = apply_client_side_filters({}, []) assert result == {} + def test_missing_fields(): data = {"ids": [1], "documents": ["foo"]} filters = [{"field": "category", "op": "contains", "value": "animal"}] @@ -69,18 +75,21 @@ def test_missing_fields(): assert result["documents"] == [] assert result["metadatas"] == [] + def test_case_sensitivity(): data = {"ids": [1], "documents": ["Hello World"], "metadatas": [{"author": "Alice"}]} filters = [{"field": "document", "op": "contains", "value": "hello world"}] result = apply_client_side_filters(data, filters) assert result["ids"] == [1] + def test_non_string_metadata_value(): data = {"ids": [1], "documents": ["foo"], "metadatas": [{"num": 123}]} filters = [{"field": "num", "op": "contains", "value": "123"}] result = apply_client_side_filters(data, filters) assert result["ids"] == [1] + def test_unknown_operator(): data = {"ids": [1], "documents": ["foo"], "metadatas": [{"author": "Bob"}]} filters = [{"field": "author", "op": "unknown", "value": "Bob"}] @@ -88,6 +97,7 @@ def test_unknown_operator(): # Unknown op: should not filter out assert result["ids"] == [1] + def test_large_input(): data = { "ids": list(range(1000)), diff --git a/tests/services/test_install_service.py b/tests/services/test_install_service.py index 900a8b1..64e4270 100644 --- a/tests/services/test_install_service.py +++ b/tests/services/test_install_service.py @@ -142,7 +142,7 @@ def test_install_feature_success(): fake_proc = _make_fake_process(0, ["Successfully installed scikit-learn\n"]) with patch("subprocess.Popen", return_value=fake_proc) as mock_popen: with patch(_TELEMETRY_PATH): - returncode, output = install("viz") + returncode, _output = install("viz") assert returncode == 0 cmd_used = mock_popen.call_args[0][0] assert "vector-inspector[viz]" in cmd_used[-1] @@ -207,7 +207,7 @@ def test_uninstall_feature_success(): fake_proc = _make_fake_process(0, ["Successfully uninstalled scikit-learn\n"]) with patch("subprocess.Popen", return_value=fake_proc): with patch(_TELEMETRY_PATH): - returncode, output = uninstall("viz") + returncode, _output = uninstall("viz") assert returncode == 0 diff --git a/tests/services/test_profile_service.py b/tests/services/test_profile_service.py index 9fadc2e..3d869df 100644 --- a/tests/services/test_profile_service.py +++ b/tests/services/test_profile_service.py @@ -277,7 +277,7 @@ def test_export_profiles_no_credentials(svc): def test_export_profiles_with_credentials(svc): - pid = svc.create_profile("DB", "chromadb", {}) + svc.create_profile("DB", "chromadb", {}) with patch.object(svc.credential_service, "get_credentials", return_value={"key": "v"}): exported = svc.export_profiles(include_credentials=True) assert exported[0]["credentials"] == {"key": "v"} diff --git a/tests/services/test_telemetry_service.py b/tests/services/test_telemetry_service.py index cb0bca0..8827a08 100644 --- a/tests/services/test_telemetry_service.py +++ b/tests/services/test_telemetry_service.py @@ -883,4 +883,3 @@ def test_session_start_deduplication_keeps_other_events(): assert event_names.count("session.start") == 1 assert "app_launch" in event_names TelemetryService.reset_for_tests() - diff --git a/tests/services/test_visualization_service.py b/tests/services/test_visualization_service.py index a5f5a1b..e92a120 100644 --- a/tests/services/test_visualization_service.py +++ b/tests/services/test_visualization_service.py @@ -118,7 +118,7 @@ def test_prepare_plot_data_basic(): # Simple reduced embeddings reduced = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) - embeddings, labels, colors = svc.prepare_plot_data(reduced) + _embeddings, labels, colors = svc.prepare_plot_data(reduced) assert len(labels) == 3 assert labels[0] == "Point 0" diff --git a/tests/test_content_column_detection.py b/tests/test_content_column_detection.py index 991f67f..04aa5bc 100644 --- a/tests/test_content_column_detection.py +++ b/tests/test_content_column_detection.py @@ -1,6 +1,5 @@ """Test content column detection across different schemas.""" -import pytest from vector_inspector.core.connections.pgvector_connection import PgVectorConnection @@ -135,7 +134,7 @@ def test_multiple_text_columns_priority(): "body": "text", "document": "text", "notes": "text", - "embedding": "vector" + "embedding": "vector", } result = conn._detect_content_column("test_col", schema) assert result == "document" diff --git a/tests/test_content_column_persistence.py b/tests/test_content_column_persistence.py index ab2069f..2fee23d 100644 --- a/tests/test_content_column_persistence.py +++ b/tests/test_content_column_persistence.py @@ -1,4 +1,5 @@ """Tests for content column persistence across sessions.""" + from vector_inspector.core.connections.lancedb_connection import LanceDBConnection diff --git a/tests/test_lancedb_metadata_padding.py b/tests/test_lancedb_metadata_padding.py index cedbe0b..775f286 100644 --- a/tests/test_lancedb_metadata_padding.py +++ b/tests/test_lancedb_metadata_padding.py @@ -19,12 +19,14 @@ def test_query_collection_metadata_padding(lancedb_conn): """Test that query_collection pads metadatas to match result count.""" # Mock table with results that have no metadata column mock_table = Mock() - mock_results_df = pd.DataFrame({ - "id": ["id1", "id2", "id3"], - "document": ["doc1", "doc2", "doc3"], - "_distance": [0.1, 0.2, 0.3], - "vector": [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] - }) + mock_results_df = pd.DataFrame( + { + "id": ["id1", "id2", "id3"], + "document": ["doc1", "doc2", "doc3"], + "_distance": [0.1, 0.2, 0.3], + "vector": [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + } + ) mock_search = Mock() mock_search.limit.return_value.to_pandas.return_value = mock_results_df @@ -32,11 +34,7 @@ def test_query_collection_metadata_padding(lancedb_conn): lancedb_conn._db.open_table.return_value = mock_table # Query with embeddings - result = lancedb_conn.query_collection( - "test_collection", - query_embeddings=[[0.1, 0.2]], - n_results=3 - ) + result = lancedb_conn.query_collection("test_collection", query_embeddings=[[0.1, 0.2]], n_results=3) assert result is not None assert len(result["ids"]) == 3 @@ -50,11 +48,9 @@ def test_get_all_items_metadata_padding(lancedb_conn): """Test that get_all_items pads metadatas to match result count.""" # Mock table with no metadata column mock_table = Mock() - mock_df = pd.DataFrame({ - "id": ["id1", "id2"], - "document": ["content1", "content2"], - "vector": [[1.0, 2.0], [3.0, 4.0]] - }) + mock_df = pd.DataFrame( + {"id": ["id1", "id2"], "document": ["content1", "content2"], "vector": [[1.0, 2.0], [3.0, 4.0]]} + ) mock_table.to_pandas.return_value = mock_df lancedb_conn._db.open_table.return_value = mock_table @@ -74,24 +70,22 @@ def test_query_collection_with_sparse_metadata(lancedb_conn): mock_table = Mock() # Simulate sparse metadata - some rows have no metadata - mock_results_df = pd.DataFrame({ - "id": ["id1", "id2", "id3"], - "document": ["doc1", "doc2", "doc3"], - "metadata": ['{"key": "value1"}', None, None], # Only first has metadata - "_distance": [0.1, 0.2, 0.3], - "vector": [[0.1], [0.2], [0.3]] - }) + mock_results_df = pd.DataFrame( + { + "id": ["id1", "id2", "id3"], + "document": ["doc1", "doc2", "doc3"], + "metadata": ['{"key": "value1"}', None, None], # Only first has metadata + "_distance": [0.1, 0.2, 0.3], + "vector": [[0.1], [0.2], [0.3]], + } + ) mock_search = Mock() mock_search.limit.return_value.to_pandas.return_value = mock_results_df mock_table.search.return_value = mock_search lancedb_conn._db.open_table.return_value = mock_table - result = lancedb_conn.query_collection( - "test_collection", - query_embeddings=[[0.1]], - n_results=3 - ) + result = lancedb_conn.query_collection("test_collection", query_embeddings=[[0.1]], n_results=3) assert len(result["ids"]) == 3 assert len(result["metadatas"]) == 3 @@ -104,11 +98,7 @@ def test_query_collection_with_sparse_metadata(lancedb_conn): def test_get_all_items_empty_collection(lancedb_conn): """Test get_all_items with empty collection.""" mock_table = Mock() - mock_df = pd.DataFrame({ - "id": [], - "document": [], - "vector": [] - }) + mock_df = pd.DataFrame({"id": [], "document": [], "vector": []}) mock_table.to_pandas.return_value = mock_df lancedb_conn._db.open_table.return_value = mock_table @@ -125,24 +115,22 @@ def test_query_collection_metadata_matches_documents(lancedb_conn): mock_table = Mock() # Create results with 5 items but metadata column missing - mock_results_df = pd.DataFrame({ - "id": [f"id{i}" for i in range(5)], - "document": [f"doc{i}" for i in range(5)], - "_distance": [0.1 * i for i in range(5)], - "vector": [[float(i)] for i in range(5)] - # No metadata column at all - }) + mock_results_df = pd.DataFrame( + { + "id": [f"id{i}" for i in range(5)], + "document": [f"doc{i}" for i in range(5)], + "_distance": [0.1 * i for i in range(5)], + "vector": [[float(i)] for i in range(5)], + # No metadata column at all + } + ) mock_search = Mock() mock_search.limit.return_value.to_pandas.return_value = mock_results_df mock_table.search.return_value = mock_search lancedb_conn._db.open_table.return_value = mock_table - result = lancedb_conn.query_collection( - "test_collection", - query_embeddings=[[0.5]], - n_results=5 - ) + result = lancedb_conn.query_collection("test_collection", query_embeddings=[[0.5]], n_results=5) # All arrays must have same length assert len(result["ids"]) == len(result["documents"]) diff --git a/tests/test_ollama_embedding.py b/tests/test_ollama_embedding.py index 11c1e92..dacbbab 100644 --- a/tests/test_ollama_embedding.py +++ b/tests/test_ollama_embedding.py @@ -15,12 +15,10 @@ def test_load_embedding_model_ollama(): def test_encode_text_ollama_success(): """Test encoding text with Ollama.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: # Mock HTTP response mock_response = Mock() - mock_response.read.return_value = json.dumps({ - "embeddings": [[0.1, 0.2, 0.3, 0.4]] - }).encode('utf-8') + mock_response.read.return_value = json.dumps({"embeddings": [[0.1, 0.2, 0.3, 0.4]]}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response @@ -35,11 +33,9 @@ def test_encode_text_ollama_success(): def test_encode_text_ollama_empty_embeddings(): """Test encoding with empty embeddings response raises error.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() - mock_response.read.return_value = json.dumps({ - "embeddings": [] - }).encode('utf-8') + mock_response.read.return_value = json.dumps({"embeddings": []}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response @@ -50,7 +46,7 @@ def test_encode_text_ollama_empty_embeddings(): def test_encode_text_ollama_connection_error(): """Test encoding handles connection errors.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_urlopen.side_effect = Exception("Connection refused") with pytest.raises(RuntimeError, match="Failed to get embedding from Ollama"): @@ -59,10 +55,9 @@ def test_encode_text_ollama_connection_error(): def test_encode_text_ollama_request_format(): """Test that Ollama request is formatted correctly.""" - with patch('urllib.request.Request') as mock_request, \ - patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.Request") as mock_request, patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() - mock_response.read.return_value = json.dumps({"embeddings": [[0.5]]}).encode('utf-8') + mock_response.read.return_value = json.dumps({"embeddings": [[0.5]]}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response @@ -122,7 +117,7 @@ def test_encode_text_with_string_sentence_transformer(): def test_encode_text_ollama_malformed_json(): """Test handling of malformed JSON response from Ollama.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() mock_response.read.return_value = b"not valid json" mock_response.__enter__ = Mock(return_value=mock_response) @@ -135,9 +130,9 @@ def test_encode_text_ollama_malformed_json(): def test_encode_text_ollama_missing_embeddings_key(): """Test handling of response without 'embeddings' key raises error.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() - mock_response.read.return_value = json.dumps({"error": "model not found"}).encode('utf-8') + mock_response.read.return_value = json.dumps({"error": "model not found"}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response @@ -149,14 +144,11 @@ def test_encode_text_ollama_missing_embeddings_key(): def test_encode_text_ollama_http_error(): """Test handling of HTTP error responses.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: from urllib.error import HTTPError + mock_urlopen.side_effect = HTTPError( - url="http://localhost:11434/api/embed", - code=404, - msg="Not Found", - hdrs={}, - fp=None + url="http://localhost:11434/api/embed", code=404, msg="Not Found", hdrs={}, fp=None ) with pytest.raises(RuntimeError, match="Failed to get embedding from Ollama"): @@ -165,8 +157,9 @@ def test_encode_text_ollama_http_error(): def test_encode_text_ollama_url_error(): """Test handling of URL/connection errors.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: from urllib.error import URLError + mock_urlopen.side_effect = URLError("Connection refused") with pytest.raises(RuntimeError, match="Failed to get embedding from Ollama"): @@ -175,11 +168,9 @@ def test_encode_text_ollama_url_error(): def test_encode_text_ollama_multiple_embeddings(): """Test that only first embedding is returned when multiple are present.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() - mock_response.read.return_value = json.dumps({ - "embeddings": [[0.1, 0.2], [0.3, 0.4]] - }).encode('utf-8') + mock_response.read.return_value = json.dumps({"embeddings": [[0.1, 0.2], [0.3, 0.4]]}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response @@ -190,11 +181,9 @@ def test_encode_text_ollama_multiple_embeddings(): def test_encode_text_empty_string(): """Test encoding empty string with Ollama.""" - with patch('urllib.request.urlopen') as mock_urlopen: + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = Mock() - mock_response.read.return_value = json.dumps({ - "embeddings": [[0.0, 0.0, 0.0]] - }).encode('utf-8') + mock_response.read.return_value = json.dumps({"embeddings": [[0.0, 0.0, 0.0]]}).encode("utf-8") mock_response.__enter__ = Mock(return_value=mock_response) mock_response.__exit__ = Mock(return_value=None) mock_urlopen.return_value = mock_response diff --git a/tests/test_pgvector_content_column.py b/tests/test_pgvector_content_column.py index dbd9f0f..f89aa74 100644 --- a/tests/test_pgvector_content_column.py +++ b/tests/test_pgvector_content_column.py @@ -25,11 +25,7 @@ def test_get_table_schema_basic(): schema = conn._get_table_schema("test_table") # Uses data_type for standard types, udt_name for USER-DEFINED - assert schema == { - "id": "integer", - "content": "text", - "embedding": "vector" - } + assert schema == {"id": "integer", "content": "text", "embedding": "vector"} def test_get_table_schema_varchar_variations(): @@ -115,12 +111,7 @@ def test_content_column_with_vector_dimensions(): """Test that vector columns with dimensions are handled.""" conn = PgVectorConnection() - schema = { - "id": "integer", - "content": "text", - "embedding_768": "vector(768)", - "embedding_384": "vector(384)" - } + schema = {"id": "integer", "content": "text", "embedding_768": "vector(768)", "embedding_384": "vector(384)"} result = conn._detect_content_column("test", schema) assert result == "content" @@ -177,11 +168,7 @@ def test_detect_with_mixed_case_columns(): """Test detection with mixed-case column names.""" conn = PgVectorConnection() - schema = { - "Id": "integer", - "Content": "text", - "Embedding": "vector" - } + schema = {"Id": "integer", "Content": "text", "Embedding": "vector"} # Case-sensitive match should not find 'content' but should find 'Content' result = conn._detect_content_column("test", schema) diff --git a/tests/test_runner.py b/tests/test_runner.py index e1324c6..ec7e566 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -16,7 +16,7 @@ def run_tests(): if len(args) == 0: args = ["tests"] - returncode = pytest.main( + pytest.main( [ # Turn up verbosity "-vv", @@ -29,8 +29,6 @@ def run_tests(): + args ) - print(f">>>>>>>>>> EXIT {returncode} <<<<<<<<<<") - if __name__ == "__main__": run_tests() diff --git a/tests/ui/test_llm_console.py b/tests/ui/test_llm_console.py index 7a2478e..7702373 100644 --- a/tests/ui/test_llm_console.py +++ b/tests/ui/test_llm_console.py @@ -78,7 +78,7 @@ def test_run_emits_single_chunk_and_done(self, qtbot): provider = FakeLLMProvider(mode="echo") # echo mode; streaming=False # Patch capabilities to report non-streaming - caps = provider.get_capabilities() + provider.get_capabilities() caps_no_stream = MagicMock() caps_no_stream.supports_streaming = False with patch.object(provider, "get_capabilities", return_value=caps_no_stream): diff --git a/tests/utils/fake_llm_provider.py b/tests/utils/fake_llm_provider.py index 132944d..57696f2 100644 --- a/tests/utils/fake_llm_provider.py +++ b/tests/utils/fake_llm_provider.py @@ -136,8 +136,7 @@ def generate_messages( self._maybe_inject_error(model, request_id) if stream or self._mode == _MODE_STREAMING: return self.stream_messages(messages, model, **{**kwargs, "request_id": request_id}) - response = self._build_response(messages) - return response + return self._build_response(messages) def stream_messages( self, diff --git a/tests/utils/test_exception_handler.py b/tests/utils/test_exception_handler.py index c532766..a710ca8 100644 --- a/tests/utils/test_exception_handler.py +++ b/tests/utils/test_exception_handler.py @@ -60,7 +60,7 @@ def will_raise(x): will_raise(1) assert "sent" in recorded - msg, event, extra = recorded["sent"] + _msg, event, extra = recorded["sent"] assert event == "TestEvent" assert extra.get("function") == "will_raise" diff --git a/tests/views/test_clustering_panel.py b/tests/views/test_clustering_panel.py index 85e6df0..8e760cc 100644 --- a/tests/views/test_clustering_panel.py +++ b/tests/views/test_clustering_panel.py @@ -138,7 +138,6 @@ def test_get_clustering_params_hdbscan_basic(panel): def test_get_clustering_params_hdbscan_advanced(qtbot, app_state_with_fake_provider): """HDBSCAN params include advanced params when advanced_features_enabled.""" - app_state = app_state_with_fake_provider # Override advanced_features_enabled to return True class AdvancedAppState: diff --git a/tests/views/test_collection_worker.py b/tests/views/test_collection_worker.py index 37ea6dd..1258919 100644 --- a/tests/views/test_collection_worker.py +++ b/tests/views/test_collection_worker.py @@ -78,7 +78,7 @@ def test_worker_emits_creation_complete_false_on_service_failure(qtbot, monkeypa with qtbot.waitSignal(worker.creation_complete, timeout=5000) as blocker: worker.start() - success, msg = blocker.args + success, _msg = blocker.args assert success is False diff --git a/tests/views/test_connection_view.py b/tests/views/test_connection_view.py index 12d058a..aca81c5 100644 --- a/tests/views/test_connection_view.py +++ b/tests/views/test_connection_view.py @@ -634,8 +634,6 @@ def test_show_connection_dialog_accepted(monkeypatch, qtbot): mod = __import__("vector_inspector.ui.views.connection_view", fromlist=["*"]) _make_fake_connection_view_dependencies(monkeypatch, mod) - captured_config = {} - class FakeDialog: def __init__(self, parent=None): pass diff --git a/tests/views/test_metadata_view_inline_details.py b/tests/views/test_metadata_view_inline_details.py index 09f211c..b2c8d03 100644 --- a/tests/views/test_metadata_view_inline_details.py +++ b/tests/views/test_metadata_view_inline_details.py @@ -135,7 +135,7 @@ def test_open_full_details_from_inline_pane(qtbot, metadata_view): metadata_view.table.selectRow(0) metadata_view._on_selection_changed() - with patch("vector_inspector.ui.views.metadata_view._show_item_details") as mock_show: + with patch("vector_inspector.ui.views.metadata_view._show_item_details"): # Click "Open full details" button in inline pane metadata_view.details_pane.full_details_btn.click() diff --git a/tests/views/test_search_view_ask_ai.py b/tests/views/test_search_view_ask_ai.py index 818ae41..f356619 100644 --- a/tests/views/test_search_view_ask_ai.py +++ b/tests/views/test_search_view_ask_ai.py @@ -365,7 +365,6 @@ def show(self): ) monkeypatch.setattr(_qw.QMessageBox, "show", lambda self: None) # Override _check_llm_configured to return False and record call - original_check = sv_with_results._check_llm_configured def _fake_check(): shown.append(True) diff --git a/tests/views/test_search_view_inline_details.py b/tests/views/test_search_view_inline_details.py index dbed37c..0e78a5c 100644 --- a/tests/views/test_search_view_inline_details.py +++ b/tests/views/test_search_view_inline_details.py @@ -302,7 +302,7 @@ def test_query_section_content_hugs_top(search_view): # Last item should be a stretch last_item = layout.itemAt(layout.count() - 1) # Stretch items have no widget - has_stretch = last_item and last_item.widget() is None + last_item and last_item.widget() is None # This is a proxy check, but indicates stretch was added assert layout.count() > 2 # Should have multiple items