Exemplo n.º 1
0
def make_hashed_id(app, need_type, full_title, content, id_length=None):
    """
    Creates an ID based on title or need.

    Also cares about the correct prefix, which is specified for each need type.

    :param app: Sphinx application object
    :param need_type: name of the need directive, e.g. req
    :param full_title: full title of the need
    :param content: content of the need
    :param id_length: maximum length of the generated ID
    :return: ID as string
    """
    types = app.config.needs_types
    if id_length is None:
        id_length = app.config.needs_id_length
    type_prefix = None
    for ntype in types:
        if ntype["directive"] == need_type:
            type_prefix = ntype["prefix"]
            break
    if type_prefix is None:
        raise NeedsInvalidException(
            'Given need_type {} is unknown. File {}'.format(
                need_type, app.env.docname))

    hashable_content = full_title or '\n'.join(content)
    return "%s%s" % (type_prefix, hashlib.sha1(
        hashable_content.encode("UTF-8")).hexdigest().upper()[:id_length])
Exemplo n.º 2
0
def _merge_global_options(app, needs_info, global_options):
    """Add all global defined options to needs_info"""
    if global_options is None:
        return
    for key, value in global_options.items():

        # If key already exists in needs_info, this global_option got overwritten manually in current need
        if key in needs_info and needs_info[key]:
            continue

        if isinstance(value, tuple):
            values = [value]
        elif isinstance(value, list):
            values = value
        else:
            needs_info[key] = value
            continue

        for single_value in values:
            if len(single_value) < 2 or len(single_value) > 3:
                raise NeedsInvalidException(
                    f"global option tuple has wrong amount of parameters: {key}"
                )
            if filter_single_need(app, needs_info, single_value[1]):
                # Set value, if filter has matched
                needs_info[key] = single_value[0]
            elif len(single_value) == 3 and (key not in needs_info.keys()
                                             or len(str(needs_info[key])) > 0):
                # Otherwise set default, but only if no value was set before or value is "" and a default is defined
                needs_info[key] = single_value[2]
            else:
                # If not value was set until now, we have to set an empty value, so that we are sure that each need
                # has at least the key.
                if key not in needs_info.keys():
                    needs_info[key] = ""
Exemplo n.º 3
0
 def _get_full_title(self):
     """
     Determines the title for the need in order of precedence:
     directive argument, first sentence of requirement (if
     `:title_from_content:` was set, and '' if no title is to be derived."""
     if len(self.arguments) > 0:  # a title was passed
         if "title_from_content" in self.options:
             self.log.warning(
                 'Needs: need "{}" has :title_from_content: set, '
                 "but a title was provided. (see file {})".format(
                     self.arguments[0], self.docname))
         return self.arguments[0]
     elif self.title_from_content:
         first_sentence = re.split(r"[.\n]", "\n".join(self.content))[0]
         if not first_sentence:
             raise NeedsInvalidException(":title_from_content: set, but "
                                         "no content provided. "
                                         "(Line {} of file {}".format(
                                             self.lineno, self.docname))
         return first_sentence
     else:
         return ""
