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)
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
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
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
andnew_dict
is different
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
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 filefile
. First line is considered as
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
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.