todonotifier.utils

This module provides utility functions used in the application

  1"""This module provides utility functions used in the application
  2"""
  3
  4import logging
  5import os
  6import re
  7from typing import Dict, List, Tuple
  8
  9from todonotifier.models import TODO
 10from todonotifier.summary_generators import BaseSummaryGenerator
 11
 12# logging configuration
 13logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s")
 14logger = logging.getLogger(__name__)
 15
 16
 17class InCompatibleTypesException(Exception):
 18    """Raised when two different types of data is passed for recursive update of a dictionary
 19
 20    E.g. {"a": []} and {"a": {}}, Here value of key "a" is of type list and dict which are not same
 21    """
 22
 23    pass
 24
 25
 26def _ignore_dir_or_file(dir_or_file_path: str, exclude_dirs_or_files: dict) -> bool:
 27    """Checks and returns bool about whether a directory should be excluded based on rules in `exclude_dirs_or_files`
 28
 29    Args:
 30        dir_or_file_path (str): Path of the directory.file that needs to be checked
 31        exclude_dirs_or_files (dict): Directories/Files that shouldn't be considered
 32
 33    Returns:
 34        bool: True if `dir_or_file_path` should be ignored based on rules in `exclude_dirs_or_files` else False
 35    """
 36    dir_name = os.path.basename(dir_or_file_path)
 37
 38    for pattern in exclude_dirs_or_files.get("PATTERN", []):
 39        if bool(re.match(pattern, dir_name)):
 40            return True
 41
 42    if dir_name in exclude_dirs_or_files.get("NAME", []):
 43        return True
 44
 45    if dir_or_file_path in exclude_dirs_or_files.get("ABS_PATH", []):
 46        return True
 47
 48    return False
 49
 50
 51def get_files_in_dir(dir_path: str, extension: str, exclude_subdirs: dict, exclude_files: dict) -> List[str]:
 52    """Provides a list of files in the give directory `path` and its subdirectories
 53
 54    Args:
 55        dir_path (str): Path of the directory
 56        extension (str): Extension of file that needs to be looked for e.g. "py" (without dot and quotations)
 57        exclude_subdirs (dict): Sub directories of `parent_dir_name` that shouldn't be considered
 58        exclude_files (dict): Files in directory `parent_dir_name` or its sub-directories that shouldn't be considered
 59    """
 60    all_files = []
 61    file_extension = f".{extension}"
 62
 63    for sub_dir_or_file in os.listdir(dir_path):
 64        try:
 65            sub_dir_or_file_path = os.path.join(dir_path, sub_dir_or_file)
 66
 67            if os.path.isdir(sub_dir_or_file_path):
 68                if not _ignore_dir_or_file(sub_dir_or_file_path, exclude_subdirs):
 69                    sub_dir_all_files = get_files_in_dir(sub_dir_or_file_path, extension, exclude_subdirs, exclude_files)
 70                    all_files.extend(sub_dir_all_files)
 71            elif os.path.isfile(sub_dir_or_file_path):
 72                if sub_dir_or_file_path.endswith(file_extension) and not _ignore_dir_or_file(sub_dir_or_file_path, exclude_files):
 73                    all_files.append(sub_dir_or_file_path)
 74        except Exception:
 75            logger.exception(f"Error in getting files in directory: {sub_dir_or_file}")
 76
 77    return all_files
 78
 79
 80def recursive_update(base_dict: dict, new_dict: dict) -> None:
 81    """Performs in-place recursive update of dictionary `base_dict` from contents of dictionary `new_dict`
 82
 83    Args:
 84        base_dict (dict): Base dictionary that needs to be updated
 85        new_dict (dict): Dictionary from which `base_dict` needs to be updated with
 86
 87    Raises:
 88        InCompatibleTypesException: Raised if type of same key in `base_dict` and `new_dict` is different
 89    """
 90    for key in new_dict:
 91        if key in base_dict:
 92            if type(base_dict[key]) is dict and type(new_dict[key]) is dict:
 93                recursive_update(base_dict[key], new_dict[key])
 94            elif type(base_dict[key]) == type(new_dict[key]):  # noqa
 95                base_dict[key] = new_dict[key]
 96            else:
 97                raise InCompatibleTypesException(f"Different types passed: {type(base_dict[key])}, {type(new_dict[key])} for recursive update")
 98        else:
 99            base_dict[key] = new_dict[key]
