Skip to content

Commit

Permalink
Manual update from ferrocene/ferrocene
Browse files Browse the repository at this point in the history
mirrored-commit: 71e22d4e212241da6d5ae4c5f951bc6899958e41
  • Loading branch information
Hoverbear committed Nov 20, 2024
1 parent 52d3766 commit c70ce1a
Show file tree
Hide file tree
Showing 11 changed files with 500 additions and 99 deletions.
43 changes: 36 additions & 7 deletions exts/ferrocene_domain_cli/domain.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: The Ferrocene Developers


from dataclasses import dataclass
import re
import string

from docutils import nodes
from docutils.parsers.rst import directives

from sphinx import addnodes
from sphinx.directives import SphinxDirective, ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.roles import XRefRole
import re
import sphinx
import string


PROGRAM_STORAGE = "ferrocene_domain_cli:program"
Expand All @@ -18,17 +23,29 @@ class ProgramDirective(SphinxDirective):
has_content = True
required_arguments = 1
final_argument_whitespace = True
option_spec = {"no_traceability_matrix": directives.flag}

def run(self):
# if there already is program data in storage, a ProgramDirective is
# within a ProgramDirective, which isn't supported
if PROGRAM_STORAGE in self.env.temp_data:
warn("cli:program inside cli:program isn't supported", self.get_location())
return []
self.env.temp_data[PROGRAM_STORAGE] = self.arguments[0]

# store arguments, so they can be accessed by child `OptionDirective`s
self.env.temp_data[PROGRAM_STORAGE] = ProgramStorage(
self.arguments[0],
"no_traceability_matrix" in self.options,
)

# parse and process content of `ProgramDirective``
# (one or more `OptionDirective`s)
node = nodes.container()
self.state.nested_parse(self.content, self.content_offset, node)

# clear program storage
del self.env.temp_data[PROGRAM_STORAGE]

return [node]


Expand All @@ -43,11 +60,16 @@ def handle_signature(self, sig, signode):
def add_target_and_index(self, name_cls, sig, signode):
if PROGRAM_STORAGE not in self.env.temp_data:
warn("cli:option outside cli:program isn't supported", self.get_location())
program = "PLACEHOLDER"
program_storage = ProgramStorage("PLACEHOLDER", False)
else:
program = self.env.temp_data[PROGRAM_STORAGE]
program_storage: ProgramStorage = self.env.temp_data[PROGRAM_STORAGE]

option = Option(self.env.docname, program, sig)
option = Option(
self.env.docname,
program_storage.program_name,
sig,
program_storage.no_traceability_matrix,
)

signode["ids"].append(option.id())

Expand All @@ -61,10 +83,11 @@ def add_target_and_index(self, name_cls, sig, signode):


class Option:
def __init__(self, document, program, option):
def __init__(self, document, program, option, no_traceability_matrix):
self.document = document
self.program = program
self.option = option
self.no_traceability_matrix = no_traceability_matrix

