Merge pull request #8330 from sphinx-doc/3.2.x_to_3.x

Merge 3.2.x to 3.x
This commit is contained in:
Takeshi KOMIYA 2020-10-24 17:34:11 +09:00 committed by GitHub
commit 624f693719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 49 deletions

View File

@ -24,9 +24,6 @@ jobs:
env: env:
- TOXENV=du15 - TOXENV=du15
- PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg" - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg"
- python: 'nightly'
env:
- TOXENV=du16
- language: node_js - language: node_js
node_js: '10.7' node_js: '10.7'

View File

@ -84,6 +84,11 @@ Bugs fixed
* #8188: C, add missing items to internal object types dictionary, * #8188: C, add missing items to internal object types dictionary,
e.g., preventing intersphinx from resolving them. e.g., preventing intersphinx from resolving them.
* C, fix anon objects in intersphinx.
* #8270, C++, properly reject functions as duplicate declarations if a
non-function declaration of the same name already exists.
* C, fix references to function parameters.
Link to the function instead of a non-existing anchor.
Testing Testing

View File

@ -44,7 +44,7 @@ extras_require = {
'lint': [ 'lint': [
'flake8>=3.5.0', 'flake8>=3.5.0',
'flake8-import-order', 'flake8-import-order',
'mypy>=0.780', 'mypy>=0.790',
'docutils-stubs', 'docutils-stubs',
], ],
'test': [ 'test': [

View File

@ -10,9 +10,8 @@
import re import re
from typing import ( from typing import (
Any, Callable, Dict, Generator, Iterator, List, Type, TypeVar, Tuple, Union Any, Callable, cast, Dict, Generator, Iterator, List, Type, TypeVar, Tuple, Union
) )
from typing import cast
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node, TextElement, system_message from docutils.nodes import Element, Node, TextElement, system_message
@ -47,6 +46,11 @@ from sphinx.util.nodes import make_refnode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar('T') T = TypeVar('T')
DeclarationType = Union[
"ASTStruct", "ASTUnion", "ASTEnum", "ASTEnumerator",
"ASTType", "ASTTypeWithInit", "ASTMacro",
]
# https://en.cppreference.com/w/c/keyword # https://en.cppreference.com/w/c/keyword
_keywords = [ _keywords = [
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double',
@ -636,6 +640,10 @@ class ASTFunctionParameter(ASTBase):
self.arg = arg self.arg = arg
self.ellipsis = ellipsis self.ellipsis = ellipsis
def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str:
# the anchor will be our parent
return symbol.parent.declaration.get_id(version, prefixed=False)
def _stringify(self, transform: StringifyTransform) -> str: def _stringify(self, transform: StringifyTransform) -> str:
if self.ellipsis: if self.ellipsis:
return '...' return '...'
@ -1149,6 +1157,9 @@ class ASTType(ASTBase):
def name(self) -> ASTNestedName: def name(self) -> ASTNestedName:
return self.decl.name return self.decl.name
def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str:
return symbol.get_full_nested_name().get_id(version)
@property @property
def function_params(self) -> List[ASTFunctionParameter]: def function_params(self) -> List[ASTFunctionParameter]:
return self.decl.function_params return self.decl.function_params
@ -1191,6 +1202,9 @@ class ASTTypeWithInit(ASTBase):
def name(self) -> ASTNestedName: def name(self) -> ASTNestedName:
return self.type.name return self.type.name
def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str:
return self.type.get_id(version, objectType, symbol)
def _stringify(self, transform: StringifyTransform) -> str: def _stringify(self, transform: StringifyTransform) -> str:
res = [] res = []
res.append(transform(self.type)) res.append(transform(self.type))
@ -1242,6 +1256,9 @@ class ASTMacro(ASTBase):
def name(self) -> ASTNestedName: def name(self) -> ASTNestedName:
return self.ident return self.ident
def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str:
return symbol.get_full_nested_name().get_id(version)
def _stringify(self, transform: StringifyTransform) -> str: def _stringify(self, transform: StringifyTransform) -> str:
res = [] res = []
res.append(transform(self.ident)) res.append(transform(self.ident))
@ -1342,7 +1359,8 @@ class ASTEnumerator(ASTBase):
class ASTDeclaration(ASTBaseBase): class ASTDeclaration(ASTBaseBase):
def __init__(self, objectType: str, directiveType: str, declaration: Any, def __init__(self, objectType: str, directiveType: str,
declaration: Union[DeclarationType, ASTFunctionParameter],
semicolon: bool = False) -> None: semicolon: bool = False) -> None:
self.objectType = objectType self.objectType = objectType
self.directiveType = directiveType self.directiveType = directiveType
@ -1359,18 +1377,20 @@ class ASTDeclaration(ASTBaseBase):
@property @property
def name(self) -> ASTNestedName: def name(self) -> ASTNestedName:
return self.declaration.name decl = cast(DeclarationType, self.declaration)
return decl.name
@property @property
def function_params(self) -> List[ASTFunctionParameter]: def function_params(self) -> List[ASTFunctionParameter]:
if self.objectType != 'function': if self.objectType != 'function':
return None return None
return self.declaration.function_params decl = cast(ASTType, self.declaration)
return decl.function_params
def get_id(self, version: int, prefixed: bool = True) -> str: def get_id(self, version: int, prefixed: bool = True) -> str:
if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: if self.objectType == 'enumerator' and self.enumeratorScopedSymbol:
return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed) return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed)
id_ = self.symbol.get_full_nested_name().get_id(version) id_ = self.declaration.get_id(version, self.objectType, self.symbol)
if prefixed: if prefixed:
return _id_prefix[version] + id_ return _id_prefix[version] + id_
else: else:
@ -1413,7 +1433,8 @@ class ASTDeclaration(ASTBaseBase):
elif self.objectType == 'enumerator': elif self.objectType == 'enumerator':
mainDeclNode += addnodes.desc_annotation('enumerator ', 'enumerator ') mainDeclNode += addnodes.desc_annotation('enumerator ', 'enumerator ')
elif self.objectType == 'type': elif self.objectType == 'type':
prefix = self.declaration.get_type_declaration_prefix() decl = cast(ASTType, self.declaration)
prefix = decl.get_type_declaration_prefix()
prefix += ' ' prefix += ' '
mainDeclNode += addnodes.desc_annotation(prefix, prefix) mainDeclNode += addnodes.desc_annotation(prefix, prefix)
else: else:
@ -2988,7 +3009,7 @@ class DefinitionParser(BaseParser):
def parse_pre_v3_type_definition(self) -> ASTDeclaration: def parse_pre_v3_type_definition(self) -> ASTDeclaration:
self.skip_ws() self.skip_ws()
declaration = None # type: Any declaration = None # type: DeclarationType
if self.skip_word('struct'): if self.skip_word('struct'):
typ = 'struct' typ = 'struct'
declaration = self._parse_struct() declaration = self._parse_struct()
@ -3011,7 +3032,7 @@ class DefinitionParser(BaseParser):
'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'):
raise Exception('Internal error, unknown directiveType "%s".' % directiveType) raise Exception('Internal error, unknown directiveType "%s".' % directiveType)
declaration = None # type: Any declaration = None # type: DeclarationType
if objectType == 'member': if objectType == 'member':
declaration = self._parse_type_with_init(named=True, outer='member') declaration = self._parse_type_with_init(named=True, outer='member')
elif objectType == 'function': elif objectType == 'function':
@ -3158,10 +3179,6 @@ class CObject(ObjectDescription):
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
domain = cast(CDomain, self.env.get_domain('c'))
if name not in domain.objects:
domain.objects[name] = (domain.env.docname, newestId, self.objtype)
if 'noindexentry' not in self.options: if 'noindexentry' not in self.options:
indexText = self.get_index_text(name) indexText = self.get_index_text(name)
self.indexnode['entries'].append(('single', indexText, newestId, '', None)) self.indexnode['entries'].append(('single', indexText, newestId, '', None))
@ -3681,10 +3698,6 @@ class CDomain(Domain):
'objects': {}, # fullname -> docname, node_id, objtype 'objects': {}, # fullname -> docname, node_id, objtype
} # type: Dict[str, Union[Symbol, Dict[str, Tuple[str, str, str]]]] } # type: Dict[str, Union[Symbol, Dict[str, Tuple[str, str, str]]]]
@property
def objects(self) -> Dict[str, Tuple[str, str, str]]:
return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype
def clear_doc(self, docname: str) -> None: def clear_doc(self, docname: str) -> None:
if Symbol.debug_show_tree: if Symbol.debug_show_tree:
print("clear_doc:", docname) print("clear_doc:", docname)
@ -3700,9 +3713,6 @@ class CDomain(Domain):
print(self.data['root_symbol'].dump(1)) print(self.data['root_symbol'].dump(1))
print("\tafter end") print("\tafter end")
print("clear_doc end:", docname) print("clear_doc end:", docname)
for fullname, (fn, _id, _l) in list(self.objects.items()):
if fn == docname:
del self.objects[fullname]
def process_doc(self, env: BuildEnvironment, docname: str, def process_doc(self, env: BuildEnvironment, docname: str,
document: nodes.document) -> None: document: nodes.document) -> None:
@ -3788,8 +3798,18 @@ class CDomain(Domain):
return [] return []
def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
for refname, (docname, node_id, objtype) in list(self.objects.items()): rootSymbol = self.data['root_symbol']
yield (refname, refname, objtype, docname, node_id, 1) for symbol in rootSymbol.get_all_symbols():
if symbol.declaration is None:
continue
assert symbol.docname
fullNestedName = symbol.get_full_nested_name()
name = str(fullNestedName).lstrip('.')
dispname = fullNestedName.get_display_string().lstrip('.')
objectType = symbol.declaration.objectType
docname = symbol.docname
newestId = symbol.declaration.get_newest_id()
yield (name, dispname, objectType, docname, newestId, 1)
def setup(app: Sphinx) -> Dict[str, Any]: def setup(app: Sphinx) -> Dict[str, Any]:

View File

@ -1836,7 +1836,7 @@ class ASTFunctionParameter(ASTBase):
# this is not part of the normal name mangling in C++ # this is not part of the normal name mangling in C++
if symbol: if symbol:
# the anchor will be our parent # the anchor will be our parent
return symbol.parent.declaration.get_id(version, prefixed=None) return symbol.parent.declaration.get_id(version, prefixed=False)
# else, do the usual # else, do the usual
if self.ellipsis: if self.ellipsis:
return 'z' return 'z'
@ -4107,7 +4107,7 @@ class Symbol:
Symbol.debug_print("self:") Symbol.debug_print("self:")
print(self.to_string(Symbol.debug_indent + 1), end="") print(self.to_string(Symbol.debug_indent + 1), end="")
Symbol.debug_print("nestedName: ", nestedName) Symbol.debug_print("nestedName: ", nestedName)
Symbol.debug_print("templateDecls: ", templateDecls) Symbol.debug_print("templateDecls: ", ",".join(str(t) for t in templateDecls))
Symbol.debug_print("strictTemplateParamArgLists:", strictTemplateParamArgLists) Symbol.debug_print("strictTemplateParamArgLists:", strictTemplateParamArgLists)
Symbol.debug_print("ancestorLookupType:", ancestorLookupType) Symbol.debug_print("ancestorLookupType:", ancestorLookupType)
Symbol.debug_print("templateShorthand: ", templateShorthand) Symbol.debug_print("templateShorthand: ", templateShorthand)
@ -4231,7 +4231,7 @@ class Symbol:
Symbol.debug_indent += 1 Symbol.debug_indent += 1
Symbol.debug_print("_add_symbols:") Symbol.debug_print("_add_symbols:")
Symbol.debug_indent += 1 Symbol.debug_indent += 1
Symbol.debug_print("tdecls:", templateDecls) Symbol.debug_print("tdecls:", ",".join(str(t) for t in templateDecls))
Symbol.debug_print("nn: ", nestedName) Symbol.debug_print("nn: ", nestedName)
Symbol.debug_print("decl: ", declaration) Symbol.debug_print("decl: ", declaration)
Symbol.debug_print("doc: ", docname) Symbol.debug_print("doc: ", docname)
@ -4360,6 +4360,11 @@ class Symbol:
if Symbol.debug_lookup: if Symbol.debug_lookup:
Symbol.debug_print("candId:", candId) Symbol.debug_print("candId:", candId)
for symbol in withDecl: for symbol in withDecl:
# but all existing must be functions as well,
# otherwise we declare it to be a duplicate
if symbol.declaration.objectType != 'function':
handleDuplicateDeclaration(symbol, candSymbol)
# (not reachable)
oldId = symbol.declaration.get_newest_id() oldId = symbol.declaration.get_newest_id()
if Symbol.debug_lookup: if Symbol.debug_lookup:
Symbol.debug_print("oldId: ", oldId) Symbol.debug_print("oldId: ", oldId)
@ -4370,7 +4375,11 @@ class Symbol:
# if there is an empty symbol, fill that one # if there is an empty symbol, fill that one
if len(noDecl) == 0: if len(noDecl) == 0:
if Symbol.debug_lookup: if Symbol.debug_lookup:
Symbol.debug_print("no match, no empty, candSybmol is not None?:", candSymbol is not None) # NOQA Symbol.debug_print("no match, no empty")
if candSymbol is not None:
Symbol.debug_print("result is already created candSymbol")
else:
Symbol.debug_print("result is makeCandSymbol()")
Symbol.debug_indent -= 2 Symbol.debug_indent -= 2
if candSymbol is not None: if candSymbol is not None:
return candSymbol return candSymbol
@ -6814,10 +6823,12 @@ class CPPObject(ObjectDescription):
parentSymbol = env.temp_data['cpp:parent_symbol'] parentSymbol = env.temp_data['cpp:parent_symbol']
parentDecl = parentSymbol.declaration parentDecl = parentSymbol.declaration
if parentDecl is not None and parentDecl.objectType == 'function': if parentDecl is not None and parentDecl.objectType == 'function':
logger.warning("C++ declarations inside functions are not supported." + msg = "C++ declarations inside functions are not supported." \
" Parent function is " + " Parent function: {}\nDirective name: {}\nDirective arg: {}"
str(parentSymbol.get_full_nested_name()), logger.warning(msg.format(
location=self.get_source_info()) str(parentSymbol.get_full_nested_name()),
self.name, self.arguments[0]
), location=self.get_source_info())
name = _make_phony_error_name() name = _make_phony_error_name()
symbol = parentSymbol.add_name(name) symbol = parentSymbol.add_name(name)
env.temp_data['cpp:last_symbol'] = symbol env.temp_data['cpp:last_symbol'] = symbol

View File

@ -1764,7 +1764,7 @@ class TypeVarDocumenter(DataDocumenter):
@classmethod @classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool: ) -> bool:
return isinstance(member, TypeVar) and isattr # type: ignore return isinstance(member, TypeVar) and isattr
def add_directive_header(self, sig: str) -> None: def add_directive_header(self, sig: str) -> None:
self.options = Options(self.options) self.options = Options(self.options)

