diff --git a/src/ZPublisher/BaseRequest.py b/src/ZPublisher/BaseRequest.py index 297808fc66..a42b096a68 100644 --- a/src/ZPublisher/BaseRequest.py +++ b/src/ZPublisher/BaseRequest.py @@ -729,7 +729,7 @@ def ensure_publishable(self, obj, for_call=False): "to `ZPublisher.zpublish` decorator or have a docstring to be " "published.") if deprecate_docstrings: - warn(DocstringWarning(url)) + warn(DocstringWarning(obj, url)) def exec_callables(callables): @@ -828,7 +828,44 @@ def typeCheck(obj, deny=itypes): class DocstringWarning(DeprecationWarning): + def tag(self): + import inspect as i + + def lineno(o, m=False): + """try to determine where *o* has been defined. + + *o* is either a function or a class. + """ + try: + _, lineno = i.getsourcelines(o) + except (OSError, TypeError): + return "" + return f"[{o.__module__}:{lineno}]" if m else f" at line {lineno}" + + obj, url = self.args + desc = None + if i.ismethod(obj): + f = i.unwrap(obj.__func__) + c = obj.__self__.__class__ + desc = f"'{c.__module__}.{c.__qualname__}' " \ + f"method '{obj.__qualname__}'{lineno(f, 1)}" + elif i.isfunction(obj): + f = i.unwrap(obj) + desc = f"function '{f.__module__}.{f.__qualname__}'" \ + f"{lineno(f)}" + else: + try: + cls_doc = "__doc__" not in obj.__dict__ + except AttributeError: + cls_doc = True + if cls_doc: + c = obj.__class__ + desc = f"'{c.__module__}.{c.__qualname__}'{lineno(c)}" + if desc is None: + desc = f"object at '{url}'" + return desc + def __str__(self): - return (f"The object at {self.args[0]} uses deprecated docstring " + return (f"{self.tag()} uses deprecated docstring " "publication control. Use the `ZPublisher.zpublish` decorator " "instead") diff --git a/src/ZPublisher/tests/docstring.py b/src/ZPublisher/tests/docstring.py new file mode 100644 index 0000000000..bd9bd4a20b --- /dev/null +++ b/src/ZPublisher/tests/docstring.py @@ -0,0 +1,13 @@ +"""Resources for ``testBaseRequest.TestDocstringWarning``. + +The tests there depend on line numbers in this module. +""" + + +def f(): + "f" + + +class C: + "C" + g = f diff --git a/src/ZPublisher/tests/testBaseRequest.py b/src/ZPublisher/tests/testBaseRequest.py index 3a234d0c19..a4b7665bf8 100644 --- a/src/ZPublisher/tests/testBaseRequest.py +++ b/src/ZPublisher/tests/testBaseRequest.py @@ -6,6 +6,9 @@ from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound as ztkNotFound from ZPublisher import zpublish +from ZPublisher.BaseRequest import DocstringWarning + +from . import docstring @implementer(IPublishTraverse) @@ -885,3 +888,37 @@ def test__str__returns_native_string(self): root, folder = self._makeRootAndFolder() r = self._makeOne(root) self.assertIsInstance(str(r), str) + + +class TestDocstringWarning(unittest.TestCase): + def test_method(self): + c = docstring.C() + ds = DocstringWarning(c.g, "URL") + self.assertEqual(ds.tag(), + "'ZPublisher.tests.docstring.C' method " + "'f'[ZPublisher.tests.docstring:7]") + + def test_function(self): + f = docstring.f + ds = DocstringWarning(f, "URL") + self.assertEqual(ds.tag(), + "function 'ZPublisher.tests.docstring.f' at line 7") + + def test_instance_with_own_docstring(self): + c = docstring.C() + c.__doc__ = "c" + ds = DocstringWarning(c, "URL") + self.assertEqual(ds.tag(), "object at 'URL'") + + def test_instance_with_inherited_docstring(self): + c = docstring.C() + ds = DocstringWarning(c, "URL") + self.assertEqual(ds.tag(), "'ZPublisher.tests.docstring.C' at line 11") + + def test__str__(self): + ds = DocstringWarning("special", "URL") + self.assertEqual( + str(ds), + "'builtins.str' uses deprecated docstring " + "publication control. Use the `ZPublisher.zpublish` decorator " + "instead")