Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
timeout-minutes: 45
strategy:
matrix:
python-minor-version: ["10", "13"]
python-minor-version: ["10", "13", "14"]
name: "Python Linux 3.${{ matrix.python-minor-version }} x86_64"
runs-on: "ubuntu-24.04-4x"
defaults:
Expand All @@ -124,7 +124,7 @@ jobs:
with:
memtest: true
- name: Upload wheels as artifacts
if: ${{ matrix.python-minor-version == '13' }}
if: ${{ matrix.python-minor-version == '14' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: linux-wheels
Expand All @@ -147,7 +147,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: 3.13
python-version: 3.14
- name: Download wheels
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
Expand All @@ -165,7 +165,7 @@ jobs:
linux-arm:
timeout-minutes: 45
runs-on: ubuntu-24.04-arm64-4x
name: Python Linux 3.13 ARM
name: Python Linux 3.14 ARM
defaults:
run:
shell: bash
Expand All @@ -178,7 +178,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: 3.13
python-version: 3.14
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
workspaces: python
Expand All @@ -195,7 +195,7 @@ jobs:
- uses: ./.github/workflows/run_tests
mac:
timeout-minutes: 45
name: Python macOS 3.13 ARM
name: Python macOS 3.14 ARM
runs-on: "warp-macos-14-arm64-6x"
defaults:
run:
Expand All @@ -209,7 +209,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
python-version: "3.14"
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
workspaces: python
Expand Down
10 changes: 5 additions & 5 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ build-backend = "maturin"
[project.optional-dependencies]
tests = [
"boto3",
"datasets",
"duckdb",
"datasets==4.4.0",
"duckdb>=1.5.0,<1.6.0",
"ml_dtypes",
"pillow",
"pandas",
Expand All @@ -72,8 +72,8 @@ geo = [
[dependency-groups]
tests = [
"boto3==1.40.43",
"datasets==4.1.1",
"duckdb==1.4.0",
"datasets==4.4.0",
"duckdb>=1.5.0,<1.6.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"ml_dtypes==0.5.3",
"pillow==11.3.0",
"pandas==2.3.3",
Expand Down Expand Up @@ -103,7 +103,7 @@ lint.select = ["F", "E", "W", "I", "G", "TCH", "PERF", "B019"]
"*.pyi" = ["E301", "E302"]

[tool.pyright]
pythonVersion = "3.13"
pythonVersion = "3.14"

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
printf '\n== python/pyproject.toml (around line 106) ==\n'
nl -ba python/pyproject.toml | sed -n '1,180p'

printf '\n== Search for pyright config and requires-python ==\n'
rg -n --hidden -S 'pythonVersion|requires-python|pyright' python . -g '!**/.git/**'

Repository: lance-format/lance

Length of output: 243


🏁 Script executed:

set -euo pipefail

printf '\n== python/pyproject.toml (excerpt) ==\n'
awk 'NR>=1 && NR<=180 { printf "%4d  %s\n", NR, $0 }' python/pyproject.toml

printf '\n== Search for pyright/min-version settings ==\n'
rg -n --hidden -S 'pythonVersion|requires-python|pyright|minimum version|min-version|3\.14|3\.10' python .

Repository: lance-format/lance

Length of output: 50375


🏁 Script executed:

set -euo pipefail

printf '\n== python/pyproject.toml (lines 90-120) ==\n'
sed -n '90,120p' python/pyproject.toml | awk '{ printf "%4d  %s\n", NR+89, $0 }'

printf '\n== config files mentioning pythonVersion/requires-python ==\n'
fd -HI -t f 'pyproject.toml|pyrightconfig.json|*.json|*.toml|*.yaml|*.yml' python . | head -n 50

printf '\n== exact matches in python/ directory ==\n'
rg -n --hidden -S '^(pythonVersion|requires-python)\s*=|pythonVersion|requires-python' python/pyproject.toml python/pyrightconfig.json python/.github python/.circleci python/.pre-commit-config.yaml python/.pre-commit-config.yml python/pyrightconfig.json

Repository: lance-format/lance

Length of output: 1551


🏁 Script executed:

set -euo pipefail

printf '\n== python/pyproject.toml (lines 90-120) ==\n'
awk 'NR>=90 && NR<=120 { printf "%4d  %s\n", NR, $0 }' python/pyproject.toml

printf '\n== likely config files under python/ ==\n'
find python -maxdepth 2 \( -name 'pyproject.toml' -o -name 'pyrightconfig.json' -o -name '*.toml' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' \) | sort

printf '\n== exact matches in python config files ==\n'
rg -n --hidden -S 'pythonVersion|requires-python' python/pyproject.toml python/pyrightconfig.json python/.pre-commit-config.yaml python/.pre-commit-config.yml python/.github python/.circleci 2>/dev/null || true

Repository: lance-format/lance

Length of output: 1395


🏁 Script executed:

set -euo pipefail

printf '\n== python/pyproject.toml (lines 90-120) ==\n'
awk 'NR>=90 && NR<=120 { printf "%4d  %s\n", NR, $0 }' python/pyproject.toml

printf '\n== matches for pythonVersion/requires-python under python/ ==\n'
rg -n --hidden -S 'pythonVersion|requires-python' python

Repository: lance-format/lance

Length of output: 1324


Align Pyright with the supported baseline python/pyproject.toml:105-106

requires-python is still >=3.10, but pythonVersion = "3.14" makes Pyright type-check against 3.14 syntax/stubs, so 3.10–3.13 compatibility issues can slip through. Set this to 3.10 or add a separate minimum-version check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/pyproject.toml` at line 106, Align the Pyright target with the
project’s supported baseline by changing the pythonVersion setting in
pyproject.toml to 3.10, matching requires-python >=3.10; alternatively, add a
separate check that type-checks against the minimum supported Python version.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a valid point. We should probably set pythonVersion to 3.10 for linting. I would keep this as is for consistency for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mocobeta Understood, thanks for confirming. I'll leave this as-is for now since you're intentionally prioritizing consistency with the CI baseline. Feel free to revisit this when you're ready to formalize the linting target at the minimum supported version (3.10).


✏️ Learnings added
Learnt from: mocobeta
Repo: lance-format/lance PR: 7728
File: python/pyproject.toml:106-106
Timestamp: 2026-07-11T16:30:06.317Z
Learning: In `python/pyproject.toml`, the `[tool.pyright]` `pythonVersion` setting is intentionally kept aligned with the latest supported Python version (currently 3.14) for consistency, even though `requires-python` allows a lower minimum (`>=3.10`). This is a known/accepted inconsistency; don't flag it again unless asked.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

# TODO: expand this list as we fix more files.
include = [
"python/lance/util.py",
Expand Down
70 changes: 62 additions & 8 deletions python/python/tests/compat/venv_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pathlib import Path
from typing import Any, Optional

import pytest
from packaging.version import InvalidVersion, Version

try:
Expand Down Expand Up @@ -96,6 +97,36 @@ def _safe(ref: str) -> str:
return re.sub(r"[^A-Za-z0-9._-]", "_", ref)


# Explicit skip whitelist for known (lance_version, python_version) incompatibilities.
# Each entry is (max_lance_version_inclusive, min_python_version_inclusive).
# A test is skipped when the tested lance version <= max_lance AND the current
# Python >= min_python. Add entries here only after verifying the incompatibility
# is a runtime/ABI issue rather than a format regression — anything NOT listed
# will surface as a test failure so new problems are visible.
_COMPAT_SKIP: list[tuple[str, tuple[int, int]]] = [
# lance 0.22.x abi3 wheel was built with old PyO3 (<0.23) that crashes on
# Python 3.14+ due to removed internal CPython APIs.
("0.22.0", (3, 14)),
]


def _skip_reason(lance_version: str) -> Optional[str]:
"""Return a skip reason if this lance/Python combo is whitelisted, else None."""
try:
ver = Version(lance_version)
except InvalidVersion:
return None
py = sys.version_info[:2]
for max_lance, min_python in _COMPAT_SKIP:
if ver <= Version(max_lance) and py >= min_python:
py_str = f"{py[0]}.{py[1]}"
return (
f"Lance {lance_version} + Python {py_str}: whitelisted skip "
f"(see _COMPAT_SKIP in venv_manager.py)"
)
return None


class VenvExecutor:
"""Manages a virtual environment with a specific Lance version."""

Expand Down Expand Up @@ -130,15 +161,29 @@ def python_path(self) -> Path:
def _marker_path(self) -> Path:
return self.venv_path / ".compat_ref"

@staticmethod
def _python_version_tag() -> str:
return f"{sys.version_info.major}.{sys.version_info.minor}"

def _validate_venv(self) -> bool:
"""A cached venv is reusable if it exists and its recorded ref matches. A marker
file is used (not `pip show`) so source-built commit refs also validate."""
"""A cached venv is reusable if it exists, its recorded ref matches, and it was
built with the same Python major.minor as the current interpreter.

The marker file format is two lines: `<lance_ref>\\n<python_major.minor>`.
Old single-line markers (no Python version) are treated as stale so the venv
is rebuilt — this handles cached venvs from a different Python installation."""
if not self.python_path.exists():
return False
try:
return self._marker_path.read_text().strip() == self.version
lines = self._marker_path.read_text().strip().splitlines()
except OSError:
return False
if not lines or lines[0] != self.version:
return False
# Require a Python version line; single-line markers are stale.
if len(lines) < 2 or lines[1] != self._python_version_tag():
return False
return True

def create(self):
"""Create the virtual environment and install the specified Lance version."""
Expand Down Expand Up @@ -170,7 +215,9 @@ def create(self):
self._install_release_wheel()
else:
self._build_from_source()
self._marker_path.write_text(self.version)
self._marker_path.write_text(
f"{self.version}\n{self._python_version_tag()}"
)
self._created = True

def _install_wheel(self, wheel: str):
Expand Down Expand Up @@ -264,10 +311,13 @@ def _ensure_subprocess(self):
# Start persistent subprocess
runner_script = Path(__file__).parent / "venv_runner.py"

# Set PYTHONPATH to include the tests directory
# Set PYTHONPATH so the subprocess can import compat test modules.
# pytest adds the `tests/` directory (the first ancestor without __init__.py)
# to sys.path, so test modules are imported as `compat.<module>`.
env = os.environ.copy()
tests_dir = Path(__file__).parent.parent
env["PYTHONPATH"] = str(tests_dir)
env.setdefault("RUST_BACKTRACE", "full")

# Capture stderr to a file so a Rust panic (which crashes the runner) can be
# surfaced in the error instead of an opaque "broken pipe".
Expand Down Expand Up @@ -357,6 +407,10 @@ def execute_method(
if not self._created:
raise RuntimeError("Virtual environment not created. Call create() first.")

reason = _skip_reason(self.version)
if reason:
pytest.skip(reason)

# Ensure subprocess is running
self._ensure_subprocess()
try:
Expand All @@ -379,14 +433,16 @@ def execute_method(

except (BrokenPipeError, EOFError, struct.error) as e:
# Subprocess died (usually a Rust panic); flush it, then surface that.
returncode = "unknown"
if self._subprocess is not None:
try:
self._subprocess.wait(timeout=2)
returncode = str(self._subprocess.returncode)
except Exception:
pass
panic = self._last_panic()
detail = panic or f"subprocess communication failed: {e}"
raise RuntimeError(f"Lance {self.version}: {detail}")
raise RuntimeError(f"Lance {self.version} (exit={returncode}): {detail}")

def cleanup(self):
"""Remove the virtual environment directory and terminate subprocess."""
Expand All @@ -404,8 +460,6 @@ def cleanup(self):

# Remove venv directory
if self.venv_path.exists():
import shutil

shutil.rmtree(self.venv_path)
self._created = False

Expand Down
27 changes: 22 additions & 5 deletions python/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ use pyo3::{PyResult, Python, exceptions::PyRuntimeError};

pub const SIGNAL_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);

fn is_python314_or_later(py: Option<Python<'_>>) -> bool {
py.is_some_and(|py| py.version_info() >= (3, 14))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// A wrapper around tokio runtime.
///
/// This is used to spawn tasks in the background and wait synchronously for them
Expand Down Expand Up @@ -195,6 +199,8 @@ impl BackgroundExecutor {
F::Output: Send,
P: FnMut() -> PyResult<()>,
{
let should_propagate_on_completion = is_python314_or_later(py);

let mut future = std::pin::pin!(future);

loop {
Expand Down Expand Up @@ -236,11 +242,22 @@ impl BackgroundExecutor {
};

if let Some(output) = maybe_output {
if let Err(err) = pump() {
log::warn!(
"Ignoring progress callback error after operation completed successfully: {}",
err
);
// When the index build finishes so fast that no pump cycles
// occurred during execution, pending events sit in the channel
// buffer and get drained after completion. Python ≥ 3.14 changed
// GIL/async scheduling timing such that callback invocations may
// only be visible in this post-completion drain, so we propagate
// errors for 3.14+. For ≤ 3.13 we keep the old tolerant behavior
// to avoid spurious failures when scheduling shifts slightly.
if should_propagate_on_completion {
pump()?;
} else {
if let Err(err) = pump() {
log::warn!(
"Ignoring progress callback error after operation completed successfully: {}",
err
);
}
}
return Ok(output);
}
Expand Down
Loading
Loading