docs: generate Starlark domain markup instead of regular markdown (#1919)
This switches our doc generation over to using the Starlark domain
markup from the sphinx_stardoc plugin instead of using regular markdown.
This allows the docs generated from code to better integrate with each
other and other parts of the doc site.
Overview of changes:
* Makes the doc paths under the API directory more directly mirror their
actual location. e.g. moves "defs.md" -> "python/defs.md". This is so
the //tools doc entries have a more natural location, but can also be
used for our other top-level directories.
* Adds API docs for some of the well known targets we have. These aren't
automatically generated, but use the Starlark domain markup, so
integrate nicely with everything.
* Ensures default values are parsable as Python expressions. Stardoc
returns values like "<function foo>" or 'Label(*, "//bar")' in some
cases for the default value of args/attrs.
* Ensures function signatures don't crash doc rendering. Stardoc gives
bad/incomplete information, so reconstructing the original signature of
a function is tricky.
* Allows references flags using leading slashes and a value, e.g.
`--foo=bar`. This makes it more natural to write while cross referencing
to the flag.
* Implements `{any}` xref resolution. It was just totally broken before.
* Adds some additional bzl files that get documented.
* Adds some more Bazel external references.
* Fixes some missing bzl_library dependencies.
* A few minor QoL improvements to the docs dev server:
* Print the serving directory when CTRL+C is received. This makes it
easier to find the raw files that are being generated.
* Fix an error during shutdown about an unterminated generator.
* The `sphinx_stardocs.footer` arg is removed. This was always just a
hack to get extra link targets into the generated bzl docs. It's no
longer needed when the bzl domain is used.
* Using `@repo//pkg:file.bzl%Name` syntax is supported in type
expressions (e.g. `:type:` option or `{type}` role) by quoting the
label. The quoting is necessary because, under the hood, the expressions
are parsed as Python.
* Objects directives support an `:origin-key` directive. This records
the label identity that Bazel sees for an object (as from the Stardoc
origin_key field). The markdown generate doesn't generate this for
everything yet because some things are documented twice (e.g. py_binary
in defs.bzl and py_binary.bzl), which would cause a crash (type things
trying to define the same id).
* Add `*` and `**` to var-args and var-kwargs in signatures.
* Allow providers to be refered to using the `type` role. This allows
providers to be referenced in `:type:` directives (e.g. in a provider
field).diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel
index e3b9ad3..c2a1690 100644
--- a/docs/sphinx/BUILD.bazel
+++ b/docs/sphinx/BUILD.bazel
@@ -30,7 +30,7 @@
"@platforms//os:linux": [],
"@platforms//os:macos": [],
"//conditions:default": ["@platforms//:incompatible"],
-})
+}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"]
# See README.md for instructions. Short version:
# * `bazel run //docs/sphinx:docs.serve` in a separate terminal
@@ -76,24 +76,34 @@
sphinx_stardocs(
name = "bzl_api_docs",
docs = {
- "api/cc/py_cc_toolchain.md": dict(
+ "api/python/cc/py_cc_toolchain.md": dict(
dep = "//python/private:py_cc_toolchain_bzl",
input = "//python/private:py_cc_toolchain_rule.bzl",
public_load_path = "//python/cc:py_cc_toolchain.bzl",
),
- "api/cc/py_cc_toolchain_info.md": "//python/cc:py_cc_toolchain_info_bzl",
- "api/defs.md": "//python:defs_bzl",
- "api/entry_points/py_console_script_binary.md": "//python/entry_points:py_console_script_binary_bzl",
- "api/packaging.md": "//python:packaging_bzl",
- "api/pip.md": "//python:pip_bzl",
+ "api/python/cc/py_cc_toolchain_info.md": "//python/cc:py_cc_toolchain_info_bzl",
+ "api/python/defs.md": "//python:defs_bzl",
+ "api/python/entry_points/py_console_script_binary.md": "//python/entry_points:py_console_script_binary_bzl",
+ "api/python/packaging.md": "//python:packaging_bzl",
+ "api/python/pip.md": "//python:pip_bzl",
+ "api/python/py_binary.md": "//python:py_binary_bzl",
+ "api/python/py_cc_link_params_info.md": "//python:py_cc_link_params_info_bzl",
+ "api/python/py_library.md": "//python:py_library_bzl",
+ "api/python/py_runtime.md": "//python:py_runtime_bzl",
+ "api/python/py_runtime_info.md": "//python:py_runtime_info_bzl",
+ "api/python/py_runtime_pair.md": dict(
+ dep = "//python/private:py_runtime_pair_rule_bzl",
+ input = "//python/private:py_runtime_pair_rule.bzl",
+ public_load_path = "//python:py_runtime_pair.bzl",
+ ),
+ "api/python/py_test.md": "//python:py_test_bzl",
} | ({
# Bazel 6 + Stardoc isn't able to parse something about the python bzlmod extension
- "api/extensions/python.md": "//python/extensions:python_bzl",
+ "api/python/extensions/python.md": "//python/extensions:python_bzl",
} if IS_BAZEL_7_OR_HIGHER else {}) | ({
# This depends on @pythons_hub, which is only created under bzlmod,
- "api/extensions/pip.md": "//python/extensions:pip_bzl",
+ "api/python/extensions/pip.md": "//python/extensions:pip_bzl",
} if IS_BAZEL_7_OR_HIGHER and BZLMOD_ENABLED else {}),
- footer = "_stardoc_footer.md",
tags = ["docs"],
target_compatible_with = _TARGET_COMPATIBLE_WITH,
)
@@ -112,6 +122,8 @@
requirement("sphinx_rtd_theme"),
requirement("myst_parser"),
requirement("readthedocs_sphinx_ext"),
+ requirement("typing_extensions"),
+ "//sphinxdocs/src/sphinx_stardoc",
],
)
diff --git a/docs/sphinx/api/python/config_settings/index.md b/docs/sphinx/api/python/config_settings/index.md
new file mode 100644
index 0000000..82a5b2a
--- /dev/null
+++ b/docs/sphinx/api/python/config_settings/index.md
@@ -0,0 +1,68 @@
+:::{bzl:currentfile} //python/config_settings:BUILD.bazel
+:::
+
+# //python/config_settings
+
+:::{bzl:flag} precompile
+Determines if Python source files should be compiled at build time.
+
+NOTE: The flag value is overridden by the target level `precompile` attribute,
+except for the case of `force_enabled` and `forced_disabled`.
+
+Values:
+
+* `auto`: Automatically decide the effective value based on environment,
+ target platform, etc.
+* `enabled`: Compile Python source files at build time. Note that
+ {bzl:obj}`--precompile_add_to_runfiles` affects how the compiled files are included into
+ a downstream binary.
+* `disabled`: Don't compile Python source files at build time.
+* `if_generated_source`: Compile Python source files, but only if they're a
+ generated file.
+* `force_enabled`: Like `enabled`, except overrides target-level setting. This
+ is mostly useful for development, testing enabling precompilation more
+ broadly, or as an escape hatch if build-time compiling is not available.
+* `force_disabled`: Like `disabled`, except overrides target-level setting. This
+ is useful useful for development, testing enabling precompilation more
+ broadly, or as an escape hatch if build-time compiling is not available.
+:::
+
+:::{bzl:flag} precompile_source_retention
+Determines, when a source file is compiled, if the source file is kept
+in the resulting output or not.
+
+NOTE: This flag is overridden by the target level `precompile_source_retention`
+attribute.
+
+Values:
+
+* `keep_source`: Include the original Python source.
+* `omit_source`: Don't include the orignal py source.
+* `omit_if_generated_source`: Keep the original source if it's a regular source
+ file, but omit it if it's a generated file.
+:::
+
+:::{bzl:flag} precompile_add_to_runfiles
+Determines if a target adds its compiled files to its runfiles.
+
+When a target compiles its files, but doesn't add them to its own runfiles, it
+relies on a downstream target to retrieve them from
+{bzl:obj}`PyInfo.transitive_pyc_files`
+
+Values:
+* `always`: Always include the compiled files in the target's runfiles.
+* `decided_elsewhere`: Don't include the compiled files in the target's
+ runfiles; they are still added to {bzl:obj}`PyInfo.transitive_pyc_files`. See
+ also: {bzl:obj}`py_binary.pyc_collection` attribute. This is useful for allowing
+ incrementally enabling precompilation on a per-binary basis.
+:::
+
+:::{bzl:flag} pyc_collection
+Determine if `py_binary` collects transitive pyc files.
+
+NOTE: This flag is overridden by the target level `pyc_collection` attribute.
+
+Values:
+* `include_pyc`: Include `PyInfo.transitive_pyc_files` as part of the binary.
+* `disabled`: Don't include `PyInfo.transitive_pyc_files` as part of the binary.
+:::
diff --git a/docs/sphinx/api/python/index.md b/docs/sphinx/api/python/index.md
new file mode 100644
index 0000000..8026a7f
--- /dev/null
+++ b/docs/sphinx/api/python/index.md
@@ -0,0 +1,23 @@
+:::{bzl:currentfile} //python:BUILD.bazel
+:::
+
+# //python
+
+:::{bzl:target} toolchain_type
+
+Identifier for the toolchain type for the target platform.
+:::
+
+:::{bzl:target} exec_tools_toolchain_type
+
+Identifier for the toolchain type for exec tools used to build Python targets.
+:::
+
+:::{bzl:target} current_py_toolchain
+
+Helper target to resolve to the consumer's current Python toolchain. This target
+provides:
+
+* `PyRuntimeInfo`: The consuming target's target toolchain information
+
+:::
diff --git a/docs/sphinx/api/tools/precompiler/index.md b/docs/sphinx/api/tools/precompiler/index.md
new file mode 100644
index 0000000..1a47651
--- /dev/null
+++ b/docs/sphinx/api/tools/precompiler/index.md
@@ -0,0 +1,15 @@
+:::{bzl:currentfile} //tools/precompiler:BUILD.bazel
+:::
+
+# //tools/precompiler
+
+:::{bzl:flag} execution_requirements
+Determines the execution requirements `//tools/precompiler:precompiler` uses.
+
+This is a repeatable string_list flag. The values are `key=value` entries, each
+of which are added to the execution requirements for the `PyCompile` action to
+generate pyc files.
+
+Customizing this flag mostly allows controlling whether Bazel runs the
+precompiler as a regular worker, persistent worker, or regular action.
+:::
diff --git a/docs/sphinx/bazel_inventory.txt b/docs/sphinx/bazel_inventory.txt
index b099f42..62cbdf8 100644
--- a/docs/sphinx/bazel_inventory.txt
+++ b/docs/sphinx/bazel_inventory.txt
@@ -22,3 +22,5 @@
str bzl:type 1 rules/lib/string -
struct bzl:type 1 rules/lib/builtins/struct -
target-name bzl:doc 1 concepts/labels#target-names -
+CcInfo bzl:provider 1 rules/lib/providers/CcInfo -
+CcInfo.linking_context bzl:provider-field 1 rules/lib/providers/CcInfo#linking_context -
diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py
index e9af97a..fef083c 100644
--- a/docs/sphinx/conf.py
+++ b/docs/sphinx/conf.py
@@ -27,6 +27,7 @@
"sphinx.ext.intersphinx",
"myst_parser",
"sphinx_rtd_theme", # Necessary to get jquery to make flyout work
+ "sphinx_stardoc.stardoc",
]
# Adapted from the template code:
@@ -89,6 +90,10 @@
myst_substitutions = {}
+# --- sphinx_stardoc configuration
+
+bzl_default_repository_name = "@rules_python"
+
# -- Options for HTML output
# See https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# For additional html settings
diff --git a/docs/sphinx/precompiling.md b/docs/sphinx/precompiling.md
index e30fc94..52678e6 100644
--- a/docs/sphinx/precompiling.md
+++ b/docs/sphinx/precompiling.md
@@ -29,14 +29,15 @@
To use this approach, the two basic steps are:
1. Disable pyc files from being automatically added to runfiles:
- `--@rules_python//python/config_settings:precompile_add_to_runfiles=decided_elsewhere`,
+ {bzl:obj}`--@rules_python//python/config_settings:precompile_add_to_runfiles=decided_elsewhere`,
2. Set the `pyc_collection` attribute on the binaries/tests that should or should
not use precompiling.
-The default for the `pyc_collection` attribute is controlled by a flag, so you
-can use an opt-in or opt-out approach by setting the flag:
-* targets must opt-out: `--@rules_python//python/config_settings:pyc_collection=include_pyc`,
-* targets must opt-in: `--@rules_python//python/config_settings:pyc_collection=disabled`,
+The default for the `pyc_collection` attribute is controlled by the flag
+{bzl:obj}`--@rules_python//python/config_settings:pyc_collection`, so you
+can use an opt-in or opt-out approach by setting its value:
+* targets must opt-out: `--@rules_python//python/config_settings:pyc_collection=include_pyc`
+* targets must opt-in: `--@rules_python//python/config_settings:pyc_collection=disabled`
## Advanced precompiler customization
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index 3ab390d..5d31df5 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -130,6 +130,10 @@
bzl_library(
name = "py_cc_link_params_info_bzl",
srcs = ["py_cc_link_params_info.bzl"],
+ deps = [
+ "//python/private/common:providers_bzl",
+ "@rules_python_internal//:rules_python_config_bzl",
+ ],
)
bzl_library(
@@ -185,6 +189,7 @@
"//python/private:reexports_bzl",
"//python/private:util_bzl",
"//python/private/common:providers_bzl",
+ "@rules_python_internal//:rules_python_config_bzl",
],
)
diff --git a/python/private/common/providers.bzl b/python/private/common/providers.bzl
index ab56fbe..5b84549 100644
--- a/python/private/common/providers.bzl
+++ b/python/private/common/providers.bzl
@@ -144,63 +144,86 @@
""",
init = _PyRuntimeInfo_init,
fields = {
- "bootstrap_template": (
- "See py_runtime_rule.bzl%py_runtime.bootstrap_template for docs."
- ),
- "coverage_files": (
- "The files required at runtime for using `coverage_tool`. " +
- "Will be `None` if no `coverage_tool` was provided."
- ),
- "coverage_tool": (
- "If set, this field is a `File` representing tool used for collecting code coverage information from python tests. Otherwise, this is `None`."
- ),
- "files": (
- "If this is an in-build runtime, this field is a `depset` of `File`s" +
- "that need to be added to the runfiles of an executable target that " +
- "uses this runtime (in particular, files needed by `interpreter`). " +
- "The value of `interpreter` need not be included in this field. If " +
- "this is a platform runtime then this field is `None`."
- ),
- "implementation_name": "Optional string; the Python implementation name (`sys.implementation.name`)",
- "interpreter": (
- "If this is an in-build runtime, this field is a `File` representing " +
- "the interpreter. Otherwise, this is `None`. Note that an in-build " +
- "runtime can use either a prebuilt, checked-in interpreter or an " +
- "interpreter built from source."
- ),
- "interpreter_path": (
- "If this is a platform runtime, this field is the absolute " +
- "filesystem path to the interpreter on the target platform. " +
- "Otherwise, this is `None`."
- ),
- "interpreter_version_info": (
- "Version information about the interpreter this runtime provides. " +
- "It should match the format given by `sys.version_info`, however " +
- "for simplicity, the micro, releaselevel, and serial values are " +
- "optional." +
- "A struct with the following fields:\n" +
- " * major: int, the major version number\n" +
- " * minor: int, the minor version number\n" +
- " * micro: optional int, the micro version number\n" +
- " * releaselevel: optional str, the release level\n" +
- " * serial: optional int, the serial number of the release"
- ),
- "pyc_tag": """
-Optional string; the tag portion of a pyc filename, e.g. the `cpython-39` infix
-of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed
-from `implementation_name` and `interpreter_version_info`. If no pyc_tag is
-available, then only source-less pyc generation will function correctly.
+ "bootstrap_template": """
+:type: File
+
+See py_runtime_rule.bzl%py_runtime.bootstrap_template for docs.
""",
- "python_version": (
- "Indicates whether this runtime uses Python major version 2 or 3. " +
- "Valid values are (only) `\"PY2\"` and " +
- "`\"PY3\"`."
- ),
- "stub_shebang": (
- "\"Shebang\" expression prepended to the bootstrapping Python stub " +
- "script used when executing `py_binary` targets. Does not " +
- "apply to Windows."
- ),
+ "coverage_files": """
+:type: depset[File] | None
+
+The files required at runtime for using `coverage_tool`. Will be `None` if no
+`coverage_tool` was provided.
+""",
+ "coverage_tool": """
+:type: File | None
+
+If set, this field is a `File` representing tool used for collecting code
+coverage information from python tests. Otherwise, this is `None`.
+""",
+ "files": """
+:type: depset[File] | None
+
+If this is an in-build runtime, this field is a `depset` of `File`s that need to
+be added to the runfiles of an executable target that uses this runtime (in
+particular, files needed by `interpreter`). The value of `interpreter` need not
+be included in this field. If this is a platform runtime then this field is
+`None`.
+""",
+ "implementation_name": """
+:type: str | None
+
+The Python implementation name (`sys.implementation.name`)
+""",
+ "interpreter": """
+:type: File | None
+
+If this is an in-build runtime, this field is a `File` representing the
+interpreter. Otherwise, this is `None`. Note that an in-build runtime can use
+either a prebuilt, checked-in interpreter or an interpreter built from source.
+""",
+ "interpreter_path": """
+:type: str | None
+
+If this is a platform runtime, this field is the absolute filesystem path to the
+interpreter on the target platform. Otherwise, this is `None`.
+""",
+ "interpreter_version_info": """
+:type: struct
+
+Version information about the interpreter this runtime provides.
+It should match the format given by `sys.version_info`, however
+for simplicity, the micro, releaselevel, and serial values are
+optional.
+A struct with the following fields:
+* `major`: {type}`int`, the major version number
+* `minor`: {type}`int`, the minor version number
+* `micro`: {type}`int | None`, the micro version number
+* `releaselevel`: {type}`str | None`, the release level
+* `serial`: {type}`int | None`, the serial number of the release
+""",
+ "pyc_tag": """
+:type: str | None
+
+The tag portion of a pyc filename, e.g. the `cpython-39` infix
+of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed
+from {obj}`implementation_name` and {obj}`interpreter_version_info`. If no
+pyc_tag is available, then only source-less pyc generation will function
+correctly.
+""",
+ "python_version": """
+:type: str
+
+Indicates whether this runtime uses Python major version 2 or 3. Valid values
+are (only) `"PY2"` and `"PY3"`.
+""",
+ "stub_shebang": """
+:type: str
+
+"Shebang" expression prepended to the bootstrapping Python stub
+script used when executing {obj}`py_binary` targets. Does not
+apply to Windows.
+""",
},
)
@@ -248,26 +271,43 @@
init = _PyInfo_init,
fields = {
"direct_pyc_files": """
-depset[File] of precompiled Python files that are considered directly provided
+:type: depset[File]
+
+Precompiled Python files that are considered directly provided
by the target.
""",
- "has_py2_only_sources": "Whether any of this target's transitive sources requires a Python 2 runtime.",
- "has_py3_only_sources": "Whether any of this target's transitive sources requires a Python 3 runtime.",
+ "has_py2_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 2 runtime.
+""",
+ "has_py3_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 3 runtime.
+""",
"imports": """\
+:type: depset[str]
+
A depset of import path strings to be added to the `PYTHONPATH` of executable
Python targets. These are accumulated from the transitive `deps`.
The order of the depset is not guaranteed and may be changed in the future. It
is recommended to use `default` order (the default).
""",
"transitive_pyc_files": """
-depset[File] of direct and transitive precompiled Python files that are provied
-by the target.
+:type: depset[File]
+
+Direct and transitive precompiled Python files that are provided by the target.
""",
"transitive_sources": """\
+:type: depset[File]
+
A (`postorder`-compatible) depset of `.py` files appearing in the target's
`srcs` and the `srcs` of the target's transitive `deps`.
""",
"uses_shared_libraries": """
+:type: bool
+
Whether any of this target's transitive `deps` has a shared library file (such
as a `.so` file).
@@ -283,11 +323,15 @@
# buildifier: disable=name-conventions
PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider(
- doc = ("Python-wrapper to forward CcInfo.linking_context. This is to " +
+ doc = ("Python-wrapper to forward {obj}`CcInfo.linking_context`. This is to " +
"allow Python targets to propagate C++ linking information, but " +
"without the Python target appearing to be a valid C++ rule dependency"),
init = _PyCcLinkParamsProvider_init,
fields = {
- "cc_info": "A CcInfo instance; it has only linking_context set",
+ "cc_info": """
+:type: CcInfo
+
+Linking information; it has only {obj}`CcInfo.linking_context` set.
+""",
},
)
diff --git a/python/private/py_cc_toolchain_info.bzl b/python/private/py_cc_toolchain_info.bzl
index a47a6a5..ae46bf4 100644
--- a/python/private/py_cc_toolchain_info.bzl
+++ b/python/private/py_cc_toolchain_info.bzl
@@ -18,7 +18,9 @@
doc = "C/C++ information about the Python runtime.",
fields = {
"headers": """\
-(struct) Information about the header files, with fields:
+:type: struct
+
+Information about the header files, struct with fields:
* providers_map: a dict of string to provider instances. The key should be
a fully qualified name (e.g. `@rules_foo//bar:baz.bzl#MyInfo`) of the
provider to uniquely identify its type.
@@ -39,7 +41,9 @@
represents).
""",
"libs": """\
-(struct) Information about C libraries, with fields:
+:type: struct
+
+Information about C libraries, struct with fields:
* providers_map: A dict of string to provider instances. The key should be
a fully qualified name (e.g. `@rules_foo//bar:baz.bzl#MyInfo`) of the
provider to uniquely identify its type.
@@ -59,6 +63,10 @@
e.g. `:current_py_cc_headers` to act as the underlying headers target it
represents).
""",
- "python_version": "(str) The Python Major.Minor version.",
+ "python_version": """
+:type: str
+
+The Python Major.Minor version.
+""",
},
)
diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl
index d17b008..02f9a5b 100644
--- a/python/private/py_runtime_pair_rule.bzl
+++ b/python/private/py_runtime_pair_rule.bzl
@@ -98,15 +98,12 @@
Usually the wrapped runtimes are declared using the `py_runtime` rule, but any
rule returning a `PyRuntimeInfo` provider may be used.
-This rule returns a `platform_common.ToolchainInfo` provider with the following
-schema:
+This rule returns a {obj}`ToolchainInfo` provider with fields:
-```python
-platform_common.ToolchainInfo(
- py2_runtime = <PyRuntimeInfo or None>,
- py3_runtime = <PyRuntimeInfo or None>,
-)
-```
+* `py2_runtime`: {type}`PyRuntimeInfo | None`, runtime information for a
+ Python 2 runtime.
+* `py3_runtime`: {type}`PyRuntimeInfo | None`. runtime information for a
+ Python 3 runtime.
Example usage:
diff --git a/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/private/proto_to_markdown.py
index 18d4e1e..d667eec 100644
--- a/sphinxdocs/private/proto_to_markdown.py
+++ b/sphinxdocs/private/proto_to_markdown.py
@@ -80,6 +80,12 @@
yield i == 0, i == len(values) - 1, value
+def _sort_attributes_inplace(attributes):
+ # Sort attributes so the iteration order results in a Python-syntax
+ # valid signature. Keep name first because that's convention.
+ attributes.sort(key=lambda a: (a.name != "name", bool(a.default_value), a.name))
+
+
class _MySTRenderer:
def __init__(
self,
@@ -99,6 +105,9 @@
bzl_path = self._public_load_path
else:
bzl_path = "//" + self._module.file.split("//")[1]
+
+ self._write(":::{default-domain} bzl\n:::\n")
+ self._write(":::{bzl:currentfile} ", bzl_path, "\n:::\n\n")
self._write(
f"# {bzl_path}\n",
"\n",
@@ -129,320 +138,344 @@
self._write("\n")
def _render_aspect(self, aspect: stardoc_output_pb2.AspectInfo):
- aspect_anchor = _anchor_id(aspect.aspect_name)
- self._write(
- _block_attrs(".starlark-object"),
- f"## {aspect.aspect_name}\n\n",
- "_Propagates on attributes:_ ", # todo add link here
- ", ".join(sorted(f"`{attr}`" for attr in aspect.aspect_attribute)),
- "\n\n",
- aspect.doc_string.strip(),
- "\n\n",
- )
+ _sort_attributes_inplace(aspect.attribute)
+ self._write("::::::{bzl:aspect} ", aspect.aspect_name, "\n\n")
+ edges = ", ".join(sorted(f"`{attr}`" for attr in aspect.aspect_attribute))
+ self._write(":aspect-attributes: ", edges, "\n\n")
+ self._write(aspect.doc_string.strip(), "\n\n")
if aspect.attribute:
- self._render_attributes(aspect_anchor, aspect.attribute)
- self._write("\n")
+ self._render_attributes(aspect.attribute)
+ self._write("\n")
+ self._write("::::::\n")
def _render_module_extension(self, mod_ext: stardoc_output_pb2.ModuleExtensionInfo):
- self._write(
- _block_attrs(".starlark-object"),
- f"## {mod_ext.extension_name}\n\n",
- )
-
+ self._write("::::::{bzl:module-extension} ", mod_ext.extension_name, "\n\n")
self._write(mod_ext.doc_string.strip(), "\n\n")
- mod_ext_anchor = _anchor_id(mod_ext.extension_name)
for tag in mod_ext.tag_class:
tag_name = f"{mod_ext.extension_name}.{tag.tag_name}"
- tag_anchor = f"{mod_ext_anchor}_{tag.tag_name}"
- self._write(
- _block_attrs(".starlark-module-extension-tag-class"),
- f"### {tag_name}\n\n",
- )
+ tag_name = f"{tag.tag_name}"
+ self._write(":::::{bzl:tag-class} ", tag_name, "\n\n")
+
+ _sort_attributes_inplace(tag.attribute)
self._render_signature(
tag_name,
- tag_anchor,
tag.attribute,
get_name=lambda a: a.name,
get_default=lambda a: a.default_value,
)
self._write(tag.doc_string.strip(), "\n\n")
- self._render_attributes(tag_anchor, tag.attribute)
- self._write("\n")
+ self._render_attributes(tag.attribute)
+ self._write(":::::\n")
+ self._write("::::::\n")
def _render_repository_rule(self, repo_rule: stardoc_output_pb2.RepositoryRuleInfo):
- self._write(
- _block_attrs(".starlark-object"),
- f"## {repo_rule.rule_name}\n\n",
- )
- repo_anchor = _anchor_id(repo_rule.rule_name)
+ self._write("::::::{bzl:repo-rule} ")
+ _sort_attributes_inplace(repo_rule.attribute)
self._render_signature(
repo_rule.rule_name,
- repo_anchor,
repo_rule.attribute,
get_name=lambda a: a.name,
get_default=lambda a: a.default_value,
)
self._write(repo_rule.doc_string.strip(), "\n\n")
if repo_rule.attribute:
- self._render_attributes(repo_anchor, repo_rule.attribute)
+ self._render_attributes(repo_rule.attribute)
if repo_rule.environ:
- self._write(
- "**ENVIRONMENT VARIABLES** ",
- _link_here_icon(repo_anchor + "_env"),
- "\n",
- )
- for name in sorted(repo_rule.environ):
- self._write(f"* `{name}`\n")
+ self._write(":envvars: ", ", ".join(sorted(repo_rule.environ)))
self._write("\n")
def _render_rule(self, rule: stardoc_output_pb2.RuleInfo):
rule_name = rule.rule_name
- rule_anchor = _anchor_id(rule_name)
- self._write(
- _block_attrs(".starlark-object"),
- f"## {rule_name}\n\n",
- )
-
+ _sort_attributes_inplace(rule.attribute)
+ self._write("::::{bzl:rule} ")
self._render_signature(
rule_name,
- rule_anchor,
rule.attribute,
get_name=lambda r: r.name,
get_default=lambda r: r.default_value,
)
-
self._write(rule.doc_string.strip(), "\n\n")
- if len(rule.advertised_providers.provider_name) == 0:
- self._write("_Provides_: no providers advertised.")
- else:
- self._write(
- "_Provides_: ",
- ", ".join(rule.advertised_providers.provider_name),
- )
- self._write("\n\n")
+ if rule.advertised_providers.provider_name:
+ self._write(":provides: ")
+ self._write(" | ".join(rule.advertised_providers.provider_name))
+ self._write("\n")
+ self._write("\n")
if rule.attribute:
- self._render_attributes(rule_anchor, rule.attribute)
+ self._render_attributes(rule.attribute)
+ self._write("\n")
+ self._write("::::\n")
def _rule_attr_type_string(self, attr: stardoc_output_pb2.AttributeInfo) -> str:
if attr.type == _AttributeType.NAME:
- return _link("Name", ref="target-name")
+ return "Name"
elif attr.type == _AttributeType.INT:
- return _link("int", ref="int")
+ return "int"
elif attr.type == _AttributeType.LABEL:
- return _link("label", ref="attr-label")
+ return "label"
elif attr.type == _AttributeType.STRING:
- return _link("string", ref="str")
+ return "str"
elif attr.type == _AttributeType.STRING_LIST:
- return "list of " + _link("string", ref="str")
+ return "list[str]"
elif attr.type == _AttributeType.INT_LIST:
- return "list of " + _link("int", ref="int")
+ return "list[int]"
elif attr.type == _AttributeType.LABEL_LIST:
- return "list of " + _link("label", ref="attr-label") + "s"
+ return "list[label]"
elif attr.type == _AttributeType.BOOLEAN:
- return _link("bool", ref="bool")
+ return "bool"
elif attr.type == _AttributeType.LABEL_STRING_DICT:
- return "dict of {key} to {value}".format(
- key=_link("label", ref="attr-label"), value=_link("string", ref="str")
- )
+ return "dict[label, str]"
elif attr.type == _AttributeType.STRING_DICT:
- return "dict of {key} to {value}".format(
- key=_link("string", ref="str"), value=_link("string", ref="str")
- )
+ return "dict[str, str]"
elif attr.type == _AttributeType.STRING_LIST_DICT:
- return "dict of {key} to list of {value}".format(
- key=_link("string", ref="str"), value=_link("string", ref="str")
- )
+ return "dict[str, list[str]]"
elif attr.type == _AttributeType.OUTPUT:
- return _link("label", ref="attr-label")
+ return "label"
elif attr.type == _AttributeType.OUTPUT_LIST:
- return "list of " + _link("label", ref="attr-label")
+ return "list[label]"
else:
# If we get here, it means the value was unknown for some reason.
# Rather than error, give some somewhat understandable value.
return _AttributeType.Name(attr.type)
def _render_func(self, func: stardoc_output_pb2.StarlarkFunctionInfo):
- func_name = func.function_name
- func_anchor = _anchor_id(func_name)
- self._write(
- _block_attrs(".starlark-object"),
- f"## {func_name}\n\n",
- )
+ self._write("::::::{bzl:function} ")
- parameters = [param for param in func.parameter if param.name != "self"]
-
- self._render_signature(
- func_name,
- func_anchor,
- parameters,
- get_name=lambda p: p.name,
- get_default=lambda p: p.default_value,
- )
+ parameters = self._render_func_signature(func)
self._write(func.doc_string.strip(), "\n\n")
if parameters:
- self._write(
- _block_attrs(f"{func_anchor}_parameters"),
- "**PARAMETERS** ",
- _link_here_icon(f"{func_anchor}_parameters"),
- "\n\n",
- )
- entries = []
for param in parameters:
- entries.append(
- [
- f"{func_anchor}_{param.name}",
- param.name,
- f"(_default `{param.default_value}`_) "
- if param.default_value
- else "",
- param.doc_string if param.doc_string else "_undocumented_",
- ]
- )
- self._render_field_list(entries)
+ self._write(f":arg {param.name}:\n")
+ if param.default_value:
+ default_value = self._format_default_value(param.default_value)
+ self._write(" {default-value}`", default_value, "`\n")
+ if param.doc_string:
+ self._write(" ", _indent_block_text(param.doc_string), "\n")
+ else:
+ self._write(" _undocumented_\n")
+ self._write("\n")
- if getattr(func, "return").doc_string:
- return_doc = _indent_block_text(getattr(func, "return").doc_string)
- self._write(
- _block_attrs(f"{func_anchor}_returns"),
- "RETURNS",
- _link_here_icon(func_anchor + "_returns"),
- "\n",
- ": ",
- return_doc,
- "\n",
- )
+ if return_doc := getattr(func, "return").doc_string:
+ self._write(":returns:\n")
+ self._write(" ", _indent_block_text(return_doc), "\n")
if func.deprecated.doc_string:
- self._write(
- "\n\n**DEPRECATED**\n\n", func.deprecated.doc_string.strip(), "\n"
- )
+ self._write(":::::{deprecated}: unknown\n")
+ self._write(" ", _indent_block_text(func.deprecated.doc_string), "\n")
+ self._write(":::::\n")
+ self._write("::::::\n")
+
+ def _render_func_signature(self, func):
+ self._write(f"{func.function_name}(")
+ # TODO: Have an "is method" directive in the docstring to decide if
+ # the self parameter should be removed.
+ parameters = [param for param in func.parameter if param.name != "self"]
+
+ # Unfortunately, the stardoc info is incomplete and inaccurate:
+ # * The position of the `*args` param is wrong; it'll always
+ # be last (or second to last, if kwargs is present).
+ # * Stardoc doesn't explicitly tell us if an arg is `*args` or
+ # `**kwargs`. Hence f(*args) or f(**kwargs) is ambigiguous.
+ # See these issues:
+ # https://github.com/bazelbuild/stardoc/issues/226
+ # https://github.com/bazelbuild/stardoc/issues/225
+ #
+ # Below, we try to take what info we have and infer what the original
+ # signature was. In short:
+ # * A default=empty, mandatory=false arg is either *args or **kwargs
+ # * If two of those are seen, the first is *args and the second is
+ # **kwargs. Recall, however, the position of *args is mis-represented.
+ # * If a single default=empty, mandatory=false arg is found, then
+ # it's ambiguous as to whether its *args or **kwargs. To figure
+ # that out, we:
+ # * If it's not the last arg, then it must be *args. In practice,
+ # this never occurs due to #226 above.
+ # * If we saw a mandatory arg after an optional arg, then *args
+ # was supposed to be between them (otherwise it wouldn't be
+ # valid syntax).
+ # * Otherwise, it's ambiguous. We just guess by looking at the
+ # parameter name.
+ var_args = None
+ var_kwargs = None
+ saw_mandatory_after_optional = False
+ first_mandatory_after_optional_index = None
+ optionals_started = False
+ for i, p in enumerate(parameters):
+ optionals_started = optionals_started or not p.mandatory
+ if p.mandatory and optionals_started:
+ saw_mandatory_after_optional = True
+ if first_mandatory_after_optional_index is None:
+ first_mandatory_after_optional_index = i
+
+ if not p.default_value and not p.mandatory:
+ if var_args is None:
+ var_args = (i, p)
+ else:
+ var_kwargs = p
+
+ if var_args and not var_kwargs:
+ if var_args[0] != len(parameters) - 1:
+ pass
+ elif saw_mandatory_after_optional:
+ var_kwargs = var_args[1]
+ var_args = None
+ elif var_args[1].name in ("kwargs", "attrs"):
+ var_kwargs = var_args[1]
+ var_args = None
+
+ # Partial workaround for
+ # https://github.com/bazelbuild/stardoc/issues/226: `*args` renders last
+ if var_args and var_kwargs and first_mandatory_after_optional_index is not None:
+ parameters.pop(var_args[0])
+ parameters.insert(first_mandatory_after_optional_index, var_args[1])
+
+ # The only way a mandatory-after-optional can occur is
+ # if there was `*args` before it. But if we didn't see it,
+ # it must have been the unbound `*` symbol, which stardoc doesn't
+ # tell us exists.
+ if saw_mandatory_after_optional and not var_args:
+ self._write("*, ")
+ for _, is_last, p in _position_iter(parameters):
+ if var_args and p.name == var_args[1].name:
+ self._write("*")
+ elif var_kwargs and p.name == var_kwargs.name:
+ self._write("**")
+ self._write(p.name)
+ if p.default_value:
+ self._write("=", self._format_default_value(p.default_value))
+ if not is_last:
+ self._write(", ")
+ self._write(")\n")
+ return parameters
def _render_provider(self, provider: stardoc_output_pb2.ProviderInfo):
- self._write(
- _block_attrs(".starlark-object"),
- f"## {provider.provider_name}\n\n",
- )
-
- provider_anchor = _anchor_id(provider.provider_name)
- self._render_signature(
- provider.provider_name,
- provider_anchor,
- provider.field_info,
- get_name=lambda f: f.name,
- )
+ self._write("::::::{bzl:provider} ", provider.provider_name, "\n")
+ if provider.origin_key:
+ self._render_origin_key_option(provider.origin_key)
+ self._write("\n")
self._write(provider.doc_string.strip(), "\n\n")
- if provider.field_info:
- self._write(
- _block_attrs(provider_anchor),
- "**FIELDS** ",
- _link_here_icon(provider_anchor + "_fields"),
- "\n",
- "\n",
- )
- entries = []
- for field in provider.field_info:
- entries.append(
- [
- f"{provider_anchor}_{field.name}",
- field.name,
- field.doc_string,
- ]
- )
- self._render_field_list(entries)
-
- def _render_attributes(
- self, base_anchor: str, attributes: list[stardoc_output_pb2.AttributeInfo]
- ):
- self._write(
- _block_attrs(f"{base_anchor}_attributes"),
- "**ATTRIBUTES** ",
- _link_here_icon(f"{base_anchor}_attributes"),
- "\n",
+ self._write(":::::{bzl:function} ")
+ provider.field_info.sort(key=lambda f: f.name)
+ self._render_signature(
+ "<init>",
+ provider.field_info,
+ get_name=lambda f: f.name,
)
- entries = []
- for attr in attributes:
- anchor = f"{base_anchor}_{attr.name}"
- required = "required" if attr.mandatory else "optional"
- attr_type = self._rule_attr_type_string(attr)
- default = f", default `{attr.default_value}`" if attr.default_value else ""
- providers_parts = []
- if attr.provider_name_group:
- providers_parts.append("\n\n_Required providers_: ")
- if len(attr.provider_name_group) == 1:
- provider_group = attr.provider_name_group[0]
- if len(provider_group.provider_name) == 1:
- providers_parts.append(provider_group.provider_name[0])
- else:
- providers_parts.extend(
- ["all of ", _join_csv_and(provider_group.provider_name)]
- )
- elif len(attr.provider_name_group) > 1:
- providers_parts.append("any of \n")
- for group in attr.provider_name_group:
- providers_parts.extend(["* ", _join_csv_and(group.provider_name)])
- if providers_parts:
- providers_parts.append("\n")
+ # TODO: Add support for provider.init once our Bazel version supports
+ # that field
+ self._write(":::::\n")
- entries.append(
- [
- anchor,
- attr.name,
- f"_({required} {attr_type}{default})_\n",
- attr.doc_string,
- *providers_parts,
- ]
- )
- self._render_field_list(entries)
+ for field in provider.field_info:
+ self._write(":::::{bzl:provider-field} ", field.name, "\n")
+ self._write(field.doc_string.strip())
+ self._write("\n")
+ self._write(":::::\n")
+ self._write("::::::\n")
+
+ def _render_attributes(self, attributes: list[stardoc_output_pb2.AttributeInfo]):
+ for attr in attributes:
+ attr_type = self._rule_attr_type_string(attr)
+ self._write(f":attr {attr.name}:\n")
+ if attr.default_value:
+ self._write(" {bzl:default-value}`%s`\n" % attr.default_value)
+ self._write(" {type}`%s`\n" % attr_type)
+ self._write(" ", _indent_block_text(attr.doc_string), "\n")
+ self._write(" :::{bzl:attr-info} Info\n")
+ if attr.mandatory:
+ self._write(" :mandatory:\n")
+ self._write(" :::\n")
+ self._write("\n")
+
+ if attr.provider_name_group:
+ self._write(" {required-providers}`")
+ for _, outer_is_last, provider_group in _position_iter(
+ attr.provider_name_group
+ ):
+ pairs = list(
+ zip(
+ provider_group.origin_key,
+ provider_group.provider_name,
+ strict=True,
+ )
+ )
+ if len(pairs) > 1:
+ self._write("[")
+ for _, inner_is_last, (origin_key, name) in _position_iter(pairs):
+ if origin_key.file == "<native>":
+ origin = origin_key.name
+ else:
+ origin = f"{origin_key.file}%{origin_key.name}"
+ # We have to use "title <ref>" syntax because the same
+ # name might map to different origins. Stardoc gives us
+ # the provider's actual name, not the name of the symbol
+ # used in the source.
+ self._write(f"'{name} <{origin}>'")
+ if not inner_is_last:
+ self._write(", ")
+
+ if len(pairs) > 1:
+ self._write("]")
+
+ if not outer_is_last:
+ self._write(" | ")
+ self._write("`\n")
+
+ self._write("\n")
def _render_signature(
self,
name: str,
- base_anchor: str,
parameters: list[_T],
*,
get_name: Callable[_T, str],
get_default: Callable[_T, str] = lambda v: None,
):
- self._write(_block_attrs(".starlark-signature"), name, "(")
+ self._write(name, "(")
for _, is_last, param in _position_iter(parameters):
param_name = get_name(param)
- self._write(_link(param_name, f"{base_anchor}_{param_name}"))
+ self._write(f"{param_name}")
default_value = get_default(param)
if default_value:
+ default_value = self._format_default_value(default_value)
self._write(f"={default_value}")
if not is_last:
- self._write(",\n")
+ self._write(", ")
self._write(")\n\n")
- def _render_field_list(self, entries: list[list[str]]):
- """Render a list of field lists.
+ def _render_origin_key_option(self, origin_key, indent=""):
+ self._write(
+ indent,
+ ":origin-key: ",
+ self._format_option_value(f"{origin_key.file}%{origin_key.name}"),
+ "\n",
+ )
- Args:
- entries: list of field list entries. Each element is 3
- pieces: an anchor, field description, and one or more
- text strings for the body of the field list entry.
- """
- for anchor, description, *body_pieces in entries:
- body_pieces = [_block_attrs(anchor), *body_pieces]
- self._write(
- ":",
- _span(description + _link_here_icon(anchor)),
- ":\n ",
- # The text has to be indented to be associated with the block correctly.
- "".join(body_pieces).strip().replace("\n", "\n "),
- "\n",
- )
- # Ensure there is an empty line after the field list, otherwise
- # the next line of content will fold into the field list
- self._write("\n")
+ def _format_default_value(self, default_value):
+ # Handle <function foo from //baz:bar.bzl>
+ # For now, just use quotes for lack of a better option
+ if default_value.startswith("<"):
+ return f"'{default_value}'"
+ elif default_value.startswith("Label("):
+ # Handle Label(*, "@some//label:target")
+ start_quote = default_value.find('"')
+ end_quote = default_value.rfind('"')
+ return default_value[start_quote : end_quote + 1]
+ else:
+ return default_value
+
+ def _format_option_value(self, value):
+ # Leading @ symbols are special markup; escape them.
+ if value.startswith("@"):
+ return "\\" + value
+ else:
+ return value
def _write(self, *lines: str):
self._out_stream.writelines(lines)
@@ -452,21 +485,15 @@
*,
proto: pathlib.Path,
output: pathlib.Path,
- footer: pathlib.Path,
public_load_path: str,
):
- if footer:
- footer_content = footer.read_text()
-
module = stardoc_output_pb2.ModuleInfo.FromString(proto.read_bytes())
with output.open("wt", encoding="utf8") as out_stream:
_MySTRenderer(module, out_stream, public_load_path).render()
- out_stream.write(footer_content)
def _create_parser():
parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
- parser.add_argument("--footer", dest="footer", type=pathlib.Path)
parser.add_argument("--proto", dest="proto", type=pathlib.Path)
parser.add_argument("--output", dest="output", type=pathlib.Path)
parser.add_argument("--public-load-path", dest="public_load_path")
@@ -478,7 +505,6 @@
_convert(
proto=options.proto,
output=options.output,
- footer=options.footer,
public_load_path=options.public_load_path,
)
return 0
diff --git a/sphinxdocs/private/sphinx_server.py b/sphinxdocs/private/sphinx_server.py
index e71889a..1f4fae8 100644
--- a/sphinxdocs/private/sphinx_server.py
+++ b/sphinxdocs/private/sphinx_server.py
@@ -2,6 +2,7 @@
import errno
import os
import sys
+import time
from http import server
@@ -17,17 +18,33 @@
address = ("0.0.0.0", 8000)
# with server.ThreadingHTTPServer(address, DirectoryHandler) as (ip, port, httpd):
with _start_server(DirectoryHandler, "0.0.0.0", 8000) as (ip, port, httpd):
- print(f"Serving...")
- print(f" Address: http://{ip}:{port}")
- print(f" Serving directory: {serve_directory}")
- print(f" CWD: {os.getcwd()}")
- print()
- print("*** You do not need to restart this server to see changes ***")
- print()
- try:
- httpd.serve_forever()
- except KeyboardInterrupt:
- pass
+
+ def _print_server_info():
+ print(f"Serving...")
+ print(f" Address: http://{ip}:{port}")
+ print(f" Serving directory: {serve_directory}")
+ print(f" url: file://{serve_directory}")
+ print(f" Server CWD: {os.getcwd()}")
+ print()
+ print("*** You do not need to restart this server to see changes ***")
+ print("*** CTRL+C once to reprint this info ***")
+ print("*** CTRL+C twice to exit ***")
+ print()
+
+ while True:
+ _print_server_info()
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ _print_server_info()
+ print(
+ "*** KeyboardInterrupt received: CTRL+C again to terminate server ***"
+ )
+ try:
+ time.sleep(1)
+ print("Restarting serving ...")
+ except KeyboardInterrupt:
+ break
return 0
@@ -37,6 +54,7 @@
try:
with server.ThreadingHTTPServer((ip, port), handler) as httpd:
yield ip, port, httpd
+ return
except OSError as e:
if e.errno == errno.EADDRINUSE:
pass
diff --git a/sphinxdocs/private/sphinx_stardoc.bzl b/sphinxdocs/private/sphinx_stardoc.bzl
index 810dca3..e2b1756 100644
--- a/sphinxdocs/private/sphinx_stardoc.bzl
+++ b/sphinxdocs/private/sphinx_stardoc.bzl
@@ -19,7 +19,7 @@
load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc")
load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility
-def sphinx_stardocs(name, docs, footer = None, **kwargs):
+def sphinx_stardocs(name, docs, **kwargs):
"""Generate Sphinx-friendly Markdown docs using Stardoc for bzl libraries.
A `build_test` for the docs is also generated to ensure Stardoc is able
@@ -39,7 +39,6 @@
* A `dict` with keys `input` and `dep`. The `input` key is a string
label to the bzl file to generate docs for. The `dep` key is a
string label to a `bzl_library` providing the necessary dependencies.
- footer: optional [`label`] File to append to generated docs.
**kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target
"""
add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs")
@@ -60,7 +59,6 @@
doc_name = "_{}_{}".format(name.lstrip("_"), out_name.replace("/", "_"))
_sphinx_stardoc(
name = doc_name,
- footer = footer,
out = out_name,
**stardoc_kwargs
)
@@ -77,7 +75,7 @@
**common_kwargs
)
-def _sphinx_stardoc(*, name, out, footer = None, public_load_path = None, **kwargs):
+def _sphinx_stardoc(*, name, out, public_load_path = None, **kwargs):
stardoc_name = "_{}_stardoc".format(name.lstrip("_"))
stardoc_pb = stardoc_name + ".binaryproto"
@@ -95,7 +93,6 @@
name = name,
src = stardoc_pb,
output = out,
- footer = footer,
public_load_path = public_load_path,
)
@@ -108,9 +105,6 @@
args.add("--proto", ctx.file.src)
args.add("--output", ctx.outputs.output)
- if ctx.file.footer:
- args.add("--footer", ctx.file.footer)
- inputs.append(ctx.file.footer)
if ctx.attr.public_load_path:
args.add("--public-load-path={}".format(ctx.attr.public_load_path))
@@ -126,7 +120,6 @@
_stardoc_proto_to_markdown = rule(
implementation = _stardoc_proto_to_markdown_impl,
attrs = {
- "footer": attr.label(allow_single_file = True),
"output": attr.output(mandatory = True),
"public_load_path": attr.string(),
"src": attr.label(allow_single_file = True, mandatory = True),
diff --git a/sphinxdocs/src/sphinx_stardoc/stardoc.py b/sphinxdocs/src/sphinx_stardoc/stardoc.py
index 283fb67..be38d8a 100644
--- a/sphinxdocs/src/sphinx_stardoc/stardoc.py
+++ b/sphinxdocs/src/sphinx_stardoc/stardoc.py
@@ -133,6 +133,9 @@
self.search_priority,
)
+ def __repr__(self):
+ return f"ObjectEntry({self.full_id=}, {self.object_type=}, {self.display_name=}, {self.index_entry.docname=})"
+
# A simple helper just to document what the index tuple nodes are.
def _index_node_tuple(
@@ -241,8 +244,12 @@
def visit_Constant(self, node: ast.Constant):
if node.value is None:
self._append(self.make_xref("None"))
+ elif isinstance(node.value, str):
+ self._append(self.make_xref(node.value))
else:
- raise InvalidValueError(f"Unexpected Constant node value: {node.value}")
+ raise InvalidValueError(
+ f"Unexpected Constant node value: ({type(node.value)}) {node.value=}"
+ )
def visit_Name(self, node: ast.Name):
xref_node = self.make_xref(node.id)
@@ -470,8 +477,9 @@
def run(self):
content_node = docutils_nodes.paragraph("", "")
- if "mandatory" in self.options:
- content_node += docutils_nodes.paragraph("", "mandatory (must be non-None)")
+ content_node += docutils_nodes.paragraph(
+ "", "mandatory" if "mandatory" in self.options else "optional"
+ )
if "executable" in self.options:
content_node += docutils_nodes.paragraph("", "Must be an executable")
@@ -496,6 +504,10 @@
* `foo(arg1, arg2=default) -> returntype`
"""
+ option_spec = sphinx_directives.ObjectDescription.option_spec | {
+ "origin-key": docutils_directives.unchanged,
+ }
+
@override
def before_content(self) -> None:
symbol_name = self.names[-1].symbol
@@ -588,7 +600,7 @@
if type_expr := self.options.get("type"):
- def make_xref(name):
+ def make_xref(name, title=None):
content_node = addnodes.desc_type(name, name)
return addnodes.pending_xref(
"",
@@ -608,25 +620,53 @@
sig_node += attr_annotation_node
if params_text:
- signature = inspect.signature_from_str(params_text)
+ try:
+ signature = inspect.signature_from_str(params_text)
+ except SyntaxError:
+ # Stardoc doesn't provide accurate info, so the reconstructed
+ # signature might not be valid syntax. Rather than fail, just
+ # provide a plain-text description of the approximate signature.
+ # See https://github.com/bazelbuild/stardoc/issues/225
+ sig_node += addnodes.desc_parameterlist(
+ # Offset by 1 to remove the surrounding parentheses
+ params_text[1:-1],
+ params_text[1:-1],
+ )
+ else:
+ last_kind = None
+ paramlist_node = addnodes.desc_parameterlist()
+ for param in signature.parameters.values():
+ if param.kind == param.KEYWORD_ONLY and last_kind in (
+ param.POSITIONAL_OR_KEYWORD,
+ param.POSITIONAL_ONLY,
+ None,
+ ):
+ # Add separator for keyword only parameter: *
+ paramlist_node += addnodes.desc_parameter(
+ "", "", addnodes.desc_sig_operator("", "*")
+ )
- paramlist_node = addnodes.desc_parameterlist()
- for param in signature.parameters.values():
- node = addnodes.desc_parameter()
- node += addnodes.desc_sig_name(rawsource="", text=param.name)
- if param.default is not param.empty:
- node += addnodes.desc_sig_operator("", "=")
- node += docutils_nodes.inline(
- "",
- param.default,
- classes=["default_value"],
- support_smartquotes=False,
- )
- paramlist_node += node
- sig_node += paramlist_node
+ last_kind = param.kind
+ node = addnodes.desc_parameter()
+ if param.kind == param.VAR_POSITIONAL:
+ node += addnodes.desc_sig_operator("", "*")
+ elif param.kind == param.VAR_KEYWORD:
+ node += addnodes.desc_sig_operator("", "**")
- if signature.return_annotation is not signature.empty:
- sig_node += addnodes.desc_returns("", signature.return_annotation)
+ node += addnodes.desc_sig_name(rawsource="", text=param.name)
+ if param.default is not param.empty:
+ node += addnodes.desc_sig_operator("", "=")
+ node += docutils_nodes.inline(
+ "",
+ param.default,
+ classes=["default_value"],
+ support_smartquotes=False,
+ )
+ paramlist_node += node
+ sig_node += paramlist_node
+
+ if signature.return_annotation is not signature.empty:
+ sig_node += addnodes.desc_returns("", signature.return_annotation)
obj_id = _BzlObjectId.from_env(self.env, relative_name)
@@ -685,9 +725,22 @@
),
)
- self.env.get_domain(self.domain).add_object(
- object_entry, alt_names=self._get_alt_names(object_entry)
- )
+ alt_names = []
+ if origin_key := self.options.get("origin-key"):
+ alt_names.append(
+ origin_key
+ # Options require \@ for leading @, but don't
+ # remove the escaping slash, so we have to do it manually
+ .lstrip("\\")
+ .lstrip("@")
+ .replace("//", "/")
+ .replace(".bzl%", ".")
+ .replace("/", ".")
+ .replace(":", ".")
+ )
+ alt_names.extend(self._get_alt_names(object_entry))
+
+ self.env.get_domain(self.domain).add_object(object_entry, alt_names=alt_names)
def _get_additional_index_types(self):
return []
@@ -1079,6 +1132,8 @@
return ""
+# TODO: Integrate with the option directive, since flags are options, afterall.
+# https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-option
class _BzlFlag(_BzlTarget):
"""Documents a flag"""
@@ -1319,10 +1374,11 @@
label = "Bzl"
# NOTE: Most every object type has "obj" as one of the roles because
- # an object type's role determine what reftypes can refer to it. By having
- # "obj" for all of them, it allows writing :bzl:obj`foo` to restrict
- # object searching to the bzl domain. Under the hood, this domain translates
- # requests for the :any: role as lookups for :obj:
+ # an object type's role determine what reftypes (cross referencing) can
+ # refer to it. By having "obj" for all of them, it allows writing
+ # :bzl:obj`foo` to restrict object searching to the bzl domain. Under the
+ # hood, this domain translates requests for the :any: role as lookups for
+ # :obj:.
# NOTE: We also use these object types for categorizing things in the
# generated index page.
object_types = {
@@ -1334,17 +1390,16 @@
"module-extension": domains.ObjType(
"module extension", "module_extension", "obj"
),
- "provider": domains.ObjType("provider", "provider", "obj"),
- "provider-field": domains.ObjType(
- "provider field", "field", "obj"
- ), # provider field
+ # Providers are close enough to types that we include "type". This
+ # also makes :type: Foo work in directive options.
+ "provider": domains.ObjType("provider", "provider", "type", "obj"),
+ "provider-field": domains.ObjType("provider field", "field", "obj"),
"repo-rule": domains.ObjType("repository rule", "repo_rule", "obj"),
"rule": domains.ObjType("rule", "rule", "obj"),
"tag-class": domains.ObjType("tag class", "tag_class", "obj"),
"target": domains.ObjType("target", "target", "obj"), # target in a build file
- "flag": domains.ObjType(
- "flag", "flag", "target", "obj"
- ), # flag-target in a build file
+ # Flags are also targets, so include "target" for xref'ing
+ "flag": domains.ObjType("flag", "flag", "target", "obj"),
# types are objects that have a constructor and methods/attrs
"type": domains.ObjType("type", "type", "obj"),
}
@@ -1393,7 +1448,7 @@
# Objects within each doc
# dict[str, dict[str, _ObjectEntry]]
"doc_names": {},
- # Objects by a shorter name
+ # Objects by a shorter or alternative name
# dict[str, _ObjectEntry]
"alt_names": {},
}
@@ -1420,9 +1475,16 @@
node: addnodes.pending_xref,
contnode: docutils_nodes.Element,
) -> list[tuple[str, docutils_nodes.Element]]:
- ref_node = self.resolve_xref(
- env, fromdocname, builder, "obj", target, node, contnode
+ del env, node # Unused
+ entry = self._find_entry_for_xref(fromdocname, "obj", target)
+ if not entry:
+ return []
+ to_docname = entry.index_entry.docname
+ to_anchor = entry.index_entry.anchor
+ ref_node = sphinx_nodes.make_refnode(
+ builder, fromdocname, to_docname, to_anchor, contnode, title=to_anchor
)
+
matches = [(f"bzl:{entry.object_type}", ref_node)]
return matches
@@ -1454,14 +1516,21 @@
def _find_entry_for_xref(
self, fromdocname: str, object_type: str, target: str
) -> _ObjectEntry | None:
- # Normalize labels to dotted notation
+ # Normalize a variety of formats to the dotted format used internally.
+ # --@foo//:bar flags
+ # --@foo//:bar=value labels
+ # //foo:bar.bzl labels
target = (
- target.lstrip("@/:")
+ target.lstrip("@/:-")
.replace("//", "/")
.replace(".bzl%", ".")
.replace("/", ".")
.replace(":", ".")
)
+ # Elide the value part of --foo=bar flags
+ # Note that the flag value could contain `=`
+ if "=" in target:
+ target = target[: target.find("=")]
if target in self.data["doc_names"].get(fromdocname, {}):
return self.data["doc_names"][fromdocname][target]
@@ -1486,7 +1555,11 @@
alt_names,
)
if entry.full_id in self.data["objects"]:
- raise Exception(f"Object {entry.full_id} already registered")
+ existing = self.data["objects"][entry.full_id]
+ raise Exception(
+ f"Object {entry.full_id} already registered: "
+ + f"existing={existing}, incoming={entry}"
+ )
self.data["objects"][entry.full_id] = entry
self.data["objects_by_type"].setdefault(entry.object_type, {})
self.data["objects_by_type"][entry.object_type][entry.full_id] = entry
diff --git a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel
index 2964785..09f5374 100644
--- a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel
+++ b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel
@@ -13,10 +13,12 @@
# limitations under the License.
load("//python:py_test.bzl", "py_test")
+load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
py_test(
name = "proto_to_markdown_test",
srcs = ["proto_to_markdown_test.py"],
+ target_compatible_with = [] if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"],
deps = [
"//sphinxdocs/private:proto_to_markdown_lib",
"@dev_pip//absl_py",
diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
index 2f5b22e..3b664a5 100644
--- a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
+++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
@@ -114,21 +114,22 @@
def test_basic_rendering_everything(self):
actual = self._render(_EVERYTHING_MODULE)
+ self.assertIn("{bzl:currentfile} //pkg:foo.bzl", actual)
self.assertRegex(actual, "# //pkg:foo.bzl")
self.assertRegex(actual, "MODULE_DOC_STRING")
- self.assertRegex(actual, "## rule_1.*")
+ self.assertRegex(actual, "{bzl:rule} rule_1.*")
self.assertRegex(actual, "RULE_1_DOC_STRING")
self.assertRegex(actual, "rule_1_attr_1")
self.assertRegex(actual, "RULE_1_ATTR_1_DOC_STRING")
self.assertRegex(actual, "RULE_1_ATTR_1_DEFAULT_VALUE")
- self.assertRegex(actual, "## ProviderAlpha")
+ self.assertRegex(actual, "{bzl:provider} ProviderAlpha")
self.assertRegex(actual, "PROVIDER_ALPHA_DOC_STRING")
self.assertRegex(actual, "ProviderAlpha_field_a")
self.assertRegex(actual, "PROVIDER_ALPHA_FIELD_A_DOC_STRING")
- self.assertRegex(actual, "## function_1")
+ self.assertRegex(actual, "{bzl:function} function_1")
self.assertRegex(actual, "FUNCTION_1_DOC_STRING")
self.assertRegex(actual, "function_1_param_a")
self.assertRegex(actual, "FUNCTION_1_PARAM_A_DOC_STRING")
@@ -136,22 +137,22 @@
self.assertRegex(actual, "FUNCTION_1_RETURN_DOC_STRING")
self.assertRegex(actual, "FUNCTION_1_DEPRECATED_DOC_STRING")
- self.assertRegex(actual, "## aspect_1")
+ self.assertRegex(actual, "{bzl:aspect} aspect_1")
self.assertRegex(actual, "ASPECT_1_DOC_STRING")
self.assertRegex(actual, "aspect_1_aspect_attribute_a")
self.assertRegex(actual, "aspect_1_attribute_a")
self.assertRegex(actual, "ASPECT_1_ATTRIBUTE_A_DOC_STRING")
self.assertRegex(actual, "694638")
- self.assertRegex(actual, "## bzlmod_ext")
+ self.assertRegex(actual, "{bzl:module-extension} bzlmod_ext")
self.assertRegex(actual, "BZLMOD_EXT_DOC_STRING")
- self.assertRegex(actual, "### bzlmod_ext.bzlmod_ext_tag_a")
+ self.assertRegex(actual, "{bzl:tag-class} bzlmod_ext_tag_a")
self.assertRegex(actual, "BZLMOD_EXT_TAG_A_DOC_STRING")
self.assertRegex(actual, "bzlmod_ext_tag_a_attribute_1")
self.assertRegex(actual, "BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DOC_STRING")
self.assertRegex(actual, "BZLMOD_EXT_TAG_A_ATTRIBUTE_1_DEFAULT_VALUE")
- self.assertRegex(actual, "## repository_rule")
+ self.assertRegex(actual, "{bzl:repo-rule} repository_rule")
self.assertRegex(actual, "REPOSITORY_RULE_DOC_STRING")
self.assertRegex(actual, "repository_rule_attribute_a")
self.assertRegex(actual, "REPOSITORY_RULE_ATTRIBUTE_A_DOC_STRING")
@@ -172,31 +173,25 @@
name: "param_without_default"
}
parameter: {
+ name: "param_with_function_default",
+ default_value: "<function foo from //bar:baz.bzl>"
+ }
+ parameter: {
+ name: "param_with_label_default",
+ default_value: 'Label(*, "@repo//pkg:file.bzl")'
+ }
+ parameter: {
name: "last_param"
}
}
"""
)
- self.assertIn("[param_with_default](#func_param_with_default)=DEFAULT,", actual)
- self.assertIn("[param_without_default](#func_param_without_default),", actual)
-
- def test_render_field_list(self):
- actual = self._render(
- """\
-file: "@repo//pkg:foo.bzl"
-func_info: {
- function_name: "func"
- parameter: {
- name: "param"
- default_value: "DEFAULT"
- }
-}
-"""
- )
- self.assertRegex(
- actual, re.compile("^:.*param.*¶.*headerlink.*:\n", re.MULTILINE)
- )
- self.assertRegex(actual, re.compile("^ .*#func_param", re.MULTILINE))
+ self.assertIn("param_with_default=DEFAULT,", actual)
+ self.assertIn("{default-value}`DEFAULT`", actual)
+ self.assertIn(":arg param_with_default:", actual)
+ self.assertIn("param_without_default,", actual)
+ self.assertIn('{default-value}`"@repo//pkg:file.bzl"`', actual)
+ self.assertIn("{default-value}`'<function foo from //bar:baz.bzl>'", actual)
if __name__ == "__main__":
diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
index 5cf5736..63e34fb 100644
--- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
+++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
@@ -1,4 +1,7 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
+load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")
sphinx_docs(
name = "docs",
@@ -6,7 +9,7 @@
include = [
"*.md",
],
- ),
+ ) + [":bzl_docs"],
config = "conf.py",
formats = [
"html",
@@ -22,7 +25,31 @@
"@platforms//os:linux": [],
"@platforms//os:macos": [],
"//conditions:default": ["@platforms//:incompatible"],
- }),
+ }) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"],
+)
+
+sphinx_stardocs(
+ name = "bzl_docs",
+ docs = {
+ "bzl_function.md": dict(
+ dep = ":all_bzl",
+ input = "//sphinxdocs/tests/sphinx_stardoc:bzl_function.bzl",
+ ),
+ "bzl_providers.md": dict(
+ dep = ":all_bzl",
+ input = "//sphinxdocs/tests/sphinx_stardoc:bzl_providers.bzl",
+ ),
+ "bzl_rule.md": dict(
+ dep = ":all_bzl",
+ input = "//sphinxdocs/tests/sphinx_stardoc:bzl_rule.bzl",
+ ),
+ },
+ target_compatible_with = [] if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"],
+)
+
+bzl_library(
+ name = "all_bzl",
+ srcs = glob(["*.bzl"]),
)
sphinx_build_binary(
diff --git a/sphinxdocs/tests/sphinx_stardoc/bzl_function.bzl b/sphinxdocs/tests/sphinx_stardoc/bzl_function.bzl
new file mode 100644
index 0000000..822ff26
--- /dev/null
+++ b/sphinxdocs/tests/sphinx_stardoc/bzl_function.bzl
@@ -0,0 +1,34 @@
+"""Tests for plain functions."""
+
+def middle_varargs(a, *args, b):
+ """Expect: `middle_varargs(a, *args, b)`
+
+ NOTE: https://github.com/bazelbuild/stardoc/issues/226: `*args` renders last
+
+ Args:
+ a: {type}`str` doc for a
+ *args: {type}`varags` doc for *args
+ b: {type}`list[str]` doc for c
+
+ """
+ _ = a, args, b # @unused
+
+def mixture(a, b = 1, *args, c, d = 2, **kwargs):
+ """Expect: `mixture(a, b=1, *args, c, d=2, **kwargs)`"""
+ _ = a, b, args, c, d, kwargs # @unused
+
+def only_varargs(*args):
+ """Expect: `only_varargs(*args)`"""
+ _ = args # @unused
+
+def only_varkwargs(**kwargs):
+ """Expect: `only_varkwargs(**kwargs)`"""
+ _ = kwargs # @unused
+
+def unnamed_varargs(*, a = 1, b):
+ """Expect: unnamed_varargs(*, a=1, b)"""
+ _ = a, b # @unused
+
+def varargs_and_varkwargs(*args, **kwargs):
+ """Expect: `varargs_and_varkwargs(*args, **kwargs)`"""
+ _ = args, kwargs # @unused
diff --git a/sphinxdocs/tests/sphinx_stardoc/bzl_providers.bzl b/sphinxdocs/tests/sphinx_stardoc/bzl_providers.bzl
new file mode 100644
index 0000000..189d975
--- /dev/null
+++ b/sphinxdocs/tests/sphinx_stardoc/bzl_providers.bzl
@@ -0,0 +1,4 @@
+"""Providers"""
+
+# buildifier: disable=provider-params
+GenericInfo = provider()
diff --git a/sphinxdocs/tests/sphinx_stardoc/bzl_rule.bzl b/sphinxdocs/tests/sphinx_stardoc/bzl_rule.bzl
new file mode 100644
index 0000000..d17c8bc
--- /dev/null
+++ b/sphinxdocs/tests/sphinx_stardoc/bzl_rule.bzl
@@ -0,0 +1,24 @@
+"""Tests for rules."""
+
+load(":bzl_providers.bzl", OtherGenericInfo = "GenericInfo")
+
+# buildifier: disable=provider-params
+GenericInfo = provider()
+
+# buildifier: disable=provider-params
+P1 = provider()
+
+# buildifier: disable=provider-params
+P2 = provider()
+
+def _impl(ctx):
+ _ = ctx # @unused
+
+my_rule = rule(
+ implementation = _impl,
+ attrs = {
+ "srcs": attr.label(
+ providers = [[GenericInfo], [OtherGenericInfo], [P1, P2], [platform_common.ToolchainInfo]],
+ ),
+ },
+)
diff --git a/sphinxdocs/tests/sphinx_stardoc/function.md b/sphinxdocs/tests/sphinx_stardoc/function.md
index b8cbd37..de7d16a 100644
--- a/sphinxdocs/tests/sphinx_stardoc/function.md
+++ b/sphinxdocs/tests/sphinx_stardoc/function.md
@@ -9,7 +9,7 @@
Module documentation
-:::{bzl:function} myfunc(foo, bar=False, baz=[]) -> FooObj
+::::::{bzl:function} myfunc(foo, bar=False, baz=[]) -> FooObj
This is a bazel function.
@@ -34,8 +34,13 @@
{bzl:return-type}`list | int`
description
+:::{deprecated} unspecified
+
+Some doc about the deprecation
:::
+::::::
+
:::{bzl:function} mylongfunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
:::
diff --git a/sphinxdocs/tests/sphinx_stardoc/rule.md b/sphinxdocs/tests/sphinx_stardoc/rule.md
index a6f3a56..0f90ed3 100644
--- a/sphinxdocs/tests/sphinx_stardoc/rule.md
+++ b/sphinxdocs/tests/sphinx_stardoc/rule.md
@@ -23,7 +23,7 @@
:mandatory: true
:::
- {required-providers}`LangInfo | [OtherLangInfo, AnotherLangInfo]`
+ {required-providers}`"Display <//lang:provider.bzl%LangInfo>"`
:attr ra2:
{type}`attr.label`
diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md
index f0ea038..9eb7b81 100644
--- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md
+++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md
@@ -48,3 +48,11 @@
* rule: {obj}`lang.rule.my_rule`
* rule attr: {obj}`lang.rule.my_rule.ra1`
* provider: {obj}`lang.provider.LangInfo`
+
+## Using origin keys
+
+* provider using `{type}`: {type}`"@rules_python//sphinxdocs/tests/sphinx_stardoc:bzl_rule.bzl%GenericInfo"`
+
+## Any xref
+
+* {any}`LangInfo`