Skip to content

Commit 3e09f4d

Browse files
committed
update test cases
1 parent 8d330dc commit 3e09f4d

17 files changed

Lines changed: 1368 additions & 1392 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "sqlalchemy-crud-plus"
33
description = "Asynchronous CRUD operation based on SQLAlchemy2 model"
4-
version = '1.9.0'
4+
version = '1.10.0'
55
authors = [
66
{ name = "Wu Clan", email = "jianhengwu0407@gmail.com" },
77
]

requirements_docs.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
mkdocs-material==9.5.31
1+
mkdocs-material
2+
mkdocstrings[python]

sqlalchemy_crud_plus/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
13
from __future__ import annotations
24

35
from typing import Literal, TypeVar

tests/conftest.py

Lines changed: 55 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,25 @@
55
import pytest
66
import pytest_asyncio
77

8+
from sqlalchemy import insert
89
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
910

1011
from sqlalchemy_crud_plus import CRUDPlus
11-
from tests.models.basic import Base as BasicBase
12-
from tests.models.basic import Ins, InsPks
12+
from tests.models.basic import Base, Ins, InsPks
1313
from tests.models.relations import RelationBase, RelCategory, RelPost, RelProfile, RelRole, RelUser, user_role
1414
from tests.schemas.relations import RelPostCreate, RelProfileCreate, RelRoleCreate, RelUserCreate
1515

16-
# Database configuration
1716
_async_engine = create_async_engine('sqlite+aiosqlite:///:memory:', future=True, echo=False)
18-
_async_session_factory = async_sessionmaker(_async_engine, autoflush=False, expire_on_commit=False)
17+
_async_db_session = async_sessionmaker(_async_engine, autoflush=False, expire_on_commit=False)
1918

2019

21-
@pytest_asyncio.fixture(scope='function', autouse=True)
22-
async def setup_database() -> AsyncGenerator[None, None]:
23-
"""Setup and teardown database for each test function."""
20+
@pytest_asyncio.fixture(scope='session', autouse=True)
21+
async def setup_database():
22+
"""Set up the test database."""
2423
async with _async_engine.begin() as conn:
25-
await conn.run_sync(BasicBase.metadata.create_all)
24+
await conn.run_sync(Base.metadata.create_all)
2625
await conn.run_sync(RelationBase.metadata.create_all)
2726

28-
yield
29-
30-
async with _async_engine.begin() as conn:
31-
await conn.run_sync(BasicBase.metadata.drop_all)
32-
await conn.run_sync(RelationBase.metadata.drop_all)
33-
34-
35-
@pytest_asyncio.fixture
36-
async def db_session() -> AsyncGenerator[AsyncSession, None]:
37-
"""Provide a database session for testing."""
38-
async with _async_session_factory() as session:
39-
yield session
40-
41-
42-
@pytest_asyncio.fixture
43-
async def db_session_factory() -> AsyncGenerator[async_sessionmaker[AsyncSession], None]:
44-
"""Provide a session factory for testing."""
45-
yield _async_session_factory
46-
4727

4828
@pytest.fixture
4929
def crud_ins() -> CRUDPlus[Ins]:
@@ -58,166 +38,158 @@ def crud_ins_pks() -> CRUDPlus[InsPks]:
5838

5939

6040
@pytest_asyncio.fixture
61-
async def populated_db(db_session: AsyncSession, crud_ins: CRUDPlus[Ins]) -> list[Ins]:
41+
async def async_db_session() -> AsyncGenerator[AsyncSession, None]:
42+
"""Provide a database session for testing."""
43+
async with _async_db_session() as session:
44+
yield session
45+
46+
47+
@pytest_asyncio.fixture
48+
async def populated_db(async_db_session: AsyncSession, crud_ins: CRUDPlus[Ins]) -> list[Ins]:
6249
"""Provide a database populated with test data."""
63-
async with db_session.begin():
50+
async with async_db_session.begin():
6451
test_data = [Ins(name=f'item_{i}', del_flag=(i % 2 == 0)) for i in range(1, 11)]
65-
db_session.add_all(test_data)
66-
await db_session.flush()
52+
async_db_session.add_all(test_data)
6753
return test_data
6854

6955

