chiark / gitweb /
documentation/python: implement scope enter/exit hooks.
authorVladimír Vondruš <mosra@centrum.cz>
Wed, 28 Aug 2019 20:09:37 +0000 (22:09 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Fri, 30 Aug 2019 14:47:58 +0000 (16:47 +0200)
With this we can have relative links for *everything*.

12 files changed:
doc/documentation/python.rst
documentation/python.py
documentation/test_python/content/classes.html
documentation/test_python/content/content.Class.html
documentation/test_python/content/content.html
documentation/test_python/content/docs.rst
documentation/test_python/content_parse_docstrings/content_parse_docstrings.Class.html
documentation/test_python/content_parse_docstrings/content_parse_docstrings.html
documentation/test_python/content_parse_docstrings/content_parse_docstrings.py
documentation/test_python/page_plugins/plugins/fancyline.py
documentation/test_python/test_page.py
plugins/m/sphinx.py

index 86a4bd4d4407e162f4d05d07838d59bcfdded2a8..4291b2fe1a8be27158e79a5666bdfb22a098f4ef 100644 (file)
@@ -769,6 +769,8 @@ Keyword argument            Content
 :py:`property_doc_contents` Property documentation contents
 :py:`data_doc_contents`     Data documentation contents
 :py:`hooks_post_crawl`      Hooks to call after the initial name crawl
+:py:`hooks_scope_enter`     Hooks to call on scope enter
+:py:`hooks_scope_exit`      Hooks to call on scope exit
 :py:`hooks_docstring`       Hooks to call when parsing a docstring
 :py:`hooks_pre_page`        Hooks to call before each page gets rendered
 :py:`hooks_post_run`        Hooks to call at the very end of the script run
@@ -840,16 +842,43 @@ Keyword argument    Content
     added by the plugin *need* to have :py:`object` set to :py:`None` so the
     script as well as other plugins can correctly distinguish them.
 
-Hooks listed in :py:`hooks_docstring` are called when docstrings are parsed.
-The first gets the raw docstring only processed by :py:`inspect.cleandoc()` and
-each following gets the output of the previous. When a hook returns an empty
-string, hooks later in the list are not called. String returned by the last
-hook is processed, if any, the same way as if no hooks would be present --- it
-gets partitioned into summary and content and those put to the output as-is,
-each paragraph wrapped in :html:`<p>` tags. The hooks are free to do anything
-with the docstring --- extracting metadata from it and returning it as-is,
-transpiling it from one markup language to another, or fully consuming it,
-populating the ``*_doc_contents`` variables mentioned above and returning
+The :py:`hooks_pre_scope` and :py:`hooks_post_scope` get called before entering
+and after leaving a name scope, and are meant mainly to aid with
+context-sensitive linking. Those scopes can be nested and can be called
+successively for the same scope --- for example, when rendering module docs,
+:py:`hooks_pre_scope` gets called first for the module scope, but then another
+:py:`hooks_pre_scope` gets called when rendering a summary for reference to an
+inner class. Then, :py:`hooks_post_scope` gets called in reverse order. The
+plugins are expected to implement a stack-like data structure for maintaining
+information about current scope. Both of those functions get passed the
+following arguments:
+
+.. class:: m-table
+
+=================== ===========================================================
+Keyword argument    Content
+=================== ===========================================================
+:py:`type`          Type of the scope that's being entered or exited. Same as
+                    the enum passed to `custom URL formatters`_.
+:py:`path`          Path of the module / class / function / enum / enum value /
+                    data scope that's being entered or exited. A list of names,
+                    :py:`'.'.join(path)` is equivalent to the fully qualified
+                    name.
+:py:`param_names`   In case of functions, list of parameter names. This
+                    argument is not present otherwise.
+=================== ===========================================================
+
+Hooks listed in :py:`hooks_docstring` are called when docstrings are parsed,
+and always preceded by a corresponding :py:`hooks_pre_scope` call. The first
+listed hook gets the raw docstring only processed by :py:`inspect.cleandoc()`
+and each following gets the output of the previous. When a hook returns an
+empty string, hooks later in the list are not called. String returned by the
+last hook is processed, if any, the same way as if no hooks would be present
+--- it gets partitioned into summary and content and those put to the output
+as-is, each paragraph wrapped in :html:`<p>` tags. The hooks are free to do
+anything with the docstring --- extracting metadata from it and returning it
+as-is, transpiling it from one markup language to another, or fully consuming
+it, populating the ``*_doc_contents`` variables mentioned above and returning
 nothing back. Each hook gets passed the following arguments:
 
 .. class:: m-table
@@ -872,22 +901,8 @@ Keyword argument    Content
 
 The :py:`hooks_pre_page` is called before each page of output gets rendered.
 Can be used for example for resetting some internal counter for page-wide
-unique element IDs. It gets passed the following arguments:
-
-.. class:: m-table
-
-=================== ===========================================================
-Keyword argument    Content
-=================== ===========================================================
-:py:`path`          Path of the module/class/page to render. A list of names,
-                    for modules and classes :py:`'.'.join(path)` is equivalent
-                    to the fully qualified name. Useful to provide
-                    context-sensitive linking capabilities.
-=================== ===========================================================
-
-The :py:`hooks_post_run` is called after the whole run is done, useful for
-example to serialize cached internal state. Currently, this function get no
-arguments passed.
+unique element IDs. The :py:`hooks_post_run` is called after the whole run is
+done, useful for example to serialize cached internal state. Currently, those two functions get no arguments passed.
 
 Registration function for a plugin that needs to query the :py:`OUTPUT` setting
 might look like this --- the remaining keyword arguments will collapse into
index 9ef15502d76b076c8f335b774417334f7dce4013..d7c5ca21f245539d779537996460cba47b0aed35 100755 (executable)
@@ -185,6 +185,8 @@ class State:
 
         self.hooks_post_crawl: List = []
         self.hooks_docstring: List = []
+        self.hooks_pre_scope: List = []
+        self.hooks_post_scope: List = []
         self.hooks_pre_page: List = []
         self.hooks_post_run: List = []
 
@@ -1039,22 +1041,44 @@ def extract_annotation(state: State, referrer_path: List[str], annotation) -> Tu
 def extract_module_doc(state: State, entry: Empty):
     assert inspect.ismodule(entry.object)
 
+    # Call all scope enter hooks first
+    for hook in state.hooks_pre_scope:
+        hook(type=entry.type, path=entry.path)
+
     out = Empty()
     out.url = entry.url
     out.name = entry.path[-1]
     out.summary = extract_docs(state, state.module_docs, entry.type, entry.path, entry.object.__doc__, summary_only=True)
+
+    # Call all scope exit hooks last
+    for hook in state.hooks_post_scope:
+        hook(type=entry.type, path=entry.path)
+
     return out
 
 def extract_class_doc(state: State, entry: Empty):
     assert inspect.isclass(entry.object)
 
+    # Call all scope enter hooks first
+    for hook in state.hooks_pre_scope:
+        hook(type=entry.type, path=entry.path)
+
     out = Empty()
     out.url = entry.url
     out.name = entry.path[-1]
     out.summary = extract_docs(state, state.class_docs, entry.type, entry.path, entry.object.__doc__, summary_only=True)
+
+    # Call all scope exit hooks last
+    for hook in state.hooks_post_scope:
+        hook(type=entry.type, path=entry.path)
+
     return out
 
 def extract_enum_doc(state: State, entry: Empty):
+    # Call all scope enter hooks first
+    for hook in state.hooks_pre_scope:
+        hook(type=entry.type, path=entry.path)
+
     out = Empty()
     out.name = entry.path[-1]
     out.id = state.config['ID_FORMATTER'](EntryType.ENUM, entry.path[-1:])
@@ -1134,6 +1158,10 @@ def extract_enum_doc(state: State, entry: Empty):
             result.name = value.name
             state.search += [result]
 
+    # Call all scope exit hooks last
+    for hook in state.hooks_post_scope:
+        hook(type=entry.type, path=entry.path)
+
     return out
 
 def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
@@ -1208,12 +1236,14 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
                         positional_only = False
                         break
 
+            param_names = []
             param_types = []
             signature = []
             for i, arg in enumerate(args):
                 name, type, type_link, default = arg
                 param = Empty()
                 param.name = name
+                param_names += [name]
                 # Don't include redundant type for the self argument
                 if i == 0 and name == 'self':
                     param.type, param.type_link = None, None
@@ -1253,11 +1283,19 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
             # thus name alone is not enough.
             out.id = state.config['ID_FORMATTER'](EntryType.OVERLOADED_FUNCTION, entry.path[-1:] + param_types)
 
+            # Call all scope enter hooks for this particular overload
+            for hook in state.hooks_pre_scope:
+                hook(type=entry.type, path=entry.path, param_names=param_names)
+
             # Get summary and details. Passing the signature as well, so
             # different overloads can (but don't need to) have different docs.
             out.summary, out.content = extract_docs(state, state.function_docs, entry.type, entry.path, summary, signature='({})'.format(', '.join(signature)))
             if out.content: out.has_details = True
 
+            # Call all scope exit hooks for this particular overload
+            for hook in state.hooks_post_scope:
+                hook(type=entry.type, path=entry.path, param_names=param_names)
+
             overloads += [out]
 
     # Sane introspection path for non-pybind11 code
@@ -1267,8 +1305,7 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
         out.id = state.config['ID_FORMATTER'](EntryType.FUNCTION, entry.path[-1:])
         out.params = []
         out.has_complex_params = False
-        out.summary, out.content = extract_docs(state, state.function_docs, entry.type, entry.path, entry.object.__doc__)
-        out.has_details = bool(out.content)
+        out.has_details = False
 
         # Decide if classmethod or staticmethod in case this is a method
         if inspect.isclass(parent):
@@ -1288,9 +1325,11 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
                 out.type, out.type_link = extract_annotation(state, entry.path, type_hints['return'])
             else:
                 out.type, out.type_link = extract_annotation(state, entry.path, signature.return_annotation)
+            param_names = []
             for i in signature.parameters.values():
                 param = Empty()
                 param.name = i.name
+                param_names += [i.name]
                 if i.name in type_hints:
                     param.type, param.type_link = extract_annotation(state, entry.path, type_hints[i.name])
                 else:
@@ -1314,6 +1353,20 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
             param.type, param.type_link = None, None
             out.params = [param]
             out.type, out.type_link = None, None
+            param_names = []
+
+        # Call all scope enter hooks
+        for hook in state.hooks_pre_scope:
+            hook(type=entry.type, path=entry.path, param_names=param_names)
+
+        # Get summary and details
+        # TODO: pass signature as well once @overload becomes a thing
+        out.summary, out.content = extract_docs(state, state.function_docs, entry.type, entry.path, entry.object.__doc__)
+        if out.content: out.has_details = True
+
+        # Call all scope exit hooks
+        for hook in state.hooks_post_scope:
+            hook(type=entry.type, path=entry.path, param_names=param_names)
 
         overloads = [out]
 
@@ -1321,6 +1374,11 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
     path_str = '.'.join(entry.path)
     for out in overloads:
         signature = '({})'.format(', '.join(['{}: {}'.format(param.name, param.type) if param.type else param.name for param in out.params]))
+        param_names = [param.name for param in out.params]
+
+        # Call all scope enter hooks for this particular overload
+        for hook in state.hooks_pre_scope:
+            hook(type=entry.type, path=entry.path, param_names=param_names)
 
         # Get docs for each param and for the return value. Try this
         # particular overload first, if not found then fall back to generic
@@ -1364,6 +1422,10 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
                     out.return_value = ''
                 out.has_details = True
 
+        # Call all scope exit hooks for this particular overload
+        for hook in state.hooks_post_scope:
+            hook(type=entry.type, path=entry.path, param_names=param_names)
+
         if not state.config['SEARCH_DISABLED']:
             result = Empty()
             result.flags = ResultFlag.from_type(ResultFlag.NONE, EntryType.FUNCTION)
@@ -1432,6 +1494,10 @@ def extract_property_doc(state: State, parent, entry: Empty):
         out.type = None
         return out
 
+    # Call all scope enter hooks before rendering the docs
+    for hook in state.hooks_pre_scope:
+        hook(type=entry.type, path=entry.path)
+
     out.is_gettable = entry.object.fget is not None
     if entry.object.fget or (entry.object.fset and entry.object.__doc__):
         docstring = entry.object.__doc__
@@ -1443,6 +1509,10 @@ def extract_property_doc(state: State, parent, entry: Empty):
     out.is_deletable = entry.object.fdel is not None
     out.has_details = bool(out.content)
 
+    # Call all scope exit hooks after rendering the docs
+    for hook in state.hooks_post_scope:
+        hook(type=entry.type, path=entry.path)
+
     # For the type, if the property is gettable, get it from getters's return
     # type. For write-only properties get it from setter's second argument
     # annotation.
@@ -1508,6 +1578,10 @@ def extract_property_doc(state: State, parent, entry: Empty):
 def extract_data_doc(state: State, parent, entry: Empty):
     assert not inspect.ismodule(entry.object) and not inspect.isclass(entry.object) and not inspect.isroutine(entry.object) and not inspect.isframe(entry.object) and not inspect.istraceback(entry.object) and not inspect.iscode(entry.object)
 
+    # Call all scope enter hooks before rendering the docs
+    for hook in state.hooks_pre_scope:
+        hook(type=entry.type, path=entry.path)
+
     out = Empty()
     out.name = entry.path[-1]
     out.id = state.config['ID_FORMATTER'](EntryType.DATA, entry.path[-1:])
@@ -1515,6 +1589,10 @@ def extract_data_doc(state: State, parent, entry: Empty):
     out.summary, out.content = extract_docs(state, state.data_docs, entry.type, entry.path, '')
     out.has_details = bool(out.content)
 
+    # Call all scope exit hooks after rendering the docs
+    for hook in state.hooks_post_scope:
+        hook(type=entry.type, path=entry.path)
+
     # First try to get fully dereferenced type hints (with strings converted to
     # actual annotations). If that fails (e.g. because a type doesn't exist),
     # we'll take the non-dereferenced annotations instead.
@@ -1553,6 +1631,10 @@ def render(config, template: str, page, env: jinja2.Environment):
         f.write(b'\n')
 
 def render_module(state: State, path, module, env):
+    # Call all scope enter hooks first
+    for hook in state.hooks_pre_scope:
+        hook(type=EntryType.MODULE, path=path)
+
     # Generate breadcrumb as the first thing as it generates the output
     # filename as a side effect
     breadcrumb = []
@@ -1565,8 +1647,7 @@ def render_module(state: State, path, module, env):
     logging.debug("generating %s", filename)
 
     # Call all registered page begin hooks
-    for hook in state.hooks_pre_page:
-        hook(path=path)
+    for hook in state.hooks_pre_page: hook()
 
     page = Empty()
     page.summary, page.content = extract_docs(state, state.module_docs, EntryType.MODULE, path, module.__doc__)
@@ -1626,7 +1707,15 @@ def render_module(state: State, path, module, env):
 
     render(state.config, 'module.html', page, env)
 
+    # Call all scope exit hooks last
+    for hook in state.hooks_post_scope:
+        hook(type=EntryType.MODULE, path=path)
+
 def render_class(state: State, path, class_, env):
+    # Call all scope enter hooks first
+    for hook in state.hooks_pre_scope:
+        hook(type=EntryType.CLASS, path=path)
+
     # Generate breadcrumb as the first thing as it generates the output
     # filename as a side effect. It's a bit hairy because we need to figure out
     # proper entry type for the URL formatter for each part of the breadcrumb.
@@ -1641,8 +1730,7 @@ def render_class(state: State, path, class_, env):
     logging.debug("generating %s", filename)
 
     # Call all registered page begin hooks
-    for hook in state.hooks_pre_page:
-        hook(path=path)
+    for hook in state.hooks_pre_page: hook()
 
     page = Empty()
     page.summary, page.content = extract_docs(state, state.class_docs, EntryType.CLASS, path, class_.__doc__)
@@ -1715,6 +1803,10 @@ def render_class(state: State, path, class_, env):
 
     render(state.config, 'class.html', page, env)
 
+    # Call all scope exit hooks last
+    for hook in state.hooks_post_scope:
+        hook(type=EntryType.CLASS, path=path)
+
 # Extracts image paths and transforms them to just the filenames
 class ExtractImages(Transform):
     # Max Docutils priority is 990, be sure that this is applied at the very
@@ -2053,6 +2145,8 @@ def run(basedir, config, *, templates=default_templates, search_add_lookahead_ba
             property_doc_contents=state.property_docs,
             data_doc_contents=state.data_docs,
             hooks_post_crawl=state.hooks_post_crawl,
+            hooks_pre_scope=state.hooks_pre_scope,
+            hooks_post_scope=state.hooks_post_scope,
             hooks_docstring=state.hooks_docstring,
             hooks_pre_page=state.hooks_pre_page,
             hooks_post_run=state.hooks_post_run)
index bf4f72230e8ba966e0ae7488f81979fd39089122..9199bbd77f967339c72edff1641e47e8be3b8bd8 100644 (file)
         <h1>Classes</h2>
         <ul class="m-doc">
           <li class="m-doc-collapsible">
-            <a href="#" onclick="return toggle(this)">module</a> <a href="content.html" class="m-doc">content</a> <span class="m-doc">This overwrites the docstring for <code>content</code>.</span>
+            <a href="#" onclick="return toggle(this)">module</a> <a href="content.html" class="m-doc">content</a> <span class="m-doc">This overwrites the docstring for <a class="m-doc" href="content.html">content</a>.</span>
             <ul class="m-doc">
               <li>module <a href="content.docstring_summary.html" class="m-doc">docstring_summary</a> <span class="m-doc">This module retains summary from the docstring</span></li>
               <li>module <a href="content.submodule.html" class="m-doc">submodule</a> <span class="m-doc">This submodule has an external summary.</span></li>
-              <li>class <a href="content.Class.html" class="m-doc">Class</a> <span class="m-doc">This overwrites the docstring for <code>content.Class</code>.</span></li>
+              <li>class <a href="content.Class.html" class="m-doc">Class</a> <span class="m-doc">This overwrites the docstring for <a class="m-doc" href="content.Class.html">Class</a>.</span></li>
               <li>class <a href="content.ClassWithSummary.html" class="m-doc">ClassWithSummary</a> <span class="m-doc">This class has summary from the docstring</span></li>
             </ul>
           </li>
index 26d9b05661e013710c53f9410b741807f3c1390d..5f4320f6caf518929944a9d903248a6c992106f4 100644 (file)
@@ -22,7 +22,7 @@
         <h1>
           <span class="m-breadcrumb"><a href="content.html">content</a>.<wbr/></span>Class <span class="m-thin">class</span>
         </h1>
-        <p>This overwrites the docstring for <code>content.Class</code>.</p>
+        <p>This overwrites the docstring for <a class="m-doc" href="content.Class.html">Class</a>.</p>
         <div class="m-block m-default">
           <h3>Contents</h3>
           <ul>
@@ -65,8 +65,8 @@ indented.</p>
             <dt id="method">
               <span class="m-doc-wrap-bumper">def <a href="#method" class="m-doc-self">method</a>(</span><span class="m-doc-wrap">self)</span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.Class.method</code>, but
-doesn't add any detailed block.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.Class.html#method">method()</a>, but doesn't
+add any detailed block.</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#method_param_docs" class="m-doc">method_param_docs</a>(</span><span class="m-doc-wrap">self, a, b)</span>
             </dt>
@@ -92,12 +92,12 @@ doesn't add any detailed block.</dd>
             <dt id="a_property">
               <a href="#a_property" class="m-doc-self">a_property</a> <span class="m-label m-flat m-warning">get</span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.Class.a_property</code>,
-but doesn't add any detailed block.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.Class.html#a_property">a_property</a>, but doesn't
+add any detailed block.</dd>
             <dt>
               <a href="#a_property_with_details" class="m-doc">a_property_with_details</a> <span class="m-label m-flat m-warning">get</span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.Class.a_property_with_details</code>.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.Class.html#a_property_with_details">a_property_with_details</a>.</dd>
             <dt>
               <a href="#annotated_property" class="m-doc">annotated_property</a>: float <span class="m-label m-flat m-warning">get</span>
             </dt>
@@ -171,7 +171,7 @@ but doesn't add any detailed block.</dd>
             <h3>
               content.<wbr />Class.<wbr /><a href="#a_property_with_details" class="m-doc-self">a_property_with_details</a> <span class="m-label m-flat m-warning">get</span>
             </h3>
-            <p>This overwrites the docstring for <code>content.Class.a_property_with_details</code>.</p>
+            <p>This overwrites the docstring for <a class="m-doc" href="content.Class.html#a_property_with_details">a_property_with_details</a>.</p>
 <p>Detailed property docs.</p>
           </div></section>
           <section class="m-doc-details" id="annotated_property"><div>
@@ -188,7 +188,8 @@ but doesn't add any detailed block.</dd>
             <h3>
               content.<wbr />Class.<wbr /><a href="#DATA_WITH_DETAILS" class="m-doc-self">DATA_WITH_DETAILS</a>: str
             </h3>
-<p>Detailed docs for <code>data</code> in a class to check rendering.</p>
+<p>Detailed docs for <a class="m-doc" href="content.Class.html#DATA_WITH_DETAILS">DATA_WITH_DETAILS</a> in a class to check
+rendering.</p>
           </div></section>
         </section>
       </div>
index 35622ede6729090535c6aa742cee5d6454b78403..47506485776850bed993b0b24a60926532e24fc7 100644 (file)
@@ -22,7 +22,7 @@
         <h1>
           content <span class="m-thin">module</span>
         </h1>
-        <p>This overwrites the docstring for <code>content</code>.</p>
+        <p>This overwrites the docstring for <a class="m-doc" href="content.html">content</a>.</p>
         <div class="m-block m-default">
           <h3>Contents</h3>
           <ul>
@@ -53,7 +53,7 @@ tho.</p>
           <h2><a href="#classes">Classes</a></h2>
           <dl class="m-doc">
             <dt>class <a href="content.Class.html" class="m-doc">Class</a></dt>
-            <dd>This overwrites the docstring for <code>content.Class</code>.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.Class.html">Class</a>.</dd>
             <dt>class <a href="content.ClassWithSummary.html" class="m-doc">ClassWithSummary</a></dt>
             <dd>This class has summary from the docstring</dd>
           </dl>
@@ -64,8 +64,8 @@ tho.</p>
             <dt id="Enum">
               <span class="m-doc-wrap-bumper">class <a href="#Enum" class="m-doc-self">Enum</a>(enum.Enum): </span><span class="m-doc-wrap"></span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.Enum</code>, but
-doesn't add any detailed block.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.html#Enum">Enum</a>, but doesn't
+add any detailed block.</dd>
             <dt>
               <span class="m-doc-wrap-bumper">class <a href="#EnumWithSummary" class="m-doc">EnumWithSummary</a>(enum.Enum): </span><span class="m-doc-wrap"><a href="#EnumWithSummary-VALUE" class="m-doc">VALUE</a> = 0
               <a href="#EnumWithSummary-ANOTHER" class="m-doc">ANOTHER</a> = 1</span>
@@ -79,12 +79,12 @@ doesn't add any detailed block.</dd>
             <dt id="foo">
               <span class="m-doc-wrap-bumper">def <a href="#foo" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">a, b)</span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.foo</code>, but
-doesn't add any detailed block.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.html#foo">foo()</a>, but doesn't
+add any detailed block.</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#foo_with_details" class="m-doc">foo_with_details</a>(</span><span class="m-doc-wrap">a, b)</span>
             </dt>
-            <dd>This overwrites the docstring for <code>content.foo_with_details()</code>.</dd>
+            <dd>This overwrites the docstring for <a class="m-doc" href="content.html#foo_with_details">foo_with_details()</a>.</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#full_docstring" class="m-doc">full_docstring</a>(</span><span class="m-doc-wrap">a, b) -&gt; str</span>
             </dt>
@@ -115,11 +115,11 @@ doesn't add any detailed block.</dd>
             <dt id="CONSTANT">
               <a href="#CONSTANT" class="m-doc-self">CONSTANT</a>: float = 3.14
             </dt>
-            <dd>This is finally a docstring for <code>content.CONSTANT</code></dd>
+            <dd>This is finally a docstring for <a class="m-doc" href="content.html#CONSTANT">CONSTANT</a></dd>
             <dt>
               <a href="#DATA_WITH_DETAILS" class="m-doc">DATA_WITH_DETAILS</a>: str = &#x27;heyoo&#x27;
             </dt>
-            <dd>This is finally a docstring for <code>content.DATA_WITH_DETAILS</code></dd>
+            <dd>This is finally a docstring for <a class="m-doc" href="content.html#DATA_WITH_DETAILS">DATA_WITH_DETAILS</a></dd>
             <dt>
               <a href="#DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE" class="m-doc">DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE</a> = None
             </dt>
@@ -149,7 +149,7 @@ doesn't add any detailed block.</dd>
                 </tr>
               </tbody>
             </table>
-<p>And this is detailed docs added to the docstring summary.</p>
+<p>And this is detailed docs added to the docstring summary. <a class="m-doc" href="content.html#EnumWithSummary-VALUE">VALUE</a>!!</p>
           </div></section>
         </section>
         <section>
@@ -158,7 +158,7 @@ doesn't add any detailed block.</dd>
             <h3>
               <span class="m-doc-wrap-bumper">def content.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#foo_with_details" class="m-doc-self">foo_with_details</a>(</span><span class="m-doc-wrap">a, b)</span></span>
             </h3>
-            <p>This overwrites the docstring for <code>content.foo_with_details()</code>.</p>
+            <p>This overwrites the docstring for <a class="m-doc" href="content.html#foo_with_details">foo_with_details()</a>.</p>
 <div class="m-note m-info">
 Detailed docs for this function</div>
           </div></section>
@@ -278,7 +278,7 @@ or parsed in any way.</p>
             <h3>
               content.<wbr /><a href="#DATA_WITH_DETAILS" class="m-doc-self">DATA_WITH_DETAILS</a>: str
             </h3>
-            <p>This is finally a docstring for <code>content.DATA_WITH_DETAILS</code></p>
+            <p>This is finally a docstring for <a class="m-doc" href="content.html#DATA_WITH_DETAILS">DATA_WITH_DETAILS</a></p>
 <p>Detailed docs for the data. <strong>YAY.</strong></p>
           </div></section>
           <section class="m-doc-details" id="DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE"><div>
index 902e5f7018e2d4f61662811df545065b0b1911ea..3c10487be34255daf68b78509f3d994c2e56fae8 100644 (file)
@@ -3,8 +3,13 @@
 .. role:: label-info
     :class: m-label m-info
 
+.. The actual correctness of relative references is tested in
+    inspect_type_links in order to test both absolute -> relative and
+    relative -> absolute direction at the same place. Here it's just verifying
+    that scopes are correctly propagated to all places where it matters.
+
 .. py:module:: content
-    :summary: This overwrites the docstring for ``content``.
+    :summary: This overwrites the docstring for :ref:`content`.
 
     This is detailed module docs. I kinda *hate* how it needs to be indented,
     tho.
@@ -17,7 +22,7 @@
     :summary: This submodule has an external summary.
 
 .. py:class:: content.Class
-    :summary: This overwrites the docstring for ``content.Class``.
+    :summary: This overwrites the docstring for :ref:`Class`.
 
     This is detailed class docs. Here I *also* hate how it needs to be
     indented.
@@ -35,8 +40,8 @@
     A dunder method shown in the detailed view.
 
 .. py:function:: content.Class.method
-    :summary: This overwrites the docstring for ``content.Class.method``, but
-        doesn't add any detailed block.
+    :summary: This overwrites the docstring for :ref:`method()`, but doesn't
+        add any detailed block.
 
 .. py:function:: content.Class.method_with_details
 
     The ``self`` isn't documented and thus also not included in the list.
 
 .. py:property:: content.Class.a_property
-    :summary: This overwrites the docstring for ``content.Class.a_property``,
-        but doesn't add any detailed block.
+    :summary: This overwrites the docstring for :ref:`a_property`, but doesn't
+        add any detailed block.
 
 .. py:property:: content.Class.a_property_with_details
-    :summary: This overwrites the docstring for ``content.Class.a_property_with_details``.
+    :summary: This overwrites the docstring for :ref:`a_property_with_details`.
 
     Detailed property docs.
 
 
 .. py:data:: content.Class.DATA_WITH_DETAILS
 
-    Detailed docs for ``data`` in a class to check rendering.
+    Detailed docs for :ref:`DATA_WITH_DETAILS` in a class to check
+    rendering.
 
 .. py:class:: content.ClassWithSummary
 
     This class has external details but summary from the docstring.
 
 .. py:enum:: content.Enum
-    :summary: This overwrites the docstring for ``content.Enum``, but
-        doesn't add any detailed block.
+    :summary: This overwrites the docstring for :ref:`Enum`, but doesn't
+        add any detailed block.
 
 .. py:enum:: content.EnumWithSummary
 
-    And this is detailed docs added to the docstring summary.
+    And this is detailed docs added to the docstring summary. :ref:`VALUE`!!
 
 .. py:function:: content.foo
-    :summary: This overwrites the docstring for ``content.foo``, but
-        doesn't add any detailed block.
+    :summary: This overwrites the docstring for :ref:`foo()`, but doesn't
+        add any detailed block.
 
 .. py:function:: content.foo_with_details
-    :summary: This overwrites the docstring for ``content.foo_with_details()``.
+    :summary: This overwrites the docstring for :ref:`foo_with_details()`.
 
     .. container:: m-note m-info
 
     :param b: Second
 
 .. py:data:: content.CONSTANT
-    :summary: This is finally a docstring for ``content.CONSTANT``
+    :summary: This is finally a docstring for :ref:`CONSTANT`
 
 .. py:data:: content.DATA_WITH_DETAILS
-    :summary: This is finally a docstring for ``content.DATA_WITH_DETAILS``
+    :summary: This is finally a docstring for :ref:`DATA_WITH_DETAILS`
 
     Detailed docs for the data. **YAY.**
 
index 005f94c98e2d9c6fcc8fd9df737e172f5ed5de5b..864a53cb3218271f1ce6ae00cdc68b7ce2508d3c 100644 (file)
@@ -23,7 +23,8 @@
           <span class="m-breadcrumb"><a href="content_parse_docstrings.html">content_parse_docstrings</a>.<wbr/></span>Class <span class="m-thin">class</span>
         </h1>
         <p>This class has a <em>serious</em> docstring.
-With a multi-line summary.</p>
+With a multi-line summary. Relative reference to <a class="m-doc" href="content_parse_docstrings.Class.html#a_property">a_property</a> works
+even from a summary.</p>
         <div class="m-block m-default">
           <h3>Contents</h3>
           <ul>
@@ -42,7 +43,7 @@ With a multi-line summary.</p>
             <dt>
               <a href="#a_property" class="m-doc">a_property</a>: float <span class="m-label m-flat m-warning">get</span>
             </dt>
-            <dd>This property has a <em>serious</em> docstring.</dd>
+            <dd>The <a class="m-doc" href="content_parse_docstrings.Class.html#a_property">a_property</a> has a <em>serious</em> docstring.</dd>
           </dl>
         </section>
         <section>
@@ -51,7 +52,7 @@ With a multi-line summary.</p>
             <h3>
               content_parse_docstrings.<wbr />Class.<wbr /><a href="#a_property" class="m-doc-self">a_property</a>: float <span class="m-label m-flat m-warning">get</span>
             </h3>
-            <p>This property has a <em>serious</em> docstring.</p>
+            <p>The <a class="m-doc" href="content_parse_docstrings.Class.html#a_property">a_property</a> has a <em>serious</em> docstring.</p>
 <p>And property <strong>details</strong> as well.</p>
           </div></section>
         </section>
index e3d6acb8b74c045e8683de35fdfb2acfaf9fb8d2..f37b9eaa5f2b3e2ba1ef8a0054b1e067637de42b 100644 (file)
@@ -22,7 +22,7 @@
         <h1>
           content_parse_docstrings <span class="m-thin">module</span>
         </h1>
-        <p>This module has a <em>serious</em> docstring.</p>
+        <p>This module has a <em>serious</em> docstring. And a <a class="m-doc" href="content_parse_docstrings.Class.html">Class</a>.</p>
         <div class="m-block m-default">
           <h3>Contents</h3>
           <ul>
@@ -42,7 +42,8 @@
           <dl class="m-doc">
             <dt>class <a href="content_parse_docstrings.Class.html" class="m-doc">Class</a></dt>
             <dd>This class has a <em>serious</em> docstring.
-With a multi-line summary.</dd>
+With a multi-line summary. Relative reference to <a class="m-doc" href="content_parse_docstrings.Class.html#a_property">a_property</a> works
+even from a summary.</dd>
           </dl>
         </section>
         <section id="enums">
@@ -51,7 +52,7 @@ With a multi-line summary.</dd>
             <dt>
               <span class="m-doc-wrap-bumper">class <a href="#Enum" class="m-doc">Enum</a>(enum.Enum): </span><span class="m-doc-wrap"><a href="#Enum-VALUE" class="m-doc">VALUE</a> = 3</span>
             </dt>
-            <dd>This enum has a <em>serious</em> docstring.</dd>
+            <dd>This enum has a <em>serious</em> docstring. <a class="m-doc" href="content_parse_docstrings.html#Enum-VALUE">VALUE</a> works from a summary.</dd>
           </dl>
         </section>
         <section id="functions">
@@ -65,7 +66,7 @@ With a multi-line summary.</dd>
               <span class="m-doc-wrap-bumper">def <a href="#function" class="m-doc">function</a>(</span><span class="m-doc-wrap">a: str,
               b: int) -&gt; float</span>
             </dt>
-            <dd>This function has a <em>serious</em> docstring.</dd>
+            <dd>This <a class="m-doc" href="content_parse_docstrings.html#function">function()</a> has a <em>serious</em> docstring.</dd>
             <dt id="summary_only">
               <span class="m-doc-wrap-bumper">def <a href="#summary_only" class="m-doc-self">summary_only</a>(</span><span class="m-doc-wrap">)</span>
             </dt>
@@ -82,7 +83,7 @@ With a multi-line summary.</dd>
             <h3>
               class content_parse_docstrings.<wbr /><a href="#Enum" class="m-doc-self">Enum</a>(enum.Enum)
             </h3>
-            <p>This enum has a <em>serious</em> docstring.</p>
+            <p>This enum has a <em>serious</em> docstring. <a class="m-doc" href="content_parse_docstrings.html#Enum-VALUE">VALUE</a> works from a summary.</p>
             <table class="m-table m-fullwidth m-flat m-doc">
               <thead><tr><th style="width: 1%">Enumerators</th><th></th></tr></thead>
               <tbody>
@@ -104,7 +105,7 @@ With a multi-line summary.</dd>
               <span class="m-doc-wrap-bumper">def content_parse_docstrings.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#function" class="m-doc-self">function</a>(</span><span class="m-doc-wrap">a: str,
               b: int) -&gt; float</span></span>
             </h3>