100
101
102def compute_file_line_no_to_chars_map(file: str) -> Dict[int, int]:
103    """Takes a file location and returns a dict representing number of characters in each line no.
104
105    Line numbers are 1-indexed
106
107    Args:
108        file (str): Location of file
109
110    Returns:
111        dict: Dictionary mapping line no. ot no. of characters in that line in `file`
112    """
113    line_no_to_chars_map = {}
114    with open(file, "r") as f:
115        for line_no, line in enumerate(f.readlines()):
116            line_no_to_chars_map[line_no + 1] = len(line)
117
118    return line_no_to_chars_map
119
120
121def compute_line_and_pos_given_span(line_no_to_chars_map: dict, span: Tuple[int, int]) -> int:
122    """Computes line no. given absolute start position in file and `line_no_to_chars_map` mapping of line no. to no. of characters in that line
123
124    Args:
125        line_no_to_chars_map (dict): Dictionary mapping line no. to no. of characters in that line in `file`
126        span (Tuple[int, int]): Span value as returned by `re.span()`
127
128    Returns:
129        int: Line no. of the character at `start_idx` in file `file`. First line is considered as
130    """
131    curr_count = 0
132    for line_no in range(len(line_no_to_chars_map)):
133        curr_count += line_no_to_chars_map[line_no + 1]
134        if curr_count >= span[0]:
135            todo_line_no = line_no + 1
136            break
137
138    return todo_line_no
139
140
141def generate_summary(all_todos_objs: Dict[str, List[TODO]], summary_generators: List[BaseSummaryGenerator], generate_html: bool) -> None:
142    """Function to generate multiple kind of summaries from given list of todo items
143
144    It allows users to pass a function/callable. It will call each summary generator `callable` and pass it with
145    the `all_todos_objs`. The respective callable function can read the passed todo objects and save relevant information
146    in their containers accessible via `{callable}.container`
147
148    Args:
149        all_todos_objs (Dict[str, List[TODO]]): Key-value pair where key is relative path of file parsed and value is list of todo objects in that file
150        summary_generators (List[BaseSummaryGenerator]): List of summary generators objects
151        generate_html (bool): Boolean to control whether to generate the html report for the respective summary generator
152    """
153    for summary_generator_class_instance in summary_generators:
154        try:
155            summary_generator_class_instance.generate_summary(all_todos_objs)
156            if generate_html:
157                summary_generator_class_instance.generate_html()
158        except Exception:
159            logger.exception(f"Error in generating summary from: {summary_generator_class_instance}")
160
161
162def store_html(html: str, report_name: str, target_dir: str = None) -> None:
163    """Function to store html report into files in location `target_dir`
164
165    Args:
166        html (str): HTML content of the report
167        report_name (str): Name with which `html` content needs to be stored into a file with/without extension. Default extension is `.html`
168        target_dir (str, optional): Target location(absolute path) where file needs to be stored. Defaults to folder `.reports` in current location.
169    """
170    default_folder_name = ".report"
171    if not target_dir:
172        target_dir = os.path.join(os.getcwd(), default_folder_name)
173        if not os.path.isdir(target_dir):
174            os.mkdir(target_dir)
175
176    report_name_lst = report_name.split(".")
177    if len(report_name_lst) > 1:
178        extension = report_name_lst[-1]
179        report_name = "".join(report_name_lst[:-1])
180        report_name += f".{extension}"
181    else:
182        report_name += ".html"
183
184    file_path = os.path.join(target_dir, report_name)
185
186    with open(file_path, "w") as f:
187        f.write(html)
logger = <Logger todonotifier.utils (INFO)>
class InCompatibleTypesException(builtins.Exception):
18class InCompatibleTypesException(Exception):
19    """Raised when two different types of data is passed for recursive update of a dictionary
20
21    E.g. {"a": []} and {"a": {}}, Here value of key "a" is of type list and dict which are not same
22    """
23
24    pass

Raised when two different types of data is passed for recursive update of a dictionary

