sphinx-lua-ls 0.0.4__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.
@@ -0,0 +1,632 @@
1
+ """
2
+ Autodoc directives for lua.
3
+
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ import functools
10
+ import math
11
+ import re
12
+ import textwrap
13
+ from typing import Any, Callable, ClassVar, Type
14
+
15
+ import docutils.nodes
16
+ import docutils.statemachine
17
+ import sphinx.addnodes
18
+ import sphinx.util.docutils
19
+ import sphinx.util.nodes
20
+ from docutils.parsers.rst import directives
21
+ from sphinx.util.docutils import SphinxDirective
22
+
23
+ import sphinx_lua_ls.domain
24
+ from sphinx_lua_ls.doctree import Kind, Object, Visibility
25
+
26
+
27
+ class AutodocUtilsMixin(SphinxDirective):
28
+ """
29
+ Provides facilities for rendering automatically generated documentation.
30
+
31
+ """
32
+
33
+ # Override type of `option_spec` to make it compatible with `ObjectDescription`.
34
+ option_spec: ClassVar[dict[str, Callable[[str], Any]]] # type: ignore
35
+
36
+ def push_context(self, modname: str, classname: str):
37
+ classes = self.env.ref_context.setdefault("lua:classes", [])
38
+ classes.append(self.env.ref_context.get("lua:class"))
39
+ self.env.ref_context["lua:class"] = classname
40
+
41
+ modules = self.env.ref_context.setdefault("lua:modules", [])
42
+ modules.append(self.env.ref_context.get("lua:module"))
43
+ self.env.ref_context["lua:module"] = modname
44
+
45
+ def pop_context(self):
46
+ classes = self.env.ref_context.setdefault("lua:classes", [])
47
+ if classes:
48
+ self.env.ref_context["lua:class"] = classes.pop()
49
+ else:
50
+ self.env.ref_context.pop("lua:class")
51
+
52
+ modules = self.env.ref_context.setdefault("lua:modules", [])
53
+ if modules:
54
+ self.env.ref_context["lua:module"] = modules.pop()
55
+ else:
56
+ self.env.ref_context.pop("lua:module")
57
+
58
+ @contextlib.contextmanager
59
+ def save_context(self):
60
+ modname = self.env.ref_context.get("lua:module")
61
+ classname = self.env.ref_context.get("lua:classname")
62
+ try:
63
+ yield
64
+ finally:
65
+ self.env.ref_context["lua:module"] = modname
66
+ self.env.ref_context["lua:classname"] = classname
67
+
68
+ def render(self, root: Object, name: str, pass_through: bool = False):
69
+ if not self.env.ref_context.get("lua:class") and root.kind == Kind.Module:
70
+ # This is a module.
71
+ return self.render_module(root, name, pass_through)
72
+ elif root.kind == Kind.Module:
73
+ # This is a module inside of a class. We will render it as a class.
74
+ return self.render_class(root, name, pass_through)
75
+ elif root.kind == Kind.Data:
76
+ return self.render_data(root, name, pass_through)
77
+ elif root.kind == Kind.Function:
78
+ return self.render_function(root, name, pass_through)
79
+ elif root.kind == Kind.Class:
80
+ return self.render_class(root, name, pass_through)
81
+ elif root.kind == Kind.Alias:
82
+ return self.render_alias(root, name, pass_through)
83
+ else:
84
+ raise RuntimeError(f"unknown lua object kind {root.kind}")
85
+
86
+ def render_module(self, root: Object, name: str, pass_through: bool = False):
87
+ with self.save_context():
88
+ nodes = list(
89
+ self._create_directive(
90
+ name, sphinx_lua_ls.domain.LuaModule, "lua:module", pass_through
91
+ ).run()
92
+ )
93
+
94
+ container = docutils.nodes.container()
95
+ nodes.append(container)
96
+
97
+ if root.docstring:
98
+ self.render_docs(
99
+ str(root.file or f"<docstring for {self.arguments[0]}>"),
100
+ root.line or 0,
101
+ root.docstring,
102
+ container,
103
+ )
104
+
105
+ for name, child in self.get_children(root):
106
+ nodes.extend(self.render(child, name))
107
+
108
+ return nodes
109
+
110
+ def render_data(self, root: Object, name: str, pass_through: bool = False):
111
+ with self.save_context():
112
+ return self._create_directive(
113
+ name,
114
+ LuaData,
115
+ "lua:data",
116
+ pass_through,
117
+ root=root,
118
+ ).run()
119
+
120
+ def render_function(self, root: Object, name: str, pass_through: bool = False):
121
+ with self.save_context():
122
+ return self._create_directive(
123
+ name,
124
+ LuaFunction,
125
+ "lua:function",
126
+ pass_through,
127
+ root=root,
128
+ ).run()
129
+
130
+ def render_class(self, root: Object, name: str, pass_through: bool = False):
131
+ with self.save_context():
132
+ return self._create_directive(
133
+ name,
134
+ LuaClass,
135
+ "lua:class",
136
+ pass_through,
137
+ root=root,
138
+ ).run()
139
+
140
+ def render_alias(self, root: Object, name: str, pass_through: bool = False):
141
+ with self.save_context():
142
+ return self._create_directive(
143
+ name,
144
+ LuaAlias,
145
+ "lua:alias",
146
+ pass_through,
147
+ root=root,
148
+ ).run()
149
+
150
+ def render_docs(self, path: str, line: int, docs: str, node, titles=False):
151
+ docs = re.sub(r"^\@\*\w+\*.*$", "", docs, flags=re.MULTILINE)
152
+ docs = re.sub(r"^```lua\n.*?\n```", "", docs, flags=re.MULTILINE | re.DOTALL)
153
+ see_sections = list(re.finditer(r"^See:\n", docs, flags=re.MULTILINE))
154
+ if see_sections:
155
+ match = see_sections[-1]
156
+ see_section = docs[match.span()[1] :]
157
+ docs = docs[: match.span()[0]]
158
+ else:
159
+ see_section = ""
160
+
161
+ see_lines = []
162
+ rejected_see_lines = []
163
+ for see_line in see_section.splitlines():
164
+ if match := re.match(
165
+ r"""
166
+ ^[ ][ ]\*[ ]
167
+ (?:
168
+ ~(?P<rejected_type>.+?)~ (?P<rejected_doc>.*)
169
+ |
170
+ \[(?P<type>.+?)\]\(.*?\) (?P<doc>.*)
171
+ )
172
+ $
173
+ """,
174
+ see_line,
175
+ flags=re.VERBOSE,
176
+ ):
177
+ typ = match.group("type") or match.group("rejected_type")
178
+ doc = match.group("doc") or match.group("rejected_doc") or ""
179
+ if doc:
180
+ doc = ": " + doc
181
+ see_lines.append(f":lua:obj:`{typ}`{doc}")
182
+ else:
183
+ rejected_see_lines.append(see_line)
184
+
185
+ if rejected_see_lines:
186
+ docs += "\n\nSee:\n" + "\n".join(rejected_see_lines)
187
+
188
+ docs = textwrap.dedent(docs)
189
+
190
+ if len(see_lines) > 1:
191
+ see_lines = ["", "See:", ""] + [
192
+ nl for l in see_lines for nl in (f"- {l}", "")
193
+ ]
194
+ else:
195
+ see_lines = [""] + [f"See: {l}" for l in see_lines]
196
+
197
+ lines = docs.splitlines() + see_lines
198
+
199
+ items = [(path, line)] * len(lines)
200
+
201
+ content = docutils.statemachine.StringList(lines, items=items)
202
+
203
+ with sphinx.util.docutils.switch_source_input(self.state, content):
204
+ if titles:
205
+ sphinx.util.nodes.nested_parse_with_titles(self.state, content, node)
206
+ else:
207
+ self.state.nested_parse(content, 0, node)
208
+
209
+ def _create_directive(
210
+ self,
211
+ name: str,
212
+ cls: Type[SphinxDirective],
213
+ directive_name: str,
214
+ pass_through: bool = False,
215
+ **kwargs,
216
+ ) -> SphinxDirective:
217
+ if pass_through:
218
+ options = self.options.copy()
219
+ options.pop("module", None)
220
+ else:
221
+ recursive = self.options.get("recursive", False)
222
+ members_map = lambda x: x if recursive and x is True else None
223
+ options = {
224
+ "members": members_map(self.options.get("members")),
225
+ "undoc-members": members_map(self.options.get("undoc-members")),
226
+ "private-members": members_map(self.options.get("private-members")),
227
+ "special-members": members_map(self.options.get("special-members")),
228
+ "inherited-members": members_map(self.options.get("inherited-members")),
229
+ "member-order": self.options.get("member-order", None),
230
+ "recursive": recursive,
231
+ }
232
+ if "no-index" in self.options:
233
+ # NB: `no-index` should not be present in `options` if it wasn't present
234
+ # in `self.options`.
235
+ options["no-index"] = self.options["no-index"]
236
+
237
+ return cls(
238
+ directive_name,
239
+ [name],
240
+ options,
241
+ self.content if pass_through else docutils.statemachine.StringList(),
242
+ self.lineno if pass_through else 0,
243
+ self.content_offset if pass_through else 0,
244
+ self.block_text if pass_through else "",
245
+ self.state,
246
+ self.state_machine,
247
+ **kwargs,
248
+ )
249
+
250
+ @property
251
+ def objtree(self) -> Object:
252
+ return getattr(self.env, "lua_ls_doc_root")
253
+
254
+ @functools.cached_property
255
+ def parent(self):
256
+ modname = self.env.ref_context.get("lua:module", None)
257
+ classname = self.env.ref_context.get("lua:class", None)
258
+ if classname:
259
+ basepath = ".".join(filter(None, [modname, classname]))
260
+ return self.objtree.find(basepath)
261
+
262
+ _GROUPS = {
263
+ Kind.Module: 0,
264
+ Kind.Data: 1,
265
+ Kind.Function: 2,
266
+ Kind.Class: 3,
267
+ Kind.Alias: 4,
268
+ }
269
+
270
+ def get_children(self, root: Object):
271
+ children = list(root.children.items())
272
+
273
+ order = self.options.get("member-order") or "bysource"
274
+ if order == "alphabetical":
275
+ children.sort(key=lambda ch: ch[0].lower())
276
+ elif order == "groupwise":
277
+ children.sort(key=lambda ch: (self._GROUPS[ch[1].kind], ch[0].lower()))
278
+ elif order == "bysource":
279
+ children.sort(
280
+ key=lambda ch: (
281
+ str(ch[1].file or "@"),
282
+ ch[1].line or math.inf,
283
+ ch[0].lower(),
284
+ )
285
+ )
286
+ else:
287
+ raise RuntimeError(f"unknown member order {order}")
288
+
289
+ inherited_names = set()
290
+
291
+ parent = self.parent
292
+ if (
293
+ parent
294
+ and parent.kind == Kind.Class
295
+ and isinstance(parent, sphinx_lua_ls.doctree.Class)
296
+ ):
297
+ for basename in parent.bases:
298
+ base = self.objtree.find(basename)
299
+ if base:
300
+ inherited_names.update(base.children.keys())
301
+
302
+ include_normal = False
303
+ include_undoc = False
304
+ include_private = False
305
+ include_special = False
306
+ include_inherited = False
307
+
308
+ include = set()
309
+ exclude = self.options.get("exclude-members", set())
310
+
311
+ if exclude is True:
312
+ exclude = set()
313
+
314
+ if members := self.options.get("members"):
315
+ if members is True:
316
+ include_normal = True
317
+ else:
318
+ include.update(members)
319
+ if undoc := self.options.get("undoc-members"):
320
+ if undoc is True:
321
+ include_undoc = True
322
+ else:
323
+ include.update(undoc)
324
+ if private := self.options.get("private-members"):
325
+ if private is True:
326
+ include_private = True
327
+ else:
328
+ include.update(private)
329
+ if special := self.options.get("special-members"):
330
+ if special is True:
331
+ include_special = True
332
+ else:
333
+ include.update(special)
334
+ if inherited := self.options.get("inherited-members"):
335
+ if inherited is True:
336
+ include_inherited = True
337
+ else:
338
+ include.update(inherited)
339
+
340
+ for name, child in children:
341
+ if name in exclude:
342
+ continue
343
+ if name not in include:
344
+ is_undoc = not child.docstring
345
+ if is_undoc and not include_undoc:
346
+ continue
347
+ is_private = child.visibility != Visibility.Public
348
+ if is_private and not include_private:
349
+ continue
350
+ is_special = name.startswith("__")
351
+ if is_special and not include_special:
352
+ continue
353
+ is_inherited = name in inherited_names
354
+ if is_inherited and not include_inherited:
355
+ continue
356
+ if (
357
+ not is_undoc
358
+ and not is_private
359
+ and not is_special
360
+ and not is_inherited
361
+ and not include_normal
362
+ ):
363
+ continue
364
+ yield name, child
365
+
366
+
367
+ class AutodocObjectMixin(sphinx_lua_ls.domain.LuaObject[Any], AutodocUtilsMixin):
368
+ def __init__(self, *args, root: Object):
369
+ super().__init__(*args)
370
+ self.root = root
371
+
372
+ if self.root.visibility == Visibility.Private:
373
+ self.options["private"] = True
374
+ elif self.root.visibility == Visibility.Protected:
375
+ self.options["protected"] = True
376
+ elif self.root.visibility == Visibility.Package:
377
+ self.options["package"] = True
378
+ if self.root.is_async:
379
+ self.options["async"] = True
380
+ if self.root.is_deprecated:
381
+ self.options["deprecated"] = True
382
+
383
+ def run(self) -> list[docutils.nodes.Node]:
384
+ if self.root.file:
385
+ self.state.document.settings.record_dependencies.add(str(self.root.file))
386
+ return super().run()
387
+
388
+ def transform_content(self, content_node: sphinx.addnodes.desc_content) -> None:
389
+ if self.root.docstring:
390
+ self.render_docs(
391
+ str(self.root.file or f"<docstring for {self.arguments[0]}>"),
392
+ self.root.line or 0,
393
+ self.root.docstring,
394
+ content_node,
395
+ )
396
+ if self.allow_nesting:
397
+ for name, child in self.get_children(self.root):
398
+ content_node += self.render(child, name)
399
+
400
+
401
+ class LuaFunction(sphinx_lua_ls.domain.LuaFunction, AutodocObjectMixin):
402
+ @functools.cached_property
403
+ def is_method(self):
404
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Function)
405
+
406
+ return self.parent and self.parent.kind == Kind.Class
407
+
408
+ @functools.cached_property
409
+ def is_staticmethod(self):
410
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Function)
411
+
412
+ return (
413
+ self.parent
414
+ and self.parent.kind == Kind.Class
415
+ and (not self.root.params or self.root.params[0].name != "self")
416
+ )
417
+
418
+ @property
419
+ def objtype(self):
420
+ if self.is_staticmethod:
421
+ return "staticmethod"
422
+ elif self.is_method:
423
+ return "method"
424
+ else:
425
+ return "function"
426
+
427
+ @objtype.setter
428
+ def objtype(self, value): # type: ignore
429
+ assert value == "function"
430
+
431
+ def parse_signature(self, sig):
432
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Function)
433
+ return (
434
+ self.arguments[0],
435
+ (
436
+ [(p.name or "", p.type or "") for p in self.root.params],
437
+ [(p.name or "", p.type or "") for p in self.root.returns],
438
+ ),
439
+ )
440
+
441
+ def transform_content(self, content_node: sphinx.addnodes.desc_content) -> None:
442
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Function)
443
+
444
+ if self.root.docstring:
445
+ self.render_docs(
446
+ str(self.root.file or f"<docstring for {self.arguments[0]}>"),
447
+ self.root.line or 0,
448
+ self.root.docstring,
449
+ content_node,
450
+ )
451
+
452
+ for child in content_node:
453
+ if isinstance(child, docutils.nodes.field_list):
454
+ field_list = child
455
+ break
456
+ else:
457
+ field_list = docutils.nodes.field_list()
458
+ content_node += field_list
459
+
460
+ for i, param in enumerate(self.root.params):
461
+ if param.docstring and not (i == 0 and param.name == "self"):
462
+ # if param.type:
463
+ # objtree: Object = getattr(self.env, "lua_ls_doc_root")
464
+ # obj = objtree.find(param.type)
465
+ # if obj and obj.docstring == param.docstring:
466
+ # continue
467
+
468
+ field_body = docutils.nodes.field_body("")
469
+ self.render_docs(
470
+ str(
471
+ self.root.file
472
+ or f"<docstring for {self.arguments[0]}, param {param.name}>"
473
+ ),
474
+ self.root.line or 0,
475
+ param.docstring,
476
+ field_body,
477
+ )
478
+ field_list += docutils.nodes.field(
479
+ "",
480
+ docutils.nodes.field_name("", "param " + (param.name or "_")),
481
+ field_body,
482
+ )
483
+ if param.type:
484
+ field_list += docutils.nodes.field(
485
+ "",
486
+ docutils.nodes.field_name(
487
+ "", "type " + (param.name or f"_{i + 1}")
488
+ ),
489
+ docutils.nodes.field_body("", docutils.nodes.Text(param.type)),
490
+ )
491
+
492
+ for i, param in enumerate(self.root.returns):
493
+ if param.docstring:
494
+ # if param.type:
495
+ # objtree: Object = getattr(self.env, "lua_ls_doc_root")
496
+ # obj = objtree.find(param.type)
497
+ # if obj and obj.docstring == param.docstring:
498
+ # continue
499
+
500
+ field_body = docutils.nodes.field_body("")
501
+ self.render_docs(
502
+ str(
503
+ self.root.file
504
+ or f"<docstring for {self.arguments[0]}, param {param.name}>"
505
+ ),
506
+ self.root.line or 0,
507
+ param.docstring,
508
+ field_body,
509
+ )
510
+ field_list += docutils.nodes.field(
511
+ "",
512
+ docutils.nodes.field_name(
513
+ "", "return " + (param.name or f"_{i + 1}")
514
+ ),
515
+ field_body,
516
+ )
517
+ if param.type:
518
+ field_list += docutils.nodes.field(
519
+ "",
520
+ docutils.nodes.field_name(
521
+ "", "rtype " + (param.name or f"_{i + 1}")
522
+ ),
523
+ docutils.nodes.field_body("", docutils.nodes.Text(param.type)),
524
+ )
525
+
526
+ if self.allow_nesting:
527
+ for name, child in self.get_children(self.root):
528
+ content_node += self.render(child, name)
529
+
530
+
531
+ class LuaData(sphinx_lua_ls.domain.LuaData, AutodocObjectMixin):
532
+ def parse_signature(self, sig):
533
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Data)
534
+ return self.arguments[0], self.root.type
535
+
536
+
537
+ class LuaAlias(sphinx_lua_ls.domain.LuaAlias, AutodocObjectMixin):
538
+ def parse_signature(self, sig):
539
+ assert isinstance(self.root, sphinx_lua_ls.doctree.Alias)
540
+ return self.arguments[0], self.root.type
541
+
542
+
543
+ class LuaClass(sphinx_lua_ls.domain.LuaClass, AutodocObjectMixin):
544
+ def parse_signature(self, sig):
545
+ if self.root.kind == Kind.Class:
546
+ bases = (
547
+ self.root.bases
548
+ if isinstance(self.root, sphinx_lua_ls.doctree.Class)
549
+ else []
550
+ )
551
+ return self.arguments[0], bases
552
+ else:
553
+ return self.arguments[0], []
554
+
555
+ def get_signature_prefix(self, signature: str):
556
+ if self.root.kind == Kind.Class:
557
+ return super().get_signature_prefix(signature)
558
+ else:
559
+ return sphinx_lua_ls.domain.LuaObject.get_signature_prefix(self, signature)
560
+
561
+
562
+ def _parse_members(value: str):
563
+ if not value:
564
+ return True
565
+ elif "," in value:
566
+ return {s for m in value.split(",") if (s := m.strip())}
567
+ else:
568
+ return set(value.split())
569
+
570
+
571
+ class AutoObjectDirective(AutodocUtilsMixin):
572
+ required_arguments = 1
573
+ option_spec = {
574
+ "no-index": directives.flag,
575
+ "annotation": directives.unchanged,
576
+ "virtual": directives.flag,
577
+ "private": directives.flag,
578
+ "protected": directives.flag,
579
+ "package": directives.flag,
580
+ "abstract": directives.flag,
581
+ "async": directives.flag,
582
+ "global": directives.flag,
583
+ "deprecated": directives.flag,
584
+ "members": _parse_members,
585
+ "undoc-members": _parse_members,
586
+ "private-members": _parse_members,
587
+ "special-members": _parse_members,
588
+ "inherited-members": _parse_members,
589
+ "exclude-members": _parse_members,
590
+ "recursive": lambda x: directives.flag(x) or True,
591
+ "member-order": lambda x: directives.choice(
592
+ x, ("alphabetical", "groupwise", "bysource")
593
+ ),
594
+ }
595
+
596
+ has_content = True
597
+
598
+ def run(self):
599
+ for name, option in self.env.config["lua_ls_default_options"].items():
600
+ if name not in self.options:
601
+ self.options[name] = option
602
+
603
+ name = self.arguments[0].strip()
604
+
605
+ if not name:
606
+ raise self.error(f"got an empty object name")
607
+
608
+ found = self.get_root(name, getattr(self.env, "lua_ls_doc_root"))
609
+ if not found:
610
+ raise self.error(f"unknown lua object {name}")
611
+
612
+ root, modname, classname, objname = found
613
+
614
+ self.push_context(modname, classname)
615
+ try:
616
+ return self.render(root, objname, pass_through=True)
617
+ finally:
618
+ self.pop_context()
619
+
620
+ def get_root(self, name: str, root: Object) -> tuple[Object, str, str, str] | None:
621
+ modname = self.options.get("module", self.env.ref_context.get("lua:module"))
622
+ classname = self.env.ref_context.get("lua:class", None)
623
+
624
+ candidates = [
625
+ ".".join(filter(None, [modname, classname, name])),
626
+ ".".join(filter(None, [modname, name])),
627
+ ".".join(filter(None, [name])),
628
+ ]
629
+
630
+ for candidate in candidates:
631
+ if found := root.find_path(candidate):
632
+ return found