-            <p>This function has a <em>serious</em> docstring.</p>
+            <p>This <a class="m-doc" href="content_parse_docstrings.html#function">function()</a> has a <em>serious</em> docstring.</p>
             <table class="m-table m-fullwidth m-flat">
               <thead>
                 <tr><th colspan="2">Parameters</th></tr>
@@ -112,7 +113,7 @@ With a multi-line summary.</dd>
               <tbody>
                 <tr>
                   <td style="width: 1%">a</td>
-                  <td>And parameter docs.
+                  <td>And parameter docs, referring to <a class="m-doc" href="content_parse_docstrings.html#function">function()</a> as well.
 On multiple lines.</td>
                 </tr>
                 <tr>
@@ -123,7 +124,7 @@ On multiple lines.</td>
               <tfoot>
                 <tr>
                   <th>Returns</th>
-                  <td>This too.</td>
+                  <td>This too. In the <a class="m-doc" href="content_parse_docstrings.html#function">function()</a>.</td>
                 </tr>
               </tfoot>
             </table>
index 021628db2131bdb320a529da610e5cb02df969f7..1250a76c3c67b6b75e168f8e91ed983a5ce250d8 100644 (file)
@@ -1,4 +1,4 @@
-"""This module has a *serious* docstring.
+"""This module has a *serious* docstring. And a :ref:`Class`.
 
 And module **details** as well."""
 
@@ -6,18 +6,19 @@ import enum
 
 class Class:
     """This class has a *serious* docstring.
