Run tests across multiple cores/CPUs

This commit is contained in:
Andrew Brookins 2021-10-20 23:24:31 -07:00
parent bd24050e3f
commit 2ffd4e6f5a
7 changed files with 368 additions and 292 deletions

View file

@ -44,7 +44,7 @@ format: $(INSTALL_STAMP)
.PHONY: test .PHONY: test
test: $(INSTALL_STAMP) test: $(INSTALL_STAMP)
$(POETRY) run pytest -s -vv ./tests/ --cov-report term-missing --cov $(NAME) $(POETRY) run pytest -n auto -s -vv ./tests/ --cov-report term-missing --cov $(NAME)
.PHONY: shell .PHONY: shell
shell: $(INSTALL_STAMP) shell: $(INSTALL_STAMP)

55
poetry.lock generated
View file

@ -155,6 +155,17 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "execnet"
version = "1.9.0"
description = "execnet: rapid multi-Python deployment"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
testing = ["pre-commit"]
[[package]] [[package]]
name = "flake8" name = "flake8"
version = "4.0.1" version = "4.0.1"
@ -542,6 +553,36 @@ pytest = ">=4.6"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-forked"
version = "1.3.0"
description = "run tests in isolated forked subprocesses"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
py = "*"
pytest = ">=3.10"
[[package]]
name = "pytest-xdist"
version = "2.4.0"
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
execnet = ">=1.1"
pytest = ">=6.0.0"
pytest-forked = "*"
[package.extras]
psutil = ["psutil (>=3.0)"]
setproctitle = ["setproctitle"]
testing = ["filelock"]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "0.19.1" version = "0.19.1"
@ -685,7 +726,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "863006f3f82d19317c15e4e655356efc254ad4fbf6689557e86e2d1d633f77ec" content-hash = "56b381dd9b79bd082e978019124176491c63f09dd5ce90e5f8ab642a7f79480f"
[metadata.files] [metadata.files]
aioredis = [ aioredis = [
@ -771,6 +812,10 @@ decorator = [
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
] ]
execnet = [
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
flake8 = [ flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
@ -962,6 +1007,14 @@ pytest-cov = [
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
] ]
pytest-forked = [
{file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"},
{file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"},
]
pytest-xdist = [
{file = "pytest-xdist-2.4.0.tar.gz", hash = "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9"},
{file = "pytest_xdist-2.4.0-py3-none-any.whl", hash = "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168"},
]
python-dotenv = [ python-dotenv = [
{file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"}, {file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"},
{file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"}, {file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"},

View file

@ -29,6 +29,7 @@ flake8 = "^4.0.1"
bandit = "^1.7.0" bandit = "^1.7.0"
coverage = "^6.0.2" coverage = "^6.0.2"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
pytest-xdist = "^2.4.0"
[tool.poetry.scripts] [tool.poetry.scripts]

View file

@ -47,10 +47,10 @@ def create_index(index_name, schema, current_hash):
try: try:
redis.execute_command(f"ft.info {index_name}") redis.execute_command(f"ft.info {index_name}")
except ResponseError: except ResponseError:
redis.execute_command(f"ft.create {index_name} {schema}")
redis.set(schema_hash_key(index_name), current_hash)
else:
log.info("Index already exists, skipping. Index hash: %s", index_name) log.info("Index already exists, skipping. Index hash: %s", index_name)
return
redis.execute_command(f"ft.create {index_name} {schema}")
redis.set(schema_hash_key(index_name), current_hash)
class MigrationAction(Enum): class MigrationAction(Enum):
@ -74,10 +74,16 @@ class IndexMigration:
self.drop() self.drop()
def create(self): def create(self):
return create_index(self.index_name, self.schema, self.hash) try:
return create_index(self.index_name, self.schema, self.hash)
except ResponseError:
log.info("Index already exists: %s", self.index_name)
def drop(self): def drop(self):
redis.execute_command(f"FT.DROPINDEX {self.index_name}") try:
redis.execute_command(f"FT.DROPINDEX {self.index_name}")
except ResponseError:
log.info("Index does not exist: %s", self.index_name)
class Migrator: class Migrator:

View file

@ -1,13 +1,9 @@
import random
import pytest import pytest
from redis import Redis from redis import Redis
from redis_developer.connections import get_redis_connection from redis_developer.connections import get_redis_connection
from redis_developer.model.migrations.migrator import Migrator
@pytest.fixture(scope="module", autouse=True)
def migrations():
Migrator().run()
@pytest.fixture @pytest.fixture
@ -15,16 +11,20 @@ def redis():
yield get_redis_connection() yield get_redis_connection()
@pytest.fixture
def key_prefix():
yield "redis-developer"
def _delete_test_keys(prefix: str, conn: Redis): def _delete_test_keys(prefix: str, conn: Redis):
keys = []
for key in conn.scan_iter(f"{prefix}:*"): for key in conn.scan_iter(f"{prefix}:*"):
conn.delete(key) keys.append(key)
if keys:
conn.delete(*keys)
@pytest.fixture(scope="function", autouse=True) @pytest.fixture
def key_prefix(redis):
key_prefix = f"redis-developer:{random.random()}"
yield key_prefix
_delete_test_keys(key_prefix, redis)
@pytest.fixture(autouse=True)
def delete_test_keys(redis, request, key_prefix): def delete_test_keys(redis, request, key_prefix):
_delete_test_keys(key_prefix, redis) _delete_test_keys(key_prefix, redis)

View file

@ -1,14 +1,15 @@
import abc import abc
import datetime import datetime
import decimal import decimal
from collections import namedtuple
from typing import Optional from typing import Optional
from unittest import mock from unittest import mock
import pytest import pytest
import redis
from pydantic import ValidationError from pydantic import ValidationError
from redis_developer.model import Field, HashModel from redis_developer.model import Field, HashModel
from redis_developer.model.migrations.migrator import Migrator
from redis_developer.model.model import ( from redis_developer.model.model import (
NotFoundError, NotFoundError,
QueryNotSupportedError, QueryNotSupportedError,
@ -16,36 +17,42 @@ from redis_developer.model.model import (
) )
r = redis.Redis()
today = datetime.date.today() today = datetime.date.today()
class BaseHashModel(HashModel, abc.ABC): @pytest.fixture
class Meta: def m(key_prefix):
global_key_prefix = "redis-developer" class BaseHashModel(HashModel, abc.ABC):
class Meta:
global_key_prefix = key_prefix
class Order(BaseHashModel): class Order(BaseHashModel):
total: decimal.Decimal total: decimal.Decimal
currency: str currency: str
created_on: datetime.datetime created_on: datetime.datetime
class Member(BaseHashModel): class Member(BaseHashModel):
first_name: str = Field(index=True) first_name: str = Field(index=True)
last_name: str = Field(index=True) last_name: str = Field(index=True)
email: str = Field(index=True) email: str = Field(index=True)
join_date: datetime.date join_date: datetime.date
age: int = Field(index=True) age: int = Field(index=True)
class Meta: class Meta:
model_key_prefix = "member" model_key_prefix = "member"
primary_key_pattern = "" primary_key_pattern = ""
Migrator().run()
return namedtuple('Models', ['BaseHashModel', 'Order', 'Member'])(
BaseHashModel, Order, Member)
@pytest.fixture() @pytest.fixture
def members(): def members(m):
member1 = Member( member1 = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -53,7 +60,7 @@ def members():
join_date=today, join_date=today,
) )
member2 = Member( member2 = m.Member(
first_name="Kim", first_name="Kim",
last_name="Brookins", last_name="Brookins",
email="k@example.com", email="k@example.com",
@ -61,7 +68,7 @@ def members():
join_date=today, join_date=today,
) )
member3 = Member( member3 = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Smith", last_name="Smith",
email="as@example.com", email="as@example.com",
@ -75,21 +82,21 @@ def members():
yield member1, member2, member3 yield member1, member2, member3
def test_validates_required_fields(): def test_validates_required_fields(m):
# Raises ValidationError: last_name is required # Raises ValidationError: last_name is required
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
Member(first_name="Andrew", zipcode="97086", join_date=today) m.Member(first_name="Andrew", zipcode="97086", join_date=today)
def test_validates_field(): def test_validates_field(m):
# Raises ValidationError: join_date is not a date # Raises ValidationError: join_date is not a date
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
Member(first_name="Andrew", last_name="Brookins", join_date="yesterday") m.Member(first_name="Andrew", last_name="Brookins", join_date="yesterday")
# Passes validation # Passes validation
def test_validation_passes(): def test_validation_passes(m):
member = Member( member = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -99,8 +106,8 @@ def test_validation_passes():
assert member.first_name == "Andrew" assert member.first_name == "Andrew"
def test_saves_model_and_creates_pk(): def test_saves_model_and_creates_pk(m):
member = Member( member = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -110,12 +117,12 @@ def test_saves_model_and_creates_pk():
# Save a model instance to Redis # Save a model instance to Redis
member.save() member.save()
member2 = Member.get(member.pk) member2 = m.Member.get(member.pk)
assert member2 == member assert member2 == member
def test_raises_error_with_embedded_models(): def test_raises_error_with_embedded_models(m):
class Address(BaseHashModel): class Address(m.BaseHashModel):
address_line_1: str address_line_1: str
address_line_2: Optional[str] address_line_2: Optional[str]
city: str city: str
@ -123,53 +130,52 @@ def test_raises_error_with_embedded_models():
postal_code: str postal_code: str
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class InvalidMember(m.BaseHashModel):
class InvalidMember(BaseHashModel):
address: Address address: Address
@pytest.mark.skip("Not implemented yet") @pytest.mark.skip("Not implemented yet")
def test_saves_many(): def test_saves_many(m):
members = [ members = [
Member( m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
join_date=today, join_date=today,
), ),
Member( m.Member(
first_name="Kim", first_name="Kim",
last_name="Brookins", last_name="Brookins",
email="k@example.com", email="k@example.com",
join_date=today, join_date=today,
), ),
] ]
Member.add(members) m.Member.add(members)
@pytest.mark.skip("Not ready yet") @pytest.mark.skip("Not ready yet")
def test_updates_a_model(members): def test_updates_a_model(members, m):
member1, member2, member3 = members member1, member2, member3 = members
# Or, with an implicit save: # Or, with an implicit save:
member1.update(last_name="Smith") member1.update(last_name="Smith")
assert Member.find(Member.pk == member1.pk).first() == member1 assert m.Member.find(m.Member.pk == member1.pk).first() == member1
# Or, affecting multiple model instances with an implicit save: # Or, affecting multiple model instances with an implicit save:
Member.find(Member.last_name == "Brookins").update(last_name="Smith") m.Member.find(m.Member.last_name == "Brookins").update(last_name="Smith")
results = Member.find(Member.last_name == "Smith") results = m.Member.find(m.Member.last_name == "Smith")
assert results == members assert results == members
def test_paginate_query(members): def test_paginate_query(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find().all(batch_size=1) actual = m.Member.find().sort_by('age').all(batch_size=1)
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
def test_access_result_by_index_cached(members): def test_access_result_by_index_cached(members, m):
member1, member2, member3 = members member1, member2, member3 = members
query = Member.find().sort_by("age") query = m.Member.find().sort_by("age")
# Load the cache, throw away the result. # Load the cache, throw away the result.
assert query._model_cache == [] assert query._model_cache == []
query.execute() query.execute()
@ -181,9 +187,9 @@ def test_access_result_by_index_cached(members):
assert not mock_db.called assert not mock_db.called
def test_access_result_by_index_not_cached(members): def test_access_result_by_index_not_cached(members, m):
member1, member2, member3 = members member1, member2, member3 = members
query = Member.find().sort_by("age") query = m.Member.find().sort_by("age")
# Assert that we don't have any models in the cache yet -- we # Assert that we don't have any models in the cache yet -- we
# haven't made any requests of Redis. # haven't made any requests of Redis.
@ -193,57 +199,57 @@ def test_access_result_by_index_not_cached(members):
assert query[2] == member3 assert query[2] == member3
def test_exact_match_queries(members): def test_exact_match_queries(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.last_name == "Brookins").all() actual = m.Member.find(m.Member.last_name == "Brookins").sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew") (m.Member.last_name == "Brookins") & ~(m.Member.first_name == "Andrew")
).all() ).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(~(Member.last_name == "Brookins")).all() actual = m.Member.find(~(m.Member.last_name == "Brookins")).all()
assert actual == [member3] assert actual == [member3]
actual = Member.find(Member.last_name != "Brookins").all() actual = m.Member.find(m.Member.last_name != "Brookins").all()
assert actual == [member3] assert actual == [member3]
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") & (Member.first_name == "Andrew") (m.Member.last_name == "Brookins") & (m.Member.first_name == "Andrew")
| (Member.first_name == "Kim") | (m.Member.first_name == "Kim")
).all() ).sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find( actual = m.Member.find(
Member.first_name == "Kim", Member.last_name == "Brookins" m.Member.first_name == "Kim", m.Member.last_name == "Brookins"
).all() ).all()
assert actual == [member2] assert actual == [member2]
def test_recursive_query_resolution(members): def test_recursive_query_resolution(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") (m.Member.last_name == "Brookins")
| (Member.age == 100) & (Member.last_name == "Smith") | (m.Member.age == 100) & (m.Member.last_name == "Smith")
).all() ).sort_by('age').all()
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
def test_tag_queries_boolean_logic(members): def test_tag_queries_boolean_logic(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find( actual = m.Member.find(
(Member.first_name == "Andrew") & (Member.last_name == "Brookins") (m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (Member.last_name == "Smith") | (m.Member.last_name == "Smith")
).all() ).sort_by('age').all()
assert actual == [member1, member3] assert actual == [member1, member3]
def test_tag_queries_punctuation(): def test_tag_queries_punctuation(m):
member1 = Member( member1 = m.Member(
first_name="Andrew, the Michael", first_name="Andrew, the Michael",
last_name="St. Brookins-on-Pier", last_name="St. Brookins-on-Pier",
email="a|b@example.com", # NOTE: This string uses the TAG field separator. email="a|b@example.com", # NOTE: This string uses the TAG field separator.
@ -252,7 +258,7 @@ def test_tag_queries_punctuation():
) )
member1.save() member1.save()
member2 = Member( member2 = m.Member(
first_name="Bob", first_name="Bob",
last_name="the Villain", last_name="the Villain",
email="a|villain@example.com", # NOTE: This string uses the TAG field separator. email="a|villain@example.com", # NOTE: This string uses the TAG field separator.
@ -261,18 +267,18 @@ def test_tag_queries_punctuation():
) )
member2.save() member2.save()
assert Member.find(Member.first_name == "Andrew, the Michael").first() == member1 assert m.Member.find(m.Member.first_name == "Andrew, the Michael").first() == member1
assert Member.find(Member.last_name == "St. Brookins-on-Pier").first() == member1 assert m.Member.find(m.Member.last_name == "St. Brookins-on-Pier").first() == member1
# Notice that when we index and query multiple values that use the internal # Notice that when we index and query multiple values that use the internal
# TAG separator for single-value exact-match fields, like an indexed string, # TAG separator for single-value exact-match fields, like an indexed string,
# the queries will succeed. We apply a workaround that queries for the union # the queries will succeed. We apply a workaround that queries for the union
# of the two values separated by the tag separator. # of the two values separated by the tag separator.
assert Member.find(Member.email == "a|b@example.com").all() == [member1] assert m.Member.find(m.Member.email == "a|b@example.com").all() == [member1]
assert Member.find(Member.email == "a|villain@example.com").all() == [member2] assert m.Member.find(m.Member.email == "a|villain@example.com").all() == [member2]
def test_tag_queries_negation(members): def test_tag_queries_negation(members, m):
member1, member2, member3 = members member1, member2, member3 = members
""" """
@ -281,7 +287,7 @@ def test_tag_queries_negation(members):
Andrew Andrew
""" """
query = Member.find(~(Member.first_name == "Andrew")) query = m.Member.find(~(m.Member.first_name == "Andrew"))
assert query.all() == [member2] assert query.all() == [member2]
""" """
@ -294,8 +300,8 @@ def test_tag_queries_negation(members):
Brookins Brookins
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins") ~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
) )
assert query.all() == [member2] assert query.all() == [member2]
@ -312,9 +318,9 @@ def test_tag_queries_negation(members):
EQ EQ
Smith Smith
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") ~(m.Member.first_name == "Andrew")
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith")) & ((m.Member.last_name == "Brookins") | (m.Member.last_name == "Smith"))
) )
assert query.all() == [member2] assert query.all() == [member2]
@ -331,72 +337,74 @@ def test_tag_queries_negation(members):
EQ EQ
Smith Smith
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins") ~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (Member.last_name == "Smith") | (m.Member.last_name == "Smith")
) )
assert query.all() == [member2, member3] assert query.sort_by('age').all() == [member2, member3]
actual = Member.find( actual = m.Member.find(
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins") (m.Member.first_name == "Andrew") & ~(m.Member.last_name == "Brookins")
).all() ).all()
assert actual == [member3] assert actual == [member3]
def test_numeric_queries(members): def test_numeric_queries(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.age == 34).all() actual = m.Member.find(m.Member.age == 34).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age > 34).all() actual = m.Member.find(m.Member.age > 34).sort_by('age').all()
assert actual == [member1, member3] assert actual == [member1, member3]
actual = Member.find(Member.age < 35).all() actual = m.Member.find(m.Member.age < 35).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age <= 34).all() actual = m.Member.find(m.Member.age <= 34).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age >= 100).all() actual = m.Member.find(m.Member.age >= 100).all()
assert actual == [member3] assert actual == [member3]
actual = Member.find(Member.age != 34).all() actual = m.Member.find(m.Member.age != 34).sort_by('age').all()
assert actual == [member1, member3] assert actual == [member1, member3]
actual = Member.find(~(Member.age == 100)).all() actual = m.Member.find(~(m.Member.age == 100)).sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find(Member.age > 30, Member.age < 40).all() actual = m.Member.find(
assert actual == [member1, member2] m.Member.age > 30, m.Member.age < 40
).sort_by('age').all()
assert actual == [member2, member1]
def test_sorting(members): def test_sorting(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.age > 34).sort_by("age").all() actual = m.Member.find(m.Member.age > 34).sort_by("age").all()
assert actual == [member1, member3] assert actual == [member1, member3]
actual = Member.find(Member.age > 34).sort_by("-age").all() actual = m.Member.find(m.Member.age > 34).sort_by("-age").all()
assert actual == [member3, member1] assert actual == [member3, member1]
with pytest.raises(QueryNotSupportedError): with pytest.raises(QueryNotSupportedError):
# This field does not exist. # This field does not exist.
Member.find().sort_by("not-a-real-field").all() m.Member.find().sort_by("not-a-real-field").all()
with pytest.raises(QueryNotSupportedError): with pytest.raises(QueryNotSupportedError):
# This field is not sortable. # This field is not sortable.
Member.find().sort_by("join_date").all() m.Member.find().sort_by("join_date").all()
def test_not_found(): def test_not_found(m):
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
# This ID does not exist. # This ID does not exist.
Member.get(1000) m.Member.get(1000)
def test_schema(): def test_schema(m, key_prefix):
class Address(BaseHashModel): class Address(m.BaseHashModel):
a_string: str = Field(index=True) a_string: str = Field(index=True)
a_full_text_string: str = Field(index=True, full_text_search=True) a_full_text_string: str = Field(index=True, full_text_search=True)
an_integer: int = Field(index=True, sortable=True) an_integer: int = Field(index=True, sortable=True)
@ -406,5 +414,5 @@ def test_schema():
assert ( assert (
Address.redisearch_schema() Address.redisearch_schema()
== "ON HASH PREFIX 1 redis-developer:tests.test_hash_model.Address: SCHEMA pk TAG SEPARATOR | a_string TAG SEPARATOR | a_full_text_string TAG SEPARATOR | a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE a_float NUMERIC" == f"ON HASH PREFIX 1 {key_prefix}:tests.test_hash_model.Address: SCHEMA pk TAG SEPARATOR | a_string TAG SEPARATOR | a_full_text_string TAG SEPARATOR | a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE a_float NUMERIC"
) )

