66"""
77
88import itertools
9+ import json
910import re
1011from abc import abstractmethod
1112from typing import Any , Dict , Generator , Iterable , List , Optional , Tuple , Union
1213
1314from django import VERSION as DJANGO_VERSION
1415from django .conf import settings
16+ from django .core .serializers .json import DjangoJSONEncoder
1517from django .urls import URLPattern , URLResolver , reverse
1618from django .urls .exceptions import NoReverseMatch
1719from django .urls .resolvers import RegexPattern , RoutePattern
@@ -386,7 +388,8 @@ def visit_pattern( # pylint: disable=R0914, R0915, R0912
386388 endpoint : URLPattern ,
387389 qname : str ,
388390 app_name : Optional [str ],
389- route : List [RoutePattern ]
391+ route : List [RoutePattern ],
392+ num_patterns : int
390393 ) -> Generator [Optional [str ], None , None ]:
391394 """
392395 Visit a pattern. Translates the pattern into a path component string
@@ -564,7 +567,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[
564567
565568 yield from self .visit_path (
566569 path , list (kwargs .keys ()),
567- endpoint .default_args
570+ endpoint .default_args if num_patterns > 1 else None
568571 )
569572
570573 else :
@@ -693,7 +696,7 @@ def visit_path_group(
693696 yield from self .enter_path_group (qname )
694697 for pattern in reversed (nodes ):
695698 yield from self .visit_pattern (
696- pattern , qname , app_name , route or []
699+ pattern , qname , app_name , route or [], num_patterns = len ( nodes )
697700 )
698701 yield from self .exit_path_group (qname )
699702
@@ -945,7 +948,9 @@ def visit_path(
945948 yield f'return { quote } /{ self .path_join (path ).lstrip ("/" )} { quote } ;'
946949 self .outdent ()
947950 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+ )
949954 yield (
950955 f'if (Object.keys(kwargs).length === { len (kwargs )} && '
951956 f'[{ opts_str } ].every(value => '
@@ -1093,6 +1098,71 @@ def reverse_jdoc(self) -> Generator[Optional[str], None, None]:
10931098 */""" .split ('\n ' ):
10941099 yield comment_line [8 :]
10951100
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+
10961166 def init_visit ( # pylint: disable=R0915
10971167 self
10981168 ) -> Generator [Optional [str ], None , None ]:
@@ -1159,7 +1229,7 @@ class code.
11591229 '{ return false; }'
11601230 )
11611231 else : # pragma: no cover
1162- yield 'if (kwargs[key] !== val) { return false; }'
1232+ yield 'if (!this.deepEqual( kwargs[key], val) ) { return false; }'
11631233 yield (
11641234 'if (!expected.includes(key)) '
11651235 '{ delete kwargs[key]; }'
@@ -1190,6 +1260,10 @@ class code.
11901260 self .outdent ()
11911261 yield '}'
11921262 yield ''
1263+ yield from self .deep_equal ()
1264+ yield ''
1265+ yield from self .is_object ()
1266+ yield ''
11931267 yield from self .reverse_jdoc ()
11941268 yield 'reverse(qname, options={}) {'
11951269 self .indent ()
@@ -1331,9 +1405,20 @@ def visit_path(
13311405 :yield: The JavaScript lines of code
13321406 """
13331407 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 )
13341419 if len (path ) == 1 : # there are no substitutions
13351420 if defaults :
1336- yield f'if (this.#match(kwargs, args, [], { defaults } )) ' \
1421+ yield f'if (this.#match(kwargs, args, [], { defaults_str } )) ' \
13371422 f'{{ return "/{ str (path [0 ]).lstrip ("/" )} "; }}'
13381423 else :
13391424 yield f'if (this.#match(kwargs, args)) ' \
@@ -1351,11 +1436,13 @@ def visit_path(
13511436 f'{ quote } ; }}'
13521437 )
13531438 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+ )
13551442 if defaults :
13561443 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 } )) {{'
13591446 f' return { quote } /{ self .path_join (path ).lstrip ("/" )} '
13601447 f'{ quote } ; }}'
13611448 )
0 commit comments