-    With a multi-line summary.
+    With a multi-line summary. Relative reference to :ref:`a_property` works
+    even from a summary.
 
     And class **details** as well."""
 
     @property
     def a_property(self) -> float:
-        """This property has a *serious* docstring.
+        """The :ref:`a_property` has a *serious* docstring.
 
         And property **details** as well."""
 
 class Enum(enum.Enum):
-    """This enum has a *serious* docstring.
+    """This enum has a *serious* docstring. :ref:`VALUE` works from a summary.
 
     And property **details** as well."""
 
@@ -26,12 +27,12 @@ class Enum(enum.Enum):
 Enum.VALUE.__doc__ = "Tho enum value docs are unfortunately *not* processed."
 
 def function(a: str, b: int) -> float:
-    """This function has a *serious* docstring.
+    """This :ref:`function()` has a *serious* docstring.
 
-    :param a: And parameter docs.
+    :param a: And parameter docs, referring to :ref:`function()` as well.
         On multiple lines.
     :param b: *Wow.*
-    :return: This too.
+    :return: This too. In the :ref:`function()`.
 
     And details.
     **Amazing**."""
index 3e73a1c558da42b5b06ef2c4e1db6e57bfdf75e2..fa83660512535fbdd4bfabbf628d103c8faf9ed9 100644 (file)
@@ -40,6 +40,7 @@ class FancyLine(rst.Directive):
         return [node]
 
 post_crawl_call_count = 0
