Skip to content
Merged
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
28 changes: 24 additions & 4 deletions bzl/bundle_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,14 @@ def _pure_data_runtime_path(ctx):
)
return "__data__/%s" % encoded_label

def _rebase_bundle_entry(entry, mount_at, attach_to):
def _rebase_bundle_entry(entry, mount_at, attach_to, toctree_index):
"""Place a bundle entry below a requested documentation-tree location.

A bundle's own root has no ``mount_at`` yet. For that root, an omitted
``attach_to`` means the parent directory's ``index`` page. Nested entries
retain their existing attachment and are rebased below ``mount_at``.
``attach_to`` means the parent directory's ``index`` page, and the
placement's ``toctree_index`` selects which toctree of that page receives
the entry. Nested entries retain their existing attachment (including its
own ``toctree_index``) and are rebased below ``mount_at``.

``data`` is deliberately kept with the entry that declares it. A composed
bundle may expose several source and data-only entries, each resolving its
Expand All @@ -229,17 +231,20 @@ def _rebase_bundle_entry(entry, mount_at, attach_to):
# Its default attachment is therefore the parent directory's index;
# an explicit attach_to still overrides that default.
rebased_attach_to = attach_to or _parent_index_docname(mount_at)
rebased_toctree_index = toctree_index
else:
# This entry is already below another location in the child bundle.
# Keep its attachment relative to that location and prefix the whole
# placement with the mount point chosen by the parent.
rebased_attach_to = join_path(mount_at, entry.attach_to)
rebased_toctree_index = entry.toctree_index

return struct(
runtime_path = entry.runtime_path,
src_root = entry.src_root,
mount_at = join_path(mount_at, entry.mount_at),
attach_to = rebased_attach_to,
toctree_index = rebased_toctree_index,
entry_doc = entry.entry_doc,
external = entry.external,
repository = entry.repository,
Expand Down Expand Up @@ -282,7 +287,7 @@ def _parse_bundle_declaration(bundle):
if type(bundle) != "dict":
fail("each bundle declaration must be a dict, got %r" % bundle)

allowed_keys = ["bundle", "mount_at", "attach_to"]
allowed_keys = ["bundle", "mount_at", "attach_to", "toctree_index"]
unknown = [key for key in bundle if key not in allowed_keys]
if unknown:
fail("unknown key(s) %r in %r; allowed keys: %r" %
Expand All @@ -292,11 +297,16 @@ def _parse_bundle_declaration(bundle):

mount_at = bundle["mount_at"]
attach_to = bundle.get("attach_to", "")
toctree_index = bundle.get("toctree_index", 0)
if type(toctree_index) != "int" or toctree_index < 0:
fail("each entry's 'toctree_index' must be a non-negative int; got %r" %
toctree_index)

return struct(
bundle = bundle["bundle"],
mount_at = mount_at,
attach_to = attach_to,
toctree_index = toctree_index,
)

def _docs_bundle_impl(ctx):
Expand Down Expand Up @@ -334,6 +344,7 @@ def _docs_bundle_impl(ctx):
src_root = source_dir_execroot_path,
mount_at = "",
attach_to = "",
toctree_index = 0,
entry_doc = ctx.attr.entry_doc,
external = external,
repository = ctx.label.workspace_name,
Expand Down Expand Up @@ -371,6 +382,9 @@ def _docs_bundle_impl(ctx):
src_root = source_dir_execroot_path,
mount_at = "",
attach_to = "",
# The bundle root has no toctree_index of its own; the placement
# that mounts this bundle supplies it (default 0).
toctree_index = 0,
entry_doc = ctx.attr.entry_doc,
external = external,
repository = ctx.label.workspace_name,
Expand Down Expand Up @@ -401,6 +415,9 @@ def _docs_bundle_impl(ctx):
src_root = "",
mount_at = "",
attach_to = "",
# The bundle root has no toctree_index of its own; the placement
# that mounts this bundle supplies it (default 0).
toctree_index = 0,
entry_doc = ctx.attr.entry_doc,
external = False,
repository = ctx.label.workspace_name,
Expand Down Expand Up @@ -432,6 +449,7 @@ def _docs_bundle_impl(ctx):
entry,
ctx.attr.bundle_mount_ats[index],
ctx.attr.bundle_attach_tos[index],
ctx.attr.bundle_toctree_indices[index],
)
for entry in _entries_visible_through(ctx, child)
])
Expand Down Expand Up @@ -479,6 +497,7 @@ _docs_bundle = rule(
"bundles": attr.label_list(providers = [DocsBundleInfo]),
"bundle_mount_ats": attr.string_list(),
"bundle_attach_tos": attr.string_list(),
"bundle_toctree_indices": attr.int_list(),
"data": attr.label_list(allow_files = True),
# The aspect preserves the selected target's rule kind while
# recursively collecting its source files for source-link generation.
Expand Down Expand Up @@ -515,6 +534,7 @@ def create_bundle(
bundles = [bundle.bundle for bundle in parsed_bundles],
bundle_mount_ats = [bundle.mount_at for bundle in parsed_bundles],
bundle_attach_tos = [bundle.attach_to for bundle in parsed_bundles],
bundle_toctree_indices = [bundle.toctree_index for bundle in parsed_bundles],
data = data,
code_targets = code_targets,
visibility = visibility,
Expand Down
1 change: 1 addition & 0 deletions bzl/mount_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def _composition_manifest_impl(ctx):
"runtime_path": entry.runtime_path,
"mount_at": entry.mount_at,
"attach_to": entry.attach_to,
"toctree_index": entry.toctree_index,
"entry_doc": entry.entry_doc,
"external": entry.external,
"repository": entry.repository,
Expand Down
10 changes: 6 additions & 4 deletions docs/concepts/mounts/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ content, but not its position in a consuming documentation site.
A **mount** makes a bundle visible in a host site. The host chooses the
**placement** with ``mount_at``. The bundle defines its own ``entry_doc``
(default ``index``); the mount adds that page to the parent ``index`` toctree
by default. ``attach_to`` overrides that host toctree document. This separation
lets different projects reuse the same bundle at different locations while
preserving its canonical entry page.
by default. ``attach_to`` overrides that host toctree document; ``toctree_index``
selects which of its toctrees receives the entry (``0`` is the first). This
separation lets different projects reuse the same bundle at different
locations while preserving its canonical entry page.

Bundles are read from their original source directories. Consequently, an
in-repository bundle remains editable and IDE navigation reaches its real
Expand Down Expand Up @@ -109,4 +110,5 @@ Further reading

* :ref:`howto_mount_external_sources` β€” declare and mount bundles.
* `sphinx-mounts documentation <https://sphinx-mounts.useblocks.com/>`_ β€”
configuration reference, including ``attach_to`` and ``entry_doc``.
configuration reference, including ``attach_to``, ``toctree_index``, and
``entry_doc``.
8 changes: 5 additions & 3 deletions docs/how-to/bundles/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ as ``internals/code_docs/overview``.

Every mount adds the bundle's configured entry document to a host toctree. By
default that is the ``index`` beside ``mount_at``; ``attach_to`` overrides the
host document whose first toctree receives the entry. The entry itself belongs
to the ``docs_bundle`` and defaults to ``index``.
host document that receives the entry. The host document may contain several
toctrees. ``toctree_index`` selects which one receives the entry, counting from
``0`` (the first toctree). The entry itself belongs to the ``docs_bundle`` and
defaults to ``index``.

The configuration has two separate responsibilities:

Expand All @@ -82,7 +84,7 @@ The configuration has two separate responsibilities:
rectangle "docs(bundles = [...])\nplacement" as docs
rectangle "Rendered docs" as output
bundle --> docs : select bundle
docs --> output : mount_at / attach_to
docs --> output : mount_at / attach_to / toctree_index
@enduml

For nested composition, generated bundle data, and bundles published by
Expand Down
16 changes: 11 additions & 5 deletions docs/reference/bazel_macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ Minimal example (root ``BUILD``)
``:needs_json`` target here.

- ``bundles`` (list of placement dicts)
Documentation bundles to overlay into this project's documentation tree, each with its
placement (``mount_at``). See :ref:`howto_mount_external_sources` for the full reference.
Documentation bundles to overlay into this project's documentation tree,
each with its placement (``mount_at``, optional ``attach_to`` and
``toctree_index``). See :ref:`howto_mount_external_sources` for the full reference.

- ``deps`` (list of bazel labels)
Additional Bazel dependencies to add to the Python binaries and the virtual environment
Expand Down Expand Up @@ -196,7 +197,8 @@ Signature: ``docs_bundle(name, source_dir = None, srcs = [], data = [], entry_do
- ``entry_doc`` (string, optional)
Bundle-relative docname used as the canonical navigation entry. It defaults to
``index``. Every mount attaches this entry to the parent ``index`` toctree by
default; a placement's ``attach_to`` may override that host document.
default; a placement's ``attach_to`` may override that host document, and its
``toctree_index`` selects which toctree of that document receives the entry.

- ``bundles`` (list of composition dicts, optional)
Nested bundles to compose into this one, so a bundle can aggregate other
Expand All @@ -208,6 +210,9 @@ Signature: ``docs_bundle(name, source_dir = None, srcs = [], data = [], entry_do
- ``attach_to`` (optional) β€” a docname (relative to this bundle) whose toctree
receives the child's bundle-defined entry document. When omitted, the parent
``index`` document receives it.
- ``toctree_index`` (optional) β€” the 0-based index of the toctree inside the
``attach_to`` document that receives the entry. It defaults to ``0`` (the
first toctree).

A child's ``mount_at``/``attach_to`` **prefix-stack** with the placement this
bundle later receives, so composition is fully transitive. The same underlying
Expand All @@ -225,8 +230,9 @@ Signature: ``docs_bundle(name, source_dir = None, srcs = [], data = [], entry_do

.. note::

A bundle is **placement-free**: its ``mount_at`` and ``attach_to`` are assigned
by the mounter, while its ``entry_doc`` belongs to the bundle. This lets the
A bundle is **placement-free**: its ``mount_at``, ``attach_to``, and
``toctree_index`` are assigned by the mounter, while its ``entry_doc`` belongs to
the bundle. This lets the
same bundle be mounted at different locations by different consumers without
changing its canonical entry page.

Expand Down
2 changes: 2 additions & 0 deletions src/extensions/score_mounts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def _make_mount_entry(
"dir": str(_canonical_mount_dir(walk_dir, spec)),
"mount_at": spec.mount_at,
"attach_to": spec.attach_to,
"toctree_index": spec.toctree_index,
Comment thread
a-zw marked this conversation as resolved.
"entry_doc": spec.entry_doc,
"exclude": list(exclude),
}
Expand All @@ -197,6 +198,7 @@ def _make_file_mount_entry(
"files": [str(source_file) for source_file in source_files],
"mount_at": spec.mount_at,
"attach_to": spec.attach_to,
"toctree_index": spec.toctree_index,
Comment thread
a-zw marked this conversation as resolved.
"entry_doc": spec.entry_doc,
}

Expand Down
9 changes: 9 additions & 0 deletions src/extensions/score_mounts/_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class MountSpec:
runtime_path: str
mount_at: str
attach_to: str | None = None
toctree_index: int = 0
entry_doc: str = "index"
external: bool = False
repository: str = ""
Expand All @@ -122,11 +123,19 @@ class MountSpec:
def from_manifest_entry(cls, entry: dict[str, object]) -> MountSpec:
"""Create one mount spec from the producer-owned manifest entry."""
attach_to = cast("str", entry["attach_to"])
raw_toctree_index = entry.get("toctree_index", 0)
# ``type is`` (not ``isinstance``) also rejects booleans, which are ints.
if type(raw_toctree_index) is not int:
raise ValueError(
"mounts manifest entry field 'toctree_index' must be an int: "
f"{raw_toctree_index!r}"
)
Comment thread
a-zw marked this conversation as resolved.
return cls(
src_root=cast("str", entry["src_root"]),
runtime_path=cast("str", entry["runtime_path"]),
mount_at=cast("str", entry["mount_at"]),
attach_to=attach_to or None,
toctree_index=raw_toctree_index,
entry_doc=cast("str", entry["entry_doc"]),
external=cast("bool", entry["external"]),
repository=cast("str", entry["repository"]),
Expand Down
38 changes: 38 additions & 0 deletions src/extensions/score_mounts/tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,44 @@ def test_load_bundle_metadata_and_direct_targets(tmp_path: Path) -> None:
)


def test_load_entry_with_toctree_index(tmp_path: Path) -> None:
manifest = _write_manifest(
tmp_path,
{
"mounts": [
{
"src_root": "src/docs",
"runtime_path": "src/docs_dir",
"mount_at": "x",
"attach_to": "internals/index",
"toctree_index": 1,
}
],
},
)
spec = load_mounts_manifest(str(manifest)).mounts[0]
assert spec.toctree_index == 1


def test_load_entry_without_toctree_index_defaults_to_zero(tmp_path: Path) -> None:
"""Older manifests lack the field; they must extend the first toctree."""
manifest = _write_manifest(
tmp_path,
{
"mounts": [
{
"src_root": "src/docs",
"runtime_path": "src/docs_dir",
"mount_at": "x",
"attach_to": "internals/index",
}
],
},
)
spec = load_mounts_manifest(str(manifest)).mounts[0]
assert spec.toctree_index == 0


def test_external_mount_keeps_execroot_and_runfiles_locations(tmp_path: Path) -> None:
manifest = _write_manifest(
tmp_path,
Expand Down
2 changes: 2 additions & 0 deletions src/extensions/score_sync_toml/_mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def materialize_mounts(entries: list[dict[str, Any]]) -> Path | None:
lines.append(f"mount_at = {_toml_string(entry['mount_at'])}")
if entry.get("attach_to"):
lines.append(f"attach_to = {_toml_string(entry['attach_to'])}")
if entry.get("toctree_index", 0) != 0:
lines.append(f"toctree_index = {entry['toctree_index']}")
if entry.get("entry_doc", "index") != "index":
lines.append(f"entry_doc = {_toml_string(entry['entry_doc'])}")
lines.append("")
Expand Down
2 changes: 2 additions & 0 deletions src/tests/docs_bzl/scenarios/nested_bundles/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,7 @@ docs(
bundles = [{
"bundle": ":parent",
"mount_at": "concepts/example_bundle",
# Attach to the second toctree (index 1) of concepts/index.rst.
"toctree_index": 1,
}],
)
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@
<nav class="bd-docs-nav bd-links"
aria-label="Section Navigation">
<p class="bd-links__title" role="heading" aria-level="1">Section Navigation</p>
<div class="bd-toc-item navbar-nav"><ul class="current nav bd-sidenav">
<div class="bd-toc-item navbar-nav"><p aria-level="2" class="caption" role="heading"><span class="caption-text">Mounted bundles</span></p>
<ul class="current nav bd-sidenav">
<li class="toctree-l1 current active has-children"><a class="reference internal" href="../index.html">Parent bundle</a><details open="open"><summary><span class="toctree-toggle" role="presentation"><i class="fa-solid fa-chevron-down"></i></span></summary><ul class="current">
<li class="toctree-l2 current active has-children"><a class="current reference internal" href="#">Child landing page</a><details open="open"><summary><span class="toctree-toggle" role="presentation"><i class="fa-solid fa-chevron-down"></i></span></summary><ul>
<li class="toctree-l3"><a class="reference internal" href="index.html">Child bundle</a></li>
Expand Down
Loading
Loading