mkdocs-apidescribed-plugin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mkdocs-apidescribed-plugin might be problematic. Click here for more details.

@@ -0,0 +1,2 @@
1
+
2
+ VERSION = '0.1.0'
@@ -0,0 +1,84 @@
1
+ import yaml
2
+ from mkdocs.plugins import get_plugin_logger
3
+
4
+ log = get_plugin_logger(__name__)
5
+
6
+
7
+ DEFAULT_CONFIG = {
8
+ 'debug': False, # show traceback
9
+ 'skip': False, # do not describe (allows instructions to be ignored)
10
+ 'format': 'auto', # docstrings format (sphinx, google, numpy)
11
+
12
+ 'heading_base': 2, # headings base level
13
+ 'autolinks': True, # automatically create hyperlinks
14
+ 'categorize': True, # show symbol types (categories) titles
15
+
16
+ 'location': {
17
+ # signature block title options
18
+ # hidden - do not show the block
19
+ # module - show module "path"
20
+ # file - show filepath
21
+ 'mode': 'hidden',
22
+ 'line': False, # show line number of source file
23
+ },
24
+ 'icons': {
25
+ 'attr': ':material-alpha:', # for attributes
26
+ 'func': ':material-function:', # for functions
27
+ 'cls': ':material-delta:', # for classes
28
+ 'property': ':material-alpha-p-box-outline:', # for class properties
29
+ 'classmethod': ':material-alpha-c-box-outline:', # for class properties
30
+ 'staticmethod': ':material-alpha-s-box-outline:', # for class properties
31
+ 'inherited': ':material-subdirectory-arrow-left:', # for class inherited items
32
+ 'raises': ':material-lightning-bolt:', # for descriptions of raised exceptions
33
+ 'returns': ':material-arrow-right-thin:', # for descriptions of returned value
34
+ },
35
+
36
+ 'undocumented': False, # describe items without docstrings
37
+ 'types': [
38
+ # symbol types to describe (order respected)
39
+ 'attributes',
40
+ 'functions',
41
+ 'classes',
42
+ ],
43
+ 'ignore': [
44
+ # patterns of symbol names to ignore
45
+ '_*',
46
+ ],
47
+ 'only': [
48
+ # symbol names to describe (others will be ignored)
49
+ ],
50
+ }
51
+ """Default configuration for mkdocs-apidescribed."""
52
+
53
+
54
+ def config_parse(src: str) -> dict:
55
+ try:
56
+ cfg = yaml.load(src, yaml.SafeLoader)
57
+ except yaml.MarkedYAMLError:
58
+ log.exception('Unable to parse options.')
59
+ cfg = {}
60
+
61
+ return cfg or {}
62
+
63
+
64
+ def config_merge(base: dict, *, upd: dict) -> dict:
65
+ out = {}
66
+
67
+ for key, val in base.items():
68
+
69
+ if (upd_val := upd.get(key)) is not None:
70
+
71
+ if isinstance(val, dict) and isinstance(upd_val, dict):
72
+ upd_val = config_merge(val, upd=upd_val)
73
+
74
+ val = upd_val
75
+
76
+ out[key] = val
77
+
78
+ return out
79
+
80
+
81
+ def get_config(src: str) -> dict:
82
+ parsed = config_parse(src)
83
+ merged = config_merge(DEFAULT_CONFIG, upd=parsed)
84
+ return merged
@@ -0,0 +1,104 @@
1
+ import re
2
+
3
+ from markdown import Extension
4
+ from markdown.preprocessors import Preprocessor
5
+ from mkdocs.plugins import get_plugin_logger
6
+
7
+ from .config import get_config
8
+ from .formatter import Formatter
9
+ from .inspector import inspect
10
+
11
+ log = get_plugin_logger(__name__)
12
+
13
+
14
+ class ApidescribedProcessor(Preprocessor):
15
+
16
+ RE_START = re.compile(r'^::: apidescribed:(.*)')
17
+
18
+ cls_fmt = Formatter
19
+
20
+ @classmethod
21
+ def process_instruction(cls, *, target: str, config: dict) -> list[str]:
22
+ cls_fmt = cls.cls_fmt
23
+ out = []
24
+ last_type = ''
25
+
26
+ root, symbols = inspect(target, types=config['types'], ignore=config['ignore'], only=config['only'])
27
+ formatter = cls_fmt(symbol=root, config=config)
28
+
29
+ if prolog := formatter.run():
30
+ out.append(prolog)
31
+
32
+ for symbol in symbols:
33
+ formatter = cls_fmt(symbol=symbol, config=config)
34
+
35
+ if symbol.depth == 0:
36
+ if last_type != symbol.type.name and (categorized := formatter.format_category()):
37
+ out.append(categorized)
38
+
39
+ last_type = symbol.type.name
40
+
41
+ formatted = formatter.run()
42
+ out.append(formatted)
43
+
44
+ return out
45
+
46
+ def run(self, lines: list[str]) -> list[str]:
47
+ out = []
48
+
49
+ re_start = self.RE_START
50
+ process_instruction = self.process_instruction
51
+ instruction = []
52
+
53
+ for line in lines:
54
+
55
+ to_add = [line]
56
+
57
+ if matched := re_start.search(line):
58
+ instruction.append(matched[1].strip())
59
+ to_add = []
60
+
61
+ elif instruction:
62
+ if line.startswith(' '):
63
+ # new option for instruction
64
+ instruction.append(line)
65
+ to_add = []
66
+ else:
67
+ # end of an instruction options block
68
+ debug = False
69
+
70
+ try:
71
+ target = instruction[0]
72
+ config = get_config('\n'.join(instruction[1:]))
73
+
74
+ skip = config['skip']
75
+ debug = config['debug']
76
+
77
+ if skip:
78
+ log.debug(f'Skipped instruction for: {target}')
79
+
80
+ else:
81
+ to_add = process_instruction(target=target, config=config)
82
+
83
+ except Exception as e:
84
+
85
+ to_add = [Formatter.format_error(e, instruction=instruction, debug=debug)]
86
+ log.exception('Automated API documentation error')
87
+
88
+ finally:
89
+ instruction.clear()
90
+
91
+ out.extend(to_add)
92
+
93
+ return out
94
+
95
+
96
+ class ApiDescribedExtension(Extension):
97
+
98
+ def extendMarkdown(self, md):
99
+ md.registerExtension(self)
100
+ md.preprocessors.register(ApidescribedProcessor(md.parser), "apidescribed", 110)
101
+
102
+
103
+ def makeExtension(**kwargs):
104
+ return ApiDescribedExtension(**kwargs)
@@ -0,0 +1,304 @@
1
+ import re
2
+ from textwrap import indent, dedent
3
+ from traceback import format_exc
4
+ from typing import Any
5
+
6
+ import griffe
7
+ from mkdocs.plugins import get_plugin_logger
8
+
9
+ from .inspector import Symbol, SymbolType
10
+ from .parsers import DocstrParser, Block
11
+
12
+ log = get_plugin_logger(__name__)
13
+
14
+ RE_LINK = re.compile(
15
+ r'https?://(www\.)?[A-z0-9@:%.-_+~#=]{1,256}\.[A-z0-9()]{1,6}\b([A-z0-9()@:%-_+.~#?&/=]*)'
16
+ )
17
+
18
+
19
+ class Formatter:
20
+
21
+ category_titles = {
22
+ SymbolType.attr: 'Attributes',
23
+ SymbolType.func: 'Functions',
24
+ SymbolType.cls: 'Classes',
25
+ }
26
+
27
+ @classmethod
28
+ def format_error(cls, exc: Exception, *, instruction: list[str], debug: bool = False) -> str:
29
+
30
+ def indent_raw(val: str) -> str:
31
+ return f' ```\n{indent(val, prefix=' ')}\n ```\n'
32
+
33
+ lines = [
34
+ f'??? failure "Automated documentation error: {exc.__class__.__name__}"',
35
+ ' **Configuration**',
36
+ indent_raw("\n".join(instruction)),
37
+ f' **Error**',
38
+ indent_raw(f'{exc}\n\n{format_exc() if debug else ""}'),
39
+ ]
40
+
41
+ return '\n'.join(lines)
42
+
43
+ @classmethod
44
+ def format_block_admonition(cls, block: Block) -> str:
45
+ return f'!!! {block.marker}\n{cls.reindent(block)}'
46
+
47
+ @classmethod
48
+ def format_block_code(cls, block: Block) -> str:
49
+ return f'```{block.value}\n' + dedent('\n'.join(block.addition)) + '\n```'
50
+
51
+ @classmethod
52
+ def format_block_version(cls, block: Block) -> str:
53
+ return ''
54
+
55
+ @classmethod
56
+ def reindent(cls, src: Any) -> str:
57
+ return indent(dedent(f'{src}'), ' ')
58
+
59
+ def __init__(self, *, symbol: Symbol, config: dict):
60
+ self.symbol = symbol
61
+ self.config = config
62
+
63
+ categorize = config['categorize']
64
+ self.categorize = categorize
65
+
66
+ heading_base = config['heading_base']
67
+ self.heading_base = heading_base
68
+ self.heading_level = heading_base + 1 if categorize else heading_base
69
+
70
+ self.docstr = DocstrParser.parse(self.symbol.docstr, config=config, formatter=self)
71
+
72
+ def signature_reconstruct_params(self) -> dict[str, tuple[str, str]]:
73
+ docstring = self.docstr
74
+
75
+ params = {}
76
+
77
+ for param in self.symbol.params:
78
+ param_name = param.name
79
+ param_str = param_name
80
+ param_note = docstring.params.get(param_str, '')
81
+
82
+ default = param.default
83
+ annotation = param.annotation
84
+
85
+ match param.kind:
86
+ case griffe.ParameterKind.var_positional:
87
+ param_str = f'*{param_str}'
88
+ default = False
89
+
90
+ case griffe.ParameterKind.var_keyword:
91
+ param_str = f'**{param_str}'
92
+ default = False
93
+
94
+ if annotation:
95
+ if default:
96
+ annotation = f'{annotation} '
97
+ param_str = f'{param_str}: {annotation}'
98
+
99
+ if default:
100
+ if annotation:
101
+ default = f' {default}'
102
+ param_str = f'{param_str}={default}'
103
+
104
+ params[param_name] = (f'{param_str},', param_note)
105
+
106
+ return params
107
+
108
+ def signature_build(self, params: dict) -> tuple[str, list[str]]:
109
+ symbol = self.symbol
110
+ reindent = self.reindent
111
+
112
+ notes_counter = 0
113
+ param_strings = []
114
+ param_notes = []
115
+
116
+ for param_str, param_note in params.values():
117
+ postfix = ''
118
+
119
+ if param_note:
120
+ notes_counter += 1
121
+ postfix = f' # ({notes_counter})!' # add annotation index
122
+ param_notes.append(f'{notes_counter}. {reindent(param_note)}\n')
123
+
124
+ param_strings.append(f'{param_str}{postfix}')
125
+
126
+ def get_signature() -> str:
127
+ chunks = [
128
+ '.' if symbol.is_class_member else '',
129
+ f'{symbol.name}(',
130
+ f'{joiner}{indented}',
131
+ f'{joiner}{indented}'.join(param_strings).replace(',', ', ').rstrip(', '),
132
+ f'{joiner})',
133
+ ]
134
+
135
+ if symbol.type == SymbolType.func and (returns := symbol.returns):
136
+ chunks.extend(f' -> {returns}')
137
+
138
+ return ''.join(chunks)
139
+
140
+ indented = ' '
141
+ joiner = '\n'
142
+
143
+ oneline = not len(param_notes)
144
+
145
+ if len(get_signature()) > 80:
146
+ oneline = False
147
+
148
+ if oneline:
149
+ indented = ''
150
+ joiner = ''
151
+
152
+ return get_signature(), param_notes
153
+
154
+ def handle_opt_location(self) -> str:
155
+ symbol = self.symbol
156
+ caption = ''
157
+
158
+ if location := self.config['location']:
159
+
160
+ match location['mode']:
161
+ case 'module':
162
+ caption = f'{symbol.path}'.rpartition('.')[0]
163
+
164
+ case 'file':
165
+ caption = f'{symbol.fpath}'
166
+
167
+ if caption and location['line']:
168
+ caption = f'{caption}:{symbol.line_start}'
169
+
170
+ return caption
171
+
172
+ def format_category(self) -> str:
173
+
174
+ if self.categorize and (category := self.category_titles.get(self.symbol.type)):
175
+ return f'{"#" * self.heading_base} {category}\n'
176
+
177
+ return ''
178
+
179
+ def format_signature(self) -> str:
180
+ symbol = self.symbol
181
+ param_notes = []
182
+
183
+ def get_signature():
184
+ return self.signature_build(self.signature_reconstruct_params())
185
+
186
+ match symbol.type:
187
+ case SymbolType.func | SymbolType.cls:
188
+ signature, param_notes = get_signature()
189
+
190
+ case SymbolType.attr:
191
+ if symbol.is_property:
192
+ signature, param_notes = get_signature()
193
+
194
+ else:
195
+ signature = (
196
+ ('.' if symbol.is_class_member else '') +
197
+ symbol.source
198
+ )
199
+
200
+ case _:
201
+ signature = ''
202
+
203
+ if not signature:
204
+ return ''
205
+
206
+ caption = self.handle_opt_location()
207
+ if caption:
208
+ caption = f'title="{caption}"'
209
+
210
+ lines = [
211
+ f'\n``` py {caption}\n{signature}\n```\n',
212
+ # param descriptions block
213
+ *param_notes,
214
+ ]
215
+
216
+ return '\n'.join(lines)
217
+
218
+ def format_description(self) -> str:
219
+ docstring = self.docstr
220
+
221
+ text = docstring.text
222
+
223
+ text = f'{text}\n'
224
+
225
+ icons = self.config['icons']
226
+
227
+ if raised := docstring.raises:
228
+ raises = '\n\n'.join(
229
+ f'{icons["raises"]} `{raised_key}` {raised_val}'
230
+ for raised_key, raised_val in raised.items()
231
+ )
232
+ text = f'{text}\n{raises}\n'
233
+
234
+ if returns := docstring.returns:
235
+ returns = '\n\n'.join(
236
+ f'{icons["returns"]} {returned}'
237
+ for returned in returns
238
+ )
239
+ text = f'{text}\n{returns}\n'
240
+
241
+ return text
242
+
243
+ def format_title(self) -> str:
244
+ symbol = self.symbol
245
+ icons = self.config['icons']
246
+
247
+ icon_before = icons.get(symbol.type.name, '')
248
+ icons_after = []
249
+
250
+ if symbol.is_inherited:
251
+ icons_after.append(icons['inherited'])
252
+
253
+ if symbol.is_property:
254
+ icons_after.append(icons['property'])
255
+
256
+ if symbol.is_classmethod:
257
+ icons_after.append(icons['classmethod'])
258
+
259
+ if symbol.is_staticmethod:
260
+ icons_after.append(icons['staticmethod'])
261
+
262
+ title = (
263
+ f'{"#" * (self.heading_level + symbol.depth)} '
264
+ f'{icon_before} {symbol.name} {" ".join(icons_after)}\n'
265
+ )
266
+
267
+ return title
268
+
269
+ def indent(self, *, title: str, body: str) -> str:
270
+
271
+ left = '<div style="margin-left:1.8em" markdown>'
272
+ right = '</div>'
273
+
274
+ depth = self.symbol.depth
275
+
276
+ indented = (
277
+ (left * depth) +
278
+ title + (left + body + right) +
279
+ (right * depth)
280
+ )
281
+
282
+ return indented
283
+
284
+ def run(self) -> str:
285
+
286
+ signature = self.format_signature()
287
+
288
+ out = [
289
+ signature,
290
+ self.format_description(),
291
+ ]
292
+
293
+ out = '\n'.join(out)
294
+
295
+ if signature:
296
+ out = self.indent(title=self.format_title(), body=out)
297
+
298
+ if out.strip():
299
+ out = f'{out}\n----'
300
+
301
+ if self.config['autolinks']:
302
+ out = RE_LINK.sub(r'<\g<0>>', out)
303
+
304
+ return out
@@ -0,0 +1,205 @@
1
+ from copy import deepcopy
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from fnmatch import fnmatch
5
+ from typing import Any
6
+
7
+ import griffe
8
+ from mkdocs.plugins import get_plugin_logger
9
+
10
+ log = get_plugin_logger(__name__)
11
+
12
+
13
+ class SymbolType(Enum):
14
+ attr = 1
15
+ func = 2
16
+ cls = 3
17
+ module = 4
18
+
19
+
20
+ @dataclass
21
+ class Symbol:
22
+
23
+ depth: int
24
+ raw: Any
25
+ type: SymbolType
26
+ name: str
27
+ source: str
28
+ docstr: griffe.Docstring
29
+ path: str
30
+ fpath: str
31
+ params: griffe.Parameters | None
32
+ returns: str
33
+ line_start: int = None
34
+ line_end: int = None
35
+
36
+ @property
37
+ def is_property(self) -> bool:
38
+ return 'property' in self.raw.labels
39
+
40
+ @property
41
+ def is_inherited(self) -> bool:
42
+ return self.raw.inherited
43
+
44
+ @property
45
+ def is_class_member(self) -> bool:
46
+ return self.raw.parent.is_class
47
+
48
+ @property
49
+ def is_classmethod(self) -> bool:
50
+ return 'classmethod' in self.raw.labels
51
+
52
+ @property
53
+ def is_staticmethod(self) -> bool:
54
+ return 'staticmethod' in self.raw.labels
55
+
56
+
57
+ def simplify_args(symb: griffe.Object, parameters: griffe.Parameters):
58
+ # drop self, cls
59
+ if symb.is_class or symb.parent.is_class:
60
+ if parameters._params:
61
+ parameters = deepcopy(parameters)
62
+ # Exceptions can have no params
63
+ parameters._params.pop(0)
64
+ return parameters
65
+
66
+
67
+ def get_symbol(
68
+ obj: griffe.Object,
69
+ *,
70
+ types: list[str] = None,
71
+ undocumented: bool = False,
72
+ ignore: list[str] = None,
73
+ only: set[str] = None,
74
+ depth: int = 0,
75
+ ) -> tuple[Symbol, list[Symbol]] | None:
76
+
77
+ symbol_type = 0
78
+ symbols_sub = []
79
+ types = types or []
80
+
81
+ params = getattr(obj, 'parameters', [])
82
+ returns = getattr(obj, 'returns', None)
83
+
84
+ if obj.is_attribute:
85
+ symbol_type = SymbolType.attr
86
+
87
+ elif obj.is_module:
88
+ symbol_type = SymbolType.module
89
+
90
+ elif obj.is_function:
91
+ symbol_type = SymbolType.func
92
+ params = simplify_args(obj, params)
93
+
94
+ elif obj.is_class:
95
+ symbol_type = SymbolType.cls
96
+ params = simplify_args(obj, params)
97
+
98
+ symbols_sub = get_symbols(
99
+ obj,
100
+ types=types,
101
+ undocumented=undocumented,
102
+ ignore=ignore,
103
+ only=only,
104
+ depth=depth + 1
105
+ )
106
+
107
+ if not symbol_type:
108
+ log.error(f'Unsupported symbol type: {obj}. Skipped.')
109
+ return
110
+
111
+ symbol = Symbol(
112
+ depth=depth,
113
+ raw=obj,
114
+ type=symbol_type,
115
+ name=obj.name,
116
+ source=obj.source,
117
+ docstr=obj.docstring,
118
+ path=obj.path,
119
+ fpath=f'{obj.relative_package_filepath}',
120
+ params=params,
121
+ returns=returns,
122
+ line_start=obj.lineno,
123
+ line_end=obj.endlineno,
124
+ )
125
+ return symbol, symbols_sub
126
+
127
+
128
+ def get_symbols(
129
+ container: griffe.Object,
130
+ *,
131
+ types: list[str],
132
+ undocumented: bool = False,
133
+ ignore: list[str] = None,
134
+ only: set[str] = None,
135
+ depth: int = 0
136
+ ) -> list[Symbol]:
137
+
138
+ symbols = []
139
+ ignore = ignore or []
140
+ only = only or set()
141
+
142
+ for key in types:
143
+
144
+ for _, symbol_raw in sorted(getattr(container, key).items(), key=lambda item: item[0]):
145
+
146
+ if not depth and symbol_raw.is_alias:
147
+ # hide imported from other modules
148
+ # but preserve class members inherited from base
149
+ continue
150
+
151
+ name = symbol_raw.name
152
+
153
+ docstring = symbol_raw.docstring
154
+ skip = False
155
+
156
+ if docstring is None and not undocumented:
157
+ continue
158
+
159
+ if only:
160
+ skip = name not in only
161
+
162
+ else:
163
+ for rule in ignore:
164
+ if fnmatch(name, rule):
165
+ skip = True
166
+ break
167
+
168
+ if skip:
169
+ continue
170
+
171
+ symbol, symbols_sub = get_symbol(
172
+ symbol_raw,
173
+ types=types,
174
+ undocumented=undocumented,
175
+ ignore=ignore,
176
+ only=only,
177
+ depth=depth,
178
+ )
179
+
180
+ if symbol:
181
+ symbols.append(symbol)
182
+
183
+ if symbols_sub:
184
+ symbols.extend(symbols_sub)
185
+ # symbols_sub.clear()
186
+
187
+ return symbols
188
+
189
+
190
+ def inspect(
191
+ target: str,
192
+ *,
193
+ types: list[str],
194
+ undocumented: bool = False,
195
+ ignore: list[str],
196
+ only: list[str]
197
+ ) -> tuple[Symbol, list[Symbol]]:
198
+
199
+ log.info(f'Getting API information for: {target} ...')
200
+ obj = griffe.load(target)
201
+
202
+ root, _ = get_symbol(obj)
203
+ symbols = get_symbols(obj, types=types, undocumented=undocumented, ignore=ignore, only=set(only))
204
+
205
+ return root, symbols
@@ -0,0 +1,229 @@
1
+ import re
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass, field
4
+ from textwrap import dedent
5
+ from typing import TYPE_CHECKING
6
+
7
+ import griffe
8
+
9
+ if TYPE_CHECKING:
10
+ from .formatter import Formatter
11
+
12
+
13
+ @dataclass
14
+ class Docstring:
15
+
16
+ text: str = ''
17
+ params: dict[str, str] = field(default_factory=dict)
18
+ returns: list[str] = field(default_factory=list)
19
+ raises: dict[str, str] = field(default_factory=dict)
20
+
21
+
22
+ class Block:
23
+
24
+ unmarked: str = '_unmarked'
25
+
26
+ def __init__(self, marker: str = '', arg: str = '', value: str = ''):
27
+ self.marker = marker or self.unmarked
28
+ self.arg = arg.strip()
29
+ self.value = value
30
+ self.addition = []
31
+
32
+ def __str__(self):
33
+ prefix = f'{value}\n' if (value := self.value) else ''
34
+ return prefix + '\n'.join(self.addition)
35
+
36
+ @property
37
+ def is_marked(self) -> bool:
38
+ return self.marker != self.unmarked
39
+
40
+ def contribute(self, val: str):
41
+ self.addition.append(val)
42
+
43
+
44
+ class DocstrParser:
45
+ alias: str = ''
46
+
47
+ registry: dict[str, type['DocstrParser']] = {}
48
+
49
+ @classmethod
50
+ def parse(cls, docstr: griffe.Docstring | None, *, config: dict, formatter: 'Formatter') -> Docstring:
51
+
52
+ value = getattr(docstr, 'value', None) or ''
53
+ docstring = Docstring(text=f'{value}')
54
+
55
+ if value:
56
+ preferred_parser = config['format']
57
+
58
+ parser = DocstrParser.registry.get(preferred_parser)
59
+ parsers = [parser] if parser else DocstrParser.registry.values()
60
+
61
+ for parser_cls in parsers:
62
+ parser = parser_cls(value, config=config, formatter=formatter)
63
+
64
+ if (result := parser.run()) and parser.markers_detected:
65
+ docstring = result
66
+ break
67
+
68
+ return docstring
69
+
70
+ def __init_subclass__(cls):
71
+ alias = cls.alias
72
+ registry = cls.registry
73
+
74
+ assert alias
75
+ assert alias not in registry
76
+
77
+ super().__init_subclass__()
78
+
79
+ registry[alias] = cls
80
+
81
+ def __init__(self, src: str, *, config: dict, formatter: 'Formatter'):
82
+ self.src = src
83
+ self.config = config
84
+ self.markers_detected = False
85
+ self.formatter = formatter
86
+
87
+ def run(self) -> Docstring | None:
88
+ return self._parse(self.src)
89
+
90
+ def postprocess(self, text: str) -> str:
91
+ return text
92
+
93
+ def _parse(self, value: str) -> Docstring | None: # pragma: nocover
94
+ return
95
+
96
+ def to_dict(self, blocks: list[Block]) -> dict[str, str]:
97
+ result = {
98
+ block.arg: self.postprocess(f'{block}')
99
+ for block in blocks
100
+ }
101
+ return result
102
+
103
+ def to_list(self, blocks: list[Block]) -> list[str]:
104
+ return [self.postprocess(f'{block}'.strip()) for block in blocks]
105
+
106
+
107
+ class SphinxParser(DocstrParser):
108
+ alias: str = 'sphinx'
109
+
110
+ re_markers = re.compile(
111
+ r':(?P<marker>param|type|raises|return|rtype)'
112
+ r'(?P<arg>[^:]*):\s*(?P<value>.*)'
113
+ )
114
+
115
+ re_directives = re.compile(
116
+ r'\.\. (?P<marker>note|warning|code-block|versionadded|versionchanged|deprecated)'
117
+ r'(?P<arg>[^:]*)::\s*(?P<value>.*)'
118
+ )
119
+
120
+ def postprocess(self, text: str) -> str:
121
+ text = super().postprocess(text)
122
+ formatter = self.formatter
123
+
124
+ if (split := text.splitlines()) and len(split) > 1:
125
+ # nested indented block (e.g. in param descriptions)
126
+ # reindent with prefix from 2nd line
127
+ line_two = split[1]
128
+ prefix = ' ' * (len(line_two) - len(line_two.lstrip(' ')))
129
+ text = dedent('\n'.join([
130
+ f'{prefix}{split[0].lstrip()}',
131
+ *split[1:]
132
+ ]))
133
+
134
+ _, processed = self.parse_blocks(
135
+ pattern=self.re_directives,
136
+ value=text,
137
+ )
138
+
139
+ lines = []
140
+
141
+ for block in processed:
142
+ if block.is_marked:
143
+ match block.marker:
144
+ case 'versionadded' | 'versionchanged' | 'deprecated':
145
+ block = formatter.format_block_version(block)
146
+
147
+ case 'note' | 'warning':
148
+ block = formatter.format_block_admonition(block)
149
+
150
+ case 'code-block':
151
+ block = formatter.format_block_code(block)
152
+
153
+ lines.append(f'{block}')
154
+
155
+ return '\n'.join(lines)
156
+
157
+ def parse_blocks(
158
+ self,
159
+ *,
160
+ pattern: re.Pattern,
161
+ value: str,
162
+ ) -> tuple[dict[str, list[Block]], list[Block]]:
163
+
164
+ blocks: dict[str, list[Block]] = defaultdict(list)
165
+ block_last: Block | None = None
166
+ marker_detected = False
167
+
168
+ processed = []
169
+
170
+ for line in value.splitlines():
171
+
172
+ is_empty = len(line.strip()) == 0
173
+
174
+ if block_last:
175
+ if line.startswith(' ') or is_empty:
176
+ # must be a continuation of a block
177
+ block_last.contribute(line)
178
+ continue
179
+
180
+ # previous block ended
181
+ processed.append(block_last)
182
+ block_last = None
183
+
184
+ if matched := pattern.match(line):
185
+ marker_detected = True
186
+ marker = matched.group('marker')
187
+
188
+ arg = matched.group('arg')
189
+ val = matched.group('value')
190
+
191
+ block = Block(marker, arg, val.strip())
192
+ blocks[marker].append(block)
193
+ block_last = block
194
+
195
+ else:
196
+ block = Block()
197
+ block.contribute(line)
198
+ blocks[Block.unmarked].append(block)
199
+ processed.append(block)
200
+
201
+ if block_last:
202
+ # block in the last line
203
+ processed.append(block_last)
204
+
205
+ self.markers_detected = self.markers_detected or marker_detected
206
+
207
+ return blocks, processed
208
+
209
+ def _parse(self, value: str) -> Docstring | None:
210
+
211
+ blocks, processed = self.parse_blocks(
212
+ pattern=self.re_markers,
213
+ value=value,
214
+ )
215
+
216
+ return Docstring(
217
+ text=self.postprocess('\n'.join(map(str, blocks[Block.unmarked]))),
218
+ params=self.to_dict(blocks['param']),
219
+ returns=self.to_list(blocks['return']),
220
+ raises=self.to_dict(blocks['raises']),
221
+ )
222
+
223
+
224
+ class GoogleParser(DocstrParser):
225
+ alias: str = 'google'
226
+
227
+
228
+ class NumpyParser(DocstrParser):
229
+ alias: str = 'numpy'
@@ -0,0 +1,8 @@
1
+ import mkdocs
2
+ from mkdocs.plugins import get_plugin_logger
3
+
4
+ log = get_plugin_logger(__name__)
5
+
6
+
7
+ class ApiDescribedPlugin(mkdocs.plugins.BasePlugin):
8
+ ...
@@ -0,0 +1,174 @@
1
+ """This is a module description.
2
+
3
+ This takes several lines and contains formatted text.
4
+
5
+ https://github.com/idlesign/mkdocs-apidescribed-plugin
6
+
7
+ !!! note
8
+ This text is an admonition.
9
+
10
+ .. warning:: This is a reStructuredText mixed warning.
11
+ """
12
+ from os import environ
13
+
14
+ from markdown.util import deprecated
15
+
16
+ CONSTANT: int = 1
17
+ """Constant description."""
18
+
19
+ ENVIRON = environ
20
+ """Alias description."""
21
+
22
+
23
+ def undocumented():
24
+ pass
25
+
26
+
27
+ def _hidden():
28
+ """Hide it."""
29
+
30
+
31
+ def just_ignore_that():
32
+ """Ignore it."""
33
+
34
+
35
+ def myfunc_rst(
36
+ a: list[str],
37
+ b: int = 0,
38
+ *args,
39
+ another: str,
40
+ **kwargs
41
+ ) -> bool:
42
+ """Function description.
43
+ This one is split into several lines
44
+ yet supposes a single sentence.
45
+
46
+ Uses reStructuredText markup for signature description.
47
+
48
+ See: https://github.com/idlesign/mkdocs-apidescribed-plugin
49
+
50
+ .. note::
51
+ Some note here.
52
+
53
+ .. code-block:: bash
54
+ cd ~
55
+ touch this/
56
+
57
+ :param a: comment for a
58
+ .. versionadded:: 2.5
59
+ Add this.
60
+
61
+ .. versionchanged:: 2.5
62
+ Something has changed.
63
+
64
+ :type a: List[str], optional
65
+ :param b: hint for b
66
+ Several lines.
67
+
68
+ .. warning::
69
+ Some warning
70
+
71
+ Another line.
72
+ https://github.com/idlesign/mkdocs-apidescribed-plugin
73
+ :param args: info for args
74
+ :param another: text for another
75
+ .. deprecated:: 3.1
76
+ Use something instead.
77
+
78
+ :param kwargs: description for kwargs
79
+
80
+ :raises ValueError: If something happens
81
+ :raises KeyError: If something other happens
82
+
83
+ :return: This is returned
84
+ :return: This is also returned
85
+ :rtype: bool
86
+
87
+ """
88
+
89
+
90
+ def myfunc_google(q: str, w: int, e: list, **kwargs) -> bool:
91
+ """Function description.
92
+
93
+ Uses Google markup for signature description.
94
+
95
+ Note:
96
+ Some note for you.
97
+
98
+ Args:
99
+ q (str): Param q
100
+ w: Param w
101
+ e: Param e
102
+ **kwargs: Keyword parameters
103
+
104
+ Yields:
105
+ int: This one
106
+
107
+ Returns:
108
+ bool: True if successful, False otherwise.
109
+
110
+ Even more text for return description.
111
+
112
+ Raises:
113
+ AttributeError: Can raise
114
+ this in some cases.
115
+ ValueError: If something.
116
+
117
+ Examples:
118
+ This is an example.
119
+ Multiple Lines.
120
+
121
+ """
122
+
123
+
124
+ class MyBaseClass:
125
+ """Base class description."""
126
+
127
+ base_attr: str = "base_attr value"
128
+ """Base attr description."""
129
+
130
+ @classmethod
131
+ def method_cl(cls, arg1: str):
132
+ """Class method."""
133
+
134
+ @staticmethod
135
+ def method_st(cls, val):
136
+ """Static method."""
137
+
138
+ def __init__(self, in_a: int):
139
+ """Base __init__ description."""
140
+
141
+ def __call__(self, in_b: int):
142
+ """Base __call__ description."""
143
+
144
+ @property
145
+ def prop_a(self) -> str:
146
+ """Property A."""
147
+
148
+ @prop_a.setter
149
+ def _prop_a_set(self, val: int) -> str:
150
+ """Property A setter."""
151
+
152
+ def __str__(self):
153
+ """Specialized. To string."""
154
+
155
+ def method_a(self, a=1):
156
+ """Base method_a description."""
157
+ return True
158
+
159
+
160
+ class MySubClass(MyBaseClass):
161
+ """Sub class description."""
162
+
163
+ sub_attr = "sub_attr value"
164
+ """Sub attr description."""
165
+
166
+ @deprecated('This one is deprecated.')
167
+ def method_sub_b(
168
+ self,
169
+ a: str,
170
+ b: int,
171
+ d: float,
172
+ ) -> str:
173
+ """Sub method_sub_b description."""
174
+ return ''
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-apidescribed-plugin
3
+ Version: 0.1.0
4
+ Summary: mkdocs plugin to generate API documentation for Python programs
5
+ Project-URL: Homepage, https://github.com/idlesign/mkdocs-apidescribed-plugin
6
+ Project-URL: Documentation, https://mkdocs-apidescribed-plugin.readthedocs.org/
7
+ Author-email: Igor Starikov <idlesign@yandex.ru>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: api,mkdocs,plugin
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: griffe
13
+ Requires-Dist: mkdocs
14
+ Requires-Dist: pyyaml
15
+ Description-Content-Type: text/markdown
16
+
17
+ # mkdocs-apidescribed-plugin
18
+
19
+ https://github.com/idlesign/mkdocs-apidescribed-plugin
20
+
21
+ [![PyPI - Version](https://img.shields.io/pypi/v/mkdocs-apidescribed-plugin)](https://pypi.python.org/pypi/mkdocs-apidescribed-plugin)
22
+ [![License](https://img.shields.io/pypi/l/mkdocs-apidescribed-plugin)](https://pypi.python.org/pypi/mkdocs-apidescribed-plugin)
23
+ [![Coverage](https://img.shields.io/coverallsCoverage/github/idlesign/mkdocs-apidescribed-plugin)](https://coveralls.io/r/idlesign/mkdocs-apidescribed-plugin)
24
+
25
+ ## Description
26
+
27
+ *mkdocs plugin to generate API documentation for Python programs*
28
+
29
+ * Made to autodocument Python code
30
+ * Plays well with [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
31
+ * Configurable
32
+ * Easy to use
33
+
34
+ Just add ``::: apidescribed:`` directive into your documentation followed by a module path:
35
+
36
+ ```md title="index.md"
37
+ This is my documentation
38
+
39
+ ::: apidescribed: somepackage.somemodule
40
+
41
+ ```
42
+
43
+ ## Documentation
44
+
45
+ https://mkdocs-apidescribed-plugin.readthedocs.org/
@@ -0,0 +1,13 @@
1
+ mkdocs_apidescribed/__init__.py,sha256=2FRI0QQVpqa9D3Ny5p2lmon3dWjttDU7YivT4wXMJlM,19
2
+ mkdocs_apidescribed/config.py,sha256=3P5OpESEg5FQMG0FDScVCoM6HJE1yJwu3FxEetH8L2Y,2529
3
+ mkdocs_apidescribed/extension.py,sha256=3iBv7dQr9dOBwqssimo1J3C_R5O8kmq7HUiuzk5o0uY,2980
4
+ mkdocs_apidescribed/formatter.py,sha256=ONVHitfZ0U8MyeRygy1-v9QIdIsXqvc5fv_T8qbfgEw,8387
5
+ mkdocs_apidescribed/inspector.py,sha256=uovf-2BQkoD8O2DXV0RLSvPT_PMIg6qWUAVXHE2nPDM,4731
6
+ mkdocs_apidescribed/parsers.py,sha256=nDHJ7mGECyeLFIOm8dcv6iPDId2AIgbeMbsTgLM_ag4,6429
7
+ mkdocs_apidescribed/plugin.py,sha256=cxlukYTLJnAikD_TiucINExyR6sbHJTwzUYPFGLTjFo,157
8
+ mkdocs_apidescribed/probe.py,sha256=zyXG4HPVeu5mLqcAJerfXCKeU9c8kbB_GOytUJ8A6Xo,3399
9
+ mkdocs_apidescribed_plugin-0.1.0.dist-info/METADATA,sha256=y4qH55fOn8SBCRpq28Uj8wUqLqwGz_F-WHsj7QaQVzM,1566
10
+ mkdocs_apidescribed_plugin-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ mkdocs_apidescribed_plugin-0.1.0.dist-info/entry_points.txt,sha256=xVopkdzEjXeUcUbhsbz9KB1uP2cy2A34H4w437cwXWs,78
12
+ mkdocs_apidescribed_plugin-0.1.0.dist-info/licenses/LICENSE,sha256=Mjp1Mmud42bwXWclwd8Rg-RZokGySkTc_Kt9yEeetr4,1514
13
+ mkdocs_apidescribed_plugin-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ apidescribed = mkdocs_apidescribed.plugin:ApiDescribedPlugin
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2025, Igor `idle sign` Starikov
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the mkdocs-apidescribed-plugin nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+