+scope_stack = []
 docstring_call_count = 0
 pre_page_call_count = 0
 post_run_call_count = 0
@@ -48,6 +49,15 @@ def _post_crawl(**kwargs):
     global post_crawl_call_count
     post_crawl_call_count = post_crawl_call_count + 1
 
+def _pre_scope(type, path, **kwargs):
+    global scope_stack
+    scope_stack += [(type, path)]
+
+def _post_scope(type, path, **kwargs):
+    global scope_stack
+    assert scope_stack[-1] == (type, path)
+    scope_stack = scope_stack[:-1]
+
 def _docstring(**kwargs):
     docstring_call_count += 1
 
@@ -59,8 +69,10 @@ def _post_run(**kwargs):
     global post_run_call_count
     post_run_call_count = post_run_call_count + 1
 
-def register_mcss(hooks_post_crawl, hooks_docstring, hooks_pre_page, hooks_post_run, **kwargs):
+def register_mcss(hooks_post_crawl, hooks_pre_scope, hooks_post_scope, hooks_docstring, hooks_pre_page, hooks_post_run, **kwargs):
     hooks_post_crawl += [_post_crawl]
+    hooks_pre_scope += [_pre_scope]
+    hooks_post_scope += [_post_scope]
     hooks_docstring += [_docstring]
     hooks_pre_page += [_pre_page]
     hooks_post_run += [_post_run]
