Exemple #1
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
    def run(self):
        #############################################################################################
        # Get environment
        #############################################################################################
        env = self.env
        types = env.app.config.needs_types
        type_name = ""
        type_prefix = ""
        type_color = ""
        type_style = ""
        found = False
        for type in types:
            if type["directive"] == self.name:
                type_name = type["title"]
                type_prefix = type["prefix"]
                type_color = type["color"]
                type_style = type["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" not in self.options.keys(
        ) 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")

        id = self.options.get(
            "id", self.make_hashed_id(type_prefix, env.config.needs_id_length))

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

        # Calculate target id, to be able to set a link back
        target_node = nodes.target('', '', ids=[id])

        collapse = str(self.options.get("collapse", ""))
        if isinstance(collapse, str) and len(collapse) > 0:
            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")
        else:
            collapse = getattr(env.app.config, "needs_collapse_details", True)

        hide = True if "hide" in self.options.keys() else False
        hide_tags = True if "hide_tags" in self.options.keys() else False
        hide_status = True if "hide_status" in self.options.keys() else False
        content = "\n".join(self.content)

        # Handle status
        status = self.options.get("status", None)
        # 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, id))

        tags = self.options.get("tags", [])
        if len(tags) > 0:
            # Not working regex.
            # test = re.match(r'^(?: *(\[\[.*?\]\]|[^,; ]+) *(?:,|;|$)?)+', tags)

            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(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, 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)

        # Get links
        links_string = self.options.get("links", [])
        links = []
        if len(links_string) > 0:
            # links = [link.strip() for link in re.split(";|,", links) if not link.isspace()]
            for link in re.split(";|,", links_string):
                if not link.isspace():
                    links.append(link.strip())
                else:
                    logger.warning(
                        'Grubby link definition found in need {}. '
                        'Defined link contains spaces only.'.format(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...
        links = _fix_list_dyn_func(links)

        #############################################################################################
        # 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 id in env.needs_all_needs.keys():
            if 'id' in self.options:
                raise NeedsDuplicatedId(
                    "A need with ID {} already exists! "
                    "This is not allowed. Document {}[{}]".format(
                        id, self.docname, self.lineno))
            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(
                        self.full_title)))

        # Add the need and all needed information
        needs_info = {
            'docname': self.docname,
            'lineno': self.lineno,
            'links_back': set(),
            'target_node': target_node,
            'type': self.name,
            'type_name': type_name,
            'type_prefix': type_prefix,
            'type_color': type_color,
            'type_style': type_style,
            'status': status,
            'tags': tags,
            'id': id,
            'links': links,
            'title': self.trimmed_title,
            'full_title': self.full_title,
            'content': content,
            'collapse': collapse,
            'hide': hide,
            'hide_tags': hide_tags,
            'hide_status': hide_status,
            'parts': {},
            'is_part': False,
            'is_need': True
        }
        self.merge_extra_options(needs_info)
        self.merge_global_options(needs_info)
        env.needs_all_needs[id] = needs_info

        if 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.
        node_need = Need(
            '',
            classes=['need', self.name, 'need-{}'.format(type_name.lower())])

        # Render rst-based content and add it to the need-node
        rst = ViewList()
        for line in self.content:
            rst.append(line, self.docname, self.lineno)
        node_need_content = nodes.Element()
        node_need_content.document = self.state.document
        nested_parse_with_titles(self.state, rst, node_need_content)

        need_parts = find_parts(node_need_content)
        update_need_with_parts(env, needs_info, need_parts)

        node_need += node_need_content.children

        return [target_node] + [node_need]
Exemple #3
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