View file

@ -1,6 +1,7 @@
import abc import abc
import datetime import datetime
import decimal import decimal
from collections import namedtuple
from typing import List, Optional from typing import List, Optional
from unittest import mock from unittest import mock
@ -17,61 +18,67 @@ from redis_developer.model.model import (
) )
r = redis.Redis()
today = datetime.date.today() today = datetime.date.today()
class BaseJsonModel(JsonModel, abc.ABC): @pytest.fixture
class Meta: def m(key_prefix):
global_key_prefix = "redis-developer" class BaseJsonModel(JsonModel, abc.ABC):
class Meta:
global_key_prefix = key_prefix
class Note(EmbeddedJsonModel): class Note(EmbeddedJsonModel):
# TODO: This was going to be a full-text search example, but # TODO: This was going to be a full-text search example, but
# we can't index embedded documents for full-text search in # we can't index embedded documents for full-text search in
# the preview release. # the preview release.
description: str = Field(index=True) description: str = Field(index=True)
created_on: datetime.datetime created_on: datetime.datetime
class Address(EmbeddedJsonModel): class Address(EmbeddedJsonModel):
address_line_1: str address_line_1: str
address_line_2: Optional[str] address_line_2: Optional[str]
city: str = Field(index=True) city: str = Field(index=True)
state: str state: str
country: str country: str
postal_code: str = Field(index=True) postal_code: str = Field(index=True)
note: Optional[Note] note: Optional[Note]
class Item(EmbeddedJsonModel): class Item(EmbeddedJsonModel):
price: decimal.Decimal price: decimal.Decimal
name: str = Field(index=True) name: str = Field(index=True)
class Order(EmbeddedJsonModel): class Order(EmbeddedJsonModel):
items: List[Item] items: List[Item]
created_on: datetime.datetime created_on: datetime.datetime
class Member(BaseJsonModel): class Member(BaseJsonModel):
first_name: str = Field(index=True) first_name: str = Field(index=True)
last_name: str = Field(index=True) last_name: str = Field(index=True)
email: str = Field(index=True) email: str = Field(index=True)
join_date: datetime.date join_date: datetime.date
age: int = Field(index=True) age: int = Field(index=True)
bio: Optional[str] = Field(index=True, full_text_search=True, default="") bio: Optional[str] = Field(index=True, full_text_search=True, default="")
# Creates an embedded model. # Creates an embedded model.
address: Address address: Address
# Creates an embedded list of models. # Creates an embedded list of models.
orders: Optional[List[Order]] orders: Optional[List[Order]]
Migrator().run()
return namedtuple('Models', ['BaseJsonModel', 'Note', 'Address', 'Item', 'Order', 'Member'])(
BaseJsonModel, Note, Address, Item, Order, Member)
@pytest.fixture() @pytest.fixture()
def address(): def address(m):
yield Address( yield m.Address(
address_line_1="1 Main St.", address_line_1="1 Main St.",
city="Portland", city="Portland",
state="OR", state="OR",
@ -81,8 +88,8 @@ def address():
@pytest.fixture() @pytest.fixture()
def members(address): def members(address, m):
member1 = Member( member1 = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -91,7 +98,7 @@ def members(address):
address=address, address=address,
) )
member2 = Member( member2 = m.Member(
first_name="Kim", first_name="Kim",
last_name="Brookins", last_name="Brookins",
email="k@example.com", email="k@example.com",
@ -100,7 +107,7 @@ def members(address):
address=address, address=address,
) )
member3 = Member( member3 = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Smith", last_name="Smith",
email="as@example.com", email="as@example.com",
@ -116,10 +123,10 @@ def members(address):
yield member1, member2, member3 yield member1, member2, member3
def test_validates_required_fields(address): def test_validates_required_fields(address, m):
# Raises ValidationError address is required # Raises ValidationError address is required
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
Member( m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
zipcode="97086", zipcode="97086",
@ -127,10 +134,10 @@ def test_validates_required_fields(address):
) )
def test_validates_field(address): def test_validates_field(address, m):
# Raises ValidationError: join_date is not a date # Raises ValidationError: join_date is not a date
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
Member( m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
join_date="yesterday", join_date="yesterday",
@ -139,8 +146,8 @@ def test_validates_field(address):
# Passes validation # Passes validation
def test_validation_passes(address): def test_validation_passes(address, m):
member = Member( member = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -151,8 +158,8 @@ def test_validation_passes(address):
assert member.first_name == "Andrew" assert member.first_name == "Andrew"
def test_saves_model_and_creates_pk(address): def test_saves_model_and_creates_pk(address, m):
member = Member( member = m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -163,15 +170,15 @@ def test_saves_model_and_creates_pk(address):
# Save a model instance to Redis # Save a model instance to Redis
member.save() member.save()
member2 = Member.get(member.pk) member2 = m.Member.get(member.pk)
assert member2 == member assert member2 == member
assert member2.address == address assert member2.address == address
@pytest.mark.skip("Not implemented yet") @pytest.mark.skip("Not implemented yet")
def test_saves_many(address): def test_saves_many(address, m):
members = [ members = [
Member( m.Member(
first_name="Andrew", first_name="Andrew",
last_name="Brookins", last_name="Brookins",
email="a@example.com", email="a@example.com",
@ -179,7 +186,7 @@ def test_saves_many(address):
address=address, address=address,
age=38, age=38,
), ),
Member( m.Member(
first_name="Kim", first_name="Kim",
last_name="Brookins", last_name="Brookins",
email="k@example.com", email="k@example.com",
@ -188,36 +195,36 @@ def test_saves_many(address):
age=34, age=34,
), ),
] ]
Member.add(members) m.Member.add(members)
@pytest.mark.skip("Not ready yet") @pytest.mark.skip("Not ready yet")
def test_updates_a_model(members): def test_updates_a_model(members, m):
member1, member2, member3 = members member1, member2, member3 = members
# Or, with an implicit save: # Or, with an implicit save:
member1.update(last_name="Smith") member1.update(last_name="Smith")
assert Member.find(Member.pk == member1.pk).first() == member1 assert m.Member.find(m.Member.pk == member1.pk).first() == member1
# Or, affecting multiple model instances with an implicit save: # Or, affecting multiple model instances with an implicit save:
Member.find(Member.last_name == "Brookins").update(last_name="Smith") m.Member.find(m.Member.last_name == "Brookins").update(last_name="Smith")
results = Member.find(Member.last_name == "Smith") results = m.Member.find(m.Member.last_name == "Smith")
assert results == members assert results == members
# Or, updating a field in an embedded model: # Or, updating a field in an embedded model:
member2.update(address__city="Happy Valley") member2.update(address__city="Happy Valley")
assert Member.find(Member.pk == member2.pk).first().address.city == "Happy Valley" assert m.Member.find(m.Member.pk == member2.pk).first().address.city == "Happy Valley"
def test_paginate_query(members): def test_paginate_query(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find().all(batch_size=1) actual = m.Member.find().sort_by('age').all(batch_size=1)
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
def test_access_result_by_index_cached(members): def test_access_result_by_index_cached(members, m):
member1, member2, member3 = members member1, member2, member3 = members
query = Member.find().sort_by("age") query = m.Member.find().sort_by("age")
# Load the cache, throw away the result. # Load the cache, throw away the result.
assert query._model_cache == [] assert query._model_cache == []
query.execute() query.execute()
@ -229,9 +236,9 @@ def test_access_result_by_index_cached(members):
assert not mock_db.called assert not mock_db.called
def test_access_result_by_index_not_cached(members): def test_access_result_by_index_not_cached(members, m):
member1, member2, member3 = members member1, member2, member3 = members
query = Member.find().sort_by("age") query = m.Member.find().sort_by("age")
# Assert that we don't have any models in the cache yet -- we # Assert that we don't have any models in the cache yet -- we
# haven't made any requests of Redis. # haven't made any requests of Redis.
@ -241,20 +248,20 @@ def test_access_result_by_index_not_cached(members):
assert query[2] == member3 assert query[2] == member3
def test_in_query(members): def test_in_query(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.pk << [member1.pk, member2.pk, member3.pk]).all() actual = m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk]).sort_by('age').all()
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
@pytest.mark.skip("Not implemented yet") @pytest.mark.skip("Not implemented yet")
def test_update_query(members): def test_update_query(members, m):
member1, member2, member3 = members member1, member2, member3 = members
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk]).update( m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk]).update(
first_name="Bobby" first_name="Bobby"
) )
actual = ( actual = (
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk]) m.Member.find(m.Member.pk << [member1.pk, member2.pk, member3.pk])
.sort_by("age") .sort_by("age")
.all() .all()
) )
@ -262,94 +269,94 @@ def test_update_query(members):
assert all([m.name == "Bobby" for m in actual]) assert all([m.name == "Bobby" for m in actual])
def test_exact_match_queries(members): def test_exact_match_queries(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.last_name == "Brookins").all() actual = m.Member.find(m.Member.last_name == "Brookins").sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew") (m.Member.last_name == "Brookins") & ~(m.Member.first_name == "Andrew")
).all() ).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(~(Member.last_name == "Brookins")).all() actual = m.Member.find(~(m.Member.last_name == "Brookins")).all()
assert actual == [member3] assert actual == [member3]
actual = Member.find(Member.last_name != "Brookins").all() actual = m.Member.find(m.Member.last_name != "Brookins").all()
assert actual == [member3] assert actual == [member3]
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") & (Member.first_name == "Andrew") (m.Member.last_name == "Brookins") & (m.Member.first_name == "Andrew")
| (Member.first_name == "Kim") | (m.Member.first_name == "Kim")
).all() ).sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find( actual = m.Member.find(
Member.first_name == "Kim", Member.last_name == "Brookins" m.Member.first_name == "Kim", m.Member.last_name == "Brookins"
).all() ).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.address.city == "Portland").all() actual = m.Member.find(m.Member.address.city == "Portland").sort_by('age').all()
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
def test_recursive_query_expression_resolution(members): def test_recursive_query_expression_resolution(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find( actual = m.Member.find(
(Member.last_name == "Brookins") (m.Member.last_name == "Brookins")
| (Member.age == 100) & (Member.last_name == "Smith") | (m.Member.age == 100) & (m.Member.last_name == "Smith")
).all() ).sort_by('age').all()
assert actual == [member1, member2, member3] assert actual == [member2, member1, member3]
def test_recursive_query_field_resolution(members): def test_recursive_query_field_resolution(members, m):
member1, _, _ = members member1, _, _ = members
member1.address.note = Note( member1.address.note = m.Note(
description="Weird house", created_on=datetime.datetime.now() description="Weird house", created_on=datetime.datetime.now()
) )
member1.save() member1.save()
actual = Member.find(Member.address.note.description == "Weird house").all() actual = m.Member.find(m.Member.address.note.description == "Weird house").all()
assert actual == [member1] assert actual == [member1]
member1.orders = [ member1.orders = [
Order( m.Order(
items=[Item(price=10.99, name="Ball")], items=[m.Item(price=10.99, name="Ball")],
total=10.99, total=10.99,
created_on=datetime.datetime.now(), created_on=datetime.datetime.now(),
) )
] ]
member1.save() member1.save()
actual = Member.find(Member.orders.items.name == "Ball").all() actual = m.Member.find(m.Member.orders.items.name == "Ball").all()
assert actual == [member1] assert actual == [member1]
assert actual[0].orders[0].items[0].name == "Ball" assert actual[0].orders[0].items[0].name == "Ball"
def test_full_text_search(members): def test_full_text_search(members, m):
member1, member2, _ = members member1, member2, _ = members
member1.update(bio="Hates sunsets, likes beaches") member1.update(bio="Hates sunsets, likes beaches")
member2.update(bio="Hates beaches, likes forests") member2.update(bio="Hates beaches, likes forests")
actual = Member.find(Member.bio % "beaches").all() actual = m.Member.find(m.Member.bio % "beaches").sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find(Member.bio % "forests").all() actual = m.Member.find(m.Member.bio % "forests").all()
assert actual == [member2] assert actual == [member2]
def test_tag_queries_boolean_logic(members): def test_tag_queries_boolean_logic(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find( actual = m.Member.find(
(Member.first_name == "Andrew") & (Member.last_name == "Brookins") (m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (Member.last_name == "Smith") | (m.Member.last_name == "Smith")
).all() ).sort_by('age').all()
assert actual == [member1, member3] assert actual == [member1, member3]
def test_tag_queries_punctuation(address): def test_tag_queries_punctuation(address, m):
member1 = Member( member1 = m.Member(
first_name="Andrew, the Michael", first_name="Andrew, the Michael",
last_name="St. Brookins-on-Pier", last_name="St. Brookins-on-Pier",
email="a|b@example.com", # NOTE: This string uses the TAG field separator. email="a|b@example.com", # NOTE: This string uses the TAG field separator.
@ -359,7 +366,7 @@ def test_tag_queries_punctuation(address):
) )
member1.save() member1.save()
member2 = Member( member2 = m.Member(
first_name="Bob", first_name="Bob",
last_name="the Villain", last_name="the Villain",
email="a|villain@example.com", # NOTE: This string uses the TAG field separator. email="a|villain@example.com", # NOTE: This string uses the TAG field separator.
@ -369,18 +376,18 @@ def test_tag_queries_punctuation(address):
) )
member2.save() member2.save()
assert Member.find(Member.first_name == "Andrew, the Michael").first() == member1 assert m.Member.find(m.Member.first_name == "Andrew, the Michael").first() == member1
assert Member.find(Member.last_name == "St. Brookins-on-Pier").first() == member1 assert m.Member.find(m.Member.last_name == "St. Brookins-on-Pier").first() == member1
# Notice that when we index and query multiple values that use the internal # Notice that when we index and query multiple values that use the internal
# TAG separator for single-value exact-match fields, like an indexed string, # TAG separator for single-value exact-match fields, like an indexed string,
# the queries will succeed. We apply a workaround that queries for the union # the queries will succeed. We apply a workaround that queries for the union
# of the two values separated by the tag separator. # of the two values separated by the tag separator.
assert Member.find(Member.email == "a|b@example.com").all() == [member1] assert m.Member.find(m.Member.email == "a|b@example.com").all() == [member1]
assert Member.find(Member.email == "a|villain@example.com").all() == [member2] assert m.Member.find(m.Member.email == "a|villain@example.com").all() == [member2]
def test_tag_queries_negation(members): def test_tag_queries_negation(members, m):
member1, member2, member3 = members member1, member2, member3 = members
""" """
@ -389,7 +396,7 @@ def test_tag_queries_negation(members):
Andrew Andrew
""" """
query = Member.find(~(Member.first_name == "Andrew")) query = m.Member.find(~(m.Member.first_name == "Andrew"))
assert query.all() == [member2] assert query.all() == [member2]
""" """
@ -402,8 +409,8 @@ def test_tag_queries_negation(members):
Brookins Brookins
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins") ~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
) )
assert query.all() == [member2] assert query.all() == [member2]
@ -420,9 +427,9 @@ def test_tag_queries_negation(members):
EQ EQ
Smith Smith
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") ~(m.Member.first_name == "Andrew")
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith")) & ((m.Member.last_name == "Brookins") | (m.Member.last_name == "Smith"))
) )
assert query.all() == [member2] assert query.all() == [member2]
@ -439,74 +446,75 @@ def test_tag_queries_negation(members):
EQ EQ
Smith Smith
""" """
query = Member.find( query = m.Member.find(
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins") ~(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
| (Member.last_name == "Smith") | (m.Member.last_name == "Smith")
) )
assert query.all() == [member2, member3] assert query.sort_by('age').all() == [member2, member3]
actual = Member.find( actual = m.Member.find(
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins") (m.Member.first_name == "Andrew") & ~(m.Member.last_name == "Brookins")
).all() ).all()
assert actual == [member3] assert actual == [member3]
def test_numeric_queries(members): def test_numeric_queries(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.age == 34).all() actual = m.Member.find(m.Member.age == 34).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age > 34).all() actual = m.Member.find(m.Member.age > 34).all()
assert actual == [member1, member3] assert actual == [member1, member3]
actual = Member.find(Member.age < 35).all() actual = m.Member.find(m.Member.age < 35).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age <= 34).all() actual = m.Member.find(m.Member.age <= 34).all()
assert actual == [member2] assert actual == [member2]
actual = Member.find(Member.age >= 100).all() actual = m.Member.find(m.Member.age >= 100).all()
assert actual == [member3] assert actual == [member3]
actual = Member.find(~(Member.age == 100)).all() actual = m.Member.find(~(m.Member.age == 100)).sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find(Member.age > 30, Member.age < 40).all() actual = m.Member.find(m.Member.age > 30, m.Member.age < 40).sort_by('age').all()
assert actual == [member1, member2] assert actual == [member2, member1]
actual = Member.find(Member.age != 34).all() actual = m.Member.find(m.Member.age != 34).sort_by('age').all()
assert actual == [member1, member3] assert actual == [member1, member3]
def test_sorting(members): def test_sorting(members, m):
member1, member2, member3 = members member1, member2, member3 = members
actual = Member.find(Member.age > 34).sort_by("age").all() actual = m.Member.find(m.Member.age > 34).sort_by("age").all()
assert actual == [member1, member3] assert actual == [member1, member3]
actual = Member.find(Member.age > 34).sort_by("-age").all() actual = m.Member.find(m.Member.age > 34).sort_by("-age").all()
assert actual == [member3, member1] assert actual == [member3, member1]
with pytest.raises(QueryNotSupportedError): with pytest.raises(QueryNotSupportedError):
# This field does not exist. # This field does not exist.
Member.find().sort_by("not-a-real-field").all() m.Member.find().sort_by("not-a-real-field").all()
with pytest.raises(QueryNotSupportedError): with pytest.raises(QueryNotSupportedError):
# This field is not sortable. # This field is not sortable.
Member.find().sort_by("join_date").all() m.Member.find().sort_by("join_date").all()
def test_not_found(): def test_not_found(m):
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
# This ID does not exist. # This ID does not exist.
Member.get(1000) m.Member.get(1000)
def test_list_field_limitations(): @pytest.mark.skip("Does not clean up after itself properly")
def test_list_field_limitations(m):
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class SortableTarotWitch(BaseJsonModel): class SortableTarotWitch(m.BaseJsonModel):
# We support indexing lists of strings for quality and membership # We support indexing lists of strings for quality and membership
# queries. Sorting is not supported, but is planned. # queries. Sorting is not supported, but is planned.
tarot_cards: List[str] = Field(index=True, sortable=True) tarot_cards: List[str] = Field(index=True, sortable=True)
@ -520,7 +528,7 @@ def test_list_field_limitations():
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class NumerologyWitch(BaseJsonModel): class NumerologyWitch(m.BaseJsonModel):
# We don't support indexing a list of numbers. Support for this # We don't support indexing a list of numbers. Support for this
# feature is To Be Determined. # feature is To Be Determined.
lucky_numbers: List[int] = Field(index=True) lucky_numbers: List[int] = Field(index=True)
@ -530,7 +538,7 @@ def test_list_field_limitations():
class ReadingWithPrice(EmbeddedJsonModel): class ReadingWithPrice(EmbeddedJsonModel):
gold_coins_charged: int = Field(index=True) gold_coins_charged: int = Field(index=True)
class TarotWitchWhoCharges(BaseJsonModel): class TarotWitchWhoCharges(m.BaseJsonModel):
tarot_cards: List[str] = Field(index=True) tarot_cards: List[str] = Field(index=True)
# The preview release does not support indexing numeric fields on models # The preview release does not support indexing numeric fields on models
@ -539,7 +547,7 @@ def test_list_field_limitations():
# The fate of this feature is To Be Determined. # The fate of this feature is To Be Determined.
readings: List[ReadingWithPrice] readings: List[ReadingWithPrice]
class TarotWitch(BaseJsonModel): class TarotWitch(m.BaseJsonModel):
# We support indexing lists of strings for quality and membership # We support indexing lists of strings for quality and membership
# queries. Sorting is not supported, but is planned. # queries. Sorting is not supported, but is planned.
tarot_cards: List[str] = Field(index=True) tarot_cards: List[str] = Field(index=True)
@ -555,8 +563,8 @@ def test_list_field_limitations():
assert actual == [witch] assert actual == [witch]
def test_schema(): def test_schema(m, key_prefix):
assert ( assert (
Member.redisearch_schema() m.Member.redisearch_schema()
== "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.bio AS bio TAG SEPARATOR | $.bio AS bio_fts TEXT $.address.pk AS address_pk TAG SEPARATOR | $.address.city AS address_city TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address_note_pk TAG SEPARATOR | $.address.note.description AS address_note_description TAG SEPARATOR | $.orders[*].pk AS orders_pk TAG SEPARATOR | $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |" == f"ON JSON PREFIX 1 {key_prefix}:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.bio AS bio TAG SEPARATOR | $.bio AS bio_fts TEXT $.address.pk AS address_pk TAG SEPARATOR | $.address.city AS address_city TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address_note_pk TAG SEPARATOR | $.address.note.description AS address_note_description TAG SEPARATOR | $.orders[*].pk AS orders_pk TAG SEPARATOR | $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |"
) )