index 773c584173f1bc8f2f38a97fef633e0a0cb25225..980d7d09ec447c93b3a5279536ad2404029087b5 100644 (file)
@@ -90,6 +90,8 @@ class Plugins(BaseTestCase):
         import fancyline
         self.assertEqual(fancyline.post_crawl_call_count, 1)
 
+        self.assertEqual(fancyline.scope_stack, [])
+
         # No code, thus no docstrings processed
         self.assertEqual(fancyline.docstring_call_count, 0)
 
index 0942d38b5cbf8e16567ada39663d1a9bbc3baca0..49b706e0dc3f9909e60050d709af8edd58359c4b 100755 (executable)
@@ -44,7 +44,8 @@ from docutils.parsers.rst.states import Inliner
 sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
 import m.htmlsanity
 
-referer_path = []
+# All those initialized in register() or register_mcss()
+current_referer_path = None
 module_doc_output = None
 class_doc_output = None
 enum_doc_output = None
@@ -236,7 +237,8 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]):
     # Add prefixes of the referer path to the global prefix list, iterate
     # through all of them, with names "closest" to the referer having a
     # priority and try to find the name
-    global intersphinx_inventory, intersphinx_name_prefixes
+    global current_referer_path, intersphinx_inventory, intersphinx_name_prefixes
+    referer_path = current_referer_path[-1] if current_referer_path else []
     prefixes = ['.'.join(referer_path[:len(referer_path) - i]) + '.' for i, _ in enumerate(referer_path)] + intersphinx_name_prefixes
     for prefix in prefixes:
         found = None
