Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 82 additions & 93 deletions bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,20 @@ 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

* Dropped support for Python 2 (EOL: 2020-01-01) and removed workarounds or helpers that only make sense in a Python 2/3 dual codebase.
* 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

Expand Down