diff --git a/tornado/concurrent.py b/tornado/concurrent.py
index b7e8218a..12c7c9f3 100644
--- a/tornado/concurrent.py
+++ b/tornado/concurrent.py
@@ -26,7 +26,19 @@ Future = asyncio.Future
FUTURES = (futures.Future, Future)
class DummyExecutor(futures.Executor):
- if sys.version_info >= (3, 9):
+ def submit(self, fn, *args, **kwargs):
+ future = Future()
+ try:
+ result = fn(*args, **kwargs)
+ except Exception as e:
+ future.set_exception(e)
+ else:
+ future.set_result(result)
+ return future
+
+ def shutdown(self, wait=True):
+ pass
+
dummy_executor = DummyExecutor()
def run_on_executor(*args: Any, **kwargs: Any) -> Callable:
@@ -69,9 +81,30 @@ def run_on_executor(*args: Any, **kwargs: Any) -> Callable:
The ``callback`` argument was removed.
"""
- pass
+ def run_on_executor_decorator(fn: Callable) -> Callable:
+ executor = kwargs.get("executor", "executor")
+
+ @functools.wraps(fn)
+ async def wrapper(self, *args, **kwargs):
+ executor_obj = getattr(self, executor, dummy_executor)
+ future = executor_obj.submit(fn, self, *args, **kwargs)
+ return await asyncio.wrap_future(future)
+
+ return wrapper
+
+ if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
+ return run_on_executor_decorator(args[0])
+ return run_on_executor_decorator
_NO_RESULT = object()
+def is_future(x: Any) -> bool:
+ """Returns True if ``x`` is a `.Future`.
+
+ This should work both for Tornado/asyncio `Future` objects and
+ `concurrent.futures.Future`.
+ """
+ return isinstance(x, FUTURES)
+
def chain_future(a: 'Future[_T]', b: 'Future[_T]') -> None:
"""Chain two futures together so that when one completes, so does the other.
@@ -84,7 +117,17 @@ def chain_future(a: 'Future[_T]', b: 'Future[_T]') -> None:
`concurrent.futures.Future`.
"""
- pass
+ def copy(future: 'Future[_T]') -> None:
+ if b.done():
+ return
+ if future.cancelled():
+ b.cancel()
+ else:
+ exc = future.exception()
+ if exc is not None:
+ b.set_exception(exc)
+ else:
+ b.set_result(future.result())
def future_set_result_unless_cancelled(future: 'Union[futures.Future[_T], Future[_T]]', value: _T) -> None:
"""Set the given ``value`` as the `Future`'s result, if not cancelled.
@@ -94,7 +137,8 @@ def future_set_result_unless_cancelled(future: 'Union[futures.Future[_T], Future
.. versionadded:: 5.0
"""
- pass
+ if not future.cancelled():
+ future.set_result(value)
def future_set_exception_unless_cancelled(future: 'Union[futures.Future[_T], Future[_T]]', exc: BaseException) -> None:
"""Set the given ``exc`` as the `Future`'s exception.
@@ -110,7 +154,10 @@ def future_set_exception_unless_cancelled(future: 'Union[futures.Future[_T], Fut
.. versionadded:: 6.0
"""
- pass
+ if future.cancelled():
+ app_log.error("Exception after Future was cancelled", exc_info=exc)
+ else:
+ future.set_exception(exc)
def future_set_exc_info(future: 'Union[futures.Future[_T], Future[_T]]', exc_info: Tuple[Optional[type], Optional[BaseException], Optional[types.TracebackType]]) -> None:
"""Set the given ``exc_info`` as the `Future`'s exception.
@@ -126,7 +173,8 @@ def future_set_exc_info(future: 'Union[futures.Future[_T], Future[_T]]', exc_inf
(previously ``asyncio.InvalidStateError`` would be raised)
"""
- pass
+ if exc_info[1] is not None:
+ future_set_exception_unless_cancelled(future, exc_info[1])
def future_add_done_callback(future: 'Union[futures.Future[_T], Future[_T]]', callback: Callable[..., None]) -> None:
"""Arrange to call ``callback`` when ``future`` is complete.
@@ -139,4 +187,7 @@ def future_add_done_callback(future: 'Union[futures.Future[_T], Future[_T]]', ca
.. versionadded:: 5.0
"""
- pass
\ No newline at end of file
+ if future.done():
+ callback(future)
+ else:
+ future.add_done_callback(callback)
\ No newline at end of file
diff --git a/tornado/escape.py b/tornado/escape.py
index ed651e23..5a17fc13 100644
--- a/tornado/escape.py
+++ b/tornado/escape.py
@@ -37,7 +37,11 @@ def xhtml_escape(value: Union[str, bytes]) -> str:
except that single quotes are now escaped as ``'`` instead of
``'`` and performance may be different.
"""
- pass
+ if value is None:
+ return ''
+ if isinstance(value, bytes):
+ value = value.decode('utf-8')
+ return html.escape(value)
def xhtml_unescape(value: Union[str, bytes]) -> str:
"""Un-escapes an XML-escaped string.
@@ -54,7 +58,11 @@ def xhtml_unescape(value: Union[str, bytes]) -> str:
Some invalid inputs such as surrogates now raise an error, and numeric
references to certain ISO-8859-1 characters are now handled correctly.
"""
- pass
+ if value is None:
+ return ''
+ if isinstance(value, bytes):
+ value = value.decode('utf-8')
+ return html.unescape(value)
def json_encode(value: Any) -> str:
"""JSON-encodes the given Python object.
@@ -63,18 +71,25 @@ def json_encode(value: Any) -> str:
will never contain the character sequence ``</`` which can be problematic
when JSON is embedded in an HTML ``<script>`` tag.
"""
- pass
+ # JSON permits but does not require forward slashes to be escaped.
+ # This is useful when json data is emitted in a <script> tag
+ # in HTML, as it prevents </script> from prematurely terminating
+ # the javascript. Some json libraries do this escaping by default,
+ # but json.dumps does not, so we do it here.
+ return json.dumps(value).replace("</", "<\\/")
def json_decode(value: Union[str, bytes]) -> Any:
"""Returns Python objects for the given JSON string.
Supports both `str` and `bytes` inputs. Equvalent to `json.loads`.
"""
- pass
+ if isinstance(value, bytes):
+ value = value.decode('utf-8')
+ return json.loads(value)
def squeeze(value: str) -> str:
"""Replace all sequences of whitespace chars with a single space."""
- pass
+ return re.sub(r"[\x00-\x20]+", " ", value).strip()
def url_escape(value: Union[str, bytes], plus: bool=True) -> str:
"""Returns a URL-encoded version of the given value.
@@ -91,7 +106,10 @@ def url_escape(value: Union[str, bytes], plus: bool=True) -> str:
.. versionadded:: 3.1
The ``plus`` argument
"""
- pass
+ quote = urllib.parse.quote_plus if plus else urllib.parse.quote
+ if isinstance(value, bytes):
+ value = value.decode('utf-8')
+ return quote(value)
def url_unescape(value: Union[str, bytes], encoding: Optional[str]='utf-8', plus: bool=True) -> Union[str, bytes]:
"""Decodes the given value from a URL.
@@ -111,7 +129,16 @@ def url_unescape(value: Union[str, bytes], encoding: Optional[str]='utf-8', plus
.. versionadded:: 3.1
The ``plus`` argument
"""
- pass
+ if encoding is None:
+ if plus:
+ # unquote_to_bytes doesn't have a _plus variant
+ value = to_unicode(value).replace('+', ' ').encode('utf-8')
+ return urllib.parse.unquote_to_bytes(value)
+ else:
+ if plus:
+ return urllib.parse.unquote_plus(to_unicode(value), encoding=encoding)
+ else:
+ return urllib.parse.unquote(to_unicode(value), encoding=encoding)
def parse_qs_bytes(qs: Union[str, bytes], keep_blank_values: bool=False, strict_parsing: bool=False) -> Dict[str, List[bytes]]:
"""Parses a query string like urlparse.parse_qs,
@@ -121,7 +148,11 @@ def parse_qs_bytes(qs: Union[str, bytes], keep_blank_values: bool=False, strict_
because it's too painful to keep them as byte strings in
python3 and in practice they're nearly always ascii anyway.
"""
- pass
+ result = {}
+ for key, value in urllib.parse.parse_qs(
+ to_unicode(qs), keep_blank_values, strict_parsing).items():
+ result[key] = [utf8(v) for v in value]
+ return result
_UTF8_TYPES = (bytes, type(None))
def utf8(value: Union[None, str, bytes]) -> Optional[bytes]:
@@ -130,7 +161,9 @@ def utf8(value: Union[None, str, bytes]) -> Optional[bytes]:
If the argument is already a byte string or None, it is returned unchanged.
Otherwise it must be a unicode string and is encoded as utf8.
"""
- pass
+ if value is None or isinstance(value, bytes):
+ return value
+ return value.encode('utf-8')
_TO_UNICODE_TYPES = (unicode_type, type(None))
def to_unicode(value: Union[None, str, bytes]) -> Optional[str]:
@@ -139,7 +172,9 @@ def to_unicode(value: Union[None, str, bytes]) -> Optional[str]:
If the argument is already a unicode string or None, it is returned
unchanged. Otherwise it must be a byte string and is decoded as utf8.
"""
- pass
+ if value is None or isinstance(value, unicode_type):
+ return value
+ return value.decode('utf-8')
_unicode = to_unicode
native_str = to_unicode
to_basestring = to_unicode
@@ -149,8 +184,14 @@ def recursive_unicode(obj: Any) -> Any:
Supports lists, tuples, and dictionaries.
"""
- pass
-_URL_RE = re.compile(to_unicode('\\b((?:([\\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\\s&()]|&|")*(?:[^!"#$%&\'()*+,.:;<=>?@\\[\\]^`{|}~\\s]))|(?:\\((?:[^\\s&()]|&|")*\\)))+)'))
+ if isinstance(obj, dict):
+ return dict((recursive_unicode(k), recursive_unicode(v)) for (k, v) in obj.items())
+ elif isinstance(obj, (list, tuple)):
+ return [recursive_unicode(i) for i in obj]
+ elif isinstance(obj, bytes):
+ return to_unicode(obj)
+ return obj
+_URL_RE = re.compile(r'\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&\'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)')
def linkify(text: Union[str, bytes], shorten: bool=False, extra_params: Union[str, Callable[[str], str]]='', require_protocol: bool=False, permitted_protocols: List[str]=['http', 'https']) -> str:
"""Converts plain text into HTML with links.
@@ -182,4 +223,59 @@ def linkify(text: Union[str, bytes], shorten: bool=False, extra_params: Union[st
"mailto"])``. It is very unsafe to include protocols such as
``javascript``.
"""
- pass
\ No newline at end of file
+ if isinstance(text, bytes):
+ text = text.decode('utf-8')
+
+ if not text:
+ return text
+
+ def make_link(m: 're.Match[str]') -> str:
+ url = m.group(1)
+ proto = m.group(2)
+ if require_protocol and not proto:
+ return url # not protocol, no linkify
+
+ if proto and proto not in permitted_protocols:
+ return url # bad protocol, no linkify
+
+ href = m.group(1)
+ if not proto:
+ href = 'http://' + href # no proto specified, use http
+
+ if callable(extra_params):
+ params = " " + extra_params(href)
+ else:
+ params = " " + extra_params if extra_params else ""
+
+ # clip long urls. max_len is just an approximation
+ max_len = 30
+ if shorten and len(url) > max_len:
+ before_clip = url
+ if proto:
+ proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for :
+ else:
+ proto_len = 0
+
+ parts = url[proto_len:].split("/")
+ if len(parts) > 1:
+ # Grab the whole host part plus the first bit of the path
+ # The path is usually not that interesting once shortened
+ # (no more slug, etc), so it really just provides a little
+ # extra indication of shortening.
+ url = url[:proto_len] + parts[0] + "/" + \
+ parts[1][:8].split('?')[0].split('.')[0]
+
+ if len(url) > max_len * 1.5: # still too long
+ url = url[:max_len]
+
+ if url != before_clip:
+ # Full url is visible on mouse-over.
+ params += ' title="%s"' % href
+
+ return '<a href="%s"%s>%s</a>' % (href, params, url)
+
+ # First HTML-escape so that our strings are all safe.
+ # The regex is modified to avoid character entities other than & so
+ # that we won't pick up ", etc.
+ text = _unicode(xhtml_escape(text))
+ return _URL_RE.sub(make_link, text)
\ No newline at end of file
diff --git a/tornado/httpclient.py b/tornado/httpclient.py
index 7175d225..48a063e8 100644
--- a/tornado/httpclient.py
+++ b/tornado/httpclient.py
@@ -210,6 +210,26 @@ class AsyncHTTPClient(Configurable):
"""
pass
+def main() -> None:
+ """A simple HTTP client command-line interface.
+
+ Usage: python -m tornado.httpclient [OPTIONS] URL
+
+ Options:
+ -h, --help Show this help information
+ -v, --verbose Show response headers
+ -q, --quiet Don't show response body
+ -O, --output-file=FILE Save response body to file
+ -H, --header=KEY:VALUE Add header to request
+ -m, --method=METHOD HTTP method (default GET)
+ -b, --body=BODY Request body (implies POST if no -m is given)
+ -c, --ca-certs=FILE SSL certificate authority file
+ -k, --insecure Don't validate SSL certificates
+ -p, --proxy=HOST:PORT Use HTTP proxy
+ -n, --no-verify-ssl Don't validate SSL certificates
+ """
+ pass
+
@classmethod
def configure(cls, impl: 'Union[None, str, Type[Configurable]]', **kwargs: Any) -> None:
"""Configures the `AsyncHTTPClient` subclass to use.
diff --git a/tornado/options.py b/tornado/options.py
index 427d1002..1702dc8f 100644
--- a/tornado/options.py
+++ b/tornado/options.py
@@ -105,11 +105,19 @@ class OptionParser(object):
which reference a global instance.
"""
+ def _help_callback(self, value: bool) -> None:
+ if value:
+ self.print_help()
+ sys.exit(0)
+
def __init__(self) -> None:
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: str) -> str:
+ return name.replace('-', '_')
+
def __getattr__(self, name: str) -> Any:
name = self._normalize_name(name)
if isinstance(self._options.get(name), _Option):
@@ -140,14 +148,18 @@ class OptionParser(object):
.. versionadded:: 3.1
"""
- pass
+ return [(name, self[name]) for name in self]
def groups(self) -> Set[str]:
"""The set of option-groups created by ``define``.
.. versionadded:: 3.1
"""
- pass
+ groups = set()
+ for option in self._options.values():
+ if option.group_name:
+ groups.add(option.group_name)
+ return groups
def group_dict(self, group: str) -> Dict[str, Any]:
"""The names and values of options in a group.
@@ -166,14 +178,18 @@ class OptionParser(object):
.. versionadded:: 3.1
"""
- pass
+ result = {}
+ for option in self._options.values():
+ if option.group_name == group:
+ result[option.name] = self[option.name]
+ return result
def as_dict(self) -> Dict[str, Any]:
"""The names and values of all options.
.. versionadded:: 3.1
"""
- pass
+ return dict(self.items())
def define(self, name: str, default: Any=None, type: Optional[type]=None, help: Optional[str]=None, metavar: Optional[str]=None, multiple: bool=False, group: Optional[str]=None, callback: Optional[Callable[[Any], None]]=None) -> None:
"""Defines a new command line option.
@@ -210,7 +226,25 @@ class OptionParser(object):
by later flags.
"""
- pass
+ normalized = self._normalize_name(name)
+ if normalized in self._options:
+ raise Error("Option %r already defined in %s", normalized,
+ self._options[normalized].file_name)
+ frame = sys._getframe(0)
+ options_file = frame.f_code.co_filename
+
+ if type is None:
+ if default is not None:
+ type = default.__class__
+ else:
+ type = str
+ if group is None:
+ group = options_file
+
+ self._options[normalized] = _Option(name, default=default,
+ type=type, help=help, metavar=metavar,
+ multiple=multiple, file_name=options_file,
+ group_name=group, callback=callback)
def parse_command_line(self, args: Optional[List[str]]=None, final: bool=True) -> List[str]:
"""Parses all options given on the command line (defaults to
@@ -234,7 +268,37 @@ class OptionParser(object):
from multiple sources.
"""
- pass
+ if args is None:
+ args = sys.argv
+ remaining = []
+ for i in range(1, len(args)):
+ # Skip any arguments that don't start with "-"
+ if not args[i].startswith("-"):
+ remaining.append(args[i])
+ continue
+ if args[i].startswith("--"):
+ # Strip the leading "--"
+ name = args[i][2:]
+ else:
+ # Strip the leading "-"
+ name = args[i][1:]
+ name, equals, value = name.partition("=")
+ name = self._normalize_name(name)
+ if name not in self._options:
+ self.print_help()
+ raise Error('Unrecognized command line option: %r' % name)
+ option = self._options[name]
+ if not equals:
+ if option.type == bool:
+ value = "true"
+ else:
+ raise Error('Option %r requires a value' % name)
+ option.parse(value)
+
+ if final:
+ self.run_parse_callbacks()
+
+ return remaining
def parse_config_file(self, path: str, final: bool=True) -> None:
"""Parses and loads the config file at the given path.
@@ -282,15 +346,45 @@ class OptionParser(object):
Added the ability to set options via strings in config files.
"""
- pass
+ config = {'__file__': os.path.abspath(path)}
+ with open(path, 'rb') as f:
+ exec_in(native_str(f.read()), config, config)
+ for name in config:
+ if name in self._options:
+ self._options[name].set(config[name])
def print_help(self, file: Optional[TextIO]=None) -> None:
"""Prints all the command line options to stderr (or another file)."""
- pass
+ if file is None:
+ file = sys.stderr
+ print("Usage: %s [OPTIONS]" % sys.argv[0], file=file)
+ print("\nOptions:\n", file=file)
+ by_group = {}
+ for option in self._options.values():
+ by_group.setdefault(option.group_name, []).append(option)
+
+ for filename, o in sorted(by_group.items()):
+ if filename:
+ print("\n%s options:\n" % os.path.normpath(filename), file=file)
+ o.sort(key=lambda option: option.name)
+ for option in o:
+ prefix = option.name
+ if option.metavar:
+ prefix += "=" + option.metavar
+ description = option.help or ""
+ if option.default is not None and option.default != "":
+ description += " (default %s)" % option.default
+ lines = textwrap.wrap(description, 79 - 35)
+ if len(prefix) > 30 or len(lines) == 0:
+ lines.insert(0, "")
+ print(" --%-30s %s" % (prefix, lines[0]), file=file)
+ for line in lines[1:]:
+ print("%-34s %s" % (" ", line), file=file)
+ print(file=file)
def add_parse_callback(self, callback: Callable[[], None]) -> None:
"""Adds a parse callback, to be invoked when option parsing is done."""
- pass
+ self._parse_callbacks.append(callback)
def mockable(self) -> '_Mockable':
"""Returns a wrapper around self that is compatible with
@@ -307,7 +401,7 @@ class OptionParser(object):
with mock.patch.object(options.mockable(), 'name', value):
assert options.name == value
"""
- pass
+ return _Mockable(self)
class _Mockable(object):
"""`mock.patch` compatible wrapper for `OptionParser`.
@@ -367,33 +461,35 @@ def define(name: str, default: Any=None, type: Optional[type]=None, help: Option
See `OptionParser.define`.
"""
- pass
+ return options.define(name, default=default, type=type, help=help,
+ metavar=metavar, multiple=multiple, group=group,
+ callback=callback)
def parse_command_line(args: Optional[List[str]]=None, final: bool=True) -> List[str]:
"""Parses global options from the command line.
See `OptionParser.parse_command_line`.
"""
- pass
+ return options.parse_command_line(args, final=final)
def parse_config_file(path: str, final: bool=True) -> None:
"""Parses global options from a config file.
See `OptionParser.parse_config_file`.
"""
- pass
+ return options.parse_config_file(path, final=final)
def print_help(file: Optional[TextIO]=None) -> None:
"""Prints all the command line options to stderr (or another file).
See `OptionParser.print_help`.
"""
- pass
+ return options.print_help(file)
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`
"""
- pass
+ return options.add_parse_callback(callback)
define_logging_options(options)
\ No newline at end of file
diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py
index 6b47bdfd..6935b38e 100644
--- a/tornado/platform/asyncio.py
+++ b/tornado/platform/asyncio.py
@@ -41,10 +41,28 @@ class _HasFileno(Protocol):
_FileDescriptorLike = Union[int, _HasFileno]
_T = TypeVar('_T')
_selector_loops: Set['SelectorThread'] = set()
+
+def _atexit_callback() -> None:
+ """Cleanup the selector threads at shutdown."""
+ while _selector_loops:
+ loop = _selector_loops.pop()
+ loop.close()
+
atexit.register(_atexit_callback)
class BaseAsyncIOLoop(IOLoop):
- pass
+ @classmethod
+ def configurable_base(cls):
+ return IOLoop
+
+ def initialize(self, make_current=True):
+ super().initialize(make_current=make_current)
+ self.asyncio_loop = None
+
+ def close(self, all_fds=False):
+ if self.asyncio_loop is not None:
+ self.asyncio_loop.close()
+ super().close(all_fds=all_fds)
class AsyncIOMainLoop(BaseAsyncIOLoop):
"""``AsyncIOMainLoop`` creates an `.IOLoop` that corresponds to the
@@ -60,6 +78,9 @@ class AsyncIOMainLoop(BaseAsyncIOLoop):
Closing an `AsyncIOMainLoop` now closes the underlying asyncio loop.
"""
+ def initialize(self, **kwargs):
+ super().initialize(**kwargs)
+ self.asyncio_loop = asyncio.get_event_loop()
class AsyncIOLoop(BaseAsyncIOLoop):
"""``AsyncIOLoop`` is an `.IOLoop` that runs on an ``asyncio`` event loop.
diff --git a/tornado/template.py b/tornado/template.py
index 61d611e9..9e953d50 100644
--- a/tornado/template.py
+++ b/tornado/template.py
@@ -442,6 +442,172 @@ class _CodeWriter(object):
self.include_stack = []
self._indent = 0
+def _parse(reader: _TemplateReader, template: Template) -> _ChunkList:
+ """Parses a template file and returns a _ChunkList."""
+ body = _ChunkList([])
+ while True:
+ # Find next template directive
+ curly = 0
+ while True:
+ curly = reader.find("{", curly)
+ if curly == -1 or curly + 1 == len(reader):
+ # EOF
+ if body.chunks:
+ body.chunks.extend([_Text(reader[reader.pos:], reader.line, reader.whitespace)])
+ return body
+ # Look ahead to see if this is a special sequence
+ if reader[curly + 1] == "{":
+ # Double-curly-braces is an escaped curly
+ if curly + 2 < len(reader) and reader[curly + 2] == "!":
+ # Special case: convert {{! to just {{
+ reader.consume(curly + 2)
+ body.chunks.append(_Text("{{", reader.line, reader.whitespace))
+ break
+ else:
+ # This is a template expression
+ reader.consume(curly)
+ body.chunks.append(_Expression(reader.read_until("}}"), reader.line))
+ break
+ elif reader[curly + 1] == "%":
+ # Template directive
+ reader.consume(curly)
+ if reader.current_char == "!":
+ # Special case: {% ! %} is a comment
+ reader.consume(1)
+ reader.read_until("%}")
+ break
+ else:
+ # Parse the directive
+ directive = reader.read_until("%}")
+ if not directive:
+ raise ParseError("Empty directive", reader.name, reader.line)
+ args = directive.strip().split(None, 1)
+ if not args:
+ raise ParseError("Empty directive", reader.name, reader.line)
+ cmd = args[0]
+ if cmd == "apply":
+ # apply creates a nested function so we can apply
+ # an arbitrary function to the output of a block
+ # {% apply f %} content {% end %}
+ # -> f(content)
+ if len(args) != 2:
+ raise ParseError("apply requires one argument", reader.name, reader.line)
+ body.chunks.append(_ApplyBlock(args[1], reader.line, _parse(reader, template)))
+ elif cmd == "autoescape":
+ # autoescape changes the default escaping behavior in a
+ # template. It can take a function name or None.
+ if len(args) != 2:
+ raise ParseError("autoescape requires one argument", reader.name, reader.line)
+ str_arg = args[1].strip()
+ if str_arg == "None":
+ str_arg = None
+ template.autoescape = str_arg
+ elif cmd == "block":
+ # {% block foo %} content {% end %}
+ # -> named render blocks
+ if len(args) != 2:
+ raise ParseError("block requires one argument", reader.name, reader.line)
+ block_name = args[1].strip()
+ block_body = _parse(reader, template)
+ body.chunks.append(_NamedBlock(block_name, block_body, template, reader.line))
+ elif cmd == "comment":
+ # {% comment %} blah {% end %}
+ # -> ignore everything inside
+ reader.read_until("%}")
+ continue
+ elif cmd == "extends":
+ # {% extends filename %}
+ # -> inherits from a base template
+ if len(args) != 2:
+ raise ParseError("extends requires one argument", reader.name, reader.line)
+ body.chunks.append(_ExtendsBlock(args[1].strip()))
+ elif cmd == "for":
+ # {% for var in expr %} content {% end %}
+ # -> for var in expr: content
+ if len(args) != 2:
+ raise ParseError("for requires an expression", reader.name, reader.line)
+ body.chunks.append(_ControlBlock(args[1], reader.line, _parse(reader, template)))
+ elif cmd == "from":
+ # {% from module import name [as name] %}
+ if len(args) != 2:
+ raise ParseError("from requires a module and name", reader.name, reader.line)
+ body.chunks.append(_Statement(args[1], reader.line))
+ elif cmd == "if":
+ # {% if expr %} content {% end %}
+ # -> if expr: content
+ if len(args) != 2:
+ raise ParseError("if requires an expression", reader.name, reader.line)
+ body.chunks.append(_ControlBlock(args[1], reader.line, _parse(reader, template)))
+ elif cmd == "import":
+ # {% import module %}
+ if len(args) != 2:
+ raise ParseError("import requires one argument", reader.name, reader.line)
+ body.chunks.append(_Statement(args[1], reader.line))
+ elif cmd == "include":
+ # {% include filename %}
+ if len(args) != 2:
+ raise ParseError("include requires one argument", reader.name, reader.line)
+ body.chunks.append(_IncludeBlock(args[1].strip(), reader, reader.line))
+ elif cmd == "module":
+ # {% module expr %}
+ if len(args) != 2:
+ raise ParseError("module requires one argument", reader.name, reader.line)
+ body.chunks.append(_Module(args[1], reader.line))
+ elif cmd == "raw":
+ # {% raw expr %}
+ if len(args) != 2:
+ raise ParseError("raw requires one argument", reader.name, reader.line)
+ body.chunks.append(_Expression(args[1], reader.line, raw=True))
+ elif cmd == "set":
+ # {% set x = y %}
+ if len(args) != 2:
+ raise ParseError("set requires an expression", reader.name, reader.line)
+ body.chunks.append(_Statement(args[1], reader.line))
+ elif cmd == "try":
+ # {% try %} content {% except %} content {% end %}
+ body.chunks.append(_ControlBlock(args[0], reader.line, _parse(reader, template)))
+ elif cmd == "while":
+ # {% while expr %} content {% end %}
+ if len(args) != 2:
+ raise ParseError("while requires an expression", reader.name, reader.line)
+ body.chunks.append(_ControlBlock(args[1], reader.line, _parse(reader, template)))
+ elif cmd == "whitespace":
+ # {% whitespace mode %}
+ if len(args) != 2:
+ raise ParseError("whitespace requires one argument", reader.name, reader.line)
+ reader.whitespace = args[1].strip()
+ elif cmd == "end":
+ # {% end %} or variants like {% end if %}
+ return body
+ elif cmd == "else":
+ # {% else %} or {% else if expr %}
+ body.chunks.append(_IntermediateControlBlock(directive, reader.line))
+ elif cmd == "elif":
+ # {% elif expr %}
+ if len(args) != 2:
+ raise ParseError("elif requires an expression", reader.name, reader.line)
+ body.chunks.append(_IntermediateControlBlock(directive, reader.line))
+ elif cmd == "except":
+ # {% except %} or {% except ExceptionName %}
+ body.chunks.append(_IntermediateControlBlock(directive, reader.line))
+ elif cmd == "finally":
+ # {% finally %}
+ body.chunks.append(_IntermediateControlBlock(directive, reader.line))
+ elif cmd in ("break", "continue"):
+ # {% break %}, {% continue %}
+ body.chunks.append(_Statement(cmd, reader.line))
+ else:
+ raise ParseError("unknown directive %r" % cmd, reader.name, reader.line)
+ break
+ elif reader[curly + 1] == "#":
+ # Template comment
+ reader.consume(curly + 1)
+ reader.read_until("#}")
+ break
+ else:
+ # Not a special sequence
+ curly += 1
+
class _TemplateReader(object):
def __init__(self, name: str, text: str, whitespace: str) -> None:
diff --git a/tornado/util.py b/tornado/util.py
index c4f68e3f..e0960417 100644
--- a/tornado/util.py
+++ b/tornado/util.py
@@ -15,6 +15,7 @@ import atexit
from inspect import getfullargspec
import os
import re
+import types
import typing
import zlib
from typing import Any, Optional, Dict, Mapping, List, Tuple, Match, Callable, Type, Sequence
@@ -65,12 +66,12 @@ class GzipDecompressor(object):
in ``unconsumed_tail``; you must retrieve this value and pass
it back to a future call to `decompress` if it is not empty.
"""
- pass
+ return self.decompressobj.decompress(value, max_length)
@property
def unconsumed_tail(self) -> bytes:
"""Returns the unconsumed portion left over"""
- pass
+ return self.decompressobj.unconsumed_tail
def flush(self) -> bytes:
"""Return any remaining buffered data not yet returned by decompress.
@@ -78,7 +79,7 @@ class GzipDecompressor(object):
Also checks for errors such as truncated input.
No other methods may be called on this object after `flush`.
"""
- pass
+ return self.decompressobj.flush()
def import_object(name: str) -> Any:
"""Imports an object by name.
@@ -98,7 +99,15 @@ def import_object(name: str) -> Any:
...
ImportError: No module named missing_module
"""
- pass
+ if name.count('.') == 0:
+ return __import__(name)
+
+ parts = name.split('.')
+ obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
+ try:
+ return getattr(obj, parts[-1])
+ except AttributeError:
+ raise ImportError("No module named %s" % parts[-1])
def errno_from_exception(e: BaseException) -> Optional[int]:
"""Provides the errno from an Exception object.
@@ -109,7 +118,12 @@ def errno_from_exception(e: BaseException) -> Optional[int]:
abstracts all that behavior to give you a safe way to get the
errno.
"""
- pass
+ if hasattr(e, 'errno'):
+ return e.errno
+ elif isinstance(getattr(e, 'args', None), tuple) and len(e.args) > 0:
+ if isinstance(e.args[0], int):
+ return e.args[0]
+ return None
_alphanum = frozenset('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
_re_unescape_pattern = re.compile('\\\\(.)', re.DOTALL)
@@ -122,7 +136,12 @@ def re_unescape(s: str) -> str:
.. versionadded:: 4.4
"""
- pass
+ def replace(match: Match) -> str:
+ group = match.group(1)
+ if group[0] not in _alphanum:
+ return group
+ raise ValueError("Cannot unescape '\\\\%s'" % group)
+ return _re_unescape_pattern.sub(replace, s)
class Configurable(object):
"""Base class for configurable interfaces.
@@ -178,14 +197,21 @@ class Configurable(object):
parameter).
"""
- pass
+ raise NotImplementedError()
@classmethod
def configurable_default(cls):
"""Returns the implementation class to be used if none is configured."""
+ raise NotImplementedError()
+ def initialize(self, *args: Any, **kwargs: Any) -> None:
+ """Initialize a `Configurable` subclass instance.
+
+ Configurable classes should use `initialize` instead of ``__init__``.
+
+ .. versionchanged:: 4.2
+ Now accepts positional arguments in addition to keyword arguments.
+ """
pass
- initialize = _initialize
- 'Initialize a `Configurable` subclass instance.\n\n Configurable classes should use `initialize` instead of ``__init__``.\n\n .. versionchanged:: 4.2\n Now accepts positional arguments in addition to keyword arguments.\n '
@classmethod
def configure(cls, impl, **kwargs):
@@ -195,12 +221,27 @@ class Configurable(object):
to the constructor. This can be used to set global defaults for
some parameters.
"""
- pass
+ base = cls.configurable_base()
+ if isinstance(impl, str):
+ impl = import_object(impl)
+ if impl is not None and not issubclass(impl, cls):
+ raise ValueError("Invalid subclass of %s" % cls)
+ base.__impl_class = impl
+ base.__impl_kwargs = kwargs
@classmethod
def configured_class(cls):
"""Returns the currently configured class."""
- pass
+ base = cls.configurable_base()
+ if cls is not base:
+ return cls
+ impl = getattr(base, '_Configurable__impl_class', None)
+ if impl is None:
+ impl = base.configurable_default()
+ if impl is None:
+ raise ValueError("No implementation specified for %s" % cls)
+ base.configure(impl)
+ return impl
class ArgReplacer(object):
"""Replaces one value in an ``args, kwargs`` pair.
@@ -222,7 +263,9 @@ class ArgReplacer(object):
Returns ``default`` if the argument is not present.
"""
- pass
+ if self.arg_pos is not None and len(args) > self.arg_pos:
+ return args[self.arg_pos]
+ return kwargs.get(self.name, default)
def replace(self, new_value: Any, args: Sequence[Any], kwargs: Dict[str, Any]) -> Tuple[Any, Sequence[Any], Dict[str, Any]]:
"""Replace the named argument in ``args, kwargs`` with ``new_value``.
@@ -234,11 +277,38 @@ class ArgReplacer(object):
If the named argument was not found, ``new_value`` will be added
to ``kwargs`` and None will be returned as ``old_value``.
"""
- pass
+ old_value = self.get_old_value(args, kwargs)
+ if args is None:
+ args = []
+ else:
+ args = list(args)
+
+ if self.arg_pos is not None and len(args) > self.arg_pos:
+ args[self.arg_pos] = new_value
+ else:
+ kwargs[self.name] = new_value
+ return old_value, args, kwargs
def timedelta_to_seconds(td):
"""Equivalent to ``td.total_seconds()`` (introduced in Python 2.7)."""
- pass
+ return td.total_seconds()
+
+def exec_in(code: str, glob: Dict[str, Any], loc: Dict[str, Any]=None) -> None:
+ """Execute code in a given context."""
+ if loc is None:
+ loc = glob
+ exec(code, glob, loc)
+
+def raise_exc_info(exc_info: Tuple[Optional[type], Optional[BaseException], Optional[types.TracebackType]]) -> None:
+ """Re-raise an exception from an exc_info tuple.
+
+ The argument is a ``(type, value, traceback)`` tuple as returned by
+ `sys.exc_info`."""
+ if exc_info[1] is not None:
+ if exc_info[2] is not None:
+ raise exc_info[1].with_traceback(exc_info[2])
+ else:
+ raise exc_info[1]
def _websocket_mask_python(mask: bytes, data: bytes) -> bytes:
"""Websocket masking function.
@@ -249,7 +319,11 @@ def _websocket_mask_python(mask: bytes, data: bytes) -> bytes:
This pure-python implementation may be replaced by an optimized version when available.
"""
- pass
+ mask_arr = array.array("B", mask)
+ unmasked_arr = array.array("B", data)
+ for i in range(len(data)):
+ unmasked_arr[i] = unmasked_arr[i] ^ mask_arr[i % 4]
+ return unmasked_arr.tobytes()
if os.environ.get('TORNADO_NO_EXTENSION') or os.environ.get('TORNADO_EXTENSION') == '0':
_websocket_mask = _websocket_mask_python
else:
diff --git a/tornado/web.py b/tornado/web.py
index 7619ad4d..030e0ddc 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -102,6 +102,114 @@ class _ArgDefaultMarker:
pass
_ARG_DEFAULT = _ArgDefaultMarker()
+def _unimplemented_method(*args: Any, **kwargs: Any) -> None:
+ raise HTTPError(405)
+
+def _create_signature_v1(secret: Union[str, bytes], *parts: Union[str, bytes]) -> bytes:
+ hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
+ for part in parts:
+ hash.update(utf8(part))
+ return utf8(hash.hexdigest())
+
+def get_signature_key_version(value: Union[None, str, bytes]) -> Optional[int]:
+ """Extract the signature key version from the given signed value.
+
+ Returns None if the version cannot be determined.
+ """
+ if value is None:
+ return None
+ value = utf8(value)
+ parts = value.split(b"|")
+ if len(parts) < 3:
+ return None
+ if len(parts) > 3:
+ try:
+ return int(parts[3])
+ except ValueError:
+ return None
+ return None
+
+def decode_signed_value(secret: _CookieSecretTypes, name: str, value: Union[None, str, bytes], max_age_days: Optional[float]=31, min_version: Optional[int]=None, clock: Callable[[], float]=None) -> Optional[bytes]:
+ """Decode a signed value.
+
+ Returns the decoded value if the signature is valid and the value is not
+ expired, or None otherwise.
+ """
+ if min_version is None:
+ min_version = DEFAULT_SIGNED_VALUE_MIN_VERSION
+ if clock is None:
+ clock = time.time
+ if value is None:
+ return None
+ value = utf8(value)
+ parts = value.split(b"|")
+ if len(parts) < 3:
+ return None
+ signature = parts[-1]
+ if not isinstance(secret, dict):
+ expected_sig = _create_signature_v1(secret, name, parts[0], parts[1])
+ if not hmac.compare_digest(utf8(expected_sig), utf8(signature)):
+ return None
+ timestamp = int(parts[1])
+ if timestamp < int(clock()) - max_age_days * 86400:
+ return None
+ try:
+ return base64.b64decode(parts[0])
+ except Exception:
+ return None
+ else:
+ version = 2
+ if len(parts) < 4:
+ return None
+ key_version = int(parts[-1])
+ if key_version not in secret:
+ return None
+ expected_sig = _create_signature_v1(secret[key_version], name, parts[0], parts[1])
+ if not hmac.compare_digest(utf8(expected_sig), utf8(signature)):
+ return None
+ timestamp = int(parts[1])
+ if timestamp < int(clock()) - max_age_days * 86400:
+ return None
+ try:
+ return base64.b64decode(parts[0])
+ except Exception:
+ return None
+
+def create_signed_value(secret: _CookieSecretTypes, name: str, value: Union[str, bytes], version: Optional[int]=None, clock: Callable[[], float]=None, key_version: Optional[int]=None) -> bytes:
+ """Signs and timestamps a string so it cannot be forged.
+
+ Stores signatures in the format: value|timestamp|signature[|key_version].
+ Parts are joined with pipes, and may not contain pipes themselves.
+ """
+ if version is None:
+ version = DEFAULT_SIGNED_VALUE_VERSION
+ if clock is None:
+ clock = time.time
+
+ timestamp = utf8(str(int(clock())))
+ value = base64.b64encode(utf8(value))
+
+ if version == 1:
+ if isinstance(secret, dict):
+ raise ValueError("secret_dict cannot be used with version 1")
+ signature = _create_signature_v1(secret, name, value, timestamp)
+ value = b"|".join([value, timestamp, signature])
+ return value
+ elif version == 2:
+ # Version 2 adds key versioning, but is otherwise the same.
+ if isinstance(secret, dict):
+ if key_version is None:
+ key_version = max(secret.keys())
+ assert key_version in secret
+ secret = secret[key_version]
+ signature = _create_signature_v1(secret, name, value, timestamp)
+ value = b"|".join([value, timestamp, signature])
+ if key_version is not None:
+ value = b"|".join([value, utf8(str(key_version))])
+ return value
+ else:
+ raise ValueError("Unsupported version %d" % version)
+
class RequestHandler(object):
"""Base class for HTTP request handlers.
@@ -137,6 +245,28 @@ class RequestHandler(object):
assert self.request.connection is not None
self.request.connection.set_close_callback(self.on_connection_close)
self.initialize(**kwargs)
+
+ def _initialize(self) -> None:
+ """Hook for subclass initialization.
+
+ A dictionary passed as the third argument of a ``URLSpec`` will be
+ supplied as keyword arguments to initialize().
+
+ Example::
+
+ class ProfileHandler(RequestHandler):
+ def initialize(self, database):
+ self.database = database
+
+ def get(self, username):
+ ...
+
+ app = Application([
+ (r'/user/(.*)', ProfileHandler, dict(database=database)),
+ ])
+ """
+ pass
+
initialize = _initialize
"Hook for subclass initialization. Called for each request.\n\n A dictionary passed as the third argument of a ``URLSpec`` will be\n supplied as keyword arguments to ``initialize()``.\n\n Example::\n\n class ProfileHandler(RequestHandler):\n def initialize(self, database):\n self.database = database\n\n def get(self, username):\n ...\n\n app = Application([\n (r'/user/(.*)', ProfileHandler, dict(database=database)),\n ])\n "