Coverage for / home / ubuntu / lunchbox / python / lunchbox / tools.py: 100%
278 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-26 04:02 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-26 04:02 +0000
1from typing import Any, Callable, Dict, Generator, List, Optional, Union # noqa: F401
3from itertools import dropwhile, takewhile
4from pathlib import Path
5from pprint import pformat
6import inspect
7import json
8import logging
9import os
10import re
11import urllib.request
13import wrapt
15from lunchbox.enforce import Enforce, EnforceError
16from lunchbox.stopwatch import StopWatch
18LOG_LEVEL = os.environ.get('LOG_LEVEL', 'WARNING').upper()
19logging.basicConfig(level=LOG_LEVEL)
20LOGGER = logging.getLogger(__name__)
22FilePath = Path | str
23# ------------------------------------------------------------------------------
26'''
27A library of miscellaneous tools.
28'''
31def to_snakecase(string):
32 # type: (str) -> str
33 '''
34 Converts a given string to snake_case.
36 Args:
37 string (str): String to be converted.
39 Returns:
40 str: snake_case string.
41 '''
42 output = re.sub('([A-Z]+)', r'_\1', string)
43 output = re.sub('-', '_', output)
44 output = re.sub(r'\.', '_', output)
45 output = re.sub(' ', '_', output)
46 output = re.sub('_+', '_', output)
47 output = re.sub('^_|_$', '', output)
48 output = output.lower()
49 return output
52def try_(function, item, return_item='item'):
53 # type: (Callable[[Any], Any], Any, Any) -> Any
54 '''
55 Call given function on given item, catch any exceptions and return given
56 return item.
58 Args:
59 function (function): Function of signature lambda x: x.
60 item (object): Item used to call function.
61 return_item (object, optional): Item to be returned. Default: "item".
63 Returns:
64 object: Original item if return_item is "item".
65 Exception: If return_item is "error".
66 object: Object return by function call if return_item is not "item" or
67 "error".
68 '''
69 try:
70 return function(item)
71 except Exception as error:
72 if return_item == 'item':
73 return item
74 elif return_item == 'error':
75 return error
76 return return_item
79def get_ordered_unique(items):
80 # type: (List) -> List
81 '''
82 Generates a unique list of items in same order they were received in.
84 Args:
85 items (list): List of items.
87 Returns:
88 list: Unique ordered list.
89 '''
90 output = []
91 temp = set()
92 for item in items:
93 if item not in temp:
94 output.append(item)
95 temp.add(item)
96 return output
99def relative_path(module, path):
100 # type: (Union[str, Path], Union[str, Path]) -> Path
101 '''
102 Resolve path given current module's file path and given suffix.
104 Args:
105 module (str or Path): Always __file__ of current module.
106 path (str or Path): Path relative to __file__.
108 Returns:
109 Path: Resolved Path object.
110 '''
111 module_root = Path(module).parent
112 path_ = Path(path).parts # type: Any
113 path_ = list(dropwhile(lambda x: x == ".", path_))
114 up = len(list(takewhile(lambda x: x == "..", path_)))
115 path_ = Path(*path_[up:])
116 root = list(module_root.parents)[up - 1]
117 output = Path(root, path_).absolute()
119 LOGGER.debug(
120 f'relative_path called with: {module} and {path_}. Returned: {output}')
121 return output
124def truncate_list(items, size=3):
125 # type (list, int) -> list
126 '''
127 Truncates a given list to a given size, replaces the middle contents with
128 "...".
130 Args:
131 items (list): List of objects.
132 size (int, optional): Size of output list.
134 Raises:
135 EnforceError: If item is not a list.
136 EnforceError: If size is not an integer greater than -1.
138 Returns:
139 list: List of given size.
140 '''
141 Enforce(items, 'instance of', list, message='Items must be a list.')
142 msg = 'Size must be an integer greater than -1. Given value: {a}.'
143 Enforce(size, 'instance of', int, message=msg)
144 Enforce(size, '>', -1, message=msg)
145 # --------------------------------------------------------------------------
147 if len(items) <= size:
148 return items
149 if size == 0:
150 return []
151 if size == 1:
152 return items[:1]
153 if size == 2:
154 return [items[0], items[-1]]
156 output = items[:size - 2]
157 output.append('...')
158 output.append(items[-1])
159 return output
162def truncate_blob_lists(blob, size=3):
163 # type: (dict, int) -> dict
164 '''
165 Truncates lists inside given JSON blob to a given size.
167 Args:
168 blob (dict): Blob to be truncated.
169 size (int, optional): Size of lists. Default 3.
171 Raises:
172 EnforceError: If blob is not a dict.
174 Returns:
175 dict: Truncated blob.
176 '''
177 Enforce(blob, 'instance of', dict, message='Blob must be a dict.')
178 # --------------------------------------------------------------------------
180 def recurse_list(items, size):
181 output = []
182 for item in truncate_list(items, size=size):
183 if isinstance(item, dict):
184 item = recurse(item)
185 elif isinstance(item, list):
186 item = recurse_list(item, size)
187 output.append(item)
188 return output
190 def recurse(item):
191 output = {}
192 for k, v in item.items():
193 if isinstance(v, dict):
194 output[k] = recurse(v)
195 elif isinstance(v, list):
196 output[k] = recurse_list(v, size)
197 else:
198 output[k] = v
199 return output
200 return recurse(blob)
203# LOGGING-----------------------------------------------------------------------
204def log_runtime(
205 function, *args, message_=None, _testing=False, log_level='info', **kwargs
206):
207 # type (Callable, ..., Optional[str], bool, str, ...) -> Any
208 r'''
209 Logs the duration of given function called with given arguments.
211 Args:
212 function (function): Function to be called.
213 \*args (object, optional): Arguments.
214 message_ (str, optional): Message to be returned. Default: None.
215 _testing (bool, optional): Returns message if True. Default: False.
216 log_level (str, optional): Log level. Default: info.
217 \*\*kwargs (object, optional): Keyword arguments.
219 Raises:
220 EnforceError: If log level is illegal.
222 Returns:
223 object: function(*args, **kwargs).
224 '''
225 level = log_level_to_int(log_level)
227 # this may silently break file writes in multiprocessing
228 stopwatch = StopWatch()
229 stopwatch.start()
230 output = function(*args, **kwargs)
231 stopwatch.stop()
233 if message_ is not None:
234 message_ += f'\n Runtime: {stopwatch.human_readable_delta}'
235 else:
236 message_ = f'''{function.__name__}
237 Runtime: {stopwatch.human_readable_delta}
238 Args: {pformat(args)}
239 Kwargs: {pformat(kwargs)}'''
241 if _testing:
242 return message_
244 LOGGER.log(level, message_)
245 return output
248@wrapt.decorator
249def runtime(wrapped, instance, args, kwargs):
250 # type: (Callable, Any, Any, Any) -> Any
251 r'''
252 Decorator for logging the duration of given function called with given
253 arguments.
255 Args:
256 wrapped (function): Function to be called.
257 instance (object): Needed by wrapt.
258 \*args (object, optional): Arguments.
259 \*\*kwargs (object, optional): Keyword arguments.
261 Returns:
262 function: Wrapped function.
263 '''
264 return log_runtime(wrapped, *args, **kwargs)
267def log_level_to_int(level):
268 # type: (Union[str, int]) -> int
269 '''
270 Convert a given string or integer into a log level integer.
272 Args:
273 level (str or int): Log level.
275 Raises:
276 EnforceError: If level is illegal.
278 Returns:
279 int: Log level as integer.
280 '''
281 keys = ['critical', 'debug', 'error', 'fatal', 'info', 'warn', 'warning']
282 values = [getattr(logging, x.upper()) for x in keys]
283 lut = dict(zip(keys, values)) # type: Dict[str, int]
285 msg = 'Log level must be an integer or string. Given value: {a}. '
286 lut_msg = ', '.join([f'{k}: {v}' for k, v in zip(keys, values)])
287 msg += f'Legal values: [{lut_msg}].'
289 output = 0
290 if isinstance(level, int):
291 Enforce(level, 'in', values, message=msg)
292 output = level
294 elif isinstance(level, str):
295 level = level.lower()
296 Enforce(level, 'in', keys, message=msg)
297 output = lut[level]
299 else:
300 raise EnforceError(msg.format(a=level))
302 return output
305class LogRuntime:
306 '''
307 LogRuntime is a class for logging the runtime of arbitrary code.
309 Attributes:
310 message (str): Logging message with runtime line.
311 delta (datetime.timedelta): Runtime.
312 human_readable_delta (str): Runtime in human readable format.
314 Example:
316 >>> import time
317 >>> def foobar():
318 time.sleep(1)
320 >>> with LogRuntime('Foo the bars', name=foobar.__name__, level='debug'):
321 foobar()
322 DEBUG:foobar:Foo the bars - Runtime: 0:00:01.001069 (1 second)
324 >>> with LogRuntime(message='Fooing all the bars', suppress=True) as log:
325 foobar()
326 >>> print(log.message)
327 Fooing all the bars - Runtime: 0:00:01.001069 (1 second)
328 '''
329 def __init__(
330 self,
331 message='', # type: str
332 name='LogRuntime', # type: str
333 level='info', # type: str
334 suppress=False, # type: bool
335 message_func=None, # type: Optional[Callable[[str, StopWatch], None]]
336 callback=None, # type: Optional[Callable[[str], Any]]
337 ):
338 # type: (...) -> None
339 '''
340 Constructs a LogRuntime instance.
342 Args:
343 message (str, optional): Logging message. Default: ''.
344 name (str, optional): Name of logger. Default: 'LogRuntime'.
345 level (str or int, optional): Log level. Default: info.
346 suppress (bool, optional): Whether to suppress logging.
347 Default: False.
348 message_func (function, optional): Custom message function of the
349 signature (message, StopWatch) -> str. Default: None.
350 callback (function, optional): Callback function of the signature
351 (message) -> Any. Default: None.
353 Raises:
354 EnforceError: If message is not a string.
355 EnforceError: If name is not a string.
356 EnforceError: If level is not legal logging level.
357 EnforceError: If suppress is not a boolean.
358 '''
359 Enforce(message, 'instance of', str)
360 Enforce(name, 'instance of', str)
361 Enforce(suppress, 'instance of', bool)
362 # ----------------------------------------------------------------------
364 self._message = message
365 self._stopwatch = StopWatch()
366 self._logger = logging.getLogger(name)
367 self._level = log_level_to_int(level)
368 self._suppress = suppress
369 self._message_func = message_func
370 self._callback = callback
372 @staticmethod
373 def _default_message_func(message, stopwatch):
374 # type: (str, StopWatch) -> str
375 '''
376 Add runtime information to message given StopWatch instance.
378 Args:
379 message (str): Message.
380 stopwatch (StopWatch): StopWatch instance.
382 Raises:
383 EnforeceError: If Message is not a string.
384 EnforceError: If stopwatch is not a StopWatch instance.
386 Returns:
387 str: Message with runtime information.
388 '''
389 Enforce(message, 'instance of', str)
390 Enforce(stopwatch, 'instance of', StopWatch)
391 # ----------------------------------------------------------------------
393 msg = f'Runtime: {stopwatch.delta} '
394 msg += f'({stopwatch.human_readable_delta})'
395 if message != '':
396 msg = message + ' - ' + msg
397 return msg
399 def __enter__(self):
400 # type: () -> LogRuntime
401 '''
402 Starts stopwatch.
404 Returns:
405 LogRuntime: self.
406 '''
407 self._stopwatch.start()
408 return self
410 def __exit__(self, *args):
411 # type: (Any) -> None
412 '''
413 Stops stopwatch and logs message.
414 '''
415 stopwatch = self._stopwatch
416 stopwatch.stop()
417 self.delta = self._stopwatch.delta
418 self.human_readable_delta = self._stopwatch.human_readable_delta
420 msg_func = self._message_func or self._default_message_func
421 self.message = msg_func(self._message, stopwatch)
423 if not self._suppress:
424 self._logger.log(self._level, self.message)
426 if self._callback is not None:
427 self._callback(str(self.message))
430# HTTP-REQUESTS-----------------------------------------------------------------
431def post_to_slack(url, channel, message):
432 # type (str, str, str) -> urllib.request.HttpResponse
433 '''
434 Post a given message to a given slack channel.
436 Args:
437 url (str): https://hooks.slack.com/services URL.
438 channel (str): Channel name.
439 message (str): Message to be posted.
441 Raises:
442 EnforceError: If URL is not a string.
443 EnforceError: If URL does not start with https://hooks.slack.com/services
444 EnforceError: If channel is not a string.
445 EnforceError: If message is not a string.
447 Returns:
448 HTTPResponse: Response.
449 '''
450 Enforce(url, 'instance of', str)
451 Enforce(channel, 'instance of', str)
452 Enforce(message, 'instance of', str)
453 msg = 'URL must begin with https://hooks.slack.com/services/. '
454 msg += f'Given URL: {url}'
455 Enforce(
456 url.startswith('https://hooks.slack.com/services/'), '==', True,
457 message=msg
458 )
459 # --------------------------------------------------------------------------
461 request = urllib.request.Request(
462 url,
463 method='POST',
464 headers={'Content-type': 'application/json'},
465 data=json.dumps(dict(
466 channel='#' + channel,
467 text=message,
468 )).encode(),
469 )
470 return urllib.request.urlopen(request)
473# API---------------------------------------------------------------------------
474def get_function_signature(function):
475 # type: (Callable) -> Dict
476 '''
477 Inspect a given function and return its arguments as a list and its keyword
478 arguments as a dict.
480 Args:
481 function (function): Function to be inspected.
483 Returns:
484 dict: args and kwargs.
485 '''
486 spec = inspect.getfullargspec(function)
487 args = list(spec.args)
488 kwargs = {} # type: Any
489 if spec.defaults is not None:
490 args = args[:-len(spec.defaults)]
491 kwargs = list(spec.args)[-len(spec.defaults):]
492 kwargs = dict(zip(kwargs, spec.defaults))
493 return dict(args=args, kwargs=kwargs)
496def _dir_table(obj, public=True, semiprivate=True, private=False, max_width=100):
497 # type: (Any, bool, bool, bool, int) -> str
498 '''
499 Create a table from results of calling dir(obj).
501 Args:
502 obj (object): Object to call dir on.
503 public (bool, optional): Include public attributes in table.
504 Default: True.
505 semiprivate (bool, optional): Include semiprivate attributes in table.
506 Default: True.
507 private (bool, optional): Include private attributes in table.
508 Default: False.
509 max_width (int, optional): Maximum table width: Default: 100.
511 Returns:
512 str: Table.
513 '''
514 dirkeys = dir(obj)
515 pub = set(list(filter(lambda x: re.search('^[^_]', x), dirkeys)))
516 priv = set(list(filter(lambda x: re.search('^__|^_[A-Z].*__', x), dirkeys)))
517 semipriv = set(dirkeys).difference(pub).difference(priv)
519 keys = set()
520 if public:
521 keys.update(pub)
522 if private:
523 keys.update(priv)
524 if semiprivate:
525 keys.update(semipriv)
527 data = [dict(key='NAME', atype='TYPE', val='VALUE')]
528 max_key = 0
529 max_atype = 0
530 for key in sorted(keys):
531 attr = getattr(obj, key)
532 atype = attr.__class__.__name__
533 val = str(attr).replace('\n', ' ')
534 max_key = max(max_key, len(key))
535 max_atype = max(max_atype, len(atype))
536 row = dict(key=key, atype=atype, val=val)
537 data.append(row)
539 pattern = f'{{key:<{max_key}}} {{atype:<{max_atype}}} {{val}}'
540 lines = list(map(lambda x: pattern.format(**x)[:max_width], data))
541 output = '\n'.join(lines)
542 return output
545def dir_table(obj, public=True, semiprivate=True, private=False, max_width=100):
546 # type: (Any, bool, bool, bool, int) -> None
547 '''
548 Prints a table from results of calling dir(obj).
550 Args:
551 obj (object): Object to call dir on.
552 public (bool, optional): Include public attributes in table.
553 Default: True.
554 semiprivate (bool, optional): Include semiprivate attributes in table.
555 Default: True.
556 private (bool, optional): Include private attributes in table.
557 Default: False.
558 max_width (int, optional): Maximum table width: Default: 100.
559 '''
560 print(_dir_table(
561 obj,
562 public=public,
563 semiprivate=semiprivate,
564 private=private,
565 max_width=max_width,
566 )) # pragma: no cover
569def api_function(wrapped=None, **kwargs):
570 r'''
571 A decorator that enforces keyword argument only function signatures and
572 required keyword argument values when called.
574 Args:
575 wrapped (function): For dev use. Default: None.
576 \*\*kwargs (dict): Keyword arguments. # noqa: W605
578 Raises:
579 TypeError: If non-keyword argument found in functionn signature.
580 ValueError: If keyword arg with value of '<required>' is found.
582 Returns:
583 api function.
584 '''
585 @wrapt.decorator
586 def wrapper(wrapped, instance, args, kwargs):
587 sig = get_function_signature(wrapped)
589 # ensure no arguments are present
590 if len(sig['args']) > 0:
591 msg = 'Function may only have keyword arguments. '
592 msg += f"Found non-keyword arguments: {sig['args']}."
593 raise TypeError(msg)
595 # ensure all required kwarg values are present
596 params = sig['kwargs']
597 params.update(kwargs)
598 for key, val in params.items():
599 if val == '<required>':
600 msg = f'Missing required keyword argument: {key}.'
601 raise ValueError(msg)
603 LOGGER.debug(f'{wrapped} called with {params}.')
604 return wrapped(*args, **kwargs)
605 return wrapper(wrapped)
608def format_block(text, dedent=0, indent=0, collapse=True):
609 # type: (str, int, int, bool) -> str
610 '''
611 Format text block by a given indent.
612 Also strips leading and trailing newlines.
614 Args:
615 text (str): Text to be indented.
616 dedent (int, optional): Amount of indent to be removed. Default: 0.
617 indent (int, optional): Amount of indent. Default: 0.
618 collapse (bool, optional): If text is just whitespace, return a null
619 string. Default: True.
621 Returns:
622 str: Indented text.
623 '''
624 if collapse and re.search(r'^\s*$', text):
625 return ''
627 line = re.sub('^ *| *$', '', text)
628 lines = line.lstrip('\n').rstrip('\n').split('\n')
629 buff = '^' + ' ' * dedent
630 lines = [re.sub(buff, '', x) for x in lines]
632 # indent by given amount
633 ident = ' ' * indent
634 lines = [re.sub('^', ident, x) for x in lines]
635 output = '\n'.join(lines)
636 return output
639def autoformat_block(text):
640 # type: (str) -> str
641 '''
642 Determine block indentation and format text block with 0 indentation.
644 Args:
645 text (str): Text.
647 Returns:
648 str: Dedented text.
649 '''
650 lines = text.lstrip('\n').rstrip('\n').split('\n')
651 lines = list(filter(lambda x: not re.search('^ *$', x), lines))
652 tmp = [re.search('^ *', x).group(0) for x in lines] # type: ignore
653 indents = list(map(len, tmp))
654 if indents == []:
655 indents = [0]
656 indent = min(indents)
657 return format_block(text, dedent=indent)
660def traverse_directory(
661 directory, include_regex='', exclude_regex='', entry_type='file'
662):
663 # type: (FilePath, str, str, str) -> Generator[Path, None, None]
664 '''
665 Recusively list all files or directories within a given directory.
667 Args:
668 directory (str or Path): Directory to walk.
669 include_regex (str, optional): Include filenames that match this regex.
670 Default: ''.
671 exclude_regex (str, optional): Exclude filenames that match this regex.
672 Default: ''.
673 entry_type (str, optional): Kind of directory entry to return. Options
674 include: file, directory. Default: file.
676 Raises:
677 FileNotFoundError: If argument is not a directory or does not exist.
678 EnforceError: If entry_type is not file or directory.
680 Yields:
681 Path: File.
682 '''
683 etypes = ['file', 'directory']
684 msg = 'Illegal entry type: {a}. Legal entry types: {b}.'
685 Enforce(entry_type, 'in', etypes, message=msg)
687 directory = Path(directory)
688 if not directory.is_dir():
689 msg = f'{directory} is not a directory or does not exist.'
690 raise FileNotFoundError(msg)
691 # --------------------------------------------------------------------------
693 include_re = re.compile(include_regex)
694 exclude_re = re.compile(exclude_regex)
696 for root, dirs, items in os.walk(directory):
697 if entry_type == 'directory':
698 items = dirs
699 for item in items:
700 filepath = Path(root, item)
702 output = True
703 temp = filepath.absolute().as_posix()
704 if include_regex != '' and not include_re.search(temp):
705 output = False
706 if exclude_regex != '' and exclude_re.search(temp):
707 output = False
709 if output:
710 yield filepath
713def str_to_bool(string):
714 # type: (str) -> bool
715 '''
716 Converts a string to a boolean value.
718 Args:
719 string (str): String to be converted.
721 Returns:
722 bool: Boolean
723 '''
724 if string.lower() == 'true':
725 return True
726 return False
727# ------------------------------------------------------------------------------
730class RegexMatch:
731 '''
732 A convenience class for using regular expressions in match statements.
733 '''
734 def __init__(self, string):
735 # type: (str) -> None
736 '''
737 Construct a RegexMatch instance.
739 Args:
740 string (str): String to match pattern against.
741 '''
742 self.string = string
744 def __eq__(self, pattern):
745 # type: (object) -> bool
746 '''
747 Check if string matches pattern.
749 Args:
750 pattern (str): Regular expression pattern.
752 Returns:
753 bool: True if string matches pattern.
754 '''
755 return bool(re.search(str(pattern), self.string))
756# ------------------------------------------------------------------------------
759def is_standard_module(name):
760 # type: (str) -> bool
761 '''
762 Determines if given module name is a python builtin.
764 Args:
765 name (str): Python module name.
767 Returns:
768 bool: Whether string names a python module.
769 '''
770 return name in _PYTHON_STANDARD_MODULES
773_PYTHON_STANDARD_MODULES = [
774 '__future__',
775 '__main__',
776 '_dummy_thread',
777 '_thread',
778 'abc',
779 'aifc',
780 'argparse',
781 'array',
782 'ast',
783 'asynchat',
784 'asyncio',
785 'asyncore',
786 'atexit',
787 'audioop',
788 'base64',
789 'bdb',
790 'binascii',
791 'binhex',
792 'bisect',
793 'builtins',
794 'bz2',
795 'calendar',
796 'cgi',
797 'cgitb',
798 'chunk',
799 'cmath',
800 'cmd',
801 'code',
802 'codecs',
803 'codeop',
804 'collections',
805 'collections.abc',
806 'colorsys',
807 'compileall',
808 'concurrent',
809 'concurrent.futures',
810 'configparser',
811 'contextlib',
812 'contextvars',
813 'copy',
814 'copyreg',
815 'cProfile',
816 'crypt',
817 'csv',
818 'ctypes',
819 'curses',
820 'curses.ascii',
821 'curses.panel',
822 'curses.textpad',
823 'dataclasses',
824 'datetime',
825 'dbm',
826 'dbm.dumb',
827 'dbm.gnu',
828 'dbm.ndbm',
829 'decimal',
830 'difflib',
831 'dis',
832 'distutils',
833 'distutils.archive_util',
834 'distutils.bcppcompiler',
835 'distutils.ccompiler',
836 'distutils.cmd',
837 'distutils.command',
838 'distutils.command.bdist',
839 'distutils.command.bdist_dumb',
840 'distutils.command.bdist_msi',
841 'distutils.command.bdist_packager',
842 'distutils.command.bdist_rpm',
843 'distutils.command.bdist_wininst',
844 'distutils.command.build',
845 'distutils.command.build_clib',
846 'distutils.command.build_ext',
847 'distutils.command.build_py',
848 'distutils.command.build_scripts',
849 'distutils.command.check',
850 'distutils.command.clean',
851 'distutils.command.config',
852 'distutils.command.install',
853 'distutils.command.install_data',
854 'distutils.command.install_headers',
855 'distutils.command.install_lib',
856 'distutils.command.install_scripts',
857 'distutils.command.register',
858 'distutils.command.sdist',
859 'distutils.core',
860 'distutils.cygwinccompiler',
861 'distutils.debug',
862 'distutils.dep_util',
863 'distutils.dir_util',
864 'distutils.dist',
865 'distutils.errors',
866 'distutils.extension',
867 'distutils.fancy_getopt',
868 'distutils.file_util',
869 'distutils.filelist',
870 'distutils.log',
871 'distutils.msvccompiler',
872 'distutils.spawn',
873 'distutils.sysconfig',
874 'distutils.text_file',
875 'distutils.unixccompiler',
876 'distutils.util',
877 'distutils.version',
878 'doctest',
879 'dummy_threading',
880 'email',
881 'email.charset',
882 'email.contentmanager',
883 'email.encoders',
884 'email.errors',
885 'email.generator',
886 'email.header',
887 'email.headerregistry',
888 'email.iterators',
889 'email.message',
890 'email.mime',
891 'email.parser',
892 'email.policy',
893 'email.utils',
894 'encodings',
895 'encodings.idna',
896 'encodings.mbcs',
897 'encodings.utf_8_sig',
898 'ensurepip',
899 'enum',
900 'errno',
901 'faulthandler',
902 'fcntl',
903 'filecmp',
904 'fileinput',
905 'fnmatch',
906 'formatter',
907 'fractions',
908 'ftplib',
909 'functools',
910 'gc',
911 'getopt',
912 'getpass',
913 'gettext',
914 'glob',
915 'grp',
916 'gzip',
917 'hashlib',
918 'heapq',
919 'hmac',
920 'html',
921 'html.entities',
922 'html.parser',
923 'http',
924 'http.client',
925 'http.cookiejar',
926 'http.cookies',
927 'http.server',
928 'imaplib',
929 'imghdr',
930 'imp',
931 'importlib',
932 'importlib.abc',
933 'importlib.machinery',
934 'importlib.resources',
935 'importlib.util',
936 'inspect',
937 'io',
938 'ipaddress',
939 'itertools',
940 'json',
941 'json.tool',
942 'keyword',
943 'lib2to3',
944 'linecache',
945 'locale',
946 'logging',
947 'logging.config',
948 'logging.handlers',
949 'lzma',
950 'macpath',
951 'mailbox',
952 'mailcap',
953 'marshal',
954 'math',
955 'mimetypes',
956 'mmap',
957 'modulefinder',
958 'msilib',
959 'msvcrt',
960 'multiprocessing',
961 'multiprocessing.connection',
962 'multiprocessing.dummy',
963 'multiprocessing.managers',
964 'multiprocessing.pool',
965 'multiprocessing.sharedctypes',
966 'netrc',
967 'nis',
968 'nntplib',
969 'numbers',
970 'operator',
971 'optparse',
972 'os',
973 'os.path',
974 'ossaudiodev',
975 'parser',
976 'pathlib',
977 'pdb',
978 'pickle',
979 'pickletools',
980 'pipes',
981 'pkgutil',
982 'platform',
983 'plistlib',
984 'poplib',
985 'posix',
986 'pprint',
987 'profile',
988 'pstats',
989 'pty',
990 'pwd',
991 'py_compile',
992 'pyclbr',
993 'pydoc',
994 'queue',
995 'quopri',
996 'random',
997 're',
998 'readline',
999 'reprlib',
1000 'resource',
1001 'rlcompleter',
1002 'runpy',
1003 'sched',
1004 'secrets',
1005 'select',
1006 'selectors',
1007 'shelve',
1008 'shlex',
1009 'shutil',
1010 'signal',
1011 'site',
1012 'smtpd',
1013 'smtplib',
1014 'sndhdr',
1015 'socket',
1016 'socketserver',
1017 'spwd',
1018 'sqlite3',
1019 'ssl',
1020 'stat',
1021 'statistics',
1022 'string',
1023 'stringprep',
1024 'struct',
1025 'subprocess',
1026 'sunau',
1027 'symbol',
1028 'symtable',
1029 'sys',
1030 'sysconfig',
1031 'syslog',
1032 'tabnanny',
1033 'tarfile',
1034 'telnetlib',
1035 'tempfile',
1036 'termios',
1037 'test',
1038 'test.support',
1039 'test.support.script_helper',
1040 'textwrap',
1041 'threading',
1042 'time',
1043 'timeit',
1044 'tkinter',
1045 'tkinter.scrolledtext',
1046 'tkinter.tix',
1047 'tkinter.ttk',
1048 'token',
1049 'tokenize',
1050 'trace',
1051 'traceback',
1052 'tracemalloc',
1053 'tty',
1054 'turtle',
1055 'turtledemo',
1056 'types',
1057 'typing',
1058 'unicodedata',
1059 'unittest',
1060 'unittest.mock',
1061 'urllib',
1062 'urllib.error',
1063 'urllib.parse',
1064 'urllib.request',
1065 'urllib.response',
1066 'urllib.robotparser',
1067 'uu',
1068 'uuid',
1069 'venv',
1070 'warnings',
1071 'wave',
1072 'weakref',
1073 'webbrowser',
1074 'winreg',
1075 'winsound',
1076 'wsgiref',
1077 'wsgiref.handlers',
1078 'wsgiref.headers',
1079 'wsgiref.simple_server',
1080 'wsgiref.util',
1081 'wsgiref.validate',
1082 'xdrlib',
1083 'xml',
1084 'xml.dom',
1085 'xml.dom.minidom',
1086 'xml.dom.pulldom',
1087 'xml.etree.ElementTree',
1088 'xml.parsers.expat',
1089 'xml.parsers.expat.errors',
1090 'xml.parsers.expat.model',
1091 'xml.sax',
1092 'xml.sax.handler',
1093 'xml.sax.saxutils',
1094 'xml.sax.xmlreader',
1095 'xmlrpc',
1096 'xmlrpc.client',
1097 'xmlrpc.server',
1098 'zipapp',
1099 'zipfile',
1100 'zipimport',
1101 'zlib'
1102] # type: List[str]