chiark / gitweb /
documentation/python: implement a bunch of tricks for attrs.
authorVladimír Vondruš <mosra@centrum.cz>
Wed, 28 Aug 2019 22:43:11 +0000 (00:43 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Fri, 30 Aug 2019 14:47:58 +0000 (16:47 +0200)
Workarounds! Hacks! Smelly code!

doc/documentation/python.rst
documentation/python.py
documentation/test_python/inspect_attrs/docs.rst
documentation/test_python/inspect_attrs/inspect_attrs.MyClass.html [new file with mode: 0644]
documentation/test_python/inspect_attrs/inspect_attrs.MyClassAutoAttribs.html [new file with mode: 0644]
documentation/test_python/inspect_attrs/inspect_attrs.MySlotClass.html [new file with mode: 0644]
documentation/test_python/inspect_attrs/inspect_attrs.py
documentation/test_python/test_content.py
documentation/test_python/test_inspect.py

index 4291b2fe1a8be27158e79a5666bdfb22a098f4ef..3b54d6c0e156e4f26ca3fb10391431dd8434fb21 100644 (file)
@@ -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 <https://www.attrs.org/>`_ 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`_
 =======================
 
index 647a04dffe2d5fe5c115ea4b12048e8195cb1cc3..c9e33cd82a49cf80bf3edeb6709778d273b7112a 100755 (executable)
@@ -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
index 153f558e41d263a2a1489d16a0c9c54bc995d029..df0982ee81fe1f41371b1ba90f752bbb006e91cf 100644 (file)
@@ -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 (file)
index 0000000..28c14bd
--- /dev/null
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_attrs.MyClass | 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_attrs.html">inspect_attrs</a>.<wbr/></span>MyClass <span class="m-thin">class</span>
+        </h1>
+        <p>A class with attr-defined properties</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#dunder-methods">Special methods</a></li>
+                <li><a href="#properties">Properties</a></li>
+                <li><a href="#data">Data</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="dunder-methods">
+          <h2><a href="#dunder-methods">Special methods</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#__init__" class="m-doc">__init__</a>(</span><span class="m-doc-wrap">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) -&gt; None</span>
+            </dt>
+            <dd>External docs for the init</dd>
+          </dl>
+        </section>
+        <section id="properties">
+          <h2><a href="#properties">Properties</a></h2>
+          <dl class="m-doc">
+            <dt id="annotated">
+              <a href="#annotated" class="m-doc-self">annotated</a>: float <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+            <dt id="unannotated">
+              <a href="#unannotated" class="m-doc-self">unannotated</a> <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd>External docs for this property</dd>
+            <dt id="complex_annotation">
+              <a href="#complex_annotation" class="m-doc-self">complex_annotation</a>: typing.List[typing.Tuple[int, float]] <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+            <dt id="complex_annotation_in_attr">
+              <a href="#complex_annotation_in_attr" class="m-doc-self">complex_annotation_in_attr</a>: typing.List[typing.Tuple[int, float]] <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+        <section id="data">
+          <h2><a href="#data">Data</a></h2>
+          <dl class="m-doc">
+            <dt id="plain_data">
+              <a href="#plain_data" class="m-doc-self">plain_data</a>: float = 35
+            </dt>
+            <dd>This is plain data, not handled by attrs</dd>
+          </dl>
+        </section>
+        <section>
+          <h2>Method documentation</h2>
+          <section class="m-doc-details" id="__init__"><div>
+            <h3>
+              <span class="m-doc-wrap-bumper">def inspect_attrs.<wbr />MyClass.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#__init__" class="m-doc-self">__init__</a>(</span><span class="m-doc-wrap">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) -&gt; None</span></span>
+            </h3>
+            <p>External docs for the init</p>
+            <table class="m-table m-fullwidth m-flat">
+              <thead>
+                <tr><th colspan="2">Parameters</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td style="width: 1%">annotated</td>
+                  <td>The first argument</td>
+                </tr>
+                <tr>
+                  <td>unannotated</td>
+                  <td>This gets the default of four</td>
+                </tr>
+                <tr>
+                  <td>complex_annotation</td>
+                  <td>Yes, a list</td>
+                </tr>
+                <tr>
+                  <td>complex_annotation_in_attr</td>
+                  <td>Annotated using <code>attr.ib(type=)</code>,
+should be shown as well</td>
+                </tr>
+                <tr>
+                  <td>hidden_property</td>
+                  <td>Interesting, but I don't care.</td>
+                </tr>
+              </tbody>
+            </table>
+<p>The <code>hidden_property</code> isn't shown in the output as it's prefixed with
+an underscore.</p>
+          </div></section>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..e2c2300
--- /dev/null
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_attrs.MyClassAutoAttribs | 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_attrs.html">inspect_attrs</a>.<wbr/></span>MyClassAutoAttribs <span class="m-thin">class</span>
+        </h1>
+        <p>A class with automatic attr-defined properties</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#dunder-methods">Special methods</a></li>
+                <li><a href="#properties">Properties</a></li>
+                <li><a href="#data">Data</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="dunder-methods">
+          <h2><a href="#dunder-methods">Special methods</a></h2>
+          <dl class="m-doc">
+            <dt id="__init__">
+              <span class="m-doc-wrap-bumper">def <a href="#__init__" class="m-doc-self">__init__</a>(</span><span class="m-doc-wrap">self,
+              annotated: float,
+              complex_annotation: typing.List[typing.Tuple[int, float]] = []) -&gt; None</span>
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+        <section id="properties">
+          <h2><a href="#properties">Properties</a></h2>
+          <dl class="m-doc">
+            <dt id="annotated">
+              <a href="#annotated" class="m-doc-self">annotated</a>: float <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+            <dt id="complex_annotation">
+              <a href="#complex_annotation" class="m-doc-self">complex_annotation</a>: typing.List[typing.Tuple[int, float]] <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd>This is complex.</dd>
+          </dl>
+        </section>
+        <section id="data">
+          <h2><a href="#data">Data</a></h2>
+          <dl class="m-doc">
+            <dt id="unannotated">
+              <a href="#unannotated" class="m-doc-self">unannotated</a> = 4
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..bfc0571
--- /dev/null
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_attrs.MySlotClass | 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_attrs.html">inspect_attrs</a>.<wbr/></span>MySlotClass <span class="m-thin">class</span>
+        </h1>
+        <p>A class with attr-defined slots</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#dunder-methods">Special methods</a></li>
+                <li><a href="#properties">Properties</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="dunder-methods">
+          <h2><a href="#dunder-methods">Special methods</a></h2>
+          <dl class="m-doc">
+            <dt id="__init__">
+              <span class="m-doc-wrap-bumper">def <a href="#__init__" class="m-doc-self">__init__</a>(</span><span class="m-doc-wrap">self,
+              annotated: float,
+              complex_annotation: typing.List[typing.Tuple[int, float]] = [],
+              complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = []) -&gt; None</span>
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+        <section id="properties">
+          <h2><a href="#properties">Properties</a></h2>
+          <dl class="m-doc">
+            <dt id="annotated">
+              <a href="#annotated" class="m-doc-self">annotated</a>: float <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+            <dt id="complex_annotation">
+              <a href="#complex_annotation" class="m-doc-self">complex_annotation</a>: typing.List[typing.Tuple[int, float]] <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+            <dt id="complex_annotation_in_attr">
+              <a href="#complex_annotation_in_attr" class="m-doc-self">complex_annotation_in_attr</a>: typing.List[typing.Tuple[int, float]] <span class="m-label m-flat m-success">get set del</span>
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
index cf3ef99a9fe0c5a2fb49e05d36c2bc8de5599d6c..7f275d872458dedb95cffb01150d0f64317cfb06 100644 (file)
@@ -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]])
index e073ebfff9536cb70b7f264f373cad284c53ba45..18cc1caf109ed6bb50961b01fe0af9e095b6402b 100644 (file)
@@ -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):
index a4b71c995335d2de88a27d1c1182f246c183bf16..4a88a3a58e5aa1c7360c8d925aaba9710d553d7e 100644 (file)
@@ -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'))