From: Ben Darnell Date: Sat, 11 Aug 2018 22:09:02 +0000 (-0400) Subject: log,options: Add type annotations X-Git-Tag: v6.0.0b1~33^2~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=75bdedcb44d20d598842914c28228a054707b2aa;p=thirdparty%2Ftornado.git log,options: Add type annotations --- diff --git a/setup.cfg b/setup.cfg index 21eb04699..9a33f099c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,12 @@ disallow_untyped_defs = True [mypy-tornado.locale] disallow_untyped_defs = True +[mypy-tornado.log] +disallow_untyped_defs = True + +[mypy-tornado.options] +disallow_untyped_defs = True + [mypy-tornado.testing] disallow_untyped_defs = True @@ -45,5 +51,11 @@ check_untyped_defs = True [mypy-tornado.test.locale_test] check_untyped_defs = True +[mypy-tornado.test.log_test] +check_untyped_defs = True + +[mypy-tornado.test.options_test] +check_untyped_defs = True + [mypy-tornado.test.testing_test] check_untyped_defs = True diff --git a/tornado/log.py b/tornado/log.py index 46fcfde13..5948d43ba 100644 --- a/tornado/log.py +++ b/tornado/log.py @@ -40,17 +40,19 @@ except ImportError: colorama = None try: - import curses # type: ignore + import curses except ImportError: curses = None # type: ignore +from typing import Dict, Any, cast + # Logger objects for internal tornado use access_log = logging.getLogger("tornado.access") app_log = logging.getLogger("tornado.application") gen_log = logging.getLogger("tornado.general") -def _stderr_supports_color(): +def _stderr_supports_color() -> bool: try: if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): if curses: @@ -68,7 +70,7 @@ def _stderr_supports_color(): return False -def _safe_unicode(s): +def _safe_unicode(s: Any) -> str: try: return _unicode(s) except UnicodeDecodeError: @@ -109,8 +111,8 @@ class LogFormatter(logging.Formatter): logging.ERROR: 1, # Red } - def __init__(self, fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, - style='%', color=True, colors=DEFAULT_COLORS): + def __init__(self, fmt: str=DEFAULT_FORMAT, datefmt: str=DEFAULT_DATE_FORMAT, + style: str='%', color: bool=True, colors: Dict[int, int]=DEFAULT_COLORS) -> None: r""" :arg bool color: Enables color support. :arg str fmt: Log message format. @@ -129,22 +131,16 @@ class LogFormatter(logging.Formatter): logging.Formatter.__init__(self, datefmt=datefmt) self._fmt = fmt - self._colors = {} + self._colors = {} # type: Dict[int, str] if color and _stderr_supports_color(): if curses is not None: - # The curses module has some str/bytes confusion in - # python3. Until version 3.2.3, most methods return - # bytes, but only accept strings. In addition, we want to - # output these strings with the logging module, which - # works with unicode strings. The explicit calls to - # unicode() below are harmless in python2 but will do the - # right conversion in python 3. fg_color = (curses.tigetstr("setaf") or - curses.tigetstr("setf") or "") - if (3, 0) < sys.version_info < (3, 2, 3): - fg_color = unicode_type(fg_color, "ascii") + curses.tigetstr("setf") or b"") for levelno, code in colors.items(): + # Convert the terminal control characters from + # bytes to unicode strings for easier use with the + # logging module. self._colors[levelno] = unicode_type(curses.tparm(fg_color, code), "ascii") self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii") else: @@ -156,7 +152,7 @@ class LogFormatter(logging.Formatter): else: self._normal = '' - def format(self, record): + def format(self, record: Any) -> str: try: message = record.getMessage() assert isinstance(message, basestring_type) # guaranteed by logging @@ -180,7 +176,7 @@ class LogFormatter(logging.Formatter): except Exception as e: record.message = "Bad message (%r): %r" % (e, record.__dict__) - record.asctime = self.formatTime(record, self.datefmt) + record.asctime = self.formatTime(record, cast(str, self.datefmt)) if record.levelno in self._colors: record.color = self._colors[record.levelno] @@ -203,7 +199,8 @@ class LogFormatter(logging.Formatter): return formatted.replace("\n", "\n ") -def enable_pretty_logging(options=None, logger=None): +def enable_pretty_logging(options: Any=None, + logger: logging.Logger=None) -> None: """Turns on formatted logging output as configured. This is called automatically by `tornado.options.parse_command_line` @@ -223,7 +220,7 @@ def enable_pretty_logging(options=None, logger=None): channel = logging.handlers.RotatingFileHandler( filename=options.log_file_prefix, maxBytes=options.log_file_max_size, - backupCount=options.log_file_num_backups) + backupCount=options.log_file_num_backups) # type: logging.Handler elif rotate_mode == 'time': channel = logging.handlers.TimedRotatingFileHandler( filename=options.log_file_prefix, @@ -245,7 +242,7 @@ def enable_pretty_logging(options=None, logger=None): logger.addHandler(channel) -def define_logging_options(options=None): +def define_logging_options(options: Any=None) -> None: """Add logging-related flags to ``options``. These options are present automatically on the default options instance; diff --git a/tornado/options.py b/tornado/options.py index f2d30a6e8..5129fbf6f 100644 --- a/tornado/options.py +++ b/tornado/options.py @@ -104,6 +104,12 @@ from tornado.escape import _unicode, native_str from tornado.log import define_logging_options from tornado.util import basestring_type, exec_in +import typing +from typing import Any, Iterator, Iterable, Tuple, Set, Dict, Callable, List, TextIO + +if typing.TYPE_CHECKING: + from typing import Optional # noqa: F401 + class Error(Exception): """Exception raised by errors in the options module.""" @@ -116,56 +122,56 @@ class OptionParser(object): Normally accessed via static functions in the `tornado.options` module, which reference a global instance. """ - def __init__(self): + def __init__(self) -> None: # we have to use self.__dict__ because we override setattr. self.__dict__['_options'] = {} self.__dict__['_parse_callbacks'] = [] self.define("help", type=bool, help="show this help information", callback=self._help_callback) - def _normalize_name(self, name): + def _normalize_name(self, name: str) -> str: return name.replace('_', '-') - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: name = self._normalize_name(name) if isinstance(self._options.get(name), _Option): return self._options[name].value() raise AttributeError("Unrecognized option %r" % name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: name = self._normalize_name(name) if isinstance(self._options.get(name), _Option): return self._options[name].set(value) raise AttributeError("Unrecognized option %r" % name) - def __iter__(self): + def __iter__(self) -> Iterator: return (opt.name for opt in self._options.values()) - def __contains__(self, name): + def __contains__(self, name: str) -> bool: name = self._normalize_name(name) return name in self._options - def __getitem__(self, name): + def __getitem__(self, name: str) -> Any: return self.__getattr__(name) - def __setitem__(self, name, value): + def __setitem__(self, name: str, value: Any) -> None: return self.__setattr__(name, value) - def items(self): - """A sequence of (name, value) pairs. + def items(self) -> Iterable[Tuple[str, Any]]: + """An iterable of (name, value) pairs. .. versionadded:: 3.1 """ return [(opt.name, opt.value()) for name, opt in self._options.items()] - def groups(self): + def groups(self) -> Set[str]: """The set of option-groups created by ``define``. .. versionadded:: 3.1 """ return set(opt.group_name for opt in self._options.values()) - def group_dict(self, group): + def group_dict(self, group: str) -> Dict[str, Any]: """The names and values of options in a group. Useful for copying options into Application settings:: @@ -186,7 +192,7 @@ class OptionParser(object): (opt.name, opt.value()) for name, opt in self._options.items() if not group or group == opt.group_name) - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """The names and values of all options. .. versionadded:: 3.1 @@ -194,8 +200,9 @@ class OptionParser(object): return dict( (opt.name, opt.value()) for name, opt in self._options.items()) - def define(self, name, default=None, type=None, help=None, metavar=None, - multiple=False, group=None, callback=None): + def define(self, name: str, default: Any=None, type: type=None, + help: str=None, metavar: str=None, + multiple: bool=False, group: str=None, callback: Callable[[Any], None]=None) -> None: """Defines a new command line option. ``type`` can be any of `str`, `int`, `float`, `bool`, @@ -252,7 +259,7 @@ class OptionParser(object): else: type = str if group: - group_name = group + group_name = group # type: Optional[str] else: group_name = file_name option = _Option(name, file_name=file_name, @@ -262,7 +269,7 @@ class OptionParser(object): callback=callback) self._options[normalized] = option - def parse_command_line(self, args=None, final=True): + def parse_command_line(self, args: List[str]=None, final: bool=True) -> List[str]: """Parses all options given on the command line (defaults to `sys.argv`). @@ -286,7 +293,7 @@ class OptionParser(object): """ if args is None: args = sys.argv - remaining = [] + remaining = [] # type: List[str] for i in range(1, len(args)): # All things after the last option are command line arguments if not args[i].startswith("-"): @@ -314,7 +321,7 @@ class OptionParser(object): return remaining - def parse_config_file(self, path, final=True): + def parse_config_file(self, path: str, final: bool=True) -> None: """Parses and loads the config file at the given path. The config file contains Python code that will be executed (so @@ -381,13 +388,13 @@ class OptionParser(object): if final: self.run_parse_callbacks() - def print_help(self, file=None): + def print_help(self, file: TextIO=None) -> None: """Prints all the command line options to stderr (or another file).""" if file is None: file = sys.stderr print("Usage: %s [OPTIONS]" % sys.argv[0], file=file) print("\nOptions:\n", file=file) - by_group = {} + by_group = {} # type: Dict[str, List[_Option]] for option in self._options.values(): by_group.setdefault(option.group_name, []).append(option) @@ -411,20 +418,20 @@ class OptionParser(object): print("%-34s %s" % (' ', line), file=file) print(file=file) - def _help_callback(self, value): + def _help_callback(self, value: bool) -> None: if value: self.print_help() sys.exit(0) - def add_parse_callback(self, callback): + def add_parse_callback(self, callback: Callable[[], None]) -> None: """Adds a parse callback, to be invoked when option parsing is done.""" self._parse_callbacks.append(callback) - def run_parse_callbacks(self): + def run_parse_callbacks(self) -> None: for callback in self._parse_callbacks: callback() - def mockable(self): + def mockable(self) -> '_Mockable': """Returns a wrapper around self that is compatible with `mock.patch `. @@ -454,32 +461,37 @@ class _Mockable(object): _Mockable's getattr and setattr pass through to the underlying OptionParser, and delattr undoes the effect of a previous setattr. """ - def __init__(self, options): + def __init__(self, options: OptionParser) -> None: # Modify __dict__ directly to bypass __setattr__ self.__dict__['_options'] = options self.__dict__['_originals'] = {} - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: return getattr(self._options, name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: assert name not in self._originals, "don't reuse mockable objects" self._originals[name] = getattr(self._options, name) setattr(self._options, name, value) - def __delattr__(self, name): + def __delattr__(self, name: str) -> None: setattr(self._options, name, self._originals.pop(name)) class _Option(object): + # This class could almost be made generic, but the way the types + # interact with the multiple argument makes this tricky. (default + # and the callback use List[T], but type is still Type[T]). UNSET = object() - def __init__(self, name, default=None, type=basestring_type, help=None, - metavar=None, multiple=False, file_name=None, group_name=None, - callback=None): + def __init__(self, name: str, default: Any=None, type: type=None, + help: str=None, metavar: str=None, multiple: bool=False, file_name: str=None, + group_name: str=None, callback: Callable[[Any], None]=None) -> None: if default is None and multiple: default = [] self.name = name + if type is None: + raise ValueError("type must not be None") self.type = type self.help = help self.metavar = metavar @@ -488,26 +500,26 @@ class _Option(object): self.group_name = group_name self.callback = callback self.default = default - self._value = _Option.UNSET + self._value = _Option.UNSET # type: Any - def value(self): + def value(self) -> Any: return self.default if self._value is _Option.UNSET else self._value - def parse(self, value): + def parse(self, value: str) -> Any: _parse = { datetime.datetime: self._parse_datetime, datetime.timedelta: self._parse_timedelta, bool: self._parse_bool, basestring_type: self._parse_string, - }.get(self.type, self.type) + }.get(self.type, self.type) # type: Callable[[str], Any] if self.multiple: self._value = [] for part in value.split(","): if issubclass(self.type, numbers.Integral): # allow ranges of the form X:Y (inclusive at both ends) - lo, _, hi = part.partition(":") - lo = _parse(lo) - hi = _parse(hi) if hi else lo + lo_str, _, hi_str = part.partition(":") + lo = _parse(lo_str) + hi = _parse(hi_str) if hi_str else lo self._value.extend(range(lo, hi + 1)) else: self._value.append(_parse(part)) @@ -517,7 +529,7 @@ class _Option(object): self.callback(self._value) return self.value() - def set(self, value): + def set(self, value: Any) -> None: if self.multiple: if not isinstance(value, list): raise Error("Option %r is required to be a list of %s" % @@ -548,7 +560,7 @@ class _Option(object): "%H:%M", ] - def _parse_datetime(self, value): + def _parse_datetime(self, value: str) -> datetime.datetime: for format in self._DATETIME_FORMATS: try: return datetime.datetime.strptime(value, format) @@ -573,7 +585,7 @@ class _Option(object): _TIMEDELTA_PATTERN = re.compile( r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE) - def _parse_timedelta(self, value): + def _parse_timedelta(self, value: str) -> datetime.timedelta: try: sum = datetime.timedelta() start = 0 @@ -590,10 +602,10 @@ class _Option(object): except Exception: raise - def _parse_bool(self, value): + def _parse_bool(self, value: str) -> bool: return value.lower() not in ("false", "0", "f") - def _parse_string(self, value): + def _parse_string(self, value: str) -> str: return _unicode(value) @@ -604,8 +616,9 @@ All defined options are available as attributes on this object. """ -def define(name, default=None, type=None, help=None, metavar=None, - multiple=False, group=None, callback=None): +def define(name: str, default: Any=None, type: type=None, help: str=None, + metavar: str=None, multiple: bool=False, group: str=None, + callback: Callable[[Any], None]=None) -> None: """Defines an option in the global namespace. See `OptionParser.define`. @@ -615,7 +628,7 @@ def define(name, default=None, type=None, help=None, metavar=None, callback=callback) -def parse_command_line(args=None, final=True): +def parse_command_line(args: List[str]=None, final: bool=True) -> List[str]: """Parses global options from the command line. See `OptionParser.parse_command_line`. @@ -623,7 +636,7 @@ def parse_command_line(args=None, final=True): return options.parse_command_line(args, final=final) -def parse_config_file(path, final=True): +def parse_config_file(path: str, final: bool=True) -> None: """Parses global options from a config file. See `OptionParser.parse_config_file`. @@ -631,7 +644,7 @@ def parse_config_file(path, final=True): return options.parse_config_file(path, final=final) -def print_help(file=None): +def print_help(file: TextIO=None) -> None: """Prints all the command line options to stderr (or another file). See `OptionParser.print_help`. @@ -639,7 +652,7 @@ def print_help(file=None): return options.print_help(file) -def add_parse_callback(callback): +def add_parse_callback(callback: Callable[[], None]) -> None: """Adds a parse callback, to be invoked when option parsing is done. See `OptionParser.add_parse_callback` diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py index e38db0800..6bd40dc1c 100644 --- a/tornado/test/options_test.py +++ b/tornado/test/options_test.py @@ -10,6 +10,10 @@ from tornado.options import OptionParser, Error from tornado.util import basestring_type from tornado.test.util import subTest +import typing +if typing.TYPE_CHECKING: + from typing import List # noqa: F401 + class Email(object): def __init__(self, value): @@ -113,7 +117,7 @@ class OptionsTest(unittest.TestCase): options.foo = '2' def test_setattr_with_callback(self): - values = [] + values = [] # type: List[int] options = OptionParser() options.define('foo', default=1, type=int, callback=values.append) options.foo = 2