7056
@pytest_asyncio.fixture
71-
async def populated_db_pks(db_session: AsyncSession) -> dict[str, list[InsPks]]:
57+
async def populated_db_pks(async_db_session: AsyncSession) -> dict[str, list[InsPks]]:
7258
"""Provide a database populated with composite key test data."""
73-
async with db_session.begin():
59+
async with async_db_session.begin():
7460
men_data = [InsPks(id=i, name=f'man_{i}', sex='men') for i in range(1, 4)]
75-
women_data = [InsPks(id=i, name=f'woman_{i}', sex='women') for i in range(4, 7)]
61+
women_data = [InsPks(id=i, name=f'woman_{i}', sex='women') for i in range(1, 4)]
7662
all_data = men_data + women_data
7763

78-
db_session.add_all(all_data)
79-
await db_session.flush()
64+
async_db_session.add_all(all_data)
65+
await async_db_session.flush()
8066

8167
return {'men': men_data, 'women': women_data, 'all': all_data}
8268

8369

8470
@pytest.fixture
85-
def rel_user_crud() -> CRUDPlus[RelUser]:
71+
def rel_crud_user() -> CRUDPlus[RelUser]:
8672
"""Provide CRUD instance for RelUser model."""
8773
return CRUDPlus(RelUser)
8874

8975

9076
@pytest.fixture
91-
def rel_post_crud() -> CRUDPlus[RelPost]:
77+
def rel_crud_post() -> CRUDPlus[RelPost]:
9278
"""Provide CRUD instance for RelPost model."""
9379
return CRUDPlus(RelPost)
9480

9581

9682
@pytest.fixture
97-
def rel_profile_crud() -> CRUDPlus[RelProfile]:
83+
def rel_crud_profile() -> CRUDPlus[RelProfile]:
9884
"""Provide CRUD instance for RelProfile model."""
9985
return CRUDPlus(RelProfile)
10086

10187

10288
@pytest.fixture
103-
def rel_category_crud() -> CRUDPlus[RelCategory]:
89+
def rel_crud_category() -> CRUDPlus[RelCategory]:
10490
"""Provide CRUD instance for RelCategory model."""
10591
return CRUDPlus(RelCategory)
10692

10793

10894
@pytest.fixture
109-
def rel_role_crud() -> CRUDPlus[RelRole]:
95+
def rel_crud_role() -> CRUDPlus[RelRole]:
11096
"""Provide CRUD instance for RelRole model."""
11197
return CRUDPlus(RelRole)
11298

11399

114100
@pytest_asyncio.fixture
115-
async def rel_sample_users(db_session: AsyncSession) -> list[RelUser]:
101+
async def rel_sample_users(async_db_session: AsyncSession) -> list[RelUser]:
116102
"""Create sample relation users."""
117-
async with db_session.begin():
103+
async with async_db_session.begin():
118104
users = []
119105
for i in range(1, 4):
120106
user_data = RelUserCreate(name=f'user_{i}')
121107
user = RelUser(**user_data.model_dump())
122-
db_session.add(user)
108+
async_db_session.add(user)
123109
users.append(user)
124-
await db_session.flush()
125110
return users
126111

127112

128113
@pytest_asyncio.fixture
129-
async def rel_sample_profiles(db_session: AsyncSession, rel_sample_users: list[RelUser]) -> list[RelProfile]:
114+
async def rel_sample_profiles(async_db_session: AsyncSession, rel_sample_users: list[RelUser]) -> list[RelProfile]:
130115
"""Create sample relation profiles."""
131-
async with db_session.begin():
116+
async with async_db_session.begin():
132117
profiles = []
133-
for i, user in enumerate(rel_sample_users[:2]): # Only first 2 users get profiles
118+
for i, user in enumerate(rel_sample_users[:2]):
134119
profile_data = RelProfileCreate(bio=f'Bio for {user.name}')
135120
profile = RelProfile(user_id=user.id, **profile_data.model_dump())
136-
db_session.add(profile)
121+
async_db_session.add(profile)
137122
profiles.append(profile)
138-
await db_session.flush()
139123
return profiles
140124

141125

142126
@pytest_asyncio.fixture
143-
async def rel_sample_categories(db_session: AsyncSession) -> list[RelCategory]:
127+
async def rel_sample_categories(async_db_session: AsyncSession) -> list[RelCategory]:
144128
"""Create sample relation categories."""
145-
async with db_session.begin():
146-
categories = []
147-
# Root categories
148-
tech = RelCategory(name='Technology')
149-
science = RelCategory(name='Science')
150-
db_session.add_all([tech, science])
151-
await db_session.flush()
152-
153-
# Sub categories
129+
async with async_db_session.begin():
130+
tech = RelCategory(name='Technology', parent_id=None)
131+
science = RelCategory(name='Science', parent_id=None)
132+
async_db_session.add_all([tech, science])
133+
await async_db_session.flush()
134+
154135
programming = RelCategory(name='Programming', parent_id=tech.id)
155136
ai = RelCategory(name='AI', parent_id=tech.id)
156-
db_session.add_all([programming, ai])
157-
await db_session.flush()
158-
137+
async_db_session.add_all([programming, ai])
159138
categories = [tech, science, programming, ai]
160139
return categories
161140

