diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py index 7a547b331..c0115620b 100644 --- a/pydoctor/epydoc2stan.py +++ b/pydoctor/epydoc2stan.py @@ -260,6 +260,11 @@ class ParamType: stan: Tag origin: FieldOrigin +def _report_field_has_no_meaning_on_module(obj: model.Documentable, field: Field) -> None: + if isinstance(obj, model.Module): + msg = f"Field '{field.tag}' has no meaning on module" + field.report(msg) + class FieldHandler: def __init__(self, obj: model.Documentable): @@ -310,6 +315,7 @@ def _report_unexpected_argument(field:Field) -> None: field.report('Unexpected argument in %s field' % (field.tag,)) def handle_return(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) self._report_unexpected_argument(field) if not self.return_desc: self.return_desc = ReturnDesc() @@ -317,6 +323,7 @@ def handle_return(self, field: Field) -> None: handle_returns = handle_return def handle_yield(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) self._report_unexpected_argument(field) if not self.yields_desc: self.yields_desc = FieldDesc() @@ -324,6 +331,7 @@ def handle_yield(self, field: Field) -> None: handle_yields = handle_yield def handle_returntype(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) self._report_unexpected_argument(field) if not self.return_desc: self.return_desc = ReturnDesc() @@ -332,6 +340,7 @@ def handle_returntype(self, field: Field) -> None: handle_rtype = handle_returntype def handle_yieldtype(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) self._report_unexpected_argument(field) if not self.yields_desc: self.yields_desc = FieldDesc() @@ -415,6 +424,7 @@ def handle_type(self, field: Field) -> None: self.types[name] = ParamType(field.format(), origin=FieldOrigin.FROM_DOCSTRING) def handle_param(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) name = self._handle_param_name(field) if name is not None: if any(desc.name == name for desc in self.parameter_descs): @@ -426,6 +436,7 @@ def handle_param(self, field: Field) -> None: handle_arg = handle_param def handle_keyword(self, field: Field) -> None: + _report_field_has_no_meaning_on_module(self.obj, field) name = self._handle_param_name(field) if name is not None: # TODO: How should this be matched to the type annotation? @@ -921,6 +932,11 @@ def extract_fields(obj: model.CanContainImportsDocumentable) -> None: for field in parsed_doc.fields: tag = field.tag() + # ivar and cvar fields on modules don't make sense: warn but still + # allow them to be processed so their documentation is rendered. + if tag in ('ivar', 'cvar'): + _report_field_has_no_meaning_on_module(obj, Field.from_epydoc(field, obj)) + if tag in ['ivar', 'cvar', 'var', 'type']: arg = field.arg() if arg is None: diff --git a/pydoctor/test/test_epydoc2stan.py b/pydoctor/test/test_epydoc2stan.py index b9f873d94..731c4d65d 100644 --- a/pydoctor/test/test_epydoc2stan.py +++ b/pydoctor/test/test_epydoc2stan.py @@ -10,7 +10,7 @@ from pydoctor.stanutils import flatten, flatten_text from pydoctor.epydoc.markup.epytext import ParsedEpytextDocstring from pydoctor.sphinx import SphinxInventory -from pydoctor.test.test_astbuilder import fromText, unwrap +from pydoctor.test.test_astbuilder import fromText, unwrap, type2html from pydoctor.test import CapSys, NotFoundLinker from pydoctor.templatewriter.search import stem_identifier from pydoctor.templatewriter.pages import format_signature, format_class_signature @@ -904,7 +904,8 @@ def test_missing_field_name(capsys: CapSys) -> None: ''', modname='test') epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out - assert captured == "test:5: Missing field name in @ivar\n" \ + assert captured == "test:5: Field 'ivar' has no meaning on module\n" \ + "test:5: Missing field name in @ivar\n" \ "test:6: Missing field name in @type\n" @@ -921,6 +922,131 @@ def test_unknown_field_name(capsys: CapSys) -> None: assert captured == "test:5: Unknown field 'zap'\n" +def test_param_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @param x: This makes no sense on a module. + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == ( + "test:5: Field 'param' has no meaning on module\n" + "test:5: Documented parameter \"x\" does not exist\n" + ) + html = flatten_text(stan) + assert 'This makes no sense on a module.' in html + +def test_return_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @return: Nothing to return here. + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'return' has no meaning on module\n" + html = flatten_text(stan) + assert 'Nothing to return here.' in html + +def test_rtype_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @rtype: int + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'rtype' has no meaning on module\n" + html = flatten_text(stan) + assert 'int' in html + +def test_yields_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @yields: each + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'yields' has no meaning on module\n" + html = flatten_text(stan) + assert 'each' in html + +def test_ytype_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @ytype: str + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'ytype' has no meaning on module\n" + html = flatten_text(stan) + assert 'str' in html + +def test_keyword_on_module_warns(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @keyword k: something + """ + ''', modname='test') + stan = epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'keyword' has no meaning on module\n" + html = flatten_text(stan) + assert 'something' in html + +def test_ivar_in_module_docstring_creates_attribute(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @ivar foo: module-level instance var + @type foo: string + """ + + foo = 1 + ''', modname='test') + epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'ivar' has no meaning on module\n" + a = mod.resolveName('foo') + assert isinstance(a, model.Attribute) + assert unwrap(a.parsed_docstring) == "module-level instance var" + assert type2html(a) == 'string' + +def test_cvar_in_module_docstring_creates_attribute(capsys: CapSys) -> None: + mod = fromText(''' + """ + Module docstring. + + @cvar bar: module-level class var + @type bar: int + """ + + bar = 2 + ''', modname='test') + epydoc2stan.format_docstring(mod) + captured = capsys.readouterr().out + assert captured == "test:5: Field 'cvar' has no meaning on module\n" + a = mod.resolveName('bar') + assert isinstance(a, model.Attribute) + assert unwrap(a.parsed_docstring) == "module-level class var" + assert str(unwrap(a.parsed_type)) == 'int' + def test_inline_field_type(capsys: CapSys) -> None: """The C{type} field in a variable docstring updates the C{parsed_type} of the Attribute it documents.