From f53612a8b8c81ebbceda9b6995b998f94730c319 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 27 Aug 2023 04:58:33 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Centralise=20access=20to=20sphin?= =?UTF-8?q?x-needs=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sphinx_needs/api/configuration.py | 6 +- sphinx_needs/api/need.py | 48 +++--- sphinx_needs/builder.py | 22 +-- sphinx_needs/config.py | 207 +++++++++++++++++++++++- sphinx_needs/diagrams_common.py | 6 +- sphinx_needs/directives/list2need.py | 11 +- sphinx_needs/directives/need.py | 16 +- sphinx_needs/directives/needbar.py | 4 +- sphinx_needs/directives/needextend.py | 12 +- sphinx_needs/directives/needextract.py | 6 +- sphinx_needs/directives/needfilter.py | 11 +- sphinx_needs/directives/needflow.py | 27 ++-- sphinx_needs/directives/needgantt.py | 15 +- sphinx_needs/directives/needimport.py | 7 +- sphinx_needs/directives/needlist.py | 4 +- sphinx_needs/directives/needpie.py | 4 +- sphinx_needs/directives/needreport.py | 12 +- sphinx_needs/directives/needsequence.py | 10 +- sphinx_needs/directives/needservice.py | 8 +- sphinx_needs/directives/needtable.py | 20 +-- sphinx_needs/directives/needuml.py | 13 +- sphinx_needs/directives/utils.py | 5 +- sphinx_needs/environment.py | 15 +- sphinx_needs/external_needs.py | 10 +- sphinx_needs/filter_common.py | 5 +- sphinx_needs/functions/functions.py | 10 +- sphinx_needs/layout.py | 32 ++-- sphinx_needs/need_constraints.py | 7 +- sphinx_needs/needs.py | 178 +++----------------- sphinx_needs/roles/need_incoming.py | 8 +- sphinx_needs/roles/need_outgoing.py | 16 +- sphinx_needs/roles/need_ref.py | 6 +- sphinx_needs/services/github.py | 6 +- sphinx_needs/services/manager.py | 3 +- sphinx_needs/services/open_needs.py | 13 +- sphinx_needs/utils.py | 17 +- sphinx_needs/warnings.py | 4 +- 37 files changed, 470 insertions(+), 334 deletions(-) diff --git a/sphinx_needs/api/configuration.py b/sphinx_needs/api/configuration.py index 666ac4dce..87e1041f0 100644 --- a/sphinx_needs/api/configuration.py +++ b/sphinx_needs/api/configuration.py @@ -10,7 +10,7 @@ from sphinx.util.logging import SphinxLoggerAdapter from sphinx_needs.api.exceptions import NeedsApiConfigException, NeedsApiConfigWarning -from sphinx_needs.config import NEEDS_CONFIG +from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.functions import register_func from sphinx_needs.functions.functions import DynamicFunction @@ -28,7 +28,7 @@ def get_need_types(app: Sphinx) -> List[str]: :param app: Sphinx application object :return: list of strings """ - needs_types = app.config.needs_types + needs_types = NeedsSphinxConfig(app.config).types return [x["directive"] for x in needs_types] @@ -58,7 +58,7 @@ def add_need_type( """ import sphinx_needs.directives.need - needs_types = app.config.needs_types + needs_types = NeedsSphinxConfig(app.config).types type_names = [x["directive"] for x in needs_types] if directive in type_names: diff --git a/sphinx_needs/api/need.py b/sphinx_needs/api/need.py index c22cb1e9f..24e8a9d4c 100644 --- a/sphinx_needs/api/need.py +++ b/sphinx_needs/api/need.py @@ -21,6 +21,7 @@ NeedsTagNotAllowed, NeedsTemplateException, ) +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.needuml import Needuml, NeedumlException from sphinx_needs.filter_common import filter_single_need from sphinx_needs.logging import get_logger @@ -136,7 +137,8 @@ def run(): # Get environment ############################################################################################# env = app.env - types = app.config.needs_types + needs_config = NeedsSphinxConfig(app.config) + types = needs_config.types type_name = "" type_prefix = "" type_color = "" @@ -176,7 +178,7 @@ def run(): # TODO: Check, if id was already given. If True, recalculate id # id = self.options.get("id", ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for # _ in range(5))) - if id is None and app.config.needs_id_required: + if id is None and needs_config.id_required: raise NeedsNoIdException( "An id is missing for this need and must be set, because 'needs_id_required' " "is set to True in conf.py. Need '{}' in {} ({})".format(title, docname, lineno) @@ -187,12 +189,8 @@ def run(): else: need_id = id - if app.config.needs_id_regex and not re.match(app.config.needs_id_regex, need_id): - raise NeedsInvalidException( - "Given ID '{id}' does not match configured regex '{regex}'".format( - id=need_id, regex=app.config.needs_id_regex - ) - ) + if needs_config.id_regex and not re.match(needs_config.id_regex, need_id): + raise NeedsInvalidException(f"Given ID '{need_id}' does not match configured regex '{needs_config.id_regex}'") # Calculate target id, to be able to set a link back if is_external: @@ -203,7 +201,7 @@ def run(): # Handle status # Check if status is in needs_statuses. If not raise an error. - if app.config.needs_statuses and status not in [stat["name"] for stat in app.config.needs_statuses]: + if needs_config.statuses and status not in [stat["name"] for stat in needs_config.statuses]: raise NeedsStatusNotAllowed( f"Status {status} of need id {need_id} is not allowed " "by config value 'needs_statuses'." ) @@ -226,9 +224,9 @@ def run(): tags = new_tags # Check if tag is in needs_tags. If not raise an error. - if app.config.needs_tags: + if needs_config.tags: for tag in tags: - needs_tags = [tag["name"] for tag in app.config.needs_tags] + needs_tags = [tag["name"] for tag in needs_config.tags] if tag not in needs_tags: raise NeedsTagNotAllowed( f"Tag {tag} of need id {need_id} is not allowed " "by config value 'needs_tags'." @@ -258,9 +256,9 @@ def run(): constraints = new_constraints # Check if constraint is in needs_constraints. If not raise an error. - if env.app.config.needs_constraints: + if needs_config.constraints: for constraint in constraints: - if constraint not in env.app.config.needs_constraints.keys(): + if constraint not in needs_config.constraints.keys(): raise NeedsConstraintNotAllowed( f"Constraint {constraint} of need id {need_id} is not allowed " "by config value 'needs_constraints'." @@ -294,7 +292,7 @@ def run(): ) # Trim title if it is too long - max_length = app.config.needs_max_title_length + max_length = needs_config.max_title_length if max_length == -1 or len(title) <= max_length: trimmed_title = title elif max_length <= 3: @@ -353,10 +351,10 @@ def run(): needs_extra_option_names = NEEDS_CONFIG.get("extra_options").keys() _merge_extra_options(needs_info, kwargs, needs_extra_option_names) - needs_global_options = app.config.needs_global_options + needs_global_options = needs_config.global_options _merge_global_options(app, needs_info, needs_global_options) - link_names = [x["option"] for x in app.config.needs_extra_links] + link_names = [x["option"] for x in needs_config.extra_links] for keyword in kwargs: if keyword not in needs_extra_option_names and keyword not in link_names: raise NeedsInvalidOption( @@ -368,7 +366,7 @@ def run(): # Merge links copy_links = [] - for link_type in app.config.needs_extra_links: + for link_type in needs_config.extra_links: # Check, if specific link-type got some arguments during method call if link_type["option"] not in kwargs and link_type["option"] not in needs_global_options: # if not we set no links, but entry in needS_info must be there @@ -398,8 +396,8 @@ def run(): # Jinja support for need content if jinja_content: need_content_context = {**needs_info} - need_content_context.update(**env.app.config.needs_filter_data) - need_content_context.update(**env.app.config.needs_render_context) + need_content_context.update(**needs_config.filter_data) + need_content_context.update(**needs_config.render_context) new_content = jinja_parse(need_content_context, needs_info["content"]) # Overwrite current content content = new_content @@ -572,7 +570,8 @@ def add_external_need( def _prepare_template(app: Sphinx, needs_info, template_key: str) -> str: - template_folder = app.config.needs_template_folder + needs_config = NeedsSphinxConfig(app.config) + template_folder = needs_config.template_folder if not os.path.isabs(template_folder): template_folder = os.path.join(app.srcdir, template_folder) @@ -587,7 +586,7 @@ def _prepare_template(app: Sphinx, needs_info, template_key: str) -> str: with open(template_path) as template_file: template_content = "".join(template_file.readlines()) template_obj = Template(template_content) - new_content = template_obj.render(**needs_info, **app.config.needs_render_context) + new_content = template_obj.render(**needs_info, **needs_config.render_context) return new_content @@ -653,9 +652,10 @@ def make_hashed_id(app: Sphinx, need_type: str, full_title: str, content: str, i :param id_length: maximum length of the generated ID :return: ID as string """ - types = app.config.needs_types + needs_config = NeedsSphinxConfig(app.config) + types = needs_config.types if id_length is None: - id_length = app.config.needs_id_length + id_length = needs_config.id_length type_prefix = None for ntype in types: if ntype["directive"] == need_type: @@ -669,7 +669,7 @@ def make_hashed_id(app: Sphinx, need_type: str, full_title: str, content: str, i # check if needs_id_from_title is configured cal_hashed_id = hashed_id - if app.config.needs_id_from_title: + if needs_config.id_from_title: id_from_title = full_title.upper().replace(" ", "_") + "_" cal_hashed_id = id_from_title + hashed_id diff --git a/sphinx_needs/builder.py b/sphinx_needs/builder.py index 4053e6c92..b3465b664 100644 --- a/sphinx_needs/builder.py +++ b/sphinx_needs/builder.py @@ -6,6 +6,7 @@ from sphinx.application import Sphinx from sphinx.builders import Builder +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.logging import get_logger from sphinx_needs.needsfile import NeedsList from sphinx_needs.utils import unwrap @@ -26,12 +27,12 @@ def finish(self) -> None: env = unwrap(self.env) needs = env.needs_all_needs.values() # We need a list of needs for later filter checks filters = env.needs_all_filters - config = env.config - version = getattr(config, "version", "unset") - needs_list = NeedsList(config, self.outdir, self.srcdir) + version = getattr(env.config, "version", "unset") + needs_list = NeedsList(env.config, self.outdir, self.srcdir) + needs_config = NeedsSphinxConfig(env.config) - if config.needs_file: - needs_file = config.needs_file + if needs_config.file: + needs_file = needs_config.file needs_list.load_json(needs_file) else: # check if needs.json file exists in conf.py directory @@ -46,7 +47,7 @@ def finish(self) -> None: # from sphinx_needs.filter_common import filter_needs - filter_string = self.app.config.needs_builder_filter + filter_string = needs_config.builder_filter filtered_needs = filter_needs(self.app, needs, filter_string) for need in filtered_needs: @@ -82,7 +83,7 @@ def get_target_uri(self, _docname: str, _typ: Optional[str] = None) -> str: def build_needs_json(app: Sphinx, _exception: Exception) -> None: env = unwrap(app.env) - if not env.config.needs_build_json: + if not NeedsSphinxConfig(env.config).build_json: return # Do not create an additional needs.json, if builder is already "needs". @@ -139,8 +140,9 @@ def get_target_uri(self, _docname: str, _typ: Optional[str] = None) -> str: def build_needumls_pumls(app: Sphinx, _exception: Exception) -> None: env = unwrap(app.env) + config = NeedsSphinxConfig(env.config) - if not env.config.needs_build_needumls: + if not config.build_needumls: return # Do not create additional files for saved plantuml content, if builder is already "needumls". @@ -150,10 +152,10 @@ def build_needumls_pumls(app: Sphinx, _exception: Exception) -> None: # if other builder like html used together with config: needs_build_needumls if version_info[0] >= 5: needs_builder = NeedumlsBuilder(app, env) - needs_builder.outdir = os.path.join(needs_builder.outdir, env.config.needs_build_needumls) + needs_builder.outdir = os.path.join(needs_builder.outdir, config.build_needumls) else: needs_builder = NeedumlsBuilder(app) - needs_builder.outdir = os.path.join(needs_builder.outdir, env.config.needs_build_needumls) + needs_builder.outdir = os.path.join(needs_builder.outdir, config.build_needumls) needs_builder.set_environment(env) needs_builder.finish() diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index 769270f12..01c733886 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -1,4 +1,12 @@ -from typing import Any, Callable, Dict +from __future__ import annotations + +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Callable + +from sphinx.application import Sphinx +from sphinx.config import Config as _SphinxConfig + +from sphinx_needs.defaults import DEFAULT_DIAGRAM_TEMPLATE, NEEDS_TABLES_CLASSES class Config: @@ -12,7 +20,7 @@ class Config: """ def __init__(self) -> None: - self.configs: Dict[str, Any] = {} + self.configs: dict[str, Any] = {} def add( self, name: str, value: Any, option_type: type = str, append: bool = False, overwrite: bool = False @@ -47,3 +55,198 @@ def get(self, name: str) -> Any: NEEDS_CONFIG = Config() + + +@dataclass +class NeedsSphinxConfig: + """A wrapper around the Sphinx configuration, + to access the needs specific configuration values, + with working type annotations. + """ + + # This is a modification of the normal dataclass pattern, + # such that we simply redirect all attribute access to the + # Sphinx config object, but in a manner where type annotations will work + # for static type analysis. + + def __init__(self, config: _SphinxConfig) -> None: + super().__setattr__("_config", config) + + def __getattribute__(self, name: str) -> Any: + return getattr(super().__getattribute__("_config"), f"needs_{name}") + + def __setattr__(self, name: str, value: Any) -> None: + return setattr(super().__getattribute__("_config"), f"needs_{name}", value) + + types: list[dict[str, Any]] = field( + default_factory=lambda: [ + { + "directive": "req", + "title": "Requirement", + "prefix": "R_", + "color": "#BFD8D2", + "style": "node", + }, + { + "directive": "spec", + "title": "Specification", + "prefix": "S_", + "color": "#FEDCD2", + "style": "node", + }, + { + "directive": "impl", + "title": "Implementation", + "prefix": "I_", + "color": "#DF744A", + "style": "node", + }, + { + "directive": "test", + "title": "Test Case", + "prefix": "T_", + "color": "#DCB239", + "style": "node", + }, + # Kept for backwards compatibility + { + "directive": "need", + "title": "Need", + "prefix": "N_", + "color": "#9856a5", + "style": "node", + }, + ], + metadata={"rebuild": "html", "types": ()}, + ) + """Custom user need types""" + include_needs: bool = field(default=True, metadata={"rebuild": "html", "types": (bool,)}) + need_name: str = field(default="Need", metadata={"rebuild": "html", "types": (str,)}) + spec_name: str = field(default="Specification", metadata={"rebuild": "html", "types": (str,)}) + id_prefix_needs: str = field(default="", metadata={"rebuild": "html", "types": (str,)}) + id_prefix_specs: str = field(default="", metadata={"rebuild": "html", "types": (str,)}) + id_length: int = field(default=5, metadata={"rebuild": "html", "types": (int,)}) + id_from_title: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + specs_show_needlist: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + id_required: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + id_regex: str = field(default="^[A-Z0-9_]{5,}", metadata={"rebuild": "html", "types": ()}) + show_link_type: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + show_link_title: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + show_link_id: bool = field(default=True, metadata={"rebuild": "html", "types": (bool,)}) + file: None | str = field(default=None, metadata={"rebuild": "html", "types": ()}) + table_columns: str = field( + default="ID;TITLE;STATUS;TYPE;OUTGOING;TAGS", metadata={"rebuild": "html", "types": (str,)} + ) + table_style: str = field(default="DATATABLES", metadata={"rebuild": "html", "types": (str,)}) + role_need_template: str = field(default="{title} ({id})", metadata={"rebuild": "html", "types": (str,)}) + role_need_max_title_length: int = field(default=30, metadata={"rebuild": "html", "types": (int,)}) + extra_options: list[str] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) + title_optional: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + max_title_length: int = field(default=-1, metadata={"rebuild": "html", "types": (int,)}) + title_from_content: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + diagram_template: str = field( + default=DEFAULT_DIAGRAM_TEMPLATE, + metadata={"rebuild": "html", "types": (str,)}, + ) + functions: list[Any] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) + global_options: dict[str, Any] = field(default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}) + duration_option: str = field(default="duration", metadata={"rebuild": "html", "types": (str,)}) + completion_option: str = field(default="completion", metadata={"rebuild": "html", "types": (str,)}) + needextend_strict: bool = field(default=True, metadata={"rebuild": "html", "types": (bool,)}) + statuses: list[dict[str, str]] = field(default_factory=list, metadata={"rebuild": "html", "types": ()}) + """If given, only the defined status are allowed. + Values needed for each status: + * name + * description + Example: [{"name": "open", "description": "open status"}, {...}, {...}] + """ + tags: list[dict[str, str]] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) + """If given, only the defined tags are allowed. + Values needed for each tag: + * name + * description + Example: [{"name": "new", "description": "new needs"}, {...}, {...}] + """ + css: str = field(default="modern.css", metadata={"rebuild": "html", "types": (str,)}) + """Path of css file, which shall be used for need style""" + part_prefix: str = field(default="→\xa0", metadata={"rebuild": "html", "types": (str,)}) + """Prefix for need_part output in tables""" + extra_links: list[dict[str, Any]] = field(default_factory=list, metadata={"rebuild": "html", "types": ()}) + """List of additional links, which can be used by setting related option + Values needed for each new link: + * name (will also be the option name) + * incoming + * copy_link (copy to common links data. Default: True) + * color (used for needflow. Default: #000000) + Example: [{"name": "blocks, "incoming": "is blocked by", "copy_link": True, "color": "#ffcc00"}] + """ + report_dead_links: bool = field(default=True, metadata={"rebuild": "html", "types": (bool,)}) + filter_data: dict[str, Any] = field(default_factory=dict, metadata={"rebuild": "html", "types": ()}) + allow_unsafe_filters: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + flow_show_links: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + flow_link_types: list[str] = field(default_factory=lambda: ["links"], metadata={"rebuild": "html", "types": ()}) + """Defines the link_types to show in a needflow diagram.""" + warnings: dict[str, Any] = field(default_factory=dict, metadata={"rebuild": "html", "types": ()}) + warnings_always_warn: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + layouts: dict[str, dict[str, Any]] = field(default_factory=dict, metadata={"rebuild": "html", "types": ()}) + default_layout: str = field(default="clean", metadata={"rebuild": "html", "types": (str,)}) + default_style: None | str = field(default=None, metadata={"rebuild": "html", "types": ()}) + flow_configs: dict[str, str] = field(default_factory=dict, metadata={"rebuild": "html", "types": ()}) + template_folder: str = field(default="needs_templates/", metadata={"rebuild": "html", "types": (str,)}) + services: dict[str, dict[str, Any]] = field(default_factory=dict, metadata={"rebuild": "html", "types": ()}) + service_all_data: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + debug_no_external_calls: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + external_needs: list[dict[str, Any]] = field(default_factory=list, metadata={"rebuild": "html", "types": ()}) + """Reference external needs, outside of the documentation.""" + builder_filter: str = field(default="is_external==False", metadata={"rebuild": "html", "types": (str,)}) + table_classes: list[str] = field( + default_factory=lambda: NEEDS_TABLES_CLASSES, metadata={"rebuild": "html", "types": (list,)} + ) + """Additional classes to set for needs and needtable.""" + string_links: dict[str, dict[str, Any]] = field( + default_factory=dict, metadata={"rebuild": "html", "types": (dict,)} + ) + build_json: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + build_needumls: str = field(default="", metadata={"rebuild": "html", "types": (str,)}) + permalink_file: str = field(default="permalink.html", metadata={"rebuild": "html", "types": (str,)}) + """Permalink related config values. + path to permalink.html; absolute path from web-root + """ + permalink_data: str = field(default="needs.json", metadata={"rebuild": "html", "types": (str,)}) + """path to needs.json relative to permalink.html""" + report_template: str = field(default="", metadata={"rebuild": "html", "types": (str,)}) + """path to needs_report_template file which is based on the conf.py directory.""" + + # add constraints option + constraints: dict[str, dict[str, Any]] = field(default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}) + constraint_failed_options: dict[str, dict[str, Any]] = field( + default_factory=dict, metadata={"rebuild": "html", "types": (dict,)} + ) + constraints_failed_color: str = field(default="", metadata={"rebuild": "html", "types": (str,)}) + + # add variants option + variants: dict[str, str] = field(default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}) + variant_options: list[str] = field(default_factory=list, metadata={"rebuild": "html", "types": (list,)}) + + # add render context option + render_context: dict[str, Any] = field(default_factory=dict, metadata={"rebuild": "html", "types": (dict,)}) + """Jinja context for rendering templates""" + + debug_measurement: bool = field(default=False, metadata={"rebuild": "html", "types": (bool,)}) + + @classmethod + def add_config_values(cls, app: Sphinx) -> None: + """Add all config values to the Sphinx application.""" + for item in fields(cls): + if item.default_factory is not MISSING: + default = item.default_factory() + elif item.default is not MISSING: + default = item.default + else: + raise Exception(f"Config item {item.name} has no default value or factory.") + app.add_config_value( + f"needs_{item.name}", + default, + item.metadata["rebuild"], + types=item.metadata["types"], + ) diff --git a/sphinx_needs/diagrams_common.py b/sphinx_needs/diagrams_common.py index 5abed87e0..bf6db5724 100644 --- a/sphinx_needs/diagrams_common.py +++ b/sphinx_needs/diagrams_common.py @@ -15,6 +15,7 @@ from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger from sphinx_needs.utils import unwrap @@ -69,13 +70,14 @@ def collect_diagram_attributes(self) -> Dict[str, Any]: location=(self.env.docname, self.lineno), ) + needs_config = NeedsSphinxConfig(self.config) config_names = self.options.get("config") configs = [] if config_names: for config_name in config_names.split(","): config_name = config_name.strip() - if config_name and config_name in self.config.needs_flow_configs: - configs.append(self.config.needs_flow_configs[config_name]) + if config_name and config_name in needs_config.flow_configs: + configs.append(needs_config.flow_configs[config_name]) scale = self.options.get("scale", "100").replace("%", "") if not scale.isdigit(): diff --git a/sphinx_needs/directives/list2need.py b/sphinx_needs/directives/list2need.py index f781f6dc1..18a962e99 100644 --- a/sphinx_needs/directives/list2need.py +++ b/sphinx_needs/directives/list2need.py @@ -9,6 +9,8 @@ from sphinx.errors import SphinxError, SphinxWarning from sphinx.util.docutils import SphinxDirective +from sphinx_needs.config import NeedsSphinxConfig + NEED_TEMPLATE = """.. {{type}}:: {{title}} {% if need_id is not none %}:id: {{need_id}}{%endif%} {% if set_links_down %}:{{links_down_type}}: {{ links_down|join(', ') }}{%endif%} @@ -56,6 +58,7 @@ def presentation(argument: str) -> Any: def run(self) -> Sequence[nodes.Node]: env = self.env + needs_config = NeedsSphinxConfig(env.config) presentation = self.options.get("presentation") if not presentation: @@ -74,7 +77,7 @@ def run(self) -> Sequence[nodes.Node]: # Create a dict, which delivers the need-type for the later level types = {} types_raw_list = [x.strip() for x in types_raw.split(",")] - conf_types = [x["directive"] for x in env.config.needs_types] + conf_types = [x["directive"] for x in needs_config.types] for x in range(0, len(types_raw_list)): types[x] = types_raw_list[x] if types[x] not in conf_types: @@ -90,7 +93,7 @@ def run(self) -> Sequence[nodes.Node]: down_links_raw_list = [] else: down_links_raw_list = [x.strip() for x in down_links_raw.split(",")] - link_types = [x["option"] for x in env.config.needs_extra_links] + link_types = [x["option"] for x in needs_config.extra_links] for i, down_link_raw in enumerate(down_links_raw_list): down_links_types[i] = down_link_raw if down_link_raw not in link_types: @@ -132,8 +135,8 @@ def run(self) -> Sequence[nodes.Node]: else: # Calculate the hash value, so that we can later reuse it prefix = "" - needs_id_length = env.config.needs_id_length - for need_type in env.config.needs_types: + needs_id_length = needs_config.id_length + for need_type in needs_config.types: if need_type["directive"] == types[level]: prefix = need_type["prefix"] break diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py index 1f8663576..6083077df 100644 --- a/sphinx_needs/directives/need.py +++ b/sphinx_needs/directives/need.py @@ -13,7 +13,7 @@ from sphinx_needs.api import add_need from sphinx_needs.api.exceptions import NeedsInvalidException -from sphinx_needs.config import NEEDS_CONFIG +from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.defaults import NEED_DEFAULT_OPTIONS from sphinx_needs.directives.needextend import process_needextend @@ -63,6 +63,7 @@ def __init__( state_machine: RSTStateMachine, ): super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine) + self.needs_config = NeedsSphinxConfig(self.env.config) self.log = get_logger(__name__) self.full_title = self._get_full_title() @@ -117,7 +118,7 @@ def run(self) -> Sequence[nodes.Node]: completion = self.options.get("completion") need_extra_options = {"duration": duration, "completion": completion} - for extra_link in env.config.needs_extra_links: + for extra_link in self.needs_config.extra_links: need_extra_options[extra_link["option"]] = self.options.get(extra_link["option"], "") for extra_option in NEEDS_CONFIG.get("extra_options").keys(): @@ -177,7 +178,7 @@ def make_hashed_id(self, type_prefix: str, id_length: int) -> str: @property def title_from_content(self): - return "title_from_content" in self.options or self.env.config.needs_title_from_content + return "title_from_content" in self.options or self.needs_config.title_from_content @property def docname(self) -> str: @@ -196,7 +197,7 @@ def trimmed_title(self) -> str: @property def max_title_length(self) -> int: - max_title_length: int = self.env.config.needs_max_title_length + max_title_length: int = self.needs_config.max_title_length return max_title_length # ToDo. Keep this in directive @@ -353,7 +354,8 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) - :param fromdocname: :return: """ - if not app.config.needs_include_needs: + needs_config = NeedsSphinxConfig(app.config) + if not needs_config.include_needs: for node in doctree.findall(Need): node.parent.remove(node) return @@ -375,7 +377,7 @@ def process_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str) - check_links(env) # Create back links of common links and extra links - for links in env.config.needs_extra_links: + for links in needs_config.extra_links: create_back_links(env, links["option"]) """ @@ -426,7 +428,7 @@ def print_need_nodes(app: Sphinx, doctree: nodes.document, fromdocname: str, fou for index, attribute in enumerate(node_need.attributes["classes"]): node_need.attributes["classes"][index] = check_and_get_content(attribute, need_data, env) - layout = need_data["layout"] or app.config.needs_default_layout + layout = need_data["layout"] or NeedsSphinxConfig(app.config).default_layout build_need(layout, node_need, app, fromdocname=fromdocname) diff --git a/sphinx_needs/directives/needbar.py b/sphinx_needs/directives/needbar.py index 61d302878..849d2d594 100644 --- a/sphinx_needs/directives/needbar.py +++ b/sphinx_needs/directives/needbar.py @@ -7,6 +7,7 @@ from docutils import nodes from sphinx.application import Sphinx +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list from sphinx_needs.utils import add_doc, save_matplotlib_figure, unwrap @@ -174,11 +175,12 @@ def run(self) -> Sequence[nodes.Node]: def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: list) -> None: builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) # NEEDFLOW # for node in doctree.findall(Needbar): for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. diff --git a/sphinx_needs/directives/needextend.py b/sphinx_needs/directives/needextend.py index 23d1b7fbe..4552a3196 100644 --- a/sphinx_needs/directives/needextend.py +++ b/sphinx_needs/directives/needextend.py @@ -11,6 +11,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.api.exceptions import NeedsInvalidFilter +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.filter_common import filter_needs from sphinx_needs.logging import get_logger from sphinx_needs.utils import add_doc, unwrap @@ -53,7 +54,7 @@ def run(self) -> Sequence[nodes.Node]: if not extend_filter: raise NeedsInvalidFilter(f"Filter of needextend must be set. See {env.docname}:{self.lineno}") - strict_option = self.options.get("strict", str(self.env.app.config.needs_needextend_strict)) + strict_option = self.options.get("strict", str(NeedsSphinxConfig(self.env.app.config).needextend_strict)) strict = True if strict_option.upper() == "TRUE": strict = True @@ -80,6 +81,7 @@ def process_needextend(app: Sphinx, doctree: nodes.document, fromdocname: str) - """ builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) if not hasattr(env, "need_all_needextend"): env.need_all_needextend = {} @@ -88,10 +90,10 @@ def process_needextend(app: Sphinx, doctree: nodes.document, fromdocname: str) - list_names = ( ["tags", "links"] - + [x["option"] for x in app.config.needs_extra_links] - + [f"{x['option']}_back" for x in app.config.needs_extra_links] + + [x["option"] for x in needs_config.extra_links] + + [f"{x['option']}_back" for x in needs_config.extra_links] ) # back-links (incoming) - link_names = [x["option"] for x in app.config.needs_extra_links] + link_names = [x["option"] for x in needs_config.extra_links] for current_needextend in env.need_all_needextend.values(): # Check if filter is just a need-id. @@ -100,7 +102,7 @@ def process_needextend(app: Sphinx, doctree: nodes.document, fromdocname: str) - if need_filter in env.needs_all_needs: need_filter = f'id == "{need_filter}"' # If it looks like a need id, but we haven't found one, raise an exception - elif re.fullmatch(app.config.needs_id_regex, need_filter): + elif re.fullmatch(needs_config.id_regex, need_filter): error = f"Provided id {need_filter} for needextend does not exist." if current_needextend["strict"]: raise NeedsInvalidFilter(error) diff --git a/sphinx_needs/directives/needextract.py b/sphinx_needs/directives/needextract.py index 04019e1e6..907ae896f 100644 --- a/sphinx_needs/directives/needextract.py +++ b/sphinx_needs/directives/needextract.py @@ -11,6 +11,7 @@ from sphinx.application import Sphinx from sphinx_needs.api.exceptions import NeedsInvalidFilter +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.utils import ( no_needs_found_paragraph, used_filter_paragraph, @@ -77,9 +78,10 @@ def process_needextract(app: Sphinx, doctree: nodes.document, fromdocname: str, Replace all needextract nodes with a list of the collected needs. """ env = unwrap(app.env) + needs_config = NeedsSphinxConfig(app.config) for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. @@ -104,7 +106,7 @@ def process_needextract(app: Sphinx, doctree: nodes.document, fromdocname: str, # check if given filter argument is need-id if need_filter_arg in env.needs_all_needs: need_filter_arg = f'id == "{need_filter_arg}"' - elif re.fullmatch(app.config.needs_id_regex, need_filter_arg): + elif re.fullmatch(needs_config.id_regex, need_filter_arg): # check if given filter argument is need-id, but not exists raise NeedsInvalidFilter(f"Provided id {need_filter_arg} for needextract does not exist.") current_needextract["filter"] = need_filter_arg diff --git a/sphinx_needs/directives/needfilter.py b/sphinx_needs/directives/needfilter.py index fc9be72a6..85598c300 100644 --- a/sphinx_needs/directives/needfilter.py +++ b/sphinx_needs/directives/needfilter.py @@ -6,6 +6,8 @@ from docutils.parsers.rst import directives from jinja2 import Template +from sphinx_needs.config import NeedsSphinxConfig + try: from sphinx.errors import NoUri # Sphinx 3.0 except ImportError: @@ -83,11 +85,12 @@ def process_needfilters(app: Sphinx, doctree: nodes.document, fromdocname: str, # Augment each need with a backlink to the original location. builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) # NEEDFILTER # for node in doctree.findall(Needfilter): for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. @@ -216,8 +219,8 @@ def process_needfilters(app: Sphinx, doctree: nodes.document, fromdocname: str, except NoUri: link = "" - diagram_template = Template(env.config.needs_diagram_template) - node_text = diagram_template.render(**need_info, **app.config.needs_render_context) + diagram_template = Template(needs_config.diagram_template) + node_text = diagram_template.render(**need_info, **needs_config.render_context) puml_node["uml"] += '{style} "{node_text}" as {id} [[{link}]] {color}\n'.format( id=need_info["id"], @@ -238,7 +241,7 @@ def process_needfilters(app: Sphinx, doctree: nodes.document, fromdocname: str, # Create a legend if current_needfilter["show_legend"]: - puml_node["uml"] += create_legend(app.config.needs_types) + puml_node["uml"] += create_legend(needs_config.types) puml_node["uml"] += "@enduml" puml_node["incdir"] = os.path.dirname(current_needfilter["docname"]) puml_node["filename"] = os.path.split(current_needfilter["docname"])[1] # Needed for plantuml >= 0.9 diff --git a/sphinx_needs/directives/needflow.py b/sphinx_needs/directives/needflow.py index 1ccce86a7..380b5de67 100644 --- a/sphinx_needs/directives/needflow.py +++ b/sphinx_needs/directives/needflow.py @@ -11,6 +11,7 @@ generate_name, # Need for plantuml filename calculation ) +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.diagrams_common import calculate_link, create_legend from sphinx_needs.filter_common import FilterBase, filter_single_need, process_filters @@ -52,6 +53,7 @@ class NeedflowDirective(FilterBase): @measure_time("needflow") def run(self) -> Sequence[nodes.Node]: env = self.env + needs_config = NeedsSphinxConfig(env.config) if not hasattr(env, "need_all_needflows"): env.need_all_needflows = {} @@ -63,7 +65,7 @@ def run(self) -> Sequence[nodes.Node]: targetid = f"needflow-{env.docname}-{id}" targetnode = nodes.target("", "", ids=[targetid]) - all_link_types = ",".join(x["option"] for x in env.config.needs_extra_links) + all_link_types = ",".join(x["option"] for x in needs_config.extra_links) link_types = list( split_link_types(self.options.get("link_types", all_link_types), location=(env.docname, self.lineno)) ) @@ -73,8 +75,8 @@ def run(self) -> Sequence[nodes.Node]: if config_names: for config_name in config_names.split(","): config_name = config_name.strip() - if config_name and config_name in env.config.needs_flow_configs: - configs.append(env.config.needs_flow_configs[config_name]) + if config_name and config_name in needs_config.flow_configs: + configs.append(needs_config.flow_configs[config_name]) scale = self.options.get("scale", "100").replace("%", "") if not scale.isdigit(): @@ -141,10 +143,10 @@ def get_need_node_rep_for_plantuml( app: Sphinx, fromdocname: str, current_needflow: dict, all_needs: list, need_info: dict ) -> str: """Calculate need node representation for plantuml.""" + needs_config = NeedsSphinxConfig(app.config) + diagram_template = get_template(needs_config.diagram_template) - diagram_template = get_template(app.config.needs_diagram_template) - - node_text = diagram_template.render(**need_info, **app.config.needs_render_context) + node_text = diagram_template.render(**need_info, **needs_config.render_context) node_link = calculate_link(app, need_info, fromdocname) @@ -289,14 +291,15 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou # Replace all needflow nodes with a list of the collected needs. # Augment each need with a backlink to the original location. env = unwrap(app.env) + needs_config = NeedsSphinxConfig(app.config) - link_types = app.config.needs_extra_links - allowed_link_types_options = [link.upper() for link in app.config.needs_flow_link_types] + link_types = needs_config.extra_links + allowed_link_types_options = [link.upper() for link in needs_config.flow_link_types] # NEEDFLOW # for node in doctree.findall(Needflow): for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. @@ -378,7 +381,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou # If source or target of link is a need_part, a specific style is needed if "." in link or "." in need_info["id_complete"]: final_link = link - if current_needflow["show_link_names"] or app.config.needs_flow_show_links: + if current_needflow["show_link_names"] or needs_config.flow_show_links: desc = link_type["outgoing"] + "\\n" comment = f": {desc}" else: @@ -390,7 +393,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou link_style = "[dotted]" else: final_link = link - if current_needflow["show_link_names"] or app.config.needs_flow_show_links: + if current_needflow["show_link_names"] or needs_config.flow_show_links: comment = ": {desc}".format(desc=link_type["outgoing"]) else: comment = "" @@ -433,7 +436,7 @@ def process_needflow(app: Sphinx, doctree: nodes.document, fromdocname: str, fou # Create a legend if current_needflow["show_legend"]: - puml_node["uml"] += create_legend(app.config.needs_types) + puml_node["uml"] += create_legend(needs_config.types) puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needflow["docname"]) diff --git a/sphinx_needs/directives/needgantt.py b/sphinx_needs/directives/needgantt.py index 5b790ff10..02c911838 100644 --- a/sphinx_needs/directives/needgantt.py +++ b/sphinx_needs/directives/needgantt.py @@ -8,6 +8,7 @@ generate_name, # Need for plantuml filename calculation ) +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.diagrams_common import ( DiagramBase, add_config, @@ -53,6 +54,7 @@ class NeedganttDirective(FilterBase, DiagramBase): def run(self) -> Sequence[nodes.Node]: env = self.env + needs_config = NeedsSphinxConfig(env.config) # Creates env.need_all_needgantts safely and other vars self.prepare_env("needgantts") @@ -87,8 +89,8 @@ def run(self) -> Sequence[nodes.Node]: no_color = "no_color" in self.options - duration_option = self.options.get("duration_option", env.app.config.needs_duration_option) - completion_option = self.options.get("completion_option", env.app.config.needs_completion_option) + duration_option = self.options.get("duration_option", needs_config.duration_option) + completion_option = self.options.get("completion_option", needs_config.completion_option) # Add the needgantt and all needed information env.need_all_needgantts[targetid] = { @@ -118,14 +120,15 @@ def run(self) -> Sequence[nodes.Node]: def process_needgantt(app, doctree, fromdocname, found_nodes): # Replace all needgantt nodes with a list of the collected needs. env = app.builder.env + needs_config = NeedsSphinxConfig(app.config) - # link_types = env.config.needs_extra_links - # allowed_link_types_options = [link.upper() for link in env.config.needs_flow_link_types] + # link_types = needs_config.extra_links + # allowed_link_types_options = [link.upper() for link in needs_config.flow_link_types] # NEEDGANTT # for node in doctree.findall(Needgantt): for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. @@ -268,7 +271,7 @@ def process_needgantt(app, doctree, fromdocname, found_nodes): # Create a legend if current_needgantt["show_legend"]: - puml_node["uml"] += create_legend(app.config.needs_types) + puml_node["uml"] += create_legend(needs_config.types) puml_node["uml"] += "\n@endgantt" puml_node["incdir"] = os.path.dirname(current_needgantt["docname"]) diff --git a/sphinx_needs/directives/needimport.py b/sphinx_needs/directives/needimport.py index 2210f0673..334048c3f 100644 --- a/sphinx_needs/directives/needimport.py +++ b/sphinx_needs/directives/needimport.py @@ -11,7 +11,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.api import add_need -from sphinx_needs.config import NEEDS_CONFIG +from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.filter_common import filter_single_need from sphinx_needs.needsfile import check_needs_file @@ -152,13 +152,14 @@ def run(self) -> Sequence[nodes.Node]: needs_list = needs_list_filtered # If we need to set an id prefix, we also need to manipulate all used ids in the imported data. + extra_links = NeedsSphinxConfig(self.config).extra_links if id_prefix: needs_ids = needs_list.keys() for need in needs_list.values(): for id in needs_ids: # Manipulate links in all link types - for extra_link in self.env.config.needs_extra_links: + for extra_link in extra_links: if extra_link["option"] in need and id in need[extra_link["option"]]: for n, link in enumerate(need[extra_link["option"]]): if id == link: @@ -194,7 +195,7 @@ def run(self) -> Sequence[nodes.Node]: need["content"] = need["description"] # Remove unknown options, as they may be defined in source system, but not in this sphinx project - extra_link_keys = [x["option"] for x in self.env.config.needs_extra_links] + extra_link_keys = [x["option"] for x in extra_links] extra_option_keys = list(NEEDS_CONFIG.get("extra_options").keys()) default_options = [ "title", diff --git a/sphinx_needs/directives/needlist.py b/sphinx_needs/directives/needlist.py index 2940d0b34..8164fd3d6 100644 --- a/sphinx_needs/directives/needlist.py +++ b/sphinx_needs/directives/needlist.py @@ -8,6 +8,7 @@ from docutils.parsers.rst import directives from sphinx.application import Sphinx +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.utils import ( no_needs_found_paragraph, used_filter_paragraph, @@ -73,9 +74,10 @@ def process_needlist(app: Sphinx, doctree: nodes.document, fromdocname: str, fou builder = unwrap(app.builder) env = unwrap(builder.env) + include_needs = NeedsSphinxConfig(env.config).include_needs # for node in doctree.findall(Needlist): for node in found_nodes: - if not app.config.needs_include_needs: + if not include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. diff --git a/sphinx_needs/directives/needpie.py b/sphinx_needs/directives/needpie.py index f300b6938..7fc87cfbf 100644 --- a/sphinx_needs/directives/needpie.py +++ b/sphinx_needs/directives/needpie.py @@ -7,6 +7,7 @@ from docutils import nodes from sphinx.application import Sphinx +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list @@ -121,9 +122,10 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun env = unwrap(builder.env) # NEEDFLOW + include_needs = NeedsSphinxConfig(env.config).include_needs # for node in doctree.findall(Needpie): for node in found_nodes: - if not app.config.needs_include_needs: + if not include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. diff --git a/sphinx_needs/directives/needreport.py b/sphinx_needs/directives/needreport.py index 125ff897e..1f0532ffb 100644 --- a/sphinx_needs/directives/needreport.py +++ b/sphinx_needs/directives/needreport.py @@ -7,6 +7,7 @@ from sphinx.errors import SphinxError from sphinx.util.docutils import SphinxDirective +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.utils import analyse_needs_metrics from sphinx_needs.utils import add_doc @@ -26,6 +27,7 @@ class NeedReportDirective(SphinxDirective): def run(self) -> Sequence[nodes.raw]: env = self.env + needs_config = NeedsSphinxConfig(env.config) if len(self.options.keys()) == 0: # Check if options is empty error_file, error_line = self.state_machine.input_lines.items[0] @@ -45,11 +47,11 @@ def run(self) -> Sequence[nodes.raw]: needs_metrics = {} if types is not None and isinstance(types, str): - needs_types = env.app.config.needs_types + needs_types = needs_config.types if extra_links is not None and isinstance(extra_links, str): - needs_extra_links = env.app.config.needs_extra_links + needs_extra_links = needs_config.extra_links if extra_options is not None and isinstance(extra_options, str): - needs_extra_options = env.app.config.needs_extra_options + needs_extra_options = needs_config.extra_options if usage is not None and isinstance(usage, str): needs_metrics = analyse_needs_metrics(env) @@ -59,9 +61,9 @@ def run(self) -> Sequence[nodes.raw]: "links": needs_extra_links, "usage": needs_metrics, } - report_info.update(**env.app.config.needs_render_context) + report_info.update(**needs_config.render_context) - need_report_template_path: str = env.app.config.needs_report_template + need_report_template_path: str = needs_config.report_template # Absolute path starts with /, based on the conf.py directory. The / need to be striped correct_need_report_template_path = os.path.join(env.app.srcdir, need_report_template_path.lstrip("/")) diff --git a/sphinx_needs/directives/needsequence.py b/sphinx_needs/directives/needsequence.py index 318050326..0168ffabc 100644 --- a/sphinx_needs/directives/needsequence.py +++ b/sphinx_needs/directives/needsequence.py @@ -9,6 +9,7 @@ generate_name, # Need for plantuml filename calculation ) +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.diagrams_common import ( DiagramBase, add_config, @@ -79,12 +80,15 @@ def process_needsequence(app: Sphinx, doctree: nodes.document, fromdocname: str, builder = unwrap(app.builder) env = unwrap(builder.env) - link_types = env.config.needs_extra_links + needs_config = NeedsSphinxConfig(env.config) + include_needs = needs_config.include_needs + link_types = needs_config.extra_links + needs_types = needs_config.types # NEEDSEQUENCE # for node in doctree.findall(Needsequence): for node in found_nodes: - if not app.config.needs_include_needs: + if not include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. @@ -169,7 +173,7 @@ def process_needsequence(app: Sphinx, doctree: nodes.document, fromdocname: str, # Create a legend if current_needsequence["show_legend"]: - puml_node["uml"] += create_legend(app.config.needs_types) + puml_node["uml"] += create_legend(needs_types) puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needsequence["docname"]) diff --git a/sphinx_needs/directives/needservice.py b/sphinx_needs/directives/needservice.py index 2b16bb86e..d2975a8e5 100644 --- a/sphinx_needs/directives/needservice.py +++ b/sphinx_needs/directives/needservice.py @@ -8,6 +8,7 @@ from sphinx_data_viewer.api import get_data_viewer_node from sphinx_needs.api import add_need +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.need import NeedDirective from sphinx_needs.logging import get_logger from sphinx_needs.services.base import BaseService @@ -51,6 +52,9 @@ def __init__( def run(self) -> Sequence[nodes.Node]: docname = self.env.docname app = self.env.app + needs_config = NeedsSphinxConfig(self.config) + need_types = needs_config.types + all_data = needs_config.service_all_data needs_services: Dict[str, BaseService] = getattr(app, "needs_services", {}) service_name = self.arguments[0] @@ -71,7 +75,7 @@ def run(self) -> Sequence[nodes.Node]: if "type" not in datum.keys(): # Use the first defined type, if nothing got defined by service (should not be the case) - need_type = self.env.app.config.needs_types[0]["directive"] + need_type = need_types[0]["directive"] else: need_type = datum["type"] del datum["type"] @@ -95,7 +99,7 @@ def run(self) -> Sequence[nodes.Node]: for missing_option in missing_options: del datum[missing_option] - if app.config.needs_service_all_data: + if all_data: for name, value in missing_options.items(): content.append(f"\n:{name}: {value}") diff --git a/sphinx_needs/directives/needtable.py b/sphinx_needs/directives/needtable.py index 16eb3b91f..deacc699f 100644 --- a/sphinx_needs/directives/needtable.py +++ b/sphinx_needs/directives/needtable.py @@ -6,6 +6,7 @@ from sphinx.application import Sphinx from sphinx_needs.api.exceptions import NeedsInvalidException +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.directives.utils import ( get_option_list, @@ -59,7 +60,7 @@ def run(self) -> Sequence[nodes.Node]: columns = str(self.options.get("columns", "")) if len(columns) == 0: - columns = env.app.config.needs_table_columns + columns = NeedsSphinxConfig(env.app.config).table_columns if isinstance(columns, str): columns = [col.strip() for col in re.split(";|,", columns)] @@ -125,10 +126,11 @@ def process_needtables(app: Sphinx, doctree: nodes.document, fromdocname: str, f """ builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(app.config) # Create a link_type dictionary, which keys-list can be easily used to find columns link_type_list = {} - for link_type in app.config.needs_extra_links: + for link_type in needs_config.extra_links: link_type_list[link_type["option"].upper()] = link_type link_type_list[link_type["option"].upper() + "_BACK"] = link_type link_type_list[link_type["incoming"].upper()] = link_type @@ -142,7 +144,7 @@ def process_needtables(app: Sphinx, doctree: nodes.document, fromdocname: str, f # for node in doctree.findall(Needtable): for node in found_nodes: - if not app.config.needs_include_needs: + if not needs_config.include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # If we replace a node, docutils checks, if it will not lose any attributes. @@ -159,10 +161,10 @@ def process_needtables(app: Sphinx, doctree: nodes.document, fromdocname: str, f all_needs = env.needs_all_needs if current_needtable["style"] == "" or current_needtable["style"].upper() not in ["TABLE", "DATATABLES"]: - if app.config.needs_table_style == "": + if needs_config.table_style == "": style = "DATATABLES" else: - style = app.config.needs_table_style.upper() + style = needs_config.table_style.upper() else: style = current_needtable["style"].upper() @@ -176,7 +178,7 @@ def process_needtables(app: Sphinx, doctree: nodes.document, fromdocname: str, f # care about table layout and styling. The normal "TABLE" style is using the Sphinx default table # css classes and therefore must be handled by the themes. if style != "TABLE": - classes.extend(app.config.needs_table_classes) + classes.extend(needs_config.table_classes) table_node = nodes.table(classes=classes, ids=[id + "-table_node"]) tgroup = nodes.tgroup(cols=len(current_needtable["columns"])) @@ -248,7 +250,7 @@ def sort(need): else: row = nodes.row(classes=["need_part", style_row]) temp_need["id"] = temp_need["id_complete"] - prefix = app.config.needs_part_prefix + prefix = needs_config.part_prefix temp_need["title"] = temp_need["content"] for option, _title in current_needtable["columns"]: @@ -299,7 +301,7 @@ def sort(need): temp_part, "id_complete", make_ref=True, - prefix=app.config.needs_part_prefix, + prefix=needs_config.part_prefix, ) elif option == "TITLE": row += row_col_maker( @@ -308,7 +310,7 @@ def sort(need): env.needs_all_needs, temp_part, "content", - prefix=app.config.needs_part_prefix, + prefix=needs_config.part_prefix, ) elif option in link_type_list and ( option diff --git a/sphinx_needs/directives/needuml.py b/sphinx_needs/directives/needuml.py index c53259f9d..d7cfe1f40 100644 --- a/sphinx_needs/directives/needuml.py +++ b/sphinx_needs/directives/needuml.py @@ -7,6 +7,7 @@ from jinja2 import BaseLoader, Environment, Template from sphinx.util.docutils import SphinxDirective +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.diagrams_common import calculate_link from sphinx_needs.directives.needflow import make_entity_name @@ -61,11 +62,12 @@ def run(self) -> Sequence[nodes.Node]: config_names = self.options.get("config") configs = [] + flow_configs = NeedsSphinxConfig(env.config).flow_configs if config_names: for config_name in config_names.split(","): config_name = config_name.strip() - if config_name and config_name in env.config.needs_flow_configs: - configs.append(env.config.needs_flow_configs[config_name]) + if config_name and config_name in flow_configs: + configs.append(flow_configs[config_name]) extra_dict = {} extras = self.options.get("extra") @@ -192,7 +194,7 @@ def jinja2uml( # 5. Get data for the jinja processing data = {} # 5.1 Set default config to data - data.update(**app.config.needs_render_context) + data.update(**NeedsSphinxConfig(app.config).render_context) # 5.2 Set uml() kwargs to data and maybe overwrite default settings data.update(kwargs) # 5.3 Make the helpers available during rendering and overwrite maybe wrongly default and uml() kwargs settings @@ -313,8 +315,9 @@ def flow(self, need_id) -> str: need_info = self.needs[need_id] link = calculate_link(self.app, need_info, self.fromdocname) - diagram_template = Template(self.app.builder.env.config.needs_diagram_template) - node_text = diagram_template.render(**need_info, **self.app.config.needs_render_context) + needs_config = NeedsSphinxConfig(self.app.config) + diagram_template = Template(needs_config.diagram_template) + node_text = diagram_template.render(**need_info, **needs_config.render_context) need_uml = '{style} "{node_text}" as {id} [[{link}]] #{color}'.format( id=make_entity_name(need_id), diff --git a/sphinx_needs/directives/utils.py b/sphinx_needs/directives/utils.py index ebabea4e0..ddc58a5d5 100644 --- a/sphinx_needs/directives/utils.py +++ b/sphinx_needs/directives/utils.py @@ -4,6 +4,7 @@ from docutils import nodes from sphinx.environment import BuildEnvironment +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.defaults import TITLE_REGEX @@ -39,7 +40,7 @@ def used_filter_paragraph(current_needfilter) -> nodes.paragraph: def get_link_type_option(name: str, env: BuildEnvironment, node, default: str = "") -> List[str]: link_types = [x.strip() for x in re.split(";|,", node.options.get(name, default))] - conf_link_types = env.config.needs_extra_links + conf_link_types = NeedsSphinxConfig(env.config).extra_links conf_link_types_name = [x["option"] for x in conf_link_types] final_link_types = [] @@ -96,7 +97,7 @@ def analyse_needs_metrics(env: BuildEnvironment) -> Dict[str, Any]: """ needs: Dict = env.needs_all_needs metric_data = {"needs_amount": len(needs)} - needs_types = {i["directive"]: 0 for i in env.app.config.needs_types} + needs_types = {i["directive"]: 0 for i in NeedsSphinxConfig(env.config).types} for i in needs.values(): if i["type"] in needs_types: diff --git a/sphinx_needs/environment.py b/sphinx_needs/environment.py index 2f58cc1b6..d085f0a6f 100644 --- a/sphinx_needs/environment.py +++ b/sphinx_needs/environment.py @@ -8,6 +8,7 @@ from sphinx.util.console import brown from sphinx.util.osutil import copyfile +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.utils import logger, unwrap IMAGE_DIR_NAME = "_static" @@ -84,11 +85,12 @@ def install_styles_static_files(app: Sphinx, env: BuildEnvironment) -> None: dest_dir = statics_dir / "sphinx-needs" def find_css_files() -> Iterable[Path]: + needs_css = NeedsSphinxConfig(app.config).css for theme in ["modern", "dark", "blank"]: - if app.config.needs_css == f"{theme}.css": + if needs_css == f"{theme}.css": css_dir = css_root / theme return [f for f in css_dir.glob("**/*") if f.is_file()] - return [app.config.needs_css] + return [needs_css] files_to_copy = [Path("common.css")] files_to_copy.extend(find_css_files()) @@ -203,12 +205,13 @@ def install_permalink_file(app: Sphinx, env: BuildEnvironment) -> None: template = jinja_env.get_template("permalink.html") # save file to build dir - out_file = Path(builder.outdir) / Path(env.config.needs_permalink_file).name + sphinx_config = NeedsSphinxConfig(env.config) + out_file = Path(builder.outdir) / Path(sphinx_config.permalink_file).name with open(out_file, "w", encoding="utf-8") as f: f.write( template.render( - permalink_file=env.config.needs_permalink_file, - needs_file=env.config.needs_permalink_data, - **app.config.needs_render_context, + permalink_file=sphinx_config.permalink_file, + needs_file=sphinx_config.permalink_data, + **sphinx_config.render_context, ) ) diff --git a/sphinx_needs/external_needs.py b/sphinx_needs/external_needs.py index 48309d5ee..e44aadf25 100644 --- a/sphinx_needs/external_needs.py +++ b/sphinx_needs/external_needs.py @@ -9,6 +9,7 @@ from sphinx_needs.api import add_external_need, del_need from sphinx_needs.api.exceptions import NeedsDuplicatedId +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.logging import get_logger from sphinx_needs.utils import clean_log, import_prefix_link_edit @@ -16,7 +17,8 @@ def load_external_needs(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: - for source in app.config.needs_external_needs: + needs_config = NeedsSphinxConfig(app.config) + for source in needs_config.external_needs: if source["base_url"].endswith("/"): source["base_url"] = source["base_url"][:-1] @@ -73,14 +75,14 @@ def load_external_needs(app: Sphinx, env: BuildEnvironment, _docname: str) -> No log.debug(f"Loading {len(needs)} needs.") prefix = source.get("id_prefix", "").upper() - import_prefix_link_edit(needs, prefix, env.config.needs_extra_links) + import_prefix_link_edit(needs, prefix, needs_config.extra_links) for need in needs.values(): need_params = {**need} - extra_links = [x["option"] for x in app.config.needs_extra_links] + extra_links = [x["option"] for x in needs_config.extra_links] for key in list(need_params.keys()): if ( - key not in app.config.needs_extra_options + key not in needs_config.extra_options and key not in extra_links and key not in ["title", "type", "id", "description", "tags", "docname", "status"] ): diff --git a/sphinx_needs/filter_common.py b/sphinx_needs/filter_common.py index dc22a2ea8..f373e8367 100644 --- a/sphinx_needs/filter_common.py +++ b/sphinx_needs/filter_common.py @@ -12,6 +12,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.api.exceptions import NeedsInvalidFilter +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.utils import check_and_get_external_filter_func from sphinx_needs.utils import logger as log @@ -175,7 +176,7 @@ def process_filters(app: Sphinx, all_needs, current_needlist, include_external: found_needs = [] # Check if config allow unsafe filters - if app.config.needs_allow_unsafe_filters: + if NeedsSphinxConfig(app.config).allow_unsafe_filters: found_needs = found_dirty_needs else: # Just take the ids from search result and use the related, but original need @@ -300,7 +301,7 @@ def filter_single_need( filter_context["current_need"] = need # Get needs external filter data and merge to filter_context - filter_context.update(app.config.needs_filter_data) + filter_context.update(NeedsSphinxConfig(app.config).filter_data) filter_context["search"] = re.search result = False diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 77d2ebf83..0b4ec997b 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -15,6 +15,7 @@ from sphinx.environment import BuildEnvironment from sphinx.errors import SphinxError +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.logging import get_logger from sphinx_needs.utils import NEEDS_FUNCTIONS, match_variants # noqa: F401 @@ -250,14 +251,15 @@ def resolve_variants_options(env: BuildEnvironment): if env.needs_workflow["variant_option_resolved"]: return - variants_options = env.app.config.needs_variant_options + needs_config = NeedsSphinxConfig(env.config) + variants_options = needs_config.variant_options if variants_options: needs: Dict = env.needs_all_needs for need in needs.values(): # Data to use as filter context. need_context: Dict = {**need} - need_context.update(**env.app.config.needs_filter_data) # Add needs_filter_data to filter context + need_context.update(**needs_config.filter_data) # Add needs_filter_data to filter context _sphinx_tags = env.app.builder.tags.tags # Get sphinx tags need_context.update(**_sphinx_tags) # Add sphinx tags to filter context @@ -265,10 +267,10 @@ def resolve_variants_options(env: BuildEnvironment): if var_option in need and need[var_option] not in (None, "", []): if not isinstance(need[var_option], (list, set, tuple)): option_value: str = need[var_option] - need[var_option] = match_variants(option_value, need_context, env.app.config.needs_variants) + need[var_option] = match_variants(option_value, need_context, needs_config.variants) else: option_value = need[var_option] - need[var_option] = match_variants(option_value, need_context, env.app.config.needs_variants) + need[var_option] = match_variants(option_value, need_context, needs_config.variants) # Finally set a flag so that this function gets not executed several times env.needs_workflow["variant_option_resolved"] = True diff --git a/sphinx_needs/layout.py b/sphinx_needs/layout.py index 0beb2151e..4bf6867a9 100644 --- a/sphinx_needs/layout.py +++ b/sphinx_needs/layout.py @@ -22,6 +22,7 @@ from sphinx.application import Sphinx from sphinx.environment.collectors.asset import DownloadFileCollector, ImageCollector +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.debug import measure_time from sphinx_needs.utils import INTERNALS, match_string_link, unwrap @@ -85,8 +86,9 @@ def create_need(need_id: str, app: Sphinx, layout=None, style=None, docname: Opt node_container.attributes["ids"].append(need_id) - layout = layout or need_data["layout"] or app.config.needs_default_layout - style = style or need_data["style"] or app.config.needs_default_style + needs_config = NeedsSphinxConfig(app.config) + layout = layout or need_data["layout"] or needs_config.default_layout + style = style or need_data["style"] or needs_config.default_style build_need(layout, node_container, app, style, docname) @@ -187,9 +189,10 @@ class LayoutHandler: def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Optional[str] = None) -> None: self.app = app self.need = need + self.config = NeedsSphinxConfig(app.config) self.layout_name = layout - available_layouts = app.config.needs_layouts + available_layouts = self.config.layouts if self.layout_name not in available_layouts.keys(): raise SphinxNeedLayoutException( 'Given layout "{}" is unknown for need {}. Registered layouts are: {}'.format( @@ -208,7 +211,7 @@ def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Opt # For ReadTheDocs Theme we need to add 'rtd-exclude-wy-table'. classes = ["need", "needs_grid_" + self.layout["grid"], "needs_layout_" + self.layout_name] - classes.extend(app.config.needs_table_classes) + classes.extend(self.config.table_classes) self.style = style or self.need["style"] or getattr(self.app.config, "needs_default_style", None) @@ -308,7 +311,7 @@ def __init__(self, app: Sphinx, need, layout, node, style=None, fromdocname: Opt # This would lead to deepcopy()-errors, as needs_string_links gets some "pickled" and jinja Environment is # too complex for this. self.string_links = {} - for link_name, link_conf in app.config.needs_string_links.items(): + for link_name, link_conf in self.config.string_links.items(): self.string_links[link_name] = { "url_template": Environment(loader=BaseLoader).from_string(link_conf["link_url"]), "name_template": Environment(loader=BaseLoader).from_string(link_conf["link_name"]), @@ -505,7 +508,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False # data_node.append(nodes.Text(data) # data_container.append(data_node) needs_string_links_option: List[str] = [] - for v in self.app.config.needs_string_links.values(): + for v in self.config.string_links.values(): needs_string_links_option.extend(v["options"]) if name in needs_string_links_option: @@ -525,7 +528,7 @@ def meta(self, name: str, prefix: Optional[str] = None, show_empty: bool = False data=datum, need_key=name, matching_link_confs=matching_link_confs, - render_context=self.app.config.needs_render_context, + render_context=self.config.render_context, ) else: # Normal text handling @@ -631,8 +634,8 @@ def meta_all( exclude += default_excludes if no_links: - link_names = [x["option"] for x in self.app.config.needs_extra_links] - link_names += [x["option"] + "_back" for x in self.app.config.needs_extra_links] + link_names = [x["option"] for x in self.config.extra_links] + link_names += [x["option"] + "_back" for x in self.config.extra_links] exclude += link_names data_container = nodes.inline() for data in self.need.keys(): @@ -663,13 +666,13 @@ def meta_links(self, name: str, incoming: bool = False): :return: docutils nodes """ data_container = nodes.inline(classes=[name]) - if name not in [x["option"] for x in self.app.config.needs_extra_links]: + if name not in [x["option"] for x in self.config.extra_links]: raise SphinxNeedLayoutException(f"Invalid link name {name} for link-type") # if incoming: - # link_name = self.app.config.needs_extra_links[name]['incoming'] + # link_name = self.config.extra_links[name]['incoming'] # else: - # link_name = self.app.config.needs_extra_links[name]['outgoing'] + # link_name = self.config.extra_links[name]['outgoing'] from sphinx_needs.roles.need_incoming import NeedIncoming from sphinx_needs.roles.need_outgoing import NeedOutgoing @@ -693,7 +696,7 @@ def meta_links_all(self, prefix: str = "", postfix: str = "", exclude=None): """ exclude = exclude or [] data_container = [] - for link_type in self.app.config.needs_extra_links: + for link_type in self.config.extra_links: type_key = link_type["option"] if self.need[type_key] and type_key not in exclude: outgoing_line = nodes.line() @@ -996,8 +999,7 @@ def permalink( image_url = "icon:share-2" image_width = "17px" - config = self.app.config - permalink = config.needs_permalink_file + permalink = self.config.permalink_file id = self.need["id"] docname = self.need["docname"] permalink_url = "" diff --git a/sphinx_needs/need_constraints.py b/sphinx_needs/need_constraints.py index 7894622b7..59c11c650 100644 --- a/sphinx_needs/need_constraints.py +++ b/sphinx_needs/need_constraints.py @@ -3,6 +3,7 @@ from sphinx.application import Sphinx from sphinx_needs.api.exceptions import NeedsConstraintFailed, NeedsConstraintNotAllowed +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.filter_common import filter_single_need from sphinx_needs.logging import get_logger @@ -16,8 +17,8 @@ def process_constraints(app: Sphinx, need: Dict[str, Any]) -> None: :param app: sphinx app for access to config files :param need: need object to process """ - - config_constraints = app.config.needs_constraints + needs_config = NeedsSphinxConfig(app.config) + config_constraints = needs_config.constraints need_id = need["id"] @@ -50,7 +51,7 @@ def process_constraints(app: Sphinx, need: Dict[str, Any]) -> None: need["constraints_results"][constraint] = {} # defines what to do if a constraint is not fulfilled. from conf.py - constraint_failed_options = app.config.needs_constraint_failed_options + constraint_failed_options = needs_config.constraint_failed_options # prepare structure for check_0, check_1 ... if name not in need["constraints_results"][constraint].keys(): diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index f956d47a0..2b3d269fd 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -16,14 +16,12 @@ build_needs_json, build_needumls_pumls, ) -from sphinx_needs.config import NEEDS_CONFIG +from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.defaults import ( - DEFAULT_DIAGRAM_TEMPLATE, LAYOUTS, NEED_DEFAULT_OPTIONS, NEEDEXTEND_NOT_ALLOWED_OPTIONS, NEEDFLOW_CONFIG_DEFAULTS, - NEEDS_TABLES_CLASSES, ) from sphinx_needs.directives.list2need import List2Need, List2NeedDirective from sphinx_needs.directives.need import ( @@ -141,145 +139,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_builder(NeedsBuilder) app.add_builder(NeedumlsBuilder) - app.add_config_value( - "needs_types", - [ - {"directive": "req", "title": "Requirement", "prefix": "R_", "color": "#BFD8D2", "style": "node"}, - {"directive": "spec", "title": "Specification", "prefix": "S_", "color": "#FEDCD2", "style": "node"}, - {"directive": "impl", "title": "Implementation", "prefix": "I_", "color": "#DF744A", "style": "node"}, - {"directive": "test", "title": "Test Case", "prefix": "T_", "color": "#DCB239", "style": "node"}, - # Kept for backwards compatibility - {"directive": "need", "title": "Need", "prefix": "N_", "color": "#9856a5", "style": "node"}, - ], - "html", - ) - app.add_config_value("needs_include_needs", True, "html", types=[bool]) - app.add_config_value("needs_need_name", "Need", "html", types=[str]) - app.add_config_value("needs_spec_name", "Specification", "html", types=[str]) - app.add_config_value("needs_id_prefix_needs", "", "html", types=[str]) - app.add_config_value("needs_id_prefix_specs", "", "html", types=[str]) - app.add_config_value("needs_id_length", 5, "html", types=[int]) - app.add_config_value("needs_id_from_title", False, "html", types=[bool]) - app.add_config_value("needs_specs_show_needlist", False, "html", types=[bool]) - app.add_config_value("needs_id_required", False, "html", types=[bool]) - app.add_config_value( - "needs_id_regex", - f"^[A-Z0-9_]{{{app.config.needs_id_length},}}", - "html", - ) - app.add_config_value("needs_show_link_type", False, "html", types=[bool]) - app.add_config_value("needs_show_link_title", False, "html", types=[bool]) - app.add_config_value("needs_show_link_id", True, "html", types=[bool]) - app.add_config_value("needs_file", None, "html") - app.add_config_value("needs_table_columns", "ID;TITLE;STATUS;TYPE;OUTGOING;TAGS", "html") - app.add_config_value("needs_table_style", "DATATABLES", "html") - - app.add_config_value("needs_role_need_template", "{title} ({id})", "html") - app.add_config_value("needs_role_need_max_title_length", 30, "html", types=[int]) - - app.add_config_value("needs_extra_options", [], "html") - app.add_config_value("needs_title_optional", False, "html", types=[bool]) - app.add_config_value("needs_max_title_length", -1, "html", types=[int]) - app.add_config_value("needs_title_from_content", False, "html", types=[bool]) - - app.add_config_value("needs_diagram_template", DEFAULT_DIAGRAM_TEMPLATE, "html") - - app.add_config_value("needs_functions", [], "html", types=[list]) - app.add_config_value("needs_global_options", {}, "html", types=[dict]) - - app.add_config_value("needs_duration_option", "duration", "html") - app.add_config_value("needs_completion_option", "completion", "html") - - app.add_config_value("needs_needextend_strict", True, "html", types=[bool]) - - # If given, only the defined status are allowed. - # Values needed for each status: - # * name - # * description - # Example: [{"name": "open", "description": "open status"}, {...}, {...}] - app.add_config_value("needs_statuses", [], "html") - - # If given, only the defined tags are allowed. - # Values needed for each tag: - # * name - # * description - # Example: [{"name": "new", "description": "new needs"}, {...}, {...}] - app.add_config_value("needs_tags", [], "html", types=[list]) - - # Path of css file, which shall be used for need style - app.add_config_value("needs_css", "modern.css", "html") - - # Prefix for need_part output in tables - app.add_config_value("needs_part_prefix", "\u2192\u00a0", "html") - - # List of additional links, which can be used by setting related option - # Values needed for each new link: - # * name (will also be the option name) - # * incoming - # * copy_link (copy to common links data. Default: True) - # * color (used for needflow. Default: #000000) - # Example: [{"name": "blocks, "incoming": "is blocked by", "copy_link": True, "color": "#ffcc00"}] - app.add_config_value("needs_extra_links", [], "html") - - # Deactivate log msgs of dead links if set to False, default is True - app.add_config_value("needs_report_dead_links", True, "html", types=[bool]) - - app.add_config_value("needs_filter_data", {}, "html") - app.add_config_value("needs_allow_unsafe_filters", False, "html") - - app.add_config_value("needs_flow_show_links", False, "html") - app.add_config_value("needs_flow_link_types", ["links"], "html") - - app.add_config_value("needs_warnings", {}, "html") - app.add_config_value("needs_warnings_always_warn", False, "html", types=[bool]) - app.add_config_value("needs_layouts", {}, "html") - app.add_config_value("needs_default_layout", "clean", "html") - app.add_config_value("needs_default_style", None, "html") - app.add_config_value("needs_flow_configs", {}, "html") - - app.add_config_value("needs_template_folder", "needs_templates/", "html") - - app.add_config_value("needs_services", {}, "html") - app.add_config_value("needs_service_all_data", False, "html", types=[bool]) - - app.add_config_value("needs_debug_no_external_calls", False, "html", types=[bool]) - - app.add_config_value("needs_external_needs", [], "html") - - app.add_config_value("needs_builder_filter", "is_external==False", "html", types=[str]) - - # Additional classes to set for needs and needtable. - app.add_config_value("needs_table_classes", NEEDS_TABLES_CLASSES, "html", types=[list]) - - app.add_config_value("needs_string_links", {}, "html", types=[dict]) - - app.add_config_value("needs_build_json", False, "html", types=[bool]) - - app.add_config_value("needs_build_needumls", "", "html", types=[str]) - - # Permalink related config values. - # path to permalink.html; absolute path from web-root - app.add_config_value("needs_permalink_file", "permalink.html", "html") - # path to needs.json relative to permalink.html - app.add_config_value("needs_permalink_data", "needs.json", "html") - # path to needs_report_template file which is based on the conf.py directory. - app.add_config_value("needs_report_template", "", "html", types=[str]) - - # add constraints option - app.add_config_value("needs_constraints", {}, "html", types=[dict]) - app.add_config_value("needs_constraint_failed_options", {}, "html", types=[dict]) - app.add_config_value("needs_constraints_failed_color", "", "html") - - # add variants option - app.add_config_value("needs_variants", {}, "html", types=[dict]) - app.add_config_value("needs_variant_options", [], "html", types=[list]) - - # add jinja context option - app.add_config_value("needs_render_context", {}, "html", types=[dict]) - - # - app.add_config_value("needs_debug_measurement", False, "html", types=[dict]) + NeedsSphinxConfig.add_config_values(app) # Define nodes app.add_node(Need, html=(html_visit, html_depart), latex=(latex_visit, latex_depart)) @@ -435,29 +296,30 @@ def load_config(app: Sphinx, *_args) -> None: Register extra options and directive based on config from conf.py """ log = get_logger(__name__) - types = app.config.needs_types - if isinstance(app.config.needs_extra_options, dict): + needs_config = NeedsSphinxConfig(app.config) + + if isinstance(needs_config.extra_options, dict): log.info( 'Config option "needs_extra_options" supports list and dict. However new default type since ' "Sphinx-Needs 0.7.2 is list. Please see docs for details." ) existing_extra_options = NEEDS_CONFIG.get("extra_options") - for option in app.config.needs_extra_options: + for option in needs_config.extra_options: if option in existing_extra_options: log.warning(f'extra_option "{option}" already registered. [needs]', type="needs") NEEDS_CONFIG.add("extra_options", {option: directives.unchanged}, dict, True) extra_options = NEEDS_CONFIG.get("extra_options") # Get extra links and create a dictionary of needed options. - extra_links_raw = app.config.needs_extra_links + extra_links_raw = needs_config.extra_links extra_links = {} for extra_link in extra_links_raw: extra_links[extra_link["option"]] = directives.unchanged - title_optional = app.config.needs_title_optional - title_from_content = app.config.needs_title_from_content + title_optional = needs_config.title_optional + title_from_content = needs_config.title_from_content # Update NeedDirective to use customized options NeedDirective.option_spec.update(extra_options) @@ -515,12 +377,12 @@ def load_config(app: Sphinx, *_args) -> None: NeedDirective.required_arguments = 0 NeedDirective.optional_arguments = 1 - for t in types: + for t in needs_config.types: # Register requested types of needs app.add_directive(t["directive"], NeedDirective) existing_warnings = NEEDS_CONFIG.get("warnings") - for name, check in app.config.needs_warnings.items(): + for name, check in needs_config.warnings.items(): if name not in existing_warnings: NEEDS_CONFIG.add("warnings", {name: check}, dict, append=True) else: @@ -538,6 +400,8 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: """ Prepares the sphinx environment to store sphinx-needs internal data. """ + needs_config = NeedsSphinxConfig(app.config) + if not hasattr(env, "needs_all_needs"): # Used to store all needed information about all needs in document env.needs_all_needs = {} @@ -562,14 +426,14 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: app.needs_services.register("open-needs", OpenNeedsService) # Register user defined services - for name, service in app.config.needs_services.items(): + for name, service in needs_config.services.items(): if name not in app.needs_services.services and "class" in service and "class_init" in service: # We found a not yet registered service # But only register, if service-config contains class and class_init. # Otherwise, the service may get registered later by an external sphinx-needs extension app.needs_services.register(name, service["class"], **service["class_init"]) - needs_functions = app.config.needs_functions + needs_functions = needs_config.functions # Register built-in functions for need_common_func in needs_common_functions: @@ -588,7 +452,7 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: # The default link name. Must exist in all configurations. Therefore we set it here # for the user. common_links = [] - link_types = app.config.needs_extra_links + link_types = needs_config.extra_links basic_link_type_found = False parent_needs_link_type_found = False for link_type in link_types: @@ -619,11 +483,11 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: } ) - app.config.needs_extra_links = common_links + app.config.needs_extra_links + app.config.needs_extra_links = common_links + needs_config.extra_links - app.config.needs_layouts = {**LAYOUTS, **app.config.needs_layouts} + app.config.needs_layouts = {**LAYOUTS, **needs_config.layouts} - app.config.needs_flow_configs.update(NEEDFLOW_CONFIG_DEFAULTS) + needs_config.flow_configs.update(NEEDFLOW_CONFIG_DEFAULTS) if not hasattr(env, "needs_workflow"): # Used to store workflow status information for already executed tasks. @@ -638,11 +502,11 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docname: str) -> None: "variant_option_resolved": False, "needs_extended": False, } - for link_type in app.config.needs_extra_links: + for link_type in needs_config.extra_links: env.needs_workflow["backlink_creation_{}".format(link_type["option"])] = False # Set time measurement flag - if app.config.needs_debug_measurement: + if needs_config.debug_measurement: debug.START_TIME = timer() # Store the rough start time of Sphinx build debug.EXECUTE_TIME_MEASUREMENTS = True diff --git a/sphinx_needs/roles/need_incoming.py b/sphinx_needs/roles/need_incoming.py index a75ad3f26..21aa81208 100644 --- a/sphinx_needs/roles/need_incoming.py +++ b/sphinx_needs/roles/need_incoming.py @@ -2,6 +2,7 @@ from sphinx.application import Sphinx from sphinx.util.nodes import make_refnode +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.errors import NoUri from sphinx_needs.utils import check_and_calc_base_url_rel_path, unwrap @@ -13,6 +14,7 @@ class NeedIncoming(nodes.Inline, nodes.Element): def process_need_incoming(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: list) -> None: builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) # for node_need_backref in doctree.findall(NeedIncoming): for node_need_backref in found_nodes: @@ -31,15 +33,15 @@ def process_need_incoming(app: Sphinx, doctree: nodes.document, fromdocname: str if back_link in env.needs_all_needs: try: target_need = env.needs_all_needs[back_link] - if env.config.needs_show_link_title: + if needs_config.show_link_title: link_text = f'{target_need["title"]}' - if env.config.needs_show_link_id: + if needs_config.show_link_id: link_text += f' ({target_need["id"]})' else: link_text = target_need["id"] - if env.config.needs_show_link_type: + if needs_config.show_link_type: link_text += " [{type}]".format(type=target_need["type_name"]) # if index + 1 < len(ref_need["links_back"]): diff --git a/sphinx_needs/roles/need_outgoing.py b/sphinx_needs/roles/need_outgoing.py index a8b18e4d1..c640fec18 100644 --- a/sphinx_needs/roles/need_outgoing.py +++ b/sphinx_needs/roles/need_outgoing.py @@ -4,6 +4,7 @@ from sphinx.application import Sphinx from sphinx.util.nodes import make_refnode +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger from sphinx_needs.utils import check_and_calc_base_url_rel_path, unwrap @@ -18,17 +19,16 @@ class NeedOutgoing(nodes.Inline, nodes.Element): # type: ignore def process_need_outgoing( app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: List[nodes.Element] ) -> None: + builder = unwrap(app.builder) + env = unwrap(app.env) + needs_config = NeedsSphinxConfig(app.config) + report_dead_links = needs_config.report_dead_links # for node_need_ref in doctree.findall(NeedOutgoing): for node_need_ref in found_nodes: - builder = unwrap(app.builder) - env = unwrap(builder.env) - node_link_container = nodes.inline() needs_all_needs = getattr(env, "needs_all_needs", {}) ref_need = needs_all_needs[node_need_ref["reftarget"]] - report_dead_links = getattr(env.config, "needs_report_dead_links", True) - # Let's check if NeedIncoming shall follow a specific link type if "link_type" in node_need_ref.attributes: links = ref_need[node_need_ref.attributes["link_type"]] @@ -62,15 +62,15 @@ def process_need_outgoing( target_title = target_need["title"] target_id = target_need["id"] - if env.config.needs_show_link_title: + if needs_config.show_link_title: link_text = f"{target_title}" - if env.config.needs_show_link_id: + if needs_config.show_link_id: link_text += f" ({target_id})" else: link_text = target_id - if env.config.needs_show_link_type: + if needs_config.show_link_type: link_text += " [{type}]".format(type=target_need["type_name"]) node_need_ref[0] = nodes.Text(link_text) diff --git a/sphinx_needs/roles/need_ref.py b/sphinx_needs/roles/need_ref.py index 638cb13af..b800dc58a 100644 --- a/sphinx_needs/roles/need_ref.py +++ b/sphinx_needs/roles/need_ref.py @@ -6,6 +6,7 @@ from sphinx.application import Sphinx from sphinx.util.nodes import make_refnode +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.errors import NoUri from sphinx_needs.logging import get_logger from sphinx_needs.nodes import Need @@ -54,6 +55,7 @@ def transform_need_to_dict(need: Need) -> Dict[str, str]: def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes) -> None: builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) # for node_need_ref in doctree.findall(NeedRef): for node_need_ref in found_nodes: # Let's create a dummy node, for the case we will not be able to create a real reference @@ -91,7 +93,7 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou dict_need["title"] = target_need["parts"][part_id]["content"] # Shorten title, if necessary - max_length = app.config.needs_role_need_max_title_length + max_length = needs_config.role_need_max_title_length if 3 < max_length < len(dict_need["title"]): title = dict_need["title"] title = f"{title[: max_length - 3]}..." @@ -120,7 +122,7 @@ def process_need_ref(app: Sphinx, doctree: nodes.document, fromdocname: str, fou # If ref_name differs from the need id, we treat the "ref_name content" as title. dict_need["title"] = ref_name try: - link_text = app.config.needs_role_need_template.format(**dict_need) + link_text = needs_config.role_need_template.format(**dict_need) except KeyError as e: link_text = ( '"the config parameter needs_role_need_template uses not supported placeholders: %s "' % e diff --git a/sphinx_needs/services/github.py b/sphinx_needs/services/github.py index 2f065feb9..1594cad8d 100644 --- a/sphinx_needs/services/github.py +++ b/sphinx_needs/services/github.py @@ -9,6 +9,7 @@ from sphinx_needs.api import add_need_type from sphinx_needs.api.exceptions import NeedsApiConfigException +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.services.base import BaseService from sphinx_needs.services.config.github import ( CONFIG_OPTIONS, @@ -41,8 +42,9 @@ def __init__(self, app: Sphinx, name: str, config, **kwargs) -> None: self.username = self.config.get("username") self.token = self.config.get("token") - if "github" not in self.app.config.needs_layouts.keys(): - self.app.config.needs_layouts["github"] = GITHUB_LAYOUT + layouts = NeedsSphinxConfig(self.app.config).layouts + if "github" not in layouts: + layouts["github"] = GITHUB_LAYOUT self.gh_type_config = { "issue": {"url": "search/issues", "query": "is:issue", "need_type": "issue"}, diff --git a/sphinx_needs/services/manager.py b/sphinx_needs/services/manager.py index 6463927c3..53b6f067c 100644 --- a/sphinx_needs/services/manager.py +++ b/sphinx_needs/services/manager.py @@ -4,6 +4,7 @@ from sphinx.application import Sphinx from sphinx_needs.api.configuration import NEEDS_CONFIG +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.directives.needservice import NeedserviceDirective from sphinx_needs.logging import get_logger from sphinx_needs.services.base import BaseService @@ -18,7 +19,7 @@ def __init__(self, app: Sphinx): def register(self, name: str, clazz, **kwargs) -> None: try: - config = self.app.config.needs_services[name] + config = NeedsSphinxConfig(self.app.config).services[name] except KeyError: self.log.debug( "No service config found for {}. " "Add it in your conf.py to needs_services dictionary.".format(name) diff --git a/sphinx_needs/services/open_needs.py b/sphinx_needs/services/open_needs.py index a5d9512ae..24368bf92 100644 --- a/sphinx_needs/services/open_needs.py +++ b/sphinx_needs/services/open_needs.py @@ -6,6 +6,7 @@ from jinja2 import Template from sphinx.application import Sphinx +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.utils import dict_get, jinja_parse from .base import BaseService @@ -100,17 +101,17 @@ def _extract_data(self, data: List[Dict[str, Any]], options: Dict[str, Any]) -> :param options: dict of set directive options :return: list of need-data """ - + needs_config = NeedsSphinxConfig(self.app.config) need_data = [] if options is None: options = {} # How to know if a referenced link is a need object in the data we are retrieving from the Open Needs Server id_selector = self.mappings.get("id") ids_of_needs_data = [] # list of all IDs of need objects being retrieved from the Open Needs Server - needs_id_validator = self.app.config.needs_id_regex or "^[A-Z0-9_]{5,}" + needs_id_validator = needs_config.id_regex or "^[A-Z0-9_]{5,}" for item in data: if isinstance(id_selector, str): - context = {**item, **self.app.config.needs_render_context} + context = {**item, **needs_config.render_context} value = jinja_parse(context, id_selector) else: value = str(dict_get(item, id_selector)) @@ -133,7 +134,7 @@ def _extract_data(self, data: List[Dict[str, Any]], options: Dict[str, Any]) -> if isinstance(selector, str): # Set the "hard-coded" string or # combine the "hard-coded" string and dynamic value - context = {**item, **self.app.config.needs_render_context} + context = {**item, **needs_config.render_context} selector = jinja_parse(context, selector) # Set the returned string as value extra_data[name] = selector @@ -141,7 +142,7 @@ def _extract_data(self, data: List[Dict[str, Any]], options: Dict[str, Any]) -> extra_data[name] = dict_get(item, selector) content_template = Template(self.content, autoescape=True) - context = {"data": item, "options": options, **self.app.config.needs_render_context} + context = {"data": item, "options": options, **needs_config.render_context} content = content_template.render(context) content += "\n\n| \n" # Add enough space between content and extra_data @@ -163,7 +164,7 @@ def _extract_data(self, data: List[Dict[str, Any]], options: Dict[str, Any]) -> if isinstance(selector, str): # Set the "hard-coded" string or # combine the "hard-coded" string and dynamic value - context = {**item, **self.app.config.needs_render_context} + context = {**item, **needs_config.render_context} selector = jinja_parse(context, selector) # Set the returned string as value need_values[name] = selector diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index e8ac8e5bc..6fec54487 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -13,6 +13,7 @@ from matplotlib.figure import FigureBase from sphinx.application import BuildEnvironment, Sphinx +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.defaults import NEEDS_PROFILING from sphinx_needs.logging import get_logger @@ -106,12 +107,13 @@ def row_col_maker( """ builder = unwrap(app.builder) env = unwrap(builder.env) + needs_config = NeedsSphinxConfig(env.config) row_col = nodes.entry(classes=["needs_" + need_key]) para_col = nodes.paragraph() needs_string_links_option: List[str] = [] - for v in app.config.needs_string_links.values(): + for v in needs_config.string_links.values(): needs_string_links_option.extend(v["options"]) if need_key in need_info and need_info[need_key] is not None: @@ -128,13 +130,13 @@ def row_col_maker( link_part = None link_list = [] - for link_type in env.config.needs_extra_links: + for link_type in needs_config.extra_links: link_list.append(link_type["option"]) link_list.append(link_type["option"] + "_back") # For needs_string_links link_string_list = {} - for link_name, link_conf in app.config.needs_string_links.items(): + for link_name, link_conf in needs_config.string_links.items(): link_string_list[link_name] = { "url_template": Environment(loader=BaseLoader, autoescape=True).from_string(link_conf["link_url"]), "name_template": Environment(loader=BaseLoader, autoescape=True).from_string( @@ -193,7 +195,7 @@ def row_col_maker( para_col += ref_col elif matching_link_confs: para_col += match_string_link( - datum_text, datum, need_key, matching_link_confs, render_context=app.config.needs_render_context + datum_text, datum, need_key, matching_link_confs, render_context=needs_config.render_context ) else: para_col += text_col @@ -215,7 +217,9 @@ def rstjinja(app: Sphinx, docname: str, source: List[str]) -> None: if builder.format != "html": return src = source[0] - rendered = builder.templates.render_string(src, app.config.html_context, **app.config.needs_render_context) + rendered = builder.templates.render_string( + src, app.config.html_context, **NeedsSphinxConfig(app.config).render_context + ) source[0] = rendered @@ -227,7 +231,8 @@ def import_prefix_link_edit(needs: Dict[str, Any], id_prefix: str, needs_extra_l :param needs: Dict of all needs :param id_prefix: Prefix as string - :param needs_extra_links: config var of all supported extra links. Normally coming from env.config.needs_extra_links + :param needs_extra_links: config var of all supported extra links. + Normally coming from env.config.needs_extra_links :return: """ if not id_prefix: diff --git a/sphinx_needs/warnings.py b/sphinx_needs/warnings.py index 07e89ebc5..97ae9df18 100644 --- a/sphinx_needs/warnings.py +++ b/sphinx_needs/warnings.py @@ -7,7 +7,7 @@ from sphinx.application import Sphinx from sphinx.util import logging -from sphinx_needs.config import NEEDS_CONFIG +from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.filter_common import filter_needs from sphinx_needs.logging import get_logger from sphinx_needs.utils import unwrap @@ -54,7 +54,7 @@ def process_warnings(app: Sphinx, exception: Optional[Exception]) -> None: warnings = NEEDS_CONFIG.get("warnings") - warnings_always_warn = app.config.needs_warnings_always_warn + warnings_always_warn = NeedsSphinxConfig(app.config).warnings_always_warn with logging.pending_logging(): logger.info("\nChecking sphinx-needs warnings")