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] = ""
def get_message_needs(sender, link_types, all_needs_dict, tracked_receivers=None, filter=None): msg_needs = [] if tracked_receivers is None: tracked_receivers = [] for link_type in link_types: msg_needs += [all_needs_dict[x] for x in sender[link_type]] messages = {} p_string = '' c_string = '' for msg_need in msg_needs: messages[msg_need['id']] = { 'id': msg_need['id'], 'title': msg_need['title'], 'receivers': {} } if sender['id'] not in tracked_receivers: p_string += 'participant "{}" as {}\n'.format( sender["title"], sender["id"]) tracked_receivers.append(sender['id']) for link_type in link_types: receiver_ids = msg_need[link_type] for rec_id in receiver_ids: if filter is not None: from sphinxcontrib.needs.filter_common import filter_single_need if not filter_single_need(all_needs_dict[rec_id], filter, needs=all_needs_dict.values()): continue rec_data = { 'id': rec_id, 'title': all_needs_dict[rec_id]['title'], 'messages': [] } c_string += '{} -> {}: {}\n'.format(sender["id"], rec_data["id"], msg_need["title"]) if rec_id not in tracked_receivers: rec_messages, p_string_new, c_string_new = get_message_needs( all_needs_dict[rec_id], link_types, all_needs_dict, tracked_receivers, filter=filter) p_string += p_string_new c_string += c_string_new rec_data['messages'] = rec_messages messages[msg_need['id']]['receivers'][rec_id] = rec_data return messages, p_string, c_string
def process_needgantt(app, doctree, fromdocname): # Replace all needgantt nodes with a list of the collected needs. env = app.builder.env link_types = env.config.needs_extra_links allowed_link_types_options = [ link.upper() for link in env.config.needs_flow_link_types ] # NEEDGANTT for node in doctree.traverse(Needgantt): if not app.config.needs_include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. # However, I do not understand, why losing an attribute is such a big deal, so we delete everything # before docutils claims about it. for att in ('ids', 'names', 'classes', 'dupnames'): node[att] = [] node.replace_self([]) continue id = node.attributes["ids"][0] current_needgantt = env.need_all_needgantts[id] all_needs_dict = env.needs_all_needs content = [] try: if "sphinxcontrib.plantuml" not in app.config.extensions: raise ImportError from sphinxcontrib.plantuml import plantuml except ImportError: no_plantuml(node) continue plantuml_block_text = ".. plantuml::\n" \ "\n" \ " @startuml" \ " @enduml" puml_node = plantuml(plantuml_block_text, **dict()) puml_node["uml"] = "@startuml\n" puml_connections = "" # Adding config config = current_needgantt['config'] puml_node["uml"] += add_config(config) all_needs = list(all_needs_dict.values()) found_needs = procces_filters(all_needs, current_needgantt) # Scale/timeline handling if current_needgantt['timeline'] is not None and current_needgantt[ 'timeline'] != '': puml_node["uml"] += 'printscale {}\n'.format( current_needgantt["timeline"]) # Project start date handling start_date_string = current_needgantt['start_date'] start_date_plantuml = None if start_date_string is not None and start_date_string != '': try: start_date = datetime.strptime(start_date_string, '%Y-%m-%d') # start_date = datetime.fromisoformat(start_date_string) # > py3.7 only except Exception: raise NeedGanttException( 'start_date "{}"for needgantt is invalid. ' 'File: {}:current_needgantt["lineno"]'.format( start_date_string, current_needgantt["docname"])) month = MONTH_NAMES[int(start_date.strftime("%-m"))] start_date_plantuml = start_date.strftime( "%dth of {} %Y".format(month)) if start_date_plantuml is not None: puml_node["uml"] += 'Project starts the {}\n'.format( start_date_plantuml) # Element handling puml_node["uml"] += '\n\' Elements definition \n\n' el_link_string = '' el_completion_string = '' el_color_string = '' for need in found_needs: complete = None if current_needgantt[ 'milestone_filter'] is None or current_needgantt[ 'milestone_filter'] == '': is_milestone = False else: is_milestone = filter_single_need( need, current_needgantt['milestone_filter']) if current_needgantt['milestone_filter'] is None or current_needgantt['milestone_filter'] == '' or \ not is_milestone: # Normal gantt element handling duration_option = current_needgantt['duration_option'] duration = need[duration_option] complete_option = current_needgantt['completion_option'] complete = need[complete_option] if duration is None or duration == '' or not duration.isdigit( ): logger.warning( 'Duration not set or invalid for needgantt chart. ' 'Need: {}. Duration: {}'.format(need["id"], duration)) duration = 1 gantt_element = '[{}] as [{}] lasts {} days\n'.format( need["title"], need["id"], duration) else: gantt_element = '[{}] as [{}] lasts 0 days\n'.format( need["title"], need["id"]) el_link_string += '[{}] links to [[{}]]\n'.format( need["title"], calculate_link(app, need)) if complete is not None and complete != '': complete = complete.replace('%', '') el_completion_string += '[{}] is {}% completed\n'.format( need["title"], complete) el_color_string += '[{}] is colored in {}\n'.format( need["title"], need["type_color"]) puml_node["uml"] += gantt_element puml_node["uml"] += '\n\' Element links definition \n\n' puml_node[ "uml"] += '\n\' Deactivated, as currently supported by plantuml beta only' # ToDo: Activate if linking is working with default plantuml # puml_node["uml"] += el_link_string + '\n' puml_node["uml"] += '\n\' Element completion definition \n\n' puml_node["uml"] += el_completion_string + '\n' puml_node["uml"] += '\n\' Element color definition \n\n' if current_needgantt['no_color']: puml_node["uml"] += '\' Color support deactivated via flag' else: puml_node["uml"] += el_color_string + '\n' # Constrain handling puml_node["uml"] += '\n\' Constraints definition \n\n' puml_node["uml"] += '\n\' Constraints definition \n\n' for need in found_needs: if current_needgantt[ 'milestone_filter'] is None or current_needgantt[ 'milestone_filter'] == '': is_milestone = False else: is_milestone = filter_single_need( need, current_needgantt['milestone_filter']) constrain_types = [ 'starts_with_links', 'starts_after_links', 'ends_with_links' ] for con_type in constrain_types: if is_milestone: keyword = 'happens' elif con_type in ['starts_with_links', 'starts_after_links']: keyword = 'starts' else: keyword = 'ends' if con_type in ['starts_after_links', 'ends_with_links']: start_end_sync = 'end' else: start_end_sync = 'start' for link_type in current_needgantt[con_type]: start_with_links = need[link_type] for start_with_link in start_with_links: start_need = all_needs_dict[start_with_link] gantt_constraint = '[{}] {} at [{}]\'s ' \ '{}\n'.format(need["id"], keyword, start_need["id"], start_end_sync) puml_node["uml"] += gantt_constraint # Create a legend if current_needgantt["show_legend"]: puml_node["uml"] += '\n\n\' Legend definition \n\n' puml_node["uml"] += "legend\n" puml_node["uml"] += "|= Color |= Type |\n" for need in app.config.needs_types: puml_node[ "uml"] += "|<back:{color}> {color} </back>| {name} |\n".format( color=need["color"], name=need["title"]) puml_node["uml"] += "endlegend\n" puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needgantt["docname"]) puml_node["filename"] = os.path.split( current_needgantt["docname"])[1] # Needed for plantuml >= 0.9 scale = int(current_needgantt['scale']) # if scale != 100: puml_node['scale'] = scale puml_node = nodes.figure('', puml_node) if current_needgantt['align'] is not None and len( current_needgantt['align']) != '': puml_node['align'] = current_needgantt['align'] else: puml_node['align'] = 'center' if current_needgantt['caption'] is not None and len( current_needgantt['caption']) != '': # Make the caption to a link to the original file. try: if "SVG" in app.config.plantuml_output_format.upper(): file_ext = 'svg' else: file_ext = 'png' except Exception: file_ext = 'png' gen_flow_link = generate_name(app, puml_node.children[0], file_ext) current_file_parts = fromdocname.split('/') subfolder_amount = len(current_file_parts) - 1 img_locaton = '../' * subfolder_amount + '_images/' + gen_flow_link[ 0].split('/')[-1] flow_ref = nodes.reference('t', current_needgantt['caption'], refuri=img_locaton) puml_node += nodes.caption('', '', flow_ref) content.append(puml_node) if len(content) == 0: nothing_found = "No needs passed the filters" para = nodes.paragraph() nothing_found_node = nodes.Text(nothing_found, nothing_found) para += nothing_found_node content.append(para) if current_needgantt["show_filters"]: content.append(get_filter_para(current_needgantt)) if current_needgantt['debug']: content += get_debug_containter(puml_node) puml_node['class'] = ['needgantt'] node.replace_self(content)
def links_from_content(app, need, needs, need_id=None, filter=None): """ Extracts links from content of a need. All need-links set by using ``:need:`NEED_ID``` get extracted. Same links are only added once. Example: .. req:: Requirement 1 :id: CON_REQ_1 .. req:: Requirement 2 :id: CON_REQ_2 .. spec:: Test spec :id: CON_SPEC_1 :links: [[links_from_content()]] This specification cares about the realisation of: * :need:`CON_REQ_1` * :need:`My need <CON_REQ_2>` .. spec:: Test spec 2 :id: CON_SPEC_2 :links: [[links_from_content('CON_SPEC_1')]] Links retrieved from content of :need:`CON_SPEC_1` Used code of **CON_SPEC_1**:: .. spec:: Test spec :id: CON_SPEC_1 :links: [[links_from_content()]] This specification cares about the realisation of: * :need:`CON_REQ_1` * :need:`CON_REQ_2` .. spec:: Test spec 2 :id: CON_SPEC_2 :links: [[links_from_content('CON_SPEC_1')]] Links retrieved from content of :need:`CON_SPEC_1` :param need_id: ID of need, which provides the content. If not set, current need is used. :param filter: :ref:`filter_string`, which a found need-link must pass. :return: List of linked need-ids in content """ if need_id: source_need = needs[need_id] else: source_need = need links = re.findall(r":need:`(\w+)`|:need:`.+\<(.+)\>`", source_need["content"]) raw_links = [] for link in links: if link[0] and link[0] not in raw_links: raw_links.append(link[0]) elif link[1] and link[0] not in raw_links: raw_links.append(link[1]) if filter: filtered_links = [] for link in raw_links: if link not in filtered_links and filter_single_need( app, needs[link], filter): filtered_links.append(link) return filtered_links return raw_links
def check_linked_values(app, need, needs, result, search_option, search_value, filter_string=None, one_hit=False): """ Returns a specific value, if for all linked needs a given option has a given value. The linked needs can be filtered by using the ``filter`` option. If ``one_hit`` is set to True, only one linked need must have a positive match for the searched value. **Examples** **Needs used as input data** .. code-block:: jinja .. req:: Input A :id: clv_A :status: in progress .. req:: Input B :id: clv_B :status: in progress .. spec:: Input C :id: clv_C :status: closed .. req:: Input A :id: clv_A :status: in progress :collapse: False .. req:: Input B :id: clv_B :status: in progress :collapse: False .. spec:: Input C :id: clv_C :status: closed :collapse: False **Example 1: Positive check** Status gets set to *progress*. .. code-block:: jinja .. spec:: result 1: Positive check :links: clv_A, clv_B :status: [[check_linked_values('progress', 'status', 'in progress' )]] .. spec:: result 1: Positive check :id: clv_1 :links: clv_A, clv_B :status: [[check_linked_values('progress', 'status', 'in progress' )]] :collapse: False **Example 2: Negative check** Status gets not set to *progress*, because status of linked need *clv_C* does not match *"in progress"*. .. code-block:: jinja .. spec:: result 2: Negative check :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress' )]] .. spec:: result 2: Negative check :id: clv_2 :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress' )]] :collapse: False **Example 3: Positive check thanks of used filter** status gets set to *progress*, because linked need *clv_C* is not part of the filter. .. code-block:: jinja .. spec:: result 3: Positive check thanks of used filter :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', 'type == "req" ' )]] .. spec:: result 3: Positive check thanks of used filter :id: clv_3 :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', 'type == "req" ' )]] :collapse: False **Example 4: Positive check thanks of one_hit option** Even *clv_C* has not the searched status, status gets anyway set to *progress*. That's because ``one_hit`` is used so that only one linked need must have the searched value. .. code-block:: jinja .. spec:: result 4: Positive check thanks of one_hit option :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] .. spec:: result 4: Positive check thanks of one_hit option :id: clv_4 :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] :collapse: False **Result 5: Two checks and a joint status** Two checks are performed and both are positive. So their results get joined. .. code-block:: jinja .. spec:: result 5: Two checks and a joint status :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] [[check_linked_values('closed', 'status', 'closed', one_hit=True )]] .. spec:: result 5: Two checks and a joint status :id: clv_5 :links: clv_A, clv_B, clv_C :status: [[check_linked_values('progress', 'status', 'in progress', one_hit=True )]] [[check_linked_values('closed', 'status', 'closed', one_hit=True )]] :collapse: False :param result: value, which gets returned if all linked needs have parsed the checks :param search_option: option name, which is used n linked needs for the search :param search_value: value, which an option of a linked need must match :param filter_string: Checks are only performed on linked needs, which pass the defined filter :param one_hit: If True, only one linked need must have a positive check :return: result, if all checks are positive """ links = need["links"] if not isinstance(search_value, list): search_value = [search_value] for link in links: if filter_string: try: if not filter_single_need(app, needs[link], filter_string): continue except Exception as e: logger.warning( f"CheckLinkedValues: Filter {filter_string} not valid: Error: {e}" ) if not one_hit and not needs[link][search_option] in search_value: return None elif one_hit and needs[link][search_option] in search_value: return result return result
def calc_sum(app, need, needs, option, filter=None, links_only=False): """ Sums the values of a given option in filtered needs up to single number. Useful e.g. for calculating the amount of needed hours for implementation of all linked specification needs. **Input data** .. spec:: Do this :id: sum_input_1 :hours: 7 :collapse: False .. spec:: Do that :id: sum_input_2 :hours: 15 :collapse: False .. spec:: Do too much :id: sum_input_3 :hours: 110 :collapse: False **Example 2** .. code-block:: jinja .. req:: Result 1 :amount: [[calc_sum("hours")]] .. req:: Result 1 :amount: [[calc_sum("hours")]] :collapse: False **Example 2** .. code-block:: jinja .. req:: Result 2 :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10")]] .. req:: Result 2 :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10")]] :collapse: False **Example 3** .. code-block:: jinja .. req:: Result 3 :links: sum_input_1; sum_input_3 :amount: [[calc_sum("hours", links_only="True")]] .. req:: Result 3 :links: sum_input_1; sum_input_3 :amount: [[calc_sum("hours", links_only="True")]] :collapse: False **Example 4** .. code-block:: jinja .. req:: Result 4 :links: sum_input_1; sum_input_3 :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10", "True")]] .. req:: Result 4 :links: sum_input_1; sum_input_3 :amount: [[calc_sum("hours", "hours.isdigit() and float(hours) > 10", "True")]] :collapse: False :param option: Options, from which the numbers shall be taken :param filter: Filter string, which all needs must passed to get their value added. :param links_only: If "True", only linked needs are taken into account. :return: A float number """ if links_only: check_needs = [] for link in need["links"]: check_needs.append(needs[link]) else: check_needs = needs.values() calculated_sum = 0 for check_need in check_needs: if filter: try: if not filter_single_need(app, check_need, filter): continue except ValueError: pass except NeedsInvalidFilter as ex: logger.warning(f"Given filter is not valid. Error: {ex}") try: calculated_sum += float(check_need[option]) except ValueError: pass return calculated_sum
def process_needgantt(app, doctree, fromdocname): # Replace all needgantt nodes with a list of the collected needs. env = app.builder.env # link_types = env.config.needs_extra_links # allowed_link_types_options = [link.upper() for link in env.config.needs_flow_link_types] # NEEDGANTT for node in doctree.traverse(Needgantt): if not app.config.needs_include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. # However, I do not understand, why losing an attribute is such a big deal, so we delete everything # before docutils claims about it. for att in ("ids", "names", "classes", "dupnames"): node[att] = [] node.replace_self([]) continue id = node.attributes["ids"][0] current_needgantt = env.need_all_needgantts[id] all_needs_dict = env.needs_all_needs content = [] try: if "sphinxcontrib.plantuml" not in app.config.extensions: raise ImportError from sphinxcontrib.plantuml import plantuml except ImportError: no_plantuml(node) continue plantuml_block_text = ".. plantuml::\n" "\n" " @startuml" " @enduml" puml_node = plantuml(plantuml_block_text) puml_node["uml"] = "@startuml\n" # Adding config config = current_needgantt["config"] puml_node["uml"] += add_config(config) all_needs = list(all_needs_dict.values()) found_needs = process_filters(app, all_needs, current_needgantt) # Scale/timeline handling if current_needgantt["timeline"]: puml_node["uml"] += "printscale {}\n".format( current_needgantt["timeline"]) # Project start date handling start_date_string = current_needgantt["start_date"] start_date_plantuml = None if start_date_string: try: start_date = datetime.strptime(start_date_string, "%Y-%m-%d") # start_date = datetime.fromisoformat(start_date_string) # > py3.7 only except Exception: raise NeedGanttException( 'start_date "{}"for needgantt is invalid. ' 'File: {}:current_needgantt["lineno"]'.format( start_date_string, current_needgantt["docname"])) month = MONTH_NAMES[int(start_date.strftime("%m"))] start_date_plantuml = start_date.strftime(f"%dth of {month} %Y") if start_date_plantuml: puml_node["uml"] += f"Project starts the {start_date_plantuml}\n" # Element handling puml_node["uml"] += "\n' Elements definition \n\n" el_link_string = "" el_completion_string = "" el_color_string = "" for need in found_needs: complete = None if current_needgantt["milestone_filter"]: is_milestone = filter_single_need( app, need, current_needgantt["milestone_filter"]) else: is_milestone = False if current_needgantt["milestone_filter"] and is_milestone: gantt_element = "[{}] as [{}] lasts 0 days\n".format( need["title"], need["id"]) else: # Normal gantt element handling duration_option = current_needgantt["duration_option"] duration = need[duration_option] complete_option = current_needgantt["completion_option"] complete = need[complete_option] if not (duration and duration.isdigit()): logger.warning( "Duration not set or invalid for needgantt chart. " "Need: {}. Duration: {}".format(need["id"], duration)) duration = 1 gantt_element = "[{}] as [{}] lasts {} days\n".format( need["title"], need["id"], duration) el_link_string += "[{}] links to [[{}]]\n".format( need["title"], calculate_link(app, need, fromdocname)) if complete: complete = complete.replace("%", "") el_completion_string += "[{}] is {}% completed\n".format( need["title"], complete) el_color_string += "[{}] is colored in {}\n".format( need["title"], need["type_color"]) puml_node["uml"] += gantt_element puml_node["uml"] += "\n' Element links definition \n\n" puml_node[ "uml"] += "\n' Deactivated, as currently supported by plantuml beta only" # ToDo: Activate if linking is working with default plantuml # puml_node["uml"] += el_link_string + '\n' puml_node["uml"] += "\n' Element completion definition \n\n" puml_node["uml"] += el_completion_string + "\n" puml_node["uml"] += "\n' Element color definition \n\n" if current_needgantt["no_color"]: puml_node["uml"] += "' Color support deactivated via flag" else: puml_node["uml"] += el_color_string + "\n" # Constrain handling puml_node["uml"] += "\n' Constraints definition \n\n" puml_node["uml"] += "\n' Constraints definition \n\n" for need in found_needs: if current_needgantt["milestone_filter"]: is_milestone = filter_single_need( app, need, current_needgantt["milestone_filter"]) else: is_milestone = False constrain_types = [ "starts_with_links", "starts_after_links", "ends_with_links" ] for con_type in constrain_types: if is_milestone: keyword = "happens" elif con_type in ["starts_with_links", "starts_after_links"]: keyword = "starts" else: keyword = "ends" if con_type in ["starts_after_links", "ends_with_links"]: start_end_sync = "end" else: start_end_sync = "start" for link_type in current_needgantt[con_type]: start_with_links = need[link_type] for start_with_link in start_with_links: start_need = all_needs_dict[start_with_link] gantt_constraint = "[{}] {} at [{}]'s " "{}\n".format( need["id"], keyword, start_need["id"], start_end_sync) puml_node["uml"] += gantt_constraint # Create a legend if current_needgantt["show_legend"]: puml_node["uml"] += create_legend(app.config.needs_types) puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needgantt["docname"]) puml_node["filename"] = os.path.split( current_needgantt["docname"])[1] # Needed for plantuml >= 0.9 scale = int(current_needgantt["scale"]) # if scale != 100: puml_node["scale"] = scale puml_node = nodes.figure("", puml_node) puml_node["align"] = current_needgantt["align"] or "center" if current_needgantt["caption"]: # Make the caption to a link to the original file. try: if "SVG" in app.config.plantuml_output_format.upper(): file_ext = "svg" else: file_ext = "png" except Exception: file_ext = "png" gen_flow_link = generate_name(app, puml_node.children[0], file_ext) current_file_parts = fromdocname.split("/") subfolder_amount = len(current_file_parts) - 1 img_location = "../" * subfolder_amount + "_images/" + gen_flow_link[ 0].split("/")[-1] flow_ref = nodes.reference("t", current_needgantt["caption"], refuri=img_location) puml_node += nodes.caption("", "", flow_ref) # Add lineno to node puml_node.line = current_needgantt["lineno"] content.append(puml_node) if len(content) == 0: nothing_found = "No needs passed the filters" para = nodes.paragraph() nothing_found_node = nodes.Text(nothing_found, nothing_found) para += nothing_found_node content.append(para) if current_needgantt["show_filters"]: content.append(get_filter_para(current_needgantt)) if current_needgantt["debug"]: content += get_debug_container(puml_node) puml_node["class"] = ["needgantt"] node.replace_self(content)
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", "") hide = True if "hide" in self.options.keys() else False 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): need_import_path = os.path.join(env.app.confdir, need_import_path) if not os.path.exists(need_import_path): raise ReferenceError("Could not load needs import file {0}".format(need_import_path)) with open(need_import_path, "r") 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, six.string_types): raise KeyError except KeyError: raise CorruptedNeedsFile("Key 'current_version' missing or corrupted in {0}".format(need_import_path)) if version not in needs_import_list["versions"].keys(): raise VersionNotFound("Version {0} not found in needs import file {1}".format(version, 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 = { "tags": need["tags"], "status": need["status"], "type": need["type"], "id": need["id"], "title": need["title"], "links": need["links"], # "search": re.search, # 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 "content": need["description"], "description": need["description"] } try: if filter_single_need(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 key, need in needs_list.items(): for id in needs_ids: # Manipulate links if id in need["links"]: for n, link in enumerate(need["links"]): if id == link: need["links"][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 key, need in needs_list.items(): need["tags"] = need["tags"] + tags template_location = os.path.join(os.path.dirname(os.path.abspath(__file__)), "needimport_template.rst") with open(template_location, "r") as template_file: template_content = template_file.read() template = Template(template_content) content = template.render(needs_list=needs_list, hide=hide, id_prefix=id_prefix) self.state_machine.insert_input(content.split('\n'), self.state_machine.document.attributes['source']) return []
def process_needflow(app, doctree, fromdocname): # Replace all needflow nodes with a list of the collected needs. # Augment each need with a backlink to the original location. env = app.builder.env link_types = env.config.needs_extra_links allowed_link_types_options = [link.upper() for link in env.config.needs_flow_link_types] # NEEDFLOW for node in doctree.traverse(Needflow): if not app.config.needs_include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. # However, I do not understand, why losing an attribute is such a big deal, so we delete everything # before docutils claims about it. for att in ("ids", "names", "classes", "dupnames"): node[att] = [] node.replace_self([]) continue id = node.attributes["ids"][0] current_needflow = env.need_all_needflows[id] all_needs = env.needs_all_needs option_link_types = [link.upper() for link in current_needflow["link_types"]] for lt in option_link_types: if lt not in [link["option"].upper() for link in link_types]: logger.warning( "Unknown link type {link_type} in needflow {flow}. Allowed values: {link_types}".format( link_type=lt, flow=current_needflow["target_node"], link_types=",".join(link_types) ) ) content = [] try: if "sphinxcontrib.plantuml" not in app.config.extensions: raise ImportError from sphinxcontrib.plantuml import plantuml except ImportError: content = nodes.error() para = nodes.paragraph() text = nodes.Text("PlantUML is not available!", "PlantUML is not available!") para += text content.append(para) node.replace_self(content) continue plantuml_block_text = ".. plantuml::\n" "\n" " @startuml" " @enduml" puml_node = plantuml(plantuml_block_text) puml_node["uml"] = "@startuml\n" puml_connections = "" # Adding config config = current_needflow["config"] if config and len(config) >= 3: # Remove all empty lines config = "\n".join([line.strip() for line in config.split("\n") if line.strip()]) puml_node["uml"] += "\n' Config\n\n" puml_node["uml"] += config puml_node["uml"] += "\n\n" all_needs = list(all_needs.values()) found_needs = process_filters(app, all_needs, current_needflow) processed_need_part_ids = [] puml_node["uml"] += "\n' Nodes definition \n\n" for need_info in found_needs: # Check if need_part was already handled during handling of parent need. # If this is the case, it is already part of puml-code and we do not need to create a node. if not (need_info["is_part"] and need_info["id_complete"] in processed_need_part_ids): # Check if we need to embed need_parts into parent need, because they are also part of search result. node_part_code = "" valid_need_parts = [x for x in found_needs if x["is_part"] and x["id_parent"] == need_info["id"]] for need_part in valid_need_parts: part_link = calculate_link(app, need_part, fromdocname) diagram_template = Template(env.config.needs_diagram_template) part_text = diagram_template.render(**need_part) part_colors = [] if need_part["type_color"]: # We set # later, as the user may not have given a color and the node must get highlighted part_colors.append(need_part["type_color"].replace("#", "")) if current_needflow["highlight"] and filter_single_need( app, need_part, current_needflow["highlight"], all_needs ): part_colors.append("line:FF0000") node_part_code += '{style} "{node_text}" as {id} [[{link}]] #{color}\n'.format( id=make_entity_name(need_part["id_complete"]), node_text=part_text, link=part_link, color=";".join(part_colors), style="rectangle", ) processed_need_part_ids.append(need_part["id_complete"]) link = calculate_link(app, need_info, fromdocname) diagram_template = Template(env.config.needs_diagram_template) node_text = diagram_template.render(**need_info) if need_info["is_part"]: need_id = need_info["id_complete"] else: need_id = need_info["id"] colors = [] if need_info["type_color"]: # We set # later, as the user may not have given a color and the node must get highlighted colors.append(need_info["type_color"].replace("#", "")) if current_needflow["highlight"] and filter_single_need( app, need_info, current_needflow["highlight"], all_needs ): colors.append("line:FF0000") # Only add subelements and their {...} container, if we really need them. # Otherwise plantuml may not set style correctly, if {..} is empty if node_part_code: node_part_code = f"{{\n {node_part_code} }}" style = need_info["type_style"] node_code = '{style} "{node_text}" as {id} [[{link}]] #{color} {need_parts}\n'.format( id=make_entity_name(need_id), node_text=node_text, link=link, color=";".join(colors), style=style, need_parts=node_part_code, ) puml_node["uml"] += node_code for link_type in link_types: # Skip link-type handling, if it is not part of a specified list of allowed link_types or # if not part of the overall configuration of needs_flow_link_types if (current_needflow["link_types"] and link_type["option"].upper() not in option_link_types) or ( not current_needflow["link_types"] and link_type["option"].upper() not in allowed_link_types_options ): continue for link in need_info[link_type["option"]]: # If source or target of link is a need_part, a specific style is needed if "." in link or "." in need_info["id_complete"]: final_link = link if current_needflow["show_link_names"] or env.config.needs_flow_show_links: desc = link_type["outgoing"] + "\\n" comment = f": {desc}" else: comment = "" if "style_part" in link_type and link_type["style_part"]: link_style = "[{style}]".format(style=link_type["style_part"]) else: link_style = "[dotted]" else: final_link = link if current_needflow["show_link_names"] or env.config.needs_flow_show_links: comment = ": {desc}".format(desc=link_type["outgoing"]) else: comment = "" if "style" in link_type and link_type["style"]: link_style = "[{style}]".format(style=link_type["style"]) else: link_style = "" # Do not create an links, if the link target is not part of the search result. if final_link not in [x["id"] for x in found_needs if x["is_need"]] and final_link not in [ x["id_complete"] for x in found_needs if x["is_part"] ]: continue if "style_start" in link_type and link_type["style_start"]: style_start = link_type["style_start"] else: style_start = "-" if "style_end" in link_type and link_type["style_end"]: style_end = link_type["style_end"] else: style_end = "->" puml_connections += "{id} {style_start}{link_style}{style_end} {link}{comment}\n".format( id=make_entity_name(need_info["id_complete"]), link=make_entity_name(final_link), comment=comment, link_style=link_style, style_start=style_start, style_end=style_end, ) puml_node["uml"] += "\n' Connection definition \n\n" puml_node["uml"] += puml_connections # Create a legend if current_needflow["show_legend"]: puml_node["uml"] += create_legend(app.config.needs_types) puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needflow["docname"]) puml_node["filename"] = os.path.split(current_needflow["docname"])[1] # Needed for plantuml >= 0.9 scale = int(current_needflow["scale"]) # if scale != 100: puml_node["scale"] = scale puml_node = nodes.figure("", puml_node) if current_needflow["align"]: puml_node["align"] = current_needflow["align"] else: puml_node["align"] = "center" if current_needflow["caption"]: # Make the caption to a link to the original file. try: if "SVG" in app.config.plantuml_output_format.upper(): file_ext = "svg" else: file_ext = "png" except Exception: file_ext = "png" gen_flow_link = generate_name(app, puml_node.children[0], file_ext) current_file_parts = fromdocname.split("/") subfolder_amount = len(current_file_parts) - 1 img_locaton = "../" * subfolder_amount + "_images/" + gen_flow_link[0].split("/")[-1] flow_ref = nodes.reference("t", current_needflow["caption"], refuri=img_locaton) puml_node += nodes.caption("", "", flow_ref) # Add lineno to node puml_node.line = current_needflow["lineno"] content.append(puml_node) if len(content) == 0: nothing_found = "No needs passed the filters" para = nodes.paragraph() nothing_found_node = nodes.Text(nothing_found, nothing_found) para += nothing_found_node content.append(para) if current_needflow["show_filters"]: para = nodes.paragraph() filter_text = "Used filter:" filter_text += ( " status(%s)" % " OR ".join(current_needflow["status"]) if len(current_needflow["status"]) > 0 else "" ) if len(current_needflow["status"]) > 0 and len(current_needflow["tags"]) > 0: filter_text += " AND " filter_text += ( " tags(%s)" % " OR ".join(current_needflow["tags"]) if len(current_needflow["tags"]) > 0 else "" ) if (len(current_needflow["status"]) > 0 or len(current_needflow["tags"]) > 0) and len( current_needflow["types"] ) > 0: filter_text += " AND " filter_text += ( " types(%s)" % " OR ".join(current_needflow["types"]) if len(current_needflow["types"]) > 0 else "" ) filter_node = nodes.emphasis(filter_text, filter_text) para += filter_node content.append(para) if current_needflow["debug"]: debug_container = nodes.container() if isinstance(puml_node, nodes.figure): data = puml_node.children[0]["uml"] else: data = puml_node["uml"] data = "\n".join([html.escape(line) for line in data.split("\n")]) debug_para = nodes.raw("", f"<pre>{data}</pre>", format="html") debug_container += debug_para content += debug_container node.replace_self(content)
def process_needflow(app, doctree, fromdocname): # Replace all needflow nodes with a list of the collected needs. # Augment each need with a backlink to the original location. env = app.builder.env link_types = env.config.needs_extra_links allowed_link_types_options = [ link.upper() for link in env.config.needs_flow_link_types ] # NEEDFLOW for node in doctree.traverse(Needflow): if not app.config.needs_include_needs: # Ok, this is really dirty. # If we replace a node, docutils checks, if it will not lose any attributes. # But this is here the case, because we are using the attribute "ids" of a node. # However, I do not understand, why losing an attribute is such a big deal, so we delete everything # before docutils claims about it. for att in ('ids', 'names', 'classes', 'dupnames'): node[att] = [] node.replace_self([]) continue id = node.attributes["ids"][0] current_needflow = env.need_all_needflows[id] all_needs = env.needs_all_needs option_link_types = [ link.upper() for link in current_needflow['link_types'] ] for lt in option_link_types: if lt not in [link['option'].upper() for link in link_types]: logger.warning( 'Unknown link type {link_type} in needflow {flow}. Allowed values: {link_types}' .format(link_type=lt, flow=current_needflow['target_node'], link_types=",".join(link_types))) content = [] try: if "sphinxcontrib.plantuml" not in app.config.extensions: raise ImportError from sphinxcontrib.plantuml import plantuml except ImportError: content = nodes.error() para = nodes.paragraph() text = nodes.Text("PlantUML is not available!", "PlantUML is not available!") para += text content.append(para) node.replace_self(content) continue plantuml_block_text = ".. plantuml::\n" \ "\n" \ " @startuml" \ " @enduml" puml_node = plantuml(plantuml_block_text, **dict()) puml_node["uml"] = "@startuml\n" puml_connections = "" # Adding config config = current_needflow['config'] if config is not None and len(config) >= 3: # Remove all empty lines config = '\n'.join([ line.strip() for line in config.split('\n') if line.strip() != '' ]) puml_node["uml"] += '\n\' Config\n\n' puml_node["uml"] += config puml_node["uml"] += '\n\n' all_needs = list(all_needs.values()) found_needs = procces_filters(all_needs, current_needflow) processed_need_part_ids = [] puml_node["uml"] += '\n\' Nodes definition \n\n' for need_info in found_needs: # Check if need_part was already handled during handling of parent need. # If this is the case, it is already part of puml-code and we do not need to create a node. if not (need_info['is_part'] and need_info['id_complete'] in processed_need_part_ids): # Check if we need to embed need_parts into parent need, because they are also part of search result. node_part_code = "" valid_need_parts = [ x for x in found_needs if x['is_part'] and x['id_parent'] == need_info['id'] ] for need_part in valid_need_parts: part_link = calculate_link(app, need_part) diagram_template = Template( env.config.needs_diagram_template) part_text = diagram_template.render(**need_part) part_colors = [] if need_part["type_color"] != '': # We set # later, as the user may not have given a color and the node must get highlighted part_colors.append(need_part["type_color"].replace( '#', '')) if current_needflow['highlight'] is not None and current_needflow['highlight'] != '' and \ filter_single_need(need_part, current_needflow['highlight'], all_needs): part_colors.append('line:FF0000') node_part_code += '{style} "{node_text}" as {id} [[{link}]] #{color}\n'.format( id=make_entity_name(need_part["id_complete"]), node_text=part_text, link=make_entity_name(part_link), color=';'.join(part_colors), style='rectangle') processed_need_part_ids.append(need_part['id_complete']) link = calculate_link(app, need_info) diagram_template = Template(env.config.needs_diagram_template) node_text = diagram_template.render(**need_info) if need_info['is_part']: need_id = need_info['id_complete'] else: need_id = need_info['id'] colors = [] if need_info["type_color"] != '': # We set # later, as the user may not have given a color and the node must get highlighted colors.append(need_info["type_color"].replace('#', '')) if current_needflow['highlight'] is not None and current_needflow['highlight'] != '' and \ filter_single_need(need_info, current_needflow['highlight'], all_needs): colors.append('line:FF0000') # Only add subelements and their {...} container, if we really need them. # Otherwise plantuml may not set style correctly, if {..} is empty if node_part_code != '': node_part_code = '{{\n {} }}'.format(node_part_code) style = need_info["type_style"] node_code = '{style} "{node_text}" as {id} [[{link}]] #{color} {need_parts}\n'.format( id=make_entity_name(need_id), node_text=node_text, # link=make_entity_name(link), color=';'.join(colors), link=link, color=';'.join(colors), style=style, need_parts=node_part_code) puml_node["uml"] += node_code for link_type in link_types: # Skip link-type handling, if it is not part of a specified list of allowed link_types or # if not part of the overall configuration of needs_flow_link_types if (current_needflow["link_types"] and link_type['option'].upper() not in option_link_types) or \ (not current_needflow["link_types"] and \ link_type['option'].upper() not in allowed_link_types_options): continue for link in need_info[link_type['option']]: # If source or target of link is a need_part, a specific style is needed if '.' in link or '.' in need_info["id_complete"]: final_link = link if current_needflow[ "show_link_names"] or env.config.needs_flow_show_links: desc = link_type['outgoing'] + '\\n' comment = ': {desc}'.format(desc=desc) else: comment = '' if "style_part" in link_type.keys() and link_type['style_part'] is not None and \ len(link_type['style_part']) > 0: link_style = '[{style}]'.format( style=link_type['style_part']) else: link_style = "[dotted]" else: final_link = link if current_needflow[ "show_link_names"] or env.config.needs_flow_show_links: comment = ': {desc}'.format( desc=link_type['outgoing']) else: comment = '' if "style" in link_type.keys() and link_type['style'] is not None and \ len(link_type['style']) > 0: link_style = '[{style}]'.format( style=link_type['style']) else: link_style = "" # Do not create an links, if the link target is not part of the search result. if final_link not in [x['id'] for x in found_needs if x['is_need']] and \ final_link not in [x['id_complete'] for x in found_needs if x['is_part']]: continue if 'style_start' in link_type.keys() and link_type['style_start'] is not None and \ len(link_type['style_start']) > 0: style_start = link_type['style_start'] else: style_start = '-' if 'style_end' in link_type.keys() and link_type['style_end'] is not None and \ len(link_type['style_end']) > 0: style_end = link_type['style_end'] else: style_end = '->' puml_connections += '{id} {style_start}{link_style}{style_end} {link}{comment}\n'.format( id=make_entity_name(need_info["id_complete"]), link=make_entity_name(final_link), comment=comment, link_style=link_style, style_start=style_start, style_end=style_end) puml_node["uml"] += '\n\' Connection definition \n\n' puml_node["uml"] += puml_connections # Create a legend if current_needflow["show_legend"]: puml_node["uml"] += '\n\n\' Legend definition \n\n' puml_node["uml"] += "legend\n" puml_node["uml"] += "|= Color |= Type |\n" for need in app.config.needs_types: puml_node[ "uml"] += "|<back:{color}> {color} </back>| {name} |\n".format( color=need["color"], name=need["title"]) puml_node["uml"] += "endlegend\n" puml_node["uml"] += "\n@enduml" puml_node["incdir"] = os.path.dirname(current_needflow["docname"]) puml_node["filename"] = os.path.split( current_needflow["docname"])[1] # Needed for plantuml >= 0.9 scale = int(current_needflow['scale']) # if scale != 100: puml_node['scale'] = scale puml_node = nodes.figure('', puml_node) if current_needflow['align'] is not None and len( current_needflow['align']) != '': puml_node['align'] = current_needflow['align'] else: puml_node['align'] = 'center' if current_needflow['caption'] is not None and len( current_needflow['caption']) != '': # Make the caption to a link to the original file. try: if "SVG" in app.config.plantuml_output_format.upper(): file_ext = 'svg' else: file_ext = 'png' except Exception: file_ext = 'png' gen_flow_link = generate_name(app, puml_node.children[0], file_ext) current_file_parts = fromdocname.split('/') subfolder_amount = len(current_file_parts) - 1 img_locaton = '../' * subfolder_amount + '_images/' + gen_flow_link[ 0].split('/')[-1] flow_ref = nodes.reference('t', current_needflow['caption'], refuri=img_locaton) puml_node += nodes.caption('', '', flow_ref) content.append(puml_node) if len(content) == 0: nothing_found = "No needs passed the filters" para = nodes.paragraph() nothing_found_node = nodes.Text(nothing_found, nothing_found) para += nothing_found_node content.append(para) if current_needflow["show_filters"]: para = nodes.paragraph() filter_text = "Used filter:" filter_text += " status(%s)" % " OR ".join( current_needflow["status"]) if len( current_needflow["status"]) > 0 else "" if len(current_needflow["status"]) > 0 and len( current_needflow["tags"]) > 0: filter_text += " AND " filter_text += " tags(%s)" % " OR ".join( current_needflow["tags"]) if len( current_needflow["tags"]) > 0 else "" if (len(current_needflow["status"]) > 0 or len(current_needflow["tags"]) > 0) and len( current_needflow["types"]) > 0: filter_text += " AND " filter_text += " types(%s)" % " OR ".join( current_needflow["types"]) if len( current_needflow["types"]) > 0 else "" filter_node = nodes.emphasis(filter_text, filter_text) para += filter_node content.append(para) if current_needflow['debug']: debug_container = nodes.container() if isinstance(puml_node, nodes.figure): data = puml_node.children[0]["uml"] else: data = puml_node["uml"] data = '\n'.join([html.escape(line) for line in data.split('\n')]) debug_para = nodes.raw('', '<pre>{}</pre>'.format(data), format='html') debug_container += debug_para content += debug_container node.replace_self(content)
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