diff --git a/CHANGES b/CHANGES index 9fd60a2ca..07e822f40 100644 --- a/CHANGES +++ b/CHANGES @@ -36,12 +36,46 @@ Deprecated Features added -------------- +- #10286: C++, support requires clauses not just between the template + parameter lists and the declaration. + Bugs fixed ---------- Testing -------- +Release 5.1.2 (in development) +============================== + +Dependencies +------------ + +Incompatible changes +-------------------- + +Deprecated +---------- + +Features added +-------------- + +Bugs fixed +---------- + +Testing +-------- + +Release 5.1.1 (released Jul 26, 2022) +===================================== + +Bugs fixed +---------- + +* #10701: Fix ValueError in the new ``deque`` based ``sphinx.ext.napolean`` + iterator implementation. +* #10702: Restore compatability with third-party builders. + Release 5.1.0 (released Jul 24, 2022) ===================================== diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 62a369a91..2aede5c24 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -89,10 +89,11 @@ class Builder: self.env: BuildEnvironment = env self.env.set_versioning_method(self.versioning_method, self.versioning_compare) - elif env is not Ellipsis: + else: # ... is passed by SphinxComponentRegistry.create_builder to not show two warnings. warnings.warn("The 'env' argument to Builder will be required from Sphinx 7.", RemovedInSphinx70Warning, stacklevel=2) + self.env = None self.events: EventManager = app.events self.config: Config = app.config self.tags: Tags = app.tags diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index d7dbc8b48..2534f6239 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -3687,17 +3687,23 @@ class ASTTemplateParamNonType(ASTTemplateParam): class ASTTemplateParams(ASTBase): - def __init__(self, params: List[ASTTemplateParam]) -> None: + def __init__(self, params: List[ASTTemplateParam], + requiresClause: Optional["ASTRequiresClause"]) -> None: assert params is not None self.params = params + self.requiresClause = requiresClause - def get_id(self, version: int) -> str: + def get_id(self, version: int, excludeRequires: bool = False) -> str: assert version >= 2 res = [] res.append("I") for param in self.params: res.append(param.get_id(version)) res.append("E") + if not excludeRequires and self.requiresClause: + res.append('IQ') + res.append(self.requiresClause.expr.get_id(version)) + res.append('E') return ''.join(res) def _stringify(self, transform: StringifyTransform) -> str: @@ -3705,6 +3711,9 @@ class ASTTemplateParams(ASTBase): res.append("template<") res.append(", ".join(transform(a) for a in self.params)) res.append("> ") + if self.requiresClause is not None: + res.append(transform(self.requiresClause)) + res.append(" ") return ''.join(res) def describe_signature(self, signode: TextElement, mode: str, @@ -3719,6 +3728,9 @@ class ASTTemplateParams(ASTBase): first = False param.describe_signature(signode, mode, env, symbol) signode += addnodes.desc_sig_punctuation('>', '>') + if self.requiresClause is not None: + signode += addnodes.desc_sig_space() + self.requiresClause.describe_signature(signode, mode, env, symbol) def describe_signature_as_introducer( self, parentNode: desc_signature, mode: str, env: "BuildEnvironment", @@ -3743,6 +3755,11 @@ class ASTTemplateParams(ASTBase): if lineSpec and not first: lineNode = makeLine(parentNode) lineNode += addnodes.desc_sig_punctuation('>', '>') + if self.requiresClause: + reqNode = addnodes.desc_signature_line() + reqNode.sphinx_line_type = 'requiresClause' + parentNode += reqNode + self.requiresClause.describe_signature(reqNode, 'markType', env, symbol) # Template introducers @@ -3861,12 +3878,24 @@ class ASTTemplateDeclarationPrefix(ASTBase): # templates is None means it's an explicit instantiation of a variable self.templates = templates - def get_id(self, version: int) -> str: + def get_requires_clause_in_last(self) -> Optional["ASTRequiresClause"]: + if self.templates is None: + return None + lastList = self.templates[-1] + if not isinstance(lastList, ASTTemplateParams): + return None + return lastList.requiresClause # which may be None + + def get_id_except_requires_clause_in_last(self, version: int) -> str: assert version >= 2 - # this is not part of a normal name mangling system + # This is not part of the Itanium ABI mangling system. res = [] - for t in self.templates: - res.append(t.get_id(version)) + lastIndex = len(self.templates) - 1 + for i, t in enumerate(self.templates): + if isinstance(t, ASTTemplateParams): + res.append(t.get_id(version, excludeRequires=(i == lastIndex))) + else: + res.append(t.get_id(version)) return ''.join(res) def _stringify(self, transform: StringifyTransform) -> str: @@ -3889,7 +3918,7 @@ class ASTRequiresClause(ASTBase): def _stringify(self, transform: StringifyTransform) -> str: return 'requires ' + transform(self.expr) - def describe_signature(self, signode: addnodes.desc_signature_line, mode: str, + def describe_signature(self, signode: nodes.TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode += addnodes.desc_sig_keyword('requires', 'requires') signode += addnodes.desc_sig_space() @@ -3900,16 +3929,16 @@ class ASTRequiresClause(ASTBase): ################################################################################ class ASTDeclaration(ASTBase): - def __init__(self, objectType: str, directiveType: str, visibility: str, - templatePrefix: ASTTemplateDeclarationPrefix, - requiresClause: ASTRequiresClause, declaration: Any, - trailingRequiresClause: ASTRequiresClause, + def __init__(self, objectType: str, directiveType: Optional[str] = None, + visibility: Optional[str] = None, + templatePrefix: Optional[ASTTemplateDeclarationPrefix] = None, + declaration: Any = None, + trailingRequiresClause: Optional[ASTRequiresClause] = None, semicolon: bool = False) -> None: self.objectType = objectType self.directiveType = directiveType self.visibility = visibility self.templatePrefix = templatePrefix - self.requiresClause = requiresClause self.declaration = declaration self.trailingRequiresClause = trailingRequiresClause self.semicolon = semicolon @@ -3920,11 +3949,10 @@ class ASTDeclaration(ASTBase): def clone(self) -> "ASTDeclaration": templatePrefixClone = self.templatePrefix.clone() if self.templatePrefix else None - requiresClasueClone = self.requiresClause.clone() if self.requiresClause else None trailingRequiresClasueClone = self.trailingRequiresClause.clone() \ if self.trailingRequiresClause else None return ASTDeclaration(self.objectType, self.directiveType, self.visibility, - templatePrefixClone, requiresClasueClone, + templatePrefixClone, self.declaration.clone(), trailingRequiresClasueClone, self.semicolon) @@ -3940,7 +3968,7 @@ class ASTDeclaration(ASTBase): def get_id(self, version: int, prefixed: bool = True) -> str: if version == 1: - if self.templatePrefix: + if self.templatePrefix or self.trailingRequiresClause: raise NoOldIdError() if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: return self.enumeratorScopedSymbol.declaration.get_id(version) @@ -3952,16 +3980,31 @@ class ASTDeclaration(ASTBase): res = [_id_prefix[version]] else: res = [] - if self.templatePrefix: - res.append(self.templatePrefix.get_id(version)) - if self.requiresClause or self.trailingRequiresClause: + # (See also https://github.com/sphinx-doc/sphinx/pull/10286#issuecomment-1168102147) + # The first implementation of requires clauses only supported a single clause after the + # template prefix, and no trailing clause. It put the ID after the template parameter + # list, i.e., + # "I" + template_parameter_list_id + "E" + "IQ" + requires_clause_id + "E" + # but the second implementation associates the requires clause with each list, i.e., + # "I" + template_parameter_list_id + "IQ" + requires_clause_id + "E" + "E" + # To avoid making a new ID version, we make an exception for the last requires clause + # in the template prefix, and still put it in the end. + # As we now support trailing requires clauses we add that as if it was a conjunction. + if self.templatePrefix is not None: + res.append(self.templatePrefix.get_id_except_requires_clause_in_last(version)) + requiresClauseInLast = self.templatePrefix.get_requires_clause_in_last() + else: + requiresClauseInLast = None + + if requiresClauseInLast or self.trailingRequiresClause: if version < 4: raise NoOldIdError() res.append('IQ') - if self.requiresClause and self.trailingRequiresClause: + if requiresClauseInLast and self.trailingRequiresClause: + # make a conjunction of them res.append('aa') - if self.requiresClause: - res.append(self.requiresClause.expr.get_id(version)) + if requiresClauseInLast: + res.append(requiresClauseInLast.expr.get_id(version)) if self.trailingRequiresClause: res.append(self.trailingRequiresClause.expr.get_id(version)) res.append('E') @@ -3978,9 +4021,6 @@ class ASTDeclaration(ASTBase): res.append(' ') if self.templatePrefix: res.append(transform(self.templatePrefix)) - if self.requiresClause: - res.append(transform(self.requiresClause)) - res.append(' ') res.append(transform(self.declaration)) if self.trailingRequiresClause: res.append(' ') @@ -4005,11 +4045,6 @@ class ASTDeclaration(ASTBase): self.templatePrefix.describe_signature(signode, mode, env, symbol=self.symbol, lineSpec=options.get('tparam-line-spec')) - if self.requiresClause: - reqNode = addnodes.desc_signature_line() - reqNode.sphinx_line_type = 'requiresClause' - signode.append(reqNode) - self.requiresClause.describe_signature(reqNode, 'markType', env, self.symbol) signode += mainDeclNode if self.visibility and self.visibility != "public": mainDeclNode += addnodes.desc_sig_keyword(self.visibility, self.visibility) @@ -4192,7 +4227,7 @@ class Symbol: continue # only add a declaration if we our self are from a declaration if self.declaration: - decl = ASTDeclaration('templateParam', None, None, None, None, tp, None) + decl = ASTDeclaration(objectType='templateParam', declaration=tp) else: decl = None nne = ASTNestedNameElement(tp.get_identifier(), None) @@ -4207,7 +4242,7 @@ class Symbol: if nn is None: continue # (comparing to the template params: we have checked that we are a declaration) - decl = ASTDeclaration('functionParam', None, None, None, None, fp, None) + decl = ASTDeclaration(objectType='functionParam', declaration=fp) assert not nn.rooted assert len(nn.names) == 1 self._add_symbols(nn, [], decl, self.docname, self.line) @@ -6504,7 +6539,14 @@ class DefinitionParser(BaseParser): declSpecs = self._parse_decl_specs(outer=outer, typed=False) decl = self._parse_declarator(named=True, paramMode=outer, typed=False) - self.assert_end(allowSemicolon=True) + mustEnd = True + if outer == 'function': + # Allow trailing requires on functions. + self.skip_ws() + if re.compile(r'requires\b').match(self.definition, self.pos): + mustEnd = False + if mustEnd: + self.assert_end(allowSemicolon=True) except DefinitionError as exUntyped: if outer == 'type': desc = "If just a name" @@ -6761,7 +6803,8 @@ class DefinitionParser(BaseParser): err = eParam self.skip_ws() if self.skip_string('>'): - return ASTTemplateParams(templateParams) + requiresClause = self._parse_requires_clause() + return ASTTemplateParams(templateParams, requiresClause) elif self.skip_string(','): continue else: @@ -6883,6 +6926,8 @@ class DefinitionParser(BaseParser): return ASTTemplateDeclarationPrefix(None) else: raise e + if objectType == 'concept' and params.requiresClause is not None: + self.fail('requires-clause not allowed for concept') else: params = self._parse_template_introduction() if not params: @@ -6931,7 +6976,7 @@ class DefinitionParser(BaseParser): newTemplates: List[Union[ASTTemplateParams, ASTTemplateIntroduction]] = [] for _i in range(numExtra): - newTemplates.append(ASTTemplateParams([])) + newTemplates.append(ASTTemplateParams([], requiresClause=None)) if templatePrefix and not isMemberInstantiation: newTemplates.extend(templatePrefix.templates) templatePrefix = ASTTemplateDeclarationPrefix(newTemplates) @@ -6947,7 +6992,6 @@ class DefinitionParser(BaseParser): raise Exception('Internal error, unknown directiveType "%s".' % directiveType) visibility = None templatePrefix = None - requiresClause = None trailingRequiresClause = None declaration: Any = None @@ -6955,10 +6999,8 @@ class DefinitionParser(BaseParser): if self.match(_visibility_re): visibility = self.matched_text - if objectType in ('type', 'concept', 'member', 'function', 'class'): + if objectType in ('type', 'concept', 'member', 'function', 'class', 'union'): templatePrefix = self._parse_template_declaration_prefix(objectType) - if objectType == 'function' and templatePrefix is not None: - requiresClause = self._parse_requires_clause() if objectType == 'type': prevErrors = [] @@ -6984,8 +7026,7 @@ class DefinitionParser(BaseParser): declaration = self._parse_type_with_init(named=True, outer='member') elif objectType == 'function': declaration = self._parse_type(named=True, outer='function') - if templatePrefix is not None: - trailingRequiresClause = self._parse_requires_clause() + trailingRequiresClause = self._parse_requires_clause() elif objectType == 'class': declaration = self._parse_class() elif objectType == 'union': @@ -7003,7 +7044,7 @@ class DefinitionParser(BaseParser): self.skip_ws() semicolon = self.skip_string(';') return ASTDeclaration(objectType, directiveType, visibility, - templatePrefix, requiresClause, declaration, + templatePrefix, declaration, trailingRequiresClause, semicolon) def parse_namespace_object(self) -> ASTNamespace: @@ -8048,7 +8089,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 6, + 'env_version': 7, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 441fa7f55..652fd92b1 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -44,7 +44,11 @@ _SINGLETONS = ("None", "True", "False", "Ellipsis") class Deque(collections.deque): - """A subclass of deque with an additional `.Deque.get` method.""" + """ + A subclass of deque that mimics ``pockets.iterators.modify_iter``. + + The `.Deque.get` and `.Deque.next` methods are added. + """ sentinel = object() @@ -55,6 +59,12 @@ class Deque(collections.deque): """ return self[n] if n < len(self) else self.sentinel + def next(self) -> Any: + if self: + return super().popleft() + else: + raise StopIteration + def _convert_type_spec(_type: str, translations: Dict[str, str] = {}) -> str: """Convert type specification to reference in reST.""" @@ -238,7 +248,7 @@ class GoogleDocstring: line = self._lines.get(0) while(not self._is_section_break() and (not line or self._is_indented(line, indent))): - lines.append(self._lines.popleft()) + lines.append(self._lines.next()) line = self._lines.get(0) return lines @@ -247,20 +257,20 @@ class GoogleDocstring: while (self._lines and self._lines.get(0) and not self._is_section_header()): - lines.append(self._lines.popleft()) + lines.append(self._lines.next()) return lines def _consume_empty(self) -> List[str]: lines = [] line = self._lines.get(0) while self._lines and not line: - lines.append(self._lines.popleft()) + lines.append(self._lines.next()) line = self._lines.get(0) return lines def _consume_field(self, parse_type: bool = True, prefer_type: bool = False ) -> Tuple[str, str, List[str]]: - line = self._lines.popleft() + line = self._lines.next() before, colon, after = self._partition_field_on_colon(line) _name, _type, _desc = before, '', after @@ -298,7 +308,7 @@ class GoogleDocstring: return fields def _consume_inline_attribute(self) -> Tuple[str, List[str]]: - line = self._lines.popleft() + line = self._lines.next() _type, colon, _desc = self._partition_field_on_colon(line) if not colon or not _desc: _type, _desc = _desc, _type @@ -336,7 +346,7 @@ class GoogleDocstring: return lines def _consume_section_header(self) -> str: - section = self._lines.popleft() + section = self._lines.next() stripped_section = section.strip(':') if stripped_section.lower() in self._sections: section = stripped_section @@ -345,14 +355,14 @@ class GoogleDocstring: def _consume_to_end(self) -> List[str]: lines = [] while self._lines: - lines.append(self._lines.popleft()) + lines.append(self._lines.next()) return lines def _consume_to_next_section(self) -> List[str]: self._consume_empty() lines = [] while not self._is_section_break(): - lines.append(self._lines.popleft()) + lines.append(self._lines.next()) return lines + self._consume_empty() def _dedent(self, lines: List[str], full: bool = False) -> List[str]: @@ -1155,7 +1165,7 @@ class NumpyDocstring(GoogleDocstring): def _consume_field(self, parse_type: bool = True, prefer_type: bool = False ) -> Tuple[str, str, List[str]]: - line = self._lines.popleft() + line = self._lines.next() if parse_type: _name, _, _type = self._partition_field_on_colon(line) else: @@ -1186,10 +1196,10 @@ class NumpyDocstring(GoogleDocstring): return self._consume_fields(prefer_type=True) def _consume_section_header(self) -> str: - section = self._lines.popleft() + section = self._lines.next() if not _directive_regex.match(section): # Consume the header underline - self._lines.popleft() + self._lines.next() return section def _is_section_break(self) -> bool: diff --git a/sphinx/registry.py b/sphinx/registry.py index 8383a553e..170d6eaad 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -161,7 +161,7 @@ class SphinxComponentRegistry: f"'env'argument. Report this bug to the developers of your custom builder, " f"this is likely not a issue with Sphinx. The 'env' argument will be required " f"from Sphinx 7.", RemovedInSphinx70Warning, stacklevel=2) - builder = self.builders[name](app, env=...) # type: ignore[arg-type] + builder = self.builders[name](app) if env is not None: builder.set_environment(env) return builder diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 3f31aaa18..ee8da3d39 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -891,8 +891,33 @@ def test_domain_cpp_ast_requires_clauses(): {4: 'I0EIQaa1A1BE1fvv'}) check('function', 'template requires A || B or C void f()', {4: 'I0EIQoo1Aoo1B1CE1fvv'}) + check('function', 'void f() requires A || B || C', + {4: 'IQoo1Aoo1B1CE1fv'}) + check('function', 'Foo() requires A || B || C', + {4: 'IQoo1Aoo1B1CE3Foov'}) check('function', 'template requires A && B || C and D void f()', {4: 'I0EIQooaa1A1Baa1C1DE1fvv'}) + check('function', + 'template requires R ' + + 'template requires S ' + + 'void A::f() requires B', + {4: 'I0EIQ1RI1TEEI0EIQaa1SI1TE1BEN1AI1TE1fEvv'}) + check('function', + 'template requires R typename X> ' + + 'void f()', + {2: 'II0EIQ1RI1TEE0E1fv', 4: 'II0EIQ1RI1TEE0E1fvv'}) + check('type', + 'template requires IsValid {key}T = true_type', + {4: 'I0EIQ7IsValidI1TEE1T'}, key='using') + check('class', + 'template requires IsValid {key}T : Base', + {4: 'I0EIQ7IsValidI1TEE1T'}, key='class') + check('union', + 'template requires IsValid {key}T', + {4: 'I0EIQ7IsValidI1TEE1T'}, key='union') + check('member', + 'template requires IsValid int Val = 7', + {4: 'I0EIQ7IsValidI1TEE3Val'}) def test_domain_cpp_ast_template_args():