Exemplo n.º 4
0
    def run(self):
        env = self.state.document.settings.env
        if not hasattr(env, "need_all_needtables"):
            env.need_all_needtables = {}

        # be sure, global var is available. If not, create it
        if not hasattr(env, "needs_all_needs"):
            env.needs_all_needs = {}

        targetid = "needtable-{docname}-{id}".format(
            docname=env.docname, id=env.new_serialno("needtable"))
        targetnode = nodes.target("", "", ids=[targetid])

        columns = str(self.options.get("columns", ""))
        if len(columns) == 0:
            columns = env.app.config.needs_table_columns
        if isinstance(columns, str):
            columns = [col.strip() for col in re.split(";|,", columns)]

        columns = [get_title(col) for col in columns]

        colwidths = str(self.options.get("colwidths", ""))
        colwidths_list = []
        if colwidths:
            colwidths_list = [
                int(width.strip()) for width in re.split(";|,", colwidths)
            ]
            if len(columns) != len(colwidths_list):
                raise NeedsInvalidException(
                    f"Amount of elements in colwidths and columns do not match: "
                    f"colwidths: {len(colwidths_list)} and columns: {len(columns)}"
                )

        classes = get_option_list(self.options, "class")

        style = self.options.get("style", "").upper()
        style_row = self.options.get("style_row", "")
        style_col = self.options.get("style_col", "")

        sort = self.options.get("sort", "id_complete")

        title = None
        if self.arguments:
            title = self.arguments[0]

        # Add the need and all needed information
        env.need_all_needtables[targetid] = {
            "docname": env.docname,
            "lineno": self.lineno,
            "target_node": targetnode,
            "caption": title,
            "classes": classes,
            "columns": columns,
            "colwidths": colwidths_list,
            "style": style,
            "style_row": style_row,
            "style_col": style_col,
            "sort": sort,
            # As the following options are flags, the content is None, if set.
            # If not set, the options.get() method returns False
            "show_filters": self.options.get("show_filters", False) is None,
            "show_parts": self.options.get("show_parts", False) is None,
            "env": env,
        }
        env.need_all_needtables[targetid].update(
            self.collect_filter_attributes())

        return [targetnode] + [Needtable("")]
