Document some possible error messages
This commit is contained in:
parent
60fbd09775
commit
e4e3583006
5 changed files with 262 additions and 25 deletions
|
@ -69,14 +69,6 @@ def jsonable_encoder(
|
|||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||
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):
|
||||
encoder = getattr(obj.__config__, "json_encoders", {})
|
||||
if custom_encoder:
|
||||
|
@ -154,9 +146,13 @@ def jsonable_encoder(
|
|||
)
|
||||
return encoded_list
|
||||
|
||||
# This function originally called custom encoders here,
|
||||
# which meant we couldn't override the encoder for many
|
||||
# types hard-coded into this function (lists, etc.).
|
||||
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 type(obj) in ENCODERS_BY_TYPE:
|
||||
return ENCODERS_BY_TYPE[type(obj)](obj)
|
||||
|
|
|
@ -63,6 +63,8 @@ SINGLE_VALUE_TAG_FIELD_SEPARATOR = "|"
|
|||
# multi-value field lookup, like a IN or NOT_IN.
|
||||
DEFAULT_REDISEARCH_FIELD_SEPARATOR = ","
|
||||
|
||||
ERRORS_URL = "https://github.com/redis/redis-om-python/blob/main/docs/errors.md"
|
||||
|
||||
|
||||
class RedisModelError(Exception):
|
||||
"""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
|
||||
)
|
||||
|
||||
def __rshift__(self, other: Any) -> Expression:
|
||||
return Expression(
|
||||
left=self.field, op=Operators.NOT_IN, right=other, parents=self.parents
|
||||
)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if is_supported_container_type(self.field.outer_type_):
|
||||
embedded_cls = get_args(self.field.outer_type_)
|
||||
|
@ -295,7 +302,7 @@ class ExpressionProxy:
|
|||
raise QuerySyntaxError(
|
||||
"In order to query on a list field, you must define "
|
||||
"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]
|
||||
attr = getattr(embedded_cls, item)
|
||||
|
@ -419,7 +426,7 @@ class FindQuery:
|
|||
if not getattr(field_proxy.field.field_info, "sortable", False):
|
||||
raise QueryNotSupportedError(
|
||||
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
|
||||
|
||||
|
@ -433,7 +440,7 @@ class FindQuery:
|
|||
raise QuerySyntaxError(
|
||||
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"full_text_search=True option. Docs: TODO"
|
||||
f"full_text_search=True option. Docs: {ERRORS_URL}#E3"
|
||||
)
|
||||
return RediSearchFieldTypes.TEXT
|
||||
|
||||
|
@ -460,7 +467,7 @@ class FindQuery:
|
|||
elif container_type is not None:
|
||||
raise QuerySyntaxError(
|
||||
"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):
|
||||
# Index numeric Python types as NUMERIC fields, so we can support
|
||||
|
@ -519,8 +526,8 @@ class FindQuery:
|
|||
else:
|
||||
raise QueryNotSupportedError(
|
||||
"Only equals (=), not-equals (!=), and like() "
|
||||
"comparisons are supported for TEXT fields. See "
|
||||
"docs: TODO."
|
||||
"comparisons are supported for TEXT fields. "
|
||||
f"Docs: {ERRORS_URL}#E5"
|
||||
)
|
||||
elif field_type is RediSearchFieldTypes.NUMERIC:
|
||||
if op is Operators.EQ:
|
||||
|
@ -571,7 +578,6 @@ class FindQuery:
|
|||
value = escaper.escape(value)
|
||||
result += f"-(@{field_name}:{{{value}}})"
|
||||
elif op is Operators.IN:
|
||||
# TODO: Implement IN, test this...
|
||||
expanded_value = cls.expand_tag_value(value)
|
||||
result += f"(@{field_name}:{{{expanded_value}}})"
|
||||
elif op is Operators.NOT_IN:
|
||||
|
@ -651,13 +657,12 @@ class FindQuery:
|
|||
if not field_info or not getattr(field_info, "index", None):
|
||||
raise QueryNotSupportedError(
|
||||
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:
|
||||
raise QueryNotSupportedError(
|
||||
"A query expression should start with either a field "
|
||||
"or an expression enclosed in parenthesis. See docs: "
|
||||
"TODO"
|
||||
f"or an expression enclosed in parentheses. Docs: {ERRORS_URL}#E7"
|
||||
)
|
||||
|
||||
right = expression.right
|
||||
|
@ -670,7 +675,7 @@ class FindQuery:
|
|||
else:
|
||||
raise QueryNotSupportedError(
|
||||
"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):
|
||||
|
|
228
docs/errors.md
Normal file
228
docs/errors.md
Normal 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.
|
|
@ -1,9 +1,11 @@
|
|||
# type: ignore
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
import datetime
|
||||
import decimal
|
||||
from collections import namedtuple
|
||||
from typing import Optional, Dict, Set, List
|
||||
from typing import Dict, List, Optional, Set
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
@ -372,24 +374,28 @@ def test_raises_error_with_dataclasses(m):
|
|||
address_line_1: str
|
||||
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class InvalidMember(m.BaseHashModel):
|
||||
address: Address
|
||||
|
||||
|
||||
def test_raises_error_with_dicts(m):
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class InvalidMember(m.BaseHashModel):
|
||||
address: Dict[str, str]
|
||||
|
||||
|
||||
def test_raises_error_with_sets(m):
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class InvalidMember(m.BaseHashModel):
|
||||
friend_ids: Set[str]
|
||||
|
||||
|
||||
def test_raises_error_with_lists(m):
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class InvalidMember(m.BaseHashModel):
|
||||
friend_ids: List[str]
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# type: ignore
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
import datetime
|
||||
import decimal
|
||||
from collections import namedtuple
|
||||
from typing import List, Optional, Set, Dict
|
||||
from typing import Dict, List, Optional, Set
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
@ -687,7 +689,7 @@ async def test_allows_and_serializes_dicts(m):
|
|||
|
||||
member2 = await ValidMember.get(member.pk)
|
||||
assert member2 == member
|
||||
assert member2.address['address_line_1'] == "hey"
|
||||
assert member2.address["address_line_1"] == "hey"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
Loading…
Reference in a new issue