Exemplo n.º 1
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
    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
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
    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 []
Exemplo n.º 13
0
    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
Exemplo n.º 14
0
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