From: Vladimír Vondruš Date: Wed, 28 Aug 2019 22:43:11 +0000 (+0200) Subject: documentation/python: implement a bunch of tricks for attrs. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=c81c924d97026a93812f0d50c249c89b36b2c32a;p=blog.git documentation/python: implement a bunch of tricks for attrs. Workarounds! Hacks! Smelly code! --- diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 4291b2fe..3b54d6c0 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -217,11 +217,16 @@ Variable Description :py:`CLASS_INDEX_EXPAND_INNER` Whether to expand inner classes in the class index. If not set, :py:`False` is used. -:py:`PYBIND11_COMPATIBILITY` Enable some additional tricks for better +:py:`PYBIND11_COMPATIBILITY: bool` Enable some additional tricks for better compatibility with pybind11. If not set, :py:`False` is used. See `pybind11 compatibility`_ for more information. +:py:`ATTRS_COMPATIBILITY: bool` Enable some additional tricks for better + compatibility with attrs. If not set, + :py:`False` is used. See + `attrs compatibility`_ for more + information. :py:`SEARCH_DISABLED: bool` Disable search functionality. If this option is set, no search data is compiled and the rendered HTML does not contain @@ -728,6 +733,25 @@ enum class itself, not the values. :gh:`pybind/pybind11#1160`). Support for this feature is not done on the script side yet. +`attrs compatibility`_ +====================== + +If a codebase is using the `attrs `_ package and the +:py:`ATTRS_COMPATIBILITY` option is enabled, the script is able to extract the +(otherwise inaccessible by normal means) information about attributes defined +using :py:`attr.ib()` or via the :py:`@attr.s(auto_attribs=True)` decorator. +Note that attributes of classes using :py:`@attr.s(slots=True)` are visible +even without the compatibility enabled. + +In all cases, there's no possibility of adding in-source docstrings for any of +these and you need to supply the documentation with the :rst:`.. py:property::` +directive as described in `External documentation content`_. + +Additionally, various dunder methods that say just "*Automatically created by +attrs.*" in their docstring are implicitly hidden from the output if this +option is enabled. In order to show them again, override the docstring to +something meaningful. + `Command-line options`_ ======================= diff --git a/documentation/python.py b/documentation/python.py index 647a04df..c9e33cd8 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -148,6 +148,7 @@ default_config = { 'CLASS_INDEX_EXPAND_INNER': False, 'PYBIND11_COMPATIBILITY': False, + 'ATTRS_COMPATIBILITY': False, 'SEARCH_DISABLED': False, 'SEARCH_DOWNLOAD_BINARY': False, @@ -276,6 +277,26 @@ _filtered_builtin_properties = set([ ('__weakref__', "list of weak references to the object (if defined)") ]) +_automatically_created_by_attrs = """ + Automatically created by attrs. + """ +_automatically_created_by_attrs_even_more_indented = """ + Automatically created by attrs. + """ +_filtered_attrs_functions = set([ + ('__ne__', """ + Check equality and either forward a NotImplemented or return the result + negated. + """), + ('__lt__', _automatically_created_by_attrs), + ('__le__', _automatically_created_by_attrs), + ('__gt__', _automatically_created_by_attrs), + ('__ge__', _automatically_created_by_attrs), + ('__repr__', _automatically_created_by_attrs), + ('__getstate__', _automatically_created_by_attrs_even_more_indented), + ('__setstate__', _automatically_created_by_attrs_even_more_indented) +]) + def crawl_enum(state: State, path: List[str], enum_, parent_url): enum_entry = Empty() enum_entry.type = EntryType.ENUM @@ -352,8 +373,22 @@ def crawl_class(state: State, path: List[str], class_): # Filter out underscored methods (but not dunder methods such # as __init__) if name.startswith('_') and not (name.startswith('__') and name.endswith('__')): continue - # Filter out dunder methods that don't have their own docs - if name.startswith('__') and (name, object.__doc__) in _filtered_builtin_functions: continue + # Filter out dunder methods that ... + if name.startswith('__'): + # ... don't have their own docs + if (name, object.__doc__) in _filtered_builtin_functions: continue + # ... or are auto-generated by attrs + if state.config['ATTRS_COMPATIBILITY']: + if (name, object.__doc__) in _filtered_attrs_functions: continue + # Unfortunately the __eq__ doesn't have a docstring, + # try to match it just from the param names + if name == '__eq__' and object.__doc__ is None: + try: + signature = inspect.signature(object) + if 'self' in signature.parameters and 'other' in signature.parameters: + continue + except ValueError: # pragma: no cover + pass elif type == EntryType.PROPERTY: if (name, object.__doc__) in _filtered_builtin_properties: continue if name.startswith('_'): continue # TODO: are there any dunder props? @@ -372,6 +407,28 @@ def crawl_class(state: State, path: List[str], class_): class_entry.members += [name] + # If attrs compatibility is enabled, look for more properties in hidden + # places. + if state.config['ATTRS_COMPATIBILITY'] and hasattr(class_, '__attrs_attrs__'): + for attrib in class_.__attrs_attrs__: + if attrib.name.startswith('_'): continue + + # In some cases, the attribute can be present also among class + # data (for example when using slots). Prefer the info provided by + # attrs (instead of `continue`) as it can provide type annotation + # also when the native annotation isn't used + 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 + entry.path = subpath + entry.url = '{}#{}'.format(class_entry.url, state.config['ID_FORMATTER'](EntryType.PROPERTY, subpath[-1:])) + state.name_map['.'.join(subpath)] = entry + # Add itself to the name map state.name_map['.'.join(path)] = class_entry @@ -1444,12 +1501,36 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: return overloads def extract_property_doc(state: State, parent, entry: Empty): - assert inspect.isdatadescriptor(entry.object) - out = Empty() out.name = entry.path[-1] out.id = state.config['ID_FORMATTER'](EntryType.PROPERTY, entry.path[-1:]) + # If this is a property hammered out of attrs, we parse it differently + if state.config['ATTRS_COMPATIBILITY'] and type(entry.object).__name__ == 'Attribute' and type(entry.object).__module__ == 'attr._make': + # TODO: are there readonly attrs? + out.is_gettable = True + out.is_settable = True + out.is_deletable = True + out.type, out.type_link = extract_annotation(state, entry.path, entry.object.type) + + # Call all scope enter hooks before rendering the docs + for hook in state.hooks_pre_scope: + hook(type=entry.type, path=entry.path) + + # Unfortunately we can't get any docstring for these + out.summary, out.content = extract_docs(state, state.property_docs, entry.type, entry.path, '') + + # Call all scope exit hooks after rendering the docs + for hook in state.hooks_post_scope: + hook(type=entry.type, path=entry.path) + + out.has_details = bool(out.content) + + return out + + # Otherwise we expect a sane thing + assert inspect.isdatadescriptor(entry.object) + # If this is a slot, there won't be any fget / fset / fdel. Assume they're # gettable and settable (couldn't find any way to make them *inspectably* # readonly, all solutions involved throwing from __setattr__()) and diff --git a/documentation/test_python/inspect_attrs/docs.rst b/documentation/test_python/inspect_attrs/docs.rst index 153f558e..df0982ee 100644 --- a/documentation/test_python/inspect_attrs/docs.rst +++ b/documentation/test_python/inspect_attrs/docs.rst @@ -7,11 +7,16 @@ .. py:data:: inspect_attrs.MySlotClass.annotated :summary: This is a float slot. +.. py:data:: inspect_attrs.MyClass.plain_data + :summary: This is plain data, not handled by attrs + .. py:function:: inspect_attrs.MyClass.__init__ :summary: External docs for the init :param annotated: The first argument :param unannotated: This gets the default of four :param complex_annotation: Yes, a list + :param complex_annotation_in_attr: Annotated using ``attr.ib(type=)``, + should be shown as well :param hidden_property: Interesting, but I don't care. The :p:`hidden_property` isn't shown in the output as it's prefixed with diff --git a/documentation/test_python/inspect_attrs/inspect_attrs.MyClass.html b/documentation/test_python/inspect_attrs/inspect_attrs.MyClass.html new file mode 100644 index 00000000..28c14bda --- /dev/null +++ b/documentation/test_python/inspect_attrs/inspect_attrs.MyClass.html @@ -0,0 +1,132 @@ + + + + + inspect_attrs.MyClass | My Python Project + + + + + +
+
+
+
+
+

