Document some possible error messages

This commit is contained in:
Andrew Brookins 2021-11-26 15:25:18 -08:00
parent 60fbd09775
commit e4e3583006
5 changed files with 262 additions and 25 deletions

View file

@ -69,14 +69,6 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) exclude = set(exclude)
if custom_encoder:
if type(obj) in custom_encoder:
return custom_encoder[type(obj)](obj)
else:
for encoder_type, encoder in custom_encoder.items():
if isinstance(obj, encoder_type):
return encoder(obj)
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
encoder = getattr(obj.__config__, "json_encoders", {}) encoder = getattr(obj.__config__, "json_encoders", {})
if custom_encoder: if custom_encoder:
@ -154,9 +146,13 @@ def jsonable_encoder(
) )
return encoded_list return encoded_list
# This function originally called custom encoders here, if custom_encoder:
# which meant we couldn't override the encoder for many if type(obj) in custom_encoder:
# types hard-coded into this function (lists, etc.). return custom_encoder[type(obj)](obj)
else:
for encoder_type, encoder in custom_encoder.items():
if isinstance(obj, encoder_type):
return encoder(obj)
if type(obj) in ENCODERS_BY_TYPE: if type(obj) in ENCODERS_BY_TYPE:
return ENCODERS_BY_TYPE[type(obj)](obj) return ENCODERS_BY_TYPE[type(obj)](obj)

View file

@ -63,6 +63,8 @@ SINGLE_VALUE_TAG_FIELD_SEPARATOR = "|"
# multi-value field lookup, like a IN or NOT_IN. # multi-value field lookup, like a IN or NOT_IN.
DEFAULT_REDISEARCH_FIELD_SEPARATOR = "," DEFAULT_REDISEARCH_FIELD_SEPARATOR = ","
ERRORS_URL = "https://github.com/redis/redis-om-python/blob/main/docs/errors.md"
class RedisModelError(Exception): class RedisModelError(Exception):
"""Raised when a problem exists in the definition of a RedisModel.""" """Raised when a problem exists in the definition of a RedisModel."""
@ -288,6 +290,11 @@ class ExpressionProxy:
left=self.field, op=Operators.IN, right=other, parents=self.parents left=self.field, op=Operators.IN, right=other, parents=self.parents
) )
def __rshift__(self, other: Any) -> Expression:
return Expression(
left=self.field, op=Operators.NOT_IN, right=other, parents=self.parents
)
def __getattr__(self, item): def __getattr__(self, item):
if is_supported_container_type(self.field.outer_type_): if is_supported_container_type(self.field.outer_type_):
embedded_cls = get_args(self.field.outer_type_) embedded_cls = get_args(self.field.outer_type_)
@ -295,7 +302,7 @@ class ExpressionProxy:
raise QuerySyntaxError( raise QuerySyntaxError(
"In order to query on a list field, you must define " "In order to query on a list field, you must define "
"the contents of the list with a type annotation, like: " "the contents of the list with a type annotation, like: "
"orders: List[Order]. Docs: TODO" f"orders: List[Order]. Docs: {ERRORS_URL}#E1"
) )
embedded_cls = embedded_cls[0] embedded_cls = embedded_cls[0]
attr = getattr(embedded_cls, item) attr = getattr(embedded_cls, item)
@ -419,7 +426,7 @@ class FindQuery:
if not getattr(field_proxy.field.field_info, "sortable", False): if not getattr(field_proxy.field.field_info, "sortable", False):
raise QueryNotSupportedError( raise QueryNotSupportedError(
f"You tried sort by {field_name}, but {self.model} does " f"You tried sort by {field_name}, but {self.model} does "
"not define that field as sortable. See docs: XXX" f"not define that field as sortable. Docs: {ERRORS_URL}#E2"
) )
return sort_fields return sort_fields
@ -433,7 +440,7 @@ class FindQuery:
raise QuerySyntaxError( raise QuerySyntaxError(
f"You tried to do a full-text search on the field '{field.name}', " f"You tried to do a full-text search on the field '{field.name}', "
f"but the field is not indexed for full-text search. Use the " f"but the field is not indexed for full-text search. Use the "
f"full_text_search=True option. Docs: TODO" f"full_text_search=True option. Docs: {ERRORS_URL}#E3"
) )
return RediSearchFieldTypes.TEXT return RediSearchFieldTypes.TEXT
@ -460,7 +467,7 @@ class FindQuery:
elif container_type is not None: elif container_type is not None:
raise QuerySyntaxError( raise QuerySyntaxError(
"Only lists and tuples are supported for multi-value fields. " "Only lists and tuples are supported for multi-value fields. "
"See docs: TODO" f"Docs: {ERRORS_URL}#E4"
) )
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES): elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
# Index numeric Python types as NUMERIC fields, so we can support # Index numeric Python types as NUMERIC fields, so we can support
@ -519,8 +526,8 @@ class FindQuery:
else: else:
raise QueryNotSupportedError( raise QueryNotSupportedError(
"Only equals (=), not-equals (!=), and like() " "Only equals (=), not-equals (!=), and like() "
"comparisons are supported for TEXT fields. See " "comparisons are supported for TEXT fields. "
"docs: TODO." f"Docs: {ERRORS_URL}#E5"
) )
elif field_type is RediSearchFieldTypes.NUMERIC: elif field_type is RediSearchFieldTypes.NUMERIC:
if op is Operators.EQ: if op is Operators.EQ:
@ -571,7 +578,6 @@ class FindQuery:
value = escaper.escape(value) value = escaper.escape(value)
result += f"-(@{field_name}:{{{value}}})" result += f"-(@{field_name}:{{{value}}})"
elif op is Operators.IN: elif op is Operators.IN:
# TODO: Implement IN, test this...
expanded_value = cls.expand_tag_value(value) expanded_value = cls.expand_tag_value(value)
result += f"(@{field_name}:{{{expanded_value}}})" result += f"(@{field_name}:{{{expanded_value}}})"
elif op is Operators.NOT_IN: elif op is Operators.NOT_IN:
@ -651,13 +657,12 @@ class FindQuery:
if not field_info or not getattr(field_info, "index", None): if not field_info or not getattr(field_info, "index", None):
raise QueryNotSupportedError( raise QueryNotSupportedError(
f"You tried to query by a field ({field_name}) " f"You tried to query by a field ({field_name}) "
f"that isn't indexed. See docs: TODO" f"that isn't indexed. Docs: {ERRORS_URL}#E6"
) )
else: else:
raise QueryNotSupportedError( raise QueryNotSupportedError(
"A query expression should start with either a field " "A query expression should start with either a field "
"or an expression enclosed in parenthesis. See docs: " f"or an expression enclosed in parentheses. Docs: {ERRORS_URL}#E7"
"TODO"
) )
right = expression.right right = expression.right
@ -670,7 +675,7 @@ class FindQuery:
else: else:
raise QueryNotSupportedError( raise QueryNotSupportedError(
"You can only combine two query expressions with" "You can only combine two query expressions with"
"AND (&) or OR (|). See docs: TODO" f"AND (&) or OR (|). Docs: {ERRORS_URL}#E8"
) )
if isinstance(right, NegatedExpression): if isinstance(right, NegatedExpression):