162141

163142
@pytest_asyncio.fixture
164143
async def rel_sample_posts(
165-
db_session: AsyncSession, rel_sample_users: list[RelUser], rel_sample_categories: list[RelCategory]
144+
async_db_session: AsyncSession, rel_sample_users: list[RelUser], rel_sample_categories: list[RelCategory]
166145
) -> list[RelPost]:
167146
"""Create sample relation posts."""
168-
async with db_session.begin():
147+
async with async_db_session.begin():
169148
posts = []
170149
for i in range(6):
171150
post_data = RelPostCreate(
172151
title=f'Post {i + 1}',
173152
category_id=rel_sample_categories[i % len(rel_sample_categories)].id if i < 4 else None,
174153
)
175154
post = RelPost(author_id=rel_sample_users[i % len(rel_sample_users)].id, **post_data.model_dump())
176-
db_session.add(post)
155+
async_db_session.add(post)
177156
posts.append(post)
178-
await db_session.flush()
179157
return posts
180158

181159

182160
@pytest_asyncio.fixture
183-
async def rel_sample_roles(db_session: AsyncSession) -> list[RelRole]:
161+
async def rel_sample_roles(async_db_session: AsyncSession) -> list[RelRole]:
184162
"""Create sample relation roles."""
185-
async with db_session.begin():
163+
async with async_db_session.begin():
186164
roles = []
187-
for role_name in ['admin', 'editor', 'viewer']:
165+
for role_name in ['admin', 'editor']:
188166
role_data = RelRoleCreate(name=role_name)
189167
role = RelRole(**role_data.model_dump())
190-
db_session.add(role)
168+
async_db_session.add(role)
191169
roles.append(role)
192-
await db_session.flush()
193170
return roles
194171

195172

