]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
utils: modelling: json schema includes attribute descriptions
authorVasek Sraier <git@vakabus.cz>
Thu, 9 Dec 2021 16:10:49 +0000 (17:10 +0100)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:53 +0000 (16:17 +0200)
related to #17

manager/knot_resolver_manager/datamodel/server_schema.py
manager/knot_resolver_manager/utils/modelling.py
manager/pyproject.toml
manager/tests/unit/datamodel/test_config_schema.py

index 8278165034bbb71ef5e85ac96e390f3f2c427f49..fc9985caaf46b6504250bf3d747147641b34f650 100644 (file)
@@ -37,6 +37,11 @@ BackendEnum = LiteralEnum["auto", "systemd", "supervisord"]
 class ManagementSchema(SchemaNode):
     """
     Configuration of the Manager itself.
+
+    ---
+    listen: Specifies where does the manager listen with its API. Can't be changed in runtime!
+    backend: Forces manager to use a specific service manager. Defaults to autodetection.
+    rundir: Directory where the manager can create files and which will be manager's cwd
     """
 
     # the default listen path here MUST use the default rundir
index 391b695429d2370fb662f0314c1fb4d9d2ddcc63..b368a047ca4ab697183ba9effba65944e7b436a4 100644 (file)
@@ -1,6 +1,8 @@
 import inspect
 from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union, cast
 
+import yaml
+
 from knot_resolver_manager.exceptions import DataException, SchemaException
 from knot_resolver_manager.utils.custom_types import CustomValueType
 from knot_resolver_manager.utils.functional import all_matches
@@ -74,17 +76,64 @@ class Serializable:
         return obj
 
 
+def _split_docstring(docstring: str) -> Tuple[str, Optional[str]]:
+    """
+    Splits docstring into description of the class and description of attributes
+    """
+
+    if "---" not in docstring:
+        return (docstring, None)
+
+    first, last = docstring.split("---", maxsplit=1)
+    return (
+        "\n".join([s.strip() for s in first.splitlines()]).strip(),
+        "\n".join([s.strip() for s in last.splitlines()]).strip(),
+    )
+
+
+def _parse_attrs_docstrings(docstring: str) -> Optional[Dict[str, str]]:
+    """
+    Given a docstring of a SchemaNode, return a dict with descriptions of individual attributes.
+    """
+
+    _, attrs_doc = _split_docstring(docstring)
+    if attrs_doc is None:
+        return None
+
+    # try to parse it as yaml:
+    data = yaml.safe_load(attrs_doc)
+    assert isinstance(data, dict), "Invalid format of attribute description"
+    return cast(Dict[str, str], data)
+
+
 def _get_properties_schema(typ: Type[Any]) -> Dict[Any, Any]:
     schema: Dict[Any, Any] = {}
     annot = typ.__dict__.get("__annotations__", {})
+    docstring: str = typ.__dict__.get("__doc__", "") or ""
+    attribute_documentation = _parse_attrs_docstrings(docstring)
     for name, python_type in annot.items():
         schema[name] = _describe_type(python_type)
+
+        # description
+        if attribute_documentation is not None:
+            if name not in attribute_documentation:
+                raise SchemaException(f"The docstring does not describe field '{name}'", str(typ))
+            schema[name]["description"] = attribute_documentation[name]
+            del attribute_documentation[name]
+
+        # default value
         if hasattr(typ, name):
             assert Serializable.is_serializable(
                 python_type
             ), f"Type '{python_type}' does not appear to be JSON serializable"
             schema[name]["default"] = Serializable.serialize(getattr(typ, name), python_type)
 
+    if attribute_documentation is not None and len(attribute_documentation) > 0:
+        raise SchemaException(
+            f"The docstring describes attributes which are not present - {tuple(attribute_documentation.keys())}",
+            str(typ),
+        )
+
     return schema
 
 
@@ -515,7 +564,7 @@ class SchemaNode:
         if include_schema_definition:
             schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
         if cls.__doc__ is not None:
-            schema["description"] = cls.__doc__.strip()
+            schema["description"] = _split_docstring(cls.__doc__)[0]
         schema["type"] = "object"
         schema["properties"] = _get_properties_schema(cls)
 
index ee28de1f8e92b3778176c9f94410ce05c47b5127..7c30f68989ad72b0072b5d41adb09046ba5e302d 100644 (file)
@@ -114,6 +114,9 @@ disable= [
     "too-many-arguments",  # sure, but how can we change the signatures to take less arguments? artificially create objects with arguments? That's stupid...
     "no-member",  # checked by pyright
     "import-error", # checked by pyright (and pylint does not do it properly)
+    "unsupported-delete-operation", # checked by pyright
+    "unsubscriptable-object", # checked by pyright
+    "unsupported-membership-test", # checked by pyright
 ]
 
 [tool.pylint.SIMILARITIES]
index cbce9e2de5cb1e8aa76e55a610c4dac94cd60c7f..d7786cb0fd8647b10b6e00635c5bd2caa69cc4be 100644 (file)
@@ -1,8 +1,13 @@
 import json
 from typing import Any, Dict, cast
 
+from pytest import raises
+from yaml.nodes import Node
+
 from knot_resolver_manager.datamodel import KresConfig
 from knot_resolver_manager.datamodel.types import IPv6Network96, TimeUnit
+from knot_resolver_manager.exceptions import SchemaException
+from knot_resolver_manager.utils.modelling import SchemaNode
 
 
 def test_dns64_true():
@@ -55,3 +60,61 @@ def test_json_schema():
                 raise Exception(f"failed to serialize '{path}'") from e
 
     recser(dct)
+
+
+def test_attribute_parsing():
+    class TestClass(SchemaNode):
+        """
+        This is an awesome test class
+
+        ---
+        field: This field does nothing interesting
+        value: Neither does this
+        """
+
+        field: str
+        value: int
+
+    schema = TestClass.json_schema()
+    assert schema["properties"]["field"]["description"] == "This field does nothing interesting"
+    assert schema["properties"]["value"]["description"] == "Neither does this"
+
+    class AdditionalItem(SchemaNode):
+        """
+        This class is wrong
+
+        ---
+        field: nope
+        nothing: really nothing
+        """
+
+        nothing: str
+
+    with raises(SchemaException):
+        _ = AdditionalItem.json_schema()
+
+    class WrongDescription(SchemaNode):
+        """
+        This class is wrong
+
+        ---
+        other: description
+        """
+
+        nothing: str
+
+    with raises(SchemaException):
+        _ = WrongDescription.json_schema()
+
+    class NoDescription(SchemaNode):
+        nothing: str
+
+    _ = NoDescription.json_schema()
+
+    class NormalDescription(SchemaNode):
+        """
+        Does nothing special
+        Really
+        """
+
+    _ = NormalDescription.json_schema()