diff --git a/CHANGES b/CHANGES index 736359ce6..ea938f6a2 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,9 @@ Release 1.0.3 (in development) ============================== +* #507: Fix crash parsing Python argument lists containing brackets + in string literals. + * #501: Fix regression when building LaTeX docs with figures that don't have captions. diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 582e2adc6..98c7948c2 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -13,8 +13,8 @@ from sphinx import addnodes from sphinx.domains import Domain, ObjType from sphinx.locale import l_, _ from sphinx.directives import ObjectDescription -from sphinx.domains.python import py_paramlist_re as js_paramlist_re from sphinx.roles import XRefRole +from sphinx.domains.python import _pseudo_parse_arglist from sphinx.util.nodes import make_refnode from sphinx.util.docfields import Field, GroupedField, TypedField @@ -68,28 +68,10 @@ class JSObject(ObjectDescription): signode += addnodes.desc_addname(nameprefix + '.', nameprefix + '.') signode += addnodes.desc_name(name, name) if self.has_arguments: - signode += addnodes.desc_parameterlist() - if not arglist: - return fullname, nameprefix - - stack = [signode[-1]] - for token in js_paramlist_re.split(arglist): - if token == '[': - opt = addnodes.desc_optional() - stack[-1] += opt - stack.append(opt) - elif token == ']': - try: - stack.pop() - except IndexError: - raise ValueError() - elif not token or token == ',' or token.isspace(): - pass + if not arglist: + signode += addnodes.desc_parameterlist() else: - token = token.strip() - stack[-1] += addnodes.desc_parameter(token, token) - if len(stack) != 1: - raise ValueError() + _pseudo_parse_arglist(signode, arglist) return fullname, nameprefix def add_target_and_index(self, name_obj, sig, signode): diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index cd87bfbda..cb34492f9 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -33,7 +33,52 @@ py_sig_re = re.compile( )? $ # and nothing more ''', re.VERBOSE) -py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' + +def _pseudo_parse_arglist(signode, arglist): + """"Parse" a list of arguments separated by commas. + + Arguments can have "optional" annotations given by enclosing them in + brackets. Currently, this will split at any comma, even if it's inside a + string literal (e.g. default argument value). + """ + paramlist = addnodes.desc_parameterlist() + stack = [paramlist] + try: + for argument in arglist.split(','): + argument = argument.strip() + ends_open = ends_close = 0 + while argument.startswith('['): + stack.append(addnodes.desc_optional()) + stack[-2] += stack[-1] + argument = argument[1:].strip() + while argument.startswith(']'): + stack.pop() + argument = argument[1:].strip() + while argument.endswith(']'): + ends_close += 1 + argument = argument[:-1].strip() + while argument.endswith('['): + ends_open += 1 + argument = argument[:-1].strip() + if argument: + stack[-1] += addnodes.desc_parameter(argument, argument) + while ends_open: + stack.append(addnodes.desc_optional()) + stack[-2] += stack[-1] + ends_open -= 1 + while ends_close: + stack.pop() + ends_close -= 1 + if len(stack) != 1: + raise IndexError + except IndexError: + # if there are too few or too many elements on the stack, just give up + # and treat the whole argument list as one argument, discarding the + # already partially populated paramlist node + signode += addnodes.desc_parameterlist() + signode[-1] += addnodes.desc_parameter(arglist, arglist) + else: + signode += paramlist class PyObject(ObjectDescription): @@ -142,26 +187,7 @@ class PyObject(ObjectDescription): if retann: signode += addnodes.desc_returns(retann, retann) return fullname, name_prefix - signode += addnodes.desc_parameterlist() - - stack = [signode[-1]] - for token in py_paramlist_re.split(arglist): - if token == '[': - opt = addnodes.desc_optional() - stack[-1] += opt - stack.append(opt) - elif token == ']': - try: - stack.pop() - except IndexError: - raise ValueError - elif not token or token == ',' or token.isspace(): - pass - else: - token = token.strip() - stack[-1] += addnodes.desc_parameter(token, token) - if len(stack) != 1: - raise ValueError + _pseudo_parse_arglist(signode, arglist) if retann: signode += addnodes.desc_returns(retann, retann) return fullname, name_prefix