forked from platypush/platypush
Added multiple parsers for the entities referenced in docstrings.
The `inspect` plugin can now detect references to plugins, backends, events, responses and schemas in docstrings and replace them either with links to the documentation or auto-generated examples.
This commit is contained in:
parent
4f11d7cf74
commit
d7405ad05d
12 changed files with 428 additions and 70 deletions
|
@ -156,6 +156,8 @@ nav {
|
||||||
display: block;
|
display: block;
|
||||||
color: $nav-fg;
|
color: $nav-fg;
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $nav-fg;
|
color: $nav-fg;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row plugin execute-container">
|
<div class="row plugin execute-container" @click="onClick">
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<div class="section command-container">
|
<div class="section command-container">
|
||||||
<div class="section-title">Execute Action</div>
|
<div class="section-title">Execute Action</div>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
<div class="doc-container" v-if="selectedDoc">
|
<div class="doc-container" v-if="selectedDoc">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
Action documentation
|
<a :href="currentActionDocURL">Action documentation</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="doc html">
|
<div class="doc html">
|
||||||
|
@ -217,6 +217,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
currentActionDocURL() {
|
||||||
|
if (!this.action?.name?.length)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
const plugin = this.action.name.split('.').slice(0, -1).join('.')
|
||||||
|
const actionName = this.action.name.split('.').slice(-1)
|
||||||
|
const actionClass = this.action.name
|
||||||
|
.split('.')
|
||||||
|
.slice(0, -1)
|
||||||
|
.map((token) => token.slice(0, 1).toUpperCase() + token.slice(1))
|
||||||
|
.join('') + 'Plugin'
|
||||||
|
|
||||||
|
return 'https://docs.platypush.tech/platypush/plugins/' +
|
||||||
|
`${plugin}.html#platypush.plugins.${plugin}.${actionClass}.${actionName}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async refresh() {
|
async refresh() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
@ -457,6 +475,15 @@ export default {
|
||||||
this.request('procedure.' + this.selectedProcedure.name, args)
|
this.request('procedure.' + this.selectedProcedure.name, args)
|
||||||
.then(this.onResponse).catch(this.onError).finally(this.onDone)
|
.then(this.onResponse).catch(this.onError).finally(this.onDone)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClick(event) {
|
||||||
|
// Intercept any clicks from RST rendered links and open them in a new tab
|
||||||
|
if (event.target.tagName.toLowerCase() === 'a') {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
window.open(event.target.getAttribute('href', '_blank'))
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -857,6 +884,10 @@ $request-headers-btn-width: 7.5em;
|
||||||
background: $procedure-submit-btn-bg;
|
background: $procedure-submit-btn-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-param-value {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
|
|
@ -42,8 +42,13 @@ ul {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
color: $default-link-fg;
|
||||||
|
text-decoration: underline dotted;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Type
|
from typing import Callable, Optional, Type
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
@ -9,12 +9,30 @@ from platypush.message.response import Response
|
||||||
from platypush.plugins import Plugin
|
from platypush.plugins import Plugin
|
||||||
from platypush.utils import get_decorators
|
from platypush.utils import get_decorators
|
||||||
|
|
||||||
|
from ._parsers import (
|
||||||
|
BackendParser,
|
||||||
|
EventParser,
|
||||||
|
MethodParser,
|
||||||
|
PluginParser,
|
||||||
|
ResponseParser,
|
||||||
|
SchemaParser,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Model:
|
class Model:
|
||||||
"""
|
"""
|
||||||
Base class for component models.
|
Base class for component models.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_parsers = [
|
||||||
|
BackendParser,
|
||||||
|
EventParser,
|
||||||
|
MethodParser,
|
||||||
|
PluginParser,
|
||||||
|
ResponseParser,
|
||||||
|
SchemaParser,
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
obj_type: type,
|
obj_type: type,
|
||||||
|
@ -32,8 +50,22 @@ class Model:
|
||||||
self._obj_type = obj_type
|
self._obj_type = obj_type
|
||||||
self.package = obj_type.__module__[len(prefix) :]
|
self.package = obj_type.__module__[len(prefix) :]
|
||||||
self.name = name or self.package
|
self.name = name or self.package
|
||||||
self.doc = doc or obj_type.__doc__
|
|
||||||
self.last_modified = last_modified
|
self.last_modified = last_modified
|
||||||
|
self.doc, argsdoc = self._parse_docstring(doc or obj_type.__doc__ or '')
|
||||||
|
self.args = {}
|
||||||
|
self.has_kwargs = False
|
||||||
|
|
||||||
|
for arg in list(inspect.signature(obj_type).parameters.values())[1:]:
|
||||||
|
if arg.kind == arg.VAR_KEYWORD:
|
||||||
|
self.has_kwargs = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.args[arg.name] = {
|
||||||
|
'default': arg.default
|
||||||
|
if not issubclass(arg.default.__class__, type)
|
||||||
|
else None,
|
||||||
|
'doc': argsdoc.get(arg.name),
|
||||||
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -51,9 +83,62 @@ class Model:
|
||||||
"""
|
"""
|
||||||
Iterator for the model public attributes/values pairs.
|
Iterator for the model public attributes/values pairs.
|
||||||
"""
|
"""
|
||||||
for attr in ['name', 'doc']:
|
for attr in ['name', 'args', 'doc', 'has_kwargs']:
|
||||||
yield attr, getattr(self, attr)
|
yield attr, getattr(self, attr)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_docstring(cls, docstring: str):
|
||||||
|
new_docstring = ''
|
||||||
|
params = {}
|
||||||
|
cur_param = None
|
||||||
|
cur_param_docstring = ''
|
||||||
|
|
||||||
|
if not docstring:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
for line in docstring.split('\n'):
|
||||||
|
m = re.match(r'^\s*:param ([^:]+):\s*(.*)', line)
|
||||||
|
if m:
|
||||||
|
if cur_param:
|
||||||
|
params[cur_param] = cur_param_docstring
|
||||||
|
|
||||||
|
cur_param = m.group(1)
|
||||||
|
cur_param_docstring = m.group(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = re.match(r'^\s*:return:\s+(.*)', line)
|
||||||
|
if m:
|
||||||
|
if cur_param:
|
||||||
|
params[cur_param] = cur_param_docstring
|
||||||
|
|
||||||
|
new_docstring += '\n\n**Returns:**\n\n' + m.group(1).strip() + ' '
|
||||||
|
cur_param = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cur_param:
|
||||||
|
if not line.strip():
|
||||||
|
params[cur_param] = cur_param_docstring
|
||||||
|
cur_param = None
|
||||||
|
cur_param_docstring = ''
|
||||||
|
else:
|
||||||
|
cur_param_docstring += '\n' + line.strip() + ' '
|
||||||
|
else:
|
||||||
|
new_docstring += line + '\n'
|
||||||
|
|
||||||
|
if cur_param:
|
||||||
|
params[cur_param] = cur_param_docstring
|
||||||
|
|
||||||
|
for param, doc in params.items():
|
||||||
|
params[param] = cls._post_process_docstring(doc)
|
||||||
|
|
||||||
|
return cls._post_process_docstring(new_docstring), params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _post_process_docstring(cls, docstring: str) -> str:
|
||||||
|
for parsers in cls._parsers:
|
||||||
|
docstring = parsers.parse(docstring)
|
||||||
|
return docstring.strip()
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class BackendModel(Model):
|
class BackendModel(Model):
|
||||||
|
@ -90,7 +175,7 @@ class PluginModel(Model):
|
||||||
Overrides the default implementation of ``__iter__`` to also include
|
Overrides the default implementation of ``__iter__`` to also include
|
||||||
plugin actions.
|
plugin actions.
|
||||||
"""
|
"""
|
||||||
for attr in ['name', 'actions', 'doc']:
|
for attr in ['name', 'args', 'actions', 'doc', 'has_kwargs']:
|
||||||
if attr == 'actions':
|
if attr == 'actions':
|
||||||
yield attr, {
|
yield attr, {
|
||||||
name: dict(action) for name, action in self.actions.items()
|
name: dict(action) for name, action in self.actions.items()
|
||||||
|
@ -122,66 +207,5 @@ class ActionModel(Model):
|
||||||
Model for plugin action components.
|
Model for plugin action components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, action, **kwargs):
|
def __init__(self, obj_type: Type[Callable], *args, **kwargs):
|
||||||
doc, argsdoc = self._parse_docstring(action.__doc__)
|
super().__init__(obj_type, name=obj_type.__name__, *args, **kwargs)
|
||||||
super().__init__(action, name=action.__name__, doc=doc, **kwargs)
|
|
||||||
|
|
||||||
self.args = {}
|
|
||||||
self.has_kwargs = False
|
|
||||||
|
|
||||||
for arg in list(inspect.signature(action).parameters.values())[1:]:
|
|
||||||
if arg.kind == arg.VAR_KEYWORD:
|
|
||||||
self.has_kwargs = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.args[arg.name] = {
|
|
||||||
'default': arg.default
|
|
||||||
if not issubclass(arg.default.__class__, type)
|
|
||||||
else None,
|
|
||||||
'doc': argsdoc.get(arg.name),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _parse_docstring(cls, docstring: str):
|
|
||||||
new_docstring = ''
|
|
||||||
params = {}
|
|
||||||
cur_param = None
|
|
||||||
cur_param_docstring = ''
|
|
||||||
|
|
||||||
if not docstring:
|
|
||||||
return None, {}
|
|
||||||
|
|
||||||
for line in docstring.split('\n'):
|
|
||||||
if m := re.match(r'^\s*:param ([^:]+):\s*(.*)', line):
|
|
||||||
if cur_param:
|
|
||||||
params[cur_param] = cur_param_docstring
|
|
||||||
|
|
||||||
cur_param = m.group(1)
|
|
||||||
cur_param_docstring = m.group(2)
|
|
||||||
elif m := re.match(r'^\s*:return:\s+(.*)', line):
|
|
||||||
if cur_param:
|
|
||||||
params[cur_param] = cur_param_docstring
|
|
||||||
|
|
||||||
new_docstring += '\n\n**Returns:**\n\n' + m.group(1).strip() + ' '
|
|
||||||
cur_param = None
|
|
||||||
elif re.match(r'^\s*:[^:]+:\s*.*', line):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if cur_param:
|
|
||||||
if not line.strip():
|
|
||||||
params[cur_param] = cur_param_docstring
|
|
||||||
cur_param = None
|
|
||||||
cur_param_docstring = ''
|
|
||||||
else:
|
|
||||||
cur_param_docstring += '\n' + line.strip() + ' '
|
|
||||||
else:
|
|
||||||
new_docstring += line.strip() + ' '
|
|
||||||
|
|
||||||
if cur_param:
|
|
||||||
params[cur_param] = cur_param_docstring
|
|
||||||
|
|
||||||
return new_docstring.strip(), params
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
for attr in ['name', 'args', 'doc', 'has_kwargs']:
|
|
||||||
yield attr, getattr(self, attr)
|
|
||||||
|
|
16
platypush/plugins/inspect/_parsers/__init__.py
Normal file
16
platypush/plugins/inspect/_parsers/__init__.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from ._backend import BackendParser
|
||||||
|
from ._event import EventParser
|
||||||
|
from ._method import MethodParser
|
||||||
|
from ._plugin import PluginParser
|
||||||
|
from ._response import ResponseParser
|
||||||
|
from ._schema import SchemaParser
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BackendParser',
|
||||||
|
'EventParser',
|
||||||
|
'MethodParser',
|
||||||
|
'PluginParser',
|
||||||
|
'ResponseParser',
|
||||||
|
'SchemaParser',
|
||||||
|
]
|
34
platypush/plugins/inspect/_parsers/_backend.py
Normal file
34
platypush/plugins/inspect/_parsers/_backend.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import re
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class BackendParser(Parser):
|
||||||
|
"""
|
||||||
|
Parse backend references in the docstrings with rendered links to their
|
||||||
|
respective documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_backend_regex = re.compile(
|
||||||
|
r'(\s*):class:`(platypush\.backend\.(.+?))`', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._backend_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
class_name = m.group(3).split('.')[-1]
|
||||||
|
package = '.'.join(m.group(3).split('.')[:-1])
|
||||||
|
docstring = cls._backend_regex.sub(
|
||||||
|
f'{m.group(1)}`{class_name} '
|
||||||
|
f'<https://docs.platypush.tech/platypush/backend/{package}.html#{m.group(2)}>`_',
|
||||||
|
docstring,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
12
platypush/plugins/inspect/_parsers/_base.py
Normal file
12
platypush/plugins/inspect/_parsers/_base.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(ABC):
|
||||||
|
"""
|
||||||
|
Base class for parsers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
raise NotImplementedError()
|
34
platypush/plugins/inspect/_parsers/_event.py
Normal file
34
platypush/plugins/inspect/_parsers/_event.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import re
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class EventParser(Parser):
|
||||||
|
"""
|
||||||
|
Parse event references in the docstrings with rendered links to their
|
||||||
|
respective documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_event_regex = re.compile(
|
||||||
|
r'(\s*):class:`(platypush\.message\.event\.(.+?))`', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._event_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
class_name = m.group(3).split('.')[-1]
|
||||||
|
package = '.'.join(m.group(3).split('.')[:-1])
|
||||||
|
docstring = cls._event_regex.sub(
|
||||||
|
f'{m.group(1)}`{class_name} '
|
||||||
|
f'<https://docs.platypush.tech/platypush/events/{package}.html#{m.group(2)}>`_',
|
||||||
|
docstring,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
35
platypush/plugins/inspect/_parsers/_method.py
Normal file
35
platypush/plugins/inspect/_parsers/_method.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import re
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class MethodParser(Parser):
|
||||||
|
"""
|
||||||
|
Parse method references in the docstrings with rendered links to their
|
||||||
|
respective documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_method_regex = re.compile(
|
||||||
|
r'(\s*):meth:`(platypush\.plugins\.(.+?))`', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._method_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
tokens = m.group(3).split('.')
|
||||||
|
method = tokens[-1]
|
||||||
|
package = '.'.join(tokens[:-2])
|
||||||
|
docstring = cls._method_regex.sub(
|
||||||
|
f'{m.group(1)}`{package}.{method} '
|
||||||
|
f'<https://docs.platypush.tech/platypush/plugins/{package}.html#{m.group(2)}>`_',
|
||||||
|
docstring,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
34
platypush/plugins/inspect/_parsers/_plugin.py
Normal file
34
platypush/plugins/inspect/_parsers/_plugin.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import re
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class PluginParser(Parser):
|
||||||
|
"""
|
||||||
|
Parse plugin references in the docstrings with rendered links to their
|
||||||
|
respective documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_plugin_regex = re.compile(
|
||||||
|
r'(\s*):class:`(platypush\.plugins\.(.+?))`', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._plugin_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
class_name = m.group(3).split('.')[-1]
|
||||||
|
package = '.'.join(m.group(3).split('.')[:-1])
|
||||||
|
docstring = cls._plugin_regex.sub(
|
||||||
|
f'{m.group(1)}`{class_name} '
|
||||||
|
f'<https://docs.platypush.tech/platypush/plugins/{package}.html#{m.group(2)}>`_',
|
||||||
|
docstring,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
34
platypush/plugins/inspect/_parsers/_response.py
Normal file
34
platypush/plugins/inspect/_parsers/_response.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import re
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseParser(Parser):
|
||||||
|
"""
|
||||||
|
Parse response references in the docstrings with rendered links to their
|
||||||
|
respective documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_response_regex = re.compile(
|
||||||
|
r'(\s*):class:`(platypush\.message\.response\.(.+?))`', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._response_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
class_name = m.group(3).split('.')[-1]
|
||||||
|
package = '.'.join(m.group(3).split('.')[:-1])
|
||||||
|
docstring = cls._response_regex.sub(
|
||||||
|
f'{m.group(1)}`{class_name} '
|
||||||
|
f'<https://docs.platypush.tech/platypush/responses/{package}.html#{m.group(2)}>`_',
|
||||||
|
docstring,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
97
platypush/plugins/inspect/_parsers/_schema.py
Normal file
97
platypush/plugins/inspect/_parsers/_schema.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from random import randint
|
||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
import platypush.schemas
|
||||||
|
|
||||||
|
from ._base import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaParser(Parser):
|
||||||
|
"""
|
||||||
|
Support for response/message schemas in the docs. Format: ``.. schema:: rel_path.SchemaClass(arg1=value1, ...)``,
|
||||||
|
where ``rel_path`` is the path of the schema relative to ``platypush/schemas``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_schemas_path = os.path.dirname(inspect.getfile(platypush.schemas))
|
||||||
|
_schema_regex = re.compile(
|
||||||
|
r'^(\s*)\.\.\s+schema::\s*([a-zA-Z0-9._]+)\s*(\((.+?)\))?', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_field_value(cls, field):
|
||||||
|
metadata = getattr(field, 'metadata', {})
|
||||||
|
if metadata.get('example'):
|
||||||
|
return metadata['example']
|
||||||
|
if metadata.get('description'):
|
||||||
|
return metadata['description']
|
||||||
|
|
||||||
|
if isinstance(field, fields.Number):
|
||||||
|
return randint(1, 99)
|
||||||
|
if isinstance(field, fields.Boolean):
|
||||||
|
return bool(randint(0, 1))
|
||||||
|
if isinstance(field, fields.URL):
|
||||||
|
return 'https://example.org'
|
||||||
|
if isinstance(field, fields.List):
|
||||||
|
return [cls._get_field_value(field.inner)]
|
||||||
|
if isinstance(field, fields.Dict):
|
||||||
|
return {
|
||||||
|
cls._get_field_value(field.key_field)
|
||||||
|
if field.key_field
|
||||||
|
else 'key': cls._get_field_value(field.value_field)
|
||||||
|
if field.value_field
|
||||||
|
else 'value'
|
||||||
|
}
|
||||||
|
if isinstance(field, fields.Nested):
|
||||||
|
ret = {
|
||||||
|
name: cls._get_field_value(f)
|
||||||
|
for name, f in field.nested().fields.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ret] if field.many else ret
|
||||||
|
|
||||||
|
return str(field.__class__.__name__).lower()
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, docstring: str) -> str:
|
||||||
|
while True:
|
||||||
|
m = cls._schema_regex.search(docstring)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
|
||||||
|
schema_module_name = '.'.join(
|
||||||
|
['platypush.schemas', *(m.group(2).split('.')[:-1])]
|
||||||
|
)
|
||||||
|
schema_module = importlib.import_module(schema_module_name)
|
||||||
|
schema_class = getattr(schema_module, m.group(2).split('.')[-1])
|
||||||
|
schema_args = eval(f'dict({m.group(4)})') if m.group(4) else {}
|
||||||
|
schema = schema_class(**schema_args)
|
||||||
|
parsed_schema = {
|
||||||
|
name: cls._get_field_value(field)
|
||||||
|
for name, field in schema.fields.items()
|
||||||
|
if not field.load_only
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.many:
|
||||||
|
parsed_schema = [parsed_schema]
|
||||||
|
|
||||||
|
padding = m.group(1)
|
||||||
|
docstring = cls._schema_regex.sub(
|
||||||
|
textwrap.indent('\n\n.. code-block:: json\n\n', padding)
|
||||||
|
+ textwrap.indent(
|
||||||
|
json.dumps(parsed_schema, sort_keys=True, indent=2),
|
||||||
|
padding + ' ',
|
||||||
|
).replace('\n\n', '\n')
|
||||||
|
+ '\n\n',
|
||||||
|
docstring,
|
||||||
|
)
|
||||||
|
|
||||||
|
return docstring
|
Loading…
Reference in a new issue