+ inspect_attrs.MyClass class +

+

A class with attr-defined properties

+
+

Contents

+ +
+
+

Special methods

+
+
+ def __init__(self, + annotated: float, + unannotated = 4, + complex_annotation: typing.List[typing.Tuple[int, float]] = [], + complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = [], + hidden_property: float = 3) -> None +
+
External docs for the init
+
+
+
+

Properties

+
+
+ annotated: float get set del +
+
+
+ unannotated get set del +
+
External docs for this property
+
+ complex_annotation: typing.List[typing.Tuple[int, float]] get set del +
+
+
+ complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] get set del +
+
+
+
+
+

Data

+
+
+ plain_data: float = 35 +
+
This is plain data, not handled by attrs
+
+
+
+

Method documentation

+
+

+ def inspect_attrs.MyClass.__init__(self, + annotated: float, + unannotated = 4, + complex_annotation: typing.List[typing.Tuple[int, float]] = [], + complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = [], + hidden_property: float = 3) -> None +

+

External docs for the init

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Parameters
annotatedThe first argument
unannotatedThis gets the default of four
complex_annotationYes, a list
complex_annotation_in_attrAnnotated using attr.ib(type=), +should be shown as well
hidden_propertyInteresting, but I don't care.
+

The hidden_property isn't shown in the output as it's prefixed with +an underscore.

