chiark / gitweb /
documentation/python: support write-only properties.
authorVladimír Vondruš <mosra@centrum.cz>
Sat, 13 Jul 2019 20:04:17 +0000 (22:04 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Sun, 14 Jul 2019 17:11:08 +0000 (19:11 +0200)
Supported by Python through a slightly crazy syntax and in pybind11
since 2.3.

doc/documentation/python.rst
documentation/python.py
documentation/templates/python/entry-property.html
documentation/test_python/inspect_string/inspect_string.Foo.html
documentation/test_python/inspect_string/inspect_string/__init__.py
documentation/test_python/inspect_type_links/inspect_type_links.second.Foo.html
documentation/test_python/inspect_type_links/inspect_type_links/second.py
documentation/test_python/pybind_signatures/pybind_signatures.MyClass23.html [new file with mode: 0644]
documentation/test_python/pybind_signatures/pybind_signatures.cpp
documentation/test_python/pybind_signatures/pybind_signatures.html
documentation/test_python/test_pybind.py

index 0d21fb6c3de701f5b23dc4b1f2eae937559ca422..200fb852615fc246de3411ec0997cce4d3e9c856 100644 (file)
@@ -1090,7 +1090,8 @@ Property                            Description
 :py:`property.id`                   Property ID [4]_
 :py:`property.type`                 Property getter return type annotation [1]_
 :py:`property.summary`              Doc summary
-:py:`property.is_writable`          If the property is writable
+:py:`property.is_gettable`          If the property is gettable
+:py:`property.is_settable`          If the property is settable
 :py:`property.is_deletable`         If the property is deletable with :py:`del`
 :py:`property.has_details`          If there is enough content for the full
                                     description block. Currently always set to
index 4f35ddc87d06f7f6ad71476a6b993212d9bf3e23..32c7cb5068906ff7c338db8cadaf118833dce2e0 100755 (executable)
@@ -1027,31 +1027,65 @@ def extract_property_doc(state: State, path: List[str], property):
     out.name = path[-1]
     out.id = state.config['ID_FORMATTER'](EntryType.PROPERTY, path[-1:])
     # TODO: external summary for properties
-    out.summary = extract_summary(state, {}, [], property.__doc__)
+    out.is_gettable = property.fget is not None
+    if property.fget or (property.fset and property.__doc__):
+        out.summary = extract_summary(state, {}, [], property.__doc__)
+    else:
+        assert property.fset
+        out.summary = extract_summary(state, {}, [], property.fset.__doc__)
     out.is_settable = property.fset is not None
     out.is_deletable = property.fdel is not None
     out.has_details = False
 
+    # 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.
+
     try:
-        signature = inspect.signature(property.fget)
+        if property.fget:
+            signature = inspect.signature(property.fget)
+
+            # 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
+            # from inspect instead. This is deliberately done *after*
+            # inspecting the signature because pybind11 properties would throw
+            # TypeError from typing.get_type_hints(). This way they throw
+            # ValueError from inspect and we don't need to handle TypeError in
+            # get_type_hints_or_nothing().
+            type_hints = get_type_hints_or_nothing(state, path, property.fget)
 
-        # 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 from
-        # inspect instead. This is deliberately done *after* inspecting the
-        # signature because pybind11 properties would throw TypeError from
-        # typing.get_type_hints(). This way they throw ValueError from inspect
-        # and we don't need to handle TypeError in get_type_hints_or_nothing().
-        if property.fget: type_hints = get_type_hints_or_nothing(state, path, property.fget)
-
-        if 'return' in type_hints:
-            out.type = extract_annotation(state, path, type_hints['return'])
+            if 'return' in type_hints:
+                out.type = extract_annotation(state, path, type_hints['return'])
+            else:
+                out.type = extract_annotation(state, path, signature.return_annotation)
         else:
-            out.type = extract_annotation(state, path, signature.return_annotation)
+            assert property.fset
+            signature = inspect.signature(property.fset)
+
+            # Same as the lengthy comment above
+            type_hints = get_type_hints_or_nothing(state, path, property.fset)
+
+            # Get second parameter name, then try to fetch it from type_hints
+            # and if that fails get its annotation from the non-dereferenced
+            # version
+            value_parameter = list(signature.parameters.values())[1]
+            if value_parameter.name in type_hints:
+                out.type = extract_annotation(state, path, type_hints[value_parameter.name])
+            else:
+                out.type = extract_annotation(state, path, value_parameter.annotation)
+
     except ValueError:
         # pybind11 properties have the type in the docstring
         if state.config['PYBIND11_COMPATIBILITY']:
-            out.type = parse_pybind_signature(state, path, property.fget.__doc__)[3]
+            if property.fget:
+                out.type = parse_pybind_signature(state, path, property.fget.__doc__)[3]
+            else:
+                assert property.fset
+                parsed_args = parse_pybind_signature(state, path, property.fset.__doc__)[2]
+                # If argument parsing failed, we're screwed
+                if len(parsed_args) == 1: out.type = None
+                else: out.type = parsed_args[1][2]
         else:
             out.type = None
 
index b608938a7cfffa4e9749f3944a1e7da606c0e660..ebaa1ea1671141e7409ae20bcd1c22321e2b1165 100644 (file)
@@ -1,4 +1,4 @@
             <dt>
-              <a href="#{{ property.id }}" {% if property.has_details %}class="m-doc"{% else %}class="m-doc-self" id="{{ property.id }}"{% endif %}>{{ property.name }}</a>{% if property.type %}: {{ property.type }}{% endif %} <span class="m-label m-flat {% if property.is_settable %}m-success{% else %}m-warning{% endif %}">get{% if property.is_settable %} set{% endif %}{% if property.is_deletable %} del{% endif %}</span>
+              <a href="#{{ property.id }}" {% if property.has_details %}class="m-doc"{% else %}class="m-doc-self" id="{{ property.id }}"{% endif %}>{{ property.name }}</a>{% if property.type %}: {{ property.type }}{% endif %} <span class="m-label m-flat {% if property.is_gettable and property.is_settable %}m-success{% elif property.is_gettable %}m-warning{% else %}m-danger{% endif %}">{% if property.is_gettable and property.is_settable %}get set{% elif property.is_gettable %}get{% else %}set{% endif %}{% if property.is_deletable %} del{% endif %}</span>
             </dt>
             <dd>{{ property.summary }}</dd>
index ead3e32191d207ca6c392508a5ed9e28f28dccdf..a029bd112c1853cb92abe78b38faafc8814fd5d4 100644 (file)
               <a href="#writable_property" class="m-doc-self" id="writable_property">writable_property</a> <span class="m-label m-flat m-success">get set</span>
             </dt>
             <dd>Writable property</dd>
+            <dt>
+              <a href="#writeonly_property" class="m-doc-self" id="writeonly_property">writeonly_property</a> <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>Write-only property</dd>
           </dl>
         </section>
         <section id="data">
index 3c5a019b8d714cbbfda15dd3f1a87788cf49a929..425511bce7dcdfa7837110841e78a2a3f80ca267 100644 (file)
@@ -96,6 +96,12 @@ class Foo:
     def deletable_property(self):
         pass
 
+    def writeonly_property(self, a):
+        """Write-only property"""
+        pass
+
+    writeonly_property = property(None, writeonly_property)
+
     @property
     def _private_property(self):
         """A private property"""
index 42c1277d8c7ca7fa090c202a05b96f0d9df1b1f8..33ea06d9b94c7f8bdc117a7e7131b4c0bb459974 100644 (file)
               <a href="#type_property_string_nested" class="m-doc-self" id="type_property_string_nested">type_property_string_nested</a>: typing.Tuple[<a href="inspect_type_links.second.Foo.html" class="m-doc">Foo</a>, typing.List[<a href="inspect_type_links.second.html#Enum" class="m-doc">Enum</a>], typing.Any] <span class="m-label m-flat m-warning">get</span>
             </dt>
             <dd>A property</dd>
+            <dt>
+              <a href="#type_property_writeonly" class="m-doc-self" id="type_property_writeonly">type_property_writeonly</a>: <a href="inspect_type_links.second.html#Enum" class="m-doc">Enum</a> <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>A writeonly property</dd>
+            <dt>
+              <a href="#type_property_writeonly_string_invalid" class="m-doc-self" id="type_property_writeonly_string_invalid">type_property_writeonly_string_invalid</a>: Foo.Bar <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>A writeonly property with invalid string type</dd>
+            <dt>
+              <a href="#type_property_writeonly_string_nested" class="m-doc-self" id="type_property_writeonly_string_nested">type_property_writeonly_string_nested</a>: typing.Tuple[<a href="inspect_type_links.second.Foo.html" class="m-doc">Foo</a>, typing.List[<a href="inspect_type_links.second.html#Enum" class="m-doc">Enum</a>], typing.Any] <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>A writeonly property with a string nested type</dd>
           </dl>
         </section>
         <section id="data">
index 6ac7f75d3ea908f5fa755a88fe44de38843540d3..b77762cc2d4540c30487514d2dc3d318f737b81a 100644 (file)
@@ -28,6 +28,18 @@ class Foo:
     def type_property_string_invalid(self) -> 'FooBar':
         """A property"""
 
+    def type_property_writeonly(self, a: Enum):
+        """A writeonly property"""
+    type_property_writeonly = property(None, type_property_writeonly)
+
+    def type_property_writeonly_string_nested(self, a: 'Tuple[Foo, List[Enum], Any]'):
+        """A writeonly property with a string nested type"""
+    type_property_writeonly_string_nested = property(None, type_property_writeonly_string_nested)
+
+    def type_property_writeonly_string_invalid(self, a: 'Foo.Bar'):
+        """A writeonly property with invalid string type"""
+    type_property_writeonly_string_invalid = property(None, type_property_writeonly_string_invalid)
+
     # Has to be here, because if it would be globally, it would prevent all
     # other data annotations from being retrieved
     TYPE_DATA_STRING_INVALID: 'Foo.Bar' = 3
diff --git a/documentation/test_python/pybind_signatures/pybind_signatures.MyClass23.html b/documentation/test_python/pybind_signatures/pybind_signatures.MyClass23.html
new file mode 100644 (file)
index 0000000..4bb551e
--- /dev/null
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>pybind_signatures.MyClass23 | 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="pybind_signatures.html">pybind_signatures</a>.<wbr/></span>MyClass23 <span class="m-thin">class</span>
+        </h1>
+        <p>Testing pybind 2.3 features</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#properties">Properties</a></li>
+                <li><a href="#data">Data</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="properties">
+          <h2><a href="#properties">Properties</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <a href="#writeonly" class="m-doc-self" id="writeonly">writeonly</a>: float <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>A write-only property</dd>
+            <dt>
+              <a href="#writeonly_crazy" class="m-doc-self" id="writeonly_crazy">writeonly_crazy</a> <span class="m-label m-flat m-danger">set</span>
+            </dt>
+            <dd>A write-only property with a type that can&#x27;t be parsed</dd>
+          </dl>
+        </section>
+        <section id="data">
+          <h2><a href="#data">Data</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <a href="#is_pybind23" class="m-doc-self" id="is_pybind23">is_pybind23</a> = True
+            </dt>
+            <dd></dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
index 79cd63e4fec79c6660efc9ef74ae05b6774fa99c..ab69133da285568a973499721c17bef55c98b227 100644 (file)
@@ -33,6 +33,12 @@ struct MyClass {
     private: float _foo = 0.0f;
 };
 
+struct MyClass23 {
+    void setFoo(float) {}
+
+    void setFooCrazy(const Crazy<3, int>&) {}
+};
+
 void duck(py::args, py::kwargs) {}
 
 template<class T, class U> void tenOverloads(T, U) {}
@@ -68,4 +74,22 @@ PYBIND11_MODULE(pybind_signatures, m) {
         .def("instance_function_kwargs", &MyClass::instanceFunction, "Instance method with position or keyword args", py::arg("hey"), py::arg("what") = "<eh?>")
         .def("another", &MyClass::another, "Instance method with no args, 'self' is thus position-only")
         .def_property("foo", &MyClass::foo, &MyClass::setFoo, "A read/write property");
+
+    py::class_<MyClass23> pybind23{m, "MyClass23", "Testing pybind 2.3 features"};
+
+    /* Checker so the Python side can detect if testing pybind 2.3 features is
+       feasible */
+    pybind23.attr("is_pybind23") =
+        #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 203
+        true
+        #else
+        false
+        #endif
+        ;
+
+    #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 203
+    pybind23
+        .def_property("writeonly", nullptr, &MyClass23::setFoo, "A write-only property")
+        .def_property("writeonly_crazy", nullptr, &MyClass23::setFooCrazy, "A write-only property with a type that can't be parsed");
+    #endif
 }
index 6e31c86b2dd4912c88923975bdf49027108724d3..f610009cef2a62c74152baa42d9a410d9ddc6bbd 100644 (file)
@@ -40,6 +40,8 @@
           <dl class="m-doc">
             <dt>class <a href="pybind_signatures.MyClass.html" class="m-doc">MyClass</a></dt>
             <dd>My fun class!</dd>
+            <dt>class <a href="pybind_signatures.MyClass23.html" class="m-doc">MyClass23</a></dt>
+            <dd>Testing pybind 2.3 features</dd>
           </dl>
         </section>
         <section id="functions">
index b4fd37adeed5cfd02bd6f64999d5a85fd12caf2c..68d1b5639d15fc482f51474bbe6b22ea93e3e175 100644 (file)
@@ -192,6 +192,11 @@ class Signatures(BaseInspectTestCase):
         self.assertEqual(*self.actual_expected_contents('pybind_signatures.html'))
         self.assertEqual(*self.actual_expected_contents('pybind_signatures.MyClass.html'))
 
+        sys.path.append(self.path)
+        import pybind_signatures
+        if pybind_signatures.MyClass23.is_pybind23:
+            self.assertEqual(*self.actual_expected_contents('pybind_signatures.MyClass23.html'))
+
 class Enums(BaseInspectTestCase):
     def __init__(self, *args, **kwargs):
         super().__init__(__file__, 'enums', *args, **kwargs)