Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

explode a class or module contents #278

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion quartodoc/autosummary.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ def get_object(
abc
"""

# TODO: this should live in a helper module
from quartodoc.builder.blueprint import _fetch_all_members

if object_name is not None:
warnings.warn(
"object_name argument is deprecated in get_object", DeprecationWarning
Expand Down Expand Up @@ -149,7 +152,7 @@ def get_object(

# Case 2: static loading an object ----
f_parent = loader.modules_collection[griffe_path.rsplit(".", 1)[0]]
f_data = loader.modules_collection[griffe_path]
f_data = _fetch_all_members(f_parent)[griffe_path.rsplit(".", 1)[1]]

# ensure that function methods fetched off of an Alias of a class, have that
# class Alias as their parent, not the Class itself.
Expand Down
71 changes: 63 additions & 8 deletions quartodoc/builder/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from .utils import PydanticTransformer, ctx_node, WorkaroundKeyError

from typing import overload, TYPE_CHECKING
from typing import overload, Optional, TYPE_CHECKING


_log = logging.getLogger(__name__)
Expand All @@ -39,7 +39,7 @@
from pydantic import BaseModel


def _auto_package(mod: dc.Module) -> list[Section]:
def _auto_package(mod: dc.Module, prefix: Optional[str] = None) -> list[Section]:
"""Create default sections for the given package."""

import griffe.docstrings.dataclasses as ds
Expand All @@ -55,8 +55,11 @@ def _auto_package(mod: dc.Module) -> list[Section]:
)

# get module members for content ----

combined_members = _fetch_all_members(mod)

contents = []
for name, member in mod.members.items():
for name, member in combined_members.items():
external_alias = _is_external_alias(member, mod)
if (
external_alias
Expand All @@ -66,7 +69,10 @@ def _auto_package(mod: dc.Module) -> list[Section]:
):
continue

contents.append(Auto(name=name))
if prefix:
contents.append(Auto(name=f"{prefix}:{name}"))
else:
contents.append(Auto(name=name))

# try to fetch a description of the module ----
if mod.docstring and mod.docstring.parsed:
Expand All @@ -81,7 +87,34 @@ def _auto_package(mod: dc.Module) -> list[Section]:
return [Section(title=mod.path, desc=desc, contents=contents)]


def _fetch_all_members(mod: dc.Alias | dc.Object):
"""Return all members of the module, including those imported from other modules."""

if not mod.is_module:
return mod.members

all_imported_members = _unpack_internal_star_imports(mod)
combined_members = {**all_imported_members, **mod.members}

return combined_members


def _unpack_internal_star_imports(mod: dc.Module):
star_imports = [v for k, v in mod.members.items() if k.endswith("*")]

all_imported_members = {}
for sub_mod in star_imports:
for obj in sub_mod.members.values():
if obj.is_exported():
all_imported_members[obj.name] = dc.Alias(
obj.name, target=obj, parent=mod
)

return all_imported_members


def _is_external_alias(obj: dc.Alias | dc.Object, mod: dc.Module):
"""Return whether the given object is an alias to an object outside mod's package."""
package_name = mod.path.split(".")[0]

if not isinstance(obj, dc.Alias):
Expand Down Expand Up @@ -155,7 +188,7 @@ def get_object_fixed(self, path, **kwargs):
)

@staticmethod
def _clean_member_path(path, new):
def _clean_member_path(new):
if ":" in new:
return new.replace(":", ".")

Expand Down Expand Up @@ -221,6 +254,25 @@ def enter(self, el: Layout):

return super().enter(el)

@dispatch
def enter(self, el: Section):
if el.explode:
package = self.crnt_package

mod = self.get_object_fixed(f"{package}.{el.explode}", dynamic=self.dynamic)
sections = _auto_package(mod, prefix=el.explode)

only_section = sections[0]

if el.title:
only_section.title = el.title
if el.desc:
only_section.desc = el.desc

return super().enter(only_section)

return super().enter(el)

@dispatch
def exit(self, el: Section):
"""Transform top-level sections, so their contents are all Pages."""
Expand Down Expand Up @@ -287,7 +339,7 @@ def enter(self, el: Auto):
# On the other hand, we've wired get_object up to make sure getting
# the member of an Alias also returns an Alias.
# member_path = self._append_member_path(path, entry)
relative_path = self._clean_member_path(path, entry)
relative_path = self._clean_member_path(entry)

# create Doc element for member ----
# TODO: when a member is a Class, it is currently created using
Expand Down Expand Up @@ -325,7 +377,7 @@ def enter(self, el: Auto):

is_flat = el.children == ChoicesChildren.flat
return Doc.from_griffe(
el.name,
self._clean_member_path(el.name),
obj,
children,
flat=is_flat,
Expand All @@ -337,7 +389,10 @@ def _fetch_members(el: Auto, obj: dc.Object | dc.Alias):
if el.members is not None:
return el.members

options = obj.all_members if el.include_inherited else obj.members
if obj.is_module:
options = _fetch_all_members(obj)
else:
options = obj.all_members if el.include_inherited else obj.members

if el.include:
raise NotImplementedError("include argument currently unsupported.")
Expand Down
10 changes: 9 additions & 1 deletion quartodoc/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ class Section(_Structural):
If specified, all object lookups will be relative to this path.
contents:
Individual objects (e.g. functions, classes, methods) being documented.
explode:
Name of an object used to create the section. If no description is specified,
then object's docstring will be used for the description.
options:
Options to set as defaults for all content.
"""

kind: Literal["section"] = "section"
Expand All @@ -79,13 +84,16 @@ class Section(_Structural):
desc: Optional[str] = None
package: Union[str, None, MISSING] = MISSING()
contents: ContentList = []
explode: Optional[str] = None
options: Optional["AutoOptions"] = None

def __init__(self, **data):
super().__init__(**data)

# TODO: should these be a custom type? Or can we use pydantic's ValidationError?
if self.title is None and self.subtitle is None and not self.contents:
if self.explode:
pass
elif self.title is None and self.subtitle is None and not self.contents:
raise ValueError(
"Section must specify a title, subtitle, or contents field"
)
Expand Down