def add_extra_option(app, name):
    """
    Adds an extra option to the configuration. This option can then later be used inside needs or ``add_need``.

    Same impact as using :ref:`needs_extra_options` manually.

    **Usage**::

        from sphinxcontrib.needs.api import add_extra_option

        add_extra_option(app, 'my_extra_option')

    :param app: Sphinx application object
    :param name: Name as string of the extra option
    :return: None
    """

    extra_options = NEEDS_CONFIG.create_or_get("extra_options", dict)

    if name in extra_options:
        raise NeedsApiConfigWarning(f"Option {name} already registered.")

    NEEDS_CONFIG.add("extra_options", {name: directives.unchanged},
                     dict,
                     append=True)
def add_warning(app, name, function=None, filter_string=None):
    """
    Registers a warning.

    A warning can be based on the result of a given filter_string or an own defined function.

    :param app: Sphinx app object
    :param name: Name as string for the warning
    :param function: function to execute to check the warning
    :param filter_string: filter_string to use for the warning
    :return: None
    """
    warnings_option = NEEDS_CONFIG.create_or_get("warnings", dict)

    if function is None and filter_string is None:
        raise NeedsApiConfigException(
            "Function or filter_string must be given for add_warning_func")

    if function is not None and filter_string is not None:
        raise NeedsApiConfigException(
            "For add_warning_func only function or filter_string is allowed to be set, "
            "not both.")

    warning_check = function or filter_string

    if name in warnings_option:
        raise NeedsApiConfigException(f"Warning {name} already registered.")

    # warnings_option[name] = warning_check
    NEEDS_CONFIG.add("warnings", {name: warning_check}, dict, append=True)
Пример #3
0
def prepare_env(app, env, _docname):
    """
    Prepares the sphinx environment to store sphinx-needs internal data.
    """
    if not hasattr(env, "needs_all_needs"):
        # Used to store all needed information about all needs in document
        env.needs_all_needs = {}

    if not hasattr(env, "needs_all_filters"):
        # Used to store all needed information about all filters in document
        env.needs_all_filters = {}

    if not hasattr(env, "needs_services"):
        # Used to store all needed information about all services
        app.needs_services = ServiceManager(app)

    # Register embedded services
    app.needs_services.register("github-issues", GithubService, gh_type="issue")
    app.needs_services.register("github-prs", GithubService, gh_type="pr")
    app.needs_services.register("github-commits", GithubService, gh_type="commit")

    # Register user defined services
    for name, service in app.config.needs_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

    # Register built-in functions
    for need_common_func in needs_common_functions:
        register_func(need_common_func)

    # Register functions configured by user
    for needs_func in needs_functions:
        register_func(needs_func)

    # Own extra options
    for option in ["hidden", "duration", "completion", "has_dead_links", "has_forbidden_dead_links"]:
        # Check if not already set by user
        # if option not in app.config.needs_extra_options:
        #     app.config.needs_extra_options[option] = directives.unchanged
        if option not in NEEDS_CONFIG.get("extra_options"):
            add_extra_option(app, option)

    # 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
    basic_link_type_found = False
    parent_needs_link_type_found = False
    for link_type in link_types:
        if link_type["option"] == "links":
            basic_link_type_found = True
        elif link_type["option"] == "parent_needs":
            parent_needs_link_type_found = True

    if not basic_link_type_found:
        common_links.append(
            {
                "option": "links",
                "outgoing": "links outgoing",
                "incoming": "links incoming",
                "copy": False,
                "color": "#000000",
            }
        )

    if not parent_needs_link_type_found:
        common_links.append(
            {
                "option": "parent_needs",
                "outgoing": "parent needs",
                "incoming": "child needs",
                "copy": False,
                "color": "#333333",
            }
        )

    app.config.needs_extra_links = common_links + app.config.needs_extra_links

    app.config.needs_layouts = {**LAYOUTS, **app.config.needs_layouts}

    app.config.needs_flow_configs.update(NEEDFLOW_CONFIG_DEFAULTS)

    if not hasattr(env, "needs_workflow"):
        # Used to store workflow status information for already executed tasks.
        # Some tasks like backlink_creation need be be performed only once.
        # But most sphinx-events get called several times (for each single document
        # file), which would also execute our code several times...
        env.needs_workflow = {
            "backlink_creation_links": False,
            "dynamic_values_resolved": False,
            "needs_extended": False,
        }
        for link_type in app.config.needs_extra_links:
            env.needs_workflow["backlink_creation_{}".format(link_type["option"])] = False