+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_attrs/inspect_attrs.MyClassAutoAttribs.html b/documentation/test_python/inspect_attrs/inspect_attrs.MyClassAutoAttribs.html new file mode 100644 index 00000000..e2c23002 --- /dev/null +++ b/documentation/test_python/inspect_attrs/inspect_attrs.MyClassAutoAttribs.html @@ -0,0 +1,77 @@ + + + + + inspect_attrs.MyClassAutoAttribs | My Python Project + + + + + +
+
+
+
+
+

+ inspect_attrs.MyClassAutoAttribs class +

+

A class with automatic attr-defined properties

+
+

Contents

+ +
+
+

Special methods

+
+
+ def __init__(self, + annotated: float, + complex_annotation: typing.List[typing.Tuple[int, float]] = []) -> None +
+
+
+
+
+

Properties

+
+
+ annotated: float get set del +
+
+
+ complex_annotation: typing.List[typing.Tuple[int, float]] get set del +
+
This is complex.
+
+
+
+

Data

+
+
+ unannotated = 4 +
+
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_attrs/inspect_attrs.MySlotClass.html b/documentation/test_python/inspect_attrs/inspect_attrs.MySlotClass.html new file mode 100644 index 00000000..bfc0571c --- /dev/null +++ b/documentation/test_python/inspect_attrs/inspect_attrs.MySlotClass.html @@ -0,0 +1,72 @@ + + + + + inspect_attrs.MySlotClass | My Python Project + + + + + +
+
+
+
+
+

+ inspect_attrs.MySlotClass class +

+

A class with attr-defined slots

+
+

Contents

+ +
+
+

Special methods

+
+
+ def __init__(self, + annotated: float, + complex_annotation: typing.List[typing.Tuple[int, float]] = [], + complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = []) -> None +
+
+
+
+
+

Properties

+
+
+ annotated: float get set del +
+
+
+ complex_annotation: typing.List[typing.Tuple[int, float]] get set del +
+
+
+ complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] get set del +
+
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_attrs/inspect_attrs.py b/documentation/test_python/inspect_attrs/inspect_attrs.py index cf3ef99a..7f275d87 100644 --- a/documentation/test_python/inspect_attrs/inspect_attrs.py +++ b/documentation/test_python/inspect_attrs/inspect_attrs.py @@ -9,6 +9,10 @@ class MyClass: annotated: float = attr.ib() unannotated = attr.ib(4) complex_annotation: List[Tuple[int, float]] = attr.ib(default=[]) + complex_annotation_in_attr = attr.ib(default=[], type=List[Tuple[int, float]]) + + # This is just data + plain_data: float = 35 # Shouldn't be shown _hidden_property: float = attr.ib(3) @@ -21,9 +25,10 @@ class MyClassAutoAttribs: unannotated = 4 complex_annotation: List[Tuple[int, float]] = [] -@attr.s(auto_attribs=True, slots=True) +@attr.s(slots=True) class MySlotClass: """A class with attr-defined slots""" - annotated: float - complex_annotation: List[Tuple[int, float]] = [] + annotated: float = attr.ib() + complex_annotation: List[Tuple[int, float]] = attr.ib(default=[]) + complex_annotation_in_attr = attr.ib(default=[], type=List[Tuple[int, float]]) diff --git a/documentation/test_python/test_content.py b/documentation/test_python/test_content.py index e073ebff..18cc1caf 100644 --- a/documentation/test_python/test_content.py +++ b/documentation/test_python/test_content.py @@ -37,6 +37,7 @@ class Content(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('content.docstring_summary.html')) self.assertEqual(*self.actual_expected_contents('content.Class.html')) self.assertEqual(*self.actual_expected_contents('content.ClassWithSummary.html')) + self.assertEqual(*self.actual_expected_contents('content.ClassWithSlots.html')) class ParseDocstrings(BaseInspectTestCase): def test(self): diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index a4b71c99..4a88a3a5 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -215,3 +215,19 @@ inspect_create_intersphinx.pybind py:module 2 inspect_create_intersphinx.pybind. page std:doc 2 page.html - """.lstrip()) # Yes, above it should say A documentation page, but it doesn't + +try: + import attr +except ImportError: + attr = None +class Attrs(BaseInspectTestCase): + @unittest.skipUnless(attr, "the attr package was not found") + def test(self): + self.run_python({ + 'PLUGINS': ['m.sphinx'], + 'INPUT_DOCS': ['docs.rst'], + 'ATTRS_COMPATIBILITY': True + }) + 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'))