228
docs/errors.md Normal file
View file

@ -0,0 +1,228 @@
# Errors
This page lists errors that Redis OM might generate while you're using it, with more context about the error.
## E1
> In order to query on a list field, you must define the contents of the list with a type annotation, like: orders: List[Order].
You will see this error if you try to use an "IN" query, e.g., `await TarotWitch.find(TarotWitch.tarot_cards << "death").all()`, on a field that is not a list.
In this example, `TarotWitch.tarot_cards` is a list, so the query works:
```python
from typing import List
from redis_om import JsonModel, Field
class TarotWitch(JsonModel):
tarot_cards: List[str] = Field(index=True)
```
But if `tarot_cards` was _not_ a list, trying to query with `<<` would have resulted in this error.
## E2
> You tried sort by {field_name}, but {self.model} does not define that field as sortable.
You tried to sort query results by a field that is not sortable. Here is how you mark a field as sortable:
```python
from typing import List
from redis_om import JsonModel, Field
class Member(JsonModel):
age: int = Field(index=True, sortable=True)
```
**NOTE:** Only an indexed field can be sortable.
## E3
>You tried to do a full-text search on the field '{field.name}', but the field is not indexed for full-text search. Use the full_text_search=True option.
You can make a full-text search with the module (`%`) operator. Such a query looks like this:
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
bio: str = Field(index=True, full_text_search=True, default="")
Member.find(Member.bio % "beaches").all()
```
If you see this error, it means that the field you are querying (`bio` in the example) is not indexed for full-text search. Make sure you're marking the field both `index=True` and `full_text_search=True`, as in the example.
## E4
> Only lists and tuples are supported for multi-value fields.
This means that you marked a field as `index=True`, but the field is not a type that Redis OM can actually index.
Specifically, you probably used a _subscripted_ annotation, like `Dict[str, str]`. The only subscripted types that OM can index are `List` and `Tuple`.
## E5
> Only equals (=), not-equals (!=), and like() comparisons are supported for TEXT fields.
You are querying a field you marked as indexed for full-text search. You can only query such fields with the operators for equality (==), non-equality (!=), and like `(%)`.
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
bio: str = Field(index=True, full_text_search=True, default="")
# Equality
Member.find(Member.bio == "Programmer").all()
# Non-equality
Member.find(Member.bio != "Programmer").all()
# Like (full-text search). This stems "programming"
# to find any matching terms with the same stem,
# "program".
Member.find(Member.bio % "programming").all()
```
## E6
> You tried to query by a field ({field_name}) that isn't indexed.
You wrote a query using a model field that you did not make indexed. You can only query indexed fields. Here is example code that would generate this error:
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
first_name: str
bio: str = Field(index=True, full_text_search=True, default="")
# Raises a QueryNotSupportedError because we did not make
# `first_name` indexed!
Member.find(Member.first_name == "Andrew").all()
```
Fix this by making the field indexed:
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
first_name: str = Field(index=True)
bio: str = Field(index=True, full_text_search=True, default="")
# Raises a QueryNotSupportedError because we did not make
# `first_name` indexed!
Member.find(Member.first_name == "Andrew").all()
```
## E7
> A query expression should start with either a field or an expression enclosed in parentheses.
We got confused trying to parse your query expression. It's not you, it's us! Some code examples might help...
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
first_name: str = Field(index=True)
last_name: str = Field(index=True)
# Queries with a single operator are usually simple:
Member.find(Member.first_name == "Andrew").all()
# If you want to add multiple conditions, you can AND
# them together by including the conditions one after
# another as arguments.
Member.find(Member.first_name=="Andrew",
Member.last_name=="Brookins").all()
# Alternatively, you can separate the conditions with
# parenthesis and use an explicit AND.
Member.find(
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
).all()
# You can't use `!` to say NOT. Instead, use `~`.
Member.find(
(Member.first_name == "Andrew") &
~(Member.last_name == "Brookins") # <- Notice, this one is NOT now!
).all()
# Parenthesis are key to building more complex queries,
# like this one.
Member.find(
~(Member.first_name == "Andrew")
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
).all()
# If you're confused about how Redis OM interprets a query,
# use the `tree()` method to visualize the expression tree
# for a `FindQuery`.
query = Member.find(
~(Member.first_name == "Andrew")
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
)
print(query.expression.tree)
"""
┌first_name
┌NOT EQ┤
| └Andrew
AND┤
| ┌last_name
| ┌EQ┤
| | └Brookins
└OR┤
| ┌last_name
└EQ┤
└Smith
"""
```
## E8
> You can only combine two query expressions with AND (&) or OR (|).
The only two operators you can use to combine expressions in a query
are `&` and `|`. You may have accidentally used another operator,
or Redis OM might be confused. Make sure you are using parentheses
to organize your query expressions.
If you are trying to use "NOT," you can do that by prefixing a query
with the `~` operator, like this:
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
first_name: str = Field(index=True)
last_name: str = Field(index=True)
# Find people who are not named Andrew.
Member.find(~(Member.first_name == "Andrew")).all()
```
Note that this form requires parenthesis around the expression
that you are "negating." Of course, this example makes more sense
with `!=`:
```python
from redis_om import JsonModel, Field
class Member(JsonModel):
first_name: str = Field(index=True)
last_name: str = Field(index=True)
# Find people who are not named Andrew.
Member.find(Member.first_name != "Andrew").all()
```
Still, `~` is useful to negate groups of expressions
surrounded by parentheses.

