Skip to content

Commit b26e507

Browse files
committed
Merge branch 'patch-remove-py2'
2 parents a8dfef3 + 1a4618c commit b26e507

10 files changed

+163
-299
lines changed

bottle.py

+120-221
Large diffs are not rendered by default.

docs/changelog.rst

+8-3
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ to receive updates on a best-effort basis.
3636
Release 0.14 (in development)
3737
=============================
3838

39-
.. rubric:: Removed APIs (deprecated since 0.13)
39+
.. rubric:: Removed APIs
4040

41+
* Dropped support for Python 2 and removed workarounds or helpers that only make sense in a Python 2/3 dual codebase.
4142
* Removed the ``RouteReset`` exception and associated logic.
4243
* 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.
4344

4445
.. rubric:: Changes
4546

46-
* ``bottle.HTTPError`` raised on Invalid JSON now include the underlying exception in their ``exception`` field.
47+
* 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`.
48+
49+
.. rubric:: New features
50+
51+
* ``bottle.HTTPError`` raised on Invalid JSON now include the underlying exception in the ``exception`` field.
4752

4853

4954
Release 0.13
@@ -74,7 +79,7 @@ versions should not update to Bottle 0.13 and stick with 0.12 instead.
7479

7580
.. rubric:: Deprecated APIs
7681

77-
* Python 2 support is now deprecated and will be dropped with the next release.
82+
* Python 2 support is now deprecated and will be dropped with the next release. This includes helpers and workarounds that only make sense in a Python 2/3 dual codebase (e.g. ``tonat()`` or the ``py3k`` flag).
7883
* The command line executable installed along with bottle will be renamed from `bottle.py` to just `bottle`. You can still execute bottle directly as a script (e.g. `./bottle.py` or `python3 bottle.py`) or as a module (via `python3 -m bottle`). Just the executable installed by your packaging tool (e.g. `pip`) into the `bin` folder of your (virtual) environment will change.
7984
* The old route syntax (``/hello/:name``) is deprecated in favor of the more readable and flexible ``/hello/<name>`` syntax.
8085
* :meth:`Bottle.mount` now recognizes Bottle instance and will warn about parameters that are not compatible with the new mounting behavior. The old behavior (mount applications as WSGI callable) still works and is used as a fallback automatically.

docs/tutorial.rst

+3-13
Original file line numberDiff line numberDiff line change
@@ -552,28 +552,18 @@ Property Data source
552552

553553
Bottle uses a special type of dictionary to store those parameters. :class:`FormsDict` behaves like a normal dictionary, but has some additional features to make your life easier.
554554

555-
First of all, :class:`FormsDict` is a subclass of :class:`MultiDict` and can store more than one value per key. The standard dictionary access methods will only return the first of many values, but the :meth:`MultiDict.getall` method returns a (possibly empty) list of all values for a specific key::
555+
First of all, :class:`FormsDict` is a subclass of :class:`MultiDict` and can store more than one value per key. Only the first value is returned by default, but :meth:`MultiDict.getall` can be used to get a (possibly empty) list of all values for a specific key::
556556

557557
for choice in request.forms.getall('multiple_choice'):
558558
do_something(choice)
559559

560-
To simplify dealing with lots of unreliable user input, :class:`FormsDict` exposes all its values as attributes, but with a twist: These virtual attributes always return properly encoded unicode strings, even if the value is missing or character decoding fails. They never return ``None`` or throw an exception, but return an empty string instead::
560+
Attribute-like access is also supported, returning empty strings for missing values. This simplifies code a lot whend ealing with lots of optional attributes::
561561

562562
name = request.query.name # may be an empty string
563563

564564
.. rubric:: A word on unicode and character encodings
565565

566-
HTTP is a byte-based wire protocol. The server has to decode byte strings somehow before they are passed to the application. To be on the safe side, WSGI suggests ISO-8859-1 (aka latin1), a reversible single-byte codec that can be re-encoded with a different encoding later. Bottle does that for :meth:`FormsDict.getunicode` and attribute access, but not for :meth:`FormsDict.get` or item-access. These return the unchanged values as provided by the server implementation, which is probably not what you want.
567-
568-
::
569-
570-
>>> request.query['city']
571-
'Göttingen' # An utf8 string provisionally decoded as ISO-8859-1 by the server
572-
>>> request.query.city
573-
'Göttingen' # The same string correctly re-encoded as utf8 by bottle
574-
575-
If you need the whole dictionary with correctly decoded values (e.g. for WTForms), you can call :meth:`FormsDict.decode` to get a fully re-encoded copy.
576-
566+
Unicode characters in the request path, query parameters or cookies are a bit tricky. HTTP is a very old byte-based protocol that predates unicode and lacks explicit encoding information. This is why WSGI servers have to fall back on `ISO-8859-1` (aka `latin1`, a reversible input encoding) for those estrings. Modern browsers default to `utf8`, though. It's a bit much to ask application developers to translate every single user input string to the correct encoding manually. Bottle makes this easy and just assumes `utf8` for everything. All strings returned by Bottle APIs support the full range of unicode characters, as long as the webpage or HTTP client follows best practices and does not break with established standards.
577567

578568
Query Parameters
579569
--------------------------------------------------------------------------------

test/test_environ.py

+17-26
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import itertools
88

99
import bottle
10-
from bottle import request, tob, touni, tonat, json_dumps, HTTPError, parse_date, CookieError
10+
from bottle import request, tob, touni, json_dumps, HTTPError, parse_date, CookieError
1111
from . import tools
1212
import wsgiref.util
1313
import base64
@@ -160,16 +160,16 @@ def test_cookie_dict(self):
160160

161161
def test_get(self):
162162
""" Environ: GET data """
163-
qs = tonat(tob('a=a&a=1&b=b&c=c&cn=%e7%93%b6'), 'latin1')
163+
qs = touni(tob('a=a&a=1&b=b&c=c&cn=%e7%93%b6'), 'latin1')
164164
request = BaseRequest({'QUERY_STRING':qs})
165165
self.assertTrue('a' in request.query)
166166
self.assertTrue('b' in request.query)
167167
self.assertEqual(['a','1'], request.query.getall('a'))
168168
self.assertEqual(['b'], request.query.getall('b'))
169169
self.assertEqual('1', request.query['a'])
170170
self.assertEqual('b', request.query['b'])
171-
self.assertEqual(tonat(tob('瓶'), 'latin1'), request.query['cn'])
172-
self.assertEqual(touni('瓶'), request.query.cn)
171+
self.assertEqual('瓶', request.query['cn'])
172+
self.assertEqual('瓶', request.query.cn)
173173

174174
def test_post(self):
175175
""" Environ: POST data """
@@ -189,8 +189,8 @@ def test_post(self):
189189
self.assertEqual('b', request.POST['b'])
190190
self.assertEqual('', request.POST['c'])
191191
self.assertEqual('', request.POST['d'])
192-
self.assertEqual(tonat(tob('瓶'), 'latin1'), request.POST['cn'])
193-
self.assertEqual(touni('瓶'), request.POST.cn)
192+
self.assertEqual('瓶', request.POST['cn'])
193+
self.assertEqual('瓶', request.POST.cn)
194194

195195
def test_bodypost(self):
196196
sq = tob('foobar')
@@ -503,15 +503,11 @@ def cmp(app, wire):
503503
result = [v for (h, v) in rs.headerlist if h.lower()=='x-test'][0]
504504
self.assertEqual(wire, result)
505505

506-
if bottle.py3k:
507-
cmp(1, tonat('1', 'latin1'))
508-
cmp('öäü', 'öäü'.encode('utf8').decode('latin1'))
509-
# Dropped byte header support in Python 3:
510-
#cmp(tob('äöü'), 'äöü'.encode('utf8').decode('latin1'))
511-
else:
512-
cmp(1, '1')
513-
cmp('öäü', 'öäü')
514-
cmp(touni('äöü'), 'äöü')
506+
cmp(1, touni('1', 'latin1'))
507+
cmp('öäü', 'öäü'.encode('utf8').decode('latin1'))
508+
# Dropped byte header support in Python 3:
509+
#cmp(tob('äöü'), 'äöü'.encode('utf8').decode('latin1'))
510+
515511

516512
def test_set_status(self):
517513
rs = BaseResponse()
@@ -583,12 +579,11 @@ def test(): rs.status = '555' # No reason
583579
self.assertEqual(rs.status_line, '404 Brain not Found') # last value
584580

585581
# Unicode in status line (thanks RFC7230 :/)
586-
if bottle.py3k:
587-
rs.status = '400 Non-ASÎÎ'
588-
self.assertEqual(rs.status, rs.status_line)
589-
self.assertEqual(rs.status_code, 400)
590-
wire = rs._wsgi_status_line().encode('latin1')
591-
self.assertEqual(rs.status, wire.decode('utf8'))
582+
rs.status = '400 Non-ASÎÎ'
583+
self.assertEqual(rs.status, rs.status_line)
584+
self.assertEqual(rs.status_code, 400)
585+
wire = rs._wsgi_status_line().encode('latin1')
586+
self.assertEqual(rs.status, wire.decode('utf8'))
592587

593588
def test_content_type(self):
594589
rs = BaseResponse()
@@ -735,7 +730,7 @@ def test_non_string_header(self):
735730
response['x-test'] = None
736731
self.assertEqual('', response['x-test'])
737732
response['x-test'] = touni('瓶')
738-
self.assertEqual(tonat(touni('瓶')), response['x-test'])
733+
self.assertEqual(touni('瓶'), response['x-test'])
739734

740735
def test_prevent_control_characters_in_headers(self):
741736
masks = '{}test', 'test{}', 'te{}st'
@@ -895,10 +890,6 @@ def test_native(self):
895890
self.env['HTTP_TEST_HEADER'] = 'foobar'
896891
self.assertEqual(self.headers['Test-header'], 'foobar')
897892

898-
def test_bytes(self):
899-
self.env['HTTP_TEST_HEADER'] = tob('foobar')
900-
self.assertEqual(self.headers['Test-Header'], 'foobar')
901-
902893
def test_unicode(self):
903894
self.env['HTTP_TEST_HEADER'] = touni('foobar')
904895
self.assertEqual(self.headers['Test-Header'], 'foobar')

test/test_fileupload.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ def test_filename(self):
3333
self.assertFilename('.name.cfg', 'name.cfg')
3434
self.assertFilename(' . na me . ', 'na-me')
3535
self.assertFilename('path/', 'empty')
36-
self.assertFilename(bottle.tob('ümläüts$'), 'umlauts')
37-
self.assertFilename(bottle.touni('ümläüts$'), 'umlauts')
36+
self.assertFilename('ümläüts$', 'umlauts')
3837
self.assertFilename('', 'empty')
3938
self.assertFilename('a'+'b'*1337+'c', 'a'+'b'*254)
4039

test/test_formsdict.py

+4-18
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,11 @@
77
class TestFormsDict(unittest.TestCase):
88
def test_attr_access(self):
99
""" FomsDict.attribute returs string values as unicode. """
10-
d = FormsDict(py2=tob('瓶'), py3=tob('瓶').decode('latin1'))
11-
self.assertEqual(touni('瓶'), d.py2)
12-
self.assertEqual(touni('瓶'), d.py3)
10+
d = FormsDict(py3='瓶')
11+
self.assertEqual('瓶', d.py3)
12+
self.assertEqual('瓶', d["py3"])
1313

1414
def test_attr_missing(self):
1515
""" FomsDict.attribute returs u'' on missing keys. """
1616
d = FormsDict()
17-
self.assertEqual(touni(''), d.missing)
18-
19-
def test_attr_unicode_error(self):
20-
""" FomsDict.attribute returs u'' on UnicodeError. """
21-
d = FormsDict(latin=touni('öäüß').encode('latin1'))
22-
self.assertEqual(touni(''), d.latin)
23-
d.input_encoding = 'latin1'
24-
self.assertEqual(touni('öäüß'), d.latin)
25-
26-
def test_decode_method(self):
27-
d = FormsDict(py2=tob('瓶'), py3=tob('瓶').decode('latin1'))
28-
d = d.decode()
29-
self.assertFalse(d.recode_unicode)
30-
self.assertTrue(hasattr(list(d.keys())[0], 'encode'))
31-
self.assertTrue(hasattr(list(d.values())[0], 'encode'))
17+
self.assertEqual('', d.missing)

test/test_route.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,8 @@ def x(a, b):
5959
# triggers the "TypeError: 'foo' is not a Python function"
6060
self.assertEqual(set(route.get_callback_args()), set(['a', 'b']))
6161

62-
if bottle.py3k:
63-
def test_callback_inspection_newsig(self):
64-
env = {}
65-
eval(compile('def foo(a, *, b=5): pass', '<foo>', 'exec'), env, env)
66-
route = bottle.Route(bottle.Bottle(), None, None, env['foo'])
67-
self.assertEqual(set(route.get_callback_args()), set(['a', 'b']))
62+
def test_callback_inspection_newsig(self):
63+
env = {}
64+
eval(compile('def foo(a, *, b=5): pass', '<foo>', 'exec'), env, env)
65+
route = bottle.Route(bottle.Bottle(), None, None, env['foo'])
66+
self.assertEqual(set(route.get_callback_args()), set(['a', 'b']))

test/test_sendfile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_ims(self):
102102
res = static_file(basename, root=root)
103103
self.assertEqual(304, res.status_code)
104104
self.assertEqual(int(os.stat(__file__).st_mtime), parse_date(res.headers['Last-Modified']))
105-
self.assertAlmostEqual(int(time.time()), parse_date(res.headers['Date']))
105+
self.assertAlmostEqual(int(time.time()), parse_date(res.headers['Date']), delta=2)
106106
request.environ['HTTP_IF_MODIFIED_SINCE'] = bottle.http_date(100)
107107
self.assertEqual(open(__file__,'rb').read(), static_file(basename, root=root).body.read())
108108

test/test_wsgi.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,7 @@ def test(string): return string
103103
self.assertBody(tob('urf8-öäü'), '/my-öäü/urf8-öäü')
104104

105105
def test_utf8_header(self):
106-
header = 'öäü'
107-
if bottle.py3k:
108-
header = header.encode('utf8').decode('latin1')
106+
header = 'öäü'.encode('utf8').decode('latin1')
109107
@bottle.route('/test')
110108
def test():
111109
h = bottle.request.get_header('X-Test')

test/tools.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import mimetypes
1414
import uuid
1515

16-
from bottle import tob, tonat, BytesIO, py3k, unicode
16+
from bottle import tob, BytesIO
1717

1818

1919
def warn(msg):
@@ -76,10 +76,7 @@ def decorator(func):
7676

7777

7878
def wsgistr(s):
79-
if py3k:
80-
return s.encode('utf8').decode('latin1')
81-
else:
82-
return s
79+
return s.encode('utf8').decode('latin1')
8380

8481
class ServerTestBase(unittest.TestCase):
8582
def setUp(self):
@@ -170,7 +167,7 @@ def multipart_environ(fields, files):
170167
body += 'Content-Type: %s\r\n\r\n' % mimetype
171168
body += content + '\r\n'
172169
body += boundary + '--\r\n'
173-
if isinstance(body, unicode):
170+
if isinstance(body, str):
174171
body = body.encode('utf8')
175172
env['CONTENT_LENGTH'] = str(len(body))
176173
env['wsgi.input'].write(body)

0 commit comments

Comments
 (0)