@@ -304,6 +306,19 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]):
         node = nodes.literal(rawtext, target, **_options)
     return [node], []
 
+def scope_enter(path, **kwargs):
+    global current_referer_path
+    current_referer_path += [path]
+
+def scope_exit(path, **kwargs):
+    global current_referer_path
+    assert current_referer_path[-1] == path, "%s %s" % (current_referer_path, path)
+    current_referer_path = current_referer_path[:-1]
+
+def check_scope_stack_empty(**kwargs):
+    global current_referer_path
+    assert not current_referer_path
+
 def consume_docstring(type, path: List[str], signature: Optional[str], doc: str) -> str:
     # Create the directive header based on type
     if type.name == 'MODULE':
@@ -369,10 +384,6 @@ def consume_docstring(type, path: List[str], signature: Optional[str], doc: str)
     if doc_output.get('summary') is None: doc_output['summary'] = ''
     if doc_output.get('content') is None: doc_output['content'] = ''
 
-def remember_referer_path(path):
-    global referer_path
-    referer_path = path
-
 def merge_inventories(name_map, **kwargs):
     global intersphinx_inventory
 
@@ -462,8 +473,9 @@ def merge_inventories(name_map, **kwargs):
                     f.write(compressor.compress('{} {} 2 {} {}\n'.format(path, type_, url, title).encode('utf-8')))
             f.write(compressor.flush())
 
