pytkdocs
pytkdocs package.
Load Python objects documentation.
__main__
special
¤
Entry-point module, in case you use python -m pytkdocs
.
Why does this file exist, and why __main__
? For more info, read:
cli
¤
Module that contains the command line application.
discarded_stdout()
¤
Discard standard output.
Yields:
Type | Description |
---|---|
Nothing |
We only yield to act as a context manager. |
Source code in pytkdocs/cli.py
@contextmanager
def discarded_stdout():
"""
Discard standard output.
Yields:
Nothing: We only yield to act as a context manager.
"""
# Discard things printed at import time to avoid corrupting our JSON output
# See https://github.com/pawamoy/pytkdocs/issues/24
old_stdout = sys.stdout
sys.stdout = StringIO()
yield
# Flush imported modules' output, and restore true sys.stdout
sys.stdout.flush()
sys.stdout = old_stdout
extract_docstring_parsing_errors(errors, obj)
¤
Recursion helper.
Update the errors
dictionary by side-effect. Recurse on the object's children.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
errors |
dict |
The dictionary to update. |
required |
obj |
Object |
The object. |
required |
Source code in pytkdocs/cli.py
def extract_docstring_parsing_errors(errors: dict, obj: Object) -> None:
"""
Recursion helper.
Update the `errors` dictionary by side-effect. Recurse on the object's children.
Arguments:
errors: The dictionary to update.
obj: The object.
"""
if hasattr(obj, "docstring_errors") and obj.docstring_errors: # noqa: WPS421 (hasattr)
errors[obj.path] = obj.docstring_errors
for child in obj.children:
extract_docstring_parsing_errors(errors, child)
extract_errors(obj)
¤
Extract the docstring parsing errors of each object, recursively, into a flat dictionary.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
obj |
Object |
An object from |
required |
Returns:
Type | Description |
---|---|
dict |
A flat dictionary. Keys are the objects' names. |
Source code in pytkdocs/cli.py
def extract_errors(obj: Object) -> dict:
"""
Extract the docstring parsing errors of each object, recursively, into a flat dictionary.
Arguments:
obj: An object from `pytkdocs.objects`.
Returns:
A flat dictionary. Keys are the objects' names.
"""
parsing_errors: Dict[str, List[str]] = {}
extract_docstring_parsing_errors(parsing_errors, obj)
return parsing_errors
get_parser()
¤
Return the program argument parser.
Returns:
Type | Description |
---|---|
ArgumentParser |
The argument parser for the program. |
Source code in pytkdocs/cli.py
def get_parser() -> argparse.ArgumentParser:
"""
Return the program argument parser.
Returns:
The argument parser for the program.
"""
parser = argparse.ArgumentParser(prog="pytkdocs")
parser.add_argument(
"-1",
"--line-by-line",
action="store_true",
dest="line_by_line",
help="Process each line read on stdin, one by one.",
)
return parser
main(args=None)
¤
Run the main program.
This function is executed when you type pytkdocs
or python -m pytkdocs
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
args |
Optional[List[str]] |
Arguments passed from the command line. |
None |
Returns:
Type | Description |
---|---|
int |
An exit code. |
Source code in pytkdocs/cli.py
def main(args: Optional[List[str]] = None) -> int:
"""
Run the main program.
This function is executed when you type `pytkdocs` or `python -m pytkdocs`.
Arguments:
args: Arguments passed from the command line.
Returns:
An exit code.
"""
parser = get_parser()
parsed_args: argparse.Namespace = parser.parse_args(args)
if parsed_args.line_by_line:
for line in sys.stdin:
with discarded_stdout():
try:
output = json.dumps(process_json(line))
except Exception as error: # noqa: W0703 (we purposely catch everything)
# Don't fail on error. We must handle the next inputs.
# Instead, print error as JSON.
output = json.dumps({"error": str(error), "traceback": traceback.format_exc()})
print(output) # noqa: WPS421 (we need to print at some point)
else:
with discarded_stdout():
output = json.dumps(process_json(sys.stdin.read()))
print(output) # noqa: WPS421 (we need to print at some point)
return 0
process_config(config)
¤
Process a loading configuration.
The config
argument is a dictionary looking like this:
{
"objects": [
{"path": "python.dotted.path.to.the.object1"},
{"path": "python.dotted.path.to.the.object2"}
]
}
The result is a dictionary looking like this:
{
"loading_errors": [
"message1",
"message2",
],
"parsing_errors": {
"path.to.object1": [
"message1",
"message2",
],
"path.to.object2": [
"message1",
"message2",
]
},
"objects": [
{
"path": "path.to.object1",
# other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
},
{
"path": "path.to.object2",
# other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
},
]
}
Parameters:
Name | Type | Description | Default |
---|---|---|---|
config |
dict |
The configuration. |
required |
Returns:
Type | Description |
---|---|
dict |
The collected documentation along with the errors that occurred. |
Source code in pytkdocs/cli.py
def process_config(config: dict) -> dict:
"""
Process a loading configuration.
The `config` argument is a dictionary looking like this:
```python
{
"objects": [
{"path": "python.dotted.path.to.the.object1"},
{"path": "python.dotted.path.to.the.object2"}
]
}
```
The result is a dictionary looking like this:
```python
{
"loading_errors": [
"message1",
"message2",
],
"parsing_errors": {
"path.to.object1": [
"message1",
"message2",
],
"path.to.object2": [
"message1",
"message2",
]
},
"objects": [
{
"path": "path.to.object1",
# other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
},
{
"path": "path.to.object2",
# other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
},
]
}
```
Arguments:
config: The configuration.
Returns:
The collected documentation along with the errors that occurred.
"""
collected = []
loading_errors = []
parsing_errors = {}
for obj_config in config["objects"]:
path = obj_config.pop("path")
members = obj_config.pop("members", set())
if isinstance(members, list):
members = set(members)
loader = Loader(**obj_config)
obj = loader.get_object_documentation(path, members)
loading_errors.extend(loader.errors)
parsing_errors.update(extract_errors(obj))
serialized_obj = serialize_object(obj)
collected.append(serialized_obj)
return {"loading_errors": loading_errors, "parsing_errors": parsing_errors, "objects": collected}
process_json(json_input)
¤
Process JSON input.
Simply load the JSON as a Python dictionary, then pass it to process_config
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
json_input |
str |
The JSON to load. |
required |
Returns:
Type | Description |
---|---|
dict |
The result of the call to |
Source code in pytkdocs/cli.py
def process_json(json_input: str) -> dict:
"""
Process JSON input.
Simply load the JSON as a Python dictionary, then pass it to [`process_config`][pytkdocs.cli.process_config].
Arguments:
json_input: The JSON to load.
Returns:
The result of the call to [`process_config`][pytkdocs.cli.process_config].
"""
return process_config(json.loads(json_input))
loader
¤
This module is responsible for loading the documentation from Python objects.
It uses inspect
for introspecting objects,
iterating over their members, etc.
Loader
¤
This class contains the object documentation loading mechanisms.
Any error that occurred during collection of the objects and their documentation is stored in the errors
list.
Source code in pytkdocs/loader.py
class Loader:
"""
This class contains the object documentation loading mechanisms.
Any error that occurred during collection of the objects and their documentation is stored in the `errors` list.
"""
def __init__(
self,
filters: Optional[List[str]] = None,
docstring_style: str = "google",
docstring_options: Optional[dict] = None,
inherited_members: bool = False,
new_path_syntax: bool = False,
) -> None:
"""
Initialize the object.
Arguments:
filters: A list of regular expressions to fine-grain select members. It is applied recursively.
docstring_style: The style to use when parsing docstrings.
docstring_options: The options to pass to the docstrings parser.
inherited_members: Whether to select inherited members for classes.
new_path_syntax: Whether to use the "colon" syntax for the path.
"""
if not filters:
filters = []
self.filters = [(filtr, re.compile(filtr.lstrip("!"))) for filtr in filters]
self.docstring_parser = PARSERS[docstring_style](**(docstring_options or {})) # type: ignore
self.errors: List[str] = []
self.select_inherited_members = inherited_members
self.new_path_syntax = new_path_syntax
def get_object_documentation(self, dotted_path: str, members: Optional[Union[Set[str], bool]] = None) -> Object:
"""
Get the documentation for an object and its children.
Arguments:
dotted_path: The Python dotted path to the desired object.
members: `True` to select members and filter them, `False` to select no members,
or a list of names to explicitly select the members with these names.
It is applied only on the root object.
Returns:
The documented object.
"""
if members is True:
members = set()
root_object: Object
leaf = get_object_tree(dotted_path, self.new_path_syntax)
if leaf.is_module():
root_object = self.get_module_documentation(leaf, members)
elif leaf.is_class():
root_object = self.get_class_documentation(leaf, members)
elif leaf.is_staticmethod():
root_object = self.get_staticmethod_documentation(leaf)
elif leaf.is_classmethod():
root_object = self.get_classmethod_documentation(leaf)
elif leaf.is_method_descriptor():
root_object = self.get_regular_method_documentation(leaf)
elif leaf.is_method():
root_object = self.get_regular_method_documentation(leaf)
elif leaf.is_function():
root_object = self.get_function_documentation(leaf)
elif leaf.is_property():
root_object = self.get_property_documentation(leaf)
else:
root_object = self.get_attribute_documentation(leaf)
root_object.parse_all_docstrings(self.docstring_parser)
return root_object
def get_module_documentation(self, node: ObjectNode, select_members=None) -> Module:
"""
Get the documentation for a module and its children.
Arguments:
node: The node representing the module and its parents.
select_members: Explicit members to select.
Returns:
The documented module object.
"""
module = node.obj
path = node.dotted_path
name = path.split(".")[-1]
source: Optional[Source]
try:
source = Source(inspect.getsource(module), 1)
except OSError as error:
try:
code = Path(node.file_path).read_text()
except (OSError, UnicodeDecodeError):
source = None
else:
source = Source(code, 1) if code else None
root_object = Module(
name=name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(module),
source=source,
)
if select_members is False:
return root_object
select_members = select_members or set()
attributes_data = get_module_attributes(module)
root_object.parse_docstring(self.docstring_parser, attributes=attributes_data)
for member_name, member in inspect.getmembers(module):
if self.select(member_name, select_members):
child_node = ObjectNode(member, member_name, parent=node)
if child_node.is_class() and node.root.obj is inspect.getmodule(child_node.obj):
root_object.add_child(self.get_class_documentation(child_node))
elif child_node.is_function() and node.root.obj is inspect.getmodule(child_node.obj):
root_object.add_child(self.get_function_documentation(child_node))
elif member_name in attributes_data:
root_object.add_child(self.get_attribute_documentation(child_node, attributes_data[member_name]))
if hasattr(module, "__path__"): # noqa: WPS421 (hasattr)
for _, modname, _ in pkgutil.iter_modules(module.__path__):
if self.select(modname, select_members):
leaf = get_object_tree(f"{path}.{modname}")
root_object.add_child(self.get_module_documentation(leaf))
return root_object
@staticmethod
def _class_path(cls):
mod = cls.__module__
qname = cls.__qualname__
if mod == "builtins":
return qname
else:
return f"{mod}.{qname}"
def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class:
"""
Get the documentation for a class and its children.
Arguments:
node: The node representing the class and its parents.
select_members: Explicit members to select.
Returns:
The documented class object.
"""
class_ = node.obj
docstring = inspect.cleandoc(class_.__doc__ or "")
bases = [self._class_path(b) for b in class_.__bases__]
source: Optional[Source]
try:
source = Source(*inspect.getsourcelines(node.obj))
except (OSError, TypeError) as error:
source = None
root_object = Class(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=docstring,
bases=bases,
source=source,
)
# Even if we don't select members, we want to correctly parse the docstring
attributes_data: Dict[str, Dict[str, Any]] = {}
for parent_class in reversed(class_.__mro__[:-1]):
merge(attributes_data, get_class_attributes(parent_class))
context: Dict[str, Any] = {"attributes": attributes_data}
if "__init__" in class_.__dict__:
try:
attributes_data.update(get_instance_attributes(class_.__init__))
context["signature"] = inspect.signature(class_.__init__)
except (TypeError, ValueError):
pass
root_object.parse_docstring(self.docstring_parser, **context)
if select_members is False:
return root_object
select_members = select_members or set()
# Build the list of members
members = {}
inherited = set()
direct_members = class_.__dict__
all_members = dict(inspect.getmembers(class_))
for member_name, member in all_members.items():
if member is class_:
continue
if not (member is type or member is object) and self.select(member_name, select_members):
if member_name not in direct_members:
if self.select_inherited_members:
members[member_name] = member
inherited.add(member_name)
else:
members[member_name] = member
# Iterate on the selected members
child: Object
for member_name, member in members.items():
child_node = ObjectNode(member, member_name, parent=node)
if child_node.is_class():
child = self.get_class_documentation(child_node)
elif child_node.is_classmethod():
child = self.get_classmethod_documentation(child_node)
elif child_node.is_staticmethod():
child = self.get_staticmethod_documentation(child_node)
elif child_node.is_method():
child = self.get_regular_method_documentation(child_node)
elif child_node.is_property():
child = self.get_property_documentation(child_node)
elif member_name in attributes_data:
child = self.get_attribute_documentation(child_node, attributes_data[member_name])
else:
continue
if member_name in inherited:
child.properties.append("inherited")
root_object.add_child(child)
for attr_name, properties, add_method in (
("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation),
("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation),
("_meta.get_fields", ["django-model"], self.get_django_field_documentation),
("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field),
):
if self.detect_field_model(attr_name, direct_members, all_members):
root_object.properties.extend(properties)
self.add_fields(
node,
root_object,
attr_name,
all_members,
select_members,
class_,
add_method,
)
break
return root_object
def detect_field_model(self, attr_name: str, direct_members, all_members) -> bool:
"""
Detect if an attribute is present in members.
Arguments:
attr_name: The name of the attribute to detect, can contain dots.
direct_members: The direct members of the class.
all_members: All members of the class.
Returns:
Whether the attribute is present.
"""
first_order_attr_name, remainder = split_attr_name(attr_name)
if not (
first_order_attr_name in direct_members
or (self.select_inherited_members and first_order_attr_name in all_members)
):
return False
if remainder and not attrgetter(remainder)(all_members[first_order_attr_name]):
return False
return True
def add_fields(
self,
node: ObjectNode,
root_object: Object,
attr_name: str,
members,
select_members,
base_class,
add_method,
) -> None:
"""
Add detected fields to the current object.
Arguments:
node: The current object node.
root_object: The current object.
attr_name: The fields attribute name.
members: The members to pick the fields attribute in.
select_members: The members to select.
base_class: The class declaring the fields.
add_method: The method to add the children object.
"""
fields = get_fields(attr_name, members=members)
for field_name, field in fields.items():
select_field = self.select(field_name, select_members)
is_inherited = field_is_inherited(field_name, attr_name, base_class)
if select_field and (self.select_inherited_members or not is_inherited):
child_node = ObjectNode(obj=field, name=field_name, parent=node)
root_object.add_child(add_method(child_node))
def get_function_documentation(self, node: ObjectNode) -> Function:
"""
Get the documentation for a function.
Arguments:
node: The node representing the function and its parents.
Returns:
The documented function object.
"""
function = node.obj
path = node.dotted_path
source: Optional[Source]
signature: Optional[inspect.Signature]
try:
signature = inspect.signature(function)
except TypeError as error:
signature = None
try:
source = Source(*inspect.getsourcelines(function))
except OSError as error:
source = None
properties: List[str] = []
if node.is_coroutine_function():
properties.append("async")
return Function(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=inspect.getdoc(function),
signature=signature,
source=source,
properties=properties,
)
def get_property_documentation(self, node: ObjectNode) -> Attribute:
"""
Get the documentation for a property.
Arguments:
node: The node representing the property and its parents.
Returns:
The documented attribute object (properties are considered attributes for now).
"""
prop = node.obj
path = node.dotted_path
properties = ["property"]
if node.is_cached_property():
# cached_property is always writable, see the docs
properties.extend(["writable", "cached"])
sig_source_func = prop.func
else:
properties.append("readonly" if prop.fset is None else "writable")
sig_source_func = prop.fget
source: Optional[Source]
try:
signature = inspect.signature(sig_source_func)
except (TypeError, ValueError) as error:
attr_type = None
else:
attr_type = signature.return_annotation
try:
source = Source(*inspect.getsourcelines(sig_source_func))
except (OSError, TypeError) as error:
source = None
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(prop),
attr_type=attr_type,
properties=properties,
source=source,
)
@staticmethod
def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Pydantic Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["pydantic-field"]
if prop.required:
properties.append("required")
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=prop.field_info.description,
attr_type=prop.outer_type_,
properties=properties,
)
@staticmethod
def get_django_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Django Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["django-field"]
if prop.null:
properties.append("nullable")
if prop.blank:
properties.append("blank")
# set correct docstring based on verbose_name and help_text
# both should be converted to str type in case lazy translation
# is being used, which is common scenario in django
if prop.help_text:
docstring = f"{prop.verbose_name}: {prop.help_text}"
else:
docstring = str(prop.verbose_name)
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=docstring,
attr_type=prop.__class__,
properties=properties,
)
@staticmethod
def get_marshmallow_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Marshmallow Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["marshmallow-field"]
if prop.required:
properties.append("required")
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=prop.metadata.get("description"),
attr_type=type(prop),
properties=properties,
)
@staticmethod
def get_annotated_dataclass_field(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
"""
Get the documentation for a dataclass field.
Arguments:
node: The node representing the annotation and its parents.
attribute_data: Docstring and annotation for this attribute.
Returns:
The documented attribute object.
"""
if attribute_data is None:
if node.parent_is_class():
attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
else:
attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
return Attribute(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=attribute_data["docstring"],
attr_type=attribute_data["annotation"],
properties=["dataclass-field"],
)
def get_classmethod_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a class-method.
Arguments:
node: The node representing the class-method and its parents.
Returns:
The documented method object.
"""
return self.get_method_documentation(node, ["classmethod"])
def get_staticmethod_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a static-method.
Arguments:
node: The node representing the static-method and its parents.
Returns:
The documented method object.
"""
return self.get_method_documentation(node, ["staticmethod"])
def get_regular_method_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a regular method (not class- nor static-method).
We do extra processing in this method to discard docstrings of `__init__` methods
that were inherited from parent classes.
Arguments:
node: The node representing the method and its parents.
Returns:
The documented method object.
"""
method = self.get_method_documentation(node)
if node.parent:
class_ = node.parent.obj
if RE_SPECIAL.match(node.name):
docstring = method.docstring
parent_classes = class_.__mro__[1:]
for parent_class in parent_classes:
try:
parent_method = getattr(parent_class, node.name)
except AttributeError:
continue
else:
if docstring == inspect.getdoc(parent_method):
method.docstring = ""
break
return method
def get_method_documentation(self, node: ObjectNode, properties: Optional[List[str]] = None) -> Method:
"""
Get the documentation for a method or method descriptor.
Arguments:
node: The node representing the method and its parents.
properties: A list of properties to apply to the method.
Returns:
The documented method object.
"""
method = node.obj
path = node.dotted_path
signature: Optional[inspect.Signature]
source: Optional[Source]
try:
source = Source(*inspect.getsourcelines(method))
except OSError as error:
source = None
except TypeError:
source = None
if node.is_coroutine_function():
if properties is None:
properties = ["async"]
else:
properties.append("async")
try:
# for "built-in" functions, e.g. those implemented in C,
# inspect.signature() uses the __text_signature__ attribute, which
# provides a limited but still useful amount of signature information.
# "built-in" functions with no __text_signature__ will
# raise a ValueError().
signature = inspect.signature(method)
except ValueError as error:
signature = None
return Method(
name=node.name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(method),
signature=signature,
properties=properties or [],
source=source,
)
@staticmethod
def get_attribute_documentation(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
"""
Get the documentation for an attribute.
Arguments:
node: The node representing the method and its parents.
attribute_data: Docstring and annotation for this attribute.
Returns:
The documented attribute object.
"""
if attribute_data is None:
if node.parent_is_class():
attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
else:
attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
return Attribute(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=attribute_data.get("docstring", ""),
attr_type=attribute_data.get("annotation", None),
)
def select(self, name: str, names: Set[str]) -> bool:
"""
Tells whether we should select an object or not, given its name.
If the set of names is not empty, we check against it, otherwise we check against filters.
Arguments:
name: The name of the object to select or not.
names: An explicit list of names to select.
Returns:
Yes or no.
"""
if names:
return name in names
return not self.filter_name_out(name)
@lru_cache(maxsize=None)
def filter_name_out(self, name: str) -> bool:
"""
Filter a name based on the loader's filters.
Arguments:
name: The name to filter.
Returns:
True if the name was filtered out, False otherwise.
"""
if not self.filters:
return False
keep = True
for fltr, regex in self.filters:
is_matching = bool(regex.search(name))
if is_matching:
if str(fltr).startswith("!"):
is_matching = not is_matching
keep = is_matching
return not keep
__init__(self, filters=None, docstring_style='google', docstring_options=None, inherited_members=False, new_path_syntax=False)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filters |
Optional[List[str]] |
A list of regular expressions to fine-grain select members. It is applied recursively. |
None |
docstring_style |
str |
The style to use when parsing docstrings. |
'google' |
docstring_options |
Optional[dict] |
The options to pass to the docstrings parser. |
None |
inherited_members |
bool |
Whether to select inherited members for classes. |
False |
new_path_syntax |
bool |
Whether to use the "colon" syntax for the path. |
False |
Source code in pytkdocs/loader.py
def __init__(
self,
filters: Optional[List[str]] = None,
docstring_style: str = "google",
docstring_options: Optional[dict] = None,
inherited_members: bool = False,
new_path_syntax: bool = False,
) -> None:
"""
Initialize the object.
Arguments:
filters: A list of regular expressions to fine-grain select members. It is applied recursively.
docstring_style: The style to use when parsing docstrings.
docstring_options: The options to pass to the docstrings parser.
inherited_members: Whether to select inherited members for classes.
new_path_syntax: Whether to use the "colon" syntax for the path.
"""
if not filters:
filters = []
self.filters = [(filtr, re.compile(filtr.lstrip("!"))) for filtr in filters]
self.docstring_parser = PARSERS[docstring_style](**(docstring_options or {})) # type: ignore
self.errors: List[str] = []
self.select_inherited_members = inherited_members
self.new_path_syntax = new_path_syntax
add_fields(self, node, root_object, attr_name, members, select_members, base_class, add_method)
¤
Add detected fields to the current object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The current object node. |
required |
root_object |
Object |
The current object. |
required |
attr_name |
str |
The fields attribute name. |
required |
members |
The members to pick the fields attribute in. |
required | |
select_members |
The members to select. |
required | |
base_class |
The class declaring the fields. |
required | |
add_method |
The method to add the children object. |
required |
Source code in pytkdocs/loader.py
def add_fields(
self,
node: ObjectNode,
root_object: Object,
attr_name: str,
members,
select_members,
base_class,
add_method,
) -> None:
"""
Add detected fields to the current object.
Arguments:
node: The current object node.
root_object: The current object.
attr_name: The fields attribute name.
members: The members to pick the fields attribute in.
select_members: The members to select.
base_class: The class declaring the fields.
add_method: The method to add the children object.
"""
fields = get_fields(attr_name, members=members)
for field_name, field in fields.items():
select_field = self.select(field_name, select_members)
is_inherited = field_is_inherited(field_name, attr_name, base_class)
if select_field and (self.select_inherited_members or not is_inherited):
child_node = ObjectNode(obj=field, name=field_name, parent=node)
root_object.add_child(add_method(child_node))
detect_field_model(self, attr_name, direct_members, all_members)
¤
Detect if an attribute is present in members.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
attr_name |
str |
The name of the attribute to detect, can contain dots. |
required |
direct_members |
The direct members of the class. |
required | |
all_members |
All members of the class. |
required |
Returns:
Type | Description |
---|---|
bool |
Whether the attribute is present. |
Source code in pytkdocs/loader.py
def detect_field_model(self, attr_name: str, direct_members, all_members) -> bool:
"""
Detect if an attribute is present in members.
Arguments:
attr_name: The name of the attribute to detect, can contain dots.
direct_members: The direct members of the class.
all_members: All members of the class.
Returns:
Whether the attribute is present.
"""
first_order_attr_name, remainder = split_attr_name(attr_name)
if not (
first_order_attr_name in direct_members
or (self.select_inherited_members and first_order_attr_name in all_members)
):
return False
if remainder and not attrgetter(remainder)(all_members[first_order_attr_name]):
return False
return True
filter_name_out(self, name)
¤
Filter a name based on the loader's filters.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
The name to filter. |
required |
Returns:
Type | Description |
---|---|
bool |
True if the name was filtered out, False otherwise. |
Source code in pytkdocs/loader.py
@lru_cache(maxsize=None)
def filter_name_out(self, name: str) -> bool:
"""
Filter a name based on the loader's filters.
Arguments:
name: The name to filter.
Returns:
True if the name was filtered out, False otherwise.
"""
if not self.filters:
return False
keep = True
for fltr, regex in self.filters:
is_matching = bool(regex.search(name))
if is_matching:
if str(fltr).startswith("!"):
is_matching = not is_matching
keep = is_matching
return not keep
get_annotated_dataclass_field(node, attribute_data=None)
staticmethod
¤
Get the documentation for a dataclass field.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the annotation and its parents. |
required |
attribute_data |
Optional[dict] |
Docstring and annotation for this attribute. |
None |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object. |
Source code in pytkdocs/loader.py
@staticmethod
def get_annotated_dataclass_field(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
"""
Get the documentation for a dataclass field.
Arguments:
node: The node representing the annotation and its parents.
attribute_data: Docstring and annotation for this attribute.
Returns:
The documented attribute object.
"""
if attribute_data is None:
if node.parent_is_class():
attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
else:
attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
return Attribute(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=attribute_data["docstring"],
attr_type=attribute_data["annotation"],
properties=["dataclass-field"],
)
get_attribute_documentation(node, attribute_data=None)
staticmethod
¤
Get the documentation for an attribute.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the method and its parents. |
required |
attribute_data |
Optional[dict] |
Docstring and annotation for this attribute. |
None |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object. |
Source code in pytkdocs/loader.py
@staticmethod
def get_attribute_documentation(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute:
"""
Get the documentation for an attribute.
Arguments:
node: The node representing the method and its parents.
attribute_data: Docstring and annotation for this attribute.
Returns:
The documented attribute object.
"""
if attribute_data is None:
if node.parent_is_class():
attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore
else:
attribute_data = get_module_attributes(node.root.obj).get(node.name, {})
return Attribute(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=attribute_data.get("docstring", ""),
attr_type=attribute_data.get("annotation", None),
)
get_class_documentation(self, node, select_members=None)
¤
Get the documentation for a class and its children.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the class and its parents. |
required |
select_members |
Explicit members to select. |
None |
Returns:
Type | Description |
---|---|
Class |
The documented class object. |
Source code in pytkdocs/loader.py
def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class:
"""
Get the documentation for a class and its children.
Arguments:
node: The node representing the class and its parents.
select_members: Explicit members to select.
Returns:
The documented class object.
"""
class_ = node.obj
docstring = inspect.cleandoc(class_.__doc__ or "")
bases = [self._class_path(b) for b in class_.__bases__]
source: Optional[Source]
try:
source = Source(*inspect.getsourcelines(node.obj))
except (OSError, TypeError) as error:
source = None
root_object = Class(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=docstring,
bases=bases,
source=source,
)
# Even if we don't select members, we want to correctly parse the docstring
attributes_data: Dict[str, Dict[str, Any]] = {}
for parent_class in reversed(class_.__mro__[:-1]):
merge(attributes_data, get_class_attributes(parent_class))
context: Dict[str, Any] = {"attributes": attributes_data}
if "__init__" in class_.__dict__:
try:
attributes_data.update(get_instance_attributes(class_.__init__))
context["signature"] = inspect.signature(class_.__init__)
except (TypeError, ValueError):
pass
root_object.parse_docstring(self.docstring_parser, **context)
if select_members is False:
return root_object
select_members = select_members or set()
# Build the list of members
members = {}
inherited = set()
direct_members = class_.__dict__
all_members = dict(inspect.getmembers(class_))
for member_name, member in all_members.items():
if member is class_:
continue
if not (member is type or member is object) and self.select(member_name, select_members):
if member_name not in direct_members:
if self.select_inherited_members:
members[member_name] = member
inherited.add(member_name)
else:
members[member_name] = member
# Iterate on the selected members
child: Object
for member_name, member in members.items():
child_node = ObjectNode(member, member_name, parent=node)
if child_node.is_class():
child = self.get_class_documentation(child_node)
elif child_node.is_classmethod():
child = self.get_classmethod_documentation(child_node)
elif child_node.is_staticmethod():
child = self.get_staticmethod_documentation(child_node)
elif child_node.is_method():
child = self.get_regular_method_documentation(child_node)
elif child_node.is_property():
child = self.get_property_documentation(child_node)
elif member_name in attributes_data:
child = self.get_attribute_documentation(child_node, attributes_data[member_name])
else:
continue
if member_name in inherited:
child.properties.append("inherited")
root_object.add_child(child)
for attr_name, properties, add_method in (
("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation),
("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation),
("_meta.get_fields", ["django-model"], self.get_django_field_documentation),
("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field),
):
if self.detect_field_model(attr_name, direct_members, all_members):
root_object.properties.extend(properties)
self.add_fields(
node,
root_object,
attr_name,
all_members,
select_members,
class_,
add_method,
)
break
return root_object
get_classmethod_documentation(self, node)
¤
Get the documentation for a class-method.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the class-method and its parents. |
required |
Returns:
Type | Description |
---|---|
Method |
The documented method object. |
Source code in pytkdocs/loader.py
def get_classmethod_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a class-method.
Arguments:
node: The node representing the class-method and its parents.
Returns:
The documented method object.
"""
return self.get_method_documentation(node, ["classmethod"])
get_django_field_documentation(node)
staticmethod
¤
Get the documentation for a Django Field.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the Field and its parents. |
required |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object. |
Source code in pytkdocs/loader.py
@staticmethod
def get_django_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Django Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["django-field"]
if prop.null:
properties.append("nullable")
if prop.blank:
properties.append("blank")
# set correct docstring based on verbose_name and help_text
# both should be converted to str type in case lazy translation
# is being used, which is common scenario in django
if prop.help_text:
docstring = f"{prop.verbose_name}: {prop.help_text}"
else:
docstring = str(prop.verbose_name)
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=docstring,
attr_type=prop.__class__,
properties=properties,
)
get_function_documentation(self, node)
¤
Get the documentation for a function.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the function and its parents. |
required |
Returns:
Type | Description |
---|---|
Function |
The documented function object. |
Source code in pytkdocs/loader.py
def get_function_documentation(self, node: ObjectNode) -> Function:
"""
Get the documentation for a function.
Arguments:
node: The node representing the function and its parents.
Returns:
The documented function object.
"""
function = node.obj
path = node.dotted_path
source: Optional[Source]
signature: Optional[inspect.Signature]
try:
signature = inspect.signature(function)
except TypeError as error:
signature = None
try:
source = Source(*inspect.getsourcelines(function))
except OSError as error:
source = None
properties: List[str] = []
if node.is_coroutine_function():
properties.append("async")
return Function(
name=node.name,
path=node.dotted_path,
file_path=node.file_path,
docstring=inspect.getdoc(function),
signature=signature,
source=source,
properties=properties,
)
get_marshmallow_field_documentation(node)
staticmethod
¤
Get the documentation for a Marshmallow Field.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the Field and its parents. |
required |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object. |
Source code in pytkdocs/loader.py
@staticmethod
def get_marshmallow_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Marshmallow Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["marshmallow-field"]
if prop.required:
properties.append("required")
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=prop.metadata.get("description"),
attr_type=type(prop),
properties=properties,
)
get_method_documentation(self, node, properties=None)
¤
Get the documentation for a method or method descriptor.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the method and its parents. |
required |
properties |
Optional[List[str]] |
A list of properties to apply to the method. |
None |
Returns:
Type | Description |
---|---|
Method |
The documented method object. |
Source code in pytkdocs/loader.py
def get_method_documentation(self, node: ObjectNode, properties: Optional[List[str]] = None) -> Method:
"""
Get the documentation for a method or method descriptor.
Arguments:
node: The node representing the method and its parents.
properties: A list of properties to apply to the method.
Returns:
The documented method object.
"""
method = node.obj
path = node.dotted_path
signature: Optional[inspect.Signature]
source: Optional[Source]
try:
source = Source(*inspect.getsourcelines(method))
except OSError as error:
source = None
except TypeError:
source = None
if node.is_coroutine_function():
if properties is None:
properties = ["async"]
else:
properties.append("async")
try:
# for "built-in" functions, e.g. those implemented in C,
# inspect.signature() uses the __text_signature__ attribute, which
# provides a limited but still useful amount of signature information.
# "built-in" functions with no __text_signature__ will
# raise a ValueError().
signature = inspect.signature(method)
except ValueError as error:
signature = None
return Method(
name=node.name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(method),
signature=signature,
properties=properties or [],
source=source,
)
get_module_documentation(self, node, select_members=None)
¤
Get the documentation for a module and its children.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the module and its parents. |
required |
select_members |
Explicit members to select. |
None |
Returns:
Type | Description |
---|---|
Module |
The documented module object. |
Source code in pytkdocs/loader.py
def get_module_documentation(self, node: ObjectNode, select_members=None) -> Module:
"""
Get the documentation for a module and its children.
Arguments:
node: The node representing the module and its parents.
select_members: Explicit members to select.
Returns:
The documented module object.
"""
module = node.obj
path = node.dotted_path
name = path.split(".")[-1]
source: Optional[Source]
try:
source = Source(inspect.getsource(module), 1)
except OSError as error:
try:
code = Path(node.file_path).read_text()
except (OSError, UnicodeDecodeError):
source = None
else:
source = Source(code, 1) if code else None
root_object = Module(
name=name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(module),
source=source,
)
if select_members is False:
return root_object
select_members = select_members or set()
attributes_data = get_module_attributes(module)
root_object.parse_docstring(self.docstring_parser, attributes=attributes_data)
for member_name, member in inspect.getmembers(module):
if self.select(member_name, select_members):
child_node = ObjectNode(member, member_name, parent=node)
if child_node.is_class() and node.root.obj is inspect.getmodule(child_node.obj):
root_object.add_child(self.get_class_documentation(child_node))
elif child_node.is_function() and node.root.obj is inspect.getmodule(child_node.obj):
root_object.add_child(self.get_function_documentation(child_node))
elif member_name in attributes_data:
root_object.add_child(self.get_attribute_documentation(child_node, attributes_data[member_name]))
if hasattr(module, "__path__"): # noqa: WPS421 (hasattr)
for _, modname, _ in pkgutil.iter_modules(module.__path__):
if self.select(modname, select_members):
leaf = get_object_tree(f"{path}.{modname}")
root_object.add_child(self.get_module_documentation(leaf))
return root_object
get_object_documentation(self, dotted_path, members=None)
¤
Get the documentation for an object and its children.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
dotted_path |
str |
The Python dotted path to the desired object. |
required |
members |
Union[Set[str], bool] |
|
None |
Returns:
Type | Description |
---|---|
Object |
The documented object. |
Source code in pytkdocs/loader.py
def get_object_documentation(self, dotted_path: str, members: Optional[Union[Set[str], bool]] = None) -> Object:
"""
Get the documentation for an object and its children.
Arguments:
dotted_path: The Python dotted path to the desired object.
members: `True` to select members and filter them, `False` to select no members,
or a list of names to explicitly select the members with these names.
It is applied only on the root object.
Returns:
The documented object.
"""
if members is True:
members = set()
root_object: Object
leaf = get_object_tree(dotted_path, self.new_path_syntax)
if leaf.is_module():
root_object = self.get_module_documentation(leaf, members)
elif leaf.is_class():
root_object = self.get_class_documentation(leaf, members)
elif leaf.is_staticmethod():
root_object = self.get_staticmethod_documentation(leaf)
elif leaf.is_classmethod():
root_object = self.get_classmethod_documentation(leaf)
elif leaf.is_method_descriptor():
root_object = self.get_regular_method_documentation(leaf)
elif leaf.is_method():
root_object = self.get_regular_method_documentation(leaf)
elif leaf.is_function():
root_object = self.get_function_documentation(leaf)
elif leaf.is_property():
root_object = self.get_property_documentation(leaf)
else:
root_object = self.get_attribute_documentation(leaf)
root_object.parse_all_docstrings(self.docstring_parser)
return root_object
get_property_documentation(self, node)
¤
Get the documentation for a property.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the property and its parents. |
required |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object (properties are considered attributes for now). |
Source code in pytkdocs/loader.py
def get_property_documentation(self, node: ObjectNode) -> Attribute:
"""
Get the documentation for a property.
Arguments:
node: The node representing the property and its parents.
Returns:
The documented attribute object (properties are considered attributes for now).
"""
prop = node.obj
path = node.dotted_path
properties = ["property"]
if node.is_cached_property():
# cached_property is always writable, see the docs
properties.extend(["writable", "cached"])
sig_source_func = prop.func
else:
properties.append("readonly" if prop.fset is None else "writable")
sig_source_func = prop.fget
source: Optional[Source]
try:
signature = inspect.signature(sig_source_func)
except (TypeError, ValueError) as error:
attr_type = None
else:
attr_type = signature.return_annotation
try:
source = Source(*inspect.getsourcelines(sig_source_func))
except (OSError, TypeError) as error:
source = None
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=inspect.getdoc(prop),
attr_type=attr_type,
properties=properties,
source=source,
)
get_pydantic_field_documentation(node)
staticmethod
¤
Get the documentation for a Pydantic Field.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the Field and its parents. |
required |
Returns:
Type | Description |
---|---|
Attribute |
The documented attribute object. |
Source code in pytkdocs/loader.py
@staticmethod
def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
"""
Get the documentation for a Pydantic Field.
Arguments:
node: The node representing the Field and its parents.
Returns:
The documented attribute object.
"""
prop = node.obj
path = node.dotted_path
properties = ["pydantic-field"]
if prop.required:
properties.append("required")
return Attribute(
name=node.name,
path=path,
file_path=node.file_path,
docstring=prop.field_info.description,
attr_type=prop.outer_type_,
properties=properties,
)
get_regular_method_documentation(self, node)
¤
Get the documentation for a regular method (not class- nor static-method).
We do extra processing in this method to discard docstrings of __init__
methods
that were inherited from parent classes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the method and its parents. |
required |
Returns:
Type | Description |
---|---|
Method |
The documented method object. |
Source code in pytkdocs/loader.py
def get_regular_method_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a regular method (not class- nor static-method).
We do extra processing in this method to discard docstrings of `__init__` methods
that were inherited from parent classes.
Arguments:
node: The node representing the method and its parents.
Returns:
The documented method object.
"""
method = self.get_method_documentation(node)
if node.parent:
class_ = node.parent.obj
if RE_SPECIAL.match(node.name):
docstring = method.docstring
parent_classes = class_.__mro__[1:]
for parent_class in parent_classes:
try:
parent_method = getattr(parent_class, node.name)
except AttributeError:
continue
else:
if docstring == inspect.getdoc(parent_method):
method.docstring = ""
break
return method
get_staticmethod_documentation(self, node)
¤
Get the documentation for a static-method.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
node |
ObjectNode |
The node representing the static-method and its parents. |
required |
Returns:
Type | Description |
---|---|
Method |
The documented method object. |
Source code in pytkdocs/loader.py
def get_staticmethod_documentation(self, node: ObjectNode) -> Method:
"""
Get the documentation for a static-method.
Arguments:
node: The node representing the static-method and its parents.
Returns:
The documented method object.
"""
return self.get_method_documentation(node, ["staticmethod"])
select(self, name, names)
¤
Tells whether we should select an object or not, given its name.
If the set of names is not empty, we check against it, otherwise we check against filters.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
The name of the object to select or not. |
required |
names |
Set[str] |
An explicit list of names to select. |
required |
Returns:
Type | Description |
---|---|
bool |
Yes or no. |
Source code in pytkdocs/loader.py
def select(self, name: str, names: Set[str]) -> bool:
"""
Tells whether we should select an object or not, given its name.
If the set of names is not empty, we check against it, otherwise we check against filters.
Arguments:
name: The name of the object to select or not.
names: An explicit list of names to select.
Returns:
Yes or no.
"""
if names:
return name in names
return not self.filter_name_out(name)
ObjectNode
¤
Helper class to represent an object tree.
It's not really a tree but more a backward-linked list: each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
Each node stores an object, its name, and a reference to its parent node.
Source code in pytkdocs/loader.py
class ObjectNode:
"""
Helper class to represent an object tree.
It's not really a tree but more a backward-linked list:
each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs).
Each node stores an object, its name, and a reference to its parent node.
"""
def __init__(self, obj: Any, name: str, parent: Optional["ObjectNode"] = None) -> None:
"""
Initialize the object.
Arguments:
obj: A Python object.
name: The object's name.
parent: The object's parent node.
"""
try:
obj = inspect.unwrap(obj)
except Exception: # noqa: S110,W0703 (we purposely catch every possible exception)
# inspect.unwrap at some point runs hasattr(obj, "__wrapped__"),
# which triggers the __getattr__ method of the object, which in
# turn can raise various exceptions. Probably not just __getattr__.
# See https://github.com/pawamoy/pytkdocs/issues/45
pass # noqa: WPS420 (no other way than passing)
self.obj: Any = obj
"""The actual Python object."""
self.name: str = name
"""The Python object's name."""
self.parent: Optional[ObjectNode] = parent
"""The parent node."""
@property
def dotted_path(self) -> str:
"""
Return the Python dotted path to the object.
Returns:
The Python dotted path to the object.
"""
parts = [self.name]
current = self.parent
while current:
parts.append(current.name)
current = current.parent
return ".".join(reversed(parts))
@property
def file_path(self) -> str:
"""
Return the object's module file path.
Returns:
The object's module file path.
"""
return inspect.getabsfile(self.root.obj)
@property
def root(self) -> "ObjectNode":
"""
Return the root of the tree.
Returns:
The root of the tree.
"""
if self.parent is not None:
return self.parent.root
return self
def is_module(self) -> bool:
"""
Tell if this node's object is a module.
Returns:
The root of the tree.
"""
return inspect.ismodule(self.obj)
def is_class(self) -> bool:
"""
Tell if this node's object is a class.
Returns:
If this node's object is a class.
"""
return inspect.isclass(self.obj)
def is_function(self) -> bool:
"""
Tell if this node's object is a function.
Returns:
If this node's object is a function.
"""
return inspect.isfunction(self.obj)
def is_coroutine_function(self) -> bool:
"""
Tell if this node's object is a coroutine.
Returns:
If this node's object is a coroutine.
"""
return inspect.iscoroutinefunction(self.obj)
def is_property(self) -> bool:
"""
Tell if this node's object is a property.
Returns:
If this node's object is a property.
"""
return isinstance(self.obj, property) or self.is_cached_property()
def is_cached_property(self) -> bool:
"""
Tell if this node's object is a cached property.
Returns:
If this node's object is a cached property.
"""
return isinstance(self.obj, cached_property)
def parent_is_class(self) -> bool:
"""
Tell if the object of this node's parent is a class.
Returns:
If the object of this node's parent is a class.
"""
return bool(self.parent and self.parent.is_class())
def is_method(self) -> bool:
"""
Tell if this node's object is a method.
Returns:
If this node's object is a method.
"""
function_type = type(lambda: None)
return self.parent_is_class() and isinstance(self.obj, function_type)
def is_method_descriptor(self) -> bool:
"""
Tell if this node's object is a method descriptor.
Built-in methods (e.g. those implemented in C/Rust) are often
method descriptors, rather than normal methods.
Returns:
If this node's object is a method descriptor.
"""
return inspect.ismethoddescriptor(self.obj)
def is_staticmethod(self) -> bool:
"""
Tell if this node's object is a staticmethod.
Returns:
If this node's object is a staticmethod.
"""
if not self.parent:
return False
self_from_parent = self.parent.obj.__dict__.get(self.name, None)
return self.parent_is_class() and isinstance(self_from_parent, staticmethod)
def is_classmethod(self) -> bool:
"""
Tell if this node's object is a classmethod.
Returns:
If this node's object is a classmethod.
"""
if not self.parent:
return False
self_from_parent = self.parent.obj.__dict__.get(self.name, None)
return self.parent_is_class() and isinstance(self_from_parent, classmethod)
dotted_path: str
property
readonly
¤
Return the Python dotted path to the object.
Returns:
Type | Description |
---|---|
str |
The Python dotted path to the object. |
file_path: str
property
readonly
¤
Return the object's module file path.
Returns:
Type | Description |
---|---|
str |
The object's module file path. |
root: ObjectNode
property
readonly
¤
Return the root of the tree.
Returns:
Type | Description |
---|---|
ObjectNode |
The root of the tree. |
__init__(self, obj, name, parent=None)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
obj |
Any |
A Python object. |
required |
name |
str |
The object's name. |
required |
parent |
Optional[ObjectNode] |
The object's parent node. |
None |
Source code in pytkdocs/loader.py
def __init__(self, obj: Any, name: str, parent: Optional["ObjectNode"] = None) -> None:
"""
Initialize the object.
Arguments:
obj: A Python object.
name: The object's name.
parent: The object's parent node.
"""
try:
obj = inspect.unwrap(obj)
except Exception: # noqa: S110,W0703 (we purposely catch every possible exception)
# inspect.unwrap at some point runs hasattr(obj, "__wrapped__"),
# which triggers the __getattr__ method of the object, which in
# turn can raise various exceptions. Probably not just __getattr__.
# See https://github.com/pawamoy/pytkdocs/issues/45
pass # noqa: WPS420 (no other way than passing)
self.obj: Any = obj
"""The actual Python object."""
self.name: str = name
"""The Python object's name."""
self.parent: Optional[ObjectNode] = parent
"""The parent node."""
is_cached_property(self)
¤
Tell if this node's object is a cached property.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a cached property. |
Source code in pytkdocs/loader.py
def is_cached_property(self) -> bool:
"""
Tell if this node's object is a cached property.
Returns:
If this node's object is a cached property.
"""
return isinstance(self.obj, cached_property)
is_class(self)
¤
Tell if this node's object is a class.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a class. |
Source code in pytkdocs/loader.py
def is_class(self) -> bool:
"""
Tell if this node's object is a class.
Returns:
If this node's object is a class.
"""
return inspect.isclass(self.obj)
is_classmethod(self)
¤
Tell if this node's object is a classmethod.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a classmethod. |
Source code in pytkdocs/loader.py
def is_classmethod(self) -> bool:
"""
Tell if this node's object is a classmethod.
Returns:
If this node's object is a classmethod.
"""
if not self.parent:
return False
self_from_parent = self.parent.obj.__dict__.get(self.name, None)
return self.parent_is_class() and isinstance(self_from_parent, classmethod)
is_coroutine_function(self)
¤
Tell if this node's object is a coroutine.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a coroutine. |
Source code in pytkdocs/loader.py
def is_coroutine_function(self) -> bool:
"""
Tell if this node's object is a coroutine.
Returns:
If this node's object is a coroutine.
"""
return inspect.iscoroutinefunction(self.obj)
is_function(self)
¤
Tell if this node's object is a function.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a function. |
Source code in pytkdocs/loader.py
def is_function(self) -> bool:
"""
Tell if this node's object is a function.
Returns:
If this node's object is a function.
"""
return inspect.isfunction(self.obj)
is_method(self)
¤
Tell if this node's object is a method.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a method. |
Source code in pytkdocs/loader.py
def is_method(self) -> bool:
"""
Tell if this node's object is a method.
Returns:
If this node's object is a method.
"""
function_type = type(lambda: None)
return self.parent_is_class() and isinstance(self.obj, function_type)
is_method_descriptor(self)
¤
Tell if this node's object is a method descriptor.
Built-in methods (e.g. those implemented in C/Rust) are often method descriptors, rather than normal methods.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a method descriptor. |
Source code in pytkdocs/loader.py
def is_method_descriptor(self) -> bool:
"""
Tell if this node's object is a method descriptor.
Built-in methods (e.g. those implemented in C/Rust) are often
method descriptors, rather than normal methods.
Returns:
If this node's object is a method descriptor.
"""
return inspect.ismethoddescriptor(self.obj)
is_module(self)
¤
Tell if this node's object is a module.
Returns:
Type | Description |
---|---|
bool |
The root of the tree. |
Source code in pytkdocs/loader.py
def is_module(self) -> bool:
"""
Tell if this node's object is a module.
Returns:
The root of the tree.
"""
return inspect.ismodule(self.obj)
is_property(self)
¤
Tell if this node's object is a property.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a property. |
Source code in pytkdocs/loader.py
def is_property(self) -> bool:
"""
Tell if this node's object is a property.
Returns:
If this node's object is a property.
"""
return isinstance(self.obj, property) or self.is_cached_property()
is_staticmethod(self)
¤
Tell if this node's object is a staticmethod.
Returns:
Type | Description |
---|---|
bool |
If this node's object is a staticmethod. |
Source code in pytkdocs/loader.py
def is_staticmethod(self) -> bool:
"""
Tell if this node's object is a staticmethod.
Returns:
If this node's object is a staticmethod.
"""
if not self.parent:
return False
self_from_parent = self.parent.obj.__dict__.get(self.name, None)
return self.parent_is_class() and isinstance(self_from_parent, staticmethod)
parent_is_class(self)
¤
Tell if the object of this node's parent is a class.
Returns:
Type | Description |
---|---|
bool |
If the object of this node's parent is a class. |
Source code in pytkdocs/loader.py
def parent_is_class(self) -> bool:
"""
Tell if the object of this node's parent is a class.
Returns:
If the object of this node's parent is a class.
"""
return bool(self.parent and self.parent.is_class())
field_is_inherited(field_name, fields_name, base_class)
¤
Check if a field with a certain name was inherited from parent classes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
field_name |
str |
The name of the field to check. |
required |
fields_name |
str |
The name of the attribute in which the fields are stored. |
required |
base_class |
type |
The base class in which the field appears. |
required |
Returns:
Type | Description |
---|---|
bool |
Whether the field was inherited. |
Source code in pytkdocs/loader.py
def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> bool:
"""
Check if a field with a certain name was inherited from parent classes.
Arguments:
field_name: The name of the field to check.
fields_name: The name of the attribute in which the fields are stored.
base_class: The base class in which the field appears.
Returns:
Whether the field was inherited.
"""
# To tell if a field was inherited, we check if it exists in parent classes __fields__ attributes.
# We don't check the current class, nor the top one (object), hence __mro__[1:-1]
return field_name in set(
chain(
*(getattr(parent_class, fields_name, {}).keys() for parent_class in base_class.__mro__[1:-1]),
),
)
get_object_tree(path, new_path_syntax=False)
¤
Transform a path into an actual Python object.
The path can be arbitrary long. You can pass the path to a package,
a module, a class, a function or a global variable, as deep as you
want, as long as the deepest module is importable through
importlib.import_module
and each object is obtainable through
the getattr
method. It is not possible to load local objects.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path |
str |
The dot/colon-separated path of the object. |
required |
new_path_syntax |
bool |
Whether to use the "colon" syntax for the path. |
False |
Exceptions:
Type | Description |
---|---|
ValueError |
When the path is not valid (evaluates to |
ImportError |
When the object or its parent module could not be imported. |
Returns:
Type | Description |
---|---|
ObjectNode |
The leaf node representing the object and its parents. |
Source code in pytkdocs/loader.py
def get_object_tree(path: str, new_path_syntax: bool = False) -> ObjectNode:
"""
Transform a path into an actual Python object.
The path can be arbitrary long. You can pass the path to a package,
a module, a class, a function or a global variable, as deep as you
want, as long as the deepest module is importable through
`importlib.import_module` and each object is obtainable through
the `getattr` method. It is not possible to load local objects.
Args:
path: The dot/colon-separated path of the object.
new_path_syntax: Whether to use the "colon" syntax for the path.
Raises:
ValueError: When the path is not valid (evaluates to `False`).
ImportError: When the object or its parent module could not be imported.
Returns:
The leaf node representing the object and its parents.
"""
if not path:
raise ValueError(f"path must be a valid Python path, not {path}")
objects: List[str] = []
if ":" in path or new_path_syntax:
try:
module_path, object_path = path.split(":")
except ValueError: # no colon
module_path, objects = path, []
else:
objects = object_path.split(".")
# let the ImportError bubble up
parent_module = importlib.import_module(module_path)
else:
# We will try to import the longest dotted-path first.
# If it fails, we remove the right-most part and put it in a list of "objects", used later.
# We loop until we find the deepest importable submodule.
obj_parent_modules = path.split(".")
while True:
parent_module_path = ".".join(obj_parent_modules)
try:
parent_module = importlib.import_module(parent_module_path)
except ImportError as error:
if len(obj_parent_modules) == 1:
raise ImportError(
f"Importing '{path}' failed, possible causes are:\n"
f"- an exception happened while importing\n"
f"- an element in the path does not exist",
) from error
objects.insert(0, obj_parent_modules.pop(-1))
else:
break
# We now have the module containing the desired object.
# We will build the object tree by iterating over the previously stored objects names
# and trying to get them as attributes.
current_node = ObjectNode(parent_module, parent_module.__name__)
for obj_name in objects:
obj = getattr(current_node.obj, obj_name)
child = ObjectNode(obj, obj_name, parent=current_node)
current_node = child
leaf = current_node
# We now try to get the "real" parent module, not the one the object was imported into.
# This is important if we want to be able to retrieve the docstring of an attribute for example.
# Once we find an object for which we could get the module, we stop trying to get the module.
# Once we reach the node before the root, we apply the module if found, and break.
real_module = None
while current_node.parent is not None:
if real_module is None:
real_module = inspect.getmodule(current_node.obj)
if inspect.ismodule(current_node.parent.obj):
if real_module is not None and real_module is not current_node.parent.obj:
current_node.parent = ObjectNode(real_module, real_module.__name__)
break
current_node = current_node.parent
return leaf
split_attr_name(attr_name)
¤
Split an attribute name into a first-order attribute name and remainder.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
attr_name |
str |
Attribute name (a) |
required |
Returns:
Type | Description |
---|---|
Tuple containing |
first_order_attr_name: Name of the first order attribute (a) remainder: The remainder (b.c) |
Source code in pytkdocs/loader.py
def split_attr_name(attr_name: str) -> Tuple[str, Optional[str]]:
"""
Split an attribute name into a first-order attribute name and remainder.
Args:
attr_name: Attribute name (a)
Returns:
Tuple containing:
first_order_attr_name: Name of the first order attribute (a)
remainder: The remainder (b.c)
"""
first_order_attr_name, *remaining = attr_name.split(".", maxsplit=1)
remainder = remaining[0] if remaining else None
return first_order_attr_name, remainder
objects
¤
This module defines the documented objects classes.
- the generic
Object
class - the
Module
class - the
Class
class - the
Method
class - the
Function
class - the
Attribute
class
Note that properties are considered attributes, because they are used like such.
It also defines a convenient Source
class to represent source code.
Attribute (Object)
¤
A class to store information about an attribute.
It accepts an additional attr_type
argument at instantiation.
Source code in pytkdocs/objects.py
class Attribute(Object):
"""
A class to store information about an attribute.
It accepts an additional `attr_type` argument at instantiation.
"""
possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_CLASS_PRIVATE, NAME_PRIVATE]
def __init__(self, *args, attr_type=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
attr_type: The attribute type.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.type = attr_type
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
__init__(self, *args, *, attr_type=None, **kwargs)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*args |
Arguments passed to the parent class Initialize the object. |
() |
|
attr_type |
The attribute type. |
None |
|
**kwargs |
Arguments passed to the parent class Initialize the object. |
{} |
Source code in pytkdocs/objects.py
def __init__(self, *args, attr_type=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
attr_type: The attribute type.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.type = attr_type
Class (Object)
¤
A class to store information about a class.
Source code in pytkdocs/objects.py
class Class(Object):
"""A class to store information about a class."""
possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE]
def __init__(self, *args, bases: List[str] = None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
bases: The base classes (dotted paths).
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.bases = bases or ["object"]
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
__init__(self, *args, *, bases=None, **kwargs)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*args |
Arguments passed to the parent class Initialize the object. |
() |
|
bases |
List[str] |
The base classes (dotted paths). |
None |
**kwargs |
Arguments passed to the parent class Initialize the object. |
{} |
Source code in pytkdocs/objects.py
def __init__(self, *args, bases: List[str] = None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
bases: The base classes (dotted paths).
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.bases = bases or ["object"]
Function (Object)
¤
A class to store information about a function.
It accepts an additional signature
argument at instantiation.
Source code in pytkdocs/objects.py
class Function(Object):
"""
A class to store information about a function.
It accepts an additional `signature` argument at instantiation.
"""
possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE]
def __init__(self, *args, signature=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
signature: The function signature.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.signature = signature
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
__init__(self, *args, *, signature=None, **kwargs)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*args |
Arguments passed to the parent class Initialize the object. |
() |
|
signature |
The function signature. |
None |
|
**kwargs |
Arguments passed to the parent class Initialize the object. |
{} |
Source code in pytkdocs/objects.py
def __init__(self, *args, signature=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
signature: The function signature.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.signature = signature
Method (Object)
¤
A class to store information about a method.
It accepts an additional signature
argument at instantiation.
Source code in pytkdocs/objects.py
class Method(Object):
"""
A class to store information about a method.
It accepts an additional `signature` argument at instantiation.
"""
possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE]
def __init__(self, *args, signature=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
signature: The function signature.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.signature = signature
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
__init__(self, *args, *, signature=None, **kwargs)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*args |
Arguments passed to the parent class Initialize the object. |
() |
|
signature |
The function signature. |
None |
|
**kwargs |
Arguments passed to the parent class Initialize the object. |
{} |
Source code in pytkdocs/objects.py
def __init__(self, *args, signature=None, **kwargs):
"""
Initialize the object.
Arguments:
*args: Arguments passed to the parent class Initialize the object.
signature: The function signature.
**kwargs: Arguments passed to the parent class Initialize the object.
"""
super().__init__(*args, **kwargs)
self.signature = signature
Module (Object)
¤
A class to store information about a module.
Source code in pytkdocs/objects.py
class Module(Object):
"""A class to store information about a module."""
possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE]
@property
def file_name(self) -> str:
"""
Return the base name of the module file, without the extension.
Returns:
The module file's base name.
"""
return os.path.splitext(os.path.basename(self.file_path))[0]
@property
def name_to_check(self) -> str: # noqa: D102
return self.file_name
file_name: str
property
readonly
¤
Return the base name of the module file, without the extension.
Returns:
Type | Description |
---|---|
str |
The module file's base name. |
name_to_check: str
property
readonly
¤
Return the attribute to check against name-properties regular expressions (private, class-private, special).
Returns:
Type | Description |
---|---|
str |
The attribute to check (its name). |
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
Object
¤
A base class to store information about a Python object.
Each instance additionally stores references to its children, grouped by category.
Source code in pytkdocs/objects.py
class Object(metaclass=ABCMeta):
"""
A base class to store information about a Python object.
Each instance additionally stores references to its children, grouped by category.
"""
possible_name_properties: List[ApplicableNameProperty] = []
"""
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of `Object` to another.
"""
def __init__(
self,
name: str,
path: str,
file_path: str,
docstring: Optional[str] = "",
properties: Optional[List[str]] = None,
source: Optional[Source] = None,
) -> None:
"""
Initialize the object.
Arguments:
name: The object's name.
path: The object's dotted-path.
file_path: The file path of the object's direct parent module.
docstring: The object's docstring.
properties: The object's properties.
source: The object's source code.
"""
self.name = name
"""The object's name."""
self.path = path
"""The object's dotted-path."""
self.file_path = file_path
"""The file path of the object's direct parent module."""
self.docstring = docstring
"""The object's docstring."""
self.docstring_sections: List[Section] = []
"""The object's docstring parsed into sections."""
self.docstring_errors: List[str] = []
"""The errors detected while parsing the docstring."""
self.properties = properties or []
"""The object's properties."""
self.parent: Optional[Object] = None
"""The object's parent (another instance of a subclass of `Object`)."""
self.source = source
"""The object's source code."""
self._path_map = {self.path: self}
self._parsed = False
self.attributes: List[Attribute] = []
"""The list of all the object's attributes."""
self.methods: List[Method] = []
"""The list of all the object's methods."""
self.functions: List[Function] = []
"""The list of all the object's functions."""
self.modules: List[Module] = []
"""The list of all the object's submodules."""
self.classes: List[Class] = []
"""The list of all the object's classes."""
self.children: List[Object] = []
"""The list of all the object's children."""
def __str__(self) -> str:
return self.path
@property
def category(self) -> str:
"""
Return the object's category.
Returns:
The object's category (module, class, function, method or attribute).
"""
return self.__class__.__name__.lower()
@property
def root(self) -> "Object":
"""
Return the object's root.
Returns:
The object's root (top-most parent).
"""
obj = self
while obj.parent:
obj = obj.parent
return obj
@property
def relative_file_path(self) -> str:
"""
Return the relative file path of the object.
It is the relative path to the object's module,
starting at the path of the top-most package it is contained in.
For example:
- package is `a`
- package absolute path is `/abs/path/to/a`
- module is `a.b.c`
- object is `c` or anything defined in `c`
- relative file path is `a/b/c.py`
If the relative file path cannot be determined, the value returned is `""` (empty string).
Returns:
The path relative to the object's package.
"""
parts = self.path.split(".")
namespaces = [".".join(parts[:length]) for length in range(1, len(parts) + 1)] # noqa: WPS221 (not complex)
# Iterate through all sub namespaces including the last in case it is a module
for namespace in namespaces:
try: # noqa: WPS229 (more compact)
importlib.import_module(namespace)
top_package = sys.modules[namespace]
except (ModuleNotFoundError, ImportError, KeyError):
# ImportError: Triggered if the namespace is not importable
# ModuleNotFoundError: Triggered if the namespace is not a module
# KeyError: Triggered if the imported package isn't referenced under the same fully qualified name
# Namespace packages are importable, so this should work for them
return ""
try: # noqa: WPS229 (more compact)
top_package_path = Path(inspect.getabsfile(top_package)).parent
return str(Path(self.file_path).relative_to(top_package_path.parent))
except TypeError:
# Triggered if getabsfile() can't be found in the case of a Namespace package
pass # noqa: WPS420 (passing is the only way)
except ValueError:
# Triggered if Path().relative_to can't find an appropriate path
return ""
return ""
@property
def name_to_check(self) -> str:
"""
Return the attribute to check against name-properties regular expressions (private, class-private, special).
Returns:
The attribute to check (its name).
"""
return self.name
@property
def name_properties(self) -> List[str]:
"""
Return the object's name properties.
Returns:
The object's name properties (private, class-private, special).
"""
properties = []
for prop, predicate in self.possible_name_properties:
if predicate(self.name_to_check):
properties.append(prop)
return properties
@property
def parent_path(self) -> str:
"""
Return the parent's path, computed from the current path.
The parent object path is not used: this property is used to see if an object is really related to another one,
to add it as a child to the other. When we do that, the child doesn't even have a parent.
Returns:
The dotted path of the parent object.
"""
return self.path.rsplit(".", 1)[0]
def add_child(self, obj: "Object") -> None: # noqa: WPS231 (not complex)
"""
Add an object as a child of this object.
If the child computed `parent_path` is not equal to this object's path, abort.
Append the child to the `children` list, and to the right category list.
Arguments:
obj: An instance of documented object.
"""
if obj.parent_path != self.path:
return
self.children.append(obj)
if isinstance(obj, Module):
self.modules.append(obj)
elif isinstance(obj, Class):
self.classes.append(obj)
elif isinstance(obj, Function):
self.functions.append(obj)
elif isinstance(obj, Method):
self.methods.append(obj)
elif isinstance(obj, Attribute):
# Dataclass attributes with default values will already be present in `self.attributes` as they are
# resolved differently by the python interpreter. As they have a concrete value, they are already present
# in the "original" class. They should be overridden with the new "dataclass" attribute coming in here
# (having the "dataclass_field" property set)
new_attribute_name = obj.name
for attribute in self.attributes:
if attribute.name == new_attribute_name:
self.attributes.remove(attribute)
self.attributes.append(obj)
obj.parent = self
self._path_map[obj.path] = obj
def add_children(self, children: List["Object"]) -> None:
"""
Add a list of objects as children of this object.
Arguments:
children: The list of children to add.
"""
for child in children:
self.add_child(child)
def parse_docstring(self, parser: Parser, **context) -> None:
"""
Parse the docstring of this object.
Arguments:
parser: A parser to parse the docstrings.
**context: Additional context to use when parsing.
"""
if self.docstring and not self._parsed:
sections, errors = parser.parse(self.docstring, {"obj": self, **context})
self.docstring_sections = sections
self.docstring_errors = errors
self._parsed = True
def parse_all_docstrings(self, parser: Parser) -> None:
"""
Recursively parse the docstring of this object and its children.
Arguments:
parser: A parser to parse the docstrings.
"""
self.parse_docstring(parser)
for child in self.children:
child.parse_all_docstrings(parser)
@lru_cache()
def has_contents(self) -> bool:
"""
Tells if the object has "contents".
An object has contents when:
- it is the root of the object tree
- it has a docstring
- at least one of its children (whatever the depth) has contents
The value is cached, so this method should be called last, when the tree doesn't change anymore.
Returns:
Whether this object has contents or not.
"""
has_docstring = bool(self.docstring)
is_root = not self.parent
children_have_contents = any(child.has_contents() for child in self.children)
return has_docstring or is_root or children_have_contents
category: str
property
readonly
¤
Return the object's category.
Returns:
Type | Description |
---|---|
str |
The object's category (module, class, function, method or attribute). |
name_properties: List[str]
property
readonly
¤
Return the object's name properties.
Returns:
Type | Description |
---|---|
List[str] |
The object's name properties (private, class-private, special). |
name_to_check: str
property
readonly
¤
Return the attribute to check against name-properties regular expressions (private, class-private, special).
Returns:
Type | Description |
---|---|
str |
The attribute to check (its name). |
parent_path: str
property
readonly
¤
Return the parent's path, computed from the current path.
The parent object path is not used: this property is used to see if an object is really related to another one, to add it as a child to the other. When we do that, the child doesn't even have a parent.
Returns:
Type | Description |
---|---|
str |
The dotted path of the parent object. |
possible_name_properties: List[Tuple[str, Callable[[str], bool]]]
¤
The properties that we can apply to the object based on its name.
The applicable properties vary from one subclass of Object
to another.
relative_file_path: str
property
readonly
¤
Return the relative file path of the object.
It is the relative path to the object's module, starting at the path of the top-most package it is contained in.
For example:
- package is
a
- package absolute path is
/abs/path/to/a
- module is
a.b.c
- object is
c
or anything defined inc
- relative file path is
a/b/c.py
If the relative file path cannot be determined, the value returned is ""
(empty string).
Returns:
Type | Description |
---|---|
str |
The path relative to the object's package. |
root: Object
property
readonly
¤
Return the object's root.
Returns:
Type | Description |
---|---|
Object |
The object's root (top-most parent). |
__init__(self, name, path, file_path, docstring='', properties=None, source=None)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
The object's name. |
required |
path |
str |
The object's dotted-path. |
required |
file_path |
str |
The file path of the object's direct parent module. |
required |
docstring |
Optional[str] |
The object's docstring. |
'' |
properties |
Optional[List[str]] |
The object's properties. |
None |
source |
Optional[pytkdocs.objects.Source] |
The object's source code. |
None |
Source code in pytkdocs/objects.py
def __init__(
self,
name: str,
path: str,
file_path: str,
docstring: Optional[str] = "",
properties: Optional[List[str]] = None,
source: Optional[Source] = None,
) -> None:
"""
Initialize the object.
Arguments:
name: The object's name.
path: The object's dotted-path.
file_path: The file path of the object's direct parent module.
docstring: The object's docstring.
properties: The object's properties.
source: The object's source code.
"""
self.name = name
"""The object's name."""
self.path = path
"""The object's dotted-path."""
self.file_path = file_path
"""The file path of the object's direct parent module."""
self.docstring = docstring
"""The object's docstring."""
self.docstring_sections: List[Section] = []
"""The object's docstring parsed into sections."""
self.docstring_errors: List[str] = []
"""The errors detected while parsing the docstring."""
self.properties = properties or []
"""The object's properties."""
self.parent: Optional[Object] = None
"""The object's parent (another instance of a subclass of `Object`)."""
self.source = source
"""The object's source code."""
self._path_map = {self.path: self}
self._parsed = False
self.attributes: List[Attribute] = []
"""The list of all the object's attributes."""
self.methods: List[Method] = []
"""The list of all the object's methods."""
self.functions: List[Function] = []
"""The list of all the object's functions."""
self.modules: List[Module] = []
"""The list of all the object's submodules."""
self.classes: List[Class] = []
"""The list of all the object's classes."""
self.children: List[Object] = []
"""The list of all the object's children."""
add_child(self, obj)
¤
Add an object as a child of this object.
If the child computed parent_path
is not equal to this object's path, abort.
Append the child to the children
list, and to the right category list.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
obj |
Object |
An instance of documented object. |
required |
Source code in pytkdocs/objects.py
def add_child(self, obj: "Object") -> None: # noqa: WPS231 (not complex)
"""
Add an object as a child of this object.
If the child computed `parent_path` is not equal to this object's path, abort.
Append the child to the `children` list, and to the right category list.
Arguments:
obj: An instance of documented object.
"""
if obj.parent_path != self.path:
return
self.children.append(obj)
if isinstance(obj, Module):
self.modules.append(obj)
elif isinstance(obj, Class):
self.classes.append(obj)
elif isinstance(obj, Function):
self.functions.append(obj)
elif isinstance(obj, Method):
self.methods.append(obj)
elif isinstance(obj, Attribute):
# Dataclass attributes with default values will already be present in `self.attributes` as they are
# resolved differently by the python interpreter. As they have a concrete value, they are already present
# in the "original" class. They should be overridden with the new "dataclass" attribute coming in here
# (having the "dataclass_field" property set)
new_attribute_name = obj.name
for attribute in self.attributes:
if attribute.name == new_attribute_name:
self.attributes.remove(attribute)
self.attributes.append(obj)
obj.parent = self
self._path_map[obj.path] = obj
add_children(self, children)
¤
Add a list of objects as children of this object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
children |
List[Object] |
The list of children to add. |
required |
Source code in pytkdocs/objects.py
def add_children(self, children: List["Object"]) -> None:
"""
Add a list of objects as children of this object.
Arguments:
children: The list of children to add.
"""
for child in children:
self.add_child(child)
has_contents(self)
¤
Tells if the object has "contents".
An object has contents when:
- it is the root of the object tree
- it has a docstring
- at least one of its children (whatever the depth) has contents
The value is cached, so this method should be called last, when the tree doesn't change anymore.
Returns:
Type | Description |
---|---|
bool |
Whether this object has contents or not. |
Source code in pytkdocs/objects.py
@lru_cache()
def has_contents(self) -> bool:
"""
Tells if the object has "contents".
An object has contents when:
- it is the root of the object tree
- it has a docstring
- at least one of its children (whatever the depth) has contents
The value is cached, so this method should be called last, when the tree doesn't change anymore.
Returns:
Whether this object has contents or not.
"""
has_docstring = bool(self.docstring)
is_root = not self.parent
children_have_contents = any(child.has_contents() for child in self.children)
return has_docstring or is_root or children_have_contents
parse_all_docstrings(self, parser)
¤
Recursively parse the docstring of this object and its children.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
parser |
Parser |
A parser to parse the docstrings. |
required |
Source code in pytkdocs/objects.py
def parse_all_docstrings(self, parser: Parser) -> None:
"""
Recursively parse the docstring of this object and its children.
Arguments:
parser: A parser to parse the docstrings.
"""
self.parse_docstring(parser)
for child in self.children:
child.parse_all_docstrings(parser)
parse_docstring(self, parser, **context)
¤
Parse the docstring of this object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
parser |
Parser |
A parser to parse the docstrings. |
required |
**context |
Additional context to use when parsing. |
{} |
Source code in pytkdocs/objects.py
def parse_docstring(self, parser: Parser, **context) -> None:
"""
Parse the docstring of this object.
Arguments:
parser: A parser to parse the docstrings.
**context: Additional context to use when parsing.
"""
if self.docstring and not self._parsed:
sections, errors = parser.parse(self.docstring, {"obj": self, **context})
self.docstring_sections = sections
self.docstring_errors = errors
self._parsed = True
Source
¤
Helper class to represent source code.
It is simply used to wrap the result of
inspect.getsourceslines
.
Source code in pytkdocs/objects.py
class Source:
"""
Helper class to represent source code.
It is simply used to wrap the result of
[`inspect.getsourceslines`](https://docs.python.org/3/library/inspect.html#inspect.getsourcelines).
"""
def __init__(self, lines: Union[str, List[str]], line_start: int) -> None:
"""
Initialize the object.
Arguments:
lines: A list of strings. The strings should have trailing newlines.
line_start: The line number of where the code starts in the file.
"""
if isinstance(lines, list):
code = "".join(lines)
else:
code = lines
self.code = code
"""The code, as a single string."""
self.line_start = line_start
"""The first line number."""
__init__(self, lines, line_start)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
Union[str, List[str]] |
A list of strings. The strings should have trailing newlines. |
required |
line_start |
int |
The line number of where the code starts in the file. |
required |
Source code in pytkdocs/objects.py
def __init__(self, lines: Union[str, List[str]], line_start: int) -> None:
"""
Initialize the object.
Arguments:
lines: A list of strings. The strings should have trailing newlines.
line_start: The line number of where the code starts in the file.
"""
if isinstance(lines, list):
code = "".join(lines)
else:
code = lines
self.code = code
"""The code, as a single string."""
self.line_start = line_start
"""The first line number."""
parsers
special
¤
The docstrings parsers' package.
attributes
¤
Module containing functions to parse attributes in the source code.
docstrings
special
¤
The parsers' package.
base
¤
The base module for docstring parsing.
AnnotatedObject
¤
A helper class to store information about an annotated object.
Source code in pytkdocs/parsers/docstrings/base.py
class AnnotatedObject:
"""A helper class to store information about an annotated object."""
def __init__(self, annotation: Any, description: str) -> None:
"""
Initialize the object.
Arguments:
annotation: The object's annotation.
description: The object's description.
"""
self.annotation = annotation
self.description = description
__init__(self, annotation, description)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
annotation |
Any |
The object's annotation. |
required |
description |
str |
The object's description. |
required |
Source code in pytkdocs/parsers/docstrings/base.py
def __init__(self, annotation: Any, description: str) -> None:
"""
Initialize the object.
Arguments:
annotation: The object's annotation.
description: The object's description.
"""
self.annotation = annotation
self.description = description
Attribute (AnnotatedObject)
¤
A helper class to store information about a documented attribute.
Source code in pytkdocs/parsers/docstrings/base.py
class Attribute(AnnotatedObject):
"""A helper class to store information about a documented attribute."""
def __init__(self, name: str, annotation: Any, description: str) -> None:
"""
Initialize the object.
Arguments:
name: The attribute's name.
annotation: The object's annotation.
description: The object's description.
"""
super().__init__(annotation, description)
self.name = name
__init__(self, name, annotation, description)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
The attribute's name. |
required |
annotation |
Any |
The object's annotation. |
required |
description |
str |
The object's description. |
required |
Source code in pytkdocs/parsers/docstrings/base.py
def __init__(self, name: str, annotation: Any, description: str) -> None:
"""
Initialize the object.
Arguments:
name: The attribute's name.
annotation: The object's annotation.
description: The object's description.
"""
super().__init__(annotation, description)
self.name = name
Parameter (AnnotatedObject)
¤
A helper class to store information about a signature parameter.
Source code in pytkdocs/parsers/docstrings/base.py
class Parameter(AnnotatedObject):
"""A helper class to store information about a signature parameter."""
def __init__(self, name: str, annotation: Any, description: str, kind: Any, default: Any = empty) -> None:
"""
Initialize the object.
Arguments:
name: The parameter's name.
annotation: The parameter's annotation.
description: The parameter's description.
kind: The parameter's kind (positional only, keyword only, etc.).
default: The parameter's default value.
"""
super().__init__(annotation, description)
self.name = name
self.kind = kind
self.default = default
def __str__(self):
return self.name
def __repr__(self):
return f"<Parameter({self.name}, {self.annotation}, {self.description}, {self.kind}, {self.default})>"
@property
def is_optional(self):
"""Tell if this parameter is optional."""
return self.default is not empty
@property
def is_required(self):
"""Tell if this parameter is required."""
return not self.is_optional
@property
def is_args(self):
"""Tell if this parameter is positional."""
return self.kind is inspect.Parameter.VAR_POSITIONAL
@property
def is_kwargs(self):
"""Tell if this parameter is a keyword."""
return self.kind is inspect.Parameter.VAR_KEYWORD
@property
def default_string(self):
"""Return the default value as a string."""
if self.is_kwargs:
return "{}"
if self.is_args:
return "()"
if self.is_required:
return ""
return repr(self.default)
default_string
property
readonly
¤
Return the default value as a string.
is_args
property
readonly
¤
Tell if this parameter is positional.
is_kwargs
property
readonly
¤
Tell if this parameter is a keyword.
is_optional
property
readonly
¤
Tell if this parameter is optional.
is_required
property
readonly
¤
Tell if this parameter is required.
__init__(self, name, annotation, description, kind, default)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
str |
The parameter's name. |
required |
annotation |
Any |
The parameter's annotation. |
required |
description |
str |
The parameter's description. |
required |
kind |
Any |
The parameter's kind (positional only, keyword only, etc.). |
required |
default |
Any |
The parameter's default value. |
required |
Source code in pytkdocs/parsers/docstrings/base.py
def __init__(self, name: str, annotation: Any, description: str, kind: Any, default: Any = empty) -> None:
"""
Initialize the object.
Arguments:
name: The parameter's name.
annotation: The parameter's annotation.
description: The parameter's description.
kind: The parameter's kind (positional only, keyword only, etc.).
default: The parameter's default value.
"""
super().__init__(annotation, description)
self.name = name
self.kind = kind
self.default = default
Parser
¤
A class to parse docstrings.
It is instantiated with an object's path, docstring, signature and return type.
The parse
method then returns structured data,
in the form of a list of Section
s.
It also return the list of errors that occurred during parsing.
Source code in pytkdocs/parsers/docstrings/base.py
class Parser(metaclass=ABCMeta):
"""
A class to parse docstrings.
It is instantiated with an object's path, docstring, signature and return type.
The `parse` method then returns structured data,
in the form of a list of [`Section`][pytkdocs.parsers.docstrings.base.Section]s.
It also return the list of errors that occurred during parsing.
"""
def __init__(self) -> None:
"""Initialize the object."""
self.context: dict = {}
self.errors: List[str] = []
def parse(self, docstring: str, context: Optional[dict] = None) -> Tuple[List[Section], List[str]]:
"""
Parse a docstring and return a list of sections and parsing errors.
Arguments:
docstring: The docstring to parse.
context: Some context helping to parse the docstring.
Returns:
A tuple containing the list of sections and the parsing errors.
"""
self.context = context or {}
self.errors = []
sections = self.parse_sections(docstring)
errors = self.errors
return sections, errors
def error(self, message) -> None:
"""
Record a parsing error.
Arguments:
message: A message described the error.
"""
if self.context["obj"]:
message = f"{self.context['obj'].path}: {message}"
self.errors.append(message)
@abstractmethod
def parse_sections(self, docstring: str) -> List[Section]:
"""
Parse a docstring as a list of sections.
Arguments:
docstring: The docstring to parse.
Returns:
A list of [`Section`][pytkdocs.parsers.docstrings.base.Section]s.
"""
raise NotImplementedError
__init__(self)
special
¤
Initialize the object.
Source code in pytkdocs/parsers/docstrings/base.py
def __init__(self) -> None:
"""Initialize the object."""
self.context: dict = {}
self.errors: List[str] = []
error(self, message)
¤
Record a parsing error.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
message |
A message described the error. |
required |
Source code in pytkdocs/parsers/docstrings/base.py
def error(self, message) -> None:
"""
Record a parsing error.
Arguments:
message: A message described the error.
"""
if self.context["obj"]:
message = f"{self.context['obj'].path}: {message}"
self.errors.append(message)
parse(self, docstring, context=None)
¤
Parse a docstring and return a list of sections and parsing errors.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
context |
Optional[dict] |
Some context helping to parse the docstring. |
None |
Returns:
Type | Description |
---|---|
Tuple[List[pytkdocs.parsers.docstrings.base.Section], List[str]] |
A tuple containing the list of sections and the parsing errors. |
Source code in pytkdocs/parsers/docstrings/base.py
def parse(self, docstring: str, context: Optional[dict] = None) -> Tuple[List[Section], List[str]]:
"""
Parse a docstring and return a list of sections and parsing errors.
Arguments:
docstring: The docstring to parse.
context: Some context helping to parse the docstring.
Returns:
A tuple containing the list of sections and the parsing errors.
"""
self.context = context or {}
self.errors = []
sections = self.parse_sections(docstring)
errors = self.errors
return sections, errors
parse_sections(self, docstring)
¤
Parse a docstring as a list of sections.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
Returns:
Type | Description |
---|---|
List[pytkdocs.parsers.docstrings.base.Section] |
A list of |
Source code in pytkdocs/parsers/docstrings/base.py
@abstractmethod
def parse_sections(self, docstring: str) -> List[Section]:
"""
Parse a docstring as a list of sections.
Arguments:
docstring: The docstring to parse.
Returns:
A list of [`Section`][pytkdocs.parsers.docstrings.base.Section]s.
"""
raise NotImplementedError
Section
¤
A helper class to store a docstring section.
Source code in pytkdocs/parsers/docstrings/base.py
class Section:
"""A helper class to store a docstring section."""
class Type:
"""The possible section types."""
MARKDOWN = "markdown"
PARAMETERS = "parameters"
EXCEPTIONS = "exceptions"
RETURN = "return"
YIELD = "yield"
EXAMPLES = "examples"
ATTRIBUTES = "attributes"
KEYWORD_ARGS = "keyword_args"
def __init__(self, section_type: str, value: Any) -> None:
"""
Initialize the object.
Arguments:
section_type: The type of the section, from the [`Type`][pytkdocs.parsers.docstrings.base.Section.Type] enum.
value: The section value.
"""
self.type = section_type
self.value = value
def __str__(self):
return self.type
def __repr__(self):
return f"<Section(type={self.type!r})>"
Type
¤
The possible section types.
Source code in pytkdocs/parsers/docstrings/base.py
class Type:
"""The possible section types."""
MARKDOWN = "markdown"
PARAMETERS = "parameters"
EXCEPTIONS = "exceptions"
RETURN = "return"
YIELD = "yield"
EXAMPLES = "examples"
ATTRIBUTES = "attributes"
KEYWORD_ARGS = "keyword_args"
__init__(self, section_type, value)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
section_type |
str |
The type of the section, from the |
required |
value |
Any |
The section value. |
required |
Source code in pytkdocs/parsers/docstrings/base.py
def __init__(self, section_type: str, value: Any) -> None:
"""
Initialize the object.
Arguments:
section_type: The type of the section, from the [`Type`][pytkdocs.parsers.docstrings.base.Section.Type] enum.
value: The section value.
"""
self.type = section_type
self.value = value
google
¤
This module defines functions and classes to parse docstrings into structured data.
RE_DOCTEST_BLANKLINE: Pattern
¤
Regular expression to match lines of the form <BLANKLINE>
.
RE_DOCTEST_FLAGS: Pattern
¤
Regular expression to match lines containing doctest flags of the form # doctest: +FLAG
.
RE_GOOGLE_STYLE_ADMONITION: Pattern
¤
Regular expressions to match lines starting admonitions, of the form TYPE: [TITLE]
.
Google (Parser)
¤
A Google-style docstrings parser.
Source code in pytkdocs/parsers/docstrings/google.py
class Google(Parser):
"""A Google-style docstrings parser."""
def __init__(self, replace_admonitions: bool = True, trim_doctest_flags: bool = True) -> None:
"""
Initialize the object.
Arguments:
replace_admonitions: Whether to replace admonitions by their Markdown equivalent.
trim_doctest_flags: Whether to remove doctest flags.
"""
super().__init__()
self.replace_admonitions = replace_admonitions
self.trim_doctest_flags = trim_doctest_flags
self.section_reader = {
Section.Type.PARAMETERS: self.read_parameters_section,
Section.Type.KEYWORD_ARGS: self.read_keyword_arguments_section,
Section.Type.EXCEPTIONS: self.read_exceptions_section,
Section.Type.EXAMPLES: self.read_examples_section,
Section.Type.ATTRIBUTES: self.read_attributes_section,
Section.Type.RETURN: self.read_return_section,
Section.Type.YIELD: self.read_yield_section,
}
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
if "signature" not in self.context:
self.context["signature"] = getattr(self.context["obj"], "signature", None)
if "annotation" not in self.context:
self.context["annotation"] = getattr(self.context["obj"], "type", empty)
if "attributes" not in self.context:
self.context["attributes"] = {}
sections = []
current_section = []
in_code_block = False
lines = docstring.split("\n")
i = 0
while i < len(lines):
line_lower = lines[i].lower()
if in_code_block:
if line_lower.lstrip(" ").startswith("```"):
in_code_block = False
current_section.append(lines[i])
elif line_lower in SECTIONS_TITLES:
if current_section:
if any(current_section):
sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section)))
current_section = []
section_reader = self.section_reader[SECTIONS_TITLES[line_lower]]
section, i = section_reader(lines, i + 1)
if section:
sections.append(section)
elif line_lower.lstrip(" ").startswith("```"):
in_code_block = True
current_section.append(lines[i])
else:
if self.replace_admonitions and not in_code_block and i + 1 < len(lines):
match = RE_GOOGLE_STYLE_ADMONITION.match(lines[i])
if match:
groups = match.groupdict()
indent = groups["indent"]
if lines[i + 1].startswith(indent + " " * 4):
lines[i] = f"{indent}!!! {groups['type'].lower()}"
if groups["title"]:
lines[i] += f' "{groups["title"]}"'
current_section.append(lines[i])
i += 1
if current_section:
sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section)))
return sections
def read_block_items(self, lines: List[str], start_index: int) -> Tuple[List[str], int]:
"""
Parse an indented block as a list of items.
The first indentation level is used as a reference to determine if the next lines are new items
or continuation lines.
Arguments:
lines: The block lines.
start_index: The line number to start at.
Returns:
A tuple containing the list of concatenated lines and the index at which to continue parsing.
"""
if start_index >= len(lines):
return [], start_index
i = start_index
items: List[str] = []
# skip first empty lines
while is_empty_line(lines[i]):
i += 1
# get initial indent
indent = len(lines[i]) - len(lines[i].lstrip())
if indent == 0:
# first non-empty line was not indented, abort
return [], i - 1
# start processing first item
current_item = [lines[i][indent:]]
i += 1
# loop on next lines
while i < len(lines):
line = lines[i]
if line.startswith(indent * 2 * " "):
# continuation line
current_item.append(line[indent * 2 :])
elif line.startswith((indent + 1) * " "):
# indent between initial and continuation: append but add error
cont_indent = len(line) - len(line.lstrip())
current_item.append(line[cont_indent:])
self.error(
f"Confusing indentation for continuation line {i+1} in docstring, "
f"should be {indent} * 2 = {indent*2} spaces, not {cont_indent}"
)
elif line.startswith(indent * " "):
# indent equal to initial one: new item
items.append("\n".join(current_item))
current_item = [line[indent:]]
elif is_empty_line(line):
# empty line: preserve it in the current item
current_item.append("")
else:
# indent lower than initial one: end of section
break
i += 1
if current_item:
items.append("\n".join(current_item).rstrip("\n"))
return items, i - 1
def read_block(self, lines: List[str], start_index: int) -> Tuple[str, int]:
"""
Parse an indented block.
Arguments:
lines: The block lines.
start_index: The line number to start at.
Returns:
A tuple containing the list of lines and the index at which to continue parsing.
"""
if start_index >= len(lines):
return "", start_index
i = start_index
block: List[str] = []
# skip first empty lines
while is_empty_line(lines[i]):
i += 1
# get initial indent
indent = len(lines[i]) - len(lines[i].lstrip())
if indent == 0:
# first non-empty line was not indented, abort
return "", i - 1
# start processing first item
block.append(lines[i].lstrip())
i += 1
# loop on next lines
while i < len(lines) and (lines[i].startswith(indent * " ") or is_empty_line(lines[i])):
block.append(lines[i][indent:])
i += 1
return "\n".join(block).rstrip("\n"), i - 1
def _parse_parameters_section(self, lines: List[str], start_index: int) -> Tuple[List[Parameter], int]:
"""
Parse a "parameters" or "keyword args" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters = []
type_: Any
block, i = self.read_block_items(lines, start_index)
for param_line in block:
# Check that there is an annotation in the docstring
try:
name_with_type, description = param_line.split(":", 1)
except ValueError:
self.error(f"Failed to get 'name: description' pair from '{param_line}'")
continue
# Setting defaults
default = empty
annotation = empty
kind = None
# Can only get description from docstring - keep if no type was given
description = description.lstrip()
# If we have managed to find a type in the docstring use this
if " " in name_with_type:
name, type_ = name_with_type.split(" ", 1)
annotation = type_.strip("()")
if annotation.endswith(", optional"): # type: ignore
annotation = annotation[:-10] # type: ignore
# Otherwise try to use the signature as `annotation` would still be empty
else:
name = name_with_type
# Check in the signature to get extra details
try:
signature_param = self.context["signature"].parameters[name.lstrip("*")]
except (AttributeError, KeyError):
if annotation is empty:
self.error(f"No type annotation for parameter '{name}'")
else:
if annotation is empty:
annotation = signature_param.annotation
# If signature_param.X are empty it doesnt matter as defaults are empty anyway
default = signature_param.default
kind = signature_param.kind
parameters.append(
Parameter(name=name, annotation=annotation, description=description, default=default, kind=kind)
)
return parameters, i
def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "parameters" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)
if parameters:
return Section(Section.Type.PARAMETERS, parameters), i
self.error(f"Empty parameters section at line {start_index}")
return None, i
def read_keyword_arguments_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "keyword arguments" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)
for parameter in parameters:
parameter.kind = inspect.Parameter.KEYWORD_ONLY
if parameters:
return Section(Section.Type.KEYWORD_ARGS, parameters), i
self.error(f"Empty keyword arguments section at line {start_index}")
return None, i
def read_attributes_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "attributes" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
attributes = []
block, i = self.read_block_items(lines, start_index)
for attr_line in block:
try:
name_with_type, description = attr_line.split(":", 1)
except ValueError:
self.error(f"Failed to get 'name: description' pair from '{attr_line}'")
continue
description = description.lstrip()
if " " in name_with_type:
name, annotation = name_with_type.split(" ", 1)
annotation = annotation.strip("()")
if annotation.endswith(", optional"):
annotation = annotation[:-10]
else:
name = name_with_type
annotation = self.context["attributes"].get(name, {}).get("annotation", empty)
attributes.append(Attribute(name=name, annotation=annotation, description=description))
if attributes:
return Section(Section.Type.ATTRIBUTES, attributes), i
self.error(f"Empty attributes section at line {start_index}")
return None, i
def read_exceptions_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "exceptions" section.
Arguments:
lines: The exceptions block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
exceptions = []
block, i = self.read_block_items(lines, start_index)
for exception_line in block:
try:
annotation, description = exception_line.split(": ", 1)
except ValueError:
self.error(f"Failed to get 'exception: description' pair from '{exception_line}'")
else:
exceptions.append(AnnotatedObject(annotation, description.lstrip(" ")))
if exceptions:
return Section(Section.Type.EXCEPTIONS, exceptions), i
self.error(f"Empty exceptions section at line {start_index}")
return None, i
def read_return_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "returns" section.
Arguments:
lines: The return block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
# Early exit if there is no text in the return section
if not text:
self.error(f"Empty return section at line {start_index}")
return None, i
# First try to get the annotation and description from the docstring
try:
type_, text = text.split(":", 1)
except ValueError:
description = text
annotation = self.context["annotation"]
# If there was no annotation in the docstring then move to signature
if annotation is empty and self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = type_.lstrip()
description = text.lstrip()
# There was no type in the docstring and no annotation
if annotation is empty:
self.error("No return type/annotation in docstring/signature")
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description)), i
def read_yield_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "yields" section.
Arguments:
lines: The return block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
# Early exit if there is no text in the yield section
if not text:
self.error(f"Empty yield section at line {start_index}")
return None, i
# First try to get the annotation and description from the docstring
try:
type_, text = text.split(":", 1)
except ValueError:
description = text
annotation = self.context["annotation"]
# If there was no annotation in the docstring then move to signature
if annotation is empty and self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = type_.lstrip()
description = text.lstrip()
# There was no type in the docstring and no annotation
if annotation is empty:
self.error("No yield type/annotation in docstring/signature")
return Section(Section.Type.YIELD, AnnotatedObject(annotation, description)), i
def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "examples" section.
Arguments:
lines: The examples block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
sub_sections = []
in_code_example = False
in_code_block = False
current_text: List[str] = []
current_example: List[str] = []
for line in text.split("\n"):
if is_empty_line(line):
if in_code_example:
if current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
current_example = []
in_code_example = False
else:
current_text.append(line)
elif in_code_example:
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
line = RE_DOCTEST_BLANKLINE.sub("", line)
current_example.append(line)
elif line.startswith("```"):
in_code_block = not in_code_block
current_text.append(line)
elif in_code_block:
current_text.append(line)
elif line.startswith(">>>"):
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
current_text = []
in_code_example = True
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
current_example.append(line)
else:
current_text.append(line)
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
elif current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
if sub_sections:
return Section(Section.Type.EXAMPLES, sub_sections), i
self.error(f"Empty examples section at line {start_index}")
return None, i
__init__(self, replace_admonitions=True, trim_doctest_flags=True)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
replace_admonitions |
bool |
Whether to replace admonitions by their Markdown equivalent. |
True |
trim_doctest_flags |
bool |
Whether to remove doctest flags. |
True |
Source code in pytkdocs/parsers/docstrings/google.py
def __init__(self, replace_admonitions: bool = True, trim_doctest_flags: bool = True) -> None:
"""
Initialize the object.
Arguments:
replace_admonitions: Whether to replace admonitions by their Markdown equivalent.
trim_doctest_flags: Whether to remove doctest flags.
"""
super().__init__()
self.replace_admonitions = replace_admonitions
self.trim_doctest_flags = trim_doctest_flags
self.section_reader = {
Section.Type.PARAMETERS: self.read_parameters_section,
Section.Type.KEYWORD_ARGS: self.read_keyword_arguments_section,
Section.Type.EXCEPTIONS: self.read_exceptions_section,
Section.Type.EXAMPLES: self.read_examples_section,
Section.Type.ATTRIBUTES: self.read_attributes_section,
Section.Type.RETURN: self.read_return_section,
Section.Type.YIELD: self.read_yield_section,
}
parse_sections(self, docstring)
¤
Parse a docstring as a list of sections.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
Returns:
Type | Description |
---|---|
List[pytkdocs.parsers.docstrings.base.Section] |
A list of |
Source code in pytkdocs/parsers/docstrings/google.py
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
if "signature" not in self.context:
self.context["signature"] = getattr(self.context["obj"], "signature", None)
if "annotation" not in self.context:
self.context["annotation"] = getattr(self.context["obj"], "type", empty)
if "attributes" not in self.context:
self.context["attributes"] = {}
sections = []
current_section = []
in_code_block = False
lines = docstring.split("\n")
i = 0
while i < len(lines):
line_lower = lines[i].lower()
if in_code_block:
if line_lower.lstrip(" ").startswith("```"):
in_code_block = False
current_section.append(lines[i])
elif line_lower in SECTIONS_TITLES:
if current_section:
if any(current_section):
sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section)))
current_section = []
section_reader = self.section_reader[SECTIONS_TITLES[line_lower]]
section, i = section_reader(lines, i + 1)
if section:
sections.append(section)
elif line_lower.lstrip(" ").startswith("```"):
in_code_block = True
current_section.append(lines[i])
else:
if self.replace_admonitions and not in_code_block and i + 1 < len(lines):
match = RE_GOOGLE_STYLE_ADMONITION.match(lines[i])
if match:
groups = match.groupdict()
indent = groups["indent"]
if lines[i + 1].startswith(indent + " " * 4):
lines[i] = f"{indent}!!! {groups['type'].lower()}"
if groups["title"]:
lines[i] += f' "{groups["title"]}"'
current_section.append(lines[i])
i += 1
if current_section:
sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section)))
return sections
read_attributes_section(self, lines, start_index)
¤
Parse an "attributes" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The parameters block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_attributes_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "attributes" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
attributes = []
block, i = self.read_block_items(lines, start_index)
for attr_line in block:
try:
name_with_type, description = attr_line.split(":", 1)
except ValueError:
self.error(f"Failed to get 'name: description' pair from '{attr_line}'")
continue
description = description.lstrip()
if " " in name_with_type:
name, annotation = name_with_type.split(" ", 1)
annotation = annotation.strip("()")
if annotation.endswith(", optional"):
annotation = annotation[:-10]
else:
name = name_with_type
annotation = self.context["attributes"].get(name, {}).get("annotation", empty)
attributes.append(Attribute(name=name, annotation=annotation, description=description))
if attributes:
return Section(Section.Type.ATTRIBUTES, attributes), i
self.error(f"Empty attributes section at line {start_index}")
return None, i
read_block(self, lines, start_index)
¤
Parse an indented block.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[str, int] |
A tuple containing the list of lines and the index at which to continue parsing. |
Source code in pytkdocs/parsers/docstrings/google.py
def read_block(self, lines: List[str], start_index: int) -> Tuple[str, int]:
"""
Parse an indented block.
Arguments:
lines: The block lines.
start_index: The line number to start at.
Returns:
A tuple containing the list of lines and the index at which to continue parsing.
"""
if start_index >= len(lines):
return "", start_index
i = start_index
block: List[str] = []
# skip first empty lines
while is_empty_line(lines[i]):
i += 1
# get initial indent
indent = len(lines[i]) - len(lines[i].lstrip())
if indent == 0:
# first non-empty line was not indented, abort
return "", i - 1
# start processing first item
block.append(lines[i].lstrip())
i += 1
# loop on next lines
while i < len(lines) and (lines[i].startswith(indent * " ") or is_empty_line(lines[i])):
block.append(lines[i][indent:])
i += 1
return "\n".join(block).rstrip("\n"), i - 1
read_block_items(self, lines, start_index)
¤
Parse an indented block as a list of items.
The first indentation level is used as a reference to determine if the next lines are new items or continuation lines.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[List[str], int] |
A tuple containing the list of concatenated lines and the index at which to continue parsing. |
Source code in pytkdocs/parsers/docstrings/google.py
def read_block_items(self, lines: List[str], start_index: int) -> Tuple[List[str], int]:
"""
Parse an indented block as a list of items.
The first indentation level is used as a reference to determine if the next lines are new items
or continuation lines.
Arguments:
lines: The block lines.
start_index: The line number to start at.
Returns:
A tuple containing the list of concatenated lines and the index at which to continue parsing.
"""
if start_index >= len(lines):
return [], start_index
i = start_index
items: List[str] = []
# skip first empty lines
while is_empty_line(lines[i]):
i += 1
# get initial indent
indent = len(lines[i]) - len(lines[i].lstrip())
if indent == 0:
# first non-empty line was not indented, abort
return [], i - 1
# start processing first item
current_item = [lines[i][indent:]]
i += 1
# loop on next lines
while i < len(lines):
line = lines[i]
if line.startswith(indent * 2 * " "):
# continuation line
current_item.append(line[indent * 2 :])
elif line.startswith((indent + 1) * " "):
# indent between initial and continuation: append but add error
cont_indent = len(line) - len(line.lstrip())
current_item.append(line[cont_indent:])
self.error(
f"Confusing indentation for continuation line {i+1} in docstring, "
f"should be {indent} * 2 = {indent*2} spaces, not {cont_indent}"
)
elif line.startswith(indent * " "):
# indent equal to initial one: new item
items.append("\n".join(current_item))
current_item = [line[indent:]]
elif is_empty_line(line):
# empty line: preserve it in the current item
current_item.append("")
else:
# indent lower than initial one: end of section
break
i += 1
if current_item:
items.append("\n".join(current_item).rstrip("\n"))
return items, i - 1
read_examples_section(self, lines, start_index)
¤
Parse an "examples" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The examples block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "examples" section.
Arguments:
lines: The examples block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
sub_sections = []
in_code_example = False
in_code_block = False
current_text: List[str] = []
current_example: List[str] = []
for line in text.split("\n"):
if is_empty_line(line):
if in_code_example:
if current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
current_example = []
in_code_example = False
else:
current_text.append(line)
elif in_code_example:
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
line = RE_DOCTEST_BLANKLINE.sub("", line)
current_example.append(line)
elif line.startswith("```"):
in_code_block = not in_code_block
current_text.append(line)
elif in_code_block:
current_text.append(line)
elif line.startswith(">>>"):
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
current_text = []
in_code_example = True
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
current_example.append(line)
else:
current_text.append(line)
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
elif current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
if sub_sections:
return Section(Section.Type.EXAMPLES, sub_sections), i
self.error(f"Empty examples section at line {start_index}")
return None, i
read_exceptions_section(self, lines, start_index)
¤
Parse an "exceptions" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The exceptions block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_exceptions_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "exceptions" section.
Arguments:
lines: The exceptions block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
exceptions = []
block, i = self.read_block_items(lines, start_index)
for exception_line in block:
try:
annotation, description = exception_line.split(": ", 1)
except ValueError:
self.error(f"Failed to get 'exception: description' pair from '{exception_line}'")
else:
exceptions.append(AnnotatedObject(annotation, description.lstrip(" ")))
if exceptions:
return Section(Section.Type.EXCEPTIONS, exceptions), i
self.error(f"Empty exceptions section at line {start_index}")
return None, i
read_keyword_arguments_section(self, lines, start_index)
¤
Parse a "keyword arguments" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The parameters block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_keyword_arguments_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "keyword arguments" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)
for parameter in parameters:
parameter.kind = inspect.Parameter.KEYWORD_ONLY
if parameters:
return Section(Section.Type.KEYWORD_ARGS, parameters), i
self.error(f"Empty keyword arguments section at line {start_index}")
return None, i
read_parameters_section(self, lines, start_index)
¤
Parse a "parameters" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The parameters block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "parameters" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)
if parameters:
return Section(Section.Type.PARAMETERS, parameters), i
self.error(f"Empty parameters section at line {start_index}")
return None, i
read_return_section(self, lines, start_index)
¤
Parse an "returns" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The return block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_return_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "returns" section.
Arguments:
lines: The return block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
# Early exit if there is no text in the return section
if not text:
self.error(f"Empty return section at line {start_index}")
return None, i
# First try to get the annotation and description from the docstring
try:
type_, text = text.split(":", 1)
except ValueError:
description = text
annotation = self.context["annotation"]
# If there was no annotation in the docstring then move to signature
if annotation is empty and self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = type_.lstrip()
description = text.lstrip()
# There was no type in the docstring and no annotation
if annotation is empty:
self.error("No return type/annotation in docstring/signature")
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description)), i
read_yield_section(self, lines, start_index)
¤
Parse a "yields" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lines |
List[str] |
The return block lines. |
required |
start_index |
int |
The line number to start at. |
required |
Returns:
Type | Description |
---|---|
Tuple[Optional[pytkdocs.parsers.docstrings.base.Section], int] |
A tuple containing a |
Source code in pytkdocs/parsers/docstrings/google.py
def read_yield_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "yields" section.
Arguments:
lines: The return block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
# Early exit if there is no text in the yield section
if not text:
self.error(f"Empty yield section at line {start_index}")
return None, i
# First try to get the annotation and description from the docstring
try:
type_, text = text.split(":", 1)
except ValueError:
description = text
annotation = self.context["annotation"]
# If there was no annotation in the docstring then move to signature
if annotation is empty and self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = type_.lstrip()
description = text.lstrip()
# There was no type in the docstring and no annotation
if annotation is empty:
self.error("No yield type/annotation in docstring/signature")
return Section(Section.Type.YIELD, AnnotatedObject(annotation, description)), i
is_empty_line(line)
¤
Tell if a line is empty.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
line |
The line to check. |
required |
Returns:
Type | Description |
---|---|
bool |
True if the line is empty or composed of blanks only, False otherwise. |
Source code in pytkdocs/parsers/docstrings/google.py
def is_empty_line(line) -> bool:
"""
Tell if a line is empty.
Arguments:
line: The line to check.
Returns:
True if the line is empty or composed of blanks only, False otherwise.
"""
return not line.strip()
markdown
¤
This module defines functions and classes to parse docstrings into structured data.
Markdown (Parser)
¤
A Markdown docstrings parser.
Source code in pytkdocs/parsers/docstrings/markdown.py
class Markdown(Parser):
"""A Markdown docstrings parser."""
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
return [Section(Section.Type.MARKDOWN, docstring)]
parse_sections(self, docstring)
¤
Parse a docstring as a list of sections.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
Returns:
Type | Description |
---|---|
List[pytkdocs.parsers.docstrings.base.Section] |
A list of |
Source code in pytkdocs/parsers/docstrings/markdown.py
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
return [Section(Section.Type.MARKDOWN, docstring)]
numpy
¤
This module defines functions and classes to parse docstrings into structured data.
RE_DOCTEST_BLANKLINE: Pattern
¤
Regular expression to match lines of the form <BLANKLINE>
.
RE_DOCTEST_FLAGS: Pattern
¤
Regular expression to match lines containing doctest flags of the form # doctest: +FLAG
.
Numpy (Parser)
¤
A Numpy-style docstrings parser.
Source code in pytkdocs/parsers/docstrings/numpy.py
class Numpy(Parser):
"""A Numpy-style docstrings parser."""
def __init__(self, trim_doctest_flags: bool = True) -> None:
"""
Initialize the objects.
Arguments:
trim_doctest_flags: Whether to remove doctest flags.
"""
super().__init__()
self.trim_doctest_flags = trim_doctest_flags
self.section_reader = {
Section.Type.PARAMETERS: self.read_parameters_section,
Section.Type.EXCEPTIONS: self.read_exceptions_section,
Section.Type.EXAMPLES: self.read_examples_section,
Section.Type.ATTRIBUTES: self.read_attributes_section,
Section.Type.RETURN: self.read_return_section,
}
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
if "signature" not in self.context:
self.context["signature"] = getattr(self.context["obj"], "signature", None)
if "annotation" not in self.context:
self.context["annotation"] = getattr(self.context["obj"], "type", empty)
if "attributes" not in self.context:
self.context["attributes"] = {}
docstring_obj = parse(docstring)
description_all = (
none_str_cast(docstring_obj.short_description) + "\n\n" + none_str_cast(docstring_obj.long_description)
).strip()
sections = [Section(Section.Type.MARKDOWN, description_all)] if description_all else []
sections_other = [
reader(docstring_obj) # type: ignore
if sec == Section.Type.RETURN
else reader(docstring, docstring_obj) # type: ignore
for (sec, reader) in self.section_reader.items()
]
sections.extend([sec for sec in sections_other if sec])
return sections
def read_parameters_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse a "parameters" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
parameters = []
docstring_params = [p for p in docstring_obj.params if p.args[0] == "param"]
for param in docstring_params:
name = param.arg_name
kind = None
type_name = param.type_name
default = param.default or empty
try:
signature_param = self.context["signature"].parameters[name.lstrip("*")]
except (AttributeError, KeyError):
self.error(f"No type annotation for parameter '{name}'")
else:
if signature_param.annotation is not empty:
type_name = signature_param.annotation
if signature_param.default is not empty:
default = signature_param.default
kind = signature_param.kind
description = param.description or ""
if not description:
self.error(f"No description for parameter '{name}'")
parameters.append(
Parameter(
name=param.arg_name,
annotation=type_name,
description=description,
default=default,
kind=kind,
)
)
if parameters:
return Section(Section.Type.PARAMETERS, parameters)
if re.search("Parameters\n", docstring):
self.error("Empty parameter section")
return None
def read_attributes_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "attributes" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
attributes = []
docstring_attributes = [p for p in docstring_obj.params if p.args[0] == "attribute"]
for attr in docstring_attributes:
description = attr.description or ""
if not description:
self.error(f"No description for attribute '{attr.arg_name}'")
attributes.append(
Attribute(
name=attr.arg_name,
annotation=attr.type_name,
description=attr.description,
)
)
if attributes:
return Section(Section.Type.ATTRIBUTES, attributes)
if re.search("Attributes\n", docstring):
self.error("Empty attributes section")
return None
def read_exceptions_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "exceptions" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
exceptions = []
except_obj = docstring_obj.raises
for exception in except_obj:
description = exception.description or ""
if not description:
self.error(f"No description for exception '{exception.type_name}'")
exceptions.append(AnnotatedObject(exception.type_name, description))
if exceptions:
return Section(Section.Type.EXCEPTIONS, exceptions)
if re.search("Raises\n", docstring):
self.error("Empty exceptions section")
return None
def read_return_section(
self,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse a "returns" section.
Arguments:
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
if docstring_obj.returns:
return_obj = docstring_obj.returns
if return_obj.description:
description = return_obj.description
else:
self.error("Empty return description")
description = ""
if self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = self.context["annotation"]
if annotation is empty and return_obj.type_name:
annotation = return_obj.type_name
if not annotation:
self.error("No return type annotation")
annotation = ""
if annotation or description:
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description))
return None
def read_examples_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "examples" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
text = next(
(
meta.description
for meta in docstring_obj.meta
if isinstance(meta, DocstringMeta) and meta.args[0] == "examples"
),
"",
)
sub_sections = []
in_code_example = False
in_code_block = False
current_text: List[str] = []
current_example: List[str] = []
if text:
for line in text.split("\n"):
if is_empty_line(line):
if in_code_example:
if current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
current_example = []
in_code_example = False
else:
current_text.append(line)
elif in_code_example:
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
line = RE_DOCTEST_BLANKLINE.sub("", line)
current_example.append(line)
elif line.startswith("```"):
in_code_block = not in_code_block
current_text.append(line)
elif in_code_block:
current_text.append(line)
elif line.startswith(">>>"):
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
current_text = []
in_code_example = True
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
current_example.append(line)
else:
current_text.append(line)
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
elif current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
if sub_sections:
return Section(Section.Type.EXAMPLES, sub_sections)
if re.search("Examples\n", docstring):
self.error("Empty examples section")
return None
__init__(self, trim_doctest_flags=True)
special
¤
Initialize the objects.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
trim_doctest_flags |
bool |
Whether to remove doctest flags. |
True |
Source code in pytkdocs/parsers/docstrings/numpy.py
def __init__(self, trim_doctest_flags: bool = True) -> None:
"""
Initialize the objects.
Arguments:
trim_doctest_flags: Whether to remove doctest flags.
"""
super().__init__()
self.trim_doctest_flags = trim_doctest_flags
self.section_reader = {
Section.Type.PARAMETERS: self.read_parameters_section,
Section.Type.EXCEPTIONS: self.read_exceptions_section,
Section.Type.EXAMPLES: self.read_examples_section,
Section.Type.ATTRIBUTES: self.read_attributes_section,
Section.Type.RETURN: self.read_return_section,
}
parse_sections(self, docstring)
¤
Parse a docstring as a list of sections.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
Returns:
Type | Description |
---|---|
List[pytkdocs.parsers.docstrings.base.Section] |
A list of |
Source code in pytkdocs/parsers/docstrings/numpy.py
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
if "signature" not in self.context:
self.context["signature"] = getattr(self.context["obj"], "signature", None)
if "annotation" not in self.context:
self.context["annotation"] = getattr(self.context["obj"], "type", empty)
if "attributes" not in self.context:
self.context["attributes"] = {}
docstring_obj = parse(docstring)
description_all = (
none_str_cast(docstring_obj.short_description) + "\n\n" + none_str_cast(docstring_obj.long_description)
).strip()
sections = [Section(Section.Type.MARKDOWN, description_all)] if description_all else []
sections_other = [
reader(docstring_obj) # type: ignore
if sec == Section.Type.RETURN
else reader(docstring, docstring_obj) # type: ignore
for (sec, reader) in self.section_reader.items()
]
sections.extend([sec for sec in sections_other if sec])
return sections
read_attributes_section(self, docstring, docstring_obj)
¤
Parse an "attributes" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The raw docstring. |
required |
docstring_obj |
Docstring |
Docstring object parsed by docstring_parser. |
required |
Returns:
Type | Description |
---|---|
Optional[pytkdocs.parsers.docstrings.base.Section] |
A |
Source code in pytkdocs/parsers/docstrings/numpy.py
def read_attributes_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "attributes" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
attributes = []
docstring_attributes = [p for p in docstring_obj.params if p.args[0] == "attribute"]
for attr in docstring_attributes:
description = attr.description or ""
if not description:
self.error(f"No description for attribute '{attr.arg_name}'")
attributes.append(
Attribute(
name=attr.arg_name,
annotation=attr.type_name,
description=attr.description,
)
)
if attributes:
return Section(Section.Type.ATTRIBUTES, attributes)
if re.search("Attributes\n", docstring):
self.error("Empty attributes section")
return None
read_examples_section(self, docstring, docstring_obj)
¤
Parse an "examples" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The raw docstring. |
required |
docstring_obj |
Docstring |
Docstring object parsed by docstring_parser. |
required |
Returns:
Type | Description |
---|---|
Optional[pytkdocs.parsers.docstrings.base.Section] |
A |
Source code in pytkdocs/parsers/docstrings/numpy.py
def read_examples_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "examples" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
text = next(
(
meta.description
for meta in docstring_obj.meta
if isinstance(meta, DocstringMeta) and meta.args[0] == "examples"
),
"",
)
sub_sections = []
in_code_example = False
in_code_block = False
current_text: List[str] = []
current_example: List[str] = []
if text:
for line in text.split("\n"):
if is_empty_line(line):
if in_code_example:
if current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
current_example = []
in_code_example = False
else:
current_text.append(line)
elif in_code_example:
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
line = RE_DOCTEST_BLANKLINE.sub("", line)
current_example.append(line)
elif line.startswith("```"):
in_code_block = not in_code_block
current_text.append(line)
elif in_code_block:
current_text.append(line)
elif line.startswith(">>>"):
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
current_text = []
in_code_example = True
if self.trim_doctest_flags:
line = RE_DOCTEST_FLAGS.sub("", line)
current_example.append(line)
else:
current_text.append(line)
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
elif current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
if sub_sections:
return Section(Section.Type.EXAMPLES, sub_sections)
if re.search("Examples\n", docstring):
self.error("Empty examples section")
return None
read_exceptions_section(self, docstring, docstring_obj)
¤
Parse an "exceptions" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The raw docstring. |
required |
docstring_obj |
Docstring |
Docstring object parsed by docstring_parser. |
required |
Returns:
Type | Description |
---|---|
Optional[pytkdocs.parsers.docstrings.base.Section] |
A |
Source code in pytkdocs/parsers/docstrings/numpy.py
def read_exceptions_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse an "exceptions" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
exceptions = []
except_obj = docstring_obj.raises
for exception in except_obj:
description = exception.description or ""
if not description:
self.error(f"No description for exception '{exception.type_name}'")
exceptions.append(AnnotatedObject(exception.type_name, description))
if exceptions:
return Section(Section.Type.EXCEPTIONS, exceptions)
if re.search("Raises\n", docstring):
self.error("Empty exceptions section")
return None
read_parameters_section(self, docstring, docstring_obj)
¤
Parse a "parameters" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The raw docstring. |
required |
docstring_obj |
Docstring |
Docstring object parsed by docstring_parser. |
required |
Returns:
Type | Description |
---|---|
Optional[pytkdocs.parsers.docstrings.base.Section] |
A |
Source code in pytkdocs/parsers/docstrings/numpy.py
def read_parameters_section(
self,
docstring: str,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse a "parameters" section.
Arguments:
docstring: The raw docstring.
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
parameters = []
docstring_params = [p for p in docstring_obj.params if p.args[0] == "param"]
for param in docstring_params:
name = param.arg_name
kind = None
type_name = param.type_name
default = param.default or empty
try:
signature_param = self.context["signature"].parameters[name.lstrip("*")]
except (AttributeError, KeyError):
self.error(f"No type annotation for parameter '{name}'")
else:
if signature_param.annotation is not empty:
type_name = signature_param.annotation
if signature_param.default is not empty:
default = signature_param.default
kind = signature_param.kind
description = param.description or ""
if not description:
self.error(f"No description for parameter '{name}'")
parameters.append(
Parameter(
name=param.arg_name,
annotation=type_name,
description=description,
default=default,
kind=kind,
)
)
if parameters:
return Section(Section.Type.PARAMETERS, parameters)
if re.search("Parameters\n", docstring):
self.error("Empty parameter section")
return None
read_return_section(self, docstring_obj)
¤
Parse a "returns" section.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring_obj |
Docstring |
Docstring object parsed by docstring_parser. |
required |
Returns:
Type | Description |
---|---|
Optional[pytkdocs.parsers.docstrings.base.Section] |
A |
Source code in pytkdocs/parsers/docstrings/numpy.py
def read_return_section(
self,
docstring_obj: Docstring,
) -> Optional[Section]:
"""
Parse a "returns" section.
Arguments:
docstring_obj: Docstring object parsed by docstring_parser.
Returns:
A `Section` object (or `None` if section is empty).
"""
if docstring_obj.returns:
return_obj = docstring_obj.returns
if return_obj.description:
description = return_obj.description
else:
self.error("Empty return description")
description = ""
if self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = self.context["annotation"]
if annotation is empty and return_obj.type_name:
annotation = return_obj.type_name
if not annotation:
self.error("No return type annotation")
annotation = ""
if annotation or description:
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description))
return None
is_empty_line(line)
¤
Tell if a line is empty.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
line |
str |
The line to check. |
required |
Returns:
Type | Description |
---|---|
bool |
True if the line is empty or composed of blanks only, False otherwise. |
Source code in pytkdocs/parsers/docstrings/numpy.py
def is_empty_line(line: str) -> bool:
"""
Tell if a line is empty.
Arguments:
line: The line to check.
Returns:
True if the line is empty or composed of blanks only, False otherwise.
"""
return not line.strip()
restructured_text
¤
This module defines functions and classes to parse docstrings into structured data.
AttributesDict (dict)
¤
Attribute details.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
class AttributesDict(TypedDict):
"""Attribute details."""
docstring: str
annotation: Type # TODO: Not positive this is correct
FieldType
dataclass
¤
Maps directive names to parser functions.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
@dataclass(frozen=True)
class FieldType:
"""Maps directive names to parser functions."""
names: FrozenSet[str]
reader: Callable[[List[str], int], int]
def matches(self, line: str) -> bool:
"""
Check if a line matches the field type.
Args:
line: Line to check against
Returns:
True if the line matches the field type, False otherwise.
"""
return any(line.startswith(f":{name}") for name in self.names)
matches(self, line)
¤
Check if a line matches the field type.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
line |
str |
Line to check against |
required |
Returns:
Type | Description |
---|---|
bool |
True if the line matches the field type, False otherwise. |
Source code in pytkdocs/parsers/docstrings/restructured_text.py
def matches(self, line: str) -> bool:
"""
Check if a line matches the field type.
Args:
line: Line to check against
Returns:
True if the line matches the field type, False otherwise.
"""
return any(line.startswith(f":{name}") for name in self.names)
ParseContext
¤
Typed replacement for context dictionary.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
class ParseContext:
"""Typed replacement for context dictionary."""
obj: Any # I think this might be pytkdos.Object & subclasses
attributes: DefaultDict[str, AttributesDict]
signature: Optional[Signature]
# Not sure real type yet. Maybe Optional[Union[Literal[Signature.empty],str,Type]]
annotation: Any
# This might be be better as the obj & optional attributes
def __init__(self, context: Dict):
"""
Initialize the object.
Args:
context: Context of parsing operation.
"""
self.obj = context["obj"]
self.attributes = defaultdict(cast(Callable[[], AttributesDict], dict))
attributes = context.get("attributes")
if attributes is not None:
self.attributes.update(attributes)
self.signature = getattr(self.obj, "signature", None)
self.annotation = getattr(self.obj, "type", empty)
__init__(self, context)
special
¤
Initialize the object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
context |
Dict |
Context of parsing operation. |
required |
Source code in pytkdocs/parsers/docstrings/restructured_text.py
def __init__(self, context: Dict):
"""
Initialize the object.
Args:
context: Context of parsing operation.
"""
self.obj = context["obj"]
self.attributes = defaultdict(cast(Callable[[], AttributesDict], dict))
attributes = context.get("attributes")
if attributes is not None:
self.attributes.update(attributes)
self.signature = getattr(self.obj, "signature", None)
self.annotation = getattr(self.obj, "type", empty)
ParsedDirective
dataclass
¤
Directive information that has been parsed from a docstring.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
@dataclass
class ParsedDirective:
"""Directive information that has been parsed from a docstring."""
line: str
next_index: int
directive_parts: List[str]
value: str
invalid: bool = False
ParsedValues
dataclass
¤
Values parsed from the docstring to be used to produce sections.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
@dataclass
class ParsedValues:
"""Values parsed from the docstring to be used to produce sections."""
description: List[str] = field(default_factory=list)
parameters: Dict[str, Parameter] = field(default_factory=dict)
param_types: Dict[str, str] = field(default_factory=dict)
attributes: Dict[str, Attribute] = field(default_factory=dict)
attribute_types: Dict[str, str] = field(default_factory=dict)
exceptions: List[AnnotatedObject] = field(default_factory=list)
return_value: Optional[AnnotatedObject] = None
return_type: Optional[str] = None
RestructuredText (Parser)
¤
A reStructuredText docstrings parser.
Source code in pytkdocs/parsers/docstrings/restructured_text.py
class RestructuredText(Parser):
"""A reStructuredText docstrings parser."""
def __init__(self) -> None:
"""Initialize the object."""
super().__init__()
self._typed_context = ParseContext({"obj": None})
self._parsed_values: ParsedValues = ParsedValues()
# Ordering is significant so that directives like ":vartype" are checked before ":var"
self.field_types = [
FieldType(PARAM_TYPE_NAMES, self._read_parameter_type),
FieldType(PARAM_NAMES, self._read_parameter),
FieldType(ATTRIBUTE_TYPE_NAMES, self._read_attribute_type),
FieldType(ATTRIBUTE_NAMES, self._read_attribute),
FieldType(EXCEPTION_NAMES, self._read_exception),
FieldType(RETURN_NAMES, self._read_return),
FieldType(RETURN_TYPE_NAMES, self._read_return_type),
]
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
self._typed_context = ParseContext(self.context)
self._parsed_values = ParsedValues()
lines = docstring.split("\n")
curr_line_index = 0
while curr_line_index < len(lines):
line = lines[curr_line_index]
for field_type in self.field_types:
if field_type.matches(line):
# https://github.com/python/mypy/issues/5485
curr_line_index = field_type.reader(lines, curr_line_index) # type: ignore
break
else:
self._parsed_values.description.append(line)
curr_line_index += 1
return self._parsed_values_to_sections()
def _read_parameter(self, lines: List[str], start_index: int) -> int:
"""
Parse a parameter value.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
directive_type = None
if len(parsed_directive.directive_parts) == 2:
# no type info
name = parsed_directive.directive_parts[1]
elif len(parsed_directive.directive_parts) == 3:
directive_type = parsed_directive.directive_parts[1]
name = parsed_directive.directive_parts[2]
else:
self.error(f"Failed to parse field directive from '{parsed_directive.line}'")
return parsed_directive.next_index
if name in self._parsed_values.parameters:
self.errors.append(f"Duplicate parameter entry for '{name}'")
return parsed_directive.next_index
annotation = self._determine_param_annotation(name, directive_type)
default, kind = self._determine_param_details(name)
self._parsed_values.parameters[name] = Parameter(
name=name,
annotation=annotation,
description=parsed_directive.value,
default=default,
kind=kind,
)
return parsed_directive.next_index
def _determine_param_details(self, name: str) -> Tuple[Any, Any]:
default = empty
kind = empty
if self._typed_context.signature is not None:
param_signature = self._typed_context.signature.parameters.get(name.lstrip("*"))
# an error for param_signature being none is already reported by _determine_param_annotation()
if param_signature is not None:
if param_signature.default is not empty:
default = param_signature.default
kind = param_signature.kind # type: ignore[assignment]
return default, kind
def _determine_param_annotation(self, name: str, directive_type: Optional[str]) -> Any:
# Annotation precedence:
# - signature annotation
# - in-line directive type
# - "type" directive type
# - empty
annotation = empty
parsed_param_type = self._parsed_values.param_types.get(name)
if parsed_param_type is not None:
annotation = parsed_param_type # type: ignore[assignment]
if directive_type is not None:
annotation = directive_type # type: ignore[assignment]
if directive_type is not None and parsed_param_type is not None:
self.error(f"Duplicate parameter information for '{name}'")
if self._typed_context.signature is not None:
try:
param_signature = self._typed_context.signature.parameters[name.lstrip("*")]
except KeyError:
self.error(f"No matching parameter for '{name}'")
else:
if param_signature.annotation is not empty:
annotation = param_signature.annotation
return annotation
def _read_parameter_type(self, lines: List[str], start_index: int) -> int:
"""
Parse a parameter type.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
param_type = _consolidate_descriptive_type(parsed_directive.value.strip())
if len(parsed_directive.directive_parts) == 2:
param_name = parsed_directive.directive_parts[1]
else:
self.error(f"Failed to get parameter name from '{parsed_directive.line}'")
return parsed_directive.next_index
self._parsed_values.param_types[param_name] = param_type
param = self._parsed_values.parameters.get(param_name)
if param is not None:
if param.annotation is empty:
param.annotation = param_type
else:
self.error(f"Duplicate parameter information for '{param_name}'")
return parsed_directive.next_index
def _read_attribute(self, lines: List[str], start_index: int) -> int:
"""
Parse an attribute value.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
if len(parsed_directive.directive_parts) == 2:
name = parsed_directive.directive_parts[1]
else:
self.error(f"Failed to parse field directive from '{parsed_directive.line}'")
return parsed_directive.next_index
annotation = empty
# Annotation precedence:
# - external context type TODO: spend time understanding where this comes from
# - "vartype" directive type
# - empty
parsed_attribute_type = self._parsed_values.attribute_types.get(name)
if parsed_attribute_type is not None:
annotation = parsed_attribute_type # type: ignore[assignment]
context_attribute_annotation = self._typed_context.attributes[name].get("annotation")
if context_attribute_annotation is not None:
annotation = context_attribute_annotation
if name in self._parsed_values.attributes:
self.errors.append(f"Duplicate attribute entry for '{name}'")
else:
self._parsed_values.attributes[name] = Attribute(
name=name,
annotation=annotation,
description=parsed_directive.value,
)
return parsed_directive.next_index
def _read_attribute_type(self, lines: List[str], start_index: int) -> int:
"""
Parse a parameter type.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
attribute_type = _consolidate_descriptive_type(parsed_directive.value.strip())
if len(parsed_directive.directive_parts) == 2:
attribute_name = parsed_directive.directive_parts[1]
else:
self.error(f"Failed to get attribute name from '{parsed_directive.line}'")
return parsed_directive.next_index
self._parsed_values.attribute_types[attribute_name] = attribute_type
attribute = self._parsed_values.attributes.get(attribute_name)
if attribute is not None:
if attribute.annotation is empty:
attribute.annotation = attribute_type
else:
self.error(f"Duplicate attribute information for '{attribute_name}'")
return parsed_directive.next_index
def _read_exception(self, lines: List[str], start_index: int) -> int:
"""
Parse an exceptions value.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
if len(parsed_directive.directive_parts) == 2:
ex_type = parsed_directive.directive_parts[1]
self._parsed_values.exceptions.append(AnnotatedObject(ex_type, parsed_directive.value))
else:
self.error(f"Failed to parse exception directive from '{parsed_directive.line}'")
return parsed_directive.next_index
def _read_return(self, lines: List[str], start_index: int) -> int:
"""
Parse an return value.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
annotation = empty
# Annotation precedence:
# - signature annotation
# - "rtype" directive type
# - external context type TODO: spend time understanding where this comes from
# - empty
if self._typed_context.signature is not None and self._typed_context.signature.return_annotation is not empty:
annotation = self._typed_context.signature.return_annotation
elif self._parsed_values.return_type is not None:
annotation = self._parsed_values.return_type # type: ignore[assignment]
else:
annotation = self._typed_context.annotation
self._parsed_values.return_value = AnnotatedObject(annotation, parsed_directive.value)
return parsed_directive.next_index
def _read_return_type(self, lines: List[str], start_index: int) -> int:
"""
Parse an return type value.
Arguments:
lines: The docstring lines.
start_index: The line number to start at.
Returns:
Index at which to continue parsing.
"""
parsed_directive = self._parse_directive(lines, start_index)
if parsed_directive.invalid:
return parsed_directive.next_index
return_type = _consolidate_descriptive_type(parsed_directive.value.strip())
self._parsed_values.return_type = return_type
return_value = self._parsed_values.return_value
if return_value is not None:
if return_value.annotation is empty:
return_value.annotation = return_type
else:
self.error("Duplicate type information for return")
return parsed_directive.next_index
def _parsed_values_to_sections(self) -> List[Section]:
markdown_text = "\n".join(_strip_blank_lines(self._parsed_values.description))
result = [Section(Section.Type.MARKDOWN, markdown_text)]
if self._parsed_values.parameters:
param_values = list(self._parsed_values.parameters.values())
result.append(Section(Section.Type.PARAMETERS, param_values))
if self._parsed_values.attributes:
attribute_values = list(self._parsed_values.attributes.values())
result.append(Section(Section.Type.ATTRIBUTES, attribute_values))
if self._parsed_values.return_value is not None:
result.append(Section(Section.Type.RETURN, self._parsed_values.return_value))
if self._parsed_values.exceptions:
result.append(Section(Section.Type.EXCEPTIONS, self._parsed_values.exceptions))
return result
def _parse_directive(self, lines: List[str], start_index: int) -> ParsedDirective:
line, next_index = _consolidate_continuation_lines(lines, start_index)
try:
_, directive, value = line.split(":", 2)
except ValueError:
self.error(f"Failed to get ':directive: value' pair from '{line}'")
return ParsedDirective(line, next_index, [], "", invalid=True)
value = value.strip()
return ParsedDirective(line, next_index, directive.split(" "), value)
parse_sections(self, docstring)
¤
Parse a docstring as a list of sections.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
docstring |
str |
The docstring to parse. |
required |
Returns:
Type | Description |
---|---|
List[pytkdocs.parsers.docstrings.base.Section] |
A list of |
Source code in pytkdocs/parsers/docstrings/restructured_text.py
def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
self._typed_context = ParseContext(self.context)
self._parsed_values = ParsedValues()
lines = docstring.split("\n")
curr_line_index = 0
while curr_line_index < len(lines):
line = lines[curr_line_index]
for field_type in self.field_types:
if field_type.matches(line):
# https://github.com/python/mypy/issues/5485
curr_line_index = field_type.reader(lines, curr_line_index) # type: ignore
break
else:
self._parsed_values.description.append(line)
curr_line_index += 1
return self._parsed_values_to_sections()
properties
¤
This module simply defines regular expressions and their associated predicates.
NAME_CLASS_PRIVATE: Tuple[str, Callable[[str], bool]]
¤
Applicable property: class-private
.
NAME_PRIVATE: Tuple[str, Callable[[str], bool]]
¤
Applicable property: private
.
NAME_SPECIAL: Tuple[str, Callable[[str], bool]]
¤
Applicable property: special
.
RE_CLASS_PRIVATE: Pattern
¤
Regular expression to match __class_private
names.
RE_PRIVATE: Pattern
¤
Regular expression to match _private
names.
RE_SPECIAL: Pattern
¤
Regular expression to match __special__
names.
serializer
¤
This module defines function to serialize objects.
These functions simply take objects as parameters and return dictionaries that can be dumped by json.dumps
.
RE_FORWARD_REF: Pattern
¤
Regular expression to match forward-reference annotations of the form _ForwardRef('T')
.
RE_OPTIONAL: Pattern
¤
Regular expression to match optional annotations of the form Union[T, NoneType]
.
GenericMeta (type)
¤
GenericMeta type.
Source code in pytkdocs/serializer.py
class GenericMeta(type): # type: ignore # noqa: WPS440 (variable overlap)
"""GenericMeta type."""
annotation_to_string(annotation)
¤
Return an annotation as a string.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
annotation |
Any |
The annotation to return as a string. |
required |
Returns:
Type | Description |
---|---|
str |
The annotation as a string. |
Source code in pytkdocs/serializer.py
def annotation_to_string(annotation: Any) -> str:
"""
Return an annotation as a string.
Arguments:
annotation: The annotation to return as a string.
Returns:
The annotation as a string.
"""
if annotation is inspect.Signature.empty:
return ""
if inspect.isclass(annotation) and not isinstance(annotation, GenericMeta):
string = annotation.__name__
else:
string = str(annotation).replace("typing.", "")
string = RE_FORWARD_REF.sub(lambda match: match.group(1), string)
string = RE_OPTIONAL.sub(rebuild_optional, string)
return string # noqa: WPS331 (false-positive, string is not only used for the return)
rebuild_optional(match)
¤
Rebuild Union[T, None]
as Optional[T]
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
match |
Match |
The match object when matching against a regular expression (by the parent caller). |
required |
Returns:
Type | Description |
---|---|
str |
The rebuilt type string. |
Source code in pytkdocs/serializer.py
def rebuild_optional(match: Match) -> str:
"""
Rebuild `Union[T, None]` as `Optional[T]`.
Arguments:
match: The match object when matching against a regular expression (by the parent caller).
Returns:
The rebuilt type string.
"""
group = match.group(1)
brackets_level = 0
for char in group:
if char == "," and brackets_level == 0:
return f"Union[{group}]"
if char == "[":
brackets_level += 1
elif char == "]":
brackets_level -= 1
return f"Optional[{group}]"
serialize_annotated_object(obj)
¤
Serialize an instance of AnnotatedObject
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
obj |
AnnotatedObject |
The object to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_annotated_object(obj: AnnotatedObject) -> dict:
"""
Serialize an instance of [`AnnotatedObject`][pytkdocs.parsers.docstrings.base.AnnotatedObject].
Arguments:
obj: The object to serialize.
Returns:
A JSON-serializable dictionary.
"""
return {"description": obj.description, "annotation": annotation_to_string(obj.annotation)}
serialize_attribute(attribute)
¤
Serialize an instance of Attribute
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
attribute |
Attribute |
The attribute to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_attribute(attribute: Attribute) -> dict:
"""
Serialize an instance of [`Attribute`][pytkdocs.parsers.docstrings.base.Attribute].
Arguments:
attribute: The attribute to serialize.
Returns:
A JSON-serializable dictionary.
"""
return {
"name": attribute.name,
"description": attribute.description,
"annotation": annotation_to_string(attribute.annotation),
}
serialize_docstring_section(section)
¤
Serialize an instance of inspect.Signature
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
section |
Section |
The section to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_docstring_section(section: Section) -> dict: # noqa: WPS231 (not complex)
"""
Serialize an instance of `inspect.Signature`.
Arguments:
section: The section to serialize.
Returns:
A JSON-serializable dictionary.
"""
serialized = {"type": section.type}
if section.type == section.Type.MARKDOWN:
serialized.update({"value": section.value})
elif section.type == section.Type.RETURN:
serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore
elif section.type == section.Type.YIELD:
serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore
elif section.type == section.Type.EXCEPTIONS:
serialized.update({"value": [serialize_annotated_object(exc) for exc in section.value]}) # type: ignore
elif section.type == section.Type.PARAMETERS:
serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore
elif section.type == section.Type.KEYWORD_ARGS:
serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore
elif section.type == section.Type.ATTRIBUTES:
serialized.update({"value": [serialize_attribute(attr) for attr in section.value]}) # type: ignore
elif section.type == section.Type.EXAMPLES:
serialized.update({"value": section.value})
return serialized
serialize_object(obj)
¤
Serialize an instance of a subclass of Object
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
obj |
Object |
The object to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_object(obj: Object) -> dict:
"""
Serialize an instance of a subclass of [`Object`][pytkdocs.objects.Object].
Arguments:
obj: The object to serialize.
Returns:
A JSON-serializable dictionary.
"""
serialized = {
"name": obj.name,
"path": obj.path,
"category": obj.category,
"file_path": obj.file_path,
"relative_file_path": obj.relative_file_path,
"properties": sorted(set(obj.properties + obj.name_properties)),
"parent_path": obj.parent_path,
"has_contents": obj.has_contents(),
"docstring": obj.docstring,
"docstring_sections": [serialize_docstring_section(sec) for sec in obj.docstring_sections],
"source": serialize_source(obj.source),
"children": {child.path: serialize_object(child) for child in obj.children},
"attributes": [attr.path for attr in obj.attributes],
"methods": [meth.path for meth in obj.methods],
"functions": [func.path for func in obj.functions],
"modules": [mod.path for mod in obj.modules],
"classes": [clas.path for clas in obj.classes],
}
if hasattr(obj, "type"): # noqa: WPS421 (hasattr)
serialized["type"] = annotation_to_string(obj.type) # type: ignore
if hasattr(obj, "signature"): # noqa: WPS421 (hasattr)
serialized["signature"] = serialize_signature(obj.signature) # type: ignore
if hasattr(obj, "bases"): # noqa: WPS421 (hasattr)
serialized["bases"] = obj.bases # type: ignore
return serialized
serialize_parameter(parameter)
¤
Serialize an instance of Parameter
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
parameter |
Parameter |
The parameter to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_parameter(parameter: Parameter) -> dict:
"""
Serialize an instance of [`Parameter`][pytkdocs.parsers.docstrings.base.Parameter].
Arguments:
parameter: The parameter to serialize.
Returns:
A JSON-serializable dictionary.
"""
serialized = serialize_annotated_object(parameter)
serialized.update(
{
"name": parameter.name,
"kind": str(parameter.kind),
"default": parameter.default_string,
"is_optional": parameter.is_optional,
"is_required": parameter.is_required,
"is_args": parameter.is_args,
"is_kwargs": parameter.is_kwargs,
},
)
return serialized
serialize_signature(signature)
¤
Serialize an instance of inspect.Signature
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
signature |
Signature |
The signature to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_signature(signature: inspect.Signature) -> dict:
"""
Serialize an instance of `inspect.Signature`.
Arguments:
signature: The signature to serialize.
Returns:
A JSON-serializable dictionary.
"""
if signature is None:
return {}
serialized: dict = {
"parameters": [serialize_signature_parameter(value) for name, value in signature.parameters.items()],
}
if signature.return_annotation is not inspect.Signature.empty:
serialized["return_annotation"] = annotation_to_string(signature.return_annotation)
return serialized
serialize_signature_parameter(parameter)
¤
Serialize an instance of inspect.Parameter
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
parameter |
Parameter |
The parameter to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_signature_parameter(parameter: inspect.Parameter) -> dict:
"""
Serialize an instance of `inspect.Parameter`.
Arguments:
parameter: The parameter to serialize.
Returns:
A JSON-serializable dictionary.
"""
serialized = {"kind": str(parameter.kind), "name": parameter.name}
if parameter.annotation is not parameter.empty:
serialized["annotation"] = annotation_to_string(parameter.annotation)
if parameter.default is not parameter.empty:
serialized["default"] = repr(parameter.default)
return serialized
serialize_source(source)
¤
Serialize an instance of Source
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source |
Optional[pytkdocs.objects.Source] |
The source to serialize. |
required |
Returns:
Type | Description |
---|---|
dict |
A JSON-serializable dictionary. |
Source code in pytkdocs/serializer.py
def serialize_source(source: Optional[Source]) -> dict:
"""
Serialize an instance of [`Source`][pytkdocs.objects.Source].
Arguments:
source: The source to serialize.
Returns:
A JSON-serializable dictionary.
"""
if source:
return {"code": source.code, "line_start": source.line_start}
return {}