mkdocstrings-matlab 0.9.7__py3-none-any.whl → 1.0.1__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.
Files changed (45) hide show
  1. mkdocstrings_handlers/matlab/__init__.py +26 -3
  2. mkdocstrings_handlers/matlab/config.py +885 -0
  3. mkdocstrings_handlers/matlab/handler.py +155 -296
  4. mkdocstrings_handlers/matlab/logger.py +111 -0
  5. mkdocstrings_handlers/matlab/rendering.py +818 -0
  6. mkdocstrings_handlers/matlab/templates/material/attributes.html.jinja +25 -0
  7. mkdocstrings_handlers/matlab/templates/material/backlinks.html.jinja +17 -0
  8. mkdocstrings_handlers/matlab/templates/material/children.html.jinja +70 -62
  9. mkdocstrings_handlers/matlab/templates/material/class.html.jinja +236 -0
  10. mkdocstrings_handlers/matlab/templates/material/docstring/admonition.html.jinja +20 -0
  11. mkdocstrings_handlers/matlab/templates/material/docstring/classes.html.jinja +85 -0
  12. mkdocstrings_handlers/matlab/templates/material/docstring/examples.html.jinja +27 -0
  13. mkdocstrings_handlers/matlab/templates/material/docstring/functions.html.jinja +91 -0
  14. mkdocstrings_handlers/matlab/templates/material/docstring/input_arguments.html.jinja +171 -0
  15. mkdocstrings_handlers/matlab/templates/material/docstring/name_value_arguments.html.jinja +166 -0
  16. mkdocstrings_handlers/matlab/templates/material/docstring/namespaces.html.jinja +5 -6
  17. mkdocstrings_handlers/matlab/templates/material/docstring/output_arguments.html.jinja +152 -0
  18. mkdocstrings_handlers/matlab/templates/material/docstring/properties.html.jinja +25 -26
  19. mkdocstrings_handlers/matlab/templates/material/docstring.html.jinja +53 -0
  20. mkdocstrings_handlers/matlab/templates/material/expression.html.jinja +55 -0
  21. mkdocstrings_handlers/matlab/templates/material/folder.html.jinja +31 -39
  22. mkdocstrings_handlers/matlab/templates/material/function.html.jinja +148 -0
  23. mkdocstrings_handlers/matlab/templates/material/language.html.jinja +18 -0
  24. mkdocstrings_handlers/matlab/templates/material/languages/en.html.jinja +38 -0
  25. mkdocstrings_handlers/matlab/templates/material/languages/ja.html.jinja +38 -0
  26. mkdocstrings_handlers/matlab/templates/material/languages/zh.html.jinja +38 -0
  27. mkdocstrings_handlers/matlab/templates/material/namespace.html.jinja +32 -38
  28. mkdocstrings_handlers/matlab/templates/material/property.html.jinja +39 -35
  29. mkdocstrings_handlers/matlab/templates/material/script.html.jinja +3 -25
  30. mkdocstrings_handlers/matlab/templates/material/signature.html.jinja +105 -0
  31. mkdocstrings_handlers/matlab/templates/material/style.css +179 -4
  32. mkdocstrings_handlers/matlab/templates/material/summary/classes.html.jinja +25 -0
  33. mkdocstrings_handlers/matlab/templates/material/summary/functions.html.jinja +25 -0
  34. mkdocstrings_handlers/matlab/templates/material/summary/namespaces.html.jinja +17 -13
  35. mkdocstrings_handlers/matlab/templates/material/summary/properties.html.jinja +17 -13
  36. mkdocstrings_handlers/matlab/templates/material/summary.html.jinja +6 -6
  37. {mkdocstrings_matlab-0.9.7.dist-info → mkdocstrings_matlab-1.0.1.dist-info}/METADATA +17 -21
  38. mkdocstrings_matlab-1.0.1.dist-info/RECORD +41 -0
  39. mkdocstrings_handlers/matlab/collect.py +0 -783
  40. mkdocstrings_handlers/matlab/enums.py +0 -54
  41. mkdocstrings_handlers/matlab/models.py +0 -633
  42. mkdocstrings_handlers/matlab/treesitter.py +0 -707
  43. mkdocstrings_matlab-0.9.7.dist-info/RECORD +0 -22
  44. {mkdocstrings_matlab-0.9.7.dist-info → mkdocstrings_matlab-1.0.1.dist-info}/WHEEL +0 -0
  45. {mkdocstrings_matlab-0.9.7.dist-info → mkdocstrings_matlab-1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,783 +0,0 @@
1
- """Functions and classes for collecting MATLAB objects from paths."""
2
-
3
- from collections import defaultdict, deque
4
- from copy import copy, deepcopy
5
- from pathlib import Path
6
- from typing import Any, Callable, Mapping, Sequence, TypeVar
7
-
8
- from _griffe.collections import LinesCollection as GLC
9
- from _griffe.collections import ModulesCollection
10
- from _griffe.docstrings.models import (
11
- DocstringParameter,
12
- DocstringReturn,
13
- DocstringSectionOtherParameters,
14
- DocstringSectionParameters,
15
- DocstringSectionReturns,
16
- )
17
- from _griffe.enumerations import DocstringSectionKind
18
- from _griffe.expressions import Expr
19
-
20
- from mkdocstrings_handlers.matlab.enums import ParameterKind
21
- from mkdocstrings_handlers.matlab.models import (
22
- Class,
23
- Classfolder,
24
- Docstring,
25
- DocstringSectionText,
26
- Folder,
27
- Function,
28
- MatlabMixin,
29
- Namespace,
30
- PathMixin,
31
- _ParentGrabber,
32
- )
33
- from mkdocstrings_handlers.matlab.treesitter import FileParser
34
-
35
- PathType = TypeVar("PathType", bound=PathMixin)
36
-
37
- __all__ = ["LinesCollection", "PathCollection"]
38
-
39
-
40
- class LinesCollection(GLC):
41
- """A simple dictionary containing the modules source code lines."""
42
-
43
- def __init__(self) -> None:
44
- """Initialize the collection."""
45
- self._data: dict[str, list[str]] = {}
46
-
47
-
48
- class PathGlobber:
49
- """
50
- A class to recursively glob paths as MATLAB would do it.
51
- """
52
-
53
- def __init__(self, path: Path, recursive: bool = False):
54
- self._idx = 0
55
- self._paths: list[Path] = []
56
- self._glob(path, recursive)
57
-
58
- def _glob(self, path: Path, recursive: bool = False):
59
- for member in path.iterdir():
60
- if (
61
- member.is_dir()
62
- and recursive
63
- and member.stem[0] not in ["+", "@"]
64
- and member.stem != "private"
65
- ):
66
- self._glob(member, recursive=True)
67
- elif member.is_dir() and member.stem[0] == "+":
68
- self._paths.append(member)
69
- self._glob(member)
70
- elif member.is_dir() and member.stem[0] == "@":
71
- self._paths.append(member)
72
- elif (
73
- member.is_file()
74
- and member.suffix == ".m"
75
- and member.name != "Contents.m"
76
- ):
77
- self._paths.append(member)
78
-
79
- def max_stem_length(self) -> int:
80
- return max(len(path.stem) for path in self._paths)
81
-
82
- def __len__(self):
83
- return len(self._paths)
84
-
85
- def __iter__(self):
86
- return self
87
-
88
- def __next__(self):
89
- try:
90
- item = self._paths[self._idx]
91
- except IndexError as err:
92
- raise StopIteration from err
93
- self._idx += 1
94
- return item
95
-
96
-
97
- class PathCollection(ModulesCollection):
98
- """
99
- PathCollection is a class that manages a collection of MATLAB paths and their corresponding models.
100
-
101
- Attributes:
102
- config (Mapping): Configuration settings for the PathCollection.
103
- lines_collection (LinesCollection): An instance of LinesCollection for managing lines.
104
-
105
- Args:
106
- matlab_path (Sequence[str | Path]): A list of strings or Path objects representing the MATLAB paths.
107
- recursive (bool, optional): If True, recursively adds all subdirectories of the given paths to the search path. Defaults to False.
108
- config (Mapping, optional): Configuration settings for the PathCollection. Defaults to {}.
109
- config_path (Path | None, optional): The path to the configuration file. Defaults to None.
110
-
111
- Methods:
112
- members() -> dict:
113
- Returns a dictionary of members with their corresponding models.
114
-
115
- resolve(identifier: str, config: Mapping = {}) -> MatlabMixin | None:
116
- Resolves the given identifier to a model object.
117
-
118
- update_model(model: MatlabMixin, config: Mapping) -> MatlabMixin:
119
- Updates the given model object with the provided configuration.
120
-
121
- addpath(path: str | Path, to_end: bool = False, recursive: bool = False) -> list[Path]:
122
- Adds a path to the search path.
123
-
124
- rm_path(path: str | Path, recursive: bool = False) -> list[Path]:
125
- Removes a path from the search path and updates the namespace and database accordingly.
126
-
127
- get_inheritance_diagram(model: Class) -> DocstringSectionText | None:
128
- Generates an inheritance diagram for the given class model.
129
- """
130
-
131
- def __init__(
132
- self,
133
- matlab_path: Sequence[str | Path],
134
- recursive: bool = False,
135
- config: Mapping = {},
136
- config_path: Path | None = None,
137
- ) -> None:
138
- """
139
- Initialize an instance of PathCollection.
140
-
141
- Args:
142
- matlab_path (list[str | Path]): A list of strings or Path objects representing the MATLAB paths.
143
-
144
- Raises:
145
- TypeError: If any element in matlab_path is not a string or Path object.
146
- """
147
- for path in matlab_path:
148
- if not isinstance(path, (str, Path)):
149
- raise TypeError(f"Expected str or Path, got {type(path)}")
150
-
151
- self._path: deque[Path] = deque()
152
- self._mapping: dict[str, deque[Path]] = defaultdict(deque)
153
- self._models: dict[Path, LazyModel] = {}
154
- self._members: dict[Path, list[tuple[str, Path]]] = defaultdict(list)
155
- self._folders: dict[Path, LazyModel] = {}
156
- self._config_path = config_path
157
-
158
- self.config = config
159
- self.lines_collection = LinesCollection()
160
-
161
- for path in matlab_path:
162
- self.addpath(Path(path), to_end=True, recursive=recursive)
163
-
164
- @property
165
- def members(self):
166
- return {
167
- identifier: self._models[paths[0]].model()
168
- for identifier, paths in self._mapping.items()
169
- }
170
-
171
- def resolve(
172
- self,
173
- identifier: str,
174
- config: Mapping = {},
175
- ):
176
- """
177
- Resolve an identifier to a MatlabMixin model.
178
-
179
- This method attempts to resolve a given identifier to a corresponding
180
- MatlabMixin model using the internal mapping and models. If the identifier
181
- is not found directly, it will attempt to resolve it by breaking down the
182
- identifier into parts and resolving each part recursively.
183
-
184
- Args:
185
- identifier (str): The identifier to resolve.
186
- config (Mapping, optional): Configuration options to update the model. Defaults to an empty dictionary.
187
-
188
- Returns:
189
- MatlabMixin or None: The resolved MatlabMixin model if found, otherwise None.
190
- """
191
-
192
- # Find in global database
193
- if identifier in self._mapping:
194
- model = self._models[self._mapping[identifier][0]].model()
195
- if model is not None:
196
- model = self.update_model(model, config)
197
-
198
- elif self._config_path is not None and "/" in identifier:
199
- absolute_path = (self._config_path / Path(identifier)).resolve()
200
- if absolute_path.exists():
201
- if absolute_path.suffix:
202
- path, member = absolute_path.parent, absolute_path.stem
203
- else:
204
- path, member = absolute_path, None
205
- lazymodel = self._folders.get(path, None)
206
-
207
- if lazymodel is not None:
208
- model = lazymodel.model()
209
- if model is not None and member is not None:
210
- model = model.members.get(member, None)
211
- else:
212
- model = None
213
- else:
214
- model = None
215
-
216
- else:
217
- model = None
218
- name_parts = identifier.split(".")
219
- if len(name_parts) > 1:
220
- base = self.resolve(".".join(name_parts[:-1]), config=config)
221
- if base is None or name_parts[-1] not in base.members:
222
- model = None
223
- else:
224
- model = base.members[name_parts[-1]]
225
- else:
226
- model = None
227
-
228
- if isinstance(model, MatlabMixin):
229
- return model
230
- return None
231
-
232
- def update_model(self, model: MatlabMixin, config: Mapping) -> MatlabMixin:
233
- """
234
- Update the given model based on the provided configuration.
235
-
236
- This method updates the docstring parser and parser options for the model,
237
- patches return annotations for MATLAB functions, and optionally creates
238
- docstring sections from argument blocks. It also recursively updates
239
- members of the model and handles special cases for class constructors
240
- and inheritance diagrams.
241
-
242
- Args:
243
- model (MatlabMixin): The model to update.
244
- config (Mapping): The configuration dictionary.
245
-
246
- Returns:
247
- MatlabMixin: The updated model.
248
- """
249
-
250
- # Update docstring parser and parser options
251
- if hasattr(model, "docstring") and model.docstring is not None:
252
- model.docstring.parser = config.get("docstring_style", "google")
253
- model.docstring.parser_options = config.get("docstring_options", {})
254
-
255
- # Patch docstring section titles
256
- if model.docstring is not None:
257
- for section in model.docstring.parsed:
258
- match section.kind:
259
- case DocstringSectionKind.attributes:
260
- section.title = "Properties:"
261
- case DocstringSectionKind.modules:
262
- section.title = "Namespaces:"
263
- case DocstringSectionKind.parameters:
264
- section.title = "Input arguments:"
265
- case DocstringSectionKind.returns:
266
- section.title = "Output arguments:"
267
- case DocstringSectionKind.other_parameters:
268
- section.title = "Name-Value Arguments:"
269
-
270
- # Patch returns annotation
271
- # In _griffe.docstrings.<parser>.py the function _read_returns_section will enforce an annotation
272
- # on the return parameter. This annotation is grabbed from the parent. For MATLAB this is invalid.
273
- # Thus the return annotation needs to be patched back to a None.
274
- if (
275
- isinstance(model, Function)
276
- and model.docstring is not None
277
- and any(
278
- isinstance(doc, DocstringSectionReturns)
279
- for doc in model.docstring.parsed
280
- )
281
- ):
282
- section = next(
283
- doc
284
- for doc in model.docstring.parsed
285
- if isinstance(doc, DocstringSectionReturns)
286
- )
287
- for returns in section.value:
288
- if not isinstance(returns.annotation, Expr):
289
- returns.annotation = None
290
-
291
- # previous updates do not edit the model attributes persistently
292
- # However, the following updates do edit the model attributes persistently
293
- # such as adding new sections to the docstring or editing its members.abs
294
- # Thus, we need to copy the model to avoid editing the original model
295
- alias = copy(model)
296
- alias.docstring = (
297
- deepcopy(model.docstring) if model.docstring is not None else None
298
- )
299
- alias.members = {key: value for key, value in model.members.items()}
300
- if isinstance(alias, Class):
301
- alias._inherited_members = None
302
-
303
- for name, member in getattr(alias, "members", {}).items():
304
- alias.members[name] = self.update_model(member, config)
305
-
306
- # Merge constructor docstring into class
307
- if (
308
- isinstance(alias, Class)
309
- and config.get("merge_constructor_into_class", False)
310
- and alias.name in alias.members
311
- and alias.members[alias.name].docstring is not None
312
- ):
313
- constructor = alias.members.pop(alias.name)
314
- if constructor.docstring is not None:
315
- if alias.docstring is None:
316
- alias.docstring = Docstring("", parent=alias)
317
-
318
- if config.get("merge_constructor_ignore_summary", False):
319
- alias.docstring._suffixes.extend(constructor.docstring.parsed[1:])
320
- else:
321
- alias.docstring._suffixes.extend(constructor.docstring.parsed)
322
-
323
- # Hide hidden members (methods and properties)
324
- hidden_members = config.get("hidden_members", False)
325
- if isinstance(hidden_members, bool):
326
- filter_hidden = not hidden_members
327
- show_hidden = []
328
- else:
329
- filter_hidden = True
330
- show_hidden: list[str] = hidden_members
331
- if isinstance(alias, Class) and filter_hidden:
332
- alias.members = {
333
- key: value
334
- for key, value in alias.members.items()
335
- if not getattr(value, "Hidden", False)
336
- or (
337
- show_hidden
338
- and getattr(value, "Hidden", False)
339
- and key in show_hidden
340
- )
341
- }
342
- alias._inherited_members = {
343
- key: value
344
- for key, value in alias.inherited_members.items()
345
- if not getattr(value, "Hidden", False)
346
- or (
347
- show_hidden
348
- and getattr(value, "Hidden", False)
349
- and key in show_hidden
350
- )
351
- }
352
-
353
- # Hide private members (methods and properties)
354
- private_members = config.get("private_members", False)
355
- if isinstance(private_members, bool):
356
- filter_private = not private_members
357
- show_private = []
358
- else:
359
- filter_private = True
360
- show_private: list[str] = private_members
361
- if isinstance(alias, Class) and filter_private:
362
- alias.members = {
363
- key: value
364
- for key, value in alias.members.items()
365
- if not getattr(value, "Private", False)
366
- or (
367
- show_private
368
- and getattr(value, "Private", False)
369
- and key in show_private
370
- )
371
- }
372
- alias._inherited_members = {
373
- key: value
374
- for key, value in alias.inherited_members.items()
375
- if not getattr(value, "Private", False)
376
- or (
377
- show_private
378
- and getattr(value, "Private", False)
379
- and key in show_private
380
- )
381
- }
382
-
383
- # Create parameters and returns sections from argument blocks
384
- if (
385
- isinstance(alias, Function)
386
- and alias.docstring is not None
387
- and config.get("parse_arguments", True)
388
- and (
389
- config.get("show_docstring_input_arguments", True)
390
- or config.get("show_docstring_name_value_arguments", True)
391
- or config.get("show_docstring_output_arguments", True)
392
- )
393
- ):
394
- docstring_parameters = any(
395
- isinstance(doc, DocstringSectionParameters)
396
- for doc in alias.docstring.parsed
397
- )
398
- docstring_returns = any(
399
- isinstance(doc, DocstringSectionReturns)
400
- for doc in alias.docstring.parsed
401
- )
402
-
403
- if not docstring_parameters and alias.parameters:
404
- arguments_parameters = any(
405
- param.docstring is not None for param in alias.parameters
406
- )
407
- else:
408
- arguments_parameters = False
409
-
410
- if not docstring_returns and alias.returns:
411
- arguments_returns = any(
412
- ret.docstring is not None for ret in alias.returns
413
- )
414
- else:
415
- arguments_returns = False
416
-
417
- document_parameters = not docstring_parameters and arguments_parameters
418
- document_returns = not docstring_returns and arguments_returns
419
-
420
- standard_parameters = [
421
- param
422
- for param in alias.parameters
423
- if param.kind is not ParameterKind.keyword_only
424
- ]
425
-
426
- keyword_parameters = [
427
- param
428
- for param in alias.parameters
429
- if param.kind is ParameterKind.keyword_only
430
- ]
431
-
432
- if (
433
- config.get("show_docstring_input_arguments", True)
434
- and document_parameters
435
- and standard_parameters
436
- ):
437
- alias.docstring._suffixes.append(
438
- DocstringSectionParameters(
439
- [
440
- DocstringParameter(
441
- name=param.name,
442
- value=str(param.default)
443
- if param.default is not None
444
- else None,
445
- annotation=param.annotation,
446
- description=param.docstring.value
447
- if param.docstring is not None
448
- else "",
449
- )
450
- for param in standard_parameters
451
- ]
452
- )
453
- )
454
-
455
- if (
456
- config.get("show_docstring_name_value_arguments", True)
457
- and document_parameters
458
- and keyword_parameters
459
- ):
460
- alias.docstring._suffixes.append(
461
- DocstringSectionOtherParameters(
462
- [
463
- DocstringParameter(
464
- name=param.name,
465
- value=str(param.default)
466
- if param.default is not None
467
- else None,
468
- annotation=param.annotation,
469
- description=param.docstring.value
470
- if param.docstring is not None
471
- else "",
472
- )
473
- for param in keyword_parameters
474
- ],
475
- title="Name-Value Arguments:",
476
- )
477
- )
478
-
479
- if config.get("show_docstring_output_arguments", True) and document_returns:
480
- returns = DocstringSectionReturns(
481
- [
482
- DocstringReturn(
483
- name=param.name,
484
- value=str(param.default)
485
- if param.default is not None
486
- else None,
487
- annotation=param.annotation,
488
- description=param.docstring.value
489
- if param.docstring is not None
490
- else "",
491
- )
492
- for param in alias.returns or []
493
- ]
494
- )
495
- alias.docstring._suffixes.append(returns)
496
-
497
- # Add inheritance diagram to class docstring
498
- if (
499
- isinstance(alias, Class)
500
- and config.get("show_inheritance_diagram", False)
501
- and (
502
- (
503
- alias.docstring is not None
504
- and "Inheritance Diagram" not in alias.docstring.parsed
505
- )
506
- or alias.docstring is None
507
- )
508
- ):
509
- diagram = self.get_inheritance_diagram(alias)
510
- if diagram is not None:
511
- if alias.docstring is None:
512
- alias.docstring = Docstring("", parent=alias)
513
- alias.docstring._prefixes.append(diagram)
514
-
515
- return alias
516
-
517
- def addpath(self, path: str | Path, to_end: bool = False, recursive: bool = False):
518
- """
519
- Add a path to the search path.
520
-
521
- Args:
522
- path (str | Path): The path to be added.
523
- to_end (bool, optional): Whether to add the path to the end of the search path. Defaults to False.
524
-
525
- Returns:
526
- list[Path]: The previous search path before adding the new path.
527
- """
528
- if isinstance(path, str):
529
- path = Path(path)
530
-
531
- if path in self._path:
532
- self._path.remove(path)
533
-
534
- if to_end:
535
- self._path.append(path)
536
- else:
537
- self._path.appendleft(path)
538
-
539
- for member in PathGlobber(path, recursive=recursive):
540
- model = LazyModel(member, self)
541
- self._models[member] = model
542
- self._mapping[model.name].append(member)
543
- self._members[path].append((model.name, member))
544
-
545
- if self._config_path is not None and member.parent.stem[0] not in [
546
- "+",
547
- "@",
548
- ]:
549
- if member.parent.is_relative_to(self._config_path):
550
- if member.parent not in self._folders:
551
- self._folders[member.parent] = LazyModel(member.parent, self)
552
- else:
553
- pass # TODO: Issue warning?
554
-
555
- def rm_path(self, path: str | Path, recursive: bool = False):
556
- """
557
- Removes a path from the search path and updates the namespace and database accordingly.
558
-
559
- Args:
560
- path (str | Path): The path to be removed from the search path.
561
- recursive (bool, optional): If True, recursively removes all subdirectories of the given path from the search path. Defaults to False.
562
-
563
- Returns:
564
- list[Path]: The previous search path before the removal.
565
-
566
- """
567
- if isinstance(path, str):
568
- path = Path(path)
569
-
570
- if path not in self._path:
571
- return list(self._path)
572
-
573
- self._path.remove(path)
574
-
575
- for name, member in self._members.pop(path):
576
- self._mapping[name].remove(member)
577
- self._models.pop(member)
578
-
579
- if recursive:
580
- for subdir in [item for item in self._path if _is_subdirectory(path, item)]:
581
- self.rm_path(subdir, recursive=False)
582
-
583
- def get_inheritance_diagram(self, model: Class) -> DocstringSectionText | None:
584
- def get_id(str: str) -> str:
585
- return str.replace(".", "_")
586
-
587
- def get_nodes(model: Class, nodes: set[str] = set()) -> set[str]:
588
- nodes.add(f" {get_id(model.name)}[{model.name}]")
589
- for base in [str(base) for base in model.bases]:
590
- super = self.resolve(base)
591
- if super is None:
592
- nodes.add(f" {get_id(base)}[{base}]")
593
- else:
594
- if isinstance(super, Class):
595
- get_nodes(super, nodes)
596
- return nodes
597
-
598
- def get_links(model: Class, links: set[str] = set()) -> set[str]:
599
- for base in [str(base) for base in model.bases]:
600
- super = self.resolve(base)
601
- if super is None:
602
- links.add(f" {get_id(base)} --> {get_id(model.name)}")
603
- else:
604
- links.add(f" {get_id(super.name)} --> {get_id(model.name)}")
605
- if isinstance(super, Class):
606
- get_links(super, links)
607
- return links
608
-
609
- nodes = get_nodes(model)
610
- if len(nodes) == 1:
611
- return None
612
-
613
- nodes_str = "\n".join(list(nodes))
614
- links_str = "\n".join(list(get_links(model)))
615
- section = f"```mermaid\nflowchart TB\n{nodes_str}\n{links_str}\n```"
616
-
617
- return DocstringSectionText(section, title="Inheritance Diagram")
618
-
619
-
620
- def _is_subdirectory(parent_path: Path, child_path: Path) -> bool:
621
- try:
622
- child_path.relative_to(parent_path)
623
- except ValueError:
624
- return False
625
- else:
626
- return True
627
-
628
-
629
- class LazyModel:
630
- """
631
- A class to lazily collect and model MATLAB objects from a given path.
632
-
633
- Methods:
634
- is_class_folder: Checks if the path is a class folder.
635
- is_namespace: Checks if the path is a namespace.
636
- is_in_namespace: Checks if the path is within a namespace.
637
- name: Returns the name of the MATLAB object, including namespace if applicable.
638
- model: Collects and returns the MATLAB object model..
639
- """
640
-
641
- def __init__(self, path: Path, path_collection: PathCollection):
642
- self._path: Path = path
643
- self._model: MatlabMixin | None = None
644
- self._path_collection: PathCollection = path_collection
645
- self._lines_collection: LinesCollection = path_collection.lines_collection
646
-
647
- @property
648
- def is_folder(self) -> bool:
649
- return self._path.is_dir() and self._path.name[0] not in ["+", "@"]
650
-
651
- @property
652
- def is_class_folder(self) -> bool:
653
- return self._path.is_dir() and self._path.name[0] == "@"
654
-
655
- @property
656
- def is_namespace(self) -> bool:
657
- return self._path.name[0] == "+"
658
-
659
- @property
660
- def is_in_namespace(self) -> bool:
661
- return self._path.parent.name[0] == "+"
662
-
663
- @property
664
- def name(self):
665
- if self.is_in_namespace:
666
- parts = list(self._path.parts)
667
- item = len(parts) - 2
668
- nameparts = []
669
- while item >= 0:
670
- if parts[item][0] != "+":
671
- break
672
- nameparts.append(parts[item][1:])
673
- item -= 1
674
- nameparts.reverse()
675
- namespace = ".".join(nameparts) + "."
676
- else:
677
- namespace = ""
678
-
679
- if self.is_class_folder or self.is_namespace:
680
- name = namespace + self._path.name[1:]
681
- else:
682
- name = namespace + self._path.stem
683
-
684
- if self.is_namespace:
685
- return "+" + name
686
- else:
687
- return name
688
-
689
- def model(self) -> MatlabMixin | None:
690
- if not self._path.exists():
691
- return None
692
-
693
- if self._model is None:
694
- if self.is_class_folder:
695
- self._model = self._collect_classfolder(self._path)
696
- elif self.is_namespace:
697
- self._model = self._collect_namespace(self._path)
698
- elif self.is_folder:
699
- self._model = self._collect_folder(self._path)
700
- else:
701
- self._model = self._collect_path(self._path)
702
- if self._model is not None:
703
- self._model.parent = self._collect_parent(self._path.parent)
704
- return self._model
705
-
706
- def _collect_parent(self, path: Path) -> _ParentGrabber | None:
707
- if self.is_in_namespace:
708
- grabber: Callable[[], MatlabMixin | None] = self._path_collection._models[
709
- path
710
- ].model
711
- parent = _ParentGrabber(grabber)
712
- else:
713
- parent = None
714
- return parent
715
-
716
- def _collect_path(self, path: Path, **kwargs: Any) -> MatlabMixin:
717
- file = FileParser(path)
718
- model = file.parse(path_collection=self._path_collection, **kwargs)
719
- self._lines_collection[path] = file.content.split("\n")
720
- return model
721
-
722
- def _collect_directory(self, path: Path, model: PathType) -> PathType:
723
- for member in path.iterdir():
724
- if member.is_dir() and member.name[0] in ["+", "@"]:
725
- submodel = self._path_collection._models[member].model()
726
- if submodel is not None:
727
- model.members[submodel.name] = submodel
728
-
729
- elif member.is_file() and member.suffix == ".m":
730
- if member.name == "Contents.m":
731
- contentsfile = self._collect_path(member)
732
- model.docstring = contentsfile.docstring
733
- else:
734
- submodel = self._path_collection._models[member].model()
735
- if submodel is not None:
736
- model.members[submodel.name] = submodel
737
-
738
- if model.docstring is None:
739
- model.docstring = self._collect_readme_md(path, model)
740
-
741
- return model
742
-
743
- def _collect_classfolder(self, path: Path) -> Classfolder | None:
744
- classfile = path / (path.name[1:] + ".m")
745
- if not classfile.exists():
746
- return None
747
- model = self._collect_path(classfile)
748
- if not isinstance(model, Classfolder):
749
- return None
750
- for member in path.iterdir():
751
- if member.is_file() and member.suffix == ".m" and member != classfile:
752
- if member.name == "Contents.m" and model.docstring is None:
753
- contentsfile = self._collect_path(member)
754
- model.docstring = contentsfile.docstring
755
- else:
756
- method = self._collect_path(member)
757
- method.parent = model
758
- model.members[method.name] = method
759
- if model.docstring is None:
760
- model.docstring = self._collect_readme_md(path, model)
761
- return model
762
-
763
- def _collect_namespace(self, path: Path) -> Namespace:
764
- name = self.name[1:].split(".")[-1]
765
- model = Namespace(name, filepath=path, path_collection=self._path_collection)
766
- return self._collect_directory(path, model)
767
-
768
- def _collect_folder(self, path: Path) -> Folder:
769
- name = path.stem
770
- model = Folder("/" + name, filepath=path, path_collection=self._path_collection)
771
- return self._collect_directory(path, model)
772
-
773
- def _collect_readme_md(self, path, parent: PathMixin) -> Docstring | None:
774
- if (path / "README.md").exists():
775
- readme = path / "README.md"
776
- elif (path / "readme.md").exists():
777
- readme = path / "readme.md"
778
- else:
779
- return None
780
-
781
- with open(readme, "r") as file:
782
- content = file.read()
783
- return Docstring(content, parent=parent)