def id(self):
option = (
Expand Down Expand Up @@ -149,3 +172,9 @@ def warn(message, location):

def setup(app):
app.add_domain(CliDomain)


@dataclass
class ProgramStorage:
program_name: str
no_traceability_matrix: bool
3 changes: 3 additions & 0 deletions exts/ferrocene_domain_cli/traceability_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ def write_traceability_ids(app):

options_by_document = defaultdict(list)
for option in env.get_domain("cli").get_options().values():
if option.no_traceability_matrix:
continue

options_by_document[option.document].append(
{
"id": option.id(),
Expand Down
23 changes: 21 additions & 2 deletions exts/ferrocene_qualification/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: The Ferrocene Developers

from . import substitutions, document_id, domain, signature_page, target
from . import (
document_id,
domain,
intersphinx_support,
signature_page,
sphinx_needs_support,
substitutions,
target,
)
import string


Expand All @@ -11,16 +19,19 @@ def setup(app):
domain.setup(app)
signature_page.setup(app)
target.setup(app)
intersphinx_support.setup(app)
sphinx_needs_support.setup(app)

app.connect("config-inited", validate_config)
app.connect("config-inited", inject_version)
app.add_config_value("ferrocene_id", None, "env", [str])
app.add_config_value("ferrocene_substitutions_path", None, "env", [str])
app.add_config_value("ferrocene_target_names_path", None, "env", [str])
app.add_config_value("ferrocene_signature", None, "env", [str])
app.add_config_value("ferrocene_private_signature_files_dir", None, "env", [str])
app.add_config_value("rustfmt_version", None, "env", [str])
app.add_config_value("ferrocene_version", None, "env", [str])
app.add_config_value("rust_version", None, "env", [str])
app.add_config_value("channel", None, "env", [str])

return {
"version": "0",
Expand All @@ -40,3 +51,11 @@ def validate_config(app, config):

if any(c not in string.ascii_uppercase for c in config["ferrocene_id"]):
raise ValueError("ferrocene_id can only be uppercase letters")


def inject_version(app, config):
# sphinx-needs requires the document version to be configured in order for
# external needs to be loaded. Dynamically set it to the Ferrocene version
# if there is no existing version.
if not config.version and config.ferrocene_version:
config.version = config.ferrocene_version
92 changes: 92 additions & 0 deletions exts/ferrocene_qualification/intersphinx_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: The Ferrocene Developers

# This module adds some helpers needed to integrate Ferrocene's build system
# with InterSphinx. More specifically, the extension:
#
# - Defines the "ferrocene-intersphinx" Sphinx builder, which only produces the
# objects.inv file required by InterSphinx. This is used to gather all the
# inventories for all of our documentation before actually building anything,
# as we have circular references between documents.
#
# - Defines the "ferrocene_intersphinx_mappings" configuration, which this
# extension deserializes from JSON and then adds to the intersphinx_mapping
# configuration. This is needed because the format of intersphinx_mapping is
# too complex to be provided with the -D flag.

from sphinx.builders import Builder
from sphinx.builders.html import StandaloneHTMLBuilder
import json
import sphinx
import sphinx.ext.intersphinx


class IntersphinxBuilder(Builder):
name = "ferrocene-intersphinx"
format = ""
epilog = "InterSphinx inventory file generated."
allow_parallel = True

def init(self):
self.standalone_html_builder = StandaloneHTMLBuilder(self.app, self.env)

# Do not emit any warning in the ferrocene-intersphinx builder: there
# will be warnings when using the builder, as the rest of the documents
# won't be built yet, but we don't care about them.
#
# Keeping the warnings will confuse people who read the build logs,
# thinking they should fix them while they're expected to happen.
#
# Unfortunately the only reliable way to suppress the warnings is
# monkey-patching Sphinx's code, as you cannot set a global filter in
# Python's logging module.
sphinx.util.logging.WarningStreamHandler.emit = lambda _self, _record: None

def build(self, *args, **kwargs):
# Normally you're not supposed to override the build() method, as
# Sphinx calls all the relevant overrideable methods from it.
#
# Unfortunately though, Sphinx doesn't execute the finish() method if
# there are no outdated docs (as we're simulating in this builder).
#
# Returning all documents from get_outdated_docs() would fix that
# problem, but would also execute all the post_transforms for all
# documents, which on large documents can take a while.
#
# Instead, we're returning an empty list of outdated documents, and
# manually dumping the inventory here after the parent build() returns.
super().build(*args, **kwargs)
self.standalone_html_builder.dump_inventory()

def get_outdated_docs(self):
return []

def prepare_writing(self, docnames):
pass

def write_doc(self, docname, doctree):
pass

def get_target_uri(self, docname, typ=None):
# Defer to the standalone HTML builder to generate builders.
return self.standalone_html_builder.get_target_uri(docname, typ)


def inject_intersphinx_mappings(app, config):
if config.ferrocene_intersphinx_mappings is not None:
for inventory in json.loads(config.ferrocene_intersphinx_mappings):
config.intersphinx_mapping[inventory["name"]] = (
inventory["html_root"],
inventory["inventory"],
)


def setup(app):
# Automatically enable the sphinx.ext.intersphinx extension without
# requiring users to configure it in their conf.py.
sphinx.ext.intersphinx.setup(app)

app.add_builder(IntersphinxBuilder)

app.add_config_value("ferrocene_intersphinx_mappings", None, "env", [str])
app.connect("config-inited", inject_intersphinx_mappings, priority=1)
4 changes: 2 additions & 2 deletions exts/ferrocene_qualification/signature_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def load_file(self, name, *, copy=False):
self.loaded_files.append(path)
if copy:
self.copiable_files[name] = path
with open(path) as f:
with open(path, "r", encoding="utf-8") as f:
return f.read()

def load_private_file(self, name, *, copy=False):
Expand All @@ -69,7 +69,7 @@ def load_private_file(self, name, *, copy=False):
path = f"{self.app.config.ferrocene_private_signature_files_dir}/{uuid}"
if copy:
self.copiable_files[name] = path
with open(path) as f:
with open(path, "r", encoding="utf-8") as f:
return f.read()


Expand Down
84 changes: 84 additions & 0 deletions exts/ferrocene_qualification/sphinx_needs_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: The Ferrocene Developers

import sphinx_needs
import json


def configure_sphinx_needs(app, config):
config.needs_types = [
{
"directive": "hazop-use",
"title": "Use Case",
"prefix": "USE_",
"color": "",
"style": "",
},
{
"directive": "hazop-error",
"title": "Potential error",
"prefix": "ERR_",
"color": "",
"style": "",
},
{
"directive": "constraint",
"title": "Constraint",
"prefix": "CONSTR_",
"color": "",
"style": "",
},
{
"directive": "req",
"title": "Requirement",
"prefix": "REQ_",
"color": "",
"style": "",
},
]

config.needs_extra_links = [
{
"option": "caused_by",
"incoming": "causes",
"outgoing": "caused by",
},
{
"option": "mitigates",
"incoming": "mitigated by",
"outgoing": "mitigates",
},
{
"option": "implements",
"incoming": "implemented by",
"outgoing": "implements",
},
]

config.needs_default_layout = "ferrocene"
config.needs_layouts = {
# Forked from the builtin "clean" layout, but without the arrow to
# collapse the meta information. That arrow generates HTML that
# linkchecker doesn't like, unfortunately :(
"ferrocene": {
"grid": "simple",
"layout": {
"head": ['<<meta("type_name")>>: **<<meta("title")>>** <<meta_id()>>'],
"meta": ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"],
},
},
}

if config.ferrocene_external_needs is not None:
config.needs_external_needs = json.loads(config.ferrocene_external_needs)

config.needs_title_optional = True
config.needs_build_json = True
config.needs_reproducible_json = True


def setup(app):
sphinx_needs.setup(app)

app.add_config_value("ferrocene_external_needs", None, "env", str)
app.connect("config-inited", configure_sphinx_needs, priority=100)
5 changes: 1 addition & 4 deletions exts/ferrocene_qualification/substitutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def apply(self):
# look at sphinx-substitutions.toml for the rest of the substitutions
self.add_substitution("doc_title", self.app.config["html_short_title"])
self.add_substitution("doc_short_title", self.app.config["ferrocene_id"])
self.add_substitution("rustfmt_version", self.app.config["rustfmt_version"])
self.add_substitution(
"ferrocene_version",
self.app.config["ferrocene_version"],
Expand All @@ -39,10 +40,6 @@ def apply(self):
"rust_version",
self.app.config["rust_version"],
)
self.add_substitution(
"channel",
self.app.config["channel"],
)

def add_substitution(self, name, value):
substitution = nodes.substitution_definition()
Expand Down
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# SPDX-FileCopyrightText: The Ferrocene Developers

sphinx
sphinx-needs
sphinx-autobuild
sphinxcontrib-plantuml
tomli
myst_parser
pyyaml
Expand Down
Loading

0 comments on commit c70ce1a

Please sign in to comment.