-def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_doc_contents, function_doc_contents, property_doc_contents, data_doc_contents, hooks_post_crawl, hooks_docstring, hooks_pre_page, **kwargs):
-    global module_doc_output, class_doc_output, enum_doc_output, function_doc_output, property_doc_output, data_doc_output, inventory_filename
+def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_doc_contents, function_doc_contents, property_doc_contents, data_doc_contents, hooks_post_crawl, hooks_pre_scope, hooks_post_scope, hooks_docstring, hooks_post_run, **kwargs):
+    global current_referer_path, module_doc_output, class_doc_output, enum_doc_output, function_doc_output, property_doc_output, data_doc_output, inventory_filename
+    current_referer_path = []
     module_doc_output = module_doc_contents
     class_doc_output = class_doc_contents
     enum_doc_output = enum_doc_contents
@@ -484,10 +496,13 @@ def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_d
 
     rst.roles.register_local_role('ref', ref)
 
+    hooks_pre_scope += [scope_enter]
+    hooks_post_scope += [scope_exit]
     if mcss_settings.get('M_SPHINX_PARSE_DOCSTRINGS', False):
         hooks_docstring += [consume_docstring]
-    hooks_pre_page += [remember_referer_path]
     hooks_post_crawl += [merge_inventories]
+    # Just a sanity check
+    hooks_post_run += [check_scope_stack_empty]
 
 def _pelican_configure(pelicanobj):
     # For backwards compatibility, the input directory is pelican's CWD