]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
log,options: Add type annotations
authorBen Darnell <ben@bendarnell.com>
Sat, 11 Aug 2018 22:09:02 +0000 (18:09 -0400)
committerBen Darnell <ben@bendarnell.com>
Sat, 11 Aug 2018 22:09:02 +0000 (18:09 -0400)
setup.cfg
tornado/log.py
tornado/options.py
tornado/test/options_test.py

index 21eb046992893369d1b8806d57f3f714ed98ec07..9a33f099c84e9c3040aa80a3c7be35fe45abe501 100644 (file)
--- 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
index 46fcfde1342290c4285b9049fe66171c5f56e6f0..5948d43bab0b964b3e152bb90f2f3bd8aca0234e 100644 (file)
@@ -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;
index f2d30a6e8821383d4dacaec14553d5c7a50da2e9..5129fbf6fce2f53a02786074ada7120e5f5c60ac 100644 (file)
@@ -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 <unittest.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`
index e38db0800fdfd4b845634530eb4c65ece42fc928..6bd40dc1c4b97f4671a639019ad3f41c4cb2b31b 100644 (file)
@@ -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