def filter_needs(app, needs, filter_string="", current_need=None): """ Filters given needs based on a given filter string. Returns all needs, which pass the given filter. :param app: Sphinx application object :param needs: list of needs, which shall be filtered :param filter_string: strings, which gets evaluated against each need :param current_need: current need, which uses the filter. :return: list of found needs """ if not filter_string: return needs found_needs = [] # https://docs.python.org/3/library/functions.html?highlight=compile#compile filter_compiled = compile(filter_string, "<string>", "eval") for filter_need in needs: try: if filter_single_need(app, filter_need, filter_string, needs, current_need, filter_compiled=filter_compiled): found_needs.append(filter_need) except Exception as e: log.warning(f"Filter {filter_string} not valid: Error: {e}") return found_needs
def filter_needs(needs, filter_string="", filter_parts=True, merge_part_with_parent=True): """ Filters given needs based on a given filter string. Returns all needs, which pass the given filter. :param merge_part_with_parent: If True, need_parts inherit options from their parent need :param filter_parts: If True, need_parts get also filtered :param filter_string: strings, which gets evaluated against each need :param needs: list of needs, which shall be filtered :return: """ if filter_string is None or filter_string == "": return needs found_needs = [] for filter_need in needs: try: if filter_single_need(filter_need, filter_string): found_needs.append(filter_need) except Exception as e: logger.warning("Filter {0} not valid: Error: {1}".format( filter_string, e)) return found_needs
def filter_needs(needs, filter_string="", current_need=None): """ Filters given needs based on a given filter string. Returns all needs, which pass the given filter. :param needs: list of needs, which shall be filtered :param filter_string: strings, which gets evaluated against each need :param current_need: current need, which uses the filter. :return: list of found needs """ if filter_string is None or filter_string == "": return needs found_needs = [] for filter_need in needs: try: if filter_single_need(filter_need, filter_string, needs, current_need): found_needs.append(filter_need) except Exception as e: logger.warning("Filter {0} not valid: Error: {1}".format( filter_string, e)) return found_needs
def install_styles_static_files(app, env): STATICS_DIR_PATH = os.path.join(app.builder.outdir, IMAGE_DIR_NAME) dest_path = os.path.join(STATICS_DIR_PATH, 'sphinx-needs') files_to_copy = ["common.css"] if app.config.needs_css == 'modern.css': source_folder = os.path.join(os.path.dirname(__file__), "css/modern/") for root, dirs, files in os.walk(source_folder): for single_file in files: files_to_copy.append(os.path.join(root, single_file)) elif app.config.needs_css == 'dark.css': source_folder = os.path.join(os.path.dirname(__file__), "css/dark/") for root, dirs, files in os.walk(source_folder): for single_file in files: files_to_copy.append(os.path.join(root, single_file)) elif app.config.needs_css == 'blank.css': source_folder = os.path.join(os.path.dirname(__file__), "css/blank/") for root, dirs, files in os.walk(source_folder): for single_file in files: files_to_copy.append(os.path.join(root, single_file)) else: files_to_copy += [app.config.needs_css] # Be sure no "old" css layout is already set safe_remove_file("sphinx-needs/common.css", app) safe_remove_file("sphinx-needs/blank.css", app) safe_remove_file("sphinx-needs/modern.css", app) safe_remove_file("sphinx-needs/dark.css", app) if parse_version(sphinx_version) < parse_version("1.6"): global status_iterator status_iterator = app.status_iterator for source_file_path in status_iterator( files_to_copy, 'Copying static files for sphinx-needs custom style support...', brown, len(files_to_copy)): if not os.path.isabs(source_file_path): source_file_path = os.path.join(os.path.dirname(__file__), "css", source_file_path) if not os.path.exists(source_file_path): source_file_path = os.path.join(os.path.dirname(__file__), "css", "blank", "blank.css") logger.warning( "{0} not found. Copying sphinx-internal blank.css".format( source_file_path)) dest_file_path = os.path.join(dest_path, os.path.basename(source_file_path)) if not os.path.exists(os.path.dirname(dest_file_path)): ensuredir(os.path.dirname(dest_file_path)) copyfile(source_file_path, dest_file_path) safe_add_file(os.path.relpath(dest_file_path, STATICS_DIR_PATH), app)
def install_styles_static_files(app: Sphinx, env): # Do not copy static_files for our "needs" builder if app.builder.name == "needs": return statics_dir = Path(app.builder.outdir) / IMAGE_DIR_NAME css_root = Path(__file__).parent / "css" dest_dir = statics_dir / "sphinx-needs" def find_css_files() -> Iterable[Path]: for theme in ["modern", "dark", "blank"]: if app.config.needs_css == f"{theme}.css": css_dir = css_root / theme return [f for f in css_dir.glob("**/*") if f.is_file()] return [app.config.needs_css] files_to_copy = [Path("common.css")] files_to_copy.extend(find_css_files()) # Be sure no "old" css layout is already set for theme in ["common", "modern", "dark", "blank"]: path = Path("sphinx-needs") / f"{theme}.css" safe_remove_file(path, app) if parse_version(sphinx_version) < parse_version("1.6"): global status_iterator status_iterator = app.status_iterator for source_file_path in status_iterator( files_to_copy, "Copying static files for sphinx-needs custom style support...", brown, len(files_to_copy), ): source_file_path = Path(source_file_path) if not source_file_path.is_absolute(): source_file_path = css_root / source_file_path if not source_file_path.exists(): source_file_path = css_root / "blank" / "blank.css" logger.warning( f"{source_file_path} not found. Copying sphinx-internal blank.css" ) dest_file = dest_dir / source_file_path.name dest_dir.mkdir(exist_ok=True) copyfile(str(source_file_path), str(dest_file)) relative_path = Path(dest_file).relative_to(statics_dir) safe_add_file(relative_path, app)
def procces_filters(all_needs, current_needlist): """ Filters all needs with given configuration :param current_needlist: needlist object, which stores all filters :param all_needs: List of all needs inside document :return: list of needs, which passed the filters """ sort_key = current_needlist["sort_by"] if sort_key is not None: if sort_key == "status": all_needs = sorted(all_needs, key=status_sorter) else: try: sorted_needs = sorted(all_needs, key=lambda node: node[sort_key]) all_needs = sorted_needs except Exception as e: logger.warning( "Sorting parameter {0} not valid: Error: {1}".format( sort_key, e)) found_needs_by_options = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(all_needs) for need_info in all_needs_incl_parts: status_filter_passed = False if current_needlist["status"] is None or len( current_needlist["status"]) == 0: # Filtering for status was not requested status_filter_passed = True elif need_info["status"] is not None and need_info[ "status"] in current_needlist["status"]: # Match was found status_filter_passed = True tags_filter_passed = False if len(set(need_info["tags"]) & set(current_needlist["tags"])) > 0 or len( current_needlist["tags"]) == 0: tags_filter_passed = True type_filter_passed = False if need_info["type"] in current_needlist["types"] \ or need_info["type_name"] in current_needlist["types"] \ or len(current_needlist["types"]) == 0: type_filter_passed = True if status_filter_passed and tags_filter_passed and type_filter_passed: found_needs_by_options.append(need_info) found_needs_by_string = filter_needs(all_needs_incl_parts, current_needlist["filter"]) # found_needs = [x for x in found_needs_by_string if x in found_needs_by_options] found_needs = check_need_list(found_needs_by_options, found_needs_by_string) return found_needs
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 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 meta(self, name, prefix=None, show_empty=False): """ Returns the specific meta data of a need inside docutils nodes. Usage:: <<meta('status', prefix='**status**', show_empty=True)>> :param name: name of the need item :param prefix: string as rst-code, will be added infront of the value output :param show_empty: If false and requested need-value is None or '', no output is returned. Default: false :return: docutils node """ data_container = nodes.inline(classes=["needs_" + name]) if prefix: prefix_node = self._parse(prefix) label_node = nodes.inline(classes=["needs_label"]) label_node += prefix_node data_container.append(label_node) try: data = self.need[name] except KeyError: data = "" if data is None and not show_empty: return [] elif data is None and show_empty: data = "" if isinstance(data, str): if len(data) == 0 and not show_empty: return [] # data_node = nodes.inline(classes=["needs_data"]) # data_node.append(nodes.Text(data, data)) # data_container.append(data_node) matching_link_confs = [] for link_conf in self.string_links.values(): if name in link_conf["options"]: matching_link_confs.append(link_conf) string_link_error = False if matching_link_confs: try: link_name = None link_url = None for link_conf in matching_link_confs: match = link_conf["regex_compiled"].search(data) if match: render_content = match.groupdict() link_url = link_conf["url_template"].render(**render_content) link_name = link_conf["name_template"].render(**render_content) break # We only handle the first matching string_link data_node = nodes.inline(classes=["needs_data"]) if link_name: data_node.append(nodes.reference(link_name, link_name, refuri=link_url)) else: # if no string_link match was made, we handle it as normal string value data_node.append(nodes.Text(data, data)) except Exception as e: logger.warning( f'Problems dealing with string 2 link transformation for value "{data}" of ' f'option "{name}". Error: {e}' ) string_link_error = True # Create normal text output elif not matching_link_confs or string_link_error: # Normal text handling data_node = nodes.inline(classes=["needs_data"]) data_node.append(nodes.Text(data, data)) data_container.append(data_node) elif isinstance(data, list): if len(data) == 0 and not show_empty: return [] list_container = nodes.inline(classes=["needs_data_container"]) for index, element in enumerate(data): if index > 0: spacer = nodes.inline(classes=["needs_spacer"]) spacer += nodes.Text(", ", ", ") list_container += spacer inline = nodes.inline(classes=["needs_data"]) inline += nodes.Text(element, element) list_container += inline data_container += list_container else: data_container.append(nodes.Text(data, data)) return data_container
def process_filters(app, all_needs, current_needlist, include_external=True): """ Filters all needs with given configuration. Used by needlist, needtable and needflow. :param app: Sphinx application object :param current_needlist: needlist object, which stores all filters :param all_needs: List of all needs inside document :param include_external: Boolean, which decides to include external needs or not :return: list of needs, which passed the filters """ sort_key = current_needlist["sort_by"] if sort_key: try: all_needs = sorted(all_needs, key=lambda node: node[sort_key] or "") except KeyError as e: log.warning(f"Sorting parameter {sort_key} not valid: Error: {e}") # check if include external needs checked_all_needs = [] if not include_external: for need in all_needs: if not need["is_external"]: checked_all_needs.append(need) else: checked_all_needs = all_needs found_needs_by_options = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(checked_all_needs) # Check if external filter code is defined filter_func = None filter_code = None filter_func_ref = current_needlist.get("filter_func", None) if filter_func_ref: try: filter_module, filter_function = filter_func_ref.rsplit(".") except ValueError: log.warn( f'Filter function not valid "{filter_func_ref}". Example: my_module:my_func' ) return [] # No needs found because of invalid filter function try: final_module = importlib.import_module(filter_module) filter_func = getattr(final_module, filter_function) except Exception: log.warn(f"Could not import filter function: {filter_func_ref}") return [] # Get filter_code from if not filter_code: filter_code = "\n".join(current_needlist["filter_code"]) if (not filter_code or filter_code.isspace()) and not filter_func: if bool(current_needlist["status"] or current_needlist["tags"] or current_needlist["types"]): for need_info in all_needs_incl_parts: status_filter_passed = False if (not current_needlist["status"] or need_info["status"] and need_info["status"] in current_needlist["status"]): # Filtering for status was not requested or match was found status_filter_passed = True tags_filter_passed = False if (len( set(need_info["tags"]) & set(current_needlist["tags"])) > 0 or len(current_needlist["tags"]) == 0): tags_filter_passed = True type_filter_passed = False if (need_info["type"] in current_needlist["types"] or need_info["type_name"] in current_needlist["types"] or len(current_needlist["types"]) == 0): type_filter_passed = True if status_filter_passed and tags_filter_passed and type_filter_passed: found_needs_by_options.append(need_info) # Get needy by filter string found_needs_by_string = filter_needs(app, all_needs_incl_parts, current_needlist["filter"]) # Make a intersection of both lists found_needs = intersection_of_need_results(found_needs_by_options, found_needs_by_string) else: # There is no other config as the one for filter string. # So we only need this result. found_needs = filter_needs(app, all_needs_incl_parts, current_needlist["filter"]) else: # Provides only a copy of needs to avoid data manipulations. try: context = { "needs": copy.deepcopy(all_needs_incl_parts), "results": [], } except Exception as e: raise e if filter_code: # code from content exec(filter_code, context) elif filter_func: # code from external file filter_func(**context) else: log.warning("Something went wrong running filter") return [] # The filter results may be dirty, as it may continue manipulated needs. found_dirty_needs = context["results"] found_needs = [] # Just take the ids from search result and use the related, but original need found_need_ids = [x["id_complete"] for x in found_dirty_needs] for need in all_needs_incl_parts: if need["id_complete"] in found_need_ids: found_needs.append(need) # Store basic filter configuration and result global list. # Needed mainly for exporting the result to needs.json (if builder "needs" is used). env = current_needlist["env"] filter_list = env.needs_all_filters found_needs_ids = [need["id_complete"] for need in found_needs] filter_list[current_needlist["target_node"]] = { "target_node": current_needlist["target_node"], "filter": current_needlist["filter"] or "", "status": current_needlist["status"], "tags": current_needlist["tags"], "types": current_needlist["types"], "export_id": current_needlist["export_id"].upper(), "result": found_needs_ids, "amount": len(found_needs_ids), } return found_needs
def process_filters(all_needs, current_needlist): """ Filters all needs with given configuration. Used by needlist, needtable and needflow. :param current_needlist: needlist object, which stores all filters :param all_needs: List of all needs inside document :return: list of needs, which passed the filters """ sort_key = current_needlist["sort_by"] if sort_key is not None: if sort_key == "status": all_needs = sorted(all_needs, key=status_sorter) else: try: sorted_needs = sorted(all_needs, key=lambda node: node[sort_key]) all_needs = sorted_needs except Exception as e: logger.warning( "Sorting parameter {0} not valid: Error: {1}".format( sort_key, e)) found_needs_by_options = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(all_needs) filter_code = '\n'.join(current_needlist["filter_code"]) if not filter_code or filter_code.isspace(): for need_info in all_needs_incl_parts: status_filter_passed = False if current_needlist["status"] is None or len( current_needlist["status"]) == 0: # Filtering for status was not requested status_filter_passed = True elif need_info["status"] is not None and need_info[ "status"] in current_needlist["status"]: # Match was found status_filter_passed = True tags_filter_passed = False if len(set(need_info["tags"]) & set(current_needlist["tags"])) > 0 or len( current_needlist["tags"]) == 0: tags_filter_passed = True type_filter_passed = False if need_info["type"] in current_needlist["types"] \ or need_info["type_name"] in current_needlist["types"] \ or len(current_needlist["types"]) == 0: type_filter_passed = True if status_filter_passed and tags_filter_passed and type_filter_passed: found_needs_by_options.append(need_info) found_needs_by_string = filter_needs(all_needs_incl_parts, current_needlist["filter"]) found_needs = check_need_list(found_needs_by_options, found_needs_by_string) else: # Provides only a copy of needs to avoid data manipulations. context = { 'needs': copy.deepcopy(all_needs_incl_parts), 'results': [], } exec(filter_code, context) # The filter results may be dirty, as it may continue manipulated needs. found_dirty_needs = context['results'] found_needs = [] # Just take the ids from search result and use the related, but original need found_need_ids = [x['id'] for x in found_dirty_needs] for need in all_needs_incl_parts: if need['id'] in found_need_ids: found_needs.append(need) # Store basic filter configuration and result global list. # Needed mainly for exporting the result to needs.json (if builder "needs" is used). env = current_needlist['env'] filter_list = env.needs_all_filters found_needs_ids = [need['id'] for need in found_needs] filter_list[current_needlist['target_node']] = { 'target_node': current_needlist['target_node'], 'filter': current_needlist['filter'] if current_needlist['filter'] is not None else "", 'status': current_needlist['status'], 'tags': current_needlist['tags'], 'types': current_needlist['types'], 'export_id': current_needlist['export_id'].upper(), 'result': found_needs_ids, 'amount': len(found_needs_ids) } return found_needs
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 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
def procces_filters(all_needs, current_needlist): """ Filters all needs with given configuration :param current_needlist: needlist object, which stores all filters :param all_needs: List of all needs inside document :return: list of needs, which passed the filters """ sort_key = current_needlist["sort_by"] if sort_key is not None: if sort_key == "status": all_needs = sorted(all_needs, key=status_sorter) else: try: sorted_needs = sorted(all_needs, key=lambda node: node[sort_key]) all_needs = sorted_needs except Exception as e: logger.warning( "Sorting parameter {0} not valid: Error: {1}".format( sort_key, e)) found_needs_by_options = [] # Add all need_parts of given needs to the search list all_needs_incl_parts = prepare_need_list(all_needs) for need_info in all_needs_incl_parts: status_filter_passed = False if current_needlist["status"] is None or len( current_needlist["status"]) == 0: # Filtering for status was not requested status_filter_passed = True elif need_info["status"] is not None and need_info[ "status"] in current_needlist["status"]: # Match was found status_filter_passed = True tags_filter_passed = False if len(set(need_info["tags"]) & set(current_needlist["tags"])) > 0 or len( current_needlist["tags"]) == 0: tags_filter_passed = True type_filter_passed = False if need_info["type"] in current_needlist["types"] \ or need_info["type_name"] in current_needlist["types"] \ or len(current_needlist["types"]) == 0: type_filter_passed = True if status_filter_passed and tags_filter_passed and type_filter_passed: found_needs_by_options.append(need_info) found_needs_by_string = filter_needs(all_needs_incl_parts, current_needlist["filter"]) # found_needs = [x for x in found_needs_by_string if x in found_needs_by_options] found_needs = check_need_list(found_needs_by_options, found_needs_by_string) # Store basic filter configuration and result global list. # Needed mainly for exporting the result to needs.json (if builder "needs" is used). env = current_needlist['env'] filter_list = env.needs_all_filters found_needs_ids = [need['id'] for need in found_needs] filter_list[current_needlist['target_node']] = { 'target_node': current_needlist['target_node'], 'filter': current_needlist['filter'] if current_needlist['filter'] is not None else "", 'status': current_needlist['status'], 'tags': current_needlist['tags'], 'types': current_needlist['types'], 'export_id': current_needlist['export_id'].upper(), 'result': found_needs_ids, 'amount': len(found_needs_ids) } return found_needs