diff --git a/bottle.py b/bottle.py index a297ca940..bfde62e47 100755 --- a/bottle.py +++ b/bottle.py @@ -19,57 +19,12 @@ __version__ = '0.14-dev' __license__ = 'MIT' -############################################################################### -# Command-line interface ###################################################### -############################################################################### -# INFO: Some server adapters need to monkey-patch std-lib modules before they -# are imported. This is why some of the command-line handling is done here, but -# the actual call to _main() is at the end of the file. - - -def _cli_parse(args): # pragma: no coverage - from argparse import ArgumentParser - - parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app") - opt = parser.add_argument - opt("--version", action="store_true", help="show version number.") - opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") - opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") - opt("-p", "--plugin", action="append", help="install additional plugin/s.") - opt("-c", "--conf", action="append", metavar="FILE", - help="load config values from FILE.") - opt("-C", "--param", action="append", metavar="NAME=VALUE", - help="override config values.") - opt("--debug", action="store_true", help="start server in debug mode.") - opt("--reload", action="store_true", help="auto-reload on file changes.") - opt('app', help='WSGI app entry point.', nargs='?') - - cli_args = parser.parse_args(args[1:]) - - return cli_args, parser - - -def _cli_patch(cli_args): # pragma: no coverage - parsed_args, _ = _cli_parse(cli_args) - opts = parsed_args - if opts.server: - if opts.server.startswith('gevent'): - import gevent.monkey - gevent.monkey.patch_all() - elif opts.server.startswith('eventlet'): - import eventlet - eventlet.monkey_patch() - - -if __name__ == '__main__': - _cli_patch(sys.argv) - ############################################################################### # Imports and Helpers used everywhere else ##################################### ############################################################################### -import base64, calendar, email.utils, functools, hmac, itertools, \ - mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib +import base64, calendar, contextvars, email.utils, functools, hmac, itertools +import mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib from types import FunctionType from datetime import date as datedate, datetime, timedelta @@ -1860,46 +1815,66 @@ def __repr__(self): return out -def _local_property(): - ls = threading.local() +def _local_property(ctx, name, doc=None): + errmsg = f"Property not available outside of request context" def fget(_): try: - return ls.var - except AttributeError: - raise RuntimeError("Request context not initialized.") + return ctx.get()[name] + except LookupError: + raise RuntimeError(errmsg) + except KeyError: + raise AttributeError(name) def fset(_, value): - ls.var = value - - def fdel(_): - del ls.var + try: + ctx.get()[name] = value + except LookupError: + raise RuntimeError(errmsg) - return property(fget, fset, fdel, 'Thread-local property') + return property(fget, fset, None, doc or f'Context-local property') class LocalRequest(BaseRequest): - """ A thread-local subclass of :class:`BaseRequest` with a different - set of attributes for each thread. There is usually only one global - instance of this class (:data:`request`). If accessed during a - request/response cycle, this instance always refers to the *current* - request (even on a multithreaded server). """ - bind = BaseRequest.__init__ - environ = _local_property() + """ A context-aware subclass of :class:`BaseRequest`. The module-level + :data:`request` instance always represents the request handled by the + *current* thread or greenlet, even in multi-threaded environments. Use + :meth:`copy` to get a context-independent copy (e.g. to pass to to a + separate worker thread). + """ + + _ctx = contextvars.ContextVar("LocalRequest") + + def __init__(self): + pass + + def bind(self, *a, **ka): + LocalRequest._ctx.set({}) + BaseRequest.__init__(self, *a, **ka) + + environ = _local_property(_ctx, "environ", "PEP-3333 environment dict") class LocalResponse(BaseResponse): - """ A thread-local subclass of :class:`BaseResponse` with a different - set of attributes for each thread. There is usually only one global - instance of this class (:data:`response`). Its attributes are used - to build the HTTP response at the end of the request/response cycle. + """ A context-aware subclass of :class:`BaseResponse`. The module-level + :data:`response` instance always corresponds to the request handled by + the *current* thread or greenlet, even in multi-threaded environments. """ - bind = BaseResponse.__init__ - _status_line = _local_property() - _status_code = _local_property() - _cookies = _local_property() - _headers = _local_property() - body = _local_property() + + _ctx = contextvars.ContextVar("LocalResponse") + + def __init__(self): + pass + + def bind(self, *a, **ka): + LocalResponse._ctx.set({}) + BaseResponse.__init__(self, *a, **ka) + + _status_line = _local_property(_ctx, "_status_line") + _status_code = _local_property(_ctx, "_status_code") + _cookies = _local_property(_ctx, "_cookies") + _headers = _local_property(_ctx, "_headers") + body = _local_property(_ctx, "body", "Response body") Request = BaseRequest @@ -3600,10 +3575,7 @@ class GeventServer(ServerAdapter): """ def run(self, handler): - from gevent import pywsgi, local - if not isinstance(threading.local(), local.local): - msg = "Bottle requires gevent.monkey.patch_all() (before import)" - raise RuntimeError(msg) + from gevent import pywsgi if self.quiet: self.options['log'] = None address = (self.host, self.port) @@ -3649,10 +3621,7 @@ class EventletServer(ServerAdapter): """ def run(self, handler): - from eventlet import wsgi, listen, patcher - if not patcher.is_monkey_patched(os): - msg = "Bottle requires eventlet.monkey_patch() (before import)" - raise RuntimeError(msg) + from eventlet import wsgi, listen socket_args = {} for arg in ('backlog', 'family'): try: @@ -4059,22 +4028,17 @@ def render(self, *args, **kwargs): class CheetahTemplate(BaseTemplate): def prepare(self, **options): from Cheetah.Template import Template - self.context = threading.local() - self.context.vars = {} - options['searchList'] = [self.context.vars] if self.source: - self.tpl = Template(source=self.source, **options) + self.tpl = Template.compile(source=self.source, **options) else: - self.tpl = Template(file=self.filename, **options) + self.tpl = Template.compile(file=self.filename, **options) def render(self, *args, **kwargs): for dictarg in args: kwargs.update(dictarg) - self.context.vars.update(self.defaults) - self.context.vars.update(kwargs) - out = str(self.tpl) - self.context.vars.clear() - return out + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return str(self.tpl(searchList=[_defaults])) class Jinja2Template(BaseTemplate): @@ -4505,7 +4469,7 @@ def wrapper(*args, **kwargs): #: HTTP response for the *current* request. response = LocalResponse() -#: A thread-safe namespace. Not used by Bottle. +#: An instance of threading.local(). Not used by Bottle. (deprecated since 0.14) local = threading.local() # Initialize app stack (create first empty Bottle app now deferred until needed) @@ -4517,6 +4481,31 @@ def wrapper(*args, **kwargs): ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__ + ".ext", 'bottle_%s').module +############################################################################### +# Command-line interface ###################################################### +############################################################################### + +def _cli_parse(args): # pragma: no coverage + from argparse import ArgumentParser + + parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app") + opt = parser.add_argument + opt("--version", action="store_true", help="show version number.") + opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + opt("-p", "--plugin", action="append", help="install additional plugin/s.") + opt("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opt('app', help='WSGI app entry point.', nargs='?') + + cli_args = parser.parse_args(args[1:]) + + return cli_args, parser + def _main(argv): # pragma: no coverage args, parser = _cli_parse(argv) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4840c1de0..4054e3603 100755 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,6 +39,7 @@ Release 0.14 (in development) .. rubric:: Deprecated APIs or behavior * ``Route.get_undecorated_callback()`` was able to look into closure cells to guess the original function wrapped by a decorator, but this is too aggressive in some cases and may return the wrong function. To avoid this, we will depend on proper use of ``@functools.wraps(orig)`` or ``functools.update_wrapper(wrapper, orig)`` in decorators in the future. +* ``bottle.local`` is deprecated. Use :class:`contextvars.ContextVar` or assign custom attributes to ``bottle.request`` if your application depends on thread-local or request-local but globally accessible state. .. rubric:: Removed APIs @@ -46,10 +47,12 @@ Release 0.14 (in development) * Dropped support for Python 3.8 (EOL: 2024-10-07). * Removed the ``RouteReset`` exception and associated logic. * Removed the `bottle.py` console script entrypoint in favour of the new `bottle` script. You can still execute `bottle.py` directly or via `python -m bottle`. The only change is that the command installed by pip or similar tools into the bin/Scripts folder of the (virtual) environment is now called `bottle` to avoid circular import errors. +* The bottle CLI no longer monkey-patches python standard libraries when selecting the `gevent` or `eventlet` server adapter. This was an incomplete workaround anyway and is no longer required by bottle itself, but if your own application depends on it, you may need to add ``gevent.monkey.patch_all()`` to your own wsgi application script. .. rubric:: Changes * Form values, query parameters, path elements and cookies are now always decoded as `utf8` with `errors='surrogateescape'`. This is the correct approach for almost all modern web applications, but still allows applications to recover the original byte sequence if needed. This also means that ``bottle.FormsDict`` no longer re-encodes PEP-3333 `latin1` strings to `utf8` on demand (via attribute access). The ``FormsDict.getunicode()`` and ``FormsDict.decode()`` methods are deprecated and do nothing, as all values are already transcoded to `utf8`. +* Bottle now use :class:`contextvars.ContextVar` instead of :class:`threading.local` wherever possible. Contexts are natively supported by gevent and bottle no longer depends on monkey-patching in greenlet-based server environments. Running gunicorn with the gevent worker now *just works* without additional steps. .. rubric:: New features