1from __future__ import unicode_literals
2
3import re
4from decimal import Decimal
5
6import ply.yacc as yacc
7
8from .ast import *  # noqa
9from .compat import binary_type, text_type
10from .exceptions import DjangoQLParserError
11from .lexer import DjangoQLLexer
12
13
14unescape_pattern = re.compile(
15    '(' +  DjangoQLLexer.re_escaped_char + '|' +
16    DjangoQLLexer.re_escaped_unicode + ')'
17)
18
19
20def unescape_repl(m):
21    contents = m.group(1)
22    if len(contents) == 2:
23        return contents[1]
24    else:
25        return contents.encode('utf8').decode('unicode_escape')
26
27
28def unescape(value):
29    if isinstance(value, binary_type):
30        value = value.decode('utf8')
31    return re.sub(unescape_pattern, unescape_repl, value)
32
33
34class DjangoQLParser(object):
35    def __init__(self, debug=False, **kwargs):
36        self.default_lexer = DjangoQLLexer()
37        self.tokens = self.default_lexer.tokens
38        kwargs['debug'] = debug
39        if 'write_tables' not in kwargs:
40            kwargs['write_tables'] = False
41        self.yacc = yacc.yacc(module=self, **kwargs)
42
43    def parse(self, input=None, lexer=None, **kwargs):
44        lexer = lexer or self.default_lexer
45        return self.yacc.parse(input=input, lexer=lexer, **kwargs)
46
47    start = 'expression'
48
49    def p_expression_parens(self, p):
50        """
51        expression : PAREN_L expression PAREN_R
52        """
53        p[0] = p[2]
54
55    def p_expression_logical(self, p):
56        """
57        expression : expression logical expression
58        """
59        p[0] = Expression(left=p[1], operator=p[2], right=p[3])
60
61    def p_expression_comparison(self, p):
62        """
63        expression : name comparison_number number
64                   | name comparison_string string
65                   | name comparison_equality boolean_value
66                   | name comparison_equality none
67                   | name comparison_in_list const_list_value
68        """
69        p[0] = Expression(left=p[1], operator=p[2], right=p[3])
70
71    def p_name(self, p):
72        """
73        name : NAME
74        """
75        p[0] = Name(parts=p[1].split('.'))
76
77    def p_logical(self, p):
78        """
79        logical : AND
80                | OR
81        """
82        p[0] = Logical(operator=p[1])
83
84    def p_comparison_number(self, p):
85        """
86        comparison_number : comparison_equality
87                          | comparison_greater_less
88        """
89        p[0] = p[1]
90
91    def p_comparison_string(self, p):
92        """
93        comparison_string : comparison_equality
94                          | comparison_greater_less
95                          | comparison_contains
96        """
97        p[0] = p[1]
98
99    def p_comparison_equality(self, p):
100        """
101        comparison_equality : EQUALS
102                            | NOT_EQUALS
103        """
104        p[0] = Comparison(operator=p[1])
105
106    def p_comparison_greater_less(self, p):
107        """
108        comparison_greater_less : GREATER
109                                | GREATER_EQUAL
110                                | LESS
111                                | LESS_EQUAL
112        """
113        p[0] = Comparison(operator=p[1])
114
115    def p_comparison_contains(self, p):
116        """
117        comparison_contains : CONTAINS
118                            | NOT_CONTAINS
119        """
120        p[0] = Comparison(operator=p[1])
121
122    def p_comparison_in_list(self, p):
123        """
124        comparison_in_list : IN
125                           | NOT IN
126        """
127        if len(p) == 2:
128            p[0] = Comparison(operator=p[1])
129        else:
130            p[0] = Comparison(operator='%s %s' % (p[1], p[2]))
131
132    def p_const_value(self, p):
133        """
134        const_value : number
135                    | string
136                    | none
137                    | boolean_value
138        """
139        p[0] = p[1]
140
141    def p_number_int(self, p):
142        """
143        number : INT_VALUE
144        """
145        p[0] = Const(value=int(p[1]))
146
147    def p_number_float(self, p):
148        """
149        number : FLOAT_VALUE
150        """
151        p[0] = Const(value=Decimal(p[1]))
152
153    def p_string(self, p):
154        """
155        string : STRING_VALUE
156        """
157        p[0] = Const(value=unescape(p[1]))
158
159    def p_none(self, p):
160        """
161        none : NONE
162        """
163        p[0] = Const(value=None)
164
165    def p_boolean_value(self, p):
166        """
167        boolean_value : true
168                      | false
169        """
170        p[0] = p[1]
171
172    def p_true(self, p):
173        """
174        true : TRUE
175        """
176        p[0] = Const(value=True)
177
178    def p_false(self, p):
179        """
180        false : FALSE
181        """
182        p[0] = Const(value=False)
183
184    def p_const_list_value(self, p):
185        """
186        const_list_value : PAREN_L const_value_list PAREN_R
187        """
188        p[0] = List(items=p[2])
189
190    def p_const_value_list(self, p):
191        """
192        const_value_list : const_value_list COMMA const_value
193        """
194        p[0] = p[1] + [p[3]]
195
196    def p_const_value_list_single(self, p):
197        """
198        const_value_list : const_value
199        """
200        p[0] = [p[1]]
201
202    def p_error(self, token):
203        if token is None:
204            self.raise_syntax_error('Unexpected end of input')
205        else:
206            fragment = text_type(token.value)
207            if len(fragment) > 20:
208                fragment = fragment[:17] + '...'
209            self.raise_syntax_error(
210                'Syntax error at %s' % repr(fragment),
211                token=token,
212            )
213
214    def raise_syntax_error(self, message, token=None):
215        if token is None:
216            raise DjangoQLParserError(message)
217        lexer = token.lexer
218        if callable(getattr(lexer, 'find_column', None)):
219            column = lexer.find_column(token)
220        else:
221            column = None
222        raise DjangoQLParserError(
223            message=message,
224            value=token.value,
225            line=token.lineno,
226            column=column,
227        )
228