View file

@ -1,9 +1,11 @@
# type: ignore
import abc import abc
import dataclasses import dataclasses
import datetime import datetime
import decimal import decimal
from collections import namedtuple from collections import namedtuple
from typing import Optional, Dict, Set, List from typing import Dict, List, Optional, Set
from unittest import mock from unittest import mock
import pytest import pytest
@ -372,24 +374,28 @@ def test_raises_error_with_dataclasses(m):
address_line_1: str address_line_1: str
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class InvalidMember(m.BaseHashModel): class InvalidMember(m.BaseHashModel):
address: Address address: Address
def test_raises_error_with_dicts(m): def test_raises_error_with_dicts(m):
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class InvalidMember(m.BaseHashModel): class InvalidMember(m.BaseHashModel):
address: Dict[str, str] address: Dict[str, str]
def test_raises_error_with_sets(m): def test_raises_error_with_sets(m):
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class InvalidMember(m.BaseHashModel): class InvalidMember(m.BaseHashModel):
friend_ids: Set[str] friend_ids: Set[str]
def test_raises_error_with_lists(m): def test_raises_error_with_lists(m):
with pytest.raises(RedisModelError): with pytest.raises(RedisModelError):
class InvalidMember(m.BaseHashModel): class InvalidMember(m.BaseHashModel):
friend_ids: List[str] friend_ids: List[str]

View file

@ -1,9 +1,11 @@
# type: ignore
import abc import abc
import dataclasses import dataclasses
import datetime import datetime
import decimal import decimal
from collections import namedtuple from collections import namedtuple
from typing import List, Optional, Set, Dict from typing import Dict, List, Optional, Set
from unittest import mock from unittest import mock
import pytest import pytest
@ -687,7 +689,7 @@ async def test_allows_and_serializes_dicts(m):
member2 = await ValidMember.get(member.pk) member2 = await ValidMember.get(member.pk)
assert member2 == member assert member2 == member
assert member2.address['address_line_1'] == "hey" assert member2.address["address_line_1"] == "hey"
@pytest.mark.asyncio @pytest.mark.asyncio