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
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@
extensions = [
"score_sphinx_bundle",
]

score_any_folder_mapping = {
"../src/extensions/docs": "internals/extensions",
}
29 changes: 29 additions & 0 deletions docs/how-to/any_folder.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Use Any Folder for Documentation
================================

Generally, your documentation must be ``docs/``,
but the RST files for a module may live closer to the code they describe,
for example in ``src/my_module/docs/``.
You can symlink the folders by adding to your ``conf.py``:

.. code-block:: python

score_any_folder_mapping = {
"../score/containers/docs": "component/containers",
}

With this configuration, all files in ``score/containers/docs/`` become available at ``docs/component/containers/``.

If you have ``docs/component/overview.rst``, for example,
you can include the component documentation via ``toctree``:

.. code-block:: rst

.. toctree::

containers/index

Only relative links are allowed.

The symlinks will show up in your sources.
**Don't commit the symlinks to git!**
1 change: 1 addition & 0 deletions docs/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Here you find practical guides on how to use docs-as-code.
source_to_doc_links
test_to_doc_links
add_extensions
any_folder
1 change: 1 addition & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ filegroup(
srcs = glob(
["*.py"],
) + [
"//src/extensions/score_any_folder:all_sources",
"//src/extensions/score_draw_uml_funcs:all_sources",
"//src/extensions/score_header_service:all_sources",
"//src/extensions/score_layout:all_sources",
Expand Down
38 changes: 38 additions & 0 deletions src/extensions/docs/any_folder.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

Any Folder
==========

The extension ``score_any_folder`` allows documentation roots to stay in ``docs/``
while pulling in source files from anywhere else in the repository.

It does this by creating symlinks inside the Sphinx source directory (``confdir``) that point to the configured external directories.
Sphinx then discovers and buildsthose files as if they were part of ``docs/`` from the start.

The extension hooks into the ``builder-inited`` event,
which fires before Sphinx reads any documents.

Difference to Sphinx-Collections
--------------------------------

The extension `sphinx-collections <https://sphinx-collections.readthedocs.io/>`_
is very similar to this extension.
We use it for including external modules
as it allows conditional inclusion
and we need to switch between "normal" and "combo" builds.

The relevant difference is that this extension allows to include folders to anywhere
and is not restricted to a ``_collections/`` folder.
We consider this additional control over folder placement necessary.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
.. # *******************************************************************************
# Copyright (c) 2025 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
..
# *******************************************************************************
# Copyright (c) 2025 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

.. _extensions:

Expand Down Expand Up @@ -71,6 +72,16 @@ Hello there
`ubCode <https://ubcode.useblocks.com>`__ VS Code extension.
Getting IDE support for Sphinx-Needs in a Bazel context made easy.

.. grid-item-card::

Any Folder
^^^^^^^^^^

Learn about the :doc:`any_folder` extension that creates symlinks
from arbitrary repository locations into the docs folder,
allowing Sphinx to discover and build source files
that live outside the documentation root.



.. toctree::
Expand All @@ -83,3 +94,4 @@ Hello there
Source Code Linker <source_code_linker>
Extension Guide <extension_guide>
Sync TOML <sync_toml>
Any Folder <any_folder>
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
.. _`toml_sync`:

ubproject.toml sync
Expand Down
50 changes: 50 additions & 0 deletions src/extensions/score_any_folder/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("@aspect_rules_py//py:defs.bzl", "py_library")
load("@docs_as_code_hub_env//:requirements.bzl", "requirement")
load("@score_tooling//:defs.bzl", "score_py_pytest")

filegroup(
name = "sources",
srcs = glob(["*.py"]),
)

filegroup(
name = "tests",
srcs = glob(["tests/*.py"]),
)

filegroup(
name = "all_sources",
srcs = [
":sources",
":tests",
],
visibility = ["//visibility:public"],
)

py_library(
name = "score_any_folder",
srcs = [":sources"],
imports = ["."],
visibility = ["//visibility:public"],
deps = [requirement("sphinx")],
)

score_py_pytest(
name = "score_any_folder_tests",
size = "small",
srcs = glob(["tests/*.py"]),
deps = [":score_any_folder"],
)
118 changes: 118 additions & 0 deletions src/extensions/score_any_folder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Sphinx extension that creates symlinks from arbitrary locations into the
documentation source directory, allowing sphinx-build to include source
files that live outside ``docs/``.
Configuration in ``conf.py``::
score_any_folder_mapping = {
"../src/my_module/docs": "my_module",
}
Each entry is a ``source: target`` pair where:
* ``source`` – path to the directory to expose, relative to ``confdir``
(the directory containing ``conf.py``).
* ``target`` – path of the symlink to create, relative to ``confdir``.
The extension creates the symlinks on ``builder-inited``,
before Sphinx starts reading any documents.
Existing correct symlinks are left in place(idempotent);
a symlink pointing to the wrong target is replaced.
Symlinks created by this extension are removed again on ``build-finished``.
Misconfigured pairs (absolute paths, non-symlink path at the target location)
are logged as errors and skipped.
"""

from pathlib import Path

from sphinx.application import Sphinx
from sphinx.util.logging import getLogger

logger = getLogger(__name__)

_APP_ATTRIBUTE = "_score_any_folder_created_links"

def setup(app: Sphinx) -> dict[str, str | bool]:
app.add_config_value("score_any_folder_mapping", default={}, rebuild="env")
app.connect("builder-inited", _create_symlinks)
app.connect("build-finished", _cleanup_symlinks)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}


def _symlink_pairs(app: Sphinx) -> list[tuple[Path, Path]]:
"""Return ``(resolved_source, link_path)`` pairs from the mapping."""
confdir = Path(app.confdir)
pairs = []
for source_rel, target_rel in app.config.score_any_folder_mapping.items():
if Path(source_rel).is_absolute():
logger.error(
"score_any_folder: source path must be relative, got: %r; skipping",
source_rel,
)
continue
Comment on lines +65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this truly be an error then?
The build will be broken if thati s the case, that is wanted here I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that it might work locally but it will almost certainly break in the CI.

I agree that it is weird to report an error and then simply continue.

if Path(target_rel).is_absolute():
logger.error(
"score_any_folder: target path must be relative, got: %r; skipping",
target_rel,
)
continue
source = (confdir / source_rel).resolve()
link = confdir / target_rel
pairs.append((source, link))
return pairs


def _create_symlinks(app: Sphinx) -> None:
created_links: set[Path] = set()

for source, link in _symlink_pairs(app):
if link.is_symlink():
if link.resolve() == source:
logger.debug("score_any_folder: symlink already correct: %s", link)
continue
logger.info(
"score_any_folder: replacing stale symlink %s -> %s", link, source
)
link.unlink()
elif link.exists():
logger.error(
"score_any_folder: target path already exists and is not a symlink: "
"%s; skipping",
link,
)
continue

link.parent.mkdir(parents=True, exist_ok=True)
link.symlink_to(source)
created_links.add(link)
logger.debug("score_any_folder: created symlink %s -> %s", link, source)

setattr(app, _APP_ATTRIBUTE, created_links)


def _cleanup_symlinks(app: Sphinx, exception: Exception | None) -> None:
del exception

created_links: set[Path] = getattr(app, _APP_ATTRIBUTE, set())
for link in created_links:
if not link.is_symlink():
continue
link.unlink()
logger.debug("score_any_folder: removed temporary symlink %s", link)
12 changes: 12 additions & 0 deletions src/extensions/score_any_folder/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
Loading
Loading