def add_extra_option(app, name): """ Adds an extra option to the configuration. This option can then later be used inside needs or ``add_need``. Same impact as using :ref:`needs_extra_options` manually. **Usage**:: from sphinxcontrib.needs.api import add_extra_option add_extra_option(app, 'my_extra_option') :param app: Sphinx application object :param name: Name as string of the extra option :return: None """ extra_options = NEEDS_CONFIG.create_or_get("extra_options", dict) if name in extra_options: raise NeedsApiConfigWarning(f"Option {name} already registered.") NEEDS_CONFIG.add("extra_options", {name: directives.unchanged}, dict, append=True)
def add_warning(app, name, function=None, filter_string=None): """ Registers a warning. A warning can be based on the result of a given filter_string or an own defined function. :param app: Sphinx app object :param name: Name as string for the warning :param function: function to execute to check the warning :param filter_string: filter_string to use for the warning :return: None """ warnings_option = NEEDS_CONFIG.create_or_get("warnings", dict) if function is None and filter_string is None: raise NeedsApiConfigException( "Function or filter_string must be given for add_warning_func") if function is not None and filter_string is not None: raise NeedsApiConfigException( "For add_warning_func only function or filter_string is allowed to be set, " "not both.") warning_check = function or filter_string if name in warnings_option: raise NeedsApiConfigException(f"Warning {name} already registered.") # warnings_option[name] = warning_check NEEDS_CONFIG.add("warnings", {name: warning_check}, dict, append=True)
def prepare_env(app, env, _docname): """ Prepares the sphinx environment to store sphinx-needs internal data. """ if not hasattr(env, "needs_all_needs"): # Used to store all needed information about all needs in document env.needs_all_needs = {} if not hasattr(env, "needs_all_filters"): # Used to store all needed information about all filters in document env.needs_all_filters = {} if not hasattr(env, "needs_services"): # Used to store all needed information about all services app.needs_services = ServiceManager(app) # Register embedded services app.needs_services.register("github-issues", GithubService, gh_type="issue") app.needs_services.register("github-prs", GithubService, gh_type="pr") app.needs_services.register("github-commits", GithubService, gh_type="commit") # Register user defined services for name, service in app.config.needs_services.items(): if name not in app.needs_services.services and "class" in service and "class_init" in service: # We found a not yet registered service # But only register, if service-config contains class and class_init. # Otherwise the service may get registered later by an external sphinx-needs extension app.needs_services.register(name, service["class"], **service["class_init"]) needs_functions = app.config.needs_functions # Register built-in functions for need_common_func in needs_common_functions: register_func(need_common_func) # Register functions configured by user for needs_func in needs_functions: register_func(needs_func) # Own extra options for option in ["hidden", "duration", "completion", "has_dead_links", "has_forbidden_dead_links"]: # Check if not already set by user # if option not in app.config.needs_extra_options: # app.config.needs_extra_options[option] = directives.unchanged if option not in NEEDS_CONFIG.get("extra_options"): add_extra_option(app, option) # The default link name. Must exist in all configurations. Therefore we set it here # for the user. common_links = [] link_types = app.config.needs_extra_links basic_link_type_found = False parent_needs_link_type_found = False for link_type in link_types: if link_type["option"] == "links": basic_link_type_found = True elif link_type["option"] == "parent_needs": parent_needs_link_type_found = True if not basic_link_type_found: common_links.append( { "option": "links", "outgoing": "links outgoing", "incoming": "links incoming", "copy": False, "color": "#000000", } ) if not parent_needs_link_type_found: common_links.append( { "option": "parent_needs", "outgoing": "parent needs", "incoming": "child needs", "copy": False, "color": "#333333", } ) app.config.needs_extra_links = common_links + app.config.needs_extra_links app.config.needs_layouts = {**LAYOUTS, **app.config.needs_layouts} app.config.needs_flow_configs.update(NEEDFLOW_CONFIG_DEFAULTS) if not hasattr(env, "needs_workflow"): # Used to store workflow status information for already executed tasks. # Some tasks like backlink_creation need be be performed only once. # But most sphinx-events get called several times (for each single document # file), which would also execute our code several times... env.needs_workflow = { "backlink_creation_links": False, "dynamic_values_resolved": False, "needs_extended": False, } for link_type in app.config.needs_extra_links: env.needs_workflow["backlink_creation_{}".format(link_type["option"])] = False
def load_config(app: Sphinx, *_args): """ Register extra options and directive based on config from conf.py """ log = get_logger(__name__) types = app.config.needs_types if isinstance(app.config.needs_extra_options, dict): log.info( 'Config option "needs_extra_options" supports list and dict. However new default type since ' "Sphinx-Needs 0.7.2 is list. Please see docs for details." ) existing_extra_options = NEEDS_CONFIG.get("extra_options") for option in app.config.needs_extra_options: if option in existing_extra_options: log.warning(f'extra_option "{option}" already registered.') NEEDS_CONFIG.add("extra_options", {option: directives.unchanged}, dict, True) extra_options = NEEDS_CONFIG.get("extra_options") # Get extra links and create a dictionary of needed options. extra_links_raw = app.config.needs_extra_links extra_links = {} for extra_link in extra_links_raw: extra_links[extra_link["option"]] = directives.unchanged title_optional = app.config.needs_title_optional title_from_content = app.config.needs_title_from_content # Update NeedDirective to use customized options NeedDirective.option_spec.update(extra_options) NeedserviceDirective.option_spec.update(extra_options) # Update NeedDirective to use customized links NeedDirective.option_spec.update(extra_links) NeedserviceDirective.option_spec.update(extra_links) # Update NeedextendDirective with option modifiers. for key, value in NEED_DEFAULT_OPTIONS.items(): # Ignore options like "id" if key in NEEDEXTEND_NOT_ALLOWED_OPTIONS: continue NeedextendDirective.option_spec.update( { key: value, f"+{key}": value, f"-{key}": directives.flag, } ) for key, value in extra_links.items(): NeedextendDirective.option_spec.update( { key: value, f"+{key}": value, f"-{key}": directives.flag, f"{key}_back": value, f"+{key}_back": value, f"-{key}_back": directives.flag, } ) # "links" is not part of the extra_links-dict, so we need # to set the links_back values by hand NeedextendDirective.option_spec.update( { "links_back": NEED_DEFAULT_OPTIONS["links"], "+links_back": NEED_DEFAULT_OPTIONS["links"], "-links_back": directives.flag, } ) for key, value in extra_options.items(): NeedextendDirective.option_spec.update( { key: value, f"+{key}": value, f"-{key}": directives.flag, } ) if title_optional or title_from_content: NeedDirective.required_arguments = 0 NeedDirective.optional_arguments = 1 for t in types: # Register requested types of needs app.add_directive(t["directive"], NeedDirective) existing_warnings = NEEDS_CONFIG.get("warnings") for name, check in app.config.needs_warnings.items(): if name not in existing_warnings: NEEDS_CONFIG.add("warnings", {name: check}, dict, append=True) else: log.warning(f'{name} for "warnings" is already registered.')
def setup(app): log = get_logger(__name__) log.debug("Starting setup of Sphinx-Needs") log.debug("Load Sphinx-Data-Viewer for Sphinx-Needs") app.setup_extension("sphinx_data_viewer") app.add_builder(NeedsBuilder) app.add_config_value( "needs_types", [ {"directive": "req", "title": "Requirement", "prefix": "R_", "color": "#BFD8D2", "style": "node"}, {"directive": "spec", "title": "Specification", "prefix": "S_", "color": "#FEDCD2", "style": "node"}, {"directive": "impl", "title": "Implementation", "prefix": "I_", "color": "#DF744A", "style": "node"}, {"directive": "test", "title": "Test Case", "prefix": "T_", "color": "#DCB239", "style": "node"}, # Kept for backwards compatibility {"directive": "need", "title": "Need", "prefix": "N_", "color": "#9856a5", "style": "node"}, ], "html", ) app.add_config_value("needs_include_needs", True, "html", types=[bool]) app.add_config_value("needs_need_name", "Need", "html", types=[str]) app.add_config_value("needs_spec_name", "Specification", "html", types=[str]) app.add_config_value("needs_id_prefix_needs", "", "html", types=[str]) app.add_config_value("needs_id_prefix_specs", "", "html", types=[str]) app.add_config_value("needs_id_length", 5, "html", types=[int]) app.add_config_value("needs_specs_show_needlist", False, "html", types=[bool]) app.add_config_value("needs_id_required", False, "html", types=[bool]) app.add_config_value( "needs_id_regex", f"^[A-Z0-9_]{{{app.config.needs_id_length},}}", "html", ) app.add_config_value("needs_show_link_type", False, "html", types=[bool]) app.add_config_value("needs_show_link_title", False, "html", types=[bool]) app.add_config_value("needs_file", None, "html") app.add_config_value("needs_table_columns", "ID;TITLE;STATUS;TYPE;OUTGOING;TAGS", "html") app.add_config_value("needs_table_style", "DATATABLES", "html") app.add_config_value("needs_role_need_template", "{title} ({id})", "html") app.add_config_value("needs_role_need_max_title_length", 30, "html", types=[int]) app.add_config_value("needs_extra_options", [], "html") app.add_config_value("needs_title_optional", False, "html", types=[bool]) app.add_config_value("needs_max_title_length", -1, "html", types=[int]) app.add_config_value("needs_title_from_content", False, "html", types=[bool]) app.add_config_value("needs_diagram_template", DEFAULT_DIAGRAM_TEMPLATE, "html") app.add_config_value("needs_functions", [], "html", types=[list]) app.add_config_value("needs_global_options", {}, "html", types=[dict]) app.add_config_value("needs_duration_option", "duration", "html") app.add_config_value("needs_completion_option", "completion", "html") # If given, only the defined status are allowed. # Values needed for each status: # * name # * description # Example: [{"name": "open", "description": "open status"}, {...}, {...}] app.add_config_value("needs_statuses", [], "html") # If given, only the defined tags are allowed. # Values needed for each tag: # * name # * description # Example: [{"name": "new", "description": "new needs"}, {...}, {...}] app.add_config_value("needs_tags", False, "html", types=[bool]) # Path of css file, which shall be used for need style app.add_config_value("needs_css", "modern.css", "html") # Prefix for need_part output in tables app.add_config_value("needs_part_prefix", "\u2192\u00a0", "html") # List of additional links, which can be used by setting related option # Values needed for each new link: # * name (will also be the option name) # * incoming # * copy_link (copy to common links data. Default: True) # * color (used for needflow. Default: #000000) # Example: [{"name": "blocks, "incoming": "is blocked by", "copy_link": True, "color": "#ffcc00"}] app.add_config_value("needs_extra_links", [], "html") app.add_config_value("needs_filter_data", {}, "html") app.add_config_value("needs_flow_show_links", False, "html") app.add_config_value("needs_flow_link_types", ["links"], "html") app.add_config_value("needs_warnings", {}, "html") app.add_config_value("needs_warnings_always_warn", False, "html", types=[bool]) app.add_config_value("needs_layouts", {}, "html") app.add_config_value("needs_default_layout", "clean", "html") app.add_config_value("needs_default_style", None, "html") app.add_config_value("needs_flow_configs", {}, "html") app.add_config_value("needs_template_folder", "needs_templates/", "html") app.add_config_value("needs_services", {}, "html") app.add_config_value("needs_service_all_data", False, "html", types=[bool]) app.add_config_value("needs_debug_no_external_calls", False, "html", types=[bool]) app.add_config_value("needs_external_needs", [], "html") app.add_config_value("needs_builder_filter", "is_external==False", "html", types=[str]) # Additional classes to set for needs and needtable. app.add_config_value("needs_table_classes", NEEDS_TABLES_CLASSES, "html", types=[list]) app.add_config_value("needs_string_links", {}, "html", types=[dict]) # Define nodes app.add_node(Need, html=(html_visit, html_depart), latex=(latex_visit, latex_depart)) app.add_node( Needfilter, ) app.add_node(Needbar) app.add_node(Needimport) app.add_node(Needlist) app.add_node(Needtable) app.add_node(Needflow) app.add_node(Needpie) app.add_node(Needsequence) app.add_node(Needgantt) app.add_node(Needextract) app.add_node(Needservice) app.add_node(Needextend) app.add_node(NeedPart, html=(visitor_dummy, visitor_dummy), latex=(visitor_dummy, visitor_dummy)) ######################################################################## # DIRECTIVES ######################################################################## # Define directives app.add_directive("needbar", NeedbarDirective) app.add_directive("needfilter", NeedfilterDirective) app.add_directive("needlist", NeedlistDirective) app.add_directive("needtable", NeedtableDirective) app.add_directive("needflow", NeedflowDirective) app.add_directive("needpie", NeedpieDirective) app.add_directive("needsequence", NeedsequenceDirective) app.add_directive("needgantt", NeedganttDirective) app.add_directive("needimport", NeedimportDirective) app.add_directive("needextract", NeedextractDirective) app.add_directive("needservice", NeedserviceDirective) app.add_directive("needextend", NeedextendDirective) ######################################################################## # ROLES ######################################################################## # Provides :need:`ABC_123` for inline links. app.add_role("need", XRefRole(nodeclass=NeedRef, innernodeclass=nodes.emphasis, warn_dangling=True)) app.add_role("need_incoming", XRefRole(nodeclass=NeedIncoming, innernodeclass=nodes.emphasis, warn_dangling=True)) app.add_role("need_outgoing", XRefRole(nodeclass=NeedOutgoing, innernodeclass=nodes.emphasis, warn_dangling=True)) app.add_role("need_part", XRefRole(nodeclass=NeedPart, innernodeclass=nodes.inline, warn_dangling=True)) # Shortcut for need_part app.add_role("np", XRefRole(nodeclass=NeedPart, innernodeclass=nodes.inline, warn_dangling=True)) app.add_role("need_count", XRefRole(nodeclass=NeedCount, innernodeclass=nodes.inline, warn_dangling=True)) app.add_role("need_func", XRefRole(nodeclass=NeedFunc, innernodeclass=nodes.inline, warn_dangling=True)) ######################################################################## # EVENTS ######################################################################## # Make connections to events app.connect("env-purge-doc", purge_needs) app.connect("config-inited", load_config) app.connect("env-before-read-docs", prepare_env) app.connect("env-before-read-docs", load_external_needs) app.connect("config-inited", check_configuration) app.connect("env-merge-info", merge_data) # There is also the event doctree-read. # But it looks like in this event no references are already solved, which # makes trouble in our code. # However, some sphinx-internal actions (like image collection) are already called during # doctree-read. So manipulating the doctree may result in conflicts, as e.g. images get not # registered for sphinx. So some sphinx-internal tasks/functions may be called by hand again... # See also https://github.com/sphinx-doc/sphinx/issues/7054#issuecomment-578019701 for an example app.connect("doctree-resolved", add_sections) app.connect("doctree-resolved", process_need_nodes) app.connect("doctree-resolved", process_needextend) # Must be done very early, as it modifies need data app.connect("doctree-resolved", print_need_nodes) app.connect("doctree-resolved", process_needbar) app.connect("doctree-resolved", process_needextract) app.connect("doctree-resolved", process_needfilters) app.connect("doctree-resolved", process_needlist) app.connect("doctree-resolved", process_needtables) app.connect("doctree-resolved", process_needflow) app.connect("doctree-resolved", process_needpie) app.connect("doctree-resolved", process_needsequence) app.connect("doctree-resolved", process_needgantt) app.connect("doctree-resolved", process_need_part) app.connect("doctree-resolved", process_need_ref) app.connect("doctree-resolved", process_need_incoming) app.connect("doctree-resolved", process_need_outgoing) app.connect("doctree-resolved", process_need_count) app.connect("doctree-resolved", process_need_func) app.connect("build-finished", process_warnings) app.connect("env-updated", install_lib_static_files) # Called during consistency check, which if after everything got read in. # app.connect('env-check-consistency', process_warnings) # This should be called last, so that need-styles can override styles from used libraries app.connect("env-updated", install_styles_static_files) # Be sure Sphinx-Needs config gets erased before any events or external API calls get executed. # So never but this inside an event. NEEDS_CONFIG.create("extra_options", dict, overwrite=True) NEEDS_CONFIG.create("warnings", dict, overwrite=True) return { "version": VERSION, "parallel_read_safe": True, "parallel_write_safe": True, }
def process_warnings(app, exception): """ Checks the configured warnings. This func gets called by the latest sphinx-event, so that really everything is already done. :param app: application :param exception: raised exceptions :return: """ # We get called also if an exception occured during build # In this case the build is already broken and we do not need to check anything. if exception: return env = app.env # If no needs were defined, we do not need to do anything if not hasattr(env, "needs_all_needs"): return # Check if warnings already got executed. # Needed because the used event gets executed multiple times, but warnings need to be checked only # on first execution if hasattr(env, "needs_warnings_executed") and env.needs_warnings_executed: return env.needs_warnings_executed = True needs = env.needs_all_needs # Exclude external needs for warnings check checked_needs = {} for need_id, need in needs.items(): if not need["is_external"]: checked_needs[need_id] = need # warnings = app.config.needs_warnings warnings = NEEDS_CONFIG.get("warnings") warnings_always_warn = app.config.needs_warnings_always_warn with logging.pending_logging(): logger.info("\nChecking sphinx-needs warnings") warning_raised = False for warning_name, warning_filter in warnings.items(): if isinstance(warning_filter, str): # filter string used result = filter_needs(app, checked_needs.values(), warning_filter) elif callable(warning_filter): # custom defined filter code used from conf.py result = [] for need in checked_needs.values(): if warning_filter(need, logger): result.append(need) else: logger.warning( f"Unknown needs warnings filter {warning_filter}!") if len(result) == 0: logger.info(f"{warning_name}: passed") else: need_ids = [x["id"] for x in result] # Set Sphinx statuscode to 1, only if -W is used with sphinx-build # Because Sphinx statuscode got calculated in very early build phase and based on warning_count # Sphinx-needs warnings check hasn't happened yet # see deatils in https://github.com/sphinx-doc/sphinx/blob/81a4fd973d4cfcb25d01a7b0be62cdb28f82406d/sphinx/application.py#L345 # noqa # To be clear, app.keep_going = -W and --keep-going, and will overrite -W after # see details in https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/application.py#L182 if app.statuscode == 0 and (app.keep_going or app.warningiserror): app.statuscode = 1 # get the text for used filter, either from filter string or function name if callable(warning_filter): warning_text = warning_filter.__name__ elif isinstance(warning_filter, str): warning_text = warning_filter if warnings_always_warn: logger.warning( "{}: failed\n\t\tfailed needs: {} ({})\n\t\tused filter: {}" .format(warning_name, len(need_ids), ", ".join(need_ids), warning_text)) else: logger.info( "{}: failed\n\t\tfailed needs: {} ({})\n\t\tused filter: {}" .format(warning_name, len(need_ids), ", ".join(need_ids), warning_text)) warning_raised = True if warning_raised: logger.warning( "Sphinx-Needs warnings were raised. See console / log output for details." )
def run(self): ############################################################################################# # Get environment ############################################################################################# env = self.env # ToDo: Keep this in directive!!! collapse = self.options.get("collapse", None) if isinstance(collapse, str): if collapse.upper() in ["TRUE", 1, "YES"]: collapse = True elif collapse.upper() in ["FALSE", 0, "NO"]: collapse = False else: raise Exception("collapse attribute must be true or false") hide = "hide" in self.options id = self.options.get("id", None) content = "\n".join(self.content) status = self.options.get("status", None) if status: status = status.replace( "__", "" ) # Support for multiline options, which must use __ for empty lines tags = self.options.get("tags", "") style = self.options.get("style", None) layout = self.options.get("layout", "") template = self.options.get("template", None) pre_template = self.options.get("pre_template", None) post_template = self.options.get("post_template", None) duration = self.options.get("duration", None) completion = self.options.get("completion", None) need_extra_options = {"duration": duration, "completion": completion} for extra_link in env.config.needs_extra_links: need_extra_options[extra_link["option"]] = self.options.get( extra_link["option"], "") NEEDS_CONFIG.get("extra_options") for extra_option in NEEDS_CONFIG.get("extra_options").keys(): need_extra_options[extra_option] = self.options.get( extra_option, "") need_nodes = add_need( env.app, self.state, self.docname, self.lineno, need_type=self.name, title=self.trimmed_title, id=id, content=content, status=status, tags=tags, hide=hide, template=template, pre_template=pre_template, post_template=post_template, collapse=collapse, style=style, layout=layout, **need_extra_options, ) return need_nodes
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