Exemplo n.º 5
0
def add_need(app,
             state,
             docname,
             lineno,
             need_type,
             title,
             id=None,
             content="",
             status=None,
             tags=None,
             links_string=None,
             hide=False,
             hide_tags=False,
             hide_status=False,
             collapse=None,
             style=None,
             layout=None,
             template=None,
             pre_template=None,
             post_template=None,
             **kwargs):
    """
    Creates a new need and returns its node.

    ``add_need`` allows to create needs programmatically and use its returned node to be integrated in any
    docutils based structure.

    ``kwags`` can contain options defined in ``needs_extra_options`` and ``needs_extra_links``.
    If an entry is found in ``kwags``, which *is not* specified in the configuration or registered e.g. via
    ``add_extra_option``, an exception is raised.

    **Usage**:

    Normally needs get created during handling of a specialised directive.
    So this pseudo-code shows how to use ``add_need`` inside such a directive.

    .. code-block:: python

        from docutils.parsers.rst import Directive
        from sphinxcontrib.needs.api import add_need

        class MyDirective(Directive)
            # configs and init routine

            def run():
                main_section = []

                docname = self.state.document.settings.env.docname

                # All needed sphinx-internal information we can take from our current directive class.
                # e..g app, state, lineno
                main_section += add_need(self.env.app, self.state, docname, self.lineno,
                                         need_type="req", title="my title", id="ID_001"
                                         content=self.content)

                # Feel free to add custom stuff to main_section like sections, text, ...

                return main_section

    :param app: Sphinx application object.
    :param state: Current state object.
    :param docname: documentation name.
    :param lineno: line number.
    :param need_type: Name of the need type to create.
    :param title: String as title.
    :param id: ID as string. If not given, a id will get generated.
    :param content: Content as single string.
    :param status: Status as string.
    :param tags: Tags as single string.
    :param links_string: Links as single string.
    :param hide: boolean value.
    :param hide_tags: boolean value. (Not used with Sphinx-Needs >0.5.0)
    :param hide_status: boolean value. (Not used with Sphinx-Needs >0.5.0)
    :param collapse: boolean value.
    :param style: String value of class attribute of node.
    :param layout: String value of layout definition to use
    :param template: Template name to use for the content of this need
    :param pre_template: Template name to use for content added before need
    :param post_template: Template name to use for the content added after need

    :return: node
    """
    #############################################################################################
    # Get environment
    #############################################################################################
    env = app.env
    types = env.app.config.needs_types
    type_name = ""
    type_prefix = ""
    type_color = ""
    type_style = ""
    found = False
    for ntype in types:
        if ntype["directive"] == need_type:
            type_name = ntype["title"]
            type_prefix = ntype["prefix"]
            type_color = ntype["color"]
            type_style = ntype["style"]
            found = True
            break
    if not found:
        # This should never happen. But it may happen, if Sphinx is called multiples times
        # inside one ongoing python process.
        # In this case the configuration from a prior sphinx run may be active, which has registered a directive,
        # which is reused inside a current document, but no type was defined for the current run...
        # Yeah, this really has happened...
        return [nodes.Text('', '')]

    # Get the id or generate a random string/hash string, which is hopefully unique
    # 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 env.app.config.needs_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))

    if id is None:
        need_id = make_hashed_id(app, need_type, title, content)
    else:
        need_id = id

    if env.app.config.needs_id_regex and not re.match(
            env.app.config.needs_id_regex, need_id):
        raise NeedsInvalidException(
            "Given ID '{id}' does not match configured regex '{regex}'".format(
                id=need_id, regex=env.app.config.needs_id_regex))

    #print ("create need: ", need_id, id)
    # Calculate target id, to be able to set a link back
    target_node = nodes.target('', '', ids=[need_id])
    #print (target_node)

    # Removed 0.5.0
    # if collapse is None:
    #     collapse = getattr(env.app.config, "needs_collapse_details", True)

    # Handle status
    # Check if status is in needs_statuses. If not raise an error.
    if env.app.config.needs_statuses:
        if status not in [
                stat["name"] for stat in env.app.config.needs_statuses
        ]:
            raise NeedsStatusNotAllowed(
                "Status {0} of need id {1} is not allowed "
                "by config value 'needs_statuses'.".format(status, need_id))

    if tags is None:
        tags = []
    if len(tags) > 0:
        tags = [tag.strip() for tag in re.split(";|,", tags)]
        for i in range(len(tags)):
            if len(tags[i]) == 0 or tags[i].isspace():
                del (tags[i])
                logger.warning(
                    'Scruffy tag definition found in need {}. '
                    'Defined tag contains spaces only.'.format(need_id))

        # Check if tag is in needs_tags. If not raise an error.
        if env.app.config.needs_tags:
            for tag in tags:
                if tag not in [
                        tag["name"] for tag in env.app.config.needs_tags
                ]:
                    raise NeedsTagNotAllowed(
                        "Tag {0} of need id {1} is not allowed "
                        "by config value 'needs_tags'.".format(tag, need_id))
        # This may have cut also dynamic function strings, as they can contain , as well.
        # So let put them together again
        # ToDo: There may be a smart regex for the splitting. This would avoid this mess of code...
    tags = _fix_list_dyn_func(tags)

    #############################################################################################
    # Add need to global need list
    #############################################################################################
    # be sure, global var is available. If not, create it
    if not hasattr(env, 'needs_all_needs'):
        env.needs_all_needs = {}

    if need_id in env.needs_all_needs.keys():
        if id is not None:
            raise NeedsDuplicatedId(
                "A need with ID {} already exists! "
                "This is not allowed. Document {}[{}] Title: {}.".format(
                    need_id, docname, lineno, title))
        else:  # this is a generated ID
            raise NeedsDuplicatedId(
                "Needs could not generate a unique ID for a need with "
                "the title '{}' because another need had the same title. "
                "Either supply IDs for the requirements or ensure the "
                "titles are different.  NOTE: If title is being generated "
                "from the content, then ensure the first sentence of the "
                "requirements are different.".format(' '.join(title)))

    # Trim title if it is too long
    max_length = getattr(env.app.config, 'needs_max_title_length', 30)
    if max_length == -1 or len(title) <= max_length:
        trimmed_title = title
    elif max_length <= 3:
        trimmed_title = title[:max_length]
    else:
        trimmed_title = title[:max_length - 3] + '...'

    # Add the need and all needed information
    needs_info = {
        'docname': docname,
        'lineno': lineno,
        'target_node': target_node,
        'content_node': None,  # gets set after rst parsing
        'type': need_type,
        'type_name': type_name,
        'type_prefix': type_prefix,
        'type_color': type_color,
        'type_style': type_style,
        'status': status,
        'tags': tags,
        'id': need_id,
        'title': trimmed_title,
        'full_title': title,
        'content': content,
        'collapse': collapse,
        'style': style,
        'layout': layout,
        'template': template,
        'pre_template': pre_template,
        'post_template': post_template,
        'hide': hide,
        'parts': {},
        'is_part': False,
        'is_need': True
    }
    #print (needs_info)
    needs_extra_options = env.config.needs_extra_options.keys()
    _merge_extra_options(needs_info, kwargs, needs_extra_options)

    needs_global_options = env.config.needs_global_options
    _merge_global_options(needs_info, needs_global_options)

    link_names = [x['option'] for x in env.config.needs_extra_links]
    for keyword in kwargs:
        if keyword not in needs_extra_options and keyword not in link_names:
            raise NeedsInvalidOption(
                'Unknown Option {}. '
                'Use needs_extra_options or needs_extra_links in conf.py'
                'to define this option.'.format(keyword))

    # Merge links
    copy_links = []

    for link_type in env.config.needs_extra_links:
        # Check, if specific link-type got some arguments during method call
        if link_type['option'] not in list(kwargs.keys(
        )) and link_type['option'] not in needs_global_options.keys():
            # if not we set no links, but entry in needS_info must be there
            links = []
        elif link_type['option'] in needs_global_options.keys() and \
                (link_type['option'] not in list(kwargs.keys()) or len(str( kwargs[link_type['option']])) == 0):
            # If it is in global option, value got already set during prior handling of them
            links_string = needs_info[link_type['option']]
            links = _read_in_links(links_string)
        else:
            # if it is set in kwargs, take this value and maybe override set value from global_options
            links_string = kwargs[link_type['option']]
            links = _read_in_links(links_string)

        needs_info[link_type["option"]] = links
        needs_info['{}_back'.format(link_type["option"])] = set()

        if 'copy' not in link_type.keys():
            link_type['copy'] = False

        if link_type['copy'] and link_type['option'] != 'links':
            copy_links += links  # Save extra links for main-links

    needs_info['links'] += copy_links  # Set copied links to main-links

    env.needs_all_needs[need_id] = needs_info

    # Template builds
    ##############################

    # template
    if needs_info['template'] is not None and len(needs_info['template']) > 0:
        new_content = _prepare_template(app, needs_info, 'template')
        # Overwrite current content
        content = new_content
        needs_info['content'] = new_content
    else:
        new_content = None

    # pre_template
    if needs_info['pre_template'] is not None and len(
            needs_info['pre_template']) > 0:
        pre_content = _prepare_template(app, needs_info, 'pre_template')
        needs_info['pre_content'] = pre_content
    else:
        pre_content = None

    # post_template
    if needs_info['post_template'] is not None and len(
            needs_info['post_template']) > 0:
        post_content = _prepare_template(app, needs_info, 'post_template')
        needs_info['post_content'] = post_content
    else:
        post_content = None

    if needs_info['hide']:
        return [target_node]

    # Adding of basic Need node.
    ############################
    # Title and meta data information gets added alter during event handling via process_need_nodes()
    # We just add a basic need node and render the rst-based content, because this can not be done later.
    # style_classes = ['need', type_name, 'need-{}'.format(type_name.lower())]  # Used < 0.4.4
    style_classes = ['need', 'need-{}'.format(need_type.lower())]
    if style is not None and style != '':
        style_classes.append(style)

    node_need = sphinxcontrib.needs.directives.need.Need('',
                                                         classes=style_classes,
                                                         ids=[need_id])

    # Render rst-based content and add it to the need-node

    node_need_content = _render_template(content, docname, lineno, state)
    need_parts = find_parts(node_need_content)
    update_need_with_parts(env, needs_info, need_parts)

    node_need += node_need_content.children

    needs_info['content_node'] = node_need
    #print("target_node: ", target_node)
    #print("node_need: ", node_need)
    #needs_info['content_node']['id'] = need_id

    return_nodes = [target_node] + [node_need]
    if pre_content is not None:
        node_need_pre_content = _render_template(pre_content, docname, lineno,
                                                 state)
        pre_container = nodes.container()
        pre_container += node_need_pre_content.children
        return_nodes = [pre_container] + return_nodes

    if post_content is not None:
        node_need_post_content = _render_template(post_content, docname,
                                                  lineno, state)
        post_container = nodes.container()
        post_container += node_need_post_content.children
        return_nodes = return_nodes + [post_container]

    return return_nodes