View File

@ -297,8 +297,8 @@ class IndexBuilder:
frozen.get('envversion') != self.env.version: frozen.get('envversion') != self.env.version:
raise ValueError('old format') raise ValueError('old format')
index2fn = frozen['docnames'] index2fn = frozen['docnames']
self._filenames = dict(zip(index2fn, frozen['filenames'])) # type: ignore self._filenames = dict(zip(index2fn, frozen['filenames']))
self._titles = dict(zip(index2fn, frozen['titles'])) # type: ignore self._titles = dict(zip(index2fn, frozen['titles']))
def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]: def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]:
rv = {} rv = {}
@ -359,13 +359,13 @@ class IndexBuilder:
def get_terms(self, fn2index: Dict) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]: def get_terms(self, fn2index: Dict) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
rvs = {}, {} # type: Tuple[Dict[str, List[str]], Dict[str, List[str]]] rvs = {}, {} # type: Tuple[Dict[str, List[str]], Dict[str, List[str]]]
for rv, mapping in zip(rvs, (self._mapping, self._title_mapping)): for rv, mapping in zip(rvs, (self._mapping, self._title_mapping)):
for k, v in mapping.items(): # type: ignore for k, v in mapping.items():
if len(v) == 1: if len(v) == 1:
fn, = v fn, = v
if fn in fn2index: if fn in fn2index:
rv[k] = fn2index[fn] # type: ignore rv[k] = fn2index[fn]
else: else:
rv[k] = sorted([fn2index[fn] for fn in v if fn in fn2index]) # type: ignore # NOQA rv[k] = sorted([fn2index[fn] for fn in v if fn in fn2index])
return rvs return rvs
def freeze(self) -> Dict[str, Any]: def freeze(self) -> Dict[str, Any]:

