Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/get-started/overview.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ quartodoc:

The functions listed in `contents` are assumed to be imported from the package.

If no contents are provided, quartodoc will attempt to pull in all modules (i.e. `.py` files) from the package.


## Learning more

Expand Down
3 changes: 2 additions & 1 deletion quartodoc/_pydantic_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Extra,
PrivateAttr,
ValidationError,
validator,
) # noqa
except ImportError:
from pydantic import BaseModel, Field, Extra, PrivateAttr, ValidationError # noqa
from pydantic import BaseModel, Field, Extra, PrivateAttr, ValidationError, validator # noqa
74 changes: 74 additions & 0 deletions quartodoc/builder/blueprint.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import annotations

import importlib.util
import logging
import json
import yaml

from collections import OrderedDict
from pathlib import Path
from typing import Iterable, List, Optional, Set

from .._griffe_compat import dataclasses as dc
from .._griffe_compat import (
GriffeLoader,
Expand Down Expand Up @@ -44,6 +49,47 @@
from quartodoc._pydantic_compat import BaseModel


def _identify_files_to_document(
path: Path,
file_patterns: List[str],
ignore: Optional[Iterable[str]] = None,
) -> Set[Path]:
reversed_patterns = file_patterns.copy()
reversed_patterns.reverse()

files_to_document: dict = OrderedDict()
for pattern in reversed_patterns:
for file in path.rglob(pattern=pattern):
files_to_document[file.with_suffix("")] = file
result = set(files_to_document.values())

if ignore:
for pattern in ignore:
result = result.difference(set(path.glob(pattern=pattern)))

return {p.resolve() for p in result}


def _auto_contents_from_package(package_name: str) -> list[Auto]:
"""Return Auto entries for every .py file in *package_name*, excluding __init__ files."""
spec = importlib.util.find_spec(package_name)
if spec is None or not spec.submodule_search_locations:
return []

pkg_path = Path(list(spec.submodule_search_locations)[0])
files = _identify_files_to_document(
pkg_path,
file_patterns=["*.py"],
ignore=["**/__init__.py", "**/__pycache__/**"],
)

contents = []
for file in sorted(files):
parts = list(file.relative_to(pkg_path).with_suffix("").parts)
contents.append(Auto(name=".".join(parts)))
return contents


def _auto_package(mod: dc.Module) -> list[Section]:
"""Create default sections for the given package."""

Expand Down Expand Up @@ -246,6 +292,34 @@ def enter(self, el: Layout):

return super().enter(el)

@dispatch
def enter(self, el: Section):
if el.contents:
return el

package = self.crnt_package
label = el.title or el.subtitle or "(untitled)"

if not package:
_log.warning(
f"Section '{label}' has no contents and no package is configured."
" Cannot auto-populate contents."
)
return el

_log.warning(
f"Section '{label}' has no contents. Auto-populating from package '{package}'."
)

contents = _auto_contents_from_package(package)
if not contents:
_log.warning(f"No Python files found in package '{package}'.")
return el

new = el.copy()
new.contents = contents
return super().enter(new)

@dispatch
def exit(self, el: Section):
"""Transform top-level sections, so their contents are all Pages."""
Expand Down
8 changes: 7 additions & 1 deletion quartodoc/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing_extensions import Annotated
from typing import Literal, Union, Optional

from ._pydantic_compat import BaseModel, Field, Extra, PrivateAttr
from ._pydantic_compat import BaseModel, Field, Extra, PrivateAttr, validator


_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -124,6 +124,12 @@ class Page(_Structural):

contents: ContentList

@validator("contents")
def _contents_not_empty(cls, v):
if not v:
raise ValueError("Page contents must not be empty.")
return v

@property
def obj(self):
# TODO: this is for the case where pages are put as members inside
Expand Down