Coverage for src/pytkdocs/objects.py: 98.15%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2This module defines the documented objects classes.
4- the generic [`Object`][pytkdocs.objects.Object] class
5- the [`Module`][pytkdocs.objects.Module] class
6- the [`Class`][pytkdocs.objects.Class] class
7- the [`Method`][pytkdocs.objects.Method] class
8- the [`Function`][pytkdocs.objects.Function] class
9- the [`Attribute`][pytkdocs.objects.Attribute] class
11Note that properties are considered attributes, because they are used like such.
13It also defines a convenient [`Source`][pytkdocs.objects.Source] class to represent source code.
14"""
16import importlib
17import inspect
18import os
19import sys
20from abc import ABCMeta
21from functools import lru_cache
22from pathlib import Path
23from typing import List, Optional, Union
25from pytkdocs.parsers.docstrings.base import Parser, Section
26from pytkdocs.properties import NAME_CLASS_PRIVATE, NAME_PRIVATE, NAME_SPECIAL, ApplicableNameProperty
29class Source:
30 """
31 Helper class to represent source code.
33 It is simply used to wrap the result of
34 [`inspect.getsourceslines`](https://docs.python.org/3/library/inspect.html#inspect.getsourcelines).
35 """
37 def __init__(self, lines: Union[str, List[str]], line_start: int) -> None:
38 """
39 Initialize the object.
41 Arguments:
42 lines: A list of strings. The strings should have trailing newlines.
43 line_start: The line number of where the code starts in the file.
44 """
45 if isinstance(lines, list):
46 code = "".join(lines)
47 else:
48 code = lines
49 self.code = code
50 """The code, as a single string."""
51 self.line_start = line_start
52 """The first line number."""
55class Object(metaclass=ABCMeta):
56 """
57 A base class to store information about a Python object.
59 Each instance additionally stores references to its children, grouped by category.
60 """
62 possible_name_properties: List[ApplicableNameProperty] = []
63 """
64 The properties that we can apply to the object based on its name.
66 The applicable properties vary from one subclass of `Object` to another.
67 """
69 def __init__(
70 self,
71 name: str,
72 path: str,
73 file_path: str,
74 docstring: Optional[str] = "",
75 properties: Optional[List[str]] = None,
76 source: Optional[Source] = None,
77 ) -> None:
78 """
79 Initialize the object.
81 Arguments:
82 name: The object's name.
83 path: The object's dotted-path.
84 file_path: The file path of the object's direct parent module.
85 docstring: The object's docstring.
86 properties: The object's properties.
87 source: The object's source code.
88 """
89 self.name = name
90 """The object's name."""
91 self.path = path
92 """The object's dotted-path."""
93 self.file_path = file_path
94 """The file path of the object's direct parent module."""
95 self.docstring = docstring
96 """The object's docstring."""
97 self.docstring_sections: List[Section] = []
98 """The object's docstring parsed into sections."""
99 self.docstring_errors: List[str] = []
100 """The errors detected while parsing the docstring."""
101 self.properties = properties or []
102 """The object's properties."""
103 self.parent: Optional[Object] = None
104 """The object's parent (another instance of a subclass of `Object`)."""
105 self.source = source
106 """The object's source code."""
108 self._path_map = {self.path: self}
109 self._parsed = False
111 self.attributes: List[Attribute] = []
112 """The list of all the object's attributes."""
113 self.methods: List[Method] = []
114 """The list of all the object's methods."""
115 self.functions: List[Function] = []
116 """The list of all the object's functions."""
117 self.modules: List[Module] = []
118 """The list of all the object's submodules."""
119 self.classes: List[Class] = []
120 """The list of all the object's classes."""
121 self.children: List[Object] = []
122 """The list of all the object's children."""
124 def __str__(self) -> str:
125 return self.path
127 @property
128 def category(self) -> str:
129 """
130 Return the object's category.
132 Returns:
133 The object's category (module, class, function, method or attribute).
134 """
135 return self.__class__.__name__.lower()
137 @property
138 def root(self) -> "Object":
139 """
140 Return the object's root.
142 Returns:
143 The object's root (top-most parent).
144 """
145 obj = self
146 while obj.parent:
147 obj = obj.parent
148 return obj
150 @property
151 def relative_file_path(self) -> str:
152 """
153 Return the relative file path of the object.
155 It is the relative path to the object's module,
156 starting at the path of the top-most package it is contained in.
158 For example:
160 - package is `a`
161 - package absolute path is `/abs/path/to/a`
162 - module is `a.b.c`
163 - object is `c` or anything defined in `c`
164 - relative file path is `a/b/c.py`
166 If the relative file path cannot be determined, the value returned is `""` (empty string).
168 Returns:
169 The path relative to the object's package.
170 """
171 parts = self.path.split(".")
172 namespaces = [".".join(parts[:length]) for length in range(1, len(parts) + 1)] # noqa: WPS221 (not complex)
173 # Iterate through all sub namespaces including the last in case it is a module
174 for namespace in namespaces: 174 ↛ 195line 174 didn't jump to line 195, because the loop on line 174 didn't complete
175 try: # noqa: WPS229 (more compact)
176 importlib.import_module(namespace)
177 top_package = sys.modules[namespace]
178 except (ModuleNotFoundError, ImportError, KeyError):
179 # ImportError: Triggered if the namespace is not importable
180 # ModuleNotFoundError: Triggered if the namespace is not a module
181 # KeyError: Triggered if the imported package isn't referenced under the same fully qualified name
182 # Namespace packages are importable, so this should work for them
183 return ""
185 try: # noqa: WPS229 (more compact)
186 top_package_path = Path(inspect.getabsfile(top_package)).parent
187 return str(Path(self.file_path).relative_to(top_package_path.parent))
188 except TypeError:
189 # Triggered if getabsfile() can't be found in the case of a Namespace package
190 pass # noqa: WPS420 (passing is the only way)
191 except ValueError:
192 # Triggered if Path().relative_to can't find an appropriate path
193 return ""
195 return ""
197 @property
198 def name_to_check(self) -> str:
199 """
200 Return the attribute to check against name-properties regular expressions (private, class-private, special).
202 Returns:
203 The attribute to check (its name).
204 """
205 return self.name
207 @property
208 def name_properties(self) -> List[str]:
209 """
210 Return the object's name properties.
212 Returns:
213 The object's name properties (private, class-private, special).
214 """
215 properties = []
216 for prop, predicate in self.possible_name_properties:
217 if predicate(self.name_to_check):
218 properties.append(prop)
219 return properties
221 @property
222 def parent_path(self) -> str:
223 """
224 Return the parent's path, computed from the current path.
226 The parent object path is not used: this property is used to see if an object is really related to another one,
227 to add it as a child to the other. When we do that, the child doesn't even have a parent.
229 Returns:
230 The dotted path of the parent object.
231 """
232 return self.path.rsplit(".", 1)[0]
234 def add_child(self, obj: "Object") -> None: # noqa: WPS231 (not complex)
235 """
236 Add an object as a child of this object.
238 If the child computed `parent_path` is not equal to this object's path, abort.
240 Append the child to the `children` list, and to the right category list.
242 Arguments:
243 obj: An instance of documented object.
244 """
245 if obj.parent_path != self.path:
246 return
248 self.children.append(obj)
249 if isinstance(obj, Module):
250 self.modules.append(obj)
251 elif isinstance(obj, Class):
252 self.classes.append(obj)
253 elif isinstance(obj, Function):
254 self.functions.append(obj)
255 elif isinstance(obj, Method):
256 self.methods.append(obj)
257 elif isinstance(obj, Attribute): 257 ↛ 267line 257 didn't jump to line 267, because the condition on line 257 was never false
258 # Dataclass attributes with default values will already be present in `self.attributes` as they are
259 # resolved differently by the python interpreter. As they have a concrete value, they are already present
260 # in the "original" class. They should be overridden with the new "dataclass" attribute coming in here
261 # (having the "dataclass_field" property set)
262 new_attribute_name = obj.name
263 for attribute in self.attributes:
264 if attribute.name == new_attribute_name:
265 self.attributes.remove(attribute)
266 self.attributes.append(obj)
267 obj.parent = self
269 self._path_map[obj.path] = obj
271 def add_children(self, children: List["Object"]) -> None:
272 """
273 Add a list of objects as children of this object.
275 Arguments:
276 children: The list of children to add.
277 """
278 for child in children:
279 self.add_child(child)
281 def parse_docstring(self, parser: Parser, **context) -> None:
282 """
283 Parse the docstring of this object.
285 Arguments:
286 parser: A parser to parse the docstrings.
287 **context: Additional context to use when parsing.
288 """
289 if self.docstring and not self._parsed:
290 sections, errors = parser.parse(self.docstring, {"obj": self, **context})
291 self.docstring_sections = sections
292 self.docstring_errors = errors
293 self._parsed = True
295 def parse_all_docstrings(self, parser: Parser) -> None:
296 """
297 Recursively parse the docstring of this object and its children.
299 Arguments:
300 parser: A parser to parse the docstrings.
301 """
302 self.parse_docstring(parser)
303 for child in self.children:
304 child.parse_all_docstrings(parser)
306 @lru_cache()
307 def has_contents(self) -> bool:
308 """
309 Tells if the object has "contents".
311 An object has contents when:
313 - it is the root of the object tree
314 - it has a docstring
315 - at least one of its children (whatever the depth) has contents
317 The value is cached, so this method should be called last, when the tree doesn't change anymore.
319 Returns:
320 Whether this object has contents or not.
321 """
322 has_docstring = bool(self.docstring)
323 is_root = not self.parent
324 children_have_contents = any(child.has_contents() for child in self.children)
325 return has_docstring or is_root or children_have_contents
328class Module(Object):
329 """A class to store information about a module."""
331 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE]
333 @property
334 def file_name(self) -> str:
335 """
336 Return the base name of the module file, without the extension.
338 Returns:
339 The module file's base name.
340 """
341 return os.path.splitext(os.path.basename(self.file_path))[0]
343 @property
344 def name_to_check(self) -> str: # noqa: D102
345 return self.file_name
348class Class(Object):
349 """A class to store information about a class."""
351 possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE]
353 def __init__(self, *args, bases: List[str] = None, **kwargs):
354 """
355 Initialize the object.
357 Arguments:
358 *args: Arguments passed to the parent class Initialize the object.
359 bases: The base classes (dotted paths).
360 **kwargs: Arguments passed to the parent class Initialize the object.
361 """
362 super().__init__(*args, **kwargs)
363 self.bases = bases or ["object"]
366class Function(Object):
367 """
368 A class to store information about a function.
370 It accepts an additional `signature` argument at instantiation.
371 """
373 possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE]
375 def __init__(self, *args, signature=None, **kwargs):
376 """
377 Initialize the object.
379 Arguments:
380 *args: Arguments passed to the parent class Initialize the object.
381 signature: The function signature.
382 **kwargs: Arguments passed to the parent class Initialize the object.
383 """
384 super().__init__(*args, **kwargs)
385 self.signature = signature
388class Method(Object):
389 """
390 A class to store information about a method.
392 It accepts an additional `signature` argument at instantiation.
393 """
395 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE]
397 def __init__(self, *args, signature=None, **kwargs):
398 """
399 Initialize the object.
401 Arguments:
402 *args: Arguments passed to the parent class Initialize the object.
403 signature: The function signature.
404 **kwargs: Arguments passed to the parent class Initialize the object.
405 """
406 super().__init__(*args, **kwargs)
407 self.signature = signature
410class Attribute(Object):
411 """
412 A class to store information about an attribute.
414 It accepts an additional `attr_type` argument at instantiation.
415 """
417 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_CLASS_PRIVATE, NAME_PRIVATE]
419 def __init__(self, *args, attr_type=None, **kwargs):
420 """
421 Initialize the object.
423 Arguments:
424 *args: Arguments passed to the parent class Initialize the object.
425 attr_type: The attribute type.
426 **kwargs: Arguments passed to the parent class Initialize the object.
427 """
428 super().__init__(*args, **kwargs)
429 self.type = attr_type