Exemplo n.º 6
0
def add_need(
    app,
    state,
    docname,
    lineno,
    need_type,
    title,
    id=None,
    content="",
    status=None,
    tags=None,
    links_string=None,
    hide=False,
    hide_tags=False,
    hide_status=False,
    collapse=None,
    style=None,
    layout=None,
    template=None,
    pre_template=None,
    post_template=None,
    is_external=False,
    external_url=None,
    external_css="external_link",
    **kwargs,
):
    """
    Creates a new need and returns its node.

    ``add_need`` allows to create needs programmatically and use its returned node to be integrated in any
    docutils based structure.

    ``kwags`` can contain options defined in ``needs_extra_options`` and ``needs_extra_links``.
    If an entry is found in ``kwags``, which *is not* specified in the configuration or registered e.g. via
    ``add_extra_option``, an exception is raised.

    If ``is_external`` is set to ``True``, no node will be created.
    Instead the need is referencing an external url.
    Used mostly for :ref:`needs_external_needs` to integrate and reference needs from external documentation.

    **Usage**:

    Normally needs get created during handling of a specialised directive.
    So this pseudo-code shows how to use ``add_need`` inside such a directive.

    .. code-block:: python

        from docutils.parsers.rst import Directive
        from sphinxcontrib.needs.api import add_need

        class MyDirective(Directive)
            # configs and init routine

            def run():
                main_section = []

                docname = self.state.document.settings.env.docname

                # All needed sphinx-internal information we can take from our current directive class.
                # e..g app, state, lineno
                main_section += add_need(self.env.app, self.state, docname, self.lineno,
                                         need_type="req", title="my title", id="ID_001"
                                         content=self.content)

                # Feel free to add custom stuff to main_section like sections, text, ...

                return main_section

    :param app: Sphinx application object.
    :param state: Current state object.
    :param docname: documentation name.
    :param lineno: line number.
    :param need_type: Name of the need type to create.
    :param title: String as title.
    :param id: ID as string. If not given, a id will get generated.
    :param content: Content as single string.
    :param status: Status as string.
    :param tags: Tags as single string.
    :param links_string: Links as single string.
    :param hide: boolean value.
    :param hide_tags: boolean value. (Not used with Sphinx-Needs >0.5.0)
    :param hide_status: boolean value. (Not used with Sphinx-Needs >0.5.0)
    :param collapse: boolean value.
    :param style: String value of class attribute of node.
    :param layout: String value of layout definition to use
    :param template: Template name to use for the content of this need
    :param pre_template: Template name to use for content added before need
    :param post_template: Template name to use for the content added after need
    :param is_external: Is true, no node is created and need is referencing external url
    :param external_url: URL as string, which is used as target if ``is_external`` is ``True``
    :param external_css: CSS class name as string, which is set for the <a> tag.

    :return: node
    """
    #############################################################################################
    # Get environment
    #############################################################################################
    env = app.env
    types = env.app.config.needs_types
    type_name = ""
    type_prefix = ""
    type_color = ""
    type_style = ""
    found = False
    for ntype in types:
        if ntype["directive"] == need_type:
            type_name = ntype["title"]
            type_prefix = ntype["prefix"]
            type_color = ntype[
                "color"] or "#000000"  # if no color set up user in config
            type_style = ntype[
                "style"] or "node"  # if no style set up user in config
            found = True
            break
    if not found:
        # This should never happen. But it may happen, if Sphinx is called multiples times
        # inside one ongoing python process.
        # In this case the configuration from a prior sphinx run may be active, which has registered a directive,
        # which is reused inside a current document, but no type was defined for the current run...
        # Yeah, this really has happened...
        return [nodes.Text("", "")]

    # Get the id or generate a random string/hash string, which is hopefully unique
    # 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 env.app.config.needs_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))

    if id is None:
        need_id = make_hashed_id(app, need_type, title, content)
    else:
        need_id = id

    if env.app.config.needs_id_regex and not re.match(
            env.app.config.needs_id_regex, need_id):
        raise NeedsInvalidException(
            "Given ID '{id}' does not match configured regex '{regex}'".format(
                id=need_id, regex=env.app.config.needs_id_regex))

    # Calculate target id, to be able to set a link back
    if is_external:
        target_node = None
        external_url = external_url
    else:
        target_node = nodes.target("", "", ids=[need_id], refid=need_id)
        external_url = None

    # Handle status
    # Check if status is in needs_statuses. If not raise an error.
    if env.app.config.needs_statuses and status not in [
            stat["name"] for stat in env.app.config.needs_statuses
    ]:
        raise NeedsStatusNotAllowed(
            f"Status {status} of need id {need_id} is not allowed "
            "by config value 'needs_statuses'.")

    if tags is None:
        tags = []
    if len(tags) > 0:

        # tags should be a string, but it can also be already a list,which can be used.
        if isinstance(tags, str):
            tags = [tag.strip() for tag in re.split(";|,", tags)]
        new_tags = []  # Shall contain only valid tags
        for i in range(len(tags)):
            if len(tags[i]) == 0 or tags[i].isspace():
                logger.warning(
                    f"Scruffy tag definition found in need {need_id}. "
                    "Defined tag contains spaces only.")
            else:
                new_tags.append(tags[i])

        tags = new_tags
        # Check if tag is in needs_tags. If not raise an error.
        if env.app.config.needs_tags:
            for tag in tags:
                if tag not in [
                        tag["name"] for tag in env.app.config.needs_tags
                ]:
                    raise NeedsTagNotAllowed(
                        f"Tag {tag} of need id {need_id} is not allowed "
                        "by config value 'needs_tags'.")
        # This may have cut also dynamic function strings, as they can contain , as well.
        # So let put them together again
        # ToDo: There may be a smart regex for the splitting. This would avoid this mess of code...
    tags = _fix_list_dyn_func(tags)

    #############################################################################################
    # Add need to global need list
    #############################################################################################
    # be sure, global var is available. If not, create it
    if not hasattr(env, "needs_all_needs"):
        env.needs_all_needs = {}

    if need_id in env.needs_all_needs:
        if id:
            raise NeedsDuplicatedId(
                f"A need with ID {need_id} already exists! "
                f"This is not allowed. Document {docname}[{lineno}] Title: {title}."
            )
        else:  # this is a generated ID
            raise NeedsDuplicatedId(
                "Needs could not generate a unique ID for a need with "
                "the title '{}' because another need had the same title. "
                "Either supply IDs for the requirements or ensure the "
                "titles are different.  NOTE: If title is being generated "
                "from the content, then ensure the first sentence of the "
                "requirements are different.".format(" ".join(title)))

    # Trim title if it is too long
    max_length = env.app.config.needs_max_title_length
    if max_length == -1 or len(title) <= max_length:
        trimmed_title = title
    elif max_length <= 3:
        trimmed_title = title[:max_length]
    else:
        trimmed_title = title[:max_length - 3] + "..."

    # Add the need and all needed information
    needs_info = {
        "docname": docname,
        "lineno": lineno,
        "target_node": target_node,
        "external_url": external_url,
        "content_node": None,  # gets set after rst parsing
        "type": need_type,
        "type_name": type_name,
        "type_prefix": type_prefix,
        "type_color": type_color,
        "type_style": type_style,
        "status": status,
        "tags": tags,
        "id": need_id,
        "title": trimmed_title,
        "full_title": title,
        "content": content,
        "collapse": collapse,
        "style": style,
        "layout": layout,
        "template": template,
        "pre_template": pre_template,
        "post_template": post_template,
        "hide": hide,
        "parts": {},
        "is_part": False,
        "is_need": True,
        "parent_need": None,
        "is_external": is_external or False,
        "external_css": external_css or "external_link",
        "is_modified": False,  # needed by needextend
        "modifications": 0,  # needed by needextend
    }
    # needs_extra_options = env.config.needs_extra_options.keys()
    needs_extra_option_names = NEEDS_CONFIG.get("extra_options").keys()
    _merge_extra_options(needs_info, kwargs, needs_extra_option_names)

    needs_global_options = env.config.needs_global_options
    _merge_global_options(app, needs_info, needs_global_options)

    link_names = [x["option"] for x in env.config.needs_extra_links]
    for keyword in kwargs:
        if keyword not in needs_extra_option_names and keyword not in link_names:
            raise NeedsInvalidOption(
                "Unknown Option {}. "
                "Use needs_extra_options or needs_extra_links in conf.py"
                "to define this option.".format(keyword))

    # Merge links
    copy_links = []

    for link_type in env.config.needs_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
            links = []
        elif link_type["option"] in needs_global_options and (
                link_type["option"] not in kwargs
                or len(str(kwargs[link_type["option"]])) == 0):
            # If it is in global option, value got already set during prior handling of them
            links_string = needs_info[link_type["option"]]
            links = _read_in_links(links_string)
        else:
            # if it is set in kwargs, take this value and maybe override set value from global_options
            links_string = kwargs[link_type["option"]]
            links = _read_in_links(links_string)

        needs_info[link_type["option"]] = links
        needs_info["{}_back".format(link_type["option"])] = []

        if "copy" not in link_type:
            link_type["copy"] = False

        if link_type["copy"] and link_type["option"] != "links":
            copy_links += links  # Save extra links for main-links

    needs_info["links"] += copy_links  # Set copied links to main-links

    env.needs_all_needs[need_id] = needs_info

    # Template builds
    ##############################

    # template
    if needs_info["template"]:
        new_content = _prepare_template(app, needs_info, "template")
        # Overwrite current content
        content = new_content
        needs_info["content"] = new_content

    # pre_template
    if needs_info["pre_template"]:
        pre_content = _prepare_template(app, needs_info, "pre_template")
        needs_info["pre_content"] = pre_content
    else:
        pre_content = None

    # post_template
    if needs_info["post_template"]:
        post_content = _prepare_template(app, needs_info, "post_template")
        needs_info["post_content"] = post_content
    else:
        post_content = None

    if needs_info["is_external"]:
        return []

    if needs_info["hide"]:
        return [target_node]

    # Adding of basic Need node.
    ############################
    # Title and meta data information gets added alter during event handling via process_need_nodes()
    # We just add a basic need node and render the rst-based content, because this can not be done later.
    # style_classes = ['need', type_name, 'need-{}'.format(type_name.lower())]  # Used < 0.4.4
    style_classes = ["need", f"need-{need_type.lower()}"]
    if style:
        style_classes.append(style)

    node_need = Need("", classes=style_classes, ids=[need_id], refid=need_id)

    # Render rst-based content and add it to the need-node

    node_need_content = _render_template(content, docname, lineno, state)
    need_parts = find_parts(node_need_content)
    update_need_with_parts(env, needs_info, need_parts)

    node_need += node_need_content.children

    needs_info["content_node"] = node_need

    return_nodes = [target_node] + [node_need]
    if pre_content:
        node_need_pre_content = _render_template(pre_content, docname, lineno,
                                                 state)
        pre_container = nodes.container()
        pre_container += node_need_pre_content.children
        return_nodes = node_need_pre_content.children + return_nodes

    if post_content:
        node_need_post_content = _render_template(post_content, docname,
                                                  lineno, state)
        post_container = nodes.container()
        post_container += node_need_post_content.children
        return_nodes = return_nodes + node_need_post_content.children

    return return_nodes