196173
@pytest_asyncio.fixture
197174
async def rel_sample_data(
198-
db_session: AsyncSession,
175+
async_db_session: AsyncSession,
199176
rel_sample_users: list[RelUser],
200177
rel_sample_profiles: list[RelProfile],
201178
rel_sample_categories: list[RelCategory],
202179
rel_sample_posts: list[RelPost],
203180
rel_sample_roles: list[RelRole],
204181
) -> dict:
205182
"""Create complete relation sample data with relationships."""
206-
async with db_session.begin():
207-
# Create user-role relationships
208-
await db_session.execute(
209-
user_role.insert().values(
183+
async with async_db_session.begin():
184+
await async_db_session.execute(
185+
insert(user_role).values(
210186
[
211-
{'user_id': rel_sample_users[0].id, 'role_id': rel_sample_roles[0].id}, # user_1 is admin
212-
{'user_id': rel_sample_users[1].id, 'role_id': rel_sample_roles[1].id}, # user_2 is editor
213-
{'user_id': rel_sample_users[0].id, 'role_id': rel_sample_roles[1].id}, # user_1 is also editor
187+
{'user_id': rel_sample_users[0].id, 'role_id': rel_sample_roles[0].id},
188+
{'user_id': rel_sample_users[1].id, 'role_id': rel_sample_roles[1].id},
214189
]
215190
)
216191
)
217192

218-
await db_session.flush()
219-
await db_session.commit()
220-
221193
return {
222194
'users': rel_sample_users,
223195
'profiles': rel_sample_profiles,

tests/models/relations.py

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
from __future__ import annotations
44

55
from sqlalchemy import Column, ForeignKey, Integer, String, Table
6-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
6+
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column, relationship
77

88

9-
class RelationBase(DeclarativeBase):
10-
pass
9+
class RelationBase(MappedAsDataclass, DeclarativeBase):
10+
@declared_attr.directive
11+
def __tablename__(cls) -> str:
12+
return cls.__name__.lower()
1113

1214

13-
# Association table for Many-to-Many relationship between User and Role
1415
user_role = Table(
1516
'user_role',
1617
RelationBase.metadata,
@@ -22,67 +23,52 @@ class RelationBase(DeclarativeBase):
2223
class RelUser(RelationBase):
2324
__tablename__ = 'rel_user'
2425

25-
id: Mapped[int] = mapped_column(primary_key=True)
26-
name: Mapped[str] = mapped_column(String(50))
26+
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
27+
name: Mapped[str] = mapped_column(String(30))
2728

28-
# One-to-One relationship: User has one profile
29-
profile: Mapped[RelProfile | None] = relationship(back_populates='user', uselist=False)
30-
31-
# One-to-Many relationship: User has many posts
32-
posts: Mapped[list[RelPost]] = relationship(back_populates='author')
33-
34-
# Many-to-Many relationship: User has many roles, Role has many users
35-
roles: Mapped[list[RelRole]] = relationship(secondary=user_role, back_populates='users')
29+
profile: Mapped[RelProfile | None] = relationship(init=False, back_populates='user', uselist=False)
30+
posts: Mapped[list[RelPost]] = relationship(init=False, back_populates='author')
31+
roles: Mapped[list[RelRole]] = relationship(init=False, secondary=user_role, back_populates='users')
3632

3733

3834
class RelProfile(RelationBase):
3935
__tablename__ = 'rel_profile'
4036

41-
id: Mapped[int] = mapped_column(primary_key=True)
37+
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
4238
user_id: Mapped[int] = mapped_column(ForeignKey('rel_user.id'), unique=True)
43-
bio: Mapped[str] = mapped_column(String(200))
39+
bio: Mapped[str] = mapped_column(String(100))
4440

45-
# One-to-One relationship: Profile belongs to one user
46-
user: Mapped[RelUser] = relationship(back_populates='profile')
41+
user: Mapped[RelUser] = relationship(init=False, back_populates='profile')
4742

4843

4944
class RelCategory(RelationBase):
5045
__tablename__ = 'rel_category'
5146

52-
id: Mapped[int] = mapped_column(primary_key=True)
53-
name: Mapped[str] = mapped_column(String(50))
47+
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
48+
name: Mapped[str] = mapped_column(String(30))
5449
parent_id: Mapped[int | None] = mapped_column(ForeignKey('rel_category.id'))
5550

56-
# Self-referential relationship: Category belongs to parent category
57-
parent: Mapped[RelCategory | None] = relationship('RelCategory', remote_side=[id], back_populates='children')
58-
59-
# Self-referential relationship: Category has many child categories
60-
children: Mapped[list[RelCategory]] = relationship('RelCategory', back_populates='parent')
61-
62-
# One-to-Many relationship: Category has many posts
63-
posts: Mapped[list[RelPost]] = relationship(back_populates='category')
51+
parent: Mapped[RelCategory | None] = relationship(init=False, remote_side=[id], back_populates='children')
52+
children: Mapped[list[RelCategory]] = relationship(init=False, back_populates='parent')
53+
posts: Mapped[list[RelPost]] = relationship(init=False, back_populates='category')
6454

6555

6656
class RelPost(RelationBase):
6757
__tablename__ = 'rel_post'
6858

69-
id: Mapped[int] = mapped_column(primary_key=True)
70-
title: Mapped[str] = mapped_column(String(100))
59+
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
60+
title: Mapped[str] = mapped_column(String(50))
7161
author_id: Mapped[int] = mapped_column(ForeignKey('rel_user.id'))
7262
category_id: Mapped[int | None] = mapped_column(ForeignKey('rel_category.id'))
7363

74-
# Many-to-One relationship: Post belongs to one author (user)
75-
author: Mapped[RelUser] = relationship(back_populates='posts')
76-
77-
# Many-to-One relationship: Post belongs to one category (optional)
78-
category: Mapped[RelCategory | None] = relationship(back_populates='posts')
64+
author: Mapped[RelUser] = relationship(init=False, back_populates='posts')
65+
category: Mapped[RelCategory | None] = relationship(init=False, back_populates='posts')
7966

8067

8168
class RelRole(RelationBase):
8269
__tablename__ = 'rel_role'
8370

84-
id: Mapped[int] = mapped_column(primary_key=True)
85-
name: Mapped[str] = mapped_column(String(50))
71+
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
72+
name: Mapped[str] = mapped_column(String(20))
8673

87-
# Many-to-Many relationship: Role has many users, User has many roles
88-
users: Mapped[list[RelUser]] = relationship(secondary=user_role, back_populates='roles')
74+
users: Mapped[list[RelUser]] = relationship(init=False, secondary=user_role, back_populates='roles')

0 commit comments

Comments
 (0)