Coverage for /home/ubuntu/lunchbox/python/lunchbox/tools.py: 100%
225 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 03:03 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 03:03 +0000
1from typing import Any, Callable, Dict, 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__)
21# ------------------------------------------------------------------------------
24'''
25A library of miscellaneous tools.
26'''
29def to_snakecase(string):
30 # type: (str) -> str
31 '''
32 Converts a given string to snake_case.
34 Args:
35 string (str): String to be converted.
37 Returns:
38 str: snake_case string.
39 '''
40 output = re.sub('([A-Z]+)', r'_\1', string)
41 output = re.sub('-', '_', output)
42 output = re.sub(r'\.', '_', output)
43 output = re.sub(' ', '_', output)
44 output = re.sub('_+', '_', output)
45 output = re.sub('^_|_$', '', output)
46 output = output.lower()
47 return output
50def try_(function, item, return_item='item'):
51 # type: (Callable[[Any], Any], Any, Any) -> Any
52 '''
53 Call given function on given item, catch any exceptions and return given
54 return item.
56 Args:
57 function (function): Function of signature lambda x: x.
58 item (object): Item used to call function.
59 return_item (object, optional): Item to be returned. Default: "item".
61 Returns:
62 object: Original item if return_item is "item".
63 Exception: If return_item is "error".
64 object: Object return by function call if return_item is not "item" or
65 "error".
66 '''
67 try:
68 return function(item)
69 except Exception as error:
70 if return_item == 'item':
71 return item
72 elif return_item == 'error':
73 return error
74 return return_item
77def get_ordered_unique(items):
78 # type: (List) -> List
79 '''
80 Generates a unique list of items in same order they were received in.
82 Args:
83 items (list): List of items.
85 Returns:
86 list: Unique ordered list.
87 '''
88 output = []
89 temp = set()
90 for item in items:
91 if item not in temp:
92 output.append(item)
93 temp.add(item)
94 return output
97def relative_path(module, path):
98 # type: (Union[str, Path], Union[str, Path]) -> Path
99 '''
100 Resolve path given current module's file path and given suffix.
102 Args:
103 module (str or Path): Always __file__ of current module.
104 path (str or Path): Path relative to __file__.
106 Returns:
107 Path: Resolved Path object.
108 '''
109 module_root = Path(module).parent
110 path_ = Path(path).parts # type: Any
111 path_ = list(dropwhile(lambda x: x == ".", path_))
112 up = len(list(takewhile(lambda x: x == "..", path_)))
113 path_ = Path(*path_[up:])
114 root = list(module_root.parents)[up - 1]
115 output = Path(root, path_).absolute()
117 LOGGER.debug(
118 f'relative_path called with: {module} and {path_}. Returned: {output}')
119 return output
122def truncate_list(items, size=3):
123 # type (list, int) -> list
124 '''
125 Truncates a given list to a given size, replaces the middle contents with
126 "...".
128 Args:
129 items (list): List of objects.
130 size (int, optional): Size of output list.
132 Raises:
133 EnforceError: If item is not a list.
134 EnforceError: If size is not an integer greater than -1.
136 Returns:
137 list: List of given size.
138 '''
139 Enforce(items, 'instance of', list, message='Items must be a list.')
140 msg = 'Size must be an integer greater than -1. Given value: {a}.'
141 Enforce(size, 'instance of', int, message=msg)
142 Enforce(size, '>', -1, message=msg)
143 # --------------------------------------------------------------------------
145 if len(items) <= size:
146 return items
147 if size == 0:
148 return []
149 if size == 1:
150 return items[:1]
151 if size == 2:
152 return [items[0], items[-1]]
154 output = items[:size - 2]
155 output.append('...')
156 output.append(items[-1])
157 return output
160def truncate_blob_lists(blob, size=3):
161 # type: (dict, int) -> dict
162 '''
163 Truncates lists inside given JSON blob to a given size.
165 Args:
166 blob (dict): Blob to be truncated.
167 size (int, optional): Size of lists. Default 3.
169 Raises:
170 EnforceError: If blob is not a dict.
172 Returns:
173 dict: Truncated blob.
174 '''
175 Enforce(blob, 'instance of', dict, message='Blob must be a dict.')
176 # --------------------------------------------------------------------------
178 def recurse_list(items, size):
179 output = []
180 for item in truncate_list(items, size=size):
181 if isinstance(item, dict):
182 item = recurse(item)
183 elif isinstance(item, list):
184 item = recurse_list(item, size)
185 output.append(item)
186 return output
188 def recurse(item):
189 output = {}
190 for k, v in item.items():
191 if isinstance(v, dict):
192 output[k] = recurse(v)
193 elif isinstance(v, list):
194 output[k] = recurse_list(v, size)
195 else:
196 output[k] = v
197 return output
198 return recurse(blob)
201# LOGGING-----------------------------------------------------------------------
202def log_runtime(
203 function, *args, message_=None, _testing=False, log_level='info', **kwargs
204):
205 # type (Callable, ..., Optional[str], bool, str, ...) -> Any
206 r'''
207 Logs the duration of given function called with given arguments.
209 Args:
210 function (function): Function to be called.
211 \*args (object, optional): Arguments.
212 message_ (str, optional): Message to be returned. Default: None.
213 _testing (bool, optional): Returns message if True. Default: False.
214 log_level (str, optional): Log level. Default: info.
215 \*\*kwargs (object, optional): Keyword arguments.
217 Raises:
218 EnforceError: If log level is illegal.
220 Returns:
221 object: function(*args, **kwargs).
222 '''
223 level = log_level_to_int(log_level)
225 # this may silently break file writes in multiprocessing
226 stopwatch = StopWatch()
227 stopwatch.start()
228 output = function(*args, **kwargs)
229 stopwatch.stop()
231 if message_ is not None:
232 message_ += f'\n Runtime: {stopwatch.human_readable_delta}'
233 else:
234 message_ = f'''{function.__name__}
235 Runtime: {stopwatch.human_readable_delta}
236 Args: {pformat(args)}
237 Kwargs: {pformat(kwargs)}'''
239 if _testing:
240 return message_
242 LOGGER.log(level, message_)
243 return output
246@wrapt.decorator
247def runtime(wrapped, instance, args, kwargs):
248 # type: (Callable, Any, Any, Any) -> Any
249 r'''
250 Decorator for logging the duration of given function called with given
251 arguments.
253 Args:
254 wrapped (function): Function to be called.
255 instance (object): Needed by wrapt.
256 \*args (object, optional): Arguments.
257 \*\*kwargs (object, optional): Keyword arguments.
259 Returns:
260 function: Wrapped function.
261 '''
262 return log_runtime(wrapped, *args, **kwargs)
265def log_level_to_int(level):
266 # type: (Union[str, int]) -> int
267 '''
268 Convert a given string or integer into a log level integer.
270 Args:
271 level (str or int): Log level.
273 Raises:
274 EnforceError: If level is illegal.
276 Returns:
277 int: Log level as integer.
278 '''
279 keys = ['critical', 'debug', 'error', 'fatal', 'info', 'warn', 'warning']
280 values = [getattr(logging, x.upper()) for x in keys]
281 lut = dict(zip(keys, values)) # type: Dict[str, int]
283 msg = 'Log level must be an integer or string. Given value: {a}. '
284 lut_msg = ', '.join([f'{k}: {v}' for k, v in zip(keys, values)])
285 msg += f'Legal values: [{lut_msg}].'
287 output = 0
288 if isinstance(level, int):
289 Enforce(level, 'in', values, message=msg)
290 output = level
292 elif isinstance(level, str):
293 level = level.lower()
294 Enforce(level, 'in', keys, message=msg)
295 output = lut[level]
297 else:
298 raise EnforceError(msg.format(a=level))
300 return output
303class LogRuntime:
304 '''
305 LogRuntime is a class for logging the runtime of arbitrary code.
307 Attributes:
308 message (str): Logging message with runtime line.
309 delta (datetime.timedelta): Runtime.
310 human_readable_delta (str): Runtime in human readable format.
312 Example:
314 >>> import time
315 >>> def foobar():
316 time.sleep(1)
318 >>> with LogRuntime('Foo the bars', name=foobar.__name__, level='debug'):
319 foobar()
320 DEBUG:foobar:Foo the bars - Runtime: 0:00:01.001069 (1 second)
322 >>> with LogRuntime(message='Fooing all the bars', suppress=True) as log:
323 foobar()
324 >>> print(log.message)
325 Fooing all the bars - Runtime: 0:00:01.001069 (1 second)
326 '''
327 def __init__(
328 self,
329 message='', # type: str
330 name='LogRuntime', # type: str
331 level='info', # type: str
332 suppress=False, # type: bool
333 message_func=None, # type: Optional[Callable[[str, StopWatch], None]]
334 callback=None, # type: Optional[Callable[[str], Any]]
335 ):
336 # type: (...) -> None
337 '''
338 Constructs a LogRuntime instance.
340 Args:
341 message (str, optional): Logging message. Default: ''.
342 name (str, optional): Name of logger. Default: 'LogRuntime'.
343 level (str or int, optional): Log level. Default: info.
344 suppress (bool, optional): Whether to suppress logging.
345 Default: False.
346 message_func (function, optional): Custom message function of the
347 signature (message, StopWatch) -> str. Default: None.
348 callback (function, optional): Callback function of the signature
349 (message) -> Any. Default: None.
351 Raises:
352 EnforceError: If message is not a string.
353 EnforceError: If name is not a string.
354 EnforceError: If level is not legal logging level.
355 EnforceError: If suppress is not a boolean.
356 '''
357 Enforce(message, 'instance of', str)
358 Enforce(name, 'instance of', str)
359 Enforce(suppress, 'instance of', bool)
360 # ----------------------------------------------------------------------
362 self._message = message
363 self._stopwatch = StopWatch()
364 self._logger = logging.getLogger(name)
365 self._level = log_level_to_int(level)
366 self._suppress = suppress
367 self._message_func = message_func
368 self._callback = callback
370 @staticmethod
371 def _default_message_func(message, stopwatch):
372 # type: (str, StopWatch) -> str
373 '''
374 Add runtime information to message given StopWatch instance.
376 Args:
377 message (str): Message.
378 stopwatch (StopWatch): StopWatch instance.
380 Raises:
381 EnforeceError: If Message is not a string.
382 EnforceError: If stopwatch is not a StopWatch instance.
384 Returns:
385 str: Message with runtime information.
386 '''
387 Enforce(message, 'instance of', str)
388 Enforce(stopwatch, 'instance of', StopWatch)
389 # ----------------------------------------------------------------------
391 msg = f'Runtime: {stopwatch.delta} '
392 msg += f'({stopwatch.human_readable_delta})'
393 if message != '':
394 msg = message + ' - ' + msg
395 return msg
397 def __enter__(self):
398 # type: () -> LogRuntime
399 '''
400 Starts stopwatch.
402 Returns:
403 LogRuntime: self.
404 '''
405 self._stopwatch.start()
406 return self
408 def __exit__(self, *args):
409 # type: (Any) -> None
410 '''
411 Stops stopwatch and logs message.
412 '''
413 stopwatch = self._stopwatch
414 stopwatch.stop()
415 self.delta = self._stopwatch.delta
416 self.human_readable_delta = self._stopwatch.human_readable_delta
418 msg_func = self._message_func or self._default_message_func
419 self.message = msg_func(self._message, stopwatch)
421 if not self._suppress:
422 self._logger.log(self._level, self.message)
424 if self._callback is not None:
425 self._callback(str(self.message))
428# HTTP-REQUESTS-----------------------------------------------------------------
429def post_to_slack(url, channel, message):
430 # type (str, str, str) -> urllib.request.HttpResponse
431 '''
432 Post a given message to a given slack channel.
434 Args:
435 url (str): https://hooks.slack.com/services URL.
436 channel (str): Channel name.
437 message (str): Message to be posted.
439 Raises:
440 EnforceError: If URL is not a string.
441 EnforceError: If URL does not start with https://hooks.slack.com/services
442 EnforceError: If channel is not a string.
443 EnforceError: If message is not a string.
445 Returns:
446 HTTPResponse: Response.
447 '''
448 Enforce(url, 'instance of', str)
449 Enforce(channel, 'instance of', str)
450 Enforce(message, 'instance of', str)
451 msg = 'URL must begin with https://hooks.slack.com/services/. '
452 msg += f'Given URL: {url}'
453 Enforce(
454 url.startswith('https://hooks.slack.com/services/'), '==', True,
455 message=msg
456 )
457 # --------------------------------------------------------------------------
459 request = urllib.request.Request(
460 url,
461 method='POST',
462 headers={'Content-type': 'application/json'},
463 data=json.dumps(dict(
464 channel='#' + channel,
465 text=message,
466 )).encode(),
467 )
468 return urllib.request.urlopen(request)
471# API---------------------------------------------------------------------------
472def get_function_signature(function):
473 # type: (Callable) -> Dict
474 '''
475 Inspect a given function and return its arguments as a list and its keyword
476 arguments as a dict.
478 Args:
479 function (function): Function to be inspected.
481 Returns:
482 dict: args and kwargs.
483 '''
484 spec = inspect.getfullargspec(function)
485 args = list(spec.args)
486 kwargs = {} # type: Any
487 if spec.defaults is not None:
488 args = args[:-len(spec.defaults)]
489 kwargs = list(spec.args)[-len(spec.defaults):]
490 kwargs = dict(zip(kwargs, spec.defaults))
491 return dict(args=args, kwargs=kwargs)
494def _dir_table(obj, public=True, semiprivate=True, private=False, max_width=100):
495 # type: (Any, bool, bool, bool, int) -> str
496 '''
497 Create a table from results of calling dir(obj).
499 Args:
500 obj (object): Object to call dir on.
501 public (bool, optional): Include public attributes in table.
502 Default: True.
503 semiprivate (bool, optional): Include semiprivate attributes in table.
504 Default: True.
505 private (bool, optional): Include private attributes in table.
506 Default: False.
507 max_width (int, optional): Maximum table width: Default: 100.
509 Returns:
510 str: Table.
511 '''
512 dirkeys = dir(obj)
513 pub = set(list(filter(lambda x: re.search('^[^_]', x), dirkeys)))
514 priv = set(list(filter(lambda x: re.search('^__|^_[A-Z].*__', x), dirkeys)))
515 semipriv = set(dirkeys).difference(pub).difference(priv)
517 keys = set()
518 if public:
519 keys.update(pub)
520 if private:
521 keys.update(priv)
522 if semiprivate:
523 keys.update(semipriv)
525 data = [dict(key='NAME', atype='TYPE', val='VALUE')]
526 max_key = 0
527 max_atype = 0
528 for key in sorted(keys):
529 attr = getattr(obj, key)
530 atype = attr.__class__.__name__
531 val = str(attr).replace('\n', ' ')
532 max_key = max(max_key, len(key))
533 max_atype = max(max_atype, len(atype))
534 row = dict(key=key, atype=atype, val=val)
535 data.append(row)
537 pattern = f'{{key:<{max_key}}} {{atype:<{max_atype}}} {{val}}'
538 lines = list(map(lambda x: pattern.format(**x)[:max_width], data))
539 output = '\n'.join(lines)
540 return output
543def dir_table(obj, public=True, semiprivate=True, private=False, max_width=100):
544 # type: (Any, bool, bool, bool, int) -> None
545 '''
546 Prints a table from results of calling dir(obj).
548 Args:
549 obj (object): Object to call dir on.
550 public (bool, optional): Include public attributes in table.
551 Default: True.
552 semiprivate (bool, optional): Include semiprivate attributes in table.
553 Default: True.
554 private (bool, optional): Include private attributes in table.
555 Default: False.
556 max_width (int, optional): Maximum table width: Default: 100.
557 '''
558 print(_dir_table(
559 obj,
560 public=public,
561 semiprivate=semiprivate,
562 private=private,
563 max_width=max_width,
564 )) # pragma: no cover
567def api_function(wrapped=None, **kwargs):
568 # type: (Optional[Callable], Any) -> Callable
569 r'''
570 A decorator that enforces keyword argument only function signatures and
571 required keyword argument values when called.
573 Args:
574 wrapped (function): For dev use. Default: None.
575 \*\*kwargs (dict): Keyword arguments. # noqa: W605
577 Raises:
578 TypeError: If non-keyword argument found in functionn signature.
579 ValueError: If keyword arg with value of '<required>' is found.
581 Returns:
582 api function.
583 '''
584 @wrapt.decorator
585 def wrapper(wrapped, instance, args, kwargs):
586 sig = get_function_signature(wrapped)
588 # ensure no arguments are present
589 if len(sig['args']) > 0:
590 msg = 'Function may only have keyword arguments. '
591 msg += f"Found non-keyword arguments: {sig['args']}."
592 raise TypeError(msg)
594 # ensure all required kwarg values are present
595 params = sig['kwargs']
596 params.update(kwargs)
597 for key, val in params.items():
598 if val == '<required>':
599 msg = f'Missing required keyword argument: {key}.'
600 raise ValueError(msg)
602 LOGGER.debug(f'{wrapped} called with {params}.')
603 return wrapped(*args, **kwargs)
604 return wrapper(wrapped)
605# ------------------------------------------------------------------------------
608def is_standard_module(name):
609 # type: (str) -> bool
610 '''
611 Determines if given module name is a python builtin.
613 Args:
614 name (str): Python module name.
616 Returns:
617 bool: Whether string names a python module.
618 '''
619 return name in _PYTHON_STANDARD_MODULES
622_PYTHON_STANDARD_MODULES = [
623 '__future__',
624 '__main__',
625 '_dummy_thread',
626 '_thread',
627 'abc',
628 'aifc',
629 'argparse',
630 'array',
631 'ast',
632 'asynchat',
633 'asyncio',
634 'asyncore',
635 'atexit',
636 'audioop',
637 'base64',
638 'bdb',
639 'binascii',
640 'binhex',
641 'bisect',
642 'builtins',
643 'bz2',
644 'calendar',
645 'cgi',
646 'cgitb',
647 'chunk',
648 'cmath',
649 'cmd',
650 'code',
651 'codecs',
652 'codeop',
653 'collections',
654 'collections.abc',
655 'colorsys',
656 'compileall',
657 'concurrent',
658 'concurrent.futures',
659 'configparser',
660 'contextlib',
661 'contextvars',
662 'copy',
663 'copyreg',
664 'cProfile',
665 'crypt',
666 'csv',
667 'ctypes',
668 'curses',
669 'curses.ascii',
670 'curses.panel',
671 'curses.textpad',
672 'dataclasses',
673 'datetime',
674 'dbm',
675 'dbm.dumb',
676 'dbm.gnu',
677 'dbm.ndbm',
678 'decimal',
679 'difflib',
680 'dis',
681 'distutils',
682 'distutils.archive_util',
683 'distutils.bcppcompiler',
684 'distutils.ccompiler',
685 'distutils.cmd',
686 'distutils.command',
687 'distutils.command.bdist',
688 'distutils.command.bdist_dumb',
689 'distutils.command.bdist_msi',
690 'distutils.command.bdist_packager',
691 'distutils.command.bdist_rpm',
692 'distutils.command.bdist_wininst',
693 'distutils.command.build',
694 'distutils.command.build_clib',
695 'distutils.command.build_ext',
696 'distutils.command.build_py',
697 'distutils.command.build_scripts',
698 'distutils.command.check',
699 'distutils.command.clean',
700 'distutils.command.config',
701 'distutils.command.install',
702 'distutils.command.install_data',
703 'distutils.command.install_headers',
704 'distutils.command.install_lib',
705 'distutils.command.install_scripts',
706 'distutils.command.register',
707 'distutils.command.sdist',
708 'distutils.core',
709 'distutils.cygwinccompiler',
710 'distutils.debug',
711 'distutils.dep_util',
712 'distutils.dir_util',
713 'distutils.dist',
714 'distutils.errors',
715 'distutils.extension',
716 'distutils.fancy_getopt',
717 'distutils.file_util',
718 'distutils.filelist',
719 'distutils.log',
720 'distutils.msvccompiler',
721 'distutils.spawn',
722 'distutils.sysconfig',
723 'distutils.text_file',
724 'distutils.unixccompiler',
725 'distutils.util',
726 'distutils.version',
727 'doctest',
728 'dummy_threading',
729 'email',
730 'email.charset',
731 'email.contentmanager',
732 'email.encoders',
733 'email.errors',
734 'email.generator',
735 'email.header',
736 'email.headerregistry',
737 'email.iterators',
738 'email.message',
739 'email.mime',
740 'email.parser',
741 'email.policy',
742 'email.utils',
743 'encodings',
744 'encodings.idna',
745 'encodings.mbcs',
746 'encodings.utf_8_sig',
747 'ensurepip',
748 'enum',
749 'errno',
750 'faulthandler',
751 'fcntl',
752 'filecmp',
753 'fileinput',
754 'fnmatch',
755 'formatter',
756 'fractions',
757 'ftplib',
758 'functools',
759 'gc',
760 'getopt',
761 'getpass',
762 'gettext',
763 'glob',
764 'grp',
765 'gzip',
766 'hashlib',
767 'heapq',
768 'hmac',
769 'html',
770 'html.entities',
771 'html.parser',
772 'http',
773 'http.client',
774 'http.cookiejar',
775 'http.cookies',
776 'http.server',
777 'imaplib',
778 'imghdr',
779 'imp',
780 'importlib',
781 'importlib.abc',
782 'importlib.machinery',
783 'importlib.resources',
784 'importlib.util',
785 'inspect',
786 'io',
787 'ipaddress',
788 'itertools',
789 'json',
790 'json.tool',
791 'keyword',
792 'lib2to3',
793 'linecache',
794 'locale',
795 'logging',
796 'logging.config',
797 'logging.handlers',
798 'lzma',
799 'macpath',
800 'mailbox',
801 'mailcap',
802 'marshal',
803 'math',
804 'mimetypes',
805 'mmap',
806 'modulefinder',
807 'msilib',
808 'msvcrt',
809 'multiprocessing',
810 'multiprocessing.connection',
811 'multiprocessing.dummy',
812 'multiprocessing.managers',
813 'multiprocessing.pool',
814 'multiprocessing.sharedctypes',
815 'netrc',
816 'nis',
817 'nntplib',
818 'numbers',
819 'operator',
820 'optparse',
821 'os',
822 'os.path',
823 'ossaudiodev',
824 'parser',
825 'pathlib',
826 'pdb',
827 'pickle',
828 'pickletools',
829 'pipes',
830 'pkgutil',
831 'platform',
832 'plistlib',
833 'poplib',
834 'posix',
835 'pprint',
836 'profile',
837 'pstats',
838 'pty',
839 'pwd',
840 'py_compile',
841 'pyclbr',
842 'pydoc',
843 'queue',
844 'quopri',
845 'random',
846 're',
847 'readline',
848 'reprlib',
849 'resource',
850 'rlcompleter',
851 'runpy',
852 'sched',
853 'secrets',
854 'select',
855 'selectors',
856 'shelve',
857 'shlex',
858 'shutil',
859 'signal',
860 'site',
861 'smtpd',
862 'smtplib',
863 'sndhdr',
864 'socket',
865 'socketserver',
866 'spwd',
867 'sqlite3',
868 'ssl',
869 'stat',
870 'statistics',
871 'string',
872 'stringprep',
873 'struct',
874 'subprocess',
875 'sunau',
876 'symbol',
877 'symtable',
878 'sys',
879 'sysconfig',
880 'syslog',
881 'tabnanny',
882 'tarfile',
883 'telnetlib',
884 'tempfile',
885 'termios',
886 'test',
887 'test.support',
888 'test.support.script_helper',
889 'textwrap',
890 'threading',
891 'time',
892 'timeit',
893 'tkinter',
894 'tkinter.scrolledtext',
895 'tkinter.tix',
896 'tkinter.ttk',
897 'token',
898 'tokenize',
899 'trace',
900 'traceback',
901 'tracemalloc',
902 'tty',
903 'turtle',
904 'turtledemo',
905 'types',
906 'typing',
907 'unicodedata',
908 'unittest',
909 'unittest.mock',
910 'urllib',
911 'urllib.error',
912 'urllib.parse',
913 'urllib.request',
914 'urllib.response',
915 'urllib.robotparser',
916 'uu',
917 'uuid',
918 'venv',
919 'warnings',
920 'wave',
921 'weakref',
922 'webbrowser',
923 'winreg',
924 'winsound',
925 'wsgiref',
926 'wsgiref.handlers',
927 'wsgiref.headers',
928 'wsgiref.simple_server',
929 'wsgiref.util',
930 'wsgiref.validate',
931 'xdrlib',
932 'xml',
933 'xml.dom',
934 'xml.dom.minidom',
935 'xml.dom.pulldom',
936 'xml.etree.ElementTree',
937 'xml.parsers.expat',
938 'xml.parsers.expat.errors',
939 'xml.parsers.expat.model',
940 'xml.sax',
941 'xml.sax.handler',
942 'xml.sax.saxutils',
943 'xml.sax.xmlreader',
944 'xmlrpc',
945 'xmlrpc.client',
946 'xmlrpc.server',
947 'zipapp',
948 'zipfile',
949 'zipimport',
950 'zlib'
951] # type: List[str]