diff --git a/dj_rql/constants.py b/dj_rql/constants.py index 9a74f37..e559862 100644 --- a/dj_rql/constants.py +++ b/dj_rql/constants.py @@ -1,5 +1,5 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from django.db import models @@ -190,7 +190,7 @@ class LogicalOperators: @staticmethod def get_grammar_key(key): - return '{}_op'.format(key) + return '{0}_op'.format(key) class SearchOperators: diff --git a/dj_rql/drf/__init__.py b/dj_rql/drf/__init__.py index 7c007aa..43ec940 100644 --- a/dj_rql/drf/__init__.py +++ b/dj_rql/drf/__init__.py @@ -1,10 +1,11 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from dj_rql.drf.backend import FilterCache, RQLFilterBackend -from dj_rql.drf.paginations import RQLLimitOffsetPagination, RQLContentRangeLimitOffsetPagination from dj_rql.drf._utils import get_query +from dj_rql.drf.backend import FilterCache, RQLFilterBackend +from dj_rql.drf.paginations import RQLContentRangeLimitOffsetPagination, RQLLimitOffsetPagination + __all__ = [ 'get_query', diff --git a/dj_rql/drf/backend.py b/dj_rql/drf/backend.py index 4659e98..35cb6dd 100644 --- a/dj_rql/drf/backend.py +++ b/dj_rql/drf/backend.py @@ -1,11 +1,11 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from rest_framework.filters import BaseFilterBackend - from dj_rql.drf._utils import get_query +from rest_framework.filters import BaseFilterBackend + class FilterCache: CACHE = {} @@ -61,7 +61,7 @@ def get_query(cls, filter_instance, request, view): @staticmethod def _get_filter_instance(filter_class, queryset, view): - qual_name = '{}.{}'.format(view.basename, filter_class.__name__) + qual_name = '{0}.{1}'.format(view.basename, filter_class.__name__) filter_instance = FilterCache.CACHE.get(qual_name) if filter_instance: diff --git a/dj_rql/drf/compat.py b/dj_rql/drf/compat.py index 8aae7f3..587f09e 100644 --- a/dj_rql/drf/compat.py +++ b/dj_rql/drf/compat.py @@ -7,17 +7,18 @@ from dj_rql.constants import ( ComparisonOperators as CO, DjangoLookups as DJL, - FilterTypes, RQL_NULL, - SearchOperators as SO, + FilterTypes, RQL_ANY_SYMBOL, RQL_FALSE, RQL_LIMIT_PARAM, + RQL_NULL, RQL_OFFSET_PARAM, RQL_ORDERING_OPERATOR, RQL_TRUE, + SearchOperators as SO, ) -from dj_rql.drf.backend import RQLFilterBackend from dj_rql.drf._utils import get_query +from dj_rql.drf.backend import RQLFilterBackend from dj_rql.exceptions import RQLFilterParsingError @@ -178,10 +179,10 @@ def _get_one_filter_value_pair(cls, filter_instance, filter_name, value): return if filter_name in (RQL_LIMIT_PARAM, RQL_OFFSET_PARAM): - return '{}={}'.format(filter_name, value) + return '{0}={1}'.format(filter_name, value) if filter_name in cls.RESERVED_ORDERING_WORDS: - return '{}({})'.format(RQL_ORDERING_OPERATOR, value) + return '{0}({1})'.format(RQL_ORDERING_OPERATOR, value) f_item = filter_instance.get_filter_base_item(filter_name) is_nc_item = f_item and (not f_item.get('custom', False)) @@ -189,7 +190,7 @@ def _get_one_filter_value_pair(cls, filter_instance, filter_name, value): value = cls._convert_bool_value(value) if not cls._is_old_style_filter(filter_name): - return '{}={}'.format(filter_name, cls._add_quotes_to_value(value)) + return '{0}={1}'.format(filter_name, cls._add_quotes_to_value(value)) return cls._convert_filter_to_rql(filter_name, value) @@ -202,13 +203,13 @@ def _convert_filter_to_rql(cls, filter_name, value): filter_base, lookup = cls._get_filter_and_lookup(filter_name) if lookup == DJL.IN: - return 'in({},({}))'.format( + return 'in({0},({1}))'.format( filter_base, ','.join(cls._add_quotes_to_value(v) for v in value.split(',') if v), ) if lookup == DJL.NULL: operator = CO.EQ if cls._convert_bool_value(value) == 'true' else CO.NE - return '{}={}={}'.format(filter_base, operator, RQL_NULL) + return '{0}={1}={2}'.format(filter_base, operator, RQL_NULL) if lookup in (DJL.GT, DJL.GTE, DJL.LT, DJL.LTE): if lookup == DJL.GTE: @@ -217,18 +218,19 @@ def _convert_filter_to_rql(cls, filter_name, value): operator = CO.LE else: operator = lookup - return '{}={}={}'.format(filter_base, operator, value) + return '{0}={1}={2}'.format(filter_base, operator, value) operator = SO.I_LIKE if lookup[0] == 'i' else SO.LIKE - if lookup in (DJL.CONTAINS, DJL.I_CONTAINS, DJL.ENDSWITH, DJL.I_ENDSWITH) and \ - value[0] != RQL_ANY_SYMBOL: + + lookups = (DJL.CONTAINS, DJL.I_CONTAINS, DJL.ENDSWITH, DJL.I_ENDSWITH) + if lookup in lookups and value[0] != RQL_ANY_SYMBOL: value = RQL_ANY_SYMBOL + value - if lookup in (DJL.CONTAINS, DJL.I_CONTAINS, DJL.STARTSWITH, DJL.I_STARTSWITH) and \ - value[-1] != RQL_ANY_SYMBOL: + lookups = (DJL.CONTAINS, DJL.I_CONTAINS, DJL.STARTSWITH, DJL.I_STARTSWITH) + if lookup in lookups and value[-1] != RQL_ANY_SYMBOL: value += RQL_ANY_SYMBOL - return '{}({},{})'.format(operator, filter_base, cls._add_quotes_to_value(value)) + return '{0}({1},{2})'.format(operator, filter_base, cls._add_quotes_to_value(value)) @classmethod def _convert_bool_value(cls, value): @@ -243,7 +245,7 @@ def _convert_bool_value(cls, value): def _add_quotes_to_value(cls, value): for quote in ('"', "'"): if quote not in value: - return '{q}{}{q}'.format(value, q=quote) + return '{q}{0}{q}'.format(value, q=quote) cls._conversion_error() @@ -262,7 +264,7 @@ def _get_filters_similar_to_old_syntax(cls, filter_instance): if cls._is_old_style_filter(filter_name): similar_to_old_syntax_filters.add(filter_name) - setattr(filter_instance, 'old_syntax_filters', similar_to_old_syntax_filters) + filter_instance.old_syntax_filters = similar_to_old_syntax_filters return similar_to_old_syntax_filters @classmethod diff --git a/dj_rql/drf/paginations.py b/dj_rql/drf/paginations.py index 6e9e2a7..ab455dd 100644 --- a/dj_rql/drf/paginations.py +++ b/dj_rql/drf/paginations.py @@ -1,16 +1,17 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from lark.exceptions import LarkError -from rest_framework.pagination import LimitOffsetPagination, _positive_int -from rest_framework.response import Response - from dj_rql.drf._utils import get_query from dj_rql.exceptions import RQLFilterParsingError from dj_rql.parser import RQLParser from dj_rql.transformer import RQLLimitOffsetTransformer +from lark.exceptions import LarkError + +from rest_framework.pagination import LimitOffsetPagination, _positive_int +from rest_framework.response import Response + class RQLLimitOffsetPagination(LimitOffsetPagination): """ RQL limit offset pagination. """ @@ -26,7 +27,7 @@ def get_paginated_response_schema(self, schema): def paginate_queryset(self, queryset, request, view=None): rql_ast = None try: - rql_ast = getattr(request, 'rql_ast') + rql_ast = request.rql_ast except AttributeError: query = get_query(request) if query: @@ -76,7 +77,7 @@ class RQLContentRangeLimitOffsetPagination(RQLLimitOffsetPagination): def get_paginated_response(self, data): length = len(data) - 1 if data else 0 - content_range = "items {}-{}/{}".format( + content_range = 'items {0}-{1}/{2}'.format( self.offset, self.offset + length, self.count, ) - return Response(data, headers={"Content-Range": content_range}) + return Response(data, headers={'Content-Range': content_range}) diff --git a/dj_rql/drf/serializers.py b/dj_rql/drf/serializers.py index 04272d3..f5c1449 100644 --- a/dj_rql/drf/serializers.py +++ b/dj_rql/drf/serializers.py @@ -1,5 +1,5 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from collections import OrderedDict @@ -14,7 +14,7 @@ def to_representation(self, instance): def apply_rql_select(self): rql_select = self._get_field_rql_select(self) - setattr(self, 'rql_select', rql_select) + self.rql_select = rql_select deeper_rql_select = self._get_deeper_rql_select() for field_name, is_included in rql_select['select'].items(): @@ -47,8 +47,12 @@ def _get_deeper_rql_select(self): return self._deeper_rql_select def _get_field_rql_select(self, field): - if field.parent and getattr(field.parent, 'many', False) \ - and isinstance(field, field.parent.child.__class__): + take_parent = bool( + field.parent and getattr(field.parent, 'many', False) and isinstance( + field, field.parent.child.__class__, + ), + ) + if take_parent: rql_field = field.parent else: rql_field = field diff --git a/dj_rql/filter_cls.py b/dj_rql/filter_cls.py index c568c3f..8885445 100644 --- a/dj_rql/filter_cls.py +++ b/dj_rql/filter_cls.py @@ -6,10 +6,6 @@ from datetime import datetime from uuid import uuid4 -from django.db.models import Q -from django.utils.dateparse import parse_date, parse_datetime -from lark.exceptions import LarkError - from dj_rql._dataclasses import FilterArgs, OptimizationArgs from dj_rql.constants import ( ComparisonOperators, @@ -17,24 +13,30 @@ FilterLookups, FilterTypes, ListOperators, - SearchOperators, RESERVED_FILTER_NAMES, RQL_ANY_SYMBOL, RQL_EMPTY, RQL_FALSE, RQL_MINUS, - RQL_PLUS, RQL_NULL, + RQL_PLUS, RQL_SEARCH_PARAM, RQL_TRUE, SUPPORTED_FIELD_TYPES, + SearchOperators, ) -from dj_rql.exceptions import RQLFilterLookupError, RQLFilterValueError, RQLFilterParsingError +from dj_rql.exceptions import RQLFilterLookupError, RQLFilterParsingError, RQLFilterValueError from dj_rql.openapi import RQLFilterClassSpecification from dj_rql.parser import RQLParser from dj_rql.qs import Annotation from dj_rql.transformer import RQLToDjangoORMTransformer +from django.db.models import Q +from django.utils.dateparse import parse_date, parse_datetime + +from lark.exceptions import LarkError + + iterable_types = (list, tuple) @@ -47,7 +49,7 @@ class RQLFilterClass: FILTERS = None """A list or tuple of filters definitions.""" - EXTENDED_SEARCH_ORM_ROUTES = tuple() + EXTENDED_SEARCH_ORM_ROUTES = () DISTINCT = False """If True, a `SELECT DISTINCT` will always be executed.""" @@ -72,10 +74,12 @@ def __init__(self, queryset, instance=None): def _default_init(self): assert self.MODEL, 'Model must be set for Filter Class.' - assert isinstance(self.FILTERS, iterable_types) and self.FILTERS, \ - 'List of filters must be set for Filter Class.' - assert isinstance(self.EXTENDED_SEARCH_ORM_ROUTES, iterable_types), \ - 'Extended search ORM routes must be iterable.' + + e = 'List of filters must be set for Filter Class.' + assert isinstance(self.FILTERS, iterable_types) and self.FILTERS, e + + e = 'Extended search ORM routes must be iterable.' + assert isinstance(self.EXTENDED_SEARCH_ORM_ROUTES, iterable_types), e self.filters = {} self.ordering_filters = set() @@ -106,7 +110,7 @@ def build_q_for_custom_filter(self, data): :rtype: django.db.models.Q """ raise RQLFilterParsingError(details={ - 'error': 'Filter logic is not implemented: {}.'.format(data.filter_name), + 'error': 'Filter logic is not implemented: {0}.'.format(data.filter_name), }) def build_name_for_custom_ordering(self, filter_name): @@ -117,7 +121,7 @@ def build_name_for_custom_ordering(self, filter_name): :rtype: str """ raise RQLFilterParsingError(details={ - 'error': 'Ordering logic is not implemented: {}.'.format(filter_name), + 'error': 'Ordering logic is not implemented: {0}.'.format(filter_name), }) def optimize_field(self, data): @@ -196,17 +200,17 @@ def apply_filters(self, query, request=None, view=None): qs = qs.distinct() if request: - setattr(request, 'rql_ast', rql_ast) + request.rql_ast = rql_ast if self.SELECT: select_data = self._build_select_data(select_filters) qs = self._apply_optimizations(qs, select_data) if request: - setattr(request, 'rql_select', { + request.rql_select = { 'depth': 0, 'select': select_data, - }) + } self.queryset = qs @@ -238,9 +242,11 @@ def build_q_for_filter(self, data): filter_item = self.filters[filter_name] available_lookups = base_item.get('lookups', set()) if list_operator: - list_filter_lookup = FilterLookups.IN \ - if list_operator == ListOperators.IN \ - else FilterLookups.OUT + if list_operator == ListOperators.IN: + list_filter_lookup = FilterLookups.IN + else: + list_filter_lookup = FilterLookups.OUT + if list_filter_lookup not in available_lookups: raise RQLFilterLookupError(**self._get_error_details( filter_name, list_filter_lookup, str_value, @@ -324,10 +330,10 @@ def _build_select_data_for_inclusion(self, filter_name, inclusions, exclusions): for index, part in enumerate(filter_name_parts): if part not in select_tree: raise RQLFilterParsingError(details={ - 'error': 'Bad select filter: {}.'.format(filter_name), + 'error': 'Bad select filter: {0}.'.format(filter_name), }) - current_part = '{}.{}'.format(parent_parts, part) if parent_parts else part + current_part = '{0}.{1}'.format(parent_parts, part) if parent_parts else part inclusions.add(current_part) select_data[current_part] = True @@ -340,16 +346,15 @@ def _build_select_data_for_inclusion(self, filter_name, inclusions, exclusions): for neighbour_part in select_tree.keys(): if neighbour_part != part: exclusions.add( - '{}.{}'.format(parent_parts, neighbour_part), + '{0}.{1}'.format(parent_parts, neighbour_part), ) return select_data def _build_select_data_for_exclusions(self, exclude_select, inclusions, exclusions): select_data = {} - real_exclude_select = exclude_select \ - .union(self.default_exclusions - inclusions) \ - .union(exclusions - inclusions) + real_exclude_select = exclude_select.union(self.default_exclusions - inclusions) + real_exclude_select = real_exclude_select.union(exclusions - inclusions) for filter_name in real_exclude_select: if filter_name in inclusions: @@ -364,7 +369,7 @@ def _build_select_data_for_exclusions(self, exclude_select, inclusions, exclusio for index, part in enumerate(filter_name_parts): if part not in select_tree: raise RQLFilterParsingError(details={ - 'error': 'Bad select filter: -{}.'.format(filter_name), + 'error': 'Bad select filter: -{0}.'.format(filter_name), }) if index != last_filter_name_part_index: @@ -377,11 +382,9 @@ def _build_select_data_for_exclusions(self, exclude_select, inclusions, exclusio @staticmethod def _prepare_selects(select): include_select, exclude_select = [], set() - for select_prop in select: - is_included = (select_prop[0] != RQL_MINUS) - filter_name = select_prop[1:] \ - if select_prop[0] in (RQL_MINUS, RQL_PLUS) \ - else select_prop + for s_prop in select: + is_included = (s_prop[0] != RQL_MINUS) + filter_name = s_prop[1:] if s_prop[0] in (RQL_MINUS, RQL_PLUS) else s_prop if is_included: include_select.append(filter_name) @@ -393,7 +396,7 @@ def _prepare_selects(select): def _build_q_for_search(self, operator, str_value): if operator != ComparisonOperators.EQ: raise RQLFilterParsingError(details={ - 'error': 'Bad search filter: {}.'.format(operator), + 'error': 'Bad search filter: {0}.'.format(operator), }) unquoted_value = self.remove_quotes(str_value) @@ -487,7 +490,7 @@ def _apply_ordering(self, qs, properties): filter_name, sign = self._get_filter_name_with_sign_for_ordering(prop) if filter_name not in self.ordering_filters: raise RQLFilterParsingError(details={ - 'error': 'Bad ordering filter: {}.'.format(filter_name), + 'error': 'Bad ordering filter: {0}.'.format(filter_name), }) filters = self.filters[filter_name] @@ -498,7 +501,7 @@ def _apply_ordering(self, qs, properties): self._is_distinct = True ordering_name = self._get_filter_ordering_name(filter_item, filter_name) - ordering_fields.append('{}{}'.format(sign, ordering_name)) + ordering_fields.append('{0}{1}'.format(sign, ordering_name)) return qs.order_by(*ordering_fields) @@ -530,8 +533,8 @@ def _build_filters(self, filters, filter_route='', orm_route='', for item in filters: if isinstance(item, str): - field_filter_route = '{}{}'.format(filter_route, item) - field_orm_route = '{}{}'.format(orm_route, item) + field_filter_route = '{0}{1}'.format(filter_route, item) + field_orm_route = '{0}{1}'.format(orm_route, item) field = self._get_field(_model, item) self._add_filter_item( field_filter_route, self._build_mapped_item(field, field_orm_route), @@ -541,13 +544,15 @@ def _build_filters(self, filters, filter_route='', orm_route='', if 'namespace' in item: for option in ('filter', 'dynamic', 'custom'): - assert option not in item, \ - "{}: '{}' is not supported by namespaces.".format(item['namespace'], option) + e = "{0}: '{1}' is not supported by namespaces.".format( + item['namespace'], option, + ) + assert option not in item, e namespace = item['namespace'] - related_filter_route = '{}{}'.format(filter_route, namespace) + related_filter_route = '{0}{1}'.format(filter_route, namespace) orm_field_name = item.get('source', namespace) - related_orm_route = '{}{}__'.format(orm_route, orm_field_name) + related_orm_route = '{0}{1}__'.format(orm_route, orm_field_name) related_model = self._get_field( _model, orm_field_name, get_related=True, @@ -570,7 +575,7 @@ def _build_filters(self, filters, filter_route='', orm_route='', assert 'filter' in item, "All extended filters must have set 'filter' set." filter_name = item['filter'] - field_filter_route = '{}{}'.format(filter_route, filter_name) + field_filter_route = '{0}{1}'.format(filter_route, filter_name) self._fill_select_tree( filter_name, field_filter_route, select_tree, @@ -601,14 +606,14 @@ def _build_filters_for_common_item(self, item, field_filter_route, orm_route, or if 'sources' in item: items = [] for source in item['sources']: - full_orm_route = '{}{}'.format(orm_route, source) + full_orm_route = '{0}{1}'.format(orm_route, source) field = field or self._get_field(orm_model, source) items.append(self._build_mapped_item(field, full_orm_route, **kwargs)) self._check_search(item, field_filter_route, field) else: orm_field_name = item.get('source', filter_name) - full_orm_route = '{}{}'.format(orm_route, orm_field_name) + full_orm_route = '{0}{1}'.format(orm_route, orm_field_name) field = field or self._get_field(orm_model, orm_field_name) items = self._build_mapped_item(field, full_orm_route, **kwargs) self._check_search(item, field_filter_route, field) @@ -654,8 +659,9 @@ def _fill_select_tree(self, f_name, full_f_name, select_tree, return current_select_tree, parent_qs if not qs else changed_qs def _add_filter_item(self, filter_name, item): - assert filter_name not in RESERVED_FILTER_NAMES, \ - "'{}' is a reserved filter name.".format(filter_name) + e = "'{0}' is a reserved filter name.".format(filter_name) + assert filter_name not in RESERVED_FILTER_NAMES, e + self.filters[filter_name] = item def _register_ordering_and_search(self, item, field_filter_route): @@ -694,8 +700,9 @@ def _get_field(cls, base_model, field_name, get_related=False): for index, part in enumerate(field_name_parts, start=1): current_field = cls._get_model_field(current_model, part) if index == field_name_parts_length: - assert get_related or isinstance(current_field, SUPPORTED_FIELD_TYPES), \ - 'Unsupported field type: {}.'.format(field_name) + e = 'Unsupported field type: {0}.'.format(field_name) + assert get_related or isinstance(current_field, SUPPORTED_FIELD_TYPES), e + return current_field current_model = current_field.related_model @@ -804,7 +811,7 @@ def _get_searching_django_lookup(cls, filter_lookup, str_value): elif sep_count == 2 and val[0] == RQL_ANY_SYMBOL == val[-1]: pattern = 'CONTAINS' - return getattr(DjangoLookups, '{}{}'.format(prefix, pattern)) + return getattr(DjangoLookups, '{0}{1}'.format(prefix, pattern)) @classmethod def _get_typed_value(cls, filter_name, filter_lookup, str_value, django_field, @@ -827,7 +834,7 @@ def _get_typed_value(cls, filter_name, filter_lookup, str_value, django_field, def _reflect_like_value(cls, str_value): star_replacer = uuid4().hex return '\\'.join( - v.replace(r'\{}'.format(RQL_ANY_SYMBOL), star_replacer) + v.replace(r'\{0}'.format(RQL_ANY_SYMBOL), star_replacer) for v in cls.remove_quotes(str_value).split(r'\\') ), star_replacer @@ -835,7 +842,7 @@ def _reflect_like_value(cls, str_value): def _get_searching_typed_value(cls, django_lookup, str_value): val, star_replacer = cls._reflect_like_value(str_value) - if '{}{}'.format(RQL_ANY_SYMBOL, RQL_ANY_SYMBOL) in val: + if '{0}{1}'.format(RQL_ANY_SYMBOL, RQL_ANY_SYMBOL) in val: raise ValueError if django_lookup not in (DjangoLookups.REGEX, DjangoLookups.I_REGEX): @@ -846,8 +853,8 @@ def _get_searching_typed_value(cls, django_lookup, str_value): return any_symbol_regex new_val = val - new_val = new_val[1:] if val[0] == RQL_ANY_SYMBOL else '^{}'.format(new_val) - new_val = new_val[:-1] if val[-1] == RQL_ANY_SYMBOL else '{}$'.format(new_val) + new_val = new_val[1:] if val[0] == RQL_ANY_SYMBOL else '^{0}'.format(new_val) + new_val = new_val[:-1] if val[-1] == RQL_ANY_SYMBOL else '{0}$'.format(new_val) return new_val.replace(RQL_ANY_SYMBOL, any_symbol_regex).replace( star_replacer, RQL_ANY_SYMBOL, ) @@ -951,7 +958,7 @@ def _get_choice_class_db_value(value, choices, filter_type, use_repr): return db_value def _build_django_q(self, filter_item, django_lookup, filter_lookup, typed_value): - kwargs = {'{}__{}'.format(filter_item['orm_route'], django_lookup): typed_value} + kwargs = {'{0}__{1}'.format(filter_item['orm_route'], django_lookup): typed_value} return ~Q(**kwargs) if filter_lookup == FilterLookups.NE else Q(**kwargs) @staticmethod @@ -989,25 +996,29 @@ def _is_searching_lookup(filter_lookup): @staticmethod def _check_use_repr(filter_item, filter_name): - assert not (filter_item.get('use_repr') and filter_item.get('ordering')), \ - "{}: 'use_repr' and 'ordering' can't be used together.".format(filter_name) - assert not (filter_item.get('use_repr') and filter_item.get('search')), \ - "{}: 'use_repr' and 'search' can't be used together.".format(filter_name) + e = "{0}: 'use_repr' and 'ordering' can't be used together.".format(filter_name) + assert not (filter_item.get('use_repr') and filter_item.get('ordering')), e + + e = "{0}: 'use_repr' and 'search' can't be used together.".format(filter_name) + assert not (filter_item.get('use_repr') and filter_item.get('search')), e @staticmethod def _check_dynamic(filter_item, filter_name, filter_route): field = filter_item.get('field') if filter_item.get('dynamic', False): - assert filter_route == '', \ - "{}: dynamic filters are not supported in namespaces.".format(filter_name) - assert field is not None, \ - "{}: dynamic filters must have 'field' set.".format(filter_name) + e = "{0}: dynamic filters are not supported in namespaces.".format(filter_name) + assert filter_route == '', e + + e = "{0}: dynamic filters must have 'field' set.".format(filter_name) + assert field is not None, e + else: - assert not filter_item.get('custom', False) and field is None, \ - "{}: common filters can't have 'field' set.".format(filter_name) + e = "{0}: common filters can't have 'field' set.".format(filter_name) + assert not filter_item.get('custom', False) and field is None, e @staticmethod def _check_search(filter_item, filter_name, field): is_non_string_field_type = FilterTypes.field_filter_type(field) != FilterTypes.STRING - assert not (filter_item.get('search') and is_non_string_field_type), \ - "{}: 'search' can be applied only to text filters.".format(filter_name) + + e = "{0}: 'search' can be applied only to text filters.".format(filter_name) + assert not (filter_item.get('search') and is_non_string_field_type), e diff --git a/dj_rql/openapi.py b/dj_rql/openapi.py index 7297383..006936f 100644 --- a/dj_rql/openapi.py +++ b/dj_rql/openapi.py @@ -1,12 +1,13 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from copy import copy from numbers import Number -from dj_rql.constants import FilterLookups, RQL_NULL, RQL_ORDERING_OPERATOR, RQL_SEARCH_PARAM, \ - FilterTypes +from dj_rql.constants import ( + FilterLookups, FilterTypes, RQL_NULL, RQL_ORDERING_OPERATOR, RQL_SEARCH_PARAM, +) class RQLFilterDescriptionTemplate: @@ -88,7 +89,7 @@ def _render_null_inplace(cls, base, filter_item, filter_instance): @classmethod def _render_default_inplace(cls, base, filter_item, filter_instance): if filter_item.get('hidden', False): - return cls._render_common_key_inplace(base, 'default', '**hidden**',) + return cls._render_common_key_inplace(base, 'default', '**hidden**') return base diff --git a/dj_rql/parser.py b/dj_rql/parser.py index 7de99c8..48544d1 100644 --- a/dj_rql/parser.py +++ b/dj_rql/parser.py @@ -1,13 +1,13 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from lark import Lark -from lark.exceptions import LarkError - from dj_rql.exceptions import RQLFilterParsingError from dj_rql.grammar import RQL_GRAMMAR +from lark import Lark +from lark.exceptions import LarkError + class RQLLarkParser(Lark): def parse_query(self, query): diff --git a/dj_rql/qs.py b/dj_rql/qs.py index 7789e5e..0cc4518 100644 --- a/dj_rql/qs.py +++ b/dj_rql/qs.py @@ -1,5 +1,5 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from collections import namedtuple @@ -74,9 +74,10 @@ def rebuild(self, parent_optimization=None): parent_relation = real_parent_optimization.main_relation assert isinstance(parent_relation, str), 'Only simple parent relations are supported.' - parent_type = self._PR \ - if isinstance(real_parent_optimization, PrefetchRelated) \ - else self._SR + if isinstance(real_parent_optimization, PrefetchRelated): + parent_type = self._PR + else: + parent_type = self._SR return self._rebuild_nested( _ParentData(real_parent_optimization, parent_relation, parent_type), @@ -90,7 +91,7 @@ def _rebuild_nested(self, parent_data): @staticmethod def _join_relation(parent_relation, relation): - return '{}__{}'.format(parent_relation, relation) + return '{0}__{1}'.format(parent_relation, relation) class NestedPrefetchRelated(_NestedOptimizationMixin, PrefetchRelated): @@ -122,8 +123,8 @@ def _rebuild_nested(self, parent_data): class Chain(_NestedOptimizationMixin, DBOptimization): def __init__(self, *relations, **extensions): - assert all(isinstance(rel, DBOptimization) for rel in relations), \ - 'Wrong Chain() optimization configuration.' + e = 'Wrong Chain() optimization configuration.' + assert all(isinstance(rel, DBOptimization) for rel in relations), e super(Chain, self).__init__(*relations, **extensions) diff --git a/dj_rql/transformer.py b/dj_rql/transformer.py index 34cfda0..bc4cf58 100644 --- a/dj_rql/transformer.py +++ b/dj_rql/transformer.py @@ -2,30 +2,31 @@ # Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from django.db.models import Q -from lark import Transformer, Tree - from dj_rql._dataclasses import FilterArgs from dj_rql.constants import ( ComparisonOperators, ListOperators, LogicalOperators, - RQL_PLUS, RQL_LIMIT_PARAM, RQL_OFFSET_PARAM, + RQL_PLUS, ) +from django.db.models import Q + +from lark import Transformer, Tree + class BaseRQLTransformer(Transformer): @classmethod def _extract_comparison(cls, args): if len(args) == 2: - # Notation: id=1 + # Notation: id=1 # noqa: E800 operation = ComparisonOperators.EQ prop_index = 0 value_index = 1 elif args[0].data == 'comp_term': - # Notation: eq(id,1) + # Notation: eq(id,1) # noqa: E800 operation = cls._get_value(args[0]) prop_index = 1 value_index = 2 @@ -46,8 +47,9 @@ def _get_value(obj): def sign_prop(self, args): if len(args) == 2: # has sign - return '{}{}'.format(self._get_value(args[0]), self._get_value(args[1])) \ - .lstrip(RQL_PLUS) # Plus is not needed in ordering + return '{0}{1}'.format( + self._get_value(args[0]), self._get_value(args[1]), + ).lstrip(RQL_PLUS) # Plus is not needed in ordering return self._get_value(args[0]) def term(self, args): diff --git a/dj_rql/utils.py b/dj_rql/utils.py index c1c5889..a45472a 100644 --- a/dj_rql/utils.py +++ b/dj_rql/utils.py @@ -1,5 +1,5 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # @@ -23,7 +23,7 @@ def _is_filter_subset(main_dct, subset_dct): subset_keys = set(subset_dct.keys()) for key, value in subset_dct.items(): - assert key in main_dct, 'Filter `{}` is not set ({}).'.format(key, value) + assert key in main_dct, 'Filter `{0}` is not set ({1}).'.format(key, value) main_dct_value = main_dct[key] if isinstance(value, dict): @@ -31,14 +31,21 @@ def _is_filter_subset(main_dct, subset_dct): try: _is_filter_subset(main_dct_value, value) except AssertionError as e: - raise AssertionError("Wrong filter `{}` configuration: {}".format(key, str(e))) + raise AssertionError( + "Wrong filter `{0}` configuration: {1}".format(key, str(e)), + ) + elif isinstance(value, list): - assert len(value) == len(main_dct_value), \ - "Filter `{}` data doesn't match ({}).".format(key, value) + e = "Filter `{0}` data doesn't match ({1}).".format(key, value) + assert len(value) == len(main_dct_value), e + for m_dict, s_dict in zip(main_dct_value, value): _is_filter_subset(m_dict, s_dict) + else: - assert main_dct[key] == value, "{} != {}".format(main_dct[key], value) - assert {'orm_route', 'lookups'}.issubset(subset_keys), \ - "assertion data must contain `orm_route` and `lookups`." + assert main_dct[key] == value, "{0} != {1}".format(main_dct[key], value) + + e = "assertion data must contain `orm_route` and `lookups`." + assert {'orm_route', 'lookups'}.issubset(subset_keys), e + assert 'field' in main_keys diff --git a/docs/conf.py b/docs/conf.py index 2cdd056..25d5e7f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,10 +2,10 @@ import sys from datetime import datetime -from setuptools_scm import get_version - import django +from setuptools_scm import get_version + sys.path.insert(0, os.path.abspath('..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.dj_rf.settings' @@ -15,7 +15,7 @@ # -- Project information ----------------------------------------------------- project = 'django-rql' -copyright = '{}, CloudBlue Inc.'.format(datetime.now().year) +copyright = '{0}, CloudBlue Inc.'.format(datetime.now().year) author = 'CloudBlue' # The full version, including alpha/beta/rc tags diff --git a/requirements/test.txt b/requirements/test.txt index 60ad08b..f75d32e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,8 +4,18 @@ pytest pytest-cov pytest-django pytest-mock +pytest-deadfixtures +pytest-randomly django-fsm>=2.7 django-model-utils>=3.2.0 djangorestframework>=3.11.1 pyyaml -uritemplate \ No newline at end of file +uritemplate +flake8-bugbear +flake8-broken-line +flake8-commas +flake8-comprehensions +flake8-debugger +flake8-eradicate +flake8-import-order +flake8-string-format \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 8142e4d..ff31526 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,5 +8,5 @@ show-source = True ignore = W605 [tool:pytest] -addopts = -p no:cacheprovider --create-db --nomigrations --junitxml=tests/reports/out.xml --cov=dj_rql --cov-report xml:tests/reports/coverage.xml +addopts = --create-db --nomigrations --junitxml=tests/reports/out.xml --cov=dj_rql --cov-report xml:tests/reports/coverage.xml DJANGO_SETTINGS_MODULE = tests.dj_rf.settings diff --git a/setup.py b/setup.py index cc04b8e..bb0a08f 100644 --- a/setup.py +++ b/setup.py @@ -46,5 +46,5 @@ def read_file(name): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Text Processing :: Filters', - ] + ], ) diff --git a/tests/dj_rf/filters.py b/tests/dj_rf/filters.py index 6ffdacf..5a9d3e7 100644 --- a/tests/dj_rf/filters.py +++ b/tests/dj_rf/filters.py @@ -1,13 +1,14 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from django.db.models import IntegerField, CharField, AutoField, F - +from dj_rql.constants import FilterLookups, RQL_NULL from dj_rql.drf.fields import SelectField from dj_rql.filter_cls import RQLFilterClass -from dj_rql.constants import FilterLookups, RQL_NULL from dj_rql.qs import AN, NSR, PR, SR + +from django.db.models import AutoField, CharField, F, IntegerField + from tests.dj_rf.models import Book diff --git a/tests/dj_rf/models.py b/tests/dj_rf/models.py index 1534cbe..295facb 100644 --- a/tests/dj_rf/models.py +++ b/tests/dj_rf/models.py @@ -1,11 +1,13 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from uuid import uuid4 from django.db import models + from django_fsm import FSMField + from model_utils import Choices diff --git a/tests/dj_rf/serializers.py b/tests/dj_rf/serializers.py index 4a87df9..746a3b4 100644 --- a/tests/dj_rf/serializers.py +++ b/tests/dj_rf/serializers.py @@ -1,11 +1,12 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # +from dj_rql.drf.serializers import RQLMixin + from rest_framework import serializers -from dj_rql.drf.serializers import RQLMixin -from tests.dj_rf.models import Publisher, Author, Page, Book +from tests.dj_rf.models import Author, Book, Page, Publisher class PublisherReferenceSerializer(RQLMixin, serializers.ModelSerializer): diff --git a/tests/dj_rf/settings.py b/tests/dj_rf/settings.py index 4e90a71..fc69f00 100644 --- a/tests/dj_rf/settings.py +++ b/tests/dj_rf/settings.py @@ -1,5 +1,5 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # import os @@ -41,7 +41,7 @@ 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } + }, } LANGUAGE_CODE = 'en-us' diff --git a/tests/dj_rf/urls.py b/tests/dj_rf/urls.py index 5fe9a6a..ac05067 100644 --- a/tests/dj_rf/urls.py +++ b/tests/dj_rf/urls.py @@ -2,12 +2,11 @@ # Copyright © 2021 Ingram Micro Inc. All rights reserved. # -from __future__ import absolute_import, unicode_literals - from django.conf.urls import include, re_path + from rest_framework.routers import SimpleRouter -from tests.dj_rf.view import DjangoFiltersViewSet, DRFViewSet, SelectViewSet, NoFilterClsViewSet +from tests.dj_rf.view import DRFViewSet, DjangoFiltersViewSet, NoFilterClsViewSet, SelectViewSet router = SimpleRouter() diff --git a/tests/dj_rf/view.py b/tests/dj_rf/view.py index 8fbbde3..bfbb019 100644 --- a/tests/dj_rf/view.py +++ b/tests/dj_rf/view.py @@ -1,21 +1,21 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # +from dj_rql.drf.backend import RQLFilterBackend +from dj_rql.drf.compat import DjangoFiltersRQLFilterBackend +from dj_rql.drf.paginations import RQLContentRangeLimitOffsetPagination + from django.db.models import CharField, IntegerField, Value + from rest_framework import mixins from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from dj_rql.drf.backend import ( - RQLFilterBackend, -) -from dj_rql.drf.paginations import RQLContentRangeLimitOffsetPagination -from dj_rql.drf.compat import DjangoFiltersRQLFilterBackend from tests.dj_rf.filters import BooksFilterClass, SelectBooksFilterClass from tests.dj_rf.models import Book -from tests.dj_rf.serializers import SelectBookSerializer, BookSerializer +from tests.dj_rf.serializers import BookSerializer, SelectBookSerializer def apply_annotations(qs): diff --git a/tests/test_constants.py b/tests/test_constants.py index cb3218f..97e492d 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,11 +1,11 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest - from dj_rql.constants import FilterLookups, FilterTypes +import pytest + def test_field_filter_type(): custom_field = {} diff --git a/tests/test_drf/conftest.py b/tests/test_drf/conftest.py index 620b437..05a1139 100644 --- a/tests/test_drf/conftest.py +++ b/tests/test_drf/conftest.py @@ -1,11 +1,12 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # +from dj_rql.drf.backend import FilterCache + import pytest -from rest_framework.test import APIClient -from dj_rql.drf.backend import FilterCache +from rest_framework.test import APIClient @pytest.fixture diff --git a/tests/test_drf/test_common_drf_backend.py b/tests/test_drf/test_common_drf_backend.py index 1a06073..b61a3b1 100644 --- a/tests/test_drf/test_common_drf_backend.py +++ b/tests/test_drf/test_common_drf_backend.py @@ -2,13 +2,16 @@ # Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from django.test.utils import CaptureQueriesContext +from dj_rql.drf import FilterCache, RQLFilterBackend + from django.db import connection +from django.test.utils import CaptureQueriesContext + +import pytest + from rest_framework.reverse import reverse from rest_framework.status import HTTP_200_OK -from dj_rql.drf import RQLFilterBackend, FilterCache from tests.dj_rf.models import Book @@ -54,8 +57,8 @@ def test_list_empty(api_client, clear_cache): @pytest.mark.django_db def test_list_filtering(api_client, clear_cache): books = [Book.objects.create() for _ in range(2)] - query = 'id={}'.format(books[0].pk) - response = api_client.get('{}?{}'.format(reverse('book-list'), query)) + query = 'id={0}'.format(books[0].pk) + response = api_client.get('{0}?{1}'.format(reverse('book-list'), query)) assert response.status_code == HTTP_200_OK assert response.data == [{'id': books[0].pk}] @@ -64,7 +67,7 @@ def test_list_filtering(api_client, clear_cache): def test_list_pagination(api_client, clear_cache): books = [Book.objects.create() for _ in range(5)] query = 'limit=2,eq(offset,1)' - response = api_client.get('{}?{}'.format(reverse('book-list'), query)) + response = api_client.get('{0}?{1}'.format(reverse('book-list'), query)) assert response.status_code == HTTP_200_OK assert response.data == [{'id': books[1].pk}, {'id': books[2].pk}] assert response.get('Content-Range') == 'items 1-2/5' @@ -74,7 +77,7 @@ def test_list_pagination(api_client, clear_cache): def test_list_pagination_zero_limit(api_client, clear_cache): [Book.objects.create() for _ in range(5)] query = 'limit=0' - response = api_client.get('{}?{}'.format(reverse('book-list'), query)) + response = api_client.get('{0}?{1}'.format(reverse('book-list'), query)) assert response.status_code == HTTP_200_OK assert response.data == [] assert response.get('Content-Range') == 'items 0-0/5' @@ -95,14 +98,14 @@ def test_cache(api_client, clear_cache): ] assert FilterCache.CACHE == {} - response = api_client.get('{}?{}'.format(reverse('book-list'), 'title=F')) + response = api_client.get('{0}?{1}'.format(reverse('book-list'), 'title=F')) assert response.data == [{'id': books[0].pk}] expected_cache_key = 'book.BooksFilterClass' assert expected_cache_key in FilterCache.CACHE cache_item_id = id(FilterCache.CACHE[expected_cache_key]) - response = api_client.get('{}?{}'.format(reverse('book-list'), 'title=G')) + response = api_client.get('{0}?{1}'.format(reverse('book-list'), 'title=G')) assert response.data == [{'id': books[1].pk}] assert expected_cache_key in FilterCache.CACHE @@ -115,11 +118,11 @@ def test_cache(api_client, clear_cache): @pytest.mark.django_db def test_distinct_sequence(api_client, clear_cache): with CaptureQueriesContext(connection) as context: - api_client.get('{}?{}'.format(reverse('book-list'), 'status=planning')) + api_client.get('{0}?{1}'.format(reverse('book-list'), 'status=planning')) assert 'distinct' in context.captured_queries[0]['sql'].lower() with CaptureQueriesContext(connection) as context: - api_client.get('{}?{}'.format(reverse('book-list'), 'title=abc')) + api_client.get('{0}?{1}'.format(reverse('book-list'), 'title=abc')) assert 'distinct' not in context.captured_queries[0]['sql'].lower() diff --git a/tests/test_drf/test_django_filters_backend.py b/tests/test_drf/test_django_filters_backend.py index d93c611..7924f28 100644 --- a/tests/test_drf/test_django_filters_backend.py +++ b/tests/test_drf/test_django_filters_backend.py @@ -1,16 +1,19 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest +from dj_rql.constants import DjangoLookups +from dj_rql.drf.compat import CompatibilityRQLFilterBackend, DjangoFiltersRQLFilterBackend +from dj_rql.exceptions import RQLFilterParsingError, RQLFilterValueError + from django.http import QueryDict from django.utils.timezone import now + +import pytest + from rest_framework.reverse import reverse from rest_framework.status import HTTP_200_OK -from dj_rql.constants import DjangoLookups -from dj_rql.drf.compat import CompatibilityRQLFilterBackend, DjangoFiltersRQLFilterBackend -from dj_rql.exceptions import RQLFilterParsingError, RQLFilterValueError from tests.dj_rf.filters import BooksFilterClass from tests.dj_rf.models import Author, Book @@ -79,8 +82,10 @@ def test_old_syntax_filters(mocker): def test_bad_syntax_query(mocker): - query = 'limit=10&offset=0&in=(prop,(val1,val2))&in(prop.prop,(val))' \ + query = ( + 'limit=10&offset=0&in=(prop,(val1,val2))&in(prop.prop,(val))' '&ge=(created,2020-06-01T04:00:00Z)&le=(created,2020-06-24T03:59:59Z)' + ) request = mocker.MagicMock( query_params=QueryDict(query), _request=mocker.MagicMock(META={'QUERY_STRING': query}), @@ -103,7 +108,7 @@ def test_old_syntax_select_remains(query, mocker): def filter_api(api_client, query): - return api_client.get('{}?{}'.format(reverse('old_book-list'), query)) + return api_client.get('{0}?{1}'.format(reverse('old_book-list'), query)) def assert_ok_response(response, count): @@ -136,7 +141,7 @@ def test_common_comparison(api_client, clear_cache): def test__in_with_one_value(api_client, clear_cache, title, count): create_books(['G', '']) - response = filter_api(api_client, 'title__in={}'.format(title)) + response = filter_api(api_client, 'title__in={0}'.format(title)) assert_ok_response(response, count) @@ -157,7 +162,7 @@ def test__in_with_several_values(api_client, clear_cache): def test__in_with_several_values_one_empty_one_invalid(api_client, clear_cache, query): create_books(['G', '']) - response = filter_api(api_client, 'title__in={}'.format(query)) + response = filter_api(api_client, 'title__in={0}'.format(query)) assert_ok_response(response, 1) @@ -184,7 +189,7 @@ def test_boolean_value_ok(api_client, clear_cache, value, index): ] books = [Book.objects.create(author=author) for author in authors] - response = filter_api(api_client, 'author.is_male={}'.format(value)) + response = filter_api(api_client, 'author.is_male={0}'.format(value)) assert_ok_response(response, 1) assert response.data[0]['id'] == books[index].id @@ -198,7 +203,7 @@ def test_boolean_value_ok(api_client, clear_cache, value, index): )) def test_boolean_value_fail(api_client, clear_cache, value): with pytest.raises(RQLFilterValueError): - filter_api(api_client, 'author.is_male={}'.format(value)) + filter_api(api_client, 'author.is_male={0}'.format(value)) @pytest.mark.django_db @@ -213,7 +218,7 @@ def test_boolean_value_fail(api_client, clear_cache, value): def test__isnull_ok(api_client, clear_cache, value, index): books = create_books([None, 'G']) - response = filter_api(api_client, 'title__isnull={}'.format(value)) + response = filter_api(api_client, 'title__isnull={0}'.format(value)) assert_ok_response(response, 1) assert response.data[0]['id'] == books[index].id @@ -226,7 +231,7 @@ def test__isnull_ok(api_client, clear_cache, value, index): )) def test__isnull_fail(api_client, clear_cache, value): with pytest.raises(RQLFilterParsingError): - filter_api(api_client, 'title__isnull={}'.format(value)) + filter_api(api_client, 'title__isnull={0}'.format(value)) @pytest.mark.django_db @@ -238,7 +243,7 @@ def test__isnull_fail(api_client, clear_cache, value): def test__exact(api_client, clear_cache, value, count): create_book('G') - response = filter_api(api_client, 'title__exact={}'.format(value)) + response = filter_api(api_client, 'title__exact={0}'.format(value)) assert_ok_response(response, count) @@ -251,7 +256,7 @@ def test_multiple_choice(api_client, clear_cache, values, count): create_book('G') response = filter_api( - api_client, 'title__exact={}&title__exact={}'.format(values[0], values[1]), + api_client, 'title__exact={0}&title__exact={1}'.format(values[0], values[1]), ) assert_ok_response(response, count) @@ -274,7 +279,7 @@ def test_multiple_choice(api_client, clear_cache, values, count): def test__contains(api_client, clear_cache, value, count, operator): create_book('Title') - response = filter_api(api_client, 'title__{}={}'.format(operator, value)) + response = filter_api(api_client, 'title__{0}={1}'.format(operator, value)) assert_ok_response(response, count) @@ -296,7 +301,7 @@ def test__contains(api_client, clear_cache, value, count, operator): def test__startswith(api_client, clear_cache, value, count, operator): create_book('Title') - response = filter_api(api_client, 'title__{}={}'.format(operator, value)) + response = filter_api(api_client, 'title__{0}={1}'.format(operator, value)) assert_ok_response(response, count) @@ -318,7 +323,7 @@ def test__startswith(api_client, clear_cache, value, count, operator): def test__endswith(api_client, clear_cache, value, count, operator): create_book('Title') - response = filter_api(api_client, 'title__{}={}'.format(operator, value)) + response = filter_api(api_client, 'title__{0}={1}'.format(operator, value)) assert_ok_response(response, count) @@ -329,7 +334,7 @@ def test__endswith(api_client, clear_cache, value, count, operator): )) def test__regex(api_client, clear_cache, lookup): with pytest.raises(RQLFilterParsingError): - filter_api(api_client, 'title__{}=true'.format(lookup)) + filter_api(api_client, 'title__{0}=true'.format(lookup)) @pytest.mark.django_db @@ -344,7 +349,7 @@ def test__regex(api_client, clear_cache, lookup): 'day__gt', )) def test__day_week_etc(api_client, clear_cache, lookup): - response = filter_api(api_client, 'title__{}=2020'.format(lookup)) + response = filter_api(api_client, 'title__{0}=2020'.format(lookup)) assert_ok_response(response, 0) @@ -358,10 +363,10 @@ def test__day_week_etc(api_client, clear_cache, lookup): def test__gt_ge_lt_le(api_client, clear_cache, lookup, p_value, n_value): Book.objects.create(github_stars=5) - response = filter_api(api_client, 'github_stars__{}={}'.format(lookup, p_value)) + response = filter_api(api_client, 'github_stars__{0}={1}'.format(lookup, p_value)) assert_ok_response(response, 1) - response = filter_api(api_client, 'github_stars__{}={}'.format(lookup, n_value)) + response = filter_api(api_client, 'github_stars__{0}={1}'.format(lookup, n_value)) assert_ok_response(response, 0) @@ -379,9 +384,10 @@ def test_order_ok(api_client, clear_cache, ordering_term): books = [Book.objects.create(author=author, published_at=now()) for author in authors] books.append(Book.objects.create(published_at=now())) - response = filter_api(api_client, '{}=author.email,-published.at'.format(ordering_term)) - assert [d['id'] for d in response.data] == \ - list(b.id for b in Book.objects.all().order_by('author__email', '-published_at')) + response = filter_api(api_client, '{0}=author.email,-published.at'.format(ordering_term)) + + expected = [b.id for b in Book.objects.all().order_by('author__email', '-published_at')] + assert [d['id'] for d in response.data] == expected @pytest.mark.django_db @@ -391,7 +397,7 @@ def test_order_ok(api_client, clear_cache, ordering_term): )) def test_order_fail(api_client, clear_cache, ordering_term): with pytest.raises(RQLFilterParsingError): - filter_api(api_client, '{}=invalid'.format(ordering_term)) + filter_api(api_client, '{0}=invalid'.format(ordering_term)) @pytest.mark.django_db @@ -399,7 +405,7 @@ def test_quoted_value_with_special_symbols(api_client, clear_cache): title = "'|(), '" create_book(title) - response = filter_api(api_client, 'title={}'.format(title)) + response = filter_api(api_client, 'title={0}'.format(title)) assert_ok_response(response, 0) @@ -407,14 +413,14 @@ def test_quoted_value_with_special_symbols(api_client, clear_cache): def test_unquoted_value_with_special_symbols(api_client, clear_cache): title = '|(),"' with pytest.raises(RQLFilterParsingError): - filter_api(api_client, 'title={}'.format(title)) + filter_api(api_client, 'title={0}'.format(title)) @pytest.mark.django_db def test_value_with_quotes_fail(api_client, clear_cache): title = '|\'(),"' with pytest.raises(RQLFilterParsingError): - filter_api(api_client, 'title__exact={}'.format(title)) + filter_api(api_client, 'title__exact={0}'.format(title)) @pytest.mark.django_db @@ -433,7 +439,7 @@ def test_empty_property(api_client, clear_cache): def test_empty_value(api_client, clear_cache, prop): create_book() - response = filter_api(api_client, '{}='.format(prop)) + response = filter_api(api_client, '{0}='.format(prop)) assert_ok_response(response, 1) @@ -457,9 +463,9 @@ def test_pagination(api_client, clear_cache): def test_choice(api_client, clear_cache): books = [Book.objects.create(str_choice_field=choice) for choice, _ in Book.STR_CHOICES] - response = filter_api(api_client, 'str_choice_field__in={},{}'.format('one', 'two')) + response = filter_api(api_client, 'str_choice_field__in={0},{1}'.format('one', 'two')) assert_ok_response(response, 2) - response = filter_api(api_client, 'str_choice_field__in=,{}'.format('one')) + response = filter_api(api_client, 'str_choice_field__in=,{0}'.format('one')) assert_ok_response(response, 1) assert response.data[0]['id'] == books[0].id diff --git a/tests/test_drf/test_pagination.py b/tests/test_drf/test_pagination.py index a50428f..8dd5df7 100644 --- a/tests/test_drf/test_pagination.py +++ b/tests/test_drf/test_pagination.py @@ -1,15 +1,16 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from unittest import TestCase +from dj_rql.drf import RQLContentRangeLimitOffsetPagination +from dj_rql.exceptions import RQLFilterParsingError + from rest_framework.pagination import PAGE_BREAK, PageLink from rest_framework.request import Request from rest_framework.test import APIRequestFactory -from dj_rql.exceptions import RQLFilterParsingError -from dj_rql.drf import RQLContentRangeLimitOffsetPagination factory = APIRequestFactory() @@ -205,12 +206,14 @@ def test_max_limit(self): def test_rql_operators(self): limit, offset = 1, 2 - request = Request(factory.get('/?search=0&limit=eq={},eq(offset,{})'.format(limit, offset))) + request = Request( + factory.get('/?search=0&limit=eq={0},eq(offset,{1})'.format(limit, offset)), + ) queryset = self.paginate_queryset(request) assert queryset == [3] def assert_rql_parsing_error(self, query): - request = Request(factory.get('/?{}'.format(query))) + request = Request(factory.get('/?{0}'.format(query))) with self.assertRaises(RQLFilterParsingError) as e: self.paginate_queryset(request) assert e.exception.details['error'] == 'Limit and offset are set incorrectly.' diff --git a/tests/test_drf/test_select.py b/tests/test_drf/test_select.py index 577155b..19ad751 100644 --- a/tests/test_drf/test_select.py +++ b/tests/test_drf/test_select.py @@ -3,6 +3,7 @@ # import pytest + from rest_framework.reverse import reverse from rest_framework.status import HTTP_200_OK diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index aee0bb1..491c666 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,13 +1,13 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest - from dj_rql.exceptions import ( RQLFilterError, RQLFilterLookupError, RQLFilterParsingError, RQLFilterValueError, ) +import pytest + @pytest.mark.parametrize('exception_cls,message', [ (RQLFilterError, 'RQL Filtering error.'), diff --git a/tests/test_filter_cls/test_apply_filters.py b/tests/test_filter_cls/test_apply_filters.py index 38e54e8..694705e 100644 --- a/tests/test_filter_cls/test_apply_filters.py +++ b/tests/test_filter_cls/test_apply_filters.py @@ -1,17 +1,19 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from functools import partial -import pytest -from django.core.exceptions import FieldError -from django.db.models import Q, IntegerField -from django.utils.timezone import now - from dj_rql.constants import FilterLookups, ListOperators, RQL_NULL from dj_rql.exceptions import RQLFilterLookupError, RQLFilterParsingError from dj_rql.filter_cls import RQLFilterClass + +from django.core.exceptions import FieldError +from django.db.models import IntegerField, Q +from django.utils.timezone import now + +import pytest + from tests.dj_rf.filters import BooksFilterClass from tests.dj_rf.models import Author, Book, Publisher from tests.test_filter_cls.utils import book_qs, create_books @@ -37,7 +39,7 @@ def test_lookup_error(): @pytest.mark.django_db -@pytest.mark.parametrize('comparison_tpl', ['eq(title,{})', 'title=eq={}', 'title={}']) +@pytest.mark.parametrize('comparison_tpl', ['eq(title,{0})', 'title=eq={0}', 'title={0}']) def test_comparison(comparison_tpl): title = 'book' books = [ @@ -48,7 +50,7 @@ def test_comparison(comparison_tpl): @pytest.mark.django_db -@pytest.mark.parametrize('searching_tpl', ['like(title,*{}*)', 'ilike(title,*{})']) +@pytest.mark.parametrize('searching_tpl', ['like(title,*{0}*)', 'ilike(title,*{0})']) def test_searching(searching_tpl): title = 'book' books = [ @@ -62,8 +64,8 @@ def test_searching(searching_tpl): @pytest.mark.parametrize('operator', ['&', ',']) def test_and(operator): email, title = 'george@martin.com', 'book' - comp1 = 'title={}'.format(title) - comp2 = 'eq(author.email,{})'.format(email) + comp1 = 'title={0}'.format(title) + comp2 = 'eq(author.email,{0})'.format(email) query = '{comp1}{op}{comp2}'.format(comp1=comp1, op=operator, comp2=comp2) authors = [ @@ -82,8 +84,8 @@ def test_and(operator): @pytest.mark.parametrize('operator', ['|', ';']) def test_or(operator): email, title = 'george@martin.com', 'book' - comp1 = 'title={}'.format(title) - comp2 = 'eq(author.email,{})'.format(email) + comp1 = 'title={0}'.format(title) + comp2 = 'eq(author.email,{0})'.format(email) query = '({comp1}{op}{comp2})'.format(comp1=comp1, op=operator, comp2=comp2) authors = [ @@ -104,7 +106,7 @@ def test_not(): Book.objects.create(title=title), Book.objects.create(title='another'), ] - assert apply_filters('not(title={})'.format(title)) == [books[1]] + assert apply_filters('not(title={0})'.format(title)) == [books[1]] @pytest.mark.django_db @@ -158,8 +160,8 @@ def test_out(): @pytest.mark.django_db def test_null(): books = create_books() - assert apply_filters('title={}'.format(RQL_NULL)) == books - assert apply_filters('title=ne={}'.format(RQL_NULL)) == [] + assert apply_filters('title={0}'.format(RQL_NULL)) == books + assert apply_filters('title=ne={0}'.format(RQL_NULL)) == [] @pytest.mark.django_db @@ -170,8 +172,8 @@ def test_null_with_in_or(): books[0].title = title books[0].save(update_fields=['title']) - assert apply_filters('in(title,({},{}))'.format(title, RQL_NULL)) == books - assert apply_filters('or(title=eq={},eq(title,{}))'.format(title, RQL_NULL)) == books + assert apply_filters('in(title,({0},{1}))'.format(title, RQL_NULL)) == books + assert apply_filters('or(title=eq={0},eq(title,{1}))'.format(title, RQL_NULL)) == books @pytest.mark.django_db @@ -183,8 +185,8 @@ def test_null_on_foreign_key_pk(): ] books = [Book.objects.create(author=author) for author in authors] - assert apply_filters('author.publisher.id={}'.format(RQL_NULL)) == [books[1]] - assert apply_filters('ne(author.publisher.id,{})'.format(RQL_NULL)) == [books[0]] + assert apply_filters('author.publisher.id={0}'.format(RQL_NULL)) == [books[1]] + assert apply_filters('ne(author.publisher.id,{0})'.format(RQL_NULL)) == [books[0]] @pytest.mark.django_db @@ -216,8 +218,8 @@ def test_ordering_by_several_filters(): books = [Book.objects.create(author=author, published_at=now()) for author in authors] books.append(Book.objects.create(published_at=now())) - assert apply_filters('ordering(author.email,-published.at)') == \ - list(book_qs.order_by('author__email', '-published_at')) + expected = list(book_qs.order_by('author__email', '-published_at')) + assert apply_filters('ordering(author.email,-published.at)') == expected @pytest.mark.django_db @@ -229,8 +231,9 @@ def test_ordering_by_empty_value(): def test_several_ordering_operations(): with pytest.raises(RQLFilterParsingError) as e: apply_filters('ordering(d_id)&ordering(author.email)') - assert e.value.details['error'] == \ - 'Bad ordering filter: query can contain only one ordering operation.' + + expected = 'Bad ordering filter: query can contain only one ordering operation.' + assert e.value.details['error'] == expected def test_bad_ordering_filter(): @@ -271,7 +274,7 @@ def build_q_for_custom_filter(self, *args, **kwargs): books = [Book.objects.create() for _ in range(2)] assert list( - CustomCls(book_qs).apply_filters('{}(has_list_lookup,(1,2))'.format(operator))[1] + CustomCls(book_qs).apply_filters('{0}(has_list_lookup,(1,2))'.format(operator))[1], ) == [books[1]] @@ -282,7 +285,7 @@ def build_q_for_custom_filter(self, *args, **kwargs): return Q(id__gte=2) with pytest.raises(RQLFilterLookupError) as e: - CustomCls(book_qs).apply_filters('{}(no_list_lookup,(1,2))'.format(operator)) + CustomCls(book_qs).apply_filters('{0}(no_list_lookup,(1,2))'.format(operator)) assert e.value.details['lookup'] == operator @@ -293,7 +296,7 @@ def build_name_for_custom_ordering(self, filter_name): return 'id' def assert_ordering(self, filter_name, expected): - assert list(self.apply_filters('ordering({})'.format(filter_name))[1]) == expected + assert list(self.apply_filters('ordering({0})'.format(filter_name))[1]) == expected books = create_books() @@ -313,15 +316,15 @@ class CustomCls(RQLFilterClass): }] def assert_search(self, value, expected): - assert list(self.apply_filters('search={}'.format(value))[1]) == expected + assert list(self.apply_filters('search={0}'.format(value))[1]) == expected @classmethod def side_effect(cls, data): django_lookup = data.django_lookup return Q(**{ - 'title__{}'.format(django_lookup): cls._get_searching_typed_value( + 'title__{0}'.format(django_lookup): cls._get_searching_typed_value( django_lookup, data.str_value, - ) + ), }) build_q_for_custom_filter_patch = mocker.patch.object( @@ -363,7 +366,7 @@ class CustomCls(RQLFilterClass): EXTENDED_SEARCH_ORM_ROUTES = ['title'] def assert_search(self, value, expected): - assert list(self.apply_filters('search={}'.format(value))[1]) == expected + assert list(self.apply_filters('search={0}'.format(value))[1]) == expected books = [ Book.objects.create(title='book'), @@ -390,7 +393,7 @@ class CustomCls(RQLFilterClass): @pytest.mark.django_db @pytest.mark.parametrize('select', ('title,+page,-rating.blog_int', '', 'title')) def test_select(select): - assert apply_filters('select({})'.format(select)) == [] + assert apply_filters('select({0})'.format(select)) == [] @pytest.mark.django_db diff --git a/tests/test_filter_cls/test_fields_filtering.py b/tests/test_filter_cls/test_fields_filtering.py index 707c675..d95bcb5 100644 --- a/tests/test_filter_cls/test_fields_filtering.py +++ b/tests/test_filter_cls/test_fields_filtering.py @@ -1,22 +1,24 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from datetime import date, datetime from functools import partial -import pytest -from django.db.models import Q - from dj_rql._dataclasses import FilterArgs from dj_rql.constants import ( ComparisonOperators as CO, DjangoLookups, - SearchOperators, RQL_EMPTY, RQL_NULL, + SearchOperators, ) from dj_rql.exceptions import RQLFilterLookupError, RQLFilterParsingError, RQLFilterValueError + +from django.db.models import Q + +import pytest + from tests.dj_rf.filters import BooksFilterClass from tests.dj_rf.models import Author, Book, Page, Publisher from tests.test_filter_cls.utils import book_qs, create_books @@ -63,8 +65,8 @@ def test_title(): Book.objects.create(title=''), ] assert filter_field(filter_name, CO.EQ, books[0].title) == [books[0]] - assert filter_field(filter_name, CO.EQ, '"{}"'.format(books[0].title)) == [books[0]] - assert filter_field(filter_name, CO.EQ, "'{}'".format(books[0].title)) == [books[0]] + assert filter_field(filter_name, CO.EQ, '"{0}"'.format(books[0].title)) == [books[0]] + assert filter_field(filter_name, CO.EQ, "'{0}'".format(books[0].title)) == [books[0]] assert filter_field(filter_name, CO.EQ, 'N') == [] assert filter_field(filter_name, CO.NE, books[0].title) == [books[1], books[2], books[3]] assert filter_field(filter_name, CO.EQ, RQL_NULL) == [books[2]] @@ -407,7 +409,7 @@ def test_datetime_field_fail(filter_name, bad_value): @pytest.mark.parametrize('bad_operator', [CO.GT, CO.LE]) @pytest.mark.parametrize('filter_name,value', [ ('amazon_rating', '1.23'), ('page.number', '5'), - ('int_choice_field_repr', 'I'), ('str_choice_field_repr', 'I') + ('int_choice_field_repr', 'I'), ('str_choice_field_repr', 'I'), ]) def test_field_lookup_fail(filter_name, value, bad_operator): assert_filter_field_lookup_error(filter_name, bad_operator, value) @@ -463,12 +465,12 @@ def test_empty_value_fail(filter_name): def test_searching_q_ok(value, db_lookup, db_value): cls = BooksFilterClass(book_qs) - for v in (value, '"{}"'.format(value)): + for v in (value, '"{0}"'.format(value)): like_q = cls.build_q_for_filter(FilterArgs('title', SearchOperators.LIKE, v)) - assert like_q.children[0] == ('title__{}'.format(db_lookup), db_value) + assert like_q.children[0] == ('title__{0}'.format(db_lookup), db_value) i_like_q = cls.build_q_for_filter(FilterArgs('title', SearchOperators.I_LIKE, value)) - assert i_like_q.children[0] == ('title__i{}'.format(db_lookup), db_value) + assert i_like_q.children[0] == ('title__i{0}'.format(db_lookup), db_value) @pytest.mark.django_db diff --git a/tests/test_filter_cls/test_initialization.py b/tests/test_filter_cls/test_initialization.py index 77d3e23..18645a4 100644 --- a/tests/test_filter_cls/test_initialization.py +++ b/tests/test_filter_cls/test_initialization.py @@ -1,17 +1,20 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from django.core.exceptions import FieldDoesNotExist - from dj_rql.constants import FilterLookups as FL, RESERVED_FILTER_NAMES, RQL_NULL from dj_rql.filter_cls import RQLFilterClass from dj_rql.utils import assert_filter_cls + +from django.core.exceptions import FieldDoesNotExist + +import pytest + from tests.data import get_book_filter_cls_ordering_data, get_book_filter_cls_search_data from tests.dj_rf.filters import BooksFilterClass from tests.dj_rf.models import Author, Book + empty_qs = Author.objects.none() @@ -145,7 +148,7 @@ class Cls(RQLFilterClass): assert str(e.value) == 'Unsupported field type: publisher.' -@pytest.mark.parametrize('filter_name', sorted(list(RESERVED_FILTER_NAMES))) +@pytest.mark.parametrize('filter_name', sorted(RESERVED_FILTER_NAMES)) def test_reserved_filter_name_is_used(filter_name): class Cls(RQLFilterClass): MODEL = Author @@ -156,7 +159,7 @@ class Cls(RQLFilterClass): with pytest.raises(AssertionError) as e: Cls(empty_qs) - assert str(e.value) == "'{}' is a reserved filter name.".format(filter_name) + assert str(e.value) == "'{0}' is a reserved filter name.".format(filter_name) def test_bad_use_repr_and_ordering(): @@ -199,7 +202,7 @@ class Cls(RQLFilterClass): with pytest.raises(AssertionError) as e: Cls(empty_qs) - assert str(e.value) == "title: '{}' is not supported by namespaces.".format(option) + assert str(e.value) == "title: '{0}' is not supported by namespaces.".format(option) def test_bad_item_structure(): @@ -222,7 +225,7 @@ class Cls(RQLFilterClass): 'filters': [{ 'filter': 'a', 'dynamic': True, - }] + }], }] with pytest.raises(AssertionError) as e: diff --git a/tests/test_filter_cls/test_select.py b/tests/test_filter_cls/test_select.py index ad0f7be..f4d1dd2 100644 --- a/tests/test_filter_cls/test_select.py +++ b/tests/test_filter_cls/test_select.py @@ -1,15 +1,16 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from django.core.exceptions import FieldError -from django.db.models import CharField, IntegerField, Value - from dj_rql.drf.fields import SelectField from dj_rql.exceptions import RQLFilterParsingError from dj_rql.filter_cls import RQLFilterClass -from dj_rql.qs import AN, CH, PR, NPR, NSR, SR +from dj_rql.qs import AN, CH, NPR, NSR, PR, SR + +from django.core.exceptions import FieldError +from django.db.models import CharField, IntegerField, Value + +import pytest from tests.dj_rf.models import Author, Book from tests.test_filter_cls.utils import book_qs @@ -152,7 +153,7 @@ class Cls(SelectFilterCls): { 'namespace': 'vn', 'source': 'publisher', - 'filters': ('id',) + 'filters': ('id',), }, ), }, @@ -353,7 +354,7 @@ def test_signs_select(): class Cls(SelectFilterCls): FILTERS = tuple( { - 'filter': 'ft{}'.format(i), + 'filter': 'ft{0}'.format(i), 'source': 'id', } for i in range(1, 5) @@ -464,7 +465,7 @@ class Cls(SelectFilterCls): { 'namespace': 'ns', 'source': 'author', - 'filters': ('id',) + 'filters': ('id',), }, ) @@ -509,7 +510,7 @@ class Cls(SelectFilterCls): 'namespace': 'ns1', 'source': 'publisher', 'hidden': False, - 'filters': ('id',) + 'filters': ('id',), }, ), }, @@ -567,7 +568,7 @@ class Cls(SelectFilterCls): { 'filter': 'ft1', 'source': 'id', - 'qs': SR('author') + 'qs': SR('author'), }, { 'filter': 'ft2', diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 104f114..17c3a4a 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,12 +1,13 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from copy import copy +from dj_rql.openapi import RQLFilterClassSpecification, RQLFilterDescriptionTemplate + from rest_framework.schemas.openapi import SchemaGenerator -from dj_rql.openapi import RQLFilterClassSpecification, RQLFilterDescriptionTemplate from tests.dj_rf.filters import BooksFilterClass from tests.dj_rf.models import Book @@ -39,27 +40,35 @@ def test_description_common_render(): def test_description_search_render(): result = RQLFilterDescriptionTemplate.render(*filter_data('str_choice_field')) - assert result == '**Filter for: str_choice_field**\n\nlookups: ' \ - 'eq, ne, like, ilike, in, out\nsearch: true' + assert result == ( + '**Filter for: str_choice_field**\n\nlookups: ' + 'eq, ne, like, ilike, in, out\nsearch: true' + ) def test_description_ordering_render(): result = RQLFilterDescriptionTemplate.render(*filter_data('int_choice_field')) - assert result == '**Filter for: int_choice_field**\n\nlookups: ' \ - 'eq, ne, ge, gt, le, lt, in, out\nordering: true' + assert result == ( + '**Filter for: int_choice_field**\n\nlookups: ' + 'eq, ne, ge, gt, le, lt, in, out\nordering: true' + ) def test_description_null_overridden_render(): result = RQLFilterDescriptionTemplate.render(*filter_data('title')) - assert result == '**Filter for: title**\n\n' \ - 'lookups: eq, ne, like, ilike, null, in, out\nsearch: true\n' \ - 'null: NULL_ID, null()' + assert result == ( + '**Filter for: title**\n\n' + 'lookups: eq, ne, like, ilike, null, in, out\nsearch: true\n' + 'null: NULL_ID, null()' + ) def test_description_hidden_render(): result = RQLFilterDescriptionTemplate.render(*filter_data('select_author')) - assert result == '**Filter for: select_author**\n\n' \ - 'lookups: eq, ne, like, ilike, in, out\ndefault: **hidden**' + assert result == ( + '**Filter for: select_author**\n\n' + 'lookups: eq, ne, like, ilike, in, out\ndefault: **hidden**' + ) def test_description_custom_render(): diff --git a/tests/test_parser/constants.py b/tests/test_parser/constants.py index ef0c28c..f3139ee 100644 --- a/tests/test_parser/constants.py +++ b/tests/test_parser/constants.py @@ -1,10 +1,9 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from dj_rql.constants import ( - ComparisonOperators, ListOperators, LogicalOperators, SearchOperators, - RQL_EMPTY, RQL_NULL, + ComparisonOperators, ListOperators, LogicalOperators, RQL_EMPTY, RQL_NULL, SearchOperators, ) diff --git a/tests/test_parser/test_comparison.py b/tests/test_parser/test_comparison.py index 756d48a..5801b2a 100644 --- a/tests/test_parser/test_comparison.py +++ b/tests/test_parser/test_comparison.py @@ -1,15 +1,16 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from functools import partial -import pytest +from dj_rql.constants import ComparisonOperators as CompOp +from dj_rql.parser import RQLParser from lark.exceptions import LarkError -from dj_rql.constants import ComparisonOperators as CompOp -from dj_rql.parser import RQLParser +import pytest + from tests.test_parser.constants import FAIL_PROPS, FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import ComparisonTransformer @@ -55,8 +56,10 @@ def test_comparison_value_fail(operator, prop, value, func): def test_comparison_order(): - assert base_cmp_transform(CompOp.EQ, CompOp.LE, CompOp.GT) != \ - alias_cmp_transform(CompOp.LE, CompOp.EQ, CompOp.GT) + r1 = base_cmp_transform(CompOp.EQ, CompOp.LE, CompOp.GT) + r2 = alias_cmp_transform(CompOp.LE, CompOp.EQ, CompOp.GT) + + assert r1 != r2 @pytest.mark.parametrize('prop', OK_PROPS) diff --git a/tests/test_parser/test_listing.py b/tests/test_parser/test_listing.py index e25baff..73762b2 100644 --- a/tests/test_parser/test_listing.py +++ b/tests/test_parser/test_listing.py @@ -1,15 +1,18 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from lark.exceptions import LarkError - from dj_rql.constants import ListOperators from dj_rql.parser import RQLParser + +from lark.exceptions import LarkError + +import pytest + from tests.test_parser.constants import FAIL_PROPS, LIST_FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import ListTransformer + REVERSED_OK_VALUES = reversed(OK_VALUES) list_operators = [ListOperators.IN, ListOperators.OUT] diff --git a/tests/test_parser/test_logical.py b/tests/test_parser/test_logical.py index d021753..3809019 100644 --- a/tests/test_parser/test_logical.py +++ b/tests/test_parser/test_logical.py @@ -1,15 +1,17 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # from functools import partial -import pytest +from dj_rql.constants import ComparisonOperators, LogicalOperators +from dj_rql.parser import RQLParser + from lark.exceptions import LarkError -from dj_rql.parser import RQLParser +import pytest + from tests.test_parser.utils import LogicalTransformer -from dj_rql.constants import ComparisonOperators, LogicalOperators def logical_transform(tpl, operator=None, exp1=None, exp2=None): diff --git a/tests/test_parser/test_ordering.py b/tests/test_parser/test_ordering.py index a6ed84c..94b59ad 100644 --- a/tests/test_parser/test_ordering.py +++ b/tests/test_parser/test_ordering.py @@ -1,12 +1,14 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from lark.exceptions import LarkError - from dj_rql.constants import RQL_ORDERING_OPERATOR from dj_rql.parser import RQLParser + +from lark.exceptions import LarkError + +import pytest + from tests.test_parser.constants import FAIL_PROPS, OK_PROPS from tests.test_parser.utils import OrderingTransformer @@ -23,10 +25,10 @@ def ordering_transform(props): @pytest.mark.parametrize('p1,p2', zip(OK_PROPS, REVERSED_OK_PROPS)) def test_ordering_ok(p1, p2): - assert ordering_transform(('+{}'.format(p1),)) == (p1,) - assert ordering_transform(('-{}'.format(p2),)) == ('-{}'.format(p2),) + assert ordering_transform(('+{0}'.format(p1),)) == (p1,) + assert ordering_transform(('-{0}'.format(p2),)) == ('-{0}'.format(p2),) assert ordering_transform((p2, p1)) == (p2, p1) - assert ordering_transform((p2, p1, '+{}'.format(p2))) == (p2, p1, p2) + assert ordering_transform((p2, p1, '+{0}'.format(p2))) == (p2, p1, p2) def test_ordering_empty_ok(): @@ -42,4 +44,4 @@ def test_ordering_property_fail(prop): # Temporary is here @pytest.mark.parametrize('prop', ('', 'prop')) def test_select_ok(prop): - RQLParser.parse('select({})'.format(prop)) + RQLParser.parse('select({0})'.format(prop)) diff --git a/tests/test_parser/test_searching.py b/tests/test_parser/test_searching.py index 508839d..731522c 100644 --- a/tests/test_parser/test_searching.py +++ b/tests/test_parser/test_searching.py @@ -1,12 +1,14 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from lark.exceptions import LarkError - from dj_rql.constants import SearchOperators from dj_rql.parser import RQLParser + +from lark.exceptions import LarkError + +import pytest + from tests.test_parser.constants import FAIL_PROPS, FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import SearchTransformer diff --git a/tests/test_qs.py b/tests/test_qs.py index 67841b4..caa210c 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -1,22 +1,24 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # -import pytest -from django.db.models import IntegerField, Value, Prefetch - from dj_rql.qs import ( Annotation, Chain, DBOptimization, - PrefetchRelated, + NPR, + NSR, NestedPrefetchRelated, NestedSelectRelated, + PrefetchRelated, SelectRelated, - NPR, - NSR, _NestedOptimizationMixin, ) + +from django.db.models import IntegerField, Prefetch, Value + +import pytest + from tests.dj_rf.models import Book diff --git a/tests/test_utils.py b/tests/test_utils.py index 9f8be85..a086625 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,11 @@ # -# Copyright © 2020 Ingram Micro Inc. All rights reserved. +# Copyright © 2021 Ingram Micro Inc. All rights reserved. # +from dj_rql.utils import assert_filter_cls + import pytest -from dj_rql.utils import assert_filter_cls from tests.data import get_book_filter_cls_ordering_data, get_book_filter_cls_search_data from tests.dj_rf.filters import BooksFilterClass @@ -62,5 +63,7 @@ def test_mismatch_for_fields(): with pytest.raises(AssertionError) as e: assert_filter_cls(BooksFilterClass, mismatch, set(), set()) - assert str(e.value) == "Wrong filter `id` configuration: assertion data " \ - "must contain `orm_route` and `lookups`." + assert str(e.value) == ( + "Wrong filter `id` configuration: assertion data " + "must contain `orm_route` and `lookups`." + )