View File

@ -57,7 +57,7 @@ Inventory = Dict[str, Dict[str, Tuple[str, str, str, str]]]
def is_system_TypeVar(typ: Any) -> bool: def is_system_TypeVar(typ: Any) -> bool:
"""Check *typ* is system defined TypeVar.""" """Check *typ* is system defined TypeVar."""
modname = getattr(typ, '__module__', '') modname = getattr(typ, '__module__', '')
return modname == 'typing' and isinstance(typ, TypeVar) # type: ignore return modname == 'typing' and isinstance(typ, TypeVar)
def stringify(annotation: Any) -> str: def stringify(annotation: Any) -> str:
@ -68,7 +68,7 @@ def stringify(annotation: Any) -> str:
return annotation[1:-2] return annotation[1:-2]
else: else:
return annotation return annotation
elif isinstance(annotation, TypeVar): # type: ignore elif isinstance(annotation, TypeVar):
return annotation.__name__ return annotation.__name__
elif not annotation: elif not annotation:
return repr(annotation) return repr(annotation)

View File

@ -0,0 +1,5 @@
.. c:function:: void f(int i)
- :c:var:`i`
- :c:var:`f.i`

View File

@ -9,6 +9,8 @@
""" """
import pytest import pytest
from xml.etree import ElementTree
from sphinx import addnodes from sphinx import addnodes
from sphinx.addnodes import desc from sphinx.addnodes import desc
from sphinx.domains.c import DefinitionParser, DefinitionError from sphinx.domains.c import DefinitionParser, DefinitionError
@ -529,6 +531,25 @@ def filter_warnings(warning, file):
return res return res
def extract_role_links(app, filename):
t = (app.outdir / filename).read_text()
lis = [l for l in t.split('\n') if l.startswith("<li")]
entries = []
for l in lis:
li = ElementTree.fromstring(l)
aList = list(li.iter('a'))
assert len(aList) == 1
a = aList[0]
target = a.attrib['href'].lstrip('#')
title = a.attrib['title']
assert len(a) == 1
code = a[0]
assert code.tag == 'code'
text = ''.join(code.itertext())
entries.append((target, title, text))
return entries
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_build_domain_c(app, status, warning): def test_build_domain_c(app, status, warning):
app.builder.build_all() app.builder.build_all()
@ -562,6 +583,26 @@ def test_build_domain_c_semicolon(app, status, warning):
assert len(ws) == 0 assert len(ws) == 0
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_build_function_param_target(app, warning):
# the anchor for function parameters should be the function
app.builder.build_all()
ws = filter_warnings(warning, "function_param_target")
assert len(ws) == 0
entries = extract_role_links(app, "function_param_target.html")
assert entries == [
('c.f', 'i', 'i'),
('c.f', 'f.i', 'f.i'),
]
def _get_obj(app, queryName):
domain = app.env.get_domain('c')
for name, dispname, objectType, docname, anchor, prio in domain.get_objects():
if name == queryName:
return (docname, anchor, objectType)
return (queryName, "not", "found")
def test_cfunction(app): def test_cfunction(app):
text = (".. c:function:: PyObject* " text = (".. c:function:: PyObject* "
"PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)") "PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)")
@ -569,8 +610,7 @@ def test_cfunction(app):
assert_node(doctree[1], addnodes.desc, desctype="function", assert_node(doctree[1], addnodes.desc, desctype="function",
domain="c", objtype="function", noindex=False) domain="c", objtype="function", noindex=False)
domain = app.env.get_domain('c') entry = _get_obj(app, 'PyType_GenericAlloc')
entry = domain.objects.get('PyType_GenericAlloc')
assert entry == ('index', 'c.PyType_GenericAlloc', 'function') assert entry == ('index', 'c.PyType_GenericAlloc', 'function')
@ -580,8 +620,7 @@ def test_cmember(app):
assert_node(doctree[1], addnodes.desc, desctype="member", assert_node(doctree[1], addnodes.desc, desctype="member",
domain="c", objtype="member", noindex=False) domain="c", objtype="member", noindex=False)
domain = app.env.get_domain('c') entry = _get_obj(app, 'PyTypeObject.tp_bases')
entry = domain.objects.get('PyTypeObject.tp_bases')
assert entry == ('index', 'c.PyTypeObject.tp_bases', 'member') assert entry == ('index', 'c.PyTypeObject.tp_bases', 'member')
@ -591,9 +630,8 @@ def test_cvar(app):
assert_node(doctree[1], addnodes.desc, desctype="var", assert_node(doctree[1], addnodes.desc, desctype="var",
domain="c", objtype="var", noindex=False) domain="c", objtype="var", noindex=False)
domain = app.env.get_domain('c') entry = _get_obj(app, 'PyClass_Type')
entry = domain.objects.get('PyClass_Type') assert entry == ('index', 'c.PyClass_Type', 'member')
assert entry == ('index', 'c.PyClass_Type', 'var')
def test_noindexentry(app): def test_noindexentry(app):

View File

@ -1236,3 +1236,18 @@ def test_noindexentry(app):
assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
assert_node(doctree[0], addnodes.index, entries=[('single', 'f (C++ function)', '_CPPv41fv', '', None)]) assert_node(doctree[0], addnodes.index, entries=[('single', 'f (C++ function)', '_CPPv41fv', '', None)])
assert_node(doctree[2], addnodes.index, entries=[]) assert_node(doctree[2], addnodes.index, entries=[])
def test_mix_decl_duplicate(app, warning):
# Issue 8270
text = (".. cpp:struct:: A\n"
".. cpp:function:: void A()\n"
".. cpp:struct:: A\n")
restructuredtext.parse(app, text)
ws = warning.getvalue().split("\n")
assert len(ws) == 5
assert "index.rst:2: WARNING: Duplicate C++ declaration, also defined in 'index'." in ws[0]
assert "Declaration is 'void A()'." in ws[1]
assert "index.rst:3: WARNING: Duplicate C++ declaration, also defined in 'index'." in ws[2]
assert "Declaration is 'A'." in ws[3]
assert ws[4] == ""