E.g. {"a": []} and {"a": {}}, Here value of key "a" is of type list and dict which are not same

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
def get_files_in_dir( dir_path: str, extension: str, exclude_subdirs: dict, exclude_files: dict) -> List[str]:
52def get_files_in_dir(dir_path: str, extension: str, exclude_subdirs: dict, exclude_files: dict) -> List[str]:
53    """Provides a list of files in the give directory `path` and its subdirectories
54
55    Args:
56        dir_path (str): Path of the directory
57        extension (str): Extension of file that needs to be looked for e.g. "py" (without dot and quotations)
58        exclude_subdirs (dict): Sub directories of `parent_dir_name` that shouldn't be considered
59        exclude_files (dict): Files in directory `parent_dir_name` or its sub-directories that shouldn't be considered
60    """
61    all_files = []
62    file_extension = f".{extension}"
63
64    for sub_dir_or_file in os.listdir(dir_path):
65        try:
66            sub_dir_or_file_path = os.path.join(dir_path, sub_dir_or_file)
67
68            if os.path.isdir(sub_dir_or_file_path):
69                if not _ignore_dir_or_file(sub_dir_or_file_path, exclude_subdirs):
70                    sub_dir_all_files = get_files_in_dir(sub_dir_or_file_path, extension, exclude_subdirs, exclude_files)
71                    all_files.extend(sub_dir_all_files)
72            elif os.path.isfile(sub_dir_or_file_path):
73                if sub_dir_or_file_path.endswith(file_extension) and not _ignore_dir_or_file(sub_dir_or_file_path, exclude_files):
74                    all_files.append(sub_dir_or_file_path)
75        except Exception:
76            logger.exception(f"Error in getting files in directory: {sub_dir_or_file}")
77
78    return all_files

Provides a list of files in the give directory path and its subdirectories

Arguments:
  • dir_path (str): Path of the directory
  • extension (str): Extension of file that needs to be looked for e.g. "py" (without dot and quotations)
  • exclude_subdirs (dict): Sub directories of parent_dir_name that shouldn't be considered
  • exclude_files (dict): Files in directory parent_dir_name or its sub-directories that shouldn't be considered
def recursive_update(base_dict: dict, new_dict: dict) -> None:
 81def recursive_update(base_dict: dict, new_dict: dict) -> None:
 82    """Performs in-place recursive update of dictionary `base_dict` from contents of dictionary `new_dict`
 83
 84    Args:
 85        base_dict (dict): Base dictionary that needs to be updated
 86        new_dict (dict): Dictionary from which `base_dict` needs to be updated with
 87
 88    Raises:
 89        InCompatibleTypesException: Raised if type of same key in `base_dict` and `new_dict` is different
 90    """
 91    for key in new_dict:
 92        if key in base_dict:
 93            if type(base_dict[key]) is dict and type(new_dict[key]) is dict:
 94                recursive_update(base_dict[key], new_dict[key])
 95            elif type(base_dict[key]) == type(new_dict[key]):  # noqa
 96                base_dict[key] = new_dict[key]
 97            else:
 98                raise InCompatibleTypesException(f"Different types passed: {type(base_dict[key])}, {type(new_dict[key])} for recursive update")
 99        else:
100            base_dict[key] = new_dict[key]

Performs in-place recursive update of dictionary base_dict from contents of dictionary new_dict

Arguments:
  • base_dict (dict): Base dictionary that needs to be updated
  • new_dict (dict): Dictionary from which base_dict needs to be updated with
Raises:
  • InCompatibleTypesException: Raised if type of same key in base_dict and new_dict is different
def compute_file_line_no_to_chars_map(file: str) -> Dict[int, int]:
103def compute_file_line_no_to_chars_map(file: str) -> Dict[int, int]:
104    """Takes a file location and returns a dict representing number of characters in each line no.
105
106    Line numbers are 1-indexed
107
108    Args:
109        file (str): Location of file
110
111    Returns:
112        dict: Dictionary mapping line no. ot no. of characters in that line in `file`
113    """
114    line_no_to_chars_map = {}
115    with open(file, "r") as f:
116        for line_no, line in enumerate(f.readlines()):
117            line_no_to_chars_map[line_no + 1] = len(line)
118
119    return line_no_to_chars_map

Takes a file location and returns a dict representing number of characters in each line no.

Line numbers are 1-indexed

Arguments:
  • file (str): Location of file
Returns:

dict: Dictionary mapping line no. ot no. of characters in that line in file

