diff --git a/.github/workflows/check_core_versions.yml b/.github/workflows/check_core_versions.yml deleted file mode 100644 index 6f88747d517..00000000000 --- a/.github/workflows/check_core_versions.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Ensures version numbers in pyproject.toml and version.py stay in sync. -# -# (Prevents releases with mismatched version numbers) - -name: "🔍 Check Version Equality" - -on: - pull_request: - paths: - - "libs/core/pyproject.toml" - - "libs/core/langchain_core/version.py" - - "libs/partners/anthropic/pyproject.toml" - - "libs/partners/anthropic/langchain_anthropic/_version.py" - -permissions: - contents: read - -jobs: - check_version_equality: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: "✅ Verify pyproject.toml & version.py Match" - run: | - # Check core versions - CORE_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/core/pyproject.toml) - CORE_VERSION_PY_VERSION=$(grep -Po '(?<=^VERSION = ")[^"]*' libs/core/langchain_core/version.py) - - # Compare core versions - if [ "$CORE_PYPROJECT_VERSION" != "$CORE_VERSION_PY_VERSION" ]; then - echo "langchain-core versions in pyproject.toml and version.py do not match!" - echo "pyproject.toml version: $CORE_PYPROJECT_VERSION" - echo "version.py version: $CORE_VERSION_PY_VERSION" - exit 1 - else - echo "Core versions match: $CORE_PYPROJECT_VERSION" - fi - - # Check langchain_v1 versions - LANGCHAIN_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/langchain_v1/pyproject.toml) - LANGCHAIN_INIT_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/langchain_v1/langchain/__init__.py) - - # Compare langchain_v1 versions - if [ "$LANGCHAIN_PYPROJECT_VERSION" != "$LANGCHAIN_INIT_PY_VERSION" ]; then - echo "langchain_v1 versions in pyproject.toml and __init__.py do not match!" - echo "pyproject.toml version: $LANGCHAIN_PYPROJECT_VERSION" - echo "version.py version: $LANGCHAIN_INIT_PY_VERSION" - exit 1 - else - echo "Langchain v1 versions match: $LANGCHAIN_PYPROJECT_VERSION" - fi - - # Check langchain-anthropic versions - ANTHROPIC_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/partners/anthropic/pyproject.toml) - ANTHROPIC_VERSION_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/partners/anthropic/langchain_anthropic/_version.py) - - # Compare langchain-anthropic versions - if [ "$ANTHROPIC_PYPROJECT_VERSION" != "$ANTHROPIC_VERSION_PY_VERSION" ]; then - echo "langchain-anthropic versions in pyproject.toml and _version.py do not match!" - echo "pyproject.toml version: $ANTHROPIC_PYPROJECT_VERSION" - echo "_version.py version: $ANTHROPIC_VERSION_PY_VERSION" - exit 1 - else - echo "Langchain-anthropic versions match: $ANTHROPIC_PYPROJECT_VERSION" - fi diff --git a/.github/workflows/check_versions.yml b/.github/workflows/check_versions.yml new file mode 100644 index 00000000000..f33785bc9aa --- /dev/null +++ b/.github/workflows/check_versions.yml @@ -0,0 +1,135 @@ +# Ensures version numbers in pyproject.toml and version.py/_version.py stay in +# sync for packages that use hardcoded version strings. +# +# Packages using `importlib.metadata.version(__package__)` are auto-synced and +# skipped by this check. + +name: "Check Version Equality" + +on: + pull_request: + paths: + - "libs/core/pyproject.toml" + - "libs/core/langchain_core/version.py" + - "libs/langchain_v1/pyproject.toml" + - "libs/langchain_v1/langchain/__init__.py" + - "libs/partners/anthropic/pyproject.toml" + - "libs/partners/anthropic/langchain_anthropic/_version.py" + - "libs/partners/openai/pyproject.toml" + - "libs/partners/openai/langchain_openai/_version.py" + - "libs/partners/groq/pyproject.toml" + - "libs/partners/groq/langchain_groq/version.py" + - "libs/partners/deepseek/pyproject.toml" + - "libs/partners/deepseek/langchain_deepseek/_version.py" + - "libs/partners/xai/pyproject.toml" + - "libs/partners/xai/langchain_xai/_version.py" + - "libs/partners/fireworks/pyproject.toml" + - "libs/partners/fireworks/langchain_fireworks/version.py" + - "libs/partners/mistralai/pyproject.toml" + - "libs/partners/mistralai/langchain_mistralai/_version.py" + - "libs/partners/ollama/pyproject.toml" + - "libs/partners/ollama/langchain_ollama/_version.py" + - "libs/partners/openrouter/pyproject.toml" + - "libs/partners/openrouter/langchain_openrouter/_version.py" + +permissions: + contents: read + +jobs: + check_version_equality: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: "Verify pyproject.toml & version files match" + run: | + # check_version PKG_LABEL PYPROJECT_PATH VERSION_FILE PATTERN + # PATTERN: "VERSION" for `VERSION = "..."`, "__version__" for `__version__ = "..."` + # Exits 0 (skip) if the version file uses importlib.metadata (dynamic). + check_version() { + local label="$1" pyproject="$2" verfile="$3" pattern="$4" + + local pyproject_ver + pyproject_ver=$(grep -Po '(?<=^version = ")[^"]*' "$pyproject") + + local file_ver + file_ver=$(grep -Po "(?<=^${pattern} = \")[^\"]*" "$verfile" || true) + + if [ -z "$file_ver" ]; then + echo "✔ $label: version is dynamic (importlib.metadata) — skipped" + return 0 + fi + + if [ "$pyproject_ver" != "$file_ver" ]; then + echo "✘ $label: version MISMATCH" + echo " pyproject.toml: $pyproject_ver" + echo " $verfile: $file_ver" + return 1 + fi + + echo "✔ $label: $pyproject_ver" + } + + FAILED=0 + + check_version "langchain-core" \ + libs/core/pyproject.toml \ + libs/core/langchain_core/version.py \ + "VERSION" || FAILED=1 + + check_version "langchain (v1)" \ + libs/langchain_v1/pyproject.toml \ + libs/langchain_v1/langchain/__init__.py \ + "__version__" || FAILED=1 + + check_version "langchain-anthropic" \ + libs/partners/anthropic/pyproject.toml \ + libs/partners/anthropic/langchain_anthropic/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-openai" \ + libs/partners/openai/pyproject.toml \ + libs/partners/openai/langchain_openai/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-groq" \ + libs/partners/groq/pyproject.toml \ + libs/partners/groq/langchain_groq/version.py \ + "__version__" || FAILED=1 + + check_version "langchain-deepseek" \ + libs/partners/deepseek/pyproject.toml \ + libs/partners/deepseek/langchain_deepseek/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-xai" \ + libs/partners/xai/pyproject.toml \ + libs/partners/xai/langchain_xai/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-fireworks" \ + libs/partners/fireworks/pyproject.toml \ + libs/partners/fireworks/langchain_fireworks/version.py \ + "__version__" || FAILED=1 + + check_version "langchain-mistralai" \ + libs/partners/mistralai/pyproject.toml \ + libs/partners/mistralai/langchain_mistralai/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-ollama" \ + libs/partners/ollama/pyproject.toml \ + libs/partners/ollama/langchain_ollama/_version.py \ + "__version__" || FAILED=1 + + check_version "langchain-openrouter" \ + libs/partners/openrouter/pyproject.toml \ + libs/partners/openrouter/langchain_openrouter/_version.py \ + "__version__" || FAILED=1 + + if [ "$FAILED" -ne 0 ]; then + echo "" + echo "One or more version checks failed!" + exit 1 + fi diff --git a/libs/partners/deepseek/Makefile b/libs/partners/deepseek/Makefile index 908f2da3628..6f89d6419ee 100644 --- a/libs/partners/deepseek/Makefile +++ b/libs/partners/deepseek/Makefile @@ -55,6 +55,9 @@ format format_diff: check_imports: $(shell find langchain_deepseek -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -62,6 +65,7 @@ check_imports: $(shell find langchain_deepseek -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/deepseek/langchain_deepseek/__init__.py b/libs/partners/deepseek/langchain_deepseek/__init__.py index 07fec728057..10ff61a5adf 100644 --- a/libs/partners/deepseek/langchain_deepseek/__init__.py +++ b/libs/partners/deepseek/langchain_deepseek/__init__.py @@ -1,16 +1,8 @@ """LangChain DeepSeek integration.""" -from importlib import metadata - +from langchain_deepseek._version import __version__ from langchain_deepseek.chat_models import ChatDeepSeek -try: - __version__ = metadata.version(__package__) -except metadata.PackageNotFoundError: - # Case where package metadata is not available. - __version__ = "" -del metadata # optional, avoids polluting the results of dir(__package__) - __all__ = [ "ChatDeepSeek", "__version__", diff --git a/libs/partners/deepseek/langchain_deepseek/_version.py b/libs/partners/deepseek/langchain_deepseek/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/deepseek/langchain_deepseek/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/deepseek/langchain_deepseek/chat_models.py b/libs/partners/deepseek/langchain_deepseek/chat_models.py index 3a4f2ce83af..543f86df743 100644 --- a/libs/partners/deepseek/langchain_deepseek/chat_models.py +++ b/libs/partners/deepseek/langchain_deepseek/chat_models.py @@ -4,7 +4,6 @@ from __future__ import annotations import json from collections.abc import Callable, Iterator, Sequence -from importlib.metadata import PackageNotFoundError, version as _get_pkg_version from json import JSONDecodeError from typing import Any, Literal, TypeAlias, cast from urllib.parse import urlparse @@ -29,11 +28,7 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from typing_extensions import Self from langchain_deepseek.data._profiles import _PROFILES - -try: - _PKG_VERSION = _get_pkg_version("langchain-deepseek") -except PackageNotFoundError: - _PKG_VERSION = "" +from langchain_deepseek._version import __version__ DEFAULT_API_BASE = "https://api.deepseek.com/v1" DEFAULT_BETA_API_BASE = "https://api.deepseek.com/beta" @@ -233,7 +228,7 @@ class ChatDeepSeek(BaseChatOpenAI): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate necessary environment vars and client params.""" - self._add_version("langchain-deepseek", _PKG_VERSION) + self._add_version("langchain-deepseek", __version__) if self.api_base == DEFAULT_API_BASE and not ( self.api_key and self.api_key.get_secret_value() ): diff --git a/libs/partners/deepseek/scripts/check_version.py b/libs/partners/deepseek/scripts/check_version.py new file mode 100644 index 00000000000..5aed6444211 --- /dev/null +++ b/libs/partners/deepseek/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_deepseek/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_deepseek" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_deepseek/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/deepseek/tests/unit_tests/test_imports.py b/libs/partners/deepseek/tests/unit_tests/test_imports.py new file mode 100644 index 00000000000..7e5902acab1 --- /dev/null +++ b/libs/partners/deepseek/tests/unit_tests/test_imports.py @@ -0,0 +1,7 @@ +from langchain_deepseek import __all__ + +EXPECTED_ALL = ["__version__", "ChatDeepSeek"] + + +def test_all_imports() -> None: + assert sorted(EXPECTED_ALL) == sorted(__all__) diff --git a/libs/partners/fireworks/Makefile b/libs/partners/fireworks/Makefile index 16d84d2af79..6b9b752fe1e 100644 --- a/libs/partners/fireworks/Makefile +++ b/libs/partners/fireworks/Makefile @@ -53,6 +53,9 @@ format format_diff: check_imports: $(shell find langchain_fireworks -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -60,6 +63,7 @@ check_imports: $(shell find langchain_fireworks -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index faf65684e0d..cd452bfaeb3 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -6,8 +6,6 @@ import contextlib import json import logging from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from operator import itemgetter from typing import ( Any, @@ -86,11 +84,7 @@ from typing_extensions import Self from langchain_fireworks._compat import _convert_from_v1_to_chat_completions from langchain_fireworks.data._profiles import _PROFILES - -try: - _PKG_VERSION = _get_pkg_version("langchain-fireworks") -except PackageNotFoundError: - _PKG_VERSION = "" +from langchain_fireworks.version import __version__ logger = logging.getLogger(__name__) @@ -405,7 +399,7 @@ class ChatFireworks(BaseChatModel): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" - self._add_version("langchain-fireworks", _PKG_VERSION) + self._add_version("langchain-fireworks", __version__) if self.n < 1: msg = "n must be at least 1." raise ValueError(msg) diff --git a/libs/partners/fireworks/scripts/check_version.py b/libs/partners/fireworks/scripts/check_version.py new file mode 100644 index 00000000000..d82cf207488 --- /dev/null +++ b/libs/partners/fireworks/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_fireworks/version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_fireworks" / "version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_fireworks/version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/groq/Makefile b/libs/partners/groq/Makefile index b2ee3e49cae..f30c2954a8f 100644 --- a/libs/partners/groq/Makefile +++ b/libs/partners/groq/Makefile @@ -54,6 +54,9 @@ format format_diff: check_imports: $(shell find langchain_groq -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -61,6 +64,7 @@ check_imports: $(shell find langchain_groq -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/groq/scripts/check_version.py b/libs/partners/groq/scripts/check_version.py new file mode 100644 index 00000000000..25281ca8e0c --- /dev/null +++ b/libs/partners/groq/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_groq/version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_groq" / "version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_groq/version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/mistralai/Makefile b/libs/partners/mistralai/Makefile index 1dc8976ec79..6b41a5e3858 100644 --- a/libs/partners/mistralai/Makefile +++ b/libs/partners/mistralai/Makefile @@ -56,6 +56,9 @@ format format_diff: check_imports: $(shell find langchain_mistralai -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -63,6 +66,7 @@ check_imports: $(shell find langchain_mistralai -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/mistralai/langchain_mistralai/__init__.py b/libs/partners/mistralai/langchain_mistralai/__init__.py index 11fa44c371a..5ae2cc1ffb5 100644 --- a/libs/partners/mistralai/langchain_mistralai/__init__.py +++ b/libs/partners/mistralai/langchain_mistralai/__init__.py @@ -1,6 +1,7 @@ """Mistral AI integration for LangChain.""" +from langchain_mistralai._version import __version__ from langchain_mistralai.chat_models import ChatMistralAI from langchain_mistralai.embeddings import MistralAIEmbeddings -__all__ = ["ChatMistralAI", "MistralAIEmbeddings"] +__all__ = ["ChatMistralAI", "MistralAIEmbeddings", "__version__"] diff --git a/libs/partners/mistralai/langchain_mistralai/_version.py b/libs/partners/mistralai/langchain_mistralai/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/mistralai/langchain_mistralai/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/mistralai/langchain_mistralai/chat_models.py b/libs/partners/mistralai/langchain_mistralai/chat_models.py index 451cece0872..2c3ed9347a2 100644 --- a/libs/partners/mistralai/langchain_mistralai/chat_models.py +++ b/libs/partners/mistralai/langchain_mistralai/chat_models.py @@ -8,8 +8,6 @@ import re import ssl import uuid from collections.abc import Callable, Sequence # noqa: TC003 -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from operator import itemgetter from typing import ( TYPE_CHECKING, @@ -76,13 +74,9 @@ from pydantic import ( from typing_extensions import Self from langchain_mistralai._compat import _convert_from_v1_to_mistral +from langchain_mistralai._version import __version__ from langchain_mistralai.data._profiles import _PROFILES -try: - _PKG_VERSION = _get_pkg_version("langchain-mistralai") -except PackageNotFoundError: - _PKG_VERSION = "" - if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator from contextlib import AbstractAsyncContextManager @@ -607,7 +601,7 @@ class ChatMistralAI(BaseChatModel): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate api key, python package exists, temperature, and top_p.""" - self._add_version("langchain-mistralai", _PKG_VERSION) + self._add_version("langchain-mistralai", __version__) if isinstance(self.mistral_api_key, SecretStr): api_key_str: str | None = self.mistral_api_key.get_secret_value() else: diff --git a/libs/partners/mistralai/scripts/check_version.py b/libs/partners/mistralai/scripts/check_version.py new file mode 100644 index 00000000000..8c3ab94471b --- /dev/null +++ b/libs/partners/mistralai/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_mistralai/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_mistralai" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_mistralai/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/mistralai/tests/unit_tests/test_imports.py b/libs/partners/mistralai/tests/unit_tests/test_imports.py index 01c220d64c9..63f4ec7f058 100644 --- a/libs/partners/mistralai/tests/unit_tests/test_imports.py +++ b/libs/partners/mistralai/tests/unit_tests/test_imports.py @@ -1,6 +1,6 @@ from langchain_mistralai import __all__ -EXPECTED_ALL = ["ChatMistralAI", "MistralAIEmbeddings"] +EXPECTED_ALL = ["__version__", "ChatMistralAI", "MistralAIEmbeddings"] def test_all_imports() -> None: diff --git a/libs/partners/ollama/Makefile b/libs/partners/ollama/Makefile index f982c01235d..56524f1ea1f 100644 --- a/libs/partners/ollama/Makefile +++ b/libs/partners/ollama/Makefile @@ -63,6 +63,9 @@ format format_diff: check_imports: $(shell find langchain_ollama -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -70,6 +73,7 @@ check_imports: $(shell find langchain_ollama -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/ollama/langchain_ollama/_version.py b/libs/partners/ollama/langchain_ollama/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/ollama/langchain_ollama/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/ollama/langchain_ollama/chat_models.py b/libs/partners/ollama/langchain_ollama/chat_models.py index 146ea78ecfb..8ab0adabd96 100644 --- a/libs/partners/ollama/langchain_ollama/chat_models.py +++ b/libs/partners/ollama/langchain_ollama/chat_models.py @@ -45,8 +45,6 @@ import ast import json import logging from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from operator import itemgetter from typing import Any, Literal, cast from uuid import uuid4 @@ -96,11 +94,7 @@ from langchain_ollama._utils import ( parse_url_with_auth, validate_model, ) - -try: - _PKG_VERSION = _get_pkg_version("langchain-ollama") -except PackageNotFoundError: - _PKG_VERSION = "" +from langchain_ollama._version import __version__ log = logging.getLogger(__name__) @@ -797,7 +791,7 @@ class ChatOllama(BaseChatModel): @model_validator(mode="after") def _set_clients(self) -> Self: """Set clients to use for ollama.""" - self._add_version("langchain-ollama", _PKG_VERSION) + self._add_version("langchain-ollama", __version__) client_kwargs = self.client_kwargs or {} cleaned_url, auth_headers = parse_url_with_auth(self.base_url) diff --git a/libs/partners/ollama/langchain_ollama/llms.py b/libs/partners/ollama/langchain_ollama/llms.py index e7cd2ed279b..c7d56433c50 100644 --- a/libs/partners/ollama/langchain_ollama/llms.py +++ b/libs/partners/ollama/langchain_ollama/llms.py @@ -3,8 +3,6 @@ from __future__ import annotations from collections.abc import AsyncIterator, Iterator, Mapping -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from typing import Any, Literal from langchain_core.callbacks import ( @@ -22,11 +20,7 @@ from langchain_ollama._utils import ( parse_url_with_auth, validate_model, ) - -try: - _PKG_VERSION = _get_pkg_version("langchain-ollama") -except PackageNotFoundError: - _PKG_VERSION = "" +from langchain_ollama._version import __version__ class OllamaLLM(BaseLLM): @@ -329,7 +323,7 @@ class OllamaLLM(BaseLLM): @model_validator(mode="after") def _set_clients(self) -> Self: """Set clients to use for ollama.""" - self._add_version("langchain-ollama", _PKG_VERSION) + self._add_version("langchain-ollama", __version__) client_kwargs = self.client_kwargs or {} cleaned_url, auth_headers = parse_url_with_auth(self.base_url) diff --git a/libs/partners/ollama/scripts/check_version.py b/libs/partners/ollama/scripts/check_version.py new file mode 100644 index 00000000000..b9e3dfce033 --- /dev/null +++ b/libs/partners/ollama/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_ollama/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_ollama" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_ollama/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/openai/Makefile b/libs/partners/openai/Makefile index 8f8df6b5dd8..b8c1e49bcfc 100644 --- a/libs/partners/openai/Makefile +++ b/libs/partners/openai/Makefile @@ -67,6 +67,9 @@ format format_diff: check_imports: $(shell find langchain_openai -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -74,6 +77,7 @@ check_imports: $(shell find langchain_openai -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/openai/langchain_openai/__init__.py b/libs/partners/openai/langchain_openai/__init__.py index da0c6c2b25d..480826acbf6 100644 --- a/libs/partners/openai/langchain_openai/__init__.py +++ b/libs/partners/openai/langchain_openai/__init__.py @@ -1,5 +1,6 @@ """Module for OpenAI integrations.""" +from langchain_openai._version import __version__ from langchain_openai.chat_models import AzureChatOpenAI, ChatOpenAI from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings from langchain_openai.llms import AzureOpenAI, OpenAI @@ -12,5 +13,6 @@ __all__ = [ "ChatOpenAI", "OpenAI", "OpenAIEmbeddings", + "__version__", "custom_tool", ] diff --git a/libs/partners/openai/langchain_openai/_version.py b/libs/partners/openai/langchain_openai/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/openai/langchain_openai/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/openai/langchain_openai/chat_models/azure.py b/libs/partners/openai/langchain_openai/chat_models/azure.py index eb14b2fc4fb..fea2f384868 100644 --- a/libs/partners/openai/langchain_openai/chat_models/azure.py +++ b/libs/partners/openai/langchain_openai/chat_models/azure.py @@ -5,8 +5,6 @@ from __future__ import annotations import logging import os from collections.abc import AsyncIterator, Awaitable, Callable, Iterator -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from typing import Any, Literal, TypeAlias, TypeVar import openai @@ -23,11 +21,6 @@ from langchain_openai.chat_models.base import BaseChatOpenAI, _get_default_model logger = logging.getLogger(__name__) -try: - _PKG_VERSION = _get_pkg_version("langchain-openai") -except PackageNotFoundError: - _PKG_VERSION = "" - _BM = TypeVar("_BM", bound=BaseModel) _DictOrPydanticClass: TypeAlias = dict[str, Any] | type[_BM] | type @@ -602,7 +595,6 @@ class AzureChatOpenAI(BaseChatOpenAI): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" - self._add_version("langchain-openai", _PKG_VERSION) if self.n is not None and self.n < 1: msg = "n must be at least 1." raise ValueError(msg) diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 37e5e67907d..3b6c9fd6398 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -30,8 +30,6 @@ from collections.abc import ( Sequence, ) from functools import partial -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from io import BytesIO from json import JSONDecodeError from math import ceil @@ -53,12 +51,6 @@ from langchain_core.callbacks import ( AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun, ) - -try: - _PKG_VERSION = _get_pkg_version("langchain-openai") -except PackageNotFoundError: - _PKG_VERSION = "" - from langchain_core.exceptions import ContextOverflowError from langchain_core.language_models import ( LanguageModelInput, @@ -136,6 +128,7 @@ from pydantic import ( from pydantic.v1 import BaseModel as BaseModelV1 from typing_extensions import Self +from langchain_openai._version import __version__ from langchain_openai.chat_models._client_utils import ( _get_default_async_httpx_client, _get_default_httpx_client, @@ -980,7 +973,7 @@ class BaseChatOpenAI(BaseChatModel): def model_post_init(self, _context: Any, /) -> None: """Add langchain-openai version to metadata.""" super().model_post_init(_context) - self._add_version("langchain-openai", _PKG_VERSION) + self._add_version("langchain-openai", __version__) @model_validator(mode="after") def validate_environment(self) -> Self: diff --git a/libs/partners/openai/langchain_openai/llms/azure.py b/libs/partners/openai/langchain_openai/llms/azure.py index e970a4bdb37..3b65227d244 100644 --- a/libs/partners/openai/langchain_openai/llms/azure.py +++ b/libs/partners/openai/langchain_openai/llms/azure.py @@ -4,8 +4,6 @@ from __future__ import annotations import logging from collections.abc import Awaitable, Callable, Mapping -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as _get_pkg_version from typing import Any, cast import openai @@ -14,13 +12,9 @@ from langchain_core.utils import from_env, secret_from_env from pydantic import Field, SecretStr, model_validator from typing_extensions import Self +from langchain_openai._version import __version__ from langchain_openai.llms.base import BaseOpenAI -try: - _PKG_VERSION = _get_pkg_version("langchain-openai") -except PackageNotFoundError: - _PKG_VERSION = "" - logger = logging.getLogger(__name__) @@ -127,7 +121,7 @@ class AzureOpenAI(BaseOpenAI): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" - self._add_version("langchain-openai", _PKG_VERSION) + self._add_version("langchain-openai", __version__) if self.n < 1: msg = "n must be at least 1." raise ValueError(msg) diff --git a/libs/partners/openai/scripts/check_version.py b/libs/partners/openai/scripts/check_version.py new file mode 100644 index 00000000000..d47419290c7 --- /dev/null +++ b/libs/partners/openai/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_openai/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_openai" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_openai/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/openai/tests/unit_tests/test_imports.py b/libs/partners/openai/tests/unit_tests/test_imports.py index 144a394c2ff..ebc0ae30b56 100644 --- a/libs/partners/openai/tests/unit_tests/test_imports.py +++ b/libs/partners/openai/tests/unit_tests/test_imports.py @@ -1,6 +1,7 @@ from langchain_openai import __all__ EXPECTED_ALL = [ + "__version__", "OpenAI", "ChatOpenAI", "OpenAIEmbeddings", diff --git a/libs/partners/openrouter/Makefile b/libs/partners/openrouter/Makefile index b8f8f350a08..290c2894194 100644 --- a/libs/partners/openrouter/Makefile +++ b/libs/partners/openrouter/Makefile @@ -55,6 +55,9 @@ format format_diff: check_imports: $(shell find langchain_openrouter -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -62,6 +65,7 @@ check_imports: $(shell find langchain_openrouter -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/openrouter/langchain_openrouter/__init__.py b/libs/partners/openrouter/langchain_openrouter/__init__.py index ca2c47a43d7..a76779bf097 100644 --- a/libs/partners/openrouter/langchain_openrouter/__init__.py +++ b/libs/partners/openrouter/langchain_openrouter/__init__.py @@ -1,7 +1,9 @@ """LangChain OpenRouter integration.""" +from langchain_openrouter._version import __version__ from langchain_openrouter.chat_models import ChatOpenRouter __all__ = [ + "__version__", "ChatOpenRouter", ] diff --git a/libs/partners/openrouter/langchain_openrouter/_version.py b/libs/partners/openrouter/langchain_openrouter/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/openrouter/langchain_openrouter/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/openrouter/langchain_openrouter/chat_models.py b/libs/partners/openrouter/langchain_openrouter/chat_models.py index f39246fe60d..d38ce27a43c 100644 --- a/libs/partners/openrouter/langchain_openrouter/chat_models.py +++ b/libs/partners/openrouter/langchain_openrouter/chat_models.py @@ -5,7 +5,6 @@ from __future__ import annotations import json import warnings from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence -from importlib.metadata import PackageNotFoundError, version as _get_pkg_version from operator import itemgetter from typing import Any, Literal, cast @@ -70,13 +69,9 @@ from langchain_core.utils.pydantic import is_basemodel_subclass from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from typing_extensions import Self +from langchain_openrouter._version import __version__ from langchain_openrouter.data._profiles import _PROFILES -try: - _PKG_VERSION = _get_pkg_version("langchain-openrouter") -except PackageNotFoundError: - _PKG_VERSION = "" - _MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES) # LangChain-internal kwargs that must not be forwarded to the SDK. @@ -310,7 +305,7 @@ class ChatOpenRouter(BaseChatModel): @model_validator(mode="after") def validate_environment(self) -> Self: """Validate configuration and build the SDK client.""" - self._add_version("langchain-openrouter", _PKG_VERSION) + self._add_version("langchain-openrouter", __version__) if not (self.openrouter_api_key and self.openrouter_api_key.get_secret_value()): msg = "OPENROUTER_API_KEY must be set." raise ValueError(msg) diff --git a/libs/partners/openrouter/scripts/check_version.py b/libs/partners/openrouter/scripts/check_version.py new file mode 100644 index 00000000000..cea781e4adf --- /dev/null +++ b/libs/partners/openrouter/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_openrouter/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_openrouter" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_openrouter/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/openrouter/tests/unit_tests/test_imports.py b/libs/partners/openrouter/tests/unit_tests/test_imports.py index d46ab65340d..554989ddba4 100644 --- a/libs/partners/openrouter/tests/unit_tests/test_imports.py +++ b/libs/partners/openrouter/tests/unit_tests/test_imports.py @@ -3,6 +3,7 @@ from langchain_openrouter import __all__ EXPECTED_ALL = [ + "__version__", "ChatOpenRouter", ] diff --git a/libs/partners/xai/Makefile b/libs/partners/xai/Makefile index 93729819edc..c1f6a283f41 100644 --- a/libs/partners/xai/Makefile +++ b/libs/partners/xai/Makefile @@ -53,6 +53,9 @@ format format_diff: check_imports: $(shell find langchain_xai -name '*.py') $(UV_RUN_LINT) python ./scripts/check_imports.py $^ +check_version: + uv run python ./scripts/check_version.py + ###################### # HELP ###################### @@ -60,6 +63,7 @@ check_imports: $(shell find langchain_xai -name '*.py') help: @echo '----' @echo 'check_imports - check imports' + @echo 'check_version - validate version consistency' @echo 'format - run code formatters' @echo 'lint - run linters' @echo 'type - run type checking' diff --git a/libs/partners/xai/langchain_xai/__init__.py b/libs/partners/xai/langchain_xai/__init__.py index 3ba202d04d8..2b328b75d77 100644 --- a/libs/partners/xai/langchain_xai/__init__.py +++ b/libs/partners/xai/langchain_xai/__init__.py @@ -1,5 +1,6 @@ """LangChain integration with xAI.""" +from langchain_xai._version import __version__ from langchain_xai.chat_models import ChatXAI -__all__ = ["ChatXAI"] +__all__ = ["__version__", "ChatXAI"] diff --git a/libs/partners/xai/langchain_xai/_version.py b/libs/partners/xai/langchain_xai/_version.py new file mode 100644 index 00000000000..8d54cafc7e3 --- /dev/null +++ b/libs/partners/xai/langchain_xai/_version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" + +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" diff --git a/libs/partners/xai/langchain_xai/chat_models.py b/libs/partners/xai/langchain_xai/chat_models.py index 811520b8fb5..444362f46f0 100644 --- a/libs/partners/xai/langchain_xai/chat_models.py +++ b/libs/partners/xai/langchain_xai/chat_models.py @@ -3,7 +3,6 @@ from __future__ import annotations import warnings -from importlib.metadata import PackageNotFoundError, version as _get_pkg_version from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast import openai @@ -14,11 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from typing_extensions import Self from langchain_xai.data._profiles import _PROFILES - -try: - _PKG_VERSION = _get_pkg_version("langchain-xai") -except PackageNotFoundError: - _PKG_VERSION = "" +from langchain_xai._version import __version__ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator @@ -477,7 +472,7 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override] @model_validator(mode="after") def _warn_search_parameters_deprecated(self) -> Self: """Emit deprecation warning if search_parameters (Live Search) is used.""" - self._add_version("langchain-xai", _PKG_VERSION) + self._add_version("langchain-xai", __version__) if self.search_parameters: warnings.warn( "search_parameters (Live Search) is deprecated by xAI and is ignored. " diff --git a/libs/partners/xai/scripts/check_version.py b/libs/partners/xai/scripts/check_version.py new file mode 100644 index 00000000000..348fe998417 --- /dev/null +++ b/libs/partners/xai/scripts/check_version.py @@ -0,0 +1,71 @@ +"""Check version consistency between `pyproject.toml` and `_version.py`. + +This script validates that the version defined in pyproject.toml matches the +`__version__` variable in `langchain_xai/_version.py`. Intended for use as a +pre-commit hook to prevent version mismatches. +""" + +import re +import sys +from pathlib import Path + + +def get_pyproject_version(pyproject_path: Path) -> str | None: + """Extract version from `pyproject.toml`.""" + content = pyproject_path.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def get_version_py_version(version_path: Path) -> str | None: + """Extract `__version__` from `_version.py`. + + Returns ``None`` if the version is set dynamically (e.g. via + ``importlib.metadata``), indicating the check should be skipped. + """ + content = version_path.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE) + return match.group(1) if match else None + + +def main() -> int: + """Validate version consistency.""" + script_dir = Path(__file__).parent + package_dir = script_dir.parent + + pyproject_path = package_dir / "pyproject.toml" + version_path = package_dir / "langchain_xai" / "_version.py" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found") # noqa: T201 + return 1 + + if not version_path.exists(): + print(f"Error: {version_path} not found") # noqa: T201 + return 1 + + pyproject_version = get_pyproject_version(pyproject_path) + version_py_version = get_version_py_version(version_path) + + if pyproject_version is None: + print("Error: Could not find version in pyproject.toml") # noqa: T201 + return 1 + + if version_py_version is None: + print( # noqa: T201 + "Version is dynamic (importlib.metadata) — skipping check" + ) + return 0 + + if pyproject_version != version_py_version: + print("Error: Version mismatch detected!") # noqa: T201 + print(f" pyproject.toml: {pyproject_version}") # noqa: T201 + print(f" langchain_xai/_version.py: {version_py_version}") # noqa: T201 + return 1 + + print(f"Version check passed: {pyproject_version}") # noqa: T201 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/partners/xai/tests/unit_tests/test_imports.py b/libs/partners/xai/tests/unit_tests/test_imports.py index e1a75c65920..1d739d4c846 100644 --- a/libs/partners/xai/tests/unit_tests/test_imports.py +++ b/libs/partners/xai/tests/unit_tests/test_imports.py @@ -1,6 +1,6 @@ from langchain_xai import __all__ -EXPECTED_ALL = ["ChatXAI"] +EXPECTED_ALL = ["__version__", "ChatXAI"] def test_all_imports() -> None: