Skip to content

Commit

Permalink
Initial commit for plugin internals refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Schamper committed Aug 8, 2024
1 parent 22d1ca8 commit 85f4ebc
Show file tree
Hide file tree
Showing 43 changed files with 1,743 additions and 1,625 deletions.
132 changes: 69 additions & 63 deletions dissect/target/helpers/docs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import inspect
import itertools
import textwrap
from typing import Any, Callable, Tuple, Type
from typing import Any, Callable

from dissect.target.plugin import Plugin

NO_DOCS = "No documentation"

Expand All @@ -14,76 +16,23 @@

INDENT_STEP = " " * 4


def get_plugin_class_for_func(func: Callable) -> Type:
"""Return pluging class for provided function instance"""
func_parent_name = func.__qualname__.rsplit(".", 1)[0]
klass = getattr(inspect.getmodule(func), func_parent_name, None)
return klass


def get_real_func_obj(func: Callable) -> Tuple[Type, Callable]:
"""Return a tuple with plugin class and underlying func object for provided function instance"""
klass = None

if isinstance(func, property):
# turn property into function
func = func.fget

if inspect.ismethod(func):
for klass in inspect.getmro(func.__self__.__class__):
if func.__name__ in klass.__dict__:
break
else:
func = getattr(func, "__func__", func)

if inspect.isfunction(func):
klass = get_plugin_class_for_func(func)

if not klass:
raise ValueError(f"Can't find class for {func}")

return (klass, func)
FUNC_DOC_TEMPLATE = "{func_name} - {short_description} (output: {output_type})"


def get_docstring(obj: Any, placeholder=NO_DOCS) -> str:
"""Get object's docstring or a placeholder if no docstring found"""
"""Get object's docstring or a placeholder if no docstring found."""
# Use of `inspect.cleandoc()` is preferred to `textwrap.dedent()` here
# because many multi-line docstrings in the codebase
# have no indentation in the first line, which confuses `dedent()`
return inspect.cleandoc(obj.__doc__) if obj.__doc__ else placeholder


def get_func_details(func: Callable) -> Tuple[str, str]:
"""Return a tuple with function's name, output label and docstring"""
func_doc = get_docstring(func)

if hasattr(func, "__output__") and func.__output__ in FUNCTION_OUTPUT_DESCRIPTION:
func_output = FUNCTION_OUTPUT_DESCRIPTION[func.__output__]
else:
func_output = "unknown"

return (func_output, func_doc)


def get_full_func_name(plugin_class: Type, func: Callable) -> str:
func_name = func.__name__

if hasattr(plugin_class, "__namespace__") and plugin_class.__namespace__:
func_name = f"{plugin_class.__namespace__}.{func_name}"

return func_name


FUNC_DOC_TEMPLATE = "{func_name} - {short_description} (output: {output_type})"


def get_func_description(func: Callable, with_docstrings: bool = False) -> str:
klass, func = get_real_func_obj(func)
func_output, func_doc = get_func_details(func)
klass, func = _get_real_func_obj(func)
func_output, func_doc = _get_func_details(func)

# get user-friendly function name
func_name = get_full_func_name(klass, func)
func_name = _get_full_func_name(klass, func)

if with_docstrings:
func_title = f"`{func_name}` (output: {func_output})"
Expand All @@ -98,14 +47,17 @@ def get_func_description(func: Callable, with_docstrings: bool = False) -> str:
return desc


def get_plugin_functions_desc(plugin_class: Type, with_docstrings: bool = False) -> str:
def get_plugin_functions_desc(plugin_class: type[Plugin], with_docstrings: bool = False) -> str:
descriptions = []
for func_name in plugin_class.__exports__:
func_obj = getattr(plugin_class, func_name)
if func_obj is getattr(plugin_class, "__call__", None):
continue

if getattr(func_obj, "get_func_doc_spec", None):
func_desc = FUNC_DOC_TEMPLATE.format_map(func_obj.get_func_doc_spec())
else:
_, func = get_real_func_obj(func_obj)
_, func = _get_real_func_obj(func_obj)
func_desc = get_func_description(func, with_docstrings=with_docstrings)
descriptions.append(func_desc)

Expand All @@ -120,15 +72,17 @@ def get_plugin_functions_desc(plugin_class: Type, with_docstrings: bool = False)
return paragraph


def get_plugin_description(plugin_class: Type) -> str:
def get_plugin_description(plugin_class: type[Plugin]) -> str:
plugin_name = plugin_class.__name__
plugin_desc_title = f"`{plugin_name}` (`{plugin_class.__module__}.{plugin_name}`)"
plugin_doc = textwrap.indent(get_docstring(plugin_class), prefix=INDENT_STEP)
paragraph = "\n".join([plugin_desc_title, "", plugin_doc])
return paragraph


def get_plugin_overview(plugin_class: Type, with_plugin_desc: bool = False, with_func_docstrings: bool = False) -> str:
def get_plugin_overview(
plugin_class: type[Plugin], with_plugin_desc: bool = False, with_func_docstrings: bool = False
) -> str:
paragraphs = []

if with_plugin_desc:
Expand All @@ -150,3 +104,55 @@ def get_plugin_overview(plugin_class: Type, with_plugin_desc: bool = False, with
paragraphs.append(func_descriptions_paragraph)
overview = "\n".join(paragraphs)
return overview


def _get_plugin_class_for_func(func: Callable) -> type[Plugin]:
"""Return plugin class for provided function instance."""
func_parent_name = func.__qualname__.rsplit(".", 1)[0]
klass = getattr(inspect.getmodule(func), func_parent_name, None)
return klass


def _get_real_func_obj(func: Callable) -> tuple[type[Plugin], Callable]:
"""Return a tuple with plugin class and underlying function object for provided function instance."""
klass = None

if isinstance(func, property):
# turn property into function
func = func.fget

if inspect.ismethod(func):
for klass in inspect.getmro(func.__self__.__class__):
if func.__name__ in klass.__dict__:
break

Check warning on line 127 in dissect/target/helpers/docs.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/helpers/docs.py#L125-L127

Added lines #L125 - L127 were not covered by tests
else:
func = getattr(func, "__func__", func)

Check warning on line 129 in dissect/target/helpers/docs.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/helpers/docs.py#L129

Added line #L129 was not covered by tests

if inspect.isfunction(func):
klass = _get_plugin_class_for_func(func)

if not klass:
raise ValueError(f"Can't find class for {func}")

Check warning on line 135 in dissect/target/helpers/docs.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/helpers/docs.py#L135

Added line #L135 was not covered by tests

return (klass, func)


def _get_func_details(func: Callable) -> tuple[str, str]:
"""Return a tuple with function's name, output label and docstring"""
func_doc = get_docstring(func)

if hasattr(func, "__output__") and func.__output__ in FUNCTION_OUTPUT_DESCRIPTION:
func_output = FUNCTION_OUTPUT_DESCRIPTION[func.__output__]
else:
func_output = "unknown"

Check warning on line 147 in dissect/target/helpers/docs.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/helpers/docs.py#L147

Added line #L147 was not covered by tests

return (func_output, func_doc)


def _get_full_func_name(plugin_class: type[Plugin], func: Callable) -> str:
func_name = func.__name__

if hasattr(plugin_class, "__namespace__") and plugin_class.__namespace__:
func_name = f"{plugin_class.__namespace__}.{func_name}"

return func_name
Loading

0 comments on commit 85f4ebc

Please sign in to comment.