fix(python): normalize __version__ to PEP 440 (#4752)

Fixes #4749

Python's `__version__` forwards Cargo's SemVer spelling, so prereleases
report values such as `0.41.0-beta.17` while the installed package
metadata reports `0.41.0b17`.

Normalize the native version with the existing
`packaging.version.Version` dependency before exposing it in Python.
Keep native version sourcing so imports still work with `lancedb-compat`
and do not require distribution metadata. Regression tests cover
metadata equality and stable, alpha, beta, release-candidate, and
development versions with metadata lookups disabled.

Validation:

- Rebuilt the native extension through the uv-managed development
environment.
- Confirmed the metadata regression fails on the original code with the
reported string mismatch.
- Import tests, package metadata tests, the remote client user-agent
test, and CI Python dependency tests: 14 passed, 2 skipped (Windows-only
wheel check and uninstalled `lancedb-compat`).
- macOS ARM64/Python 3.13 dependency resolution and `uv lock --check`
pass with the platform-specific test dependency pins from current
`main`.
- Python-tree Ruff lint and formatting checks, focused Pyright check,
Rust formatting, and PR diff whitespace checks pass.
- Repository-wide Ruff lint reports existing findings in unchanged
`docs/web-tests` files.

<!-- lance-gatekeeper-fix:v1 agent=3e23555743cc116ea53ca8a7e3c4ebb1
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
This commit is contained in:
lancedb-gatefixer[bot]
2026-10-31 20:41:22 -07:00
committed by GitHub
parent 71b23ed9fb
commit e5d4e9bf6e
3 changed files with 41 additions and 4 deletions
+7 -1
View File
@@ -7,7 +7,13 @@ from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from typing import Dict, Optional, Union, Any, List, Iterable, TYPE_CHECKING
from ._lancedb import __version__ as __version__
from packaging.version import Version as _Version
from ._lancedb import __version__ as _native_version
# Match Python package metadata without requiring the lancedb distribution.
__version__ = str(_Version(_native_version))
from ._lancedb import connect as lancedb_connect
from ._lancedb import FtsToken
from ._lancedb import FunctionErrorFragment as FunctionErrorFragment
+21 -3
View File
@@ -34,9 +34,23 @@ def test_native_extension_does_not_link_openssl():
)
def test_top_level_version_comes_from_native_module():
@pytest.mark.parametrize(
"native_version, expected_version",
[
("0.41.0", "0.41.0"),
("0.41.0-alpha.1", "0.41.0a1"),
("0.41.0-beta.17", "0.41.0b17"),
("0.41.0-rc.1", "0.41.0rc1"),
("0.41.0-dev.1", "0.41.0.dev1"),
],
)
def test_top_level_version_comes_from_native_module(native_version, expected_version):
child_code = """
import importlib
import importlib.metadata
import sys
from packaging.version import Version
orig_version = importlib.metadata.version
@@ -51,13 +65,17 @@ import lancedb
import lancedb._lancedb as native_module
import lancedb.db
assert lancedb.__version__ == native_module.__version__
assert lancedb.__version__ == str(Version(native_module.__version__))
native_module.__version__ = sys.argv[1]
importlib.reload(lancedb)
assert lancedb.__version__ == sys.argv[2]
"""
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(path for path in sys.path if path)
result = subprocess.run(
[sys.executable, "-c", child_code],
[sys.executable, "-c", child_code, native_version, expected_version],
check=False,
capture_output=True,
text=True,
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import importlib
import importlib.metadata
import re
import sys
from pathlib import Path
@@ -9,6 +10,18 @@ from pathlib import Path
import pytest
@pytest.mark.parametrize("distribution", ["lancedb", "lancedb-compat"])
def test_version_matches_package_metadata(distribution):
import lancedb
try:
package_version = importlib.metadata.version(distribution)
except importlib.metadata.PackageNotFoundError:
pytest.skip(f"{distribution} is not installed")
assert lancedb.__version__ == package_version
@pytest.mark.parametrize("name", ["JobInfo", "JobDescription", "JobFailureInfo"])
def test_job_metadata_types_have_resolvable_modules(name):
"""Documentation tools resolve re-exports through each type's module."""