mirror of
https://github.com/hwchase17/langchain
synced 2024-11-10 01:10:59 +00:00
infra: add min version testing to pr test flow (#24358)
xfailing some sql tests that do not currently work on sqlalchemy v1 #22207 was very much not sqlalchemy v1 compatible. Moving forward, implementations should be compatible with both to pass CI
This commit is contained in:
parent
50cb0a03bc
commit
f4ee3c8a22
7
.github/scripts/get_min_versions.py
vendored
7
.github/scripts/get_min_versions.py
vendored
@ -1,6 +1,11 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import tomllib
|
if sys.version_info >= (3, 11):
|
||||||
|
import tomllib
|
||||||
|
else:
|
||||||
|
# for python 3.10 and below, which doesnt have stdlib tomllib
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
18
.github/workflows/_test.yml
vendored
18
.github/workflows/_test.yml
vendored
@ -65,3 +65,21 @@ jobs:
|
|||||||
# grep will exit non-zero if the target message isn't found,
|
# grep will exit non-zero if the target message isn't found,
|
||||||
# and `set -e` above will cause the step to fail.
|
# and `set -e` above will cause the step to fail.
|
||||||
echo "$STATUS" | grep 'nothing to commit, working tree clean'
|
echo "$STATUS" | grep 'nothing to commit, working tree clean'
|
||||||
|
|
||||||
|
- name: Get minimum versions
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
id: min-version
|
||||||
|
run: |
|
||||||
|
poetry run pip install packaging tomli
|
||||||
|
min_versions="$(poetry run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml)"
|
||||||
|
echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "min-versions=$min_versions"
|
||||||
|
|
||||||
|
- name: Run unit tests with minimum dependency versions
|
||||||
|
if: ${{ steps.min-version.outputs.min-versions != '' }}
|
||||||
|
env:
|
||||||
|
MIN_VERSIONS: ${{ steps.min-version.outputs.min-versions }}
|
||||||
|
run: |
|
||||||
|
poetry run pip install --force-reinstall $MIN_VERSIONS --editable .
|
||||||
|
make tests
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
@ -32,7 +32,6 @@ from sqlalchemy.engine import Engine
|
|||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
AsyncEngine,
|
AsyncEngine,
|
||||||
AsyncSession,
|
AsyncSession,
|
||||||
async_sessionmaker,
|
|
||||||
create_async_engine,
|
create_async_engine,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
@ -44,6 +43,12 @@ from sqlalchemy.orm import (
|
|||||||
sessionmaker,
|
sessionmaker,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
except ImportError:
|
||||||
|
# dummy for sqlalchemy < 2
|
||||||
|
async_sessionmaker = type("async_sessionmaker", (type,), {}) # type: ignore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,48 +17,74 @@ from typing import (
|
|||||||
|
|
||||||
from langchain_core.stores import BaseStore
|
from langchain_core.stores import BaseStore
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Engine,
|
|
||||||
LargeBinary,
|
LargeBinary,
|
||||||
|
Text,
|
||||||
and_,
|
and_,
|
||||||
create_engine,
|
create_engine,
|
||||||
delete,
|
delete,
|
||||||
select,
|
select,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.engine.base import Engine
|
||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
AsyncEngine,
|
AsyncEngine,
|
||||||
AsyncSession,
|
AsyncSession,
|
||||||
async_sessionmaker,
|
|
||||||
create_async_engine,
|
create_async_engine,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
Session,
|
Session,
|
||||||
declarative_base,
|
declarative_base,
|
||||||
mapped_column,
|
|
||||||
sessionmaker,
|
sessionmaker,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
except ImportError:
|
||||||
|
# dummy for sqlalchemy < 2
|
||||||
|
async_sessionmaker = type("async_sessionmaker", (type,), {}) # type: ignore
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sqlalchemy.orm import mapped_column
|
||||||
|
|
||||||
|
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc]
|
||||||
|
"""Table used to save values."""
|
||||||
|
|
||||||
|
# ATTENTION:
|
||||||
|
# Prior to modifying this table, please determine whether
|
||||||
|
# we should create migrations for this table to make sure
|
||||||
|
# users do not experience data loss.
|
||||||
|
__tablename__ = "langchain_key_value_stores"
|
||||||
|
|
||||||
|
namespace: Mapped[str] = mapped_column(
|
||||||
|
primary_key=True, index=True, nullable=False
|
||||||
|
)
|
||||||
|
key: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
|
||||||
|
value = mapped_column(LargeBinary, index=False, nullable=False)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# dummy for sqlalchemy < 2
|
||||||
|
from sqlalchemy import Column
|
||||||
|
|
||||||
|
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc,no-redef]
|
||||||
|
"""Table used to save values."""
|
||||||
|
|
||||||
|
# ATTENTION:
|
||||||
|
# Prior to modifying this table, please determine whether
|
||||||
|
# we should create migrations for this table to make sure
|
||||||
|
# users do not experience data loss.
|
||||||
|
__tablename__ = "langchain_key_value_stores"
|
||||||
|
|
||||||
|
namespace = Column(Text(), primary_key=True, index=True, nullable=False)
|
||||||
|
key = Column(Text(), primary_key=True, index=True, nullable=False)
|
||||||
|
value = Column(LargeBinary, index=False, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
def items_equal(x: Any, y: Any) -> bool:
|
def items_equal(x: Any, y: Any) -> bool:
|
||||||
return x == y
|
return x == y
|
||||||
|
|
||||||
|
|
||||||
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc]
|
|
||||||
"""Table used to save values."""
|
|
||||||
|
|
||||||
# ATTENTION:
|
|
||||||
# Prior to modifying this table, please determine whether
|
|
||||||
# we should create migrations for this table to make sure
|
|
||||||
# users do not experience data loss.
|
|
||||||
__tablename__ = "langchain_key_value_stores"
|
|
||||||
|
|
||||||
namespace: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
|
|
||||||
key: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
|
|
||||||
value = mapped_column(LargeBinary, index=False, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
# This is a fix of original SQLStore.
|
# This is a fix of original SQLStore.
|
||||||
# This can will be removed when a PR will be merged.
|
# This can will be removed when a PR will be merged.
|
||||||
class SQLStore(BaseStore[str, bytes]):
|
class SQLStore(BaseStore[str, bytes]):
|
||||||
@ -135,7 +161,10 @@ class SQLStore(BaseStore[str, bytes]):
|
|||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
|
|
||||||
def create_schema(self) -> None:
|
def create_schema(self) -> None:
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine) # problem in sqlalchemy v1
|
||||||
|
# sqlalchemy.exc.CompileError: (in table 'langchain_key_value_stores',
|
||||||
|
# column 'namespace'): Can't generate DDL for NullType(); did you forget
|
||||||
|
# to specify a type on this Column?
|
||||||
|
|
||||||
async def acreate_schema(self) -> None:
|
async def acreate_schema(self) -> None:
|
||||||
assert isinstance(self.engine, AsyncEngine)
|
assert isinstance(self.engine, AsyncEngine)
|
||||||
|
@ -338,7 +338,7 @@ class SQLDatabase:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Ignore JSON datatyped columns
|
# Ignore JSON datatyped columns
|
||||||
for k, v in table.columns.items():
|
for k, v in table.columns.items(): # AttributeError: items in sqlalchemy v1
|
||||||
if type(v.type) is NullType:
|
if type(v.type) is NullType:
|
||||||
table._columns.remove(v)
|
table._columns.remove(v)
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
from typing import AsyncGenerator, Generator, cast
|
from typing import AsyncGenerator, Generator, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import sqlalchemy as sa
|
||||||
from langchain.storage._lc_store import create_kv_docstore, create_lc_store
|
from langchain.storage._lc_store import create_kv_docstore, create_lc_store
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
from langchain_core.stores import BaseStore
|
from langchain_core.stores import BaseStore
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
from langchain_community.storage.sql import SQLStore
|
from langchain_community.storage.sql import SQLStore
|
||||||
|
|
||||||
|
is_sqlalchemy_v1 = version.parse(sa.__version__).major == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sql_store() -> Generator[SQLStore, None, None]:
|
def sql_store() -> Generator[SQLStore, None, None]:
|
||||||
@ -22,6 +26,7 @@ async def async_sql_store() -> AsyncGenerator[SQLStore, None]:
|
|||||||
yield store
|
yield store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_create_lc_store(sql_store: SQLStore) -> None:
|
def test_create_lc_store(sql_store: SQLStore) -> None:
|
||||||
"""Test that a docstore is created from a base store."""
|
"""Test that a docstore is created from a base store."""
|
||||||
docstore: BaseStore[str, Document] = cast(
|
docstore: BaseStore[str, Document] = cast(
|
||||||
@ -34,6 +39,7 @@ def test_create_lc_store(sql_store: SQLStore) -> None:
|
|||||||
assert fetched_doc.metadata == {"key": "value"}
|
assert fetched_doc.metadata == {"key": "value"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_create_kv_store(sql_store: SQLStore) -> None:
|
def test_create_kv_store(sql_store: SQLStore) -> None:
|
||||||
"""Test that a docstore is created from a base store."""
|
"""Test that a docstore is created from a base store."""
|
||||||
docstore = create_kv_docstore(sql_store)
|
docstore = create_kv_docstore(sql_store)
|
||||||
@ -57,6 +63,7 @@ async def test_async_create_kv_store(async_sql_store: SQLStore) -> None:
|
|||||||
assert fetched_doc.metadata == {"key": "value"}
|
assert fetched_doc.metadata == {"key": "value"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_sample_sql_docstore(sql_store: SQLStore) -> None:
|
def test_sample_sql_docstore(sql_store: SQLStore) -> None:
|
||||||
# Set values for keys
|
# Set values for keys
|
||||||
sql_store.mset([("key1", b"value1"), ("key2", b"value2")])
|
sql_store.mset([("key1", b"value1"), ("key2", b"value2")])
|
||||||
|
@ -55,6 +55,7 @@ def db_lazy_reflection(engine: Engine) -> SQLDatabase:
|
|||||||
return SQLDatabase(engine, lazy_table_reflection=True)
|
return SQLDatabase(engine, lazy_table_reflection=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_table_info(db: SQLDatabase) -> None:
|
def test_table_info(db: SQLDatabase) -> None:
|
||||||
"""Test that table info is constructed properly."""
|
"""Test that table info is constructed properly."""
|
||||||
output = db.table_info
|
output = db.table_info
|
||||||
@ -85,6 +86,7 @@ def test_table_info(db: SQLDatabase) -> None:
|
|||||||
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
|
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_table_info_lazy_reflection(db_lazy_reflection: SQLDatabase) -> None:
|
def test_table_info_lazy_reflection(db_lazy_reflection: SQLDatabase) -> None:
|
||||||
"""Test that table info with lazy reflection"""
|
"""Test that table info with lazy reflection"""
|
||||||
assert len(db_lazy_reflection._metadata.sorted_tables) == 0
|
assert len(db_lazy_reflection._metadata.sorted_tables) == 0
|
||||||
@ -111,6 +113,7 @@ def test_table_info_lazy_reflection(db_lazy_reflection: SQLDatabase) -> None:
|
|||||||
assert db_lazy_reflection._metadata.sorted_tables[1].name == "user"
|
assert db_lazy_reflection._metadata.sorted_tables[1].name == "user"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
|
||||||
def test_table_info_w_sample_rows(db: SQLDatabase) -> None:
|
def test_table_info_w_sample_rows(db: SQLDatabase) -> None:
|
||||||
"""Test that table info is constructed properly."""
|
"""Test that table info is constructed properly."""
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ from sqlalchemy import (
|
|||||||
insert,
|
insert,
|
||||||
schema,
|
schema,
|
||||||
)
|
)
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
from langchain_community.utilities.sql_database import SQLDatabase
|
from langchain_community.utilities.sql_database import SQLDatabase
|
||||||
|
|
||||||
@ -43,6 +46,9 @@ company = Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
version.parse(sa.__version__).major == 1, reason="SQLAlchemy 1.x issues"
|
||||||
|
)
|
||||||
def test_table_info() -> None:
|
def test_table_info() -> None:
|
||||||
"""Test that table info is constructed properly."""
|
"""Test that table info is constructed properly."""
|
||||||
engine = create_engine("duckdb:///:memory:")
|
engine = create_engine("duckdb:///:memory:")
|
||||||
@ -65,6 +71,9 @@ def test_table_info() -> None:
|
|||||||
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
|
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
version.parse(sa.__version__).major == 1, reason="SQLAlchemy 1.x issues"
|
||||||
|
)
|
||||||
def test_sql_database_run() -> None:
|
def test_sql_database_run() -> None:
|
||||||
"""Test that commands can be run successfully and returned in correct format."""
|
"""Test that commands can be run successfully and returned in correct format."""
|
||||||
engine = create_engine("duckdb:///:memory:")
|
engine = create_engine("duckdb:///:memory:")
|
||||||
|
Loading…
Reference in New Issue
Block a user