def compute_line_and_pos_given_span(line_no_to_chars_map: dict, span: Tuple[int, int]) -> int:
122def compute_line_and_pos_given_span(line_no_to_chars_map: dict, span: Tuple[int, int]) -> int:
123    """Computes line no. given absolute start position in file and `line_no_to_chars_map` mapping of line no. to no. of characters in that line
124
125    Args:
126        line_no_to_chars_map (dict): Dictionary mapping line no. to no. of characters in that line in `file`
127        span (Tuple[int, int]): Span value as returned by `re.span()`
128
129    Returns:
130        int: Line no. of the character at `start_idx` in file `file`. First line is considered as
131    """
132    curr_count = 0
133    for line_no in range(len(line_no_to_chars_map)):
134        curr_count += line_no_to_chars_map[line_no + 1]
135        if curr_count >= span[0]:
136            todo_line_no = line_no + 1
137            break
138
139    return todo_line_no

Computes line no. given absolute start position in file and line_no_to_chars_map mapping of line no. to no. of characters in that line

Arguments:
  • line_no_to_chars_map (dict): Dictionary mapping line no. to no. of characters in that line in file
  • span (Tuple[int, int]): Span value as returned by re.span()
Returns:

int: Line no. of the character at start_idx in file file. First line is considered as

def generate_summary( all_todos_objs: Dict[str, List[todonotifier.models.TODO]], summary_generators: List[todonotifier.summary_generators.BaseSummaryGenerator], generate_html: bool) -> None:
142def generate_summary(all_todos_objs: Dict[str, List[TODO]], summary_generators: List[BaseSummaryGenerator], generate_html: bool) -> None:
143    """Function to generate multiple kind of summaries from given list of todo items
144
145    It allows users to pass a function/callable. It will call each summary generator `callable` and pass it with
146    the `all_todos_objs`. The respective callable function can read the passed todo objects and save relevant information
147    in their containers accessible via `{callable}.container`
148
149    Args:
150        all_todos_objs (Dict[str, List[TODO]]): Key-value pair where key is relative path of file parsed and value is list of todo objects in that file
151        summary_generators (List[BaseSummaryGenerator]): List of summary generators objects
152        generate_html (bool): Boolean to control whether to generate the html report for the respective summary generator
153    """
154    for summary_generator_class_instance in summary_generators:
155        try:
156            summary_generator_class_instance.generate_summary(all_todos_objs)
157            if generate_html:
158                summary_generator_class_instance.generate_html()
159        except Exception:
160            logger.exception(f"Error in generating summary from: {summary_generator_class_instance}")

Function to generate multiple kind of summaries from given list of todo items

It allows users to pass a function/callable. It will call each summary generator callable and pass it with the all_todos_objs. The respective callable function can read the passed todo objects and save relevant information in their containers accessible via {callable}.container

Arguments:
  • all_todos_objs (Dict[str, List[TODO]]): Key-value pair where key is relative path of file parsed and value is list of todo objects in that file
  • summary_generators (List[BaseSummaryGenerator]): List of summary generators objects
  • generate_html (bool): Boolean to control whether to generate the html report for the respective summary generator
def store_html(html: str, report_name: str, target_dir: str = None) -> None:
163def store_html(html: str, report_name: str, target_dir: str = None) -> None:
164    """Function to store html report into files in location `target_dir`
165
166    Args:
167        html (str): HTML content of the report
168        report_name (str): Name with which `html` content needs to be stored into a file with/without extension. Default extension is `.html`
169        target_dir (str, optional): Target location(absolute path) where file needs to be stored. Defaults to folder `.reports` in current location.
170    """
171    default_folder_name = ".report"
172    if not target_dir:
173        target_dir = os.path.join(os.getcwd(), default_folder_name)
174        if not os.path.isdir(target_dir):
175            os.mkdir(target_dir)
176
177    report_name_lst = report_name.split(".")
178    if len(report_name_lst) > 1:
179        extension = report_name_lst[-1]
180        report_name = "".join(report_name_lst[:-1])
181        report_name += f".{extension}"
182    else:
183        report_name += ".html"
184
185    file_path = os.path.join(target_dir, report_name)
186
187    with open(file_path, "w") as f:
188        f.write(html)

Function to store html report into files in location target_dir

Arguments:
  • html (str): HTML content of the report
  • report_name (str): Name with which html content needs to be stored into a file with/without extension. Default extension is .html
  • target_dir (str, optional): Target location(absolute path) where file needs to be stored. Defaults to folder .reports in current location.