`Module inspection`_
====================
-By default, if a module contains the :py:`__all__` attribute, all names listed
-there are exposed in the documentation. Otherwise, all module (and class)
-members are extracted using :py:`inspect.getmembers()`, skipping names
-:py:`import`\ ed from elsewhere and underscored names.
+By default, if a module contains the :py:`__all__` attribute, *all* names
+listed there are exposed in the documentation. Otherwise, all module (and
+class) members are extracted using :py:`inspect.getmembers()`, skipping names
+:py:`import`\ ed from elsewhere and undocumented underscored names.
Detecting if a module is a submodule of the current package or if it's
:py:`import`\ ed from elsewhere is tricky, the script thus includes only
-submodules that have their :py:`__package__` property the same or one level below
-the parent package. If a module's :py:`__package__` is empty, it's assumed to
-be a plain module (instead of a package) and since those can't have submodules,
-all found submodules in it are ignored.
+submodules that have their :py:`__package__` property the same or one level
+below the parent package. If a module's :py:`__package__` is empty, it's
+assumed to be a plain module (instead of a package) and since those can't have
+submodules, all found submodules in it are ignored.
.. block-success:: Overriding the set of included names, module reorganization
The documentation output for enums includes enum value values and the class it
was derived from, so it's possible to know whether it's an enum or a flag.
+`Including underscored names in the output`_
+--------------------------------------------
+
+By default, names starting with an underscore (except for :py:`__dunder__`
+methods) are treated as private and not listed in the output. One way to expose
+them is to list them in :py:`__all__`, however that works for module content
+only. For exposing general underscored names, you either need to provide a
+docstring or `external documentation content`_ (and in case of plain data,
+external documentation content is the only option).
+
+Note that at the point where modules and classes are crawled for members,
+docstrings are *not* parsed yet --- so e.g. a data documentation via a
+:rst:`:data:` option of the :rst:`.. py:class::` `m.sphinx`_ directive won't be
+visible to the initial crawl and thus the data will stay hidden.
+
+Sometimes, however, you'll want the inverse --- keeping an underscored name
+hidden, even though it has a docstring. Solution is to remove the docstring
+while generating the docs, directly in the ``conf.py`` file during module
+import:
+
+.. code:: py
+
+ import mymodule
+ mymodule._private_thing.__doc__ = None
+
+ INPUT_MODULES = [mymodule]
+
`Pages`_
========
# caller should print a warning in this case
return None # pragma: no cover
+def is_docstring_useless(type: EntryType, docstring):
+ # Enum doc is by default set to a generic value. That's very useless.
+ if type == EntryType.ENUM and docstring == 'An enumeration.': return True
+ return not docstring or not docstring.strip()
+
+def is_underscored_and_undocumented(state: State, type, path, docstring):
+ if type == EntryType.MODULE:
+ external_docs = state.module_docs
+ elif type == EntryType.CLASS:
+ external_docs = state.class_docs
+ elif type == EntryType.ENUM:
+ external_docs = state.enum_docs
+ elif type in [EntryType.FUNCTION, EntryType.OVERLOADED_FUNCTION]:
+ external_docs = state.function_docs
+ elif type == EntryType.PROPERTY:
+ external_docs = state.property_docs
+ elif type == EntryType.DATA:
+ external_docs = state.data_docs
+ # Data don't have docstrings, those are from their type instead
+ docstring = None
+ else:
+ assert type is None, type
+ external_docs = {}
+
+ return path[-1].startswith('_') and '.'.join(path) not in external_docs and is_docstring_useless(type, docstring)
+
# Builtin dunder functions have hardcoded docstrings. This is totally useless
# to have in the docs, so filter them out. Uh... kinda ugly.
_filtered_builtin_functions = set([
# name_map)
if type == EntryType.CLASS:
if name in ['__base__', '__class__']: continue # TODO
- if name.startswith('_'): continue
+ if is_underscored_and_undocumented(state, type, subpath, object.__doc__): continue
crawl_class(state, subpath, object)
# Crawl enum values (they also add itself ot the name_map)
elif type == EntryType.ENUM:
- if name.startswith('_'): continue
+ if is_underscored_and_undocumented(state, type, subpath, object.__doc__): continue
crawl_enum(state, subpath, object, class_entry.url)
else:
# Filter out private / unwanted members
if type in [EntryType.FUNCTION, EntryType.OVERLOADED_FUNCTION]:
- # Filter out underscored methods (but not dunder methods such
- # as __init__)
- if name.startswith('_') and not (name.startswith('__') and name.endswith('__')): continue
+ # Filter out undocumented underscored methods (but not dunder
+ # methods such as __init__)
+ # TODO: this won't look into docs saved under a signature but
+ # for that we'd need to parse the signature first, ugh
+ if not (name.startswith('__') and name.endswith('__')) and is_underscored_and_undocumented(state, type, subpath, object.__doc__): continue
# Filter out dunder methods that ...
if name.startswith('__'):
# ... don't have their own docs
pass
elif type == EntryType.PROPERTY:
if (name, object.__doc__) in _filtered_builtin_properties: continue
- if name.startswith('_'): continue # TODO: are there any dunder props?
+ # TODO: are there any interesting dunder props?
+ if is_underscored_and_undocumented(state, type, subpath, object.__doc__): continue
elif type == EntryType.DATA:
- if name.startswith('_'): continue
+ if is_underscored_and_undocumented(state, type, subpath, object.__doc__): continue
else: # pragma: no cover
assert type is None; continue # ignore unknown object types
# places.
if state.config['ATTRS_COMPATIBILITY'] and hasattr(class_, '__attrs_attrs__'):
for attrib in class_.__attrs_attrs__:
- if attrib.name.startswith('_'): continue
+ subpath = path + [attrib.name]
+
+ # No docstrings for attrs (the best we could get would be a
+ # docstring of the variable type, nope to that)
+ if is_underscored_and_undocumented(state, EntryType.PROPERTY, subpath, None): continue
# In some cases, the attribute can be present also among class
# data (for example when using slots). Prefer the info provided by
if attrib.name not in class_entry.members:
class_entry.members += [attrib.name]
- subpath = path + [attrib.name]
-
entry = Empty()
entry.type = EntryType.PROPERTY
entry.object = attrib
# have __module__ equivalent to `path`.
else:
for name, object in inspect.getmembers(module):
- # Filter out underscored names
- if name.startswith('_'): continue
-
# If this is not a module, check if the enclosing module of the
# object is what expected. If not, it's a class/function/...
# imported from elsewhere and we don't want those.
type_ = object_type(state, object, name)
subpath = path + [name]
+ # Filter out undocumented underscored names
+ if is_underscored_and_undocumented(state, type_, subpath, object.__doc__): continue
+
# Crawl the submodules and subclasses recursively (they also add
# itself to the name_map), add other members directly.
- if not type_: # pragma: no cover
+ if type_ is None: # pragma: no cover
# Ignore unknown object types (with __all__ we warn instead)
continue
elif type_ == EntryType.MODULE:
# The happy case
if issubclass(entry.object, enum.Enum):
# Enum doc is by default set to a generic value. That's useless as well.
- if entry.object.__doc__ == 'An enumeration.':
+ if is_docstring_useless(EntryType.ENUM, entry.object.__doc__):
docstring = ''
else:
docstring = entry.object.__doc__
# fget / fset / fdel, instead we need to look into __get__ / __set__ /
# __delete__ directly. This is fairly rare (datetime.date is one and
# BaseException.args is another I could find), so don't bother with it much
- # --- assume readonly and no docstrings / annotations whatsoever.
+ # --- assume readonly. Some docstrings are there for properties; see the
+ # inspect_string.DerivedException test class for details.
if entry.object.__class__.__name__ == 'getset_descriptor' and entry.object.__class__.__module__ == 'builtins':
out.is_gettable = True
out.is_settable = False
out.is_deletable = False
- # Unfortunately we can't get any docstring for these
- out.summary, out.content = extract_docs(state, state.property_docs, entry.type, entry.path, '')
+ out.summary, out.content = extract_docs(state, state.property_docs, entry.type, entry.path, entry.object.__doc__)
out.has_details = bool(out.content)
out.type = None
return out
hooks_pre_page=state.hooks_pre_page,
hooks_post_run=state.hooks_post_run)
+ # First process the doc input files so we have all data for rendering
+ # module/class pages. This needs to be done first so the crawl after can
+ # have a look at the external data and include documented underscored
+ # members as well. On the other hand, this means nothing in render_doc()
+ # has access to the module hierarchy -- all actual content rendering has to
+ # happen later.
+ for file in config['INPUT_DOCS']:
+ render_doc(state, os.path.join(basedir, file))
+
# Crawl all input modules to gather the name tree, put their names into a
# list for the index. The crawl is done breadth-first, so the function
# returns a list of submodules to be crawled next.
for hook in state.hooks_post_crawl:
hook(name_map=state.name_map)
- # Call all registered page begin hooks for the doc rendering
- for hook in state.hooks_pre_page:
- hook(path=[])
-
- # Then process the doc input files so we have all data for rendering
- # module pages. This needs to be done *after* the initial crawl so
- # cross-linking works as expected.
- for file in config['INPUT_DOCS']:
- render_doc(state, os.path.join(basedir, file))
-
# Go through all crawled names and render modules, classes and pages. A
# side effect of the render is entry.summary (and entry.name for pages)
# being filled.
pass
def _private_but_exposed_func():
- # A private thing but exposed in __all__
+ # A private undocumented thing but exposed in __all__
pass
hidden_data = 34
<section id="properties">
<h2><a href="#properties">Properties</a></h2>
<dl class="m-doc">
+ <dt id="__cause__">
+ <a href="#__cause__" class="m-doc-self">__cause__</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>exception cause</dd>
+ <dt id="__context__">
+ <a href="#__context__" class="m-doc-self">__context__</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>exception context</dd>
<dt id="args">
<a href="#args" class="m-doc-self">args</a> <span class="m-label m-flat m-warning">get</span>
</dt>
# This one is a package that shouldn't be exposed
import xml
-# These are descendant packages / modules that should be exposed if not
-# underscored
-from . import subpackage, another_module, _private_module
+# These are descendant packages / modules that should be exposed
+from . import subpackage, another_module
# These are variables from an external modules, shouldn't be exposed either
from re import I
"""A subclass of Foo"""
pass
- class _PrivateSubclass:
- """A private subclass"""
- pass
-
def func(self, a, b):
"""A method"""
pass
- def _private_func(self, a, b):
- """A private function"""
- pass
-
@classmethod
def func_on_class(cls, a):
"""A class method"""
pass
- @classmethod
- def _private_func_on_class(cls, a):
- """A private class method"""
- pass
-
@staticmethod
def static_func(a):
"""A static method"""
pass
- @staticmethod
- def _private_static_func(a):
- """A private static method"""
- pass
-
@property
def a_property(self):
"""A property"""
writeonly_property = property(None, writeonly_property)
- @property
- def _private_property(self):
- """A private property"""
- pass
-
class FooSlots:
"""A class with slots. Can't have docstrings for these."""
FLAG_ONE = 1
FLAG_SEVENTEEN = 17
-class _PrivateClass:
- """Private class"""
- pass
-
def function():
"""A function"""
pass
-def _private_function():
- """A private function"""
- pass
-
A_CONSTANT = 3.24
ENUM_THING = MyEnum.YAY
+++ /dev/null
-"""Private module."""
--- /dev/null
+.. py:module:: inspect_underscored
+ :data _DATA_EXTERNAL_IN_MODULE: Externally in-module documented underscored
+ data
+
+.. py:module:: inspect_underscored._submodule_external
+ :summary: Externally documented underscored submodule
+
+.. py:class:: inspect_underscored._ClassExternal
+ :summary: Externally documented underscored class
+
+.. py:enum:: inspect_underscored._EnumExternal
+ :summary: Externally documented underscored enum
+
+.. py:function:: inspect_underscored._function_external
+ :summary: Externally documented underscored function
+
+.. py:data:: inspect_underscored._DATA_EXTERNAL
+ :summary: Externally documented underscored data
+
+.. py:class:: inspect_underscored.Class
+ :property _property_external_in_class: Externally in-class documented
+ underscored property
+ :data _DATA_EXTERNAL_IN_CLASS: Externally in-class documented underscored
+ data
+
+.. py:function:: inspect_underscored.Class._function_external
+ :summary: Externally documented underscored function
+
+.. py:property:: inspect_underscored.Class._property_external
+ :summary: Externally documented underscored property
+
+.. py:data:: inspect_underscored.Class._DATA_EXTERNAL
+ :summary: Externally documented underscored data
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_underscored.Class | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="inspect_underscored.html">inspect_underscored</a>.<wbr/></span>Class <span class="m-thin">class</span>
+ </h1>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#methods">Methods</a></li>
+ <li><a href="#properties">Properties</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="methods">
+ <h2><a href="#methods">Methods</a></h2>
+ <dl class="m-doc">
+ <dt id="_function">
+ <span class="m-doc-wrap-bumper">def <a href="#_function" class="m-doc-self">_function</a>(</span><span class="m-doc-wrap">self)</span>
+ </dt>
+ <dd>Documented underscored function</dd>
+ <dt id="_function_external">
+ <span class="m-doc-wrap-bumper">def <a href="#_function_external" class="m-doc-self">_function_external</a>(</span><span class="m-doc-wrap">self)</span>
+ </dt>
+ <dd>Externally documented underscored function</dd>
+ </dl>
+ </section>
+ <section id="properties">
+ <h2><a href="#properties">Properties</a></h2>
+ <dl class="m-doc">
+ <dt id="_property">
+ <a href="#_property" class="m-doc-self">_property</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>Documented underscored property</dd>
+ <dt id="_property_external">
+ <a href="#_property_external" class="m-doc-self">_property_external</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>Externally documented underscored property</dd>
+ <dt id="_property_external_in_class">
+ <a href="#_property_external_in_class" class="m-doc-self">_property_external_in_class</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>Externally in-class documented
+underscored property</dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt id="_DATA_EXTERNAL">
+ <a href="#_DATA_EXTERNAL" class="m-doc-self">_DATA_EXTERNAL</a>: int = 5
+ </dt>
+ <dd>Externally documented underscored data</dd>
+ <dt id="_DATA_EXTERNAL_IN_CLASS">
+ <a href="#_DATA_EXTERNAL_IN_CLASS" class="m-doc-self">_DATA_EXTERNAL_IN_CLASS</a>: int = 6
+ </dt>
+ <dd>Externally in-class documented underscored
+data</dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_underscored | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ inspect_underscored <span class="m-thin">module</span>
+ </h1>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#packages">Modules</a></li>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#enums">Enums</a></li>
+ <li><a href="#functions">Functions</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="namespaces">
+ <h2><a href="#namespaces">Modules</a></h2>
+ <dl class="m-doc">
+ <dt>module <a href="inspect_underscored._submodule.html" class="m-doc">_submodule</a></dt>
+ <dd>Documented underscored submodule</dd>
+ <dt>module <a href="inspect_underscored._submodule_external.html" class="m-doc">_submodule_external</a></dt>
+ <dd>Externally documented underscored submodule</dd>
+ </dl>
+ </section>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="inspect_underscored.Class.html" class="m-doc">Class</a></dt>
+ <dd></dd>
+ <dt>class <a href="inspect_underscored._Class.html" class="m-doc">_Class</a></dt>
+ <dd>Documented underscored class</dd>
+ <dt>class <a href="inspect_underscored._ClassExternal.html" class="m-doc">_ClassExternal</a></dt>
+ <dd>Externally documented underscored class</dd>
+ </dl>
+ </section>
+ <section id="enums">
+ <h2><a href="#enums">Enums</a></h2>
+ <dl class="m-doc">
+ <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>Documented underscored enum</dd>
+ <dt id="_EnumExternal">
+ <span class="m-doc-wrap-bumper">class <a href="#_EnumExternal" class="m-doc-self">_EnumExternal</a>(enum.Enum): </span><span class="m-doc-wrap"></span>
+ </dt>
+ <dd>Externally documented underscored enum</dd>
+ </dl>
+ </section>
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ <dt id="_function">
+ <span class="m-doc-wrap-bumper">def <a href="#_function" class="m-doc-self">_function</a>(</span><span class="m-doc-wrap">)</span>
+ </dt>
+ <dd>Documented undercored function</dd>
+ <dt id="_function_external">
+ <span class="m-doc-wrap-bumper">def <a href="#_function_external" class="m-doc-self">_function_external</a>(</span><span class="m-doc-wrap">)</span>
+ </dt>
+ <dd>Externally documented underscored function</dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt id="_DATA_EXTERNAL">
+ <a href="#_DATA_EXTERNAL" class="m-doc-self">_DATA_EXTERNAL</a>: int = 1
+ </dt>
+ <dd>Externally documented underscored data</dd>
+ <dt id="_DATA_EXTERNAL_IN_MODULE">
+ <a href="#_DATA_EXTERNAL_IN_MODULE" class="m-doc-self">_DATA_EXTERNAL_IN_MODULE</a>: int = 2
+ </dt>
+ <dd>Externally in-module documented underscored
+data</dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+"""..
+
+:data _DATA_IN_MODULE: In-module documented underscored data. This won't be
+ picked up by the initial crawl, unfortunately, as the docstrings are
+ processed much later.
+"""
+
+import enum
+
+from . import _submodule, _submodule_external, _submodule_undocumented
+
+class _Class:
+ """Documented underscored class"""
+
+class _ClassExternal: pass
+
+class _ClassUndocumented: pass
+
+class _Enum(enum.Enum):
+ """Documented underscored enum"""
+
+class _EnumExternal(enum.Enum): pass
+
+class _EnumUndocumented(enum.Enum): pass
+
+def _function():
+ """Documented undercored function"""
+
+def _function_external(): pass
+
+def _function_undocumented(): pass
+
+_DATA_IN_MODULE: int = 0
+_DATA_EXTERNAL: int = 1
+_DATA_EXTERNAL_IN_MODULE: int = 2
+_DATA_UNDOCUMENTED: int = 3
+
+class Class:
+ """..
+
+ :property _property_in_class: In-class documented underscored property.
+ This won't be picked up by the initial crawl, unfortunately, as the
+ docstrings are processed much later.
+ :data _DATA_IN_CLASS: In-class documented underscored data. This won't be
+ picked up by the initial crawl, unfortunately, as the docstrings are
+ processed much later.
+ """
+
+ def _function(self):
+ """Documented underscored function"""
+
+ def _function_external(self): pass
+
+ def _function_undocumented(self): pass
+
+ @property
+ def _property(self):
+ """Documented underscored property"""
+
+ @property
+ def _property_in_class(self): pass
+
+ @property
+ def _property_external(self): pass
+
+ @property
+ def _property_external_in_class(self): pass
+
+ @property
+ def _property_undocumented(self): pass
+
+ _DATA_IN_CLASS: int = 4
+ _DATA_EXTERNAL: int = 5
+ _DATA_EXTERNAL_IN_CLASS: int = 6
+ _DATA_UNDOCUMENTED: int = 7
--- /dev/null
+"""Documented underscored submodule"""
self.assertEqual(*self.actual_expected_contents('inspect_attrs.MyClass.html'))
self.assertEqual(*self.actual_expected_contents('inspect_attrs.MyClassAutoAttribs.html'))
self.assertEqual(*self.actual_expected_contents('inspect_attrs.MySlotClass.html'))
+
+class Underscored(BaseInspectTestCase):
+ def test(self):
+ self.run_python({
+ 'PLUGINS': ['m.sphinx'],
+ 'INPUT_DOCS': ['docs.rst'],
+ 'M_SPHINX_PARSE_DOCSTRINGS': True
+ })
+
+ self.assertEqual(*self.actual_expected_contents('inspect_underscored.html'))
+ self.assertEqual(*self.actual_expected_contents('inspect_underscored.Class.html'))
# No code, thus no docstrings processed
self.assertEqual(fancyline.docstring_call_count, 0)
- self.assertEqual(fancyline.pre_page_call_count, 4)
+ # Once for each page, but nonce for render_docs() as that shouldn't
+ # generate any output anyway
+ self.assertEqual(fancyline.pre_page_call_count, 3)
self.assertEqual(fancyline.post_run_call_count, 1)