Пример #4
0
def load_config(app: Sphinx, *_args):
    """
    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):
        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:
        if option in existing_extra_options:
            log.warning(f'extra_option "{option}" already registered.')
        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 = {}
    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

    # Update NeedDirective to use customized options
    NeedDirective.option_spec.update(extra_options)
    NeedserviceDirective.option_spec.update(extra_options)

    # Update NeedDirective to use customized links
    NeedDirective.option_spec.update(extra_links)
    NeedserviceDirective.option_spec.update(extra_links)

    # Update NeedextendDirective with option modifiers.
    for key, value in NEED_DEFAULT_OPTIONS.items():

        # Ignore options like "id"
        if key in NEEDEXTEND_NOT_ALLOWED_OPTIONS:
            continue

        NeedextendDirective.option_spec.update(
            {
                key: value,
                f"+{key}": value,
                f"-{key}": directives.flag,
            }
        )

    for key, value in extra_links.items():
        NeedextendDirective.option_spec.update(
            {
                key: value,
                f"+{key}": value,
                f"-{key}": directives.flag,
                f"{key}_back": value,
                f"+{key}_back": value,
                f"-{key}_back": directives.flag,
            }
        )

    # "links" is not part of the extra_links-dict, so we need
    # to set the links_back values by hand
    NeedextendDirective.option_spec.update(
        {
            "links_back": NEED_DEFAULT_OPTIONS["links"],
            "+links_back": NEED_DEFAULT_OPTIONS["links"],
            "-links_back": directives.flag,
        }
    )
    for key, value in extra_options.items():
        NeedextendDirective.option_spec.update(
            {
                key: value,
                f"+{key}": value,
                f"-{key}": directives.flag,
            }
        )

    if title_optional or title_from_content:
        NeedDirective.required_arguments = 0
        NeedDirective.optional_arguments = 1

    for t in 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():
        if name not in existing_warnings:
            NEEDS_CONFIG.add("warnings", {name: check}, dict, append=True)
        else:
            log.warning(f'{name} for "warnings" is already registered.')
Пример #5
0
def setup(app):
    log = get_logger(__name__)
    log.debug("Starting setup of Sphinx-Needs")
    log.debug("Load Sphinx-Data-Viewer for Sphinx-Needs")
    app.setup_extension("sphinx_data_viewer")

    app.add_builder(NeedsBuilder)
    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_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_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")

    # 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", False, "html", types=[bool])

    # 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")

    app.add_config_value("needs_filter_data", {}, "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])

    # Define nodes
    app.add_node(Need, html=(html_visit, html_depart), latex=(latex_visit, latex_depart))
    app.add_node(
        Needfilter,
    )
    app.add_node(Needbar)
    app.add_node(Needimport)
    app.add_node(Needlist)
    app.add_node(Needtable)
    app.add_node(Needflow)
    app.add_node(Needpie)
    app.add_node(Needsequence)
    app.add_node(Needgantt)
    app.add_node(Needextract)
    app.add_node(Needservice)
    app.add_node(Needextend)
    app.add_node(NeedPart, html=(visitor_dummy, visitor_dummy), latex=(visitor_dummy, visitor_dummy))

    ########################################################################
    # DIRECTIVES
    ########################################################################

    # Define directives
    app.add_directive("needbar", NeedbarDirective)
    app.add_directive("needfilter", NeedfilterDirective)
    app.add_directive("needlist", NeedlistDirective)
    app.add_directive("needtable", NeedtableDirective)
    app.add_directive("needflow", NeedflowDirective)
    app.add_directive("needpie", NeedpieDirective)
    app.add_directive("needsequence", NeedsequenceDirective)
    app.add_directive("needgantt", NeedganttDirective)
    app.add_directive("needimport", NeedimportDirective)
    app.add_directive("needextract", NeedextractDirective)
    app.add_directive("needservice", NeedserviceDirective)
    app.add_directive("needextend", NeedextendDirective)

    ########################################################################
    # ROLES
    ########################################################################
    # Provides :need:`ABC_123` for inline links.
    app.add_role("need", XRefRole(nodeclass=NeedRef, innernodeclass=nodes.emphasis, warn_dangling=True))

    app.add_role("need_incoming", XRefRole(nodeclass=NeedIncoming, innernodeclass=nodes.emphasis, warn_dangling=True))

    app.add_role("need_outgoing", XRefRole(nodeclass=NeedOutgoing, innernodeclass=nodes.emphasis, warn_dangling=True))

    app.add_role("need_part", XRefRole(nodeclass=NeedPart, innernodeclass=nodes.inline, warn_dangling=True))
    # Shortcut for need_part
    app.add_role("np", XRefRole(nodeclass=NeedPart, innernodeclass=nodes.inline, warn_dangling=True))

    app.add_role("need_count", XRefRole(nodeclass=NeedCount, innernodeclass=nodes.inline, warn_dangling=True))

    app.add_role("need_func", XRefRole(nodeclass=NeedFunc, innernodeclass=nodes.inline, warn_dangling=True))

    ########################################################################
    # EVENTS
    ########################################################################
    # Make connections to events
    app.connect("env-purge-doc", purge_needs)
    app.connect("config-inited", load_config)
    app.connect("env-before-read-docs", prepare_env)
    app.connect("env-before-read-docs", load_external_needs)
    app.connect("config-inited", check_configuration)
    app.connect("env-merge-info", merge_data)

    # There is also the event doctree-read.
    # But it looks like in this event no references are already solved, which
    # makes trouble in our code.
    # However, some sphinx-internal actions (like image collection) are already called during
    # doctree-read. So manipulating the doctree may result in conflicts, as e.g. images get not
    # registered for sphinx. So some sphinx-internal tasks/functions may be called by hand again...
    # See also https://github.com/sphinx-doc/sphinx/issues/7054#issuecomment-578019701 for an example
    app.connect("doctree-resolved", add_sections)
    app.connect("doctree-resolved", process_need_nodes)
    app.connect("doctree-resolved", process_needextend)  # Must be done very early, as it modifies need data
    app.connect("doctree-resolved", print_need_nodes)
    app.connect("doctree-resolved", process_needbar)
    app.connect("doctree-resolved", process_needextract)
    app.connect("doctree-resolved", process_needfilters)
    app.connect("doctree-resolved", process_needlist)
    app.connect("doctree-resolved", process_needtables)
    app.connect("doctree-resolved", process_needflow)
    app.connect("doctree-resolved", process_needpie)
    app.connect("doctree-resolved", process_needsequence)
    app.connect("doctree-resolved", process_needgantt)
    app.connect("doctree-resolved", process_need_part)
    app.connect("doctree-resolved", process_need_ref)
    app.connect("doctree-resolved", process_need_incoming)
    app.connect("doctree-resolved", process_need_outgoing)
    app.connect("doctree-resolved", process_need_count)
    app.connect("doctree-resolved", process_need_func)
    app.connect("build-finished", process_warnings)
    app.connect("env-updated", install_lib_static_files)

    # Called during consistency check, which if after everything got read in.
    # app.connect('env-check-consistency', process_warnings)

    # This should be called last, so that need-styles can override styles from used libraries
    app.connect("env-updated", install_styles_static_files)

    # Be sure Sphinx-Needs config gets erased before any events or external API calls get executed.
    # So never but this inside an event.
    NEEDS_CONFIG.create("extra_options", dict, overwrite=True)
    NEEDS_CONFIG.create("warnings", dict, overwrite=True)

    return {
        "version": VERSION,
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }
Пример #6
0
def process_warnings(app, exception):
    """
    Checks the configured warnings.

    This func gets called by the latest sphinx-event, so that really everything is already done.

    :param app: application
    :param exception: raised exceptions
    :return:
    """

    # We get called also if an exception occured during build
    # In this case the build is already broken and we do not need to check anything.
    if exception:
        return

    env = app.env
    # If no needs were defined, we do not need to do anything
    if not hasattr(env, "needs_all_needs"):
        return

    # Check if warnings already got executed.
    # Needed because the used event gets executed multiple times, but warnings need to be checked only
    # on first execution
    if hasattr(env, "needs_warnings_executed") and env.needs_warnings_executed:
        return

    env.needs_warnings_executed = True

    needs = env.needs_all_needs

    # Exclude external needs for warnings check
    checked_needs = {}
    for need_id, need in needs.items():
        if not need["is_external"]:
            checked_needs[need_id] = need

    # warnings = app.config.needs_warnings
    warnings = NEEDS_CONFIG.get("warnings")

    warnings_always_warn = app.config.needs_warnings_always_warn

    with logging.pending_logging():
        logger.info("\nChecking sphinx-needs warnings")
        warning_raised = False
        for warning_name, warning_filter in warnings.items():
            if isinstance(warning_filter, str):
                # filter string used
                result = filter_needs(app, checked_needs.values(),
                                      warning_filter)
            elif callable(warning_filter):
                # custom defined filter code used from conf.py
                result = []
                for need in checked_needs.values():
                    if warning_filter(need, logger):
                        result.append(need)
            else:
                logger.warning(
                    f"Unknown needs warnings filter {warning_filter}!")

            if len(result) == 0:
                logger.info(f"{warning_name}: passed")
            else:
                need_ids = [x["id"] for x in result]

                # Set Sphinx statuscode to 1, only if -W is used with sphinx-build
                # Because Sphinx statuscode got calculated in very early build phase and based on warning_count
                # Sphinx-needs warnings check hasn't happened yet
                # see deatils in https://github.com/sphinx-doc/sphinx/blob/81a4fd973d4cfcb25d01a7b0be62cdb28f82406d/sphinx/application.py#L345 # noqa
                # To be clear, app.keep_going = -W and --keep-going, and will overrite -W after
                # see details in https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/application.py#L182
                if app.statuscode == 0 and (app.keep_going
                                            or app.warningiserror):
                    app.statuscode = 1

                # get the text for used filter, either from filter string or function name
                if callable(warning_filter):
                    warning_text = warning_filter.__name__
                elif isinstance(warning_filter, str):
                    warning_text = warning_filter

                if warnings_always_warn:
                    logger.warning(
                        "{}: failed\n\t\tfailed needs: {} ({})\n\t\tused filter: {}"
                        .format(warning_name, len(need_ids),
                                ", ".join(need_ids), warning_text))
                else:
                    logger.info(
                        "{}: failed\n\t\tfailed needs: {} ({})\n\t\tused filter: {}"
                        .format(warning_name, len(need_ids),
                                ", ".join(need_ids), warning_text))
                    warning_raised = True

        if warning_raised:
            logger.warning(
                "Sphinx-Needs warnings were raised. See console / log output for details."
            )
Пример #7
0
    def run(self):
        #############################################################################################
        # Get environment
        #############################################################################################
        env = self.env

        # ToDo: Keep this in directive!!!
        collapse = self.options.get("collapse", None)
        if isinstance(collapse, str):
            if collapse.upper() in ["TRUE", 1, "YES"]:
                collapse = True
            elif collapse.upper() in ["FALSE", 0, "NO"]:
                collapse = False
            else:
                raise Exception("collapse attribute must be true or false")

        hide = "hide" in self.options

        id = self.options.get("id", None)
        content = "\n".join(self.content)
        status = self.options.get("status", None)
        if status:
            status = status.replace(
                "__", ""
            )  # Support for multiline options, which must use __ for empty lines
        tags = self.options.get("tags", "")
        style = self.options.get("style", None)
        layout = self.options.get("layout", "")
        template = self.options.get("template", None)
        pre_template = self.options.get("pre_template", None)
        post_template = self.options.get("post_template", None)
        duration = self.options.get("duration", None)
        completion = self.options.get("completion", None)

        need_extra_options = {"duration": duration, "completion": completion}
        for extra_link in env.config.needs_extra_links:
            need_extra_options[extra_link["option"]] = self.options.get(
                extra_link["option"], "")

        NEEDS_CONFIG.get("extra_options")
        for extra_option in NEEDS_CONFIG.get("extra_options").keys():
            need_extra_options[extra_option] = self.options.get(
                extra_option, "")

        need_nodes = add_need(
            env.app,
            self.state,
            self.docname,
            self.lineno,
            need_type=self.name,
            title=self.trimmed_title,
            id=id,
            content=content,
            status=status,
            tags=tags,
            hide=hide,
            template=template,
            pre_template=pre_template,
            post_template=post_template,
            collapse=collapse,
            style=style,
            layout=layout,
            **need_extra_options,
        )
        return need_nodes
Пример #8
0
    def run(self):
        needs_list = {}
        version = self.options.get("version", None)
        filter_string = self.options.get("filter", None)
        id_prefix = self.options.get("id_prefix", "")

        tags = self.options.get("tags", [])
        if len(tags) > 0:
            tags = [tag.strip() for tag in re.split(";|,", tags)]

        env = self.state.document.settings.env

        need_import_path = self.arguments[0]

        if not os.path.isabs(need_import_path):
            # Relative path should starts from current rst file directory
            curr_dir = os.path.dirname(self.docname)
            new_need_import_path = os.path.join(env.app.confdir, curr_dir,
                                                need_import_path)

            correct_need_import_path = new_need_import_path
            if not os.path.exists(new_need_import_path):
                # Check the old way that calculates relative path starting from conf.py directory
                old_need_import_path = os.path.join(env.app.confdir,
                                                    need_import_path)
                if os.path.exists(old_need_import_path):
                    correct_need_import_path = old_need_import_path
                    logger.warning(
                        "Deprecation warning: Relative path must be relative to the current document in future, "
                        "not to the conf.py location. Use a starting '/', like '/needs.json', to make the path "
                        "relative to conf.py.")
        else:
            # Absolute path starts with /, based on the conf.py directory. The / need to be striped
            correct_need_import_path = os.path.join(env.app.confdir,
                                                    need_import_path[1:])

        if not os.path.exists(correct_need_import_path):
            raise ReferenceError(
                f"Could not load needs import file {correct_need_import_path}")

        with open(correct_need_import_path) as needs_file:
            needs_file_content = needs_file.read()
        try:
            needs_import_list = json.loads(needs_file_content)
        except json.JSONDecodeError as e:
            # ToDo: Add exception handling
            raise e

        if version is None:
            try:
                version = needs_import_list["current_version"]
                if not isinstance(version, str):
                    raise KeyError
            except KeyError:
                raise CorruptedNeedsFile(
                    f"Key 'current_version' missing or corrupted in {correct_need_import_path}"
                )
        if version not in needs_import_list["versions"].keys():
            raise VersionNotFound(
                f"Version {version} not found in needs import file {correct_need_import_path}"
            )

        needs_list = needs_import_list["versions"][version]["needs"]

        # Filter imported needs
        needs_list_filtered = {}
        for key, need in needs_list.items():
            if filter_string is None:
                needs_list_filtered[key] = need
            else:
                filter_context = {key: value for key, value in need.items()}

                # Support both ways of addressing the description, as "description" is used in json file, but
                # "content" is the sphinx internal name for this kind of information
                filter_context["content"] = need["description"]
                try:
                    if filter_single_need(env.app, filter_context,
                                          filter_string):
                        needs_list_filtered[key] = need
                except Exception as e:
                    logger.warning(
                        "needimport: Filter {} not valid. Error: {}. {}{}".
                        format(filter_string, e, self.docname, self.lineno))

        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.
        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 env.config.needs_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:
                                    need[extra_link["option"]][n] = "".join(
                                        [id_prefix, id])
                    # Manipulate descriptions
                    # ToDo: Use regex for better matches.
                    need["description"] = need["description"].replace(
                        id, "".join([id_prefix, id]))

        # tags update
        for need in needs_list.values():
            need["tags"] = need["tags"] + tags

        need_nodes = []
        for need in needs_list.values():
            # Set some values based on given option or value from imported need.
            need["template"] = self.options.get(
                "template", getattr(need, "template", None))
            need["pre_template"] = self.options.get(
                "pre_template", getattr(need, "pre_template", None))
            need["post_template"] = self.options.get(
                "post_template", getattr(need, "post_template", None))
            need["layout"] = self.options.get("layout",
                                              getattr(need, "layout", None))
            need["style"] = self.options.get("style",
                                             getattr(need, "style", None))
            need["style"] = self.options.get("style",
                                             getattr(need, "style", None))
            if "hide" in self.options:
                need["hide"] = True
            else:
                need["hide"] = getattr(need, "hide", None)
            need["collapse"] = self.options.get(
                "collapse", getattr(need, "collapse", None))

            # The key needs to be different for add_need() api call.
            need["need_type"] = need["type"]

            # Replace id, to get unique ids
            need["id"] = id_prefix + need["id"]

            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 env.config.needs_extra_links
            ]
            extra_option_keys = list(NEEDS_CONFIG.get("extra_options").keys())
            default_options = [
                "title",
                "status",
                "content",
                "id",
                "tags",
                "hide",
                "template",
                "pre_template",
                "post_template",
                "collapse",
                "style",
                "layout",
                "need_type",
            ]
            delete_options = []
            for option in need.keys():
                if option not in default_options + extra_link_keys + extra_option_keys:
                    delete_options.append(option)

            for option in delete_options:
                del need[option]

            need["docname"] = self.docname
            need["lineno"] = self.lineno

            nodes = add_need(env.app, self.state, **need)
            need_nodes.extend(nodes)

        return need_nodes