6
6
"""
7
7
8
8
import itertools
9
+ import json
9
10
import re
10
11
from abc import abstractmethod
11
12
from typing import Any , Dict , Generator , Iterable , List , Optional , Tuple , Union
12
13
13
14
from django import VERSION as DJANGO_VERSION
14
15
from django .conf import settings
16
+ from django .core .serializers .json import DjangoJSONEncoder
15
17
from django .urls import URLPattern , URLResolver , reverse
16
18
from django .urls .exceptions import NoReverseMatch
17
19
from django .urls .resolvers import RegexPattern , RoutePattern
@@ -386,7 +388,8 @@ def visit_pattern( # pylint: disable=R0914, R0915, R0912
386
388
endpoint : URLPattern ,
387
389
qname : str ,
388
390
app_name : Optional [str ],
389
- route : List [RoutePattern ]
391
+ route : List [RoutePattern ],
392
+ num_patterns : int
390
393
) -> Generator [Optional [str ], None , None ]:
391
394
"""
392
395
Visit a pattern. Translates the pattern into a path component string
@@ -564,7 +567,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[
564
567
565
568
yield from self .visit_path (
566
569
path , list (kwargs .keys ()),
567
- endpoint .default_args
570
+ endpoint .default_args if num_patterns > 1 else None
568
571
)
569
572
570
573
else :
@@ -693,7 +696,7 @@ def visit_path_group(
693
696
yield from self .enter_path_group (qname )
694
697
for pattern in reversed (nodes ):
695
698
yield from self .visit_pattern (
696
- pattern , qname , app_name , route or []
699
+ pattern , qname , app_name , route or [], num_patterns = len ( nodes )
697
700
)
698
701
yield from self .exit_path_group (qname )
699
702
@@ -945,7 +948,9 @@ def visit_path(
945
948
yield f'return { quote } /{ self .path_join (path ).lstrip ("/" )} { quote } ;'
946
949
self .outdent ()
947
950
else :
948
- opts_str = "," .join ([f"'{ param } '" for param in kwargs ])
951
+ opts_str = "," .join (
952
+ [self .to_javascript (param ) for param in kwargs ]
953
+ )
949
954
yield (
950
955
f'if (Object.keys(kwargs).length === { len (kwargs )} && '
951
956
f'[{ opts_str } ].every(value => '
@@ -1093,6 +1098,71 @@ def reverse_jdoc(self) -> Generator[Optional[str], None, None]:
1093
1098
*/""" .split ('\n ' ):
1094
1099
yield comment_line [8 :]
1095
1100
1101
+
1102
+ def deep_equal (self ) -> Generator [Optional [str ], None , None ]:
1103
+ """
1104
+ The recursive deepEqual function.
1105
+ :yield: The JavaScript jdoc comment lines and deepEqual function.
1106
+ """
1107
+ for comment_line in """
1108
+ /**
1109
+ * Given two values, do a deep equality comparison. If the values are
1110
+ * objects, all keys and values are recursively compared.
1111
+ *
1112
+ * @param {Object} object1 - The first object to compare.
1113
+ * @param {Object} object2 - The second object to compare.
1114
+ */""" .split ('\n ' ):
1115
+ yield comment_line [8 :]
1116
+ yield 'deepEqual(object1, object2) {'
1117
+ self .indent ()
1118
+ yield 'if (!(this.isObject(object1) && this.isObject(object2))) {'
1119
+ self .indent ()
1120
+ yield 'return object1 === object2;'
1121
+ self .outdent ()
1122
+ yield '}'
1123
+ yield 'const keys1 = Object.keys(object1);'
1124
+ yield 'const keys2 = Object.keys(object2);'
1125
+ yield 'if (keys1.length !== keys2.length) {'
1126
+ self .indent ()
1127
+ yield 'return false;'
1128
+ self .outdent ()
1129
+ yield '}'
1130
+ yield 'for (let key of keys1) {'
1131
+ self .indent ()
1132
+ yield 'const val1 = object1[key];'
1133
+ yield 'const val2 = object2[key];'
1134
+ yield 'const areObjects = this.isObject(val1) && this.isObject(val2);'
1135
+ yield 'if ('
1136
+ self .indent ()
1137
+ yield '(areObjects && !deepEqual(val1, val2)) ||'
1138
+ yield '(!areObjects && val1 !== val2)'
1139
+ yield ') { return false; }'
1140
+ self .outdent ()
1141
+ yield '}'
1142
+ self .outdent ()
1143
+ yield 'return true;'
1144
+ self .outdent ()
1145
+ yield '}'
1146
+
1147
+ def is_object (self ) -> Generator [Optional [str ], None , None ]:
1148
+ """
1149
+ The isObject() function.
1150
+ :yield: The JavaScript jdoc comment lines and isObject function.
1151
+ """
1152
+ for comment_line in """
1153
+ /**
1154
+ * Given a variable, return true if it is an object.
1155
+ *
1156
+ * @param {Object} object - The variable to check.
1157
+ */""" .split ('\n ' ):
1158
+ yield comment_line [8 :]
1159
+ yield 'isObject(object) {'
1160
+ self .indent ()
1161
+ yield 'return object != null && typeof object === "object";'
1162
+ self .outdent ()
1163
+ yield '}'
1164
+
1165
+
1096
1166
def init_visit ( # pylint: disable=R0915
1097
1167
self
1098
1168
) -> Generator [Optional [str ], None , None ]:
@@ -1159,7 +1229,7 @@ class code.
1159
1229
'{ return false; }'
1160
1230
)
1161
1231
else : # pragma: no cover
1162
- yield 'if (kwargs[key] !== val) { return false; }'
1232
+ yield 'if (!this.deepEqual( kwargs[key], val) ) { return false; }'
1163
1233
yield (
1164
1234
'if (!expected.includes(key)) '
1165
1235
'{ delete kwargs[key]; }'
@@ -1190,6 +1260,10 @@ class code.
1190
1260
self .outdent ()
1191
1261
yield '}'
1192
1262
yield ''
1263
+ yield from self .deep_equal ()
1264
+ yield ''
1265
+ yield from self .is_object ()
1266
+ yield ''
1193
1267
yield from self .reverse_jdoc ()
1194
1268
yield 'reverse(qname, options={}) {'
1195
1269
self .indent ()
@@ -1331,9 +1405,20 @@ def visit_path(
1331
1405
:yield: The JavaScript lines of code
1332
1406
"""
1333
1407
quote = '`'
1408
+ visitor = self
1409
+ class ArgEncoder (DjangoJSONEncoder ):
1410
+ """
1411
+ An encoder that uses the configured to javascript function to
1412
+ convert any unknown types to strings.
1413
+ """
1414
+
1415
+ def default (self , o ):
1416
+ return visitor .to_javascript (o ).rstrip ('"' ).lstrip ('"' )
1417
+
1418
+ defaults_str = json .dumps (defaults , cls = ArgEncoder )
1334
1419
if len (path ) == 1 : # there are no substitutions
1335
1420
if defaults :
1336
- yield f'if (this.#match(kwargs, args, [], { defaults } )) ' \
1421
+ yield f'if (this.#match(kwargs, args, [], { defaults_str } )) ' \
1337
1422
f'{{ return "/{ str (path [0 ]).lstrip ("/" )} "; }}'
1338
1423
else :
1339
1424
yield f'if (this.#match(kwargs, args)) ' \
@@ -1351,11 +1436,13 @@ def visit_path(
1351
1436
f'{ quote } ; }}'
1352
1437
)
1353
1438
else :
1354
- opts_str = "," .join ([f"'{ param } '" for param in kwargs ])
1439
+ opts_str = "," .join (
1440
+ [self .to_javascript (param ) for param in kwargs ]
1441
+ )
1355
1442
if defaults :
1356
1443
yield (
1357
- f'if (this.#match(kwargs, args, [{ opts_str } ], { defaults } )) '
1358
- f' {{'
1444
+ f'if (this.#match(kwargs, args, [{ opts_str } ], '
1445
+ f'{ defaults_str } )) {{'
1359
1446
f' return { quote } /{ self .path_join (path ).lstrip ("/" )} '
1360
1447
f'{ quote } ; }}'
1361
1448
)
0 commit comments