From 2e9c820c9f9a5ef8facae7b4683d4dd5553f17d3 Mon Sep 17 00:00:00 2001 From: Dimitrios Semitsoglou-Tsiapos Date: Fri, 22 May 2015 14:41:15 +0200 Subject: [PATCH 001/280] Better traceback for bad imports in custom path --- tests/test_utils.py | 20 ++++++++++++++++++++ werkzeug/utils.py | 8 +------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index add8aa40c..824ee3af7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -186,6 +186,26 @@ def test_import_string(): pytest.raises(ImportError, utils.import_string, 'cgi.XXXXXXXXXX') +def test_import_string_provides_traceback(tmpdir, monkeypatch): + monkeypatch.syspath_prepend(str(tmpdir)) + # Couple of packages + dir_a = tmpdir.mkdir('a') + dir_b = tmpdir.mkdir('b') + # Totally packages, I promise + dir_a.join('__init__.py').write('') + dir_b.join('__init__.py').write('') + # 'aa.a' that depends on 'bb.b', which in turn has a broken import + dir_a.join('aa.py').write('from b import bb') + dir_b.join('bb.py').write('from os import a_typo') + + # Do we get all the useful information in the traceback? + with pytest.raises(ImportError) as baz_exc: + utils.import_string('a.aa') + traceback = ''.join((str(line) for line in baz_exc.traceback)) + assert 'bb.py\':1' in traceback # a bit different than typical python tb + assert 'from os import a_typo' in traceback + + def test_import_string_attribute_error(tmpdir, monkeypatch): monkeypatch.syspath_prepend(str(tmpdir)) tmpdir.join('foo_test.py').write('from bar_test import value') diff --git a/werkzeug/utils.py b/werkzeug/utils.py index 935029e85..bd593108b 100644 --- a/werkzeug/utils.py +++ b/werkzeug/utils.py @@ -423,13 +423,7 @@ def import_string(import_name, silent=False): return sys.modules[import_name] module_name, obj_name = import_name.rsplit('.', 1) - try: - module = __import__(module_name, None, None, [obj_name]) - except ImportError: - # support importing modules not yet set up by the parent module - # (or package for that matter) - module = import_string(module_name) - + module = __import__(module_name, globals(), locals(), [obj_name]) try: return getattr(module, obj_name) except AttributeError as e: From d95e6c6438f27eabd3a5ac32c4f28aac77185659 Mon Sep 17 00:00:00 2001 From: Florian Kaiser Date: Fri, 22 Jul 2016 17:30:26 +0200 Subject: [PATCH 002/280] Update documentation for secure_filename --- werkzeug/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werkzeug/utils.py b/werkzeug/utils.py index 935029e85..df612236b 100644 --- a/werkzeug/utils.py +++ b/werkzeug/utils.py @@ -267,8 +267,8 @@ def secure_filename(filename): 'i_contain_cool_umlauts.txt' The function might return an empty filename. It's your responsibility - to ensure that the filename is unique and that you generate random - filename if the function returned an empty one. + to ensure that the filename is unique and that you abort or + generate a random filename if the function returned an empty one. .. versionadded:: 0.5 From d2140a4b9a4f0b9c9afc0a2fbff55acb5399eec6 Mon Sep 17 00:00:00 2001 From: Sergio Salazar Date: Thu, 13 Oct 2016 16:41:49 -0600 Subject: [PATCH 003/280] Enable unix domain socket binding If hostname starts with 'unix://', assume a unix domain socket instead of a TCP socket. --- docs/serving.rst | 12 +++++++ tests/conftest.py | 3 +- tests/test_serving.py | 33 +++++++++++++++++-- werkzeug/serving.py | 77 +++++++++++++++++++++++++++++++------------ 4 files changed, 100 insertions(+), 25 deletions(-) diff --git a/docs/serving.rst b/docs/serving.rst index 39f618f3a..61da22ca3 100644 --- a/docs/serving.rst +++ b/docs/serving.rst @@ -225,3 +225,15 @@ discouraged because modern browsers do a bad job at supporting them for security reasons. This feature requires the pyOpenSSL library to be installed. + +Unix Sockets +------------ +The builtin server supports Unix socket binding. This means that it is +possible to start the server in a way that it will listen to a unix +socket instead of a TCP socket. + +In order to use a unix socket the `hostname` parameter of :func:`run_simple` +method must start with `'unix://'`:: + + from werkzeug.serving import run_simple + run_simple('unix://example.sock', 0, app) diff --git a/tests/conftest.py b/tests/conftest.py index cd78d8ceb..ee37c3956 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,7 +125,8 @@ def run_dev_server(application): appfile = app_pkg.join('__init__.py') port = next(port_generator) appfile.write('\n\n'.join(( - 'kwargs = dict(port=%d)' % port, + "kwargs = {{'hostname': 'localhost', 'port': {port:d}}}".format( + port=port), textwrap.dedent(application) ))) diff --git a/tests/test_serving.py b/tests/test_serving.py index 03bdf5b54..7ba21bd4d 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -9,12 +9,12 @@ :license: BSD, see LICENSE for more details. """ import os +import socket import ssl +import subprocess import sys import textwrap import time -import subprocess - try: import OpenSSL @@ -450,7 +450,6 @@ def app(environ, start_response): from httplib import HTTPConnection else: from http.client import HTTPConnection - conn = HTTPConnection('127.0.0.1', server.port) conn.connect() conn.putrequest('GET', '/') @@ -470,3 +469,31 @@ def app(environ, start_response): assert res.read() == b'a ,b,c ,d' conn.close() + + +@pytest.mark.skipif( + not hasattr(socket, 'AF_UNIX'), reason='Only works on UNIX') +def test_unix_socket(tmpdir, dev_server): + socket_f = str(tmpdir.join('socket')) + dev_server(''' + def app(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/html')]) + return [b'hello'] + kwargs['hostname'] = {socket!r} + '''.format(socket='unix://' + socket_f)) + + for i in reversed(range(10)): + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(socket_f) + s.send(b'GET / HTTP/1.0\n\n\n') + data = s.recv(1024) + assert b'hello' in data + s.shutdown(1) + s.close() + except socket.error: + if i == 0: + raise + time.sleep(0.1) + else: + break diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 4b0d16439..307d03a2f 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -166,6 +166,12 @@ def shutdown_server(): self.server.shutdown_signal = True url_scheme = self.server.ssl_context is None and 'http' or 'https' + if not self.client_address: + self.client_address = '' + if isinstance(self.client_address, str): + self.client_address = (self.client_address, 0) + else: + pass path_info = url_unquote(request_url.path) environ = { @@ -344,6 +350,10 @@ def version_string(self): def address_string(self): if getattr(self, 'environ', None): return self.environ['REMOTE_ADDR'] + elif not self.client_address: + return '' + elif isinstance(self.client_address, str): + return self.client_address else: return self.client_address[0] @@ -556,7 +566,7 @@ def is_ssl_error(error=None): return isinstance(error, exc_types) -def select_ip_version(host, port): +def select_address_family(host, port): """Returns AF_INET4 or AF_INET6 depending on where to connect to.""" # disabled due to problems with current ipv6 implementations # and various operating systems. Probably this code also is @@ -570,7 +580,9 @@ def select_ip_version(host, port): # return info[0][0] # except socket.gaierror: # pass - if ':' in host and hasattr(socket, 'AF_INET6'): + if host.startswith('unix://'): + return socket.AF_UNIX + elif ':' in host and hasattr(socket, 'AF_INET6'): return socket.AF_INET6 return socket.AF_INET @@ -578,11 +590,13 @@ def select_ip_version(host, port): def get_sockaddr(host, port, family): """Returns a fully qualified socket address, that can properly used by socket.bind""" + if family == socket.AF_UNIX: + return host.split('://', 1)[1] try: - res = socket.getaddrinfo(host, port, family, - socket.SOCK_STREAM, socket.SOL_TCP) + res = socket.getaddrinfo( + host, port, family, socket.SOCK_STREAM, socket.SOL_TCP) except socket.gaierror: - return (host, port) + return host, port return res[0][4] @@ -593,19 +607,31 @@ class BaseWSGIServer(HTTPServer, object): multiprocess = False request_queue_size = LISTEN_QUEUE - def __init__(self, host, port, app, handler=None, - passthrough_errors=False, ssl_context=None, fd=None): + def __init__( + self, host, port, app, handler=None, passthrough_errors=False, + ssl_context=None, fd=None + ): if handler is None: handler = WSGIRequestHandler - self.address_family = select_ip_version(host, port) + self.address_family = select_address_family(host, port) if fd is not None: - real_sock = socket.fromfd(fd, self.address_family, - socket.SOCK_STREAM) + real_sock = socket.fromfd( + fd, self.address_family, socket.SOCK_STREAM) port = 0 - HTTPServer.__init__(self, get_sockaddr(host, int(port), - self.address_family), handler) + + server_address = get_sockaddr(host, int(port), self.address_family) + + # remove socket file if it already exists + if ( + self.address_family == socket.AF_UNIX + and os.path.exists(server_address) + ): + os.unlink(server_address) + + HTTPServer.__init__(self, server_address, handler) + self.app = app self.passthrough_errors = passthrough_errors self.shutdown_signal = False @@ -738,7 +764,9 @@ def run_simple(hostname, port, application, use_reloader=False, through the `reloader_type` parameter. See :ref:`reloader` for more information. - :param hostname: The host for the application. eg: ``'localhost'`` + :param hostname: The host for the application. eg: ``'localhost'``. + In order to use an unix socket instead of a TCP socket + ``hostname`` must start with ``'unix://'``. :param port: The port for the server. eg: ``8080`` :param application: the WSGI application to execute :param use_reloader: should the server automatically restart the python @@ -786,13 +814,16 @@ def run_simple(hostname, port, application, use_reloader=False, def log_startup(sock): display_hostname = hostname not in ('', '*') and hostname or 'localhost' - if ':' in display_hostname: - display_hostname = '[%s]' % display_hostname quit_msg = '(Press CTRL+C to quit)' - port = sock.getsockname()[1] - _log('info', ' * Running on %s://%s:%d/ %s', - ssl_context is None and 'http' or 'https', - display_hostname, port, quit_msg) + if sock.family is socket.AF_UNIX: + _log('info', ' * Running on %s %s', display_hostname, quit_msg) + else: + if ':' in display_hostname: + display_hostname = '[%s]' % display_hostname + port = sock.getsockname()[1] + _log('info', ' * Running on %s://%s:%d/ %s', + ssl_context is None and 'http' or 'https', + display_hostname, port, quit_msg) def inner(): try: @@ -820,10 +851,11 @@ def inner(): # Create and destroy a socket so that any exceptions are # raised before we spawn a separate Python interpreter and # lose this ability. - address_family = select_ip_version(hostname, port) + address_family = select_address_family(hostname, port) + server_address = get_sockaddr(hostname, port, address_family) s = socket.socket(address_family, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(get_sockaddr(hostname, port, address_family)) + s.bind(server_address) if hasattr(s, 'set_inheritable'): s.set_inheritable(True) @@ -835,6 +867,9 @@ def inner(): log_startup(s) else: s.close() + if address_family is socket.AF_UNIX: + _log('info', "Unlinking %s" % server_address) + os.unlink(server_address) # Do not use relative imports, otherwise "python -m werkzeug.serving" # breaks. From d72a6777c137025a049133962ca2f365b20b2599 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 31 Dec 2017 14:32:53 +0100 Subject: [PATCH 004/280] 0.15-dev --- werkzeug/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/__init__.py b/werkzeug/__init__.py index 6c1a44e3e..606ece280 100644 --- a/werkzeug/__init__.py +++ b/werkzeug/__init__.py @@ -19,7 +19,7 @@ from werkzeug._compat import iteritems -__version__ = '0.14' +__version__ = '0.15.dev' # This import magic raises concerns quite often which is why the implementation From 86f6d0bc7c410f70ac290eef018ee48a46563d4b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 31 Dec 2017 22:14:06 +0100 Subject: [PATCH 005/280] Fixed a regression in the development server This fixes a type error being thrown for some requests. --- werkzeug/serving.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 6d50ef90b..902f1aa1c 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -220,7 +220,8 @@ def write(data): code, msg = status.split(None, 1) except ValueError: code, msg = status, "" - self.send_response(int(code), msg) + code = int(code) + self.send_response(code, msg) header_keys = set() for key, value in response_headers: self.send_header(key, value) @@ -228,7 +229,7 @@ def write(data): header_keys.add(key) if not ('content-length' in header_keys or environ['REQUEST_METHOD'] == 'HEAD' or - status < 200 or status in (204, 304)): + code < 200 or code in (204, 304)): self.close_connection = True self.send_header('Connection', 'close') if 'server' not in header_keys: From 97c91440410b93d3712651262eff9a86af391b57 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 31 Dec 2017 22:16:59 +0100 Subject: [PATCH 006/280] 0.14.2 In case we need more --- werkzeug/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/__init__.py b/werkzeug/__init__.py index 56404f4c1..9d55943f7 100644 --- a/werkzeug/__init__.py +++ b/werkzeug/__init__.py @@ -19,7 +19,7 @@ from werkzeug._compat import iteritems -__version__ = '0.14.1' +__version__ = '0.14.2.dev' # This import magic raises concerns quite often which is why the implementation From 7a25d91795c2d0a671dc281839656cdf86928d93 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 3 Jan 2018 09:00:02 -0800 Subject: [PATCH 007/280] min support is py 2.7 and 3.4 copy installation docs from flask remove py3 docs, move unicode explanation to wsgi helper docs --- .appveyor.yml | 18 +++- docs/contents.rst.inc | 1 - docs/installation.rst | 218 +++++++++++++++++++++++++----------------- docs/python3.rst | 73 -------------- docs/wsgi.rst | 46 +++++++-- setup.py | 4 +- tests/test_http.py | 6 +- tox.ini | 7 +- 8 files changed, 186 insertions(+), 187 deletions(-) delete mode 100644 docs/python3.rst diff --git a/.appveyor.yml b/.appveyor.yml index 9e59c684d..3a7d2f631 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,15 +1,23 @@ environment: global: - TOXENV: "py" + TOXENV: py matrix: - - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python36" + - PYTHON: C:\Python36 + - PYTHON: C:\Python27 + +init: + - SET PATH=%PYTHON%;%PATH% install: - - "%PYTHON%\\python.exe -m pip install -U pip setuptools wheel tox" + - python -m pip install -U pip setuptools wheel tox build: false test_script: - - "%PYTHON%\\python.exe -m tox" + - python -m tox + +branches: + only: + - master + - /^.*-maintenance$/ diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index d5d29a85b..c39934bad 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -12,7 +12,6 @@ should start here. tutorial levels quickstart - python3 Serving and Testing ------------------- diff --git a/docs/installation.rst b/docs/installation.rst index c07d4521a..803cbedbb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,133 +1,173 @@ -============ +.. _installation: + Installation ============ -Werkzeug requires at least Python 2.6 to work correctly. If you do need -to support an older version you can download an older version of Werkzeug -though we strongly recommend against that. Werkzeug currently has -experimental support for Python 3. For more information about the -Python 3 support see :ref:`python3`. +Python Version +-------------- -Installing a released version -============================= +We recommend using the latest version of Python 3. Werkzeug supports +Python 3.4 and newer and Python 2.7. -As a Python egg (via easy_install or pip) ------------------------------------------ -You can install the most recent Werkzeug version using `easy_install`_:: +Dependencies +------------ - easy_install Werkzeug +Werkzeug does not have any direct dependencies. -Alternatively you can also use pip:: - pip install Werkzeug +Optional dependencies +~~~~~~~~~~~~~~~~~~~~~ + +These distributions will not be installed automatically. Werkzeug will +detect and use them if you install them. + +* `SimpleJSON`_ is a fast JSON implementation that is compatible with + Python's ``json`` module. It is preferred for JSON operations if it is + installed. +* `termcolor`_ provides request log highlighting when using the + development server. +* `Watchdog`_ provides a faster, more efficient reloader for the + development server. + +.. _SimpleJSON: https://simplejson.readthedocs.io/ +.. _termcolor: https://pypi.org/project/termcolor/ +.. _Watchdog: https://pythonhosted.org/watchdog/ + + +Virtual environments +-------------------- + +Use a virtual environment to manage the dependencies for your project, +both in development and in production. + +What problem does a virtual environment solve? The more Python +projects you have, the more likely it is that you need to work with +different versions of Python libraries, or even Python itself. Newer +versions of libraries for one project can break compatibility in +another project. + +Virtual environments are independent groups of Python libraries, one for +each project. Packages installed for one project will not affect other +projects or the operating system's packages. + +Python 3 comes bundled with the :mod:`venv` module to create virtual +environments. If you're using a modern version of Python, you can +continue on to the next section. + +If you're using Python 2, see :ref:`install-install-virtualenv` first. + +.. _install-create-env: + +Create an environment +~~~~~~~~~~~~~~~~~~~~~ -Either way we strongly recommend using these tools in combination with -:ref:`virtualenv`. +Create a project folder and a :file:`venv` folder within: -This will install a Werkzeug egg in your Python installation's `site-packages` -directory. +.. code-block:: sh -From the tarball release -------------------------- + mkdir myproject + cd myproject + python3 -m venv venv -1. Download the most recent tarball from the `download page`_. -2. Unpack the tarball. -3. ``python setup.py install`` +On Windows: -Note that the last command will automatically download and install -`setuptools`_ if you don't already have it installed. This requires a working -Internet connection. +.. code-block:: bat -This will install Werkzeug into your Python installation's `site-packages` -directory. + py -3 -m venv venv +If you needed to install virtualenv because you are on an older version +of Python, use the following command instead: -Installing the development version -================================== +.. code-block:: sh -1. Install `Git`_ -2. ``git clone git://github.com/pallets/werkzeug.git`` -3. ``cd werkzeug`` -4. ``pip install --editable .`` + virtualenv venv -.. _virtualenv: +On Windows: -virtualenv -========== +.. code-block:: bat -Virtualenv is probably what you want to use during development, and in -production too if you have shell access there. + \Python27\Scripts\virtualenv.exe venv + + +Activate the environment +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before you work on your project, activate the corresponding environment: + +.. code-block:: sh + + . venv/bin/activate + +On Windows: + +.. code-block:: bat + + venv\Scripts\activate + +Your shell prompt will change to show the name of the activated +environment. + + +Install Werkzeug +---------------- + +Within the activated environment, use the following command to install +Werkzeug: + +.. code-block:: sh + + pip install Werkzeug -What problem does virtualenv solve? If you like Python as I do, -chances are you want to use it for other projects besides Werkzeug-based -web applications. But the more projects you have, the more likely it is -that you will be working with different versions of Python itself, or at -least different versions of Python libraries. Let's face it; quite often -libraries break backwards compatibility, and it's unlikely that any serious -application will have zero dependencies. So what do you do if two or more -of your projects have conflicting dependencies? -Virtualenv to the rescue! It basically enables multiple side-by-side -installations of Python, one for each project. It doesn't actually -install separate copies of Python, but it does provide a clever way -to keep different project environments isolated. +Living on the edge +~~~~~~~~~~~~~~~~~~ -So let's see how virtualenv works! +If you want to work with the latest Werkzeug code before it's released, +install or update the code from the master branch: -If you are on Mac OS X or Linux, chances are that one of the following two -commands will work for you:: +.. code-block:: sh - $ sudo easy_install virtualenv + pip install -U https://github.com/pallets/werkzeug/archive/master.tar.gz -or even better:: - $ sudo pip install virtualenv +.. _install-install-virtualenv: -One of these will probably install virtualenv on your system. Maybe it's -even in your package manager. If you use Ubuntu, try:: +Install virtualenv +------------------ - $ sudo apt-get install python-virtualenv +If you are using Python 2, the venv module is not available. Instead, +install `virtualenv`_. -If you are on Windows and don't have the `easy_install` command, you must -install it first. Once you have it installed, run the same commands as -above, but without the `sudo` prefix. +On Linux, virtualenv is provided by your package manager: -Once you have virtualenv installed, just fire up a shell and create -your own environment. I usually create a project folder and an `env` -folder within:: +.. code-block:: sh - $ mkdir myproject - $ cd myproject - $ virtualenv env - New python executable in env/bin/python - Installing setuptools............done. + # Debian, Ubuntu + sudo apt-get install python-virtualenv -Now, whenever you want to work on a project, you only have to activate -the corresponding environment. On OS X and Linux, do the following:: + # CentOS, Fedora + sudo yum install python-virtualenv - $ . env/bin/activate + # Arch + sudo pacman -S python-virtualenv -(Note the space between the dot and the script name. The dot means that -this script should run in the context of the current shell. If this command -does not work in your shell, try replacing the dot with ``source``) +If you are on Mac OS X or Windows, download `get-pip.py`_, then: -If you are a Windows user, the following command is for you:: +.. code-block:: sh - $ env\scripts\activate + sudo python2 Downloads/get-pip.py + sudo python2 -m pip install virtualenv -Either way, you should now be using your virtualenv (see how the prompt of -your shell has changed to show the virtualenv). +On Windows, as an administrator: -Now you can just enter the following command to get Werkzeug activated in -your virtualenv:: +.. code-block:: bat - $ pip install Werkzeug + \Python27\python.exe Downloads\get-pip.py + \Python27\python.exe -m pip install virtualenv -A few seconds later you are good to go. +Now you can continue to :ref:`install-create-env`. -.. _download page: https://pypi.python.org/pypi/Werkzeug -.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools -.. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall -.. _Git: http://git-scm.org/ +.. _virtualenv: https://virtualenv.pypa.io/ +.. _get-pip.py: https://bootstrap.pypa.io/get-pip.py diff --git a/docs/python3.rst b/docs/python3.rst deleted file mode 100644 index 5597fe542..000000000 --- a/docs/python3.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _python3: - -============== -Python 3 Notes -============== - -Since version 0.9, Werkzeug supports Python 3.3+ in addition to versions 2.6 -and 2.7. Older Python 3 versions such as 3.2 or 3.1 are not supported. - -This part of the documentation outlines special information required to -use Werkzeug and WSGI on Python 3. - -.. warning:: - - Python 3 support in Werkzeug is currently highly experimental. Please - give feedback on it and help us improve it. - - -WSGI Environment -================ - -The WSGI environment on Python 3 works slightly different than it does on -Python 2. For the most part Werkzeug hides the differences from you if -you work on the higher level APIs. The main difference between Python 2 -and Python 3 is that on Python 2 the WSGI environment contains bytes -whereas the environment on Python 3 contains a range of differently -encoded strings. - -There are two different kinds of strings in the WSGI environ on Python 3: - -- unicode strings restricted to latin1 values. These are used for - HTTP headers and a few other things. -- unicode strings carrying binary payload, roundtripped through latin1 - values. This is usually referred as “WSGI encoding dance” throughout - Werkzeug. - -Werkzeug provides you with functionality to deal with these automatically -so that you don't need to be aware of the inner workings. The following -functions and classes should be used to read information out of the -WSGI environment: - -- :func:`~werkzeug.wsgi.get_current_url` -- :func:`~werkzeug.wsgi.get_host` -- :func:`~werkzeug.wsgi.get_script_name` -- :func:`~werkzeug.wsgi.get_path_info` -- :func:`~werkzeug.wsgi.get_query_string` -- :func:`~werkzeug.datastructures.EnvironHeaders` - -Applications are strongly discouraged to create and modify a WSGI -environment themselves on Python 3 unless they take care of the proper -decoding step. All high level interfaces in Werkzeug will apply the -correct encoding and decoding steps as necessary. - -URLs -==== - -URLs in Werkzeug attempt to represent themselves as unicode strings on -Python 3. All the parsing functions generally also provide functionality -that allow operations on bytes. In some cases functions that deal with -URLs allow passing in `None` as charset to change the return value to byte -objects. Internally Werkzeug will now unify URIs and IRIs as much as -possible. - -Request Cleanup -=============== - -Request objects on Python 3 and PyPy require explicit closing when file -uploads are involved. This is required to properly close temporary file -objects created by the multipart parser. For that purpose the ``close()`` -method was introduced. - -In addition to that request objects now also act as context managers that -automatically close. diff --git a/docs/wsgi.rst b/docs/wsgi.rst index 4391c89a6..4e7fa0951 100644 --- a/docs/wsgi.rst +++ b/docs/wsgi.rst @@ -1,17 +1,16 @@ -============ WSGI Helpers ============ .. module:: werkzeug.wsgi The following classes and functions are designed to make working with -the WSGI specification easier or operate on the WSGI layer. All the +the WSGI specification easier or operate on the WSGI layer. All the functionality from this module is available on the high-level -:ref:`Request/Response classes `. +:ref:`Request / Response classes `. Iterator / Stream Helpers -========================= +------------------------- These classes and functions simplify working with the WSGI application iterator and the input stream. @@ -21,7 +20,7 @@ iterator and the input stream. .. autoclass:: FileWrapper .. autoclass:: LimitedStream - :members: + :members: .. autofunction:: make_line_iter @@ -31,7 +30,7 @@ iterator and the input stream. Environ Helpers -=============== +--------------- These functions operate on the WSGI environment. They extract useful information or perform common manipulations: @@ -58,9 +57,42 @@ information or perform common manipulations: .. autofunction:: host_is_trusted + Convenience Helpers -=================== +------------------- .. autofunction:: responder .. autofunction:: werkzeug.testapp.test_app + + +Bytes, Strings, and Encodings +----------------------------- + +The WSGI environment on Python 3 works slightly different than it does +on Python 2. Werkzeug hides the differences from you if you use the +higher level APIs. + +The WSGI specification (`PEP 3333`_) decided to always use the native +``str`` type. On Python 2 this means the raw bytes are passed through +and can be decoded directly. On Python 3, however, the raw bytes are +always decoded using the ISO-8859-1 charset to produce a Unicode string. + +Python 3 Unicode strings in the WSGI environment are restricted to +ISO-8859-1 code points. If a string read from the environment might +contain characters outside that charset, it must first be decoded to +bytes as ISO-8859-1, then encoded to a Unicode string using the proper +charset (typically UTF-8). The reverse is done when writing to the +environ. This is known as the "WSGI encoding dance". + +Werkzeug provides functions to deal with this automatically so that you +don't need to be aware of the inner workings. Use the functions on this +page as well as :func:`~werkzeug.datastructures.EnvironHeaders` to read +data out of the WSGI environment. + +Applications should avoid manually creating or modifying a WSGI +environment unless they take care of the proper encoding or decoding +step. All high level interfaces in Werkzeug will apply the encoding and +decoding as necessary. + +.. _PEP 3333: https://www.python.org/dev/peps/pep-3333/#unicode-issues diff --git a/setup.py b/setup.py index 4749ac3f6..66c5776ae 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='Werkzeug', version=version, - url='https://www.palletsprojects.org/p/werkzeug/', + url='https://www.palletsprojects.com/p/werkzeug/', license='BSD', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', @@ -28,10 +28,8 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/tests/test_http.py b/tests/test_http.py index 521cdeec5..6f28f1536 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -385,11 +385,11 @@ def test_cookies(self): def test_bad_cookies(self): strict_eq( - dict(http.parse_cookie('first=IamTheFirst ; a=1; oops ; a=2 ;' - 'second = andMeTwo;')), + dict(http.parse_cookie( + 'first=IamTheFirst ; a=1; oops ; a=2 ;second = andMeTwo;' + )), { 'first': u'IamTheFirst', - 'a': u'1', 'a': u'2', 'oops': u'', 'second': u'andMeTwo', diff --git a/tox.ini b/tox.ini index 26af92dcb..9c7bc61da 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ envlist = py{36,27}-hypothesis-uwsgi py{35,34,py} - # run py33, py26 manually stylecheck docs-html coverage-report @@ -12,12 +11,8 @@ passenv = LANG setenv = TOX_ENVTMPDIR={envtmpdir} -usedevelop = true deps = - # remove once we drop support for 2.6, 3.3 - py26,py33: py<1.5 - py26,py33: pytest<3.3 - + pytest pytest-xprocess coverage requests From f797fcc02f455f52e3b944adf2ffd5b278db563f Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 3 Jan 2018 18:52:46 +0100 Subject: [PATCH 008/280] Remove useless parentheses --- werkzeug/debug/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/debug/__init__.py b/werkzeug/debug/__init__.py index 14fe19352..2622df740 100644 --- a/werkzeug/debug/__init__.py +++ b/werkzeug/debug/__init__.py @@ -93,7 +93,7 @@ def _generate(): 'SOFTWARE\\Microsoft\\Cryptography', 0, wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk: machineGuid, wrType = wr.QueryValueEx(rk, 'MachineGuid') - if (wrType == wr.REG_SZ): + if wrType == wr.REG_SZ: return machineGuid.encode('utf-8') else: return machineGuid From 3dd2d846e672c79bc547e56f368ec3b65c9f6d41 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 3 Jan 2018 09:53:56 -0800 Subject: [PATCH 009/280] undo accidental revert from merged PR --- AUTHORS | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/AUTHORS b/AUTHORS index dc1c35283..82cd58531 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,42 +9,7 @@ are: A full list of contributors is available from git with: -- Georg Brandl -- Leif K-Brooks -- Thomas Johansson -- Marian Sigler -- Ronny Pfannschmidt -- Noah Slater -- Alec Thomas -- Shannon Behrens -- Christoph Rauch -- Clemens Hermann -- Jason Kirtland -- Ali Afshar -- Christopher Grebs -- Sean Cazzell -- Florent Xicluna -- Kyle Dawkins -- Pedro Algarvio -- Zahari Petkov -- Ludvig Ericson -- Kenneth Reitz -- Daniel Neuhäuser -- Markus Unterwaditzer -- Joe Esposito -- Abhinav Upadhyay -- immerrr -- Cédric Krier -- Phil Jones -- Michael Hunsinger -- Lars Holm Nielsen -- Joël Charles -- Benjamin Dopplinger -- Nils Steinger -- Mark Szymanski -- Andrew Bednar -- Craig Blaszczyk -- Felix König + git shortlog -sne The SSL parts of the Werkzeug development server are partially taken from Paste. The same is true for the range support which comes from From 6df9e5bdb9d216fcf2989ce051cd11b763cef4a0 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 3 Jan 2018 12:05:23 -0800 Subject: [PATCH 010/280] Revert "Add support for non-ascii filenames when uploading files using multipart/form-data (#1175)" Non-ASCII filenames were already supported. This reverts commit eb7b4255d69c935103e044b32b3a046c288d96e6. --- werkzeug/formparser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/werkzeug/formparser.py b/werkzeug/formparser.py index c56859e90..9dd8331fc 100644 --- a/werkzeug/formparser.py +++ b/werkzeug/formparser.py @@ -413,9 +413,7 @@ def parse_lines(self, file, boundary, content_length, cap_at_buffer=True): disposition, extra = parse_options_header(disposition) transfer_encoding = self.get_part_encoding(headers) name = extra.get('name') - - # Accept filename* to support non-ascii filenames as per rfc2231 - filename = extra.get('filename') or extra.get('filename*') + filename = extra.get('filename') # if no content type is given we stream into memory. A list is # used as a temporary container. From 71cdc053b0b04f4a5e61071e30134fa487cdd6ab Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 3 Jan 2018 21:29:39 -0800 Subject: [PATCH 011/280] remove re-added old cache tests move changes to current test file add tests for null cache flip default for guaranteed_deletes flag --- tests/contrib/cache/test_cache.py | 33 ++-- tests/contrib/test_cache.py | 288 ------------------------------ 2 files changed, 21 insertions(+), 300 deletions(-) delete mode 100644 tests/contrib/test_cache.py diff --git a/tests/contrib/cache/test_cache.py b/tests/contrib/cache/test_cache.py index 8dce4277e..3e6ee7f36 100644 --- a/tests/contrib/cache/test_cache.py +++ b/tests/contrib/cache/test_cache.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ import errno + import pytest from werkzeug._compat import text_type @@ -31,9 +32,9 @@ memcache = None -class CacheTests(object): +class CacheTestsBase(object): _can_use_fast_sleep = True - _guaranteed_deletes = False + _guaranteed_deletes = True @pytest.fixture def fast_sleep(self, monkeypatch): @@ -57,6 +58,8 @@ def c(self, make_cache): """Return a cache instance.""" return make_cache() + +class GenericCacheTests(CacheTestsBase): def test_generic_get_dict(self, c): assert c.set('a', 'a') assert c.set('b', 'b') @@ -142,9 +145,7 @@ def test_generic_has(self, c): assert c.has('spam') in (False, 0) -class TestSimpleCache(CacheTests): - _guaranteed_deletes = True - +class TestSimpleCache(GenericCacheTests): @pytest.fixture def make_cache(self): return cache.SimpleCache @@ -159,9 +160,7 @@ def test_purge(self): assert len(c._cache) == 3 -class TestFileSystemCache(CacheTests): - _guaranteed_deletes = True - +class TestFileSystemCache(GenericCacheTests): @pytest.fixture def make_cache(self, tmpdir): return lambda **kw: cache.FileSystemCache(cache_dir=str(tmpdir), **kw) @@ -216,9 +215,8 @@ def test_count_file_accuracy(self, c): # don't use pytest.mark.skipif on subclasses # https://bitbucket.org/hpk42/pytest/issue/568 # skip happens in requirements fixture instead -class TestRedisCache(CacheTests): +class TestRedisCache(GenericCacheTests): _can_use_fast_sleep = False - _guaranteed_deletes = True @pytest.fixture(scope='class', autouse=True) def requirements(self, subprocess): @@ -268,8 +266,9 @@ def test_empty_host(self): assert text_type(exc_info.value) == 'RedisCache host parameter may not be None' -class TestMemcachedCache(CacheTests): +class TestMemcachedCache(GenericCacheTests): _can_use_fast_sleep = False + _guaranteed_deletes = False @pytest.fixture(scope='class', autouse=True) def requirements(self, subprocess): @@ -312,8 +311,9 @@ def test_huge_timeouts(self, c): assert c.get('foo') == 'bar' -class TestUWSGICache(CacheTests): +class TestUWSGICache(GenericCacheTests): _can_use_fast_sleep = False + _guaranteed_deletes = False @pytest.fixture(scope='class', autouse=True) def requirements(self): @@ -330,3 +330,12 @@ def make_cache(self): c = cache.UWSGICache(cache='werkzeugtest') yield lambda: c c.clear() + + +class TestNullCache(CacheTestsBase): + @pytest.fixture(scope='class', autouse=True) + def make_cache(self): + return cache.NullCache + + def test_has(self, c): + assert not c.has('foo') diff --git a/tests/contrib/test_cache.py b/tests/contrib/test_cache.py deleted file mode 100644 index a6eec991f..000000000 --- a/tests/contrib/test_cache.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8 -*- -""" - tests.cache - ~~~~~~~~~~~ - - Tests the cache system - - :copyright: (c) 2014 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -import pytest -import os -import random - -from werkzeug.contrib import cache - -try: - import redis -except ImportError: - redis = None - -try: - import pylibmc as memcache -except ImportError: - try: - from google.appengine.api import memcache - except ImportError: - try: - import memcache - except ImportError: - memcache = None - - -class CacheTests(object): - _can_use_fast_sleep = True - - @pytest.fixture - def make_cache(self): - '''Return a cache class or factory.''' - raise NotImplementedError() - - @pytest.fixture - def fast_sleep(self, monkeypatch): - if self._can_use_fast_sleep: - def sleep(delta): - orig_time = cache.time - monkeypatch.setattr(cache, 'time', lambda: orig_time() + delta) - - return sleep - else: - import time - return time.sleep - - @pytest.fixture - def c(self, make_cache): - '''Return a cache instance.''' - return make_cache() - - def test_generic_get_dict(self, c): - assert c.set('a', 'a') - assert c.set('b', 'b') - d = c.get_dict('a', 'b') - assert 'a' in d - assert 'a' == d['a'] - assert 'b' in d - assert 'b' == d['b'] - - def test_generic_set_get(self, c): - for i in range(3): - assert c.set(str(i), i * i) - for i in range(3): - result = c.get(str(i)) - assert result == i * i, result - - def test_generic_get_set(self, c): - assert c.set('foo', ['bar']) - assert c.get('foo') == ['bar'] - - def test_generic_get_many(self, c): - assert c.set('foo', ['bar']) - assert c.set('spam', 'eggs') - assert list(c.get_many('foo', 'spam')) == [['bar'], 'eggs'] - - def test_generic_set_many(self, c): - assert c.set_many({'foo': 'bar', 'spam': ['eggs']}) - assert c.get('foo') == 'bar' - assert c.get('spam') == ['eggs'] - - def test_generic_expire(self, c, fast_sleep): - assert c.set('foo', 'bar', 1) - fast_sleep(5) - assert c.get('foo') is None - - def test_generic_add(self, c): - # sanity check that add() works like set() - assert c.add('foo', 'bar') - assert c.get('foo') == 'bar' - assert not c.add('foo', 'qux') - assert c.get('foo') == 'bar' - - def test_generic_delete(self, c): - assert c.add('foo', 'bar') - assert c.get('foo') == 'bar' - assert c.delete('foo') - assert c.get('foo') is None - - def test_generic_delete_many(self, c): - assert c.add('foo', 'bar') - assert c.add('spam', 'eggs') - assert c.delete_many('foo', 'spam') - assert c.get('foo') is None - assert c.get('spam') is None - - def test_generic_inc_dec(self, c): - assert c.set('foo', 1) - assert c.inc('foo') == c.get('foo') == 2 - assert c.dec('foo') == c.get('foo') == 1 - assert c.delete('foo') - - def test_generic_true_false(self, c): - assert c.set('foo', True) - assert c.get('foo') in (True, 1) - assert c.set('bar', False) - assert c.get('bar') in (False, 0) - - def test_generic_no_timeout(self, c, fast_sleep): - # Timeouts of zero should cause the cache to never expire - c.set('foo', 'bar', 0) - fast_sleep(random.randint(1, 5)) - assert c.get('foo') == 'bar' - - def test_generic_timeout(self, c, fast_sleep): - # Check that cache expires when the timeout is reached - timeout = random.randint(1, 5) - c.set('foo', 'bar', timeout) - assert c.get('foo') == 'bar' - # sleep a bit longer than timeout to ensure there are no - # race conditions - fast_sleep(timeout + 5) - assert c.get('foo') is None - - def test_generic_has(self, c): - assert c.has('foo') in (False, 0) - assert c.has('spam') in (False, 0) - assert c.set('foo', 'bar') - assert c.has('foo') in (True, 1) - assert c.has('spam') in (False, 0) - c.delete('foo') - assert c.has('foo') in (False, 0) - assert c.has('spam') in (False, 0) - - -class TestSimpleCache(CacheTests): - - @pytest.fixture - def make_cache(self): - return cache.SimpleCache - - def test_purge(self): - c = cache.SimpleCache(threshold=2) - c.set('a', 'a') - c.set('b', 'b') - c.set('c', 'c') - c.set('d', 'd') - # Cache purges old items *before* it sets new ones. - assert len(c._cache) == 3 - - -class TestFileSystemCache(CacheTests): - - @pytest.fixture - def make_cache(self, tmpdir): - return lambda **kw: cache.FileSystemCache(cache_dir=str(tmpdir), **kw) - - def test_filesystemcache_prune(self, make_cache): - THRESHOLD = 13 - c = make_cache(threshold=THRESHOLD) - for i in range(2 * THRESHOLD): - assert c.set(str(i), i) - cache_files = os.listdir(c._path) - assert len(cache_files) <= THRESHOLD - - def test_filesystemcache_clear(self, c): - assert c.set('foo', 'bar') - cache_files = os.listdir(c._path) - # count = 2 because of the count file - assert len(cache_files) == 2 - assert c.clear() - - # The only file remaining is the count file - cache_files = os.listdir(c._path) - assert os.listdir(c._path) == [ - os.path.basename(c._get_filename(c._fs_count_file))] - - -# Don't use pytest marker -# https://bitbucket.org/hpk42/pytest/issue/568 -if redis is not None: - class TestRedisCache(CacheTests): - _can_use_fast_sleep = False - - @pytest.fixture(params=[ - ([], dict()), - ([redis.Redis()], dict()), - ([redis.StrictRedis()], dict()) - ]) - def make_cache(self, xprocess, request): - def preparefunc(cwd): - return 'Ready to accept connections', ['redis-server'] - - xprocess.ensure('redis_server', preparefunc) - args, kwargs = request.param - c = cache.RedisCache(*args, key_prefix='werkzeug-test-case:', - **kwargs) - request.addfinalizer(c.clear) - return lambda: c - - def test_compat(self, c): - assert c._client.set(c.key_prefix + 'foo', 'Awesome') - assert c.get('foo') == b'Awesome' - assert c._client.set(c.key_prefix + 'foo', '42') - assert c.get('foo') == 42 - - -# Don't use pytest marker -# https://bitbucket.org/hpk42/pytest/issue/568 -if memcache is not None: - class TestMemcachedCache(CacheTests): - _can_use_fast_sleep = False - - @pytest.fixture - def make_cache(self, xprocess, request): - def preparefunc(cwd): - return '', ['memcached'] - - xprocess.ensure('memcached', preparefunc) - c = cache.MemcachedCache(key_prefix='werkzeug-test-case:') - request.addfinalizer(c.clear) - return lambda: c - - def test_compat(self, c): - assert c._client.set(c.key_prefix + 'foo', 'bar') - assert c.get('foo') == 'bar' - - def test_huge_timeouts(self, c): - # Timeouts greater than epoch are interpreted as POSIX timestamps - # (i.e. not relative to now, but relative to epoch) - import random - epoch = 2592000 - timeout = epoch + random.random() * 100 - c.set('foo', 'bar', timeout) - assert c.get('foo') == 'bar' - - -def _running_in_uwsgi(): - try: - import uwsgi # NOQA - except ImportError: - return False - else: - return True - - -@pytest.mark.skipif(not _running_in_uwsgi(), - reason="uWSGI module can't be imported outside of uWSGI") -class TestUWSGICache(CacheTests): - _can_use_fast_sleep = False - - @pytest.fixture - def make_cache(self, xprocess, request): - c = cache.UWSGICache(cache='werkzeugtest') - request.addfinalizer(c.clear) - return lambda: c - - -class TestNullCache(object): - - @pytest.fixture - def make_cache(self): - return cache.NullCache - - @pytest.fixture - def c(self, make_cache): - return make_cache() - - def test_nullcache_has(self, c): - assert c.has('foo') in (False, 0) - assert c.has('spam') in (False, 0) From 5555a416472ad7241f042052f0063270bc753f87 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 1 Feb 2018 18:14:59 +0900 Subject: [PATCH 012/280] Fix appveyor link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 90a826307..204e2a741 100644 --- a/README.rst +++ b/README.rst @@ -69,7 +69,7 @@ Links * Test status: * Linux, Mac: https://travis-ci.org/pallets/werkzeug - * Windows: https://ci.appveyor.com/project/davidism/werkzeug + * Windows: https://ci.appveyor.com/project/pallets/werkzeug * Test coverage: https://codecov.io/gh/pallets/werkzeug From 730f2434b853f3cb36573275820e23e764b27763 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 1 Feb 2018 18:33:23 +0900 Subject: [PATCH 013/280] Allow failures for Python Nightly --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 224f986ed..0741b603b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,8 @@ matrix: language: generic env: TOXENV=py allow_failures: + - python: nightly + env: TOXENV=py - os: osx language: generic env: TOXENV=py From acdbfc2908f833abc703cbe0b38a32bd0dc32cb2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Mon, 12 Feb 2018 14:47:19 +1100 Subject: [PATCH 014/280] Add translation of werkzeug --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 90a826307..db32dd42a 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ Werkzeug ======== +*werkzeug* German noun: "tool". Etymology: *werk* ("work"), *zeug* ("stuff") + + Werkzeug is a comprehensive `WSGI`_ web application library. It began as a simple collection of various utilities for WSGI applications and has become one of the most advanced WSGI utility libraries. From e07303d989ba023b8edfea0754e6b2d8e4eebaaa Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 Feb 2018 18:24:07 +0900 Subject: [PATCH 015/280] Fix ProxyMiddleware with query string --- tests/test_wsgi.py | 12 ++++++++---- werkzeug/wsgi.py | 8 +++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 53a3a529a..9d2f68072 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -480,7 +480,7 @@ def app(request): return Response(u'%s|%s|%s' % ( request.headers.get('X-Special'), request.environ['HTTP_HOST'], - request.path, + request.full_path, )) ''' @@ -509,11 +509,15 @@ def app(request): assert rv.data == b'ROOT' rv = client.get('/foo/bar') - assert rv.data.decode('ascii') == 'foo|faked.invalid|/foo/bar' + assert rv.data.decode('ascii') == 'foo|faked.invalid|/foo/bar?' rv = client.get('/bar/baz') - assert rv.data.decode('ascii') == 'bar|localhost|/baz' + assert rv.data.decode('ascii') == 'bar|localhost|/baz?' rv = client.get('/autohost/aha') - assert rv.data.decode('ascii') == 'None|%s|/autohost/aha' % url_parse( + assert rv.data.decode('ascii') == 'None|%s|/autohost/aha?' % url_parse( server.url).ascii_host + + # test query string + rv = client.get('/bar/baz?a=a&b=b') + assert rv.data.decode('ascii') == 'bar|localhost|/baz?a=a&b=b' diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index c30021a7b..2d4498a45 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -545,7 +545,13 @@ def application(environ, start_response): timeout=self.timeout, context=opts['ssl_context']) con.connect() - con.putrequest(environ['REQUEST_METHOD'], url_quote(remote_path), + + remote_url = url_quote(remote_path) + querystring = environ['QUERY_STRING'] + if querystring: + remote_url = remote_url + '?' + querystring + + con.putrequest(environ['REQUEST_METHOD'], remote_url, skip_host=True) for k, v in headers: From 8a34546332be0ab0c65494188950346acb82e633 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 Feb 2018 23:10:04 +0900 Subject: [PATCH 016/280] Fix build docs errors. It seems sphinx 1.7 has a bug. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9c7bc61da..9a9e69a1b 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ deps = flake8 commands = flake8 [] [testenv:docs-html] -deps = sphinx +deps = sphinx==1.6.7 commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html [testenv:docs-linkcheck] From eb090c2ba29050c6d8fa9e2a8923996ce7ffc62d Mon Sep 17 00:00:00 2001 From: Konrad Rotkiewicz Date: Thu, 15 Feb 2018 09:26:27 +0100 Subject: [PATCH 017/280] fix IterO.seek on newly created IterO object --- tests/contrib/test_iterio.py | 1 + werkzeug/contrib/iterio.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/contrib/test_iterio.py b/tests/contrib/test_iterio.py index f971277ac..374a78e4f 100644 --- a/tests/contrib/test_iterio.py +++ b/tests/contrib/test_iterio.py @@ -18,6 +18,7 @@ class TestIterO(object): def test_basic_native(self): io = IterIO(["Hello", "World", "1", "2", "3"]) + io.seek(0) assert io.tell() == 0 assert io.read(2) == "He" assert io.tell() == 2 diff --git a/werkzeug/contrib/iterio.py b/werkzeug/contrib/iterio.py index c0ced3700..d22bfd873 100644 --- a/werkzeug/contrib/iterio.py +++ b/werkzeug/contrib/iterio.py @@ -258,7 +258,7 @@ def seek(self, pos, mode=0): raise IOError('Invalid argument') buf = [] try: - tmp_end_pos = len(self._buf) + tmp_end_pos = len(self._buf or '') while pos > tmp_end_pos: item = next(self._gen) tmp_end_pos += len(item) From 6f3f2e824ff36507689ec418d1ba038640a21b57 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 17 Feb 2018 11:54:43 +0000 Subject: [PATCH 018/280] Change to a conditional expression. Using a conditional expression is more Pythonic. --- werkzeug/debug/tbtools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/debug/tbtools.py b/werkzeug/debug/tbtools.py index 27a31bc5b..bc2d825bb 100644 --- a/werkzeug/debug/tbtools.py +++ b/werkzeug/debug/tbtools.py @@ -159,7 +159,7 @@ def render_console_html(secret, evalex_trusted=True): return CONSOLE_HTML % { 'evalex': 'true', - 'evalex_trusted': evalex_trusted and 'true' or 'false', + 'evalex_trusted': 'true' if evalex_trusted else 'false', 'console': 'true', 'title': 'Console', 'secret': secret, From d388a734aa4785b617fba5ab6c345841ef64c7d2 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 27 Feb 2018 23:38:52 +0900 Subject: [PATCH 019/280] Add 421 status code. close https://github.com/pallets/werkzeug/issues/1256 --- werkzeug/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/werkzeug/http.py b/werkzeug/http.py index 22197221c..720d39160 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -139,6 +139,7 @@ 416: 'Requested Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a teapot', # see RFC 2324 + 421: 'Misdirected Request', # see RFC 7540 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', From c4b4c52e1d9acbb1bee5b86093eba082fb250c68 Mon Sep 17 00:00:00 2001 From: Achilles Rasquinha Date: Tue, 13 Mar 2018 10:49:54 +0530 Subject: [PATCH 020/280] [MIN] StringIO compatibility --- bench/wzbench.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bench/wzbench.py b/bench/wzbench.py index 10d4fa463..5a1065aed 100755 --- a/bench/wzbench.py +++ b/bench/wzbench.py @@ -11,12 +11,15 @@ :copyright: 2014 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import division +from __future__ import division, print_function import os import gc import sys import subprocess -from cStringIO import StringIO +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO from timeit import default_timer as timer from types import FunctionType @@ -458,4 +461,4 @@ def after_html_builder(): try: main() except KeyboardInterrupt: - print >> sys.stderr, 'interrupted!' + print('\nInterrupted!', file = sys.stderr) From 0444b683463defa7d7b35ba4a7ee023479e4d337 Mon Sep 17 00:00:00 2001 From: "Yang,Zhou" Date: Thu, 22 Mar 2018 11:27:37 +0800 Subject: [PATCH 021/280] Update datastructure.py I think `MultiDict.lists()`'s docstring contain a mistake, origin it say "Return a list of ``(key, values)`` pairs", but not true, and not consistent with `.values()`, `listvalues()` --- werkzeug/datastructures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index 9624599da..5634dc749 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -546,7 +546,7 @@ def items(self, multi=False): yield key, values[0] def lists(self): - """Return a list of ``(key, values)`` pairs, where values is the list + """Return a iterator of ``(key, values)`` pairs, where values is the list of all values associated with the key.""" for key, values in iteritems(dict, self): From 03d277c4fab498d76e7284a0f3de2915d570c8ac Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 27 Mar 2018 12:14:43 +0800 Subject: [PATCH 022/280] fix typos --- werkzeug/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/utils.py b/werkzeug/utils.py index 918e7e5b6..625a1f2fe 100644 --- a/werkzeug/utils.py +++ b/werkzeug/utils.py @@ -52,7 +52,7 @@ def foo(self): # implementation detail: A subclass of python's builtin property # decorator, we override __get__ to check for a cached value. If one - # choses to invoke __get__ by hand the property will still work as + # chooses to invoke __get__ by hand the property will still work as # expected because the lookup logic is replicated in __get__ for # manual invocation. From aceee278267eda77e42bcdb6dc3041b7aeda7d4b Mon Sep 17 00:00:00 2001 From: Neil Halelamien Date: Wed, 4 Apr 2018 16:33:50 -0700 Subject: [PATCH 023/280] Make rename exception-handling code py3-compatible sys.maxint doesn't exist in py3, but sys.maxnum will have desired functionality in both py2/py3 Similar to solution implemented here: https://github.com/pallets/werkzeug/commit/0bad0c25f7d04da98328907d1c94a9b72fe57c57#diff-5561c422f09af0c38ec8260f0d17a52dR280 Issue encountered here: https://github.com/sh4nks/flask-caching/issues/53 --- werkzeug/posixemulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/posixemulation.py b/werkzeug/posixemulation.py index 8fd6314f2..f83b63ed5 100644 --- a/werkzeug/posixemulation.py +++ b/werkzeug/posixemulation.py @@ -94,7 +94,7 @@ def rename(src, dst): except OSError as e: if e.errno != errno.EEXIST: raise - old = "%s-%08x" % (dst, random.randint(0, sys.maxint)) + old = "%s-%08x" % (dst, random.randint(0, sys.maxsize)) os.rename(dst, old) os.rename(src, dst) try: From 133d20d67aa0b2c33b6ce010c148f10242a7436c Mon Sep 17 00:00:00 2001 From: Neil Halelamien Date: Wed, 4 Apr 2018 17:05:51 -0700 Subject: [PATCH 024/280] Fix stylecheck problem encountered by Travis build Due to commit https://github.com/pallets/werkzeug/commit/c4b4c52e1d9acbb1bee5b86093eba082fb250c68 the Travis stylecheck build has been failing with the following: ```python stylecheck runtests: commands[0] | flake8 ./bench/wzbench.py:464:37: E251 unexpected spaces around keyword / parameter equals ./bench/wzbench.py:464:39: E251 unexpected spaces around keyword / parameter equals ERROR: InvocationError for command '/home/travis/build/pallets/werkzeug/.tox/stylecheck/bin/flake8' (exited with code 1) ``` --- bench/wzbench.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/wzbench.py b/bench/wzbench.py index 5a1065aed..e96ff6b1b 100755 --- a/bench/wzbench.py +++ b/bench/wzbench.py @@ -461,4 +461,4 @@ def after_html_builder(): try: main() except KeyboardInterrupt: - print('\nInterrupted!', file = sys.stderr) + print('\nInterrupted!', file=sys.stderr) From a65b6e517d28688b14ba5d2d616d6c717f370c1a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 5 Apr 2018 19:10:52 -0400 Subject: [PATCH 025/280] Added support for using same-site cookies with werkzeug.contrib.sessions I think it's probably safe to actually just default to lax here, but I'm not 100% positive --- werkzeug/contrib/sessions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/werkzeug/contrib/sessions.py b/werkzeug/contrib/sessions.py index a9c3aa7ca..898994fb7 100644 --- a/werkzeug/contrib/sessions.py +++ b/werkzeug/contrib/sessions.py @@ -318,7 +318,8 @@ class SessionMiddleware(object): def __init__(self, app, store, cookie_name='session_id', cookie_age=None, cookie_expires=None, cookie_path='/', cookie_domain=None, cookie_secure=None, - cookie_httponly=False, environ_key='werkzeug.session'): + cookie_httponly=False, cookie_samesite=None, + environ_key='werkzeug.session'): self.app = app self.store = store self.cookie_name = cookie_name @@ -328,6 +329,7 @@ def __init__(self, app, store, cookie_name='session_id', self.cookie_domain = cookie_domain self.cookie_secure = cookie_secure self.cookie_httponly = cookie_httponly + self.cookie_samesite = cookie_samesite self.environ_key = environ_key def __call__(self, environ, start_response): @@ -346,7 +348,8 @@ def injecting_start_response(status, headers, exc_info=None): session.sid, self.cookie_age, self.cookie_expires, self.cookie_path, self.cookie_domain, self.cookie_secure, - self.cookie_httponly))) + self.cookie_httponly, + samesite=self.cookie_samesite))) return start_response(status, headers, exc_info) return ClosingIterator(self.app(environ, injecting_start_response), lambda: self.store.save_if_modified(session)) From 2ef3f69d58f3bdbd9a2c1d0238ba852d6d9e071f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 5 Apr 2018 19:13:57 -0400 Subject: [PATCH 026/280] default to lax, I'm pretty sure it works --- werkzeug/contrib/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/contrib/sessions.py b/werkzeug/contrib/sessions.py index 898994fb7..b8969dd6b 100644 --- a/werkzeug/contrib/sessions.py +++ b/werkzeug/contrib/sessions.py @@ -318,7 +318,7 @@ class SessionMiddleware(object): def __init__(self, app, store, cookie_name='session_id', cookie_age=None, cookie_expires=None, cookie_path='/', cookie_domain=None, cookie_secure=None, - cookie_httponly=False, cookie_samesite=None, + cookie_httponly=False, cookie_samesite='Lax', environ_key='werkzeug.session'): self.app = app self.store = store From 94ea3ecd08da96ef9da6b36391584e85fb2a9433 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 21 Apr 2018 13:09:25 +0900 Subject: [PATCH 027/280] Modernize security module, remove _find_hashlib_algorithms. Related issue: https://github.com/pallets/werkzeug/pull/1226 --- tests/test_security.py | 2 +- werkzeug/security.py | 49 +++++++++++++----------------------------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index 8b4a9a399..d46e69731 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -50,7 +50,7 @@ def test_password_hashing(): assert hash1.startswith('sha1$') assert hash2.startswith('sha1$') - with pytest.raises(TypeError): + with pytest.raises(ValueError): check_password_hash('$made$up$', 'default') with pytest.raises(ValueError): diff --git a/werkzeug/security.py b/werkzeug/security.py index d4c5c9f48..b9cec76e6 100644 --- a/werkzeug/security.py +++ b/werkzeug/security.py @@ -19,7 +19,7 @@ from itertools import starmap from werkzeug._compat import range_type, PY2, text_type, izip, to_bytes, \ - string_types, to_native + to_native SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' @@ -33,19 +33,6 @@ if sep not in (None, '/')) -def _find_hashlib_algorithms(): - algos = getattr(hashlib, 'algorithms', None) - if algos is None: - algos = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512') - rv = {} - for algo in algos: - func = getattr(hashlib, algo, None) - if func is not None: - rv[algo] = func - return rv -_hash_funcs = _find_hashlib_algorithms() - - def pbkdf2_hex(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, keylen=None, hashfunc=None): """Like :func:`pbkdf2_bin`, but returns a hex-encoded string. @@ -86,22 +73,23 @@ def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, string name of a known hash function or a function from the hashlib module. Defaults to sha256. """ - if isinstance(hashfunc, string_types): - hashfunc = _hash_funcs[hashfunc] - elif not hashfunc: - hashfunc = hashlib.sha256 + if not hashfunc: + hashfunc = 'sha256' + data = to_bytes(data) salt = to_bytes(salt) # If we're on Python with pbkdf2_hmac we can try to use it for # compatible digests. if _has_native_pbkdf2: - _test_hash = hashfunc() - if hasattr(_test_hash, 'name') and \ - _test_hash.name in _hash_funcs: - return hashlib.pbkdf2_hmac(_test_hash.name, - data, salt, iterations, - keylen) + if callable(hashfunc): + _test_hash = hashfunc() + hash_name = getattr(_test_hash, 'name', None) + else: + hash_name = hashfunc + if hash_name: + return hashlib.pbkdf2_hmac( + hash_name, data, salt, iterations, keylen) mac = hmac.HMAC(data, None, hashfunc) if not keylen: @@ -181,23 +169,16 @@ def _hash_internal(method, salt, password): is_pbkdf2 = False actual_method = method - hash_func = _hash_funcs.get(method) - if hash_func is None: - raise TypeError('invalid method %r' % method) - if is_pbkdf2: if not salt: raise ValueError('Salt is required for PBKDF2') - rv = pbkdf2_hex(password, salt, iterations, - hashfunc=hash_func) + rv = pbkdf2_hex(password, salt, iterations, hashfunc=method) elif salt: if isinstance(salt, text_type): salt = salt.encode('utf-8') - rv = hmac.HMAC(salt, password, hash_func).hexdigest() + rv = hmac.HMAC(salt, password, method).hexdigest() else: - h = hash_func() - h.update(password) - rv = h.hexdigest() + rv = hashlib.new(method, password).hexdigest() return rv, actual_method From e836a2fd8cb77082a3d3a567657b08d0e1f6b682 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 21 Apr 2018 13:50:25 +0900 Subject: [PATCH 028/280] Fix HMAC for Python 2.7. --- werkzeug/security.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/werkzeug/security.py b/werkzeug/security.py index b9cec76e6..0da8367de 100644 --- a/werkzeug/security.py +++ b/werkzeug/security.py @@ -91,7 +91,7 @@ def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, return hashlib.pbkdf2_hmac( hash_name, data, salt, iterations, keylen) - mac = hmac.HMAC(data, None, hashfunc) + mac = _create_mac(data, None, hashfunc) if not keylen: keylen = mac.digest_size @@ -176,12 +176,23 @@ def _hash_internal(method, salt, password): elif salt: if isinstance(salt, text_type): salt = salt.encode('utf-8') - rv = hmac.HMAC(salt, password, method).hexdigest() + mac = _create_mac(salt, password, method) + rv = mac.hexdigest() else: rv = hashlib.new(method, password).hexdigest() return rv, actual_method +def _create_mac(key, msg, method): + if callable(method): + return hmac.HMAC(key, msg, method) + hashfunc = lambda d=b'': hashlib.new(method, d) + # Python 2.7 used ``hasattr(digestmod, '__call__')`` + # to detect if hashfunc is callable + hashfunc.__call__ = hashfunc + return hmac.HMAC(key, msg, hashfunc) + + def generate_password_hash(password, method='pbkdf2:sha256', salt_length=8): """Hash a password with the given method and salt with a string of the given length. The format of the string returned includes the method From 07515734363cf3a408df5661aef3d798cf4de37e Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 21 Apr 2018 14:02:06 +0900 Subject: [PATCH 029/280] Remove detection of _has_native_pbkdf2. --- tests/test_security.py | 10 ---------- werkzeug/security.py | 39 ++++++--------------------------------- 2 files changed, 6 insertions(+), 43 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index d46e69731..9dc49240a 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -134,13 +134,3 @@ def check(data, salt, iterations, keylen, hashfunc, expected): '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1') check('X' * 65, 'pass phrase exceeds block size', 1200, 32, 'sha1', '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a') - - -def test_pbkdf2_non_native(): - import werkzeug.security as sec - prev_value = sec._has_native_pbkdf2 - sec._has_native_pbkdf2 = None - - assert pbkdf2_hex('password', 'salt', 1, 20, 'sha1') \ - == '0c60c80f961f0e71f3a9b524af6012062fe037a6' - sec._has_native_pbkdf2 = prev_value diff --git a/werkzeug/security.py b/werkzeug/security.py index 0da8367de..9cd81d087 100644 --- a/werkzeug/security.py +++ b/werkzeug/security.py @@ -15,8 +15,6 @@ import codecs from struct import Struct from random import SystemRandom -from operator import xor -from itertools import starmap from werkzeug._compat import range_type, PY2, text_type, izip, to_bytes, \ to_native @@ -52,9 +50,6 @@ def pbkdf2_hex(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, return to_native(codecs.encode(rv, 'hex_codec')) -_has_native_pbkdf2 = hasattr(hashlib, 'pbkdf2_hmac') - - def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, keylen=None, hashfunc=None): """Returns a binary digest for the PBKDF2 hash algorithm of `data` @@ -79,34 +74,12 @@ def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS, data = to_bytes(data) salt = to_bytes(salt) - # If we're on Python with pbkdf2_hmac we can try to use it for - # compatible digests. - if _has_native_pbkdf2: - if callable(hashfunc): - _test_hash = hashfunc() - hash_name = getattr(_test_hash, 'name', None) - else: - hash_name = hashfunc - if hash_name: - return hashlib.pbkdf2_hmac( - hash_name, data, salt, iterations, keylen) - - mac = _create_mac(data, None, hashfunc) - if not keylen: - keylen = mac.digest_size - - def _pseudorandom(x, mac=mac): - h = mac.copy() - h.update(x) - return bytearray(h.digest()) - buf = bytearray() - for block in range_type(1, -(-keylen // mac.digest_size) + 1): - rv = u = _pseudorandom(salt + _pack_int(block)) - for i in range_type(iterations - 1): - u = _pseudorandom(bytes(u)) - rv = bytearray(starmap(xor, izip(rv, u))) - buf.extend(rv) - return bytes(buf[:keylen]) + if callable(hashfunc): + _test_hash = hashfunc() + hash_name = getattr(_test_hash, 'name', None) + else: + hash_name = hashfunc + return hashlib.pbkdf2_hmac(hash_name, data, salt, iterations, keylen) def safe_str_cmp(a, b): From 12aaa6aa05b8244c9c69ea7af25e8d448c75d93a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 21 Apr 2018 15:07:57 +0900 Subject: [PATCH 030/280] Add changelog for version 0.15 --- CHANGES.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3ca5a629a..26cf3e61b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,20 @@ Werkzeug Changelog ================== +Version 0.15 +------------ + +Release Date not Decided + +- Fix a bug in ``werkzeug.wsgi.ProxyMiddleware`` with query string. + (`#1252`_) +- Add 412 status code. +- Cleanup ``werkzeug.security`` module, remove predated hashlib support. + (`#1282`_) + +.. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 +.. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 + Version 0.14.1 -------------- From 43fe889fe5a8c495c41ec28658f67c1f7b23e2cd Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Mon, 30 Apr 2018 15:06:40 +0100 Subject: [PATCH 031/280] Catch StopIteration as a valid case. (#1280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to the WSGI spec[1], it is a valid case to both: a) lazily call `start_response` in request processing > this invocation may be performed by the iterable’s first iteration, so > servers must not assume that start_response() has been called before > they begin iterating over the iterable b) return an empty iterator (e.g. in the case of a 204 No Content) > When called by the server, the application object must return an > iterable yielding zero or more strings. When both of these were true, previously, Werkzeug's test client would propagate a `StopIteration` exception from the WSGI app's returned iterable. Since this is a valid case, we now catch the `StopIteration` exception if `start_response` has _also_ been called. If `start_response` has not been called we re-raise, as this is not a valid case under the WSGI spec, and as mentioned in the docstring of this function... > If passed an invalid WSGI application the behavior of this function is > undefined. [1]: https://www.python.org/dev/peps/pep-0333/#specification-details Co-Authored-By: Alistair Lynn --- tests/test_test.py | 23 +++++++++++++++++++++++ werkzeug/test.py | 6 ++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/test_test.py b/tests/test_test.py index 0af94ccf8..2651ace88 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -520,6 +520,29 @@ def close(self): assert not leaked_data +@pytest.mark.parametrize('buffered', (True, False)) +@pytest.mark.parametrize('iterable', (True, False)) +def test_lazy_start_response_empty_response_app(buffered, iterable): + @implements_iterator + class app: + def __init__(self, environ, start_response): + self.start_response = start_response + + def __iter__(self): + return self + + def __next__(self): + self.start_response('200 OK', [('Content-Type', 'text/html')]) + raise StopIteration + + if iterable: + app = iterable_middleware(app) + app_iter, status, headers = run_wsgi_app(app, {}, buffered=buffered) + strict_eq(status, '200 OK') + strict_eq(list(headers), [('Content-Type', 'text/html')]) + strict_eq(''.join(app_iter), '') + + def test_run_wsgi_app_closing_iterator(): got_close = [] diff --git a/werkzeug/test.py b/werkzeug/test.py index acd21793d..0c98db511 100644 --- a/werkzeug/test.py +++ b/werkzeug/test.py @@ -938,8 +938,10 @@ def start_response(status, headers, exc_info=None): # a new `ClosingIterator` if we need to restore a `close` callable from the # original return value. else: - while not response: - buffer.append(next(app_iter)) + for item in app_iter: + buffer.append(item) + if response: + break if buffer: app_iter = chain(buffer, app_iter) if close_func is not None and app_iter is not app_rv: From 098a689e8cbe39540ba361dba122069ba6200bd6 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Sat, 12 May 2018 21:49:21 -0700 Subject: [PATCH 032/280] Never send Content-Length for 204 --- tests/test_wrappers.py | 13 +++++++++++++ werkzeug/wrappers.py | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index af43d2f2f..f59723652 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1173,6 +1173,19 @@ def test_204_and_1XX_response_has_no_content_length(): assert 'Content-Length' not in headers +def test_malformed_204_response_has_no_content_length(): + # flask-restful can generate a malformed response when doing `return '', 204` + response = wrappers.Response(status=204) + response.set_data(b'test') + assert response.content_length == 4 + + env = create_environ() + app_iter, status, headers = response.get_wsgi_response(env) + assert status == '204 NO CONTENT' + assert 'Content-Length' not in headers + assert b''.join(app_iter) == b'' # ensure data will not be sent + + def test_modified_url_encoding(): class ModifiedRequest(wrappers.Request): url_charset = 'euc-kr' diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 92dfa5dac..5815560f9 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -1247,7 +1247,11 @@ def get_wsgi_headers(self, environ): isinstance(content_location, text_type): headers['Content-Location'] = iri_to_uri(content_location) - if status in (304, 412): + if 100 <= status < 200 or status == 204: + # Per section 3.3.2 of RFC 7230, "a server MUST NOT send a Content-Length header field + # in any response with a status code of 1xx (Informational) or 204 (No Content)." + headers.remove('Content-Length') + elif status in (304, 412): remove_entity_headers(headers) # if we can determine the content length automatically, we From 6a9efb10c16c39a1cea7138b37619601d17a8a51 Mon Sep 17 00:00:00 2001 From: James Alexander Date: Mon, 14 May 2018 11:25:52 -0400 Subject: [PATCH 033/280] - Add .pytest_cache/* to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9a2a9ee40..c6e62670b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ htmlcov .hypothesis test_uwsgi_failed .idea +.pytest_cache/* \ No newline at end of file From d306b4e608acf6885c61a9fc857c9dbd0bcb3649 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Mon, 14 May 2018 18:37:59 +0300 Subject: [PATCH 034/280] Use flask theme from `sphinx-pallets-themes` --- MANIFEST.in | 1 - docs/_templates/sidebarintro.html | 19 - docs/_templates/sidebarlogo.html | 3 - docs/_themes/LICENSE | 37 -- docs/_themes/README | 31 -- docs/_themes/werkzeug/layout.html | 8 - docs/_themes/werkzeug/relations.html | 19 - docs/_themes/werkzeug/static/werkzeug.css_t | 395 -------------------- docs/_themes/werkzeug/theme.conf | 4 - docs/_themes/werkzeug_theme_support.py | 85 ----- docs/conf.py | 278 +++++--------- docs/werkzeugext.py | 15 - setup.py | 1 + tox.ini | 4 +- 14 files changed, 103 insertions(+), 797 deletions(-) delete mode 100644 docs/_templates/sidebarintro.html delete mode 100644 docs/_templates/sidebarlogo.html delete mode 100644 docs/_themes/LICENSE delete mode 100644 docs/_themes/README delete mode 100644 docs/_themes/werkzeug/layout.html delete mode 100644 docs/_themes/werkzeug/relations.html delete mode 100644 docs/_themes/werkzeug/static/werkzeug.css_t delete mode 100644 docs/_themes/werkzeug/theme.conf delete mode 100644 docs/_themes/werkzeug_theme_support.py delete mode 100644 docs/werkzeugext.py diff --git a/MANIFEST.in b/MANIFEST.in index 0cc1a4079..9b3cc5fd6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,5 +5,4 @@ graft docs graft artwork graft examples prune docs/_build -prune docs/_themes global-exclude *.py[cdo] __pycache__ *.so diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html deleted file mode 100644 index 80eabe615..000000000 --- a/docs/_templates/sidebarintro.html +++ /dev/null @@ -1,19 +0,0 @@ -

About Werkzeug

-

- Werkzeug is a WSGI utility library. It can serve as the basis for a - custom framework. -

-

Other Formats

-

- You can download the documentation in other formats as well: -

- -

Useful Links

- diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html deleted file mode 100644 index c1a7ba7a7..000000000 --- a/docs/_templates/sidebarlogo.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE deleted file mode 100644 index 08cbb7fc6..000000000 --- a/docs/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2011 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README deleted file mode 100644 index b3292bdff..000000000 --- a/docs/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/docs/_themes/werkzeug/layout.html b/docs/_themes/werkzeug/layout.html deleted file mode 100644 index a0c9cab04..000000000 --- a/docs/_themes/werkzeug/layout.html +++ /dev/null @@ -1,8 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block relbar2 %}{% endblock %} -{%- block footer %} - -{%- endblock %} diff --git a/docs/_themes/werkzeug/relations.html b/docs/_themes/werkzeug/relations.html deleted file mode 100644 index 3bbcde85b..000000000 --- a/docs/_themes/werkzeug/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/docs/_themes/werkzeug/static/werkzeug.css_t b/docs/_themes/werkzeug/static/werkzeug.css_t deleted file mode 100644 index 0193276f0..000000000 --- a/docs/_themes/werkzeug/static/werkzeug.css_t +++ /dev/null @@ -1,395 +0,0 @@ -/* - * werkzeug.css_t - * ~~~~~~~~~~~~~~ - * - * :copyright: Copyright 2011 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} -{% set font_family = "'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif" %} -{% set header_font_family = "'Ubuntu', " ~ font_family %} - -@import url("basic.css"); -@import url(http://fonts.googleapis.com/css?family=Ubuntu); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: {{ font_family }}; - font-size: 15px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 13px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: {{ font_family }}; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: {{ font_family }}; - font-size: 14px; -} - -div.sphinxsidebar form.search input[name="q"] { - width: 130px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #185F6D; - text-decoration: underline; -} - -a:hover { - color: #2794AA; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: {{ header_font_family }}; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; - color: black; -} - -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: {{ font_family }}; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #E8EFF0; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #E8EFF0; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #E8EFF0; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #2BABC4; -} - -a.reference:hover { - border-bottom: 1px solid #2794AA; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/werkzeug/theme.conf b/docs/_themes/werkzeug/theme.conf deleted file mode 100644 index d9c8dbba0..000000000 --- a/docs/_themes/werkzeug/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = werkzeug.css -pygments_style = werkzeug_theme_support.WerkzeugStyle diff --git a/docs/_themes/werkzeug_theme_support.py b/docs/_themes/werkzeug_theme_support.py deleted file mode 100644 index b138a9293..000000000 --- a/docs/_themes/werkzeug_theme_support.py +++ /dev/null @@ -1,85 +0,0 @@ -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class WerkzeugStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#1B5C66", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/conf.py b/docs/conf.py index 540ae20d8..fcb4e1fe2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,214 +1,134 @@ # -*- coding: utf-8 -*- -# -# Werkzeug documentation build configuration file, created by -# sphinx-quickstart on Fri Jan 16 23:10:43 2009. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# The contents of this file are pickled, so don't put values in the namespace -# that aren't pickleable (module imports are okay, they're removed automatically). -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If your extensions are in another directory, add it here. If the directory -# is relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -sys.path.append(os.path.abspath('.')) -sys.path.append(os.path.abspath('_themes')) - -# General configuration -# --------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'sphinx.ext.doctest', 'werkzeugext'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Werkzeug' -copyright = u'2011, The Werkzeug Team' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. +from __future__ import print_function +import inspect import re -try: - import werkzeug -except ImportError: - sys.path.append(os.path.abspath('../')) -from werkzeug import __version__ as release -if 'dev' in release: - release = release[:release.find('dev') + 3] -if release == 'unknown': - version = release -else: - version = re.match(r'\d+\.\d+(?:\.\d+)?', release).group() - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' -# List of documents that shouldn't be included in the build. -#unused_docs = [] +from pallets_sphinx_themes import ProjectLink, get_version -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = ['_build'] +# Project -------------------------------------------------------------- -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +project = 'Werkzeug' +copyright = '2011 Pallets Team' +author = 'Pallets Team' +release, version = get_version('Werkzeug') -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True +# General -------------------------------------------------------------- -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'werkzeug_theme_support.WerkzeugStyle' +master_doc = 'index' -# doctest setup code -doctest_global_setup = '''\ -from werkzeug import * -''' +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.doctest', +] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'werkzeug': ('http://werkzeug.pocoo.org/docs/', None), + 'jinja': ('http://jinja.pocoo.org/docs/', None), +} -# Options for HTML output -# ----------------------- +# HTML ----------------------------------------------------------------- html_theme = 'werkzeug' -html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". +html_context = { + 'project_links': [ + ProjectLink('Donate to Pallets', 'https://psfmember.org/civicrm/contribute/transact?reset=1&id=20'), + ProjectLink('Werkzeug Website', 'https://palletsprojects.com/p/werkzeug/'), + ProjectLink('PyPI releases', 'https://pypi.org/project/Werkzeug/'), + ProjectLink('Source Code', 'https://github.com/pallets/werkzeug/'), + ProjectLink('Issue Tracker', 'https://github.com/pallets/werkzeug/issues/'), + ], + 'canonical_url': 'http://werkzeug.pocoo.org/docs/{}/'.format(version), + 'carbon_ads_args': 'zoneid=1673&serve=C6AILKT&placement=pocooorg', +} +html_sidebars = { + 'index': [ + 'project.html', + 'versions.html', + 'carbon_ads.html', + 'searchbox.html', + ], + '**': [ + 'localtoc.html', + 'relations.html', + 'versions.html', + 'carbon_ads.html', + 'searchbox.html', + ] +} html_static_path = ['_static'] +html_favicon = '_static/favicon.ico' +html_logo = '_static/werkzeug.png' +html_additional_pages = { + '404': '404.html', +} +html_show_sourcelink = False -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True +# LaTeX ---------------------------------------------------------------- -# Custom sidebar templates, maps document names to template names. -html_sidebars = { - 'index': ['sidebarlogo.html', 'sidebarintro.html', 'sourcelink.html', - 'searchbox.html'], - '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', - 'sourcelink.html', 'searchbox.html'] +latex_documents = [ + (master_doc, 'Werkzeug.tex', 'Werkzeug Documentation', 'Pallets Team', 'manual'), +] +latex_use_modindex = False +latex_elements = { + 'papersize': 'a4paper', + 'pointsize': '12pt', + 'fontpkg': r'\usepackage{mathpazo}', + 'preamble': r'\usepackage{werkzeugstyle}', } +latex_use_parts = True +latex_additional_files = ['werkzeugstyle.sty', 'logo.pdf'] -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} +# linkcheck ------------------------------------------------------------ -# If false, no module index is generated. -#html_use_modindex = True +linkcheck_anchors = False -# If false, no index is generated. -#html_use_index = True +# Local Extensions ----------------------------------------------------- -# If true, the index is split into individual pages for each letter. -#html_split_index = False +def unwrap_decorators(): + import sphinx.util.inspect as inspect + import functools -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True + old_getargspec = inspect.getargspec + def getargspec(x): + return old_getargspec(getattr(x, '_original_function', x)) + inspect.getargspec = getargspec -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' + old_update_wrapper = functools.update_wrapper + def update_wrapper(wrapper, wrapped, *a, **kw): + rv = old_update_wrapper(wrapper, wrapped, *a, **kw) + rv._original_function = wrapped + return rv + functools.update_wrapper = update_wrapper -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' -# Output file base name for HTML help builder. -htmlhelp_basename = 'Werkzeugdoc' +unwrap_decorators() +del unwrap_decorators -# Options for LaTeX output -# ------------------------ +_internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)', re.M) -# The paper size ('letter' or 'a4'). -latex_paper_size = 'a4' -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, document class [howto/manual]). -latex_documents = [ - ('latexindex', 'Werkzeug.tex', ur'Werkzeug Documentation', - ur'The Werkzeug Team', 'manual'), -] +def skip_internal(app, what, name, obj, skip, options): + docstring = inspect.getdoc(obj) or '' -# Additional stuff for LaTeX -latex_elements = { - 'fontpkg': r'\usepackage{mathpazo}', - 'papersize': 'a4paper', - 'pointsize': '12pt', - 'preamble': r''' -\usepackage{werkzeugstyle} - -% i hate you latex, here too -\DeclareUnicodeCharacter{2603}{\\N\{SNOWMAN\}} -''' -} + if skip or _internal_mark_re.search(docstring) is not None: + return True -latex_use_parts = True -latex_additional_files = ['werkzeugstyle.sty', 'logo.pdf'] +def cut_module_meta(app, what, name, obj, options, lines): + """Remove metadata from autodoc output.""" + if what != 'module': + return -latex_use_modindex = False + lines[:] = [ + line for line in lines + if not line.startswith((':copyright:', ':license:')) + ] -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'http://docs.python.org/dev': None, - 'http://docs.sqlalchemy.org/en/latest/': None -} +def setup(app): + app.connect('autodoc-skip-member', skip_internal) + app.connect('autodoc-process-docstring', cut_module_meta) diff --git a/docs/werkzeugext.py b/docs/werkzeugext.py deleted file mode 100644 index ba72e7052..000000000 --- a/docs/werkzeugext.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Werkzeug Sphinx Extensions - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Provides some more helpers for the werkzeug docs. - - :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. -""" -from sphinx.ext.autodoc import cut_lines - - -def setup(app): - app.connect('autodoc-process-docstring', cut_lines(3, 3, what=['module'])) diff --git a/setup.py b/setup.py index 66c5776ae..4e31907fa 100755 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'coverage', 'tox', 'sphinx', + 'pallets-sphinx-themes', ], }, include_package_data=True, diff --git a/tox.ini b/tox.ini index 9a9e69a1b..147e1946e 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,9 @@ deps = flake8 commands = flake8 [] [testenv:docs-html] -deps = sphinx==1.6.7 +deps = + sphinx==1.6.7 + pallets-sphinx-themes commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html [testenv:docs-linkcheck] From f8b9af67694321ae31d04f3f4252314bbe14638a Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Mon, 14 May 2018 18:43:10 +0300 Subject: [PATCH 035/280] Add `docs` extra scope similar to flask --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 4e31907fa..108161a0d 100755 --- a/setup.py +++ b/setup.py @@ -47,6 +47,10 @@ 'sphinx', 'pallets-sphinx-themes', ], + 'docs': [ + 'sphinx', + 'pallets-sphinx-themes', + ] }, include_package_data=True, zip_safe=False, From 745ef41a4604cff1417f52af23e2b6ef03f84403 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 15 May 2018 05:30:14 -0700 Subject: [PATCH 036/280] add trailing newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c6e62670b..5023b5116 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ htmlcov .hypothesis test_uwsgi_failed .idea -.pytest_cache/* \ No newline at end of file +.pytest_cache/ From eef69fe79c8ddb75342a1333362b698dafa7092a Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 15 May 2018 19:23:57 +0300 Subject: [PATCH 037/280] `get_host()` now ignores `X-Forwarded-Host` To get `X-Forwarded-*` headers considered, use `ProxyFix`. This also adds more tests for werkzeug.wsgi.get_host(). --- tests/test_wsgi.py | 47 +++++++++++++++++++++++++++++++------------- werkzeug/wrappers.py | 8 ++++---- werkzeug/wsgi.py | 8 +++----- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 9d2f68072..bab4701a8 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -98,25 +98,44 @@ def dummy_application(environ, start_response): assert b''.join(app_iter).strip() == b'NOT FOUND' -def test_get_host(): - env = {'HTTP_X_FORWARDED_HOST': 'example.org', - 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} +def test_get_host_by_http_host(): + env = {'HTTP_HOST': 'example.org'} assert wsgi.get_host(env) == 'example.org' - assert wsgi.get_host(create_environ('/', 'http://example.org')) == \ - 'example.org' + env['HTTP_HOST'] = 'example.org:8080' + assert wsgi.get_host(env) == 'example.org:8080' + env['HOST_NAME'] = 'ignore me' + assert wsgi.get_host(env) == 'example.org:8080' -def test_get_host_multiple_forwarded(): - env = {'HTTP_X_FORWARDED_HOST': 'example.com, example.org', - 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} - assert wsgi.get_host(env) == 'example.com' - assert wsgi.get_host(create_environ('/', 'http://example.com')) == \ - 'example.com' +def test_get_host_by_server_name_and_port(): + env = {'SERVER_NAME': 'example.org', 'SERVER_PORT': '80', + 'wsgi.url_scheme': 'http'} + assert wsgi.get_host(env) == 'example.org' + env['wsgi.url_scheme'] = 'https' + assert wsgi.get_host(env) == 'example.org:80' + env['SERVER_PORT'] = '8080' + assert wsgi.get_host(env) == 'example.org:8080' + env['SERVER_PORT'] = '443' + assert wsgi.get_host(env) == 'example.org' + + +def test_get_host_ignore_x_forwarded_for(): + env = {'HTTP_X_FORWARDED_HOST': 'forwarded', + 'HTTP_HOST': 'example.org'} + assert wsgi.get_host(env) == 'example.org' -def test_get_host_validation(): - env = {'HTTP_X_FORWARDED_HOST': 'example.org', - 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} +def test_get_host_validate_trusted_hosts(): + env = {'SERVER_NAME': 'example.org', 'SERVER_PORT': '80', + 'wsgi.url_scheme': 'http'} + assert wsgi.get_host(env, trusted_hosts=['.example.org']) == 'example.org' + pytest.raises(BadRequest, wsgi.get_host, env, + trusted_hosts=['example.com']) + env['SERVER_PORT'] = '8080' + assert wsgi.get_host(env, trusted_hosts=['.example.org:8080']) == 'example.org:8080' + pytest.raises(BadRequest, wsgi.get_host, env, + trusted_hosts=['.example.com']) + env = {'HTTP_HOST': 'example.org'} assert wsgi.get_host(env, trusted_hosts=['.example.org']) == 'example.org' pytest.raises(BadRequest, wsgi.get_host, env, trusted_hosts=['example.com']) diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 5815560f9..7ad38f458 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -204,10 +204,10 @@ class Request(BaseRequest, ETagRequestMixin): #: all hosts are trusted which means that whatever the client sends the #: host is will be accepted. #: - #: This is the recommended setup as a webserver should manually be set up - #: to only route correct hosts to the application, and remove the - #: `X-Forwarded-Host` header if it is not being used (see - #: :func:`werkzeug.wsgi.get_host`). + #: Because `Host` and `X-Forwarded-Host` headers can be set to any value by + #: a malicious client, it is recommended to either set this property or + #: implement similar validation in the proxy (if application is being run + #: behind one). #: #: .. versionadded:: 0.9 trusted_hosts = None diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 2d4498a45..9962798a4 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -144,8 +144,8 @@ def _normalize(hostname): def get_host(environ, trusted_hosts=None): """Return the real host for the given WSGI environment. This first checks - the `X-Forwarded-Host` header, then the normal `Host` header, and finally - the `SERVER_NAME` environment variable (using the first one it finds). + the normal `Host` header, and if it's not present, then `SERVER_NAME` + and `SERVER_PORT` environment variables. Optionally it verifies that the host is in a list of trusted hosts. If the host is not in there it will raise a @@ -155,9 +155,7 @@ def get_host(environ, trusted_hosts=None): :param trusted_hosts: a list of trusted hosts, see :func:`host_is_trusted` for more information. """ - if 'HTTP_X_FORWARDED_HOST' in environ: - rv = environ['HTTP_X_FORWARDED_HOST'].split(',', 1)[0].strip() - elif 'HTTP_HOST' in environ: + if 'HTTP_HOST' in environ: rv = environ['HTTP_HOST'] else: rv = environ['SERVER_NAME'] From 0a40289cc3854941bc106e9067c1def971adefde Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 15 May 2018 20:55:51 +0300 Subject: [PATCH 038/280] Sanitize (remove standard ports) in get_host() --- werkzeug/wsgi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 9962798a4..ec7bb0a9a 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -157,6 +157,10 @@ def get_host(environ, trusted_hosts=None): """ if 'HTTP_HOST' in environ: rv = environ['HTTP_HOST'] + if environ['wsgi.url_scheme'] == 'http' and rv.endswith(':80'): + rv = rv[:-3] + elif environ['wsgi.url_scheme'] == 'https' and rv.endswith(':443'): + rv = rv[:-4] else: rv = environ['SERVER_NAME'] if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ From fbb27e081b4fc88a2bc779e844e06b0fa341368a Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 15 May 2018 20:56:52 +0300 Subject: [PATCH 039/280] Re-use get_host() instead of duplicating the logic --- tests/test_wsgi.py | 7 ++++--- werkzeug/routing.py | 20 ++------------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index bab4701a8..4fd99ed15 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -99,7 +99,7 @@ def dummy_application(environ, start_response): def test_get_host_by_http_host(): - env = {'HTTP_HOST': 'example.org'} + env = {'HTTP_HOST': 'example.org', 'wsgi.url_scheme': 'http'} assert wsgi.get_host(env) == 'example.org' env['HTTP_HOST'] = 'example.org:8080' assert wsgi.get_host(env) == 'example.org:8080' @@ -121,7 +121,8 @@ def test_get_host_by_server_name_and_port(): def test_get_host_ignore_x_forwarded_for(): env = {'HTTP_X_FORWARDED_HOST': 'forwarded', - 'HTTP_HOST': 'example.org'} + 'HTTP_HOST': 'example.org', + 'wsgi.url_scheme': 'http'} assert wsgi.get_host(env) == 'example.org' @@ -135,7 +136,7 @@ def test_get_host_validate_trusted_hosts(): assert wsgi.get_host(env, trusted_hosts=['.example.org:8080']) == 'example.org:8080' pytest.raises(BadRequest, wsgi.get_host, env, trusted_hosts=['.example.com']) - env = {'HTTP_HOST': 'example.org'} + env = {'HTTP_HOST': 'example.org', 'wsgi.url_scheme': 'http'} assert wsgi.get_host(env, trusted_hosts=['.example.org']) == 'example.org' pytest.raises(BadRequest, wsgi.get_host, env, trusted_hosts=['example.com']) diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 2e4e2f42c..186cb8ffd 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -113,7 +113,7 @@ implements_to_string, wsgi_decoding_dance from werkzeug.datastructures import ImmutableDict, MultiDict from werkzeug.utils import cached_property - +from werkzeug.wsgi import get_host _rule_re = re.compile(r''' (?P[^<]*) # static rule data @@ -1294,23 +1294,7 @@ def bind_to_environ(self, environ, server_name=None, subdomain=None): """ environ = _get_environ(environ) - if 'HTTP_HOST' in environ: - wsgi_server_name = environ['HTTP_HOST'] - - if environ['wsgi.url_scheme'] == 'http' \ - and wsgi_server_name.endswith(':80'): - wsgi_server_name = wsgi_server_name[:-3] - elif environ['wsgi.url_scheme'] == 'https' \ - and wsgi_server_name.endswith(':443'): - wsgi_server_name = wsgi_server_name[:-4] - else: - wsgi_server_name = environ['SERVER_NAME'] - - if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ - in (('https', '443'), ('http', '80')): - wsgi_server_name += ':' + environ['SERVER_PORT'] - - wsgi_server_name = wsgi_server_name.lower() + wsgi_server_name = get_host(environ).lower() if server_name is None: server_name = wsgi_server_name From 9bbd1dc5c0e98e37824e371883567cf0a14dd42a Mon Sep 17 00:00:00 2001 From: Adam Englander Date: Mon, 14 May 2018 18:09:45 -0400 Subject: [PATCH 040/280] Update server to ensure meta-variables starting with HTTP_ having multiple header fields are rewritten as a single header value pursuant to RFC 3875 Section 4.1.18. This change resolves #1070 --- tests/test_serving.py | 34 ++++++++++++++++++++++++++++++++++ werkzeug/serving.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/test_serving.py b/tests/test_serving.py index 95462ee78..4fad9726a 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -436,3 +436,37 @@ def app(environ, start_response): assert res.read() == b'YES' conn.close() + + +def test_multiple_headers_concatenated_per_rfc_3875_section_4_1_18(dev_server): + server = dev_server(r''' + from werkzeug.wrappers import Response + def app(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [environ['HTTP_XYZ'].encode()] + ''') + + if sys.version_info[0] == 2: + from httplib import HTTPConnection + else: + from http.client import HTTPConnection + + conn = HTTPConnection('127.0.0.1', server.port) + conn.connect() + conn.putrequest('GET', '/') + conn.putheader('Accept', 'text/plain') + conn.putheader('XYZ', ' a ') + conn.putheader('X-INGNORE-1', 'Some nonsense') + conn.putheader('XYZ', ' b') + conn.putheader('X-INGNORE-2', 'Some nonsense') + conn.putheader('XYZ', 'c ') + conn.putheader('X-INGNORE-3', 'Some nonsense') + conn.putheader('XYZ', 'd') + conn.endheaders() + conn.send(b'') + res = conn.getresponse() + + assert res.status == 200 + assert res.read() == b'a ,b,c ,d' + + conn.close() diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 902f1aa1c..4b0d16439 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -189,10 +189,12 @@ def shutdown_server(): 'SERVER_PROTOCOL': self.request_version } - for key, value in self.headers.items(): + for key, value in self.get_header_items(): key = key.upper().replace('-', '_') if key not in ('CONTENT_TYPE', 'CONTENT_LENGTH'): key = 'HTTP_' + key + if key in environ: + value = "{},{}".format(environ[key], value) environ[key] = value if environ.get('HTTP_TRANSFER_ENCODING', '').strip().lower() == 'chunked': @@ -383,6 +385,35 @@ def log(self, type, message, *args): self.log_date_time_string(), message % args)) + def get_header_items(self): + """ + Get an iterable list of key/value pairs representing headers. + + This function provides Python 2/3 compatibility as related to the + parsing of request headers. Python 2.7 is not compliant with + RFC 3875 Section 4.1.18 which requires multiple values for headers + to be provided. This function will return a matching list regardless + of Python version. It can be removed once Python 2.7 support + is dropped. + + :return: List of tuples containing header hey/value pairs + """ + if PY2: + # For Python 2, process the headers manually according to W3C RFC 2616 Section 4.2 + items = [] + for header in self.headers.headers: + # Remove the \n\r from the header and split on the : to get the field name and value + key, value = header[0:-2].split(":", 1) + # Add the key and the value once stripped of leading white space. The specification + # allows for stripping trailing white space but the Python 3 code does not strip + # trailing white space. Therefore, trailing space will be left as is to match the + # Python 3 behavior + items.append((key, value.lstrip())) + else: + items = self.headers.items() + + return items + #: backwards compatible name if someone is subclassing it BaseRequestHandler = WSGIRequestHandler From 6213060d5d61628be83099e1fa0e98ea686edf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarjei=20Hus=C3=B8y?= Date: Fri, 18 May 2018 13:53:56 -0700 Subject: [PATCH 041/280] Fix typo in monkeypatch test --- tests/test_serving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_serving.py b/tests/test_serving.py index 4fad9726a..03bdf5b54 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -264,7 +264,7 @@ def test_windows_get_args_for_reloading(monkeypatch, tmpdir): assert rv == [test_exe.strpath, 'run'] -def test_monkeypached_sleep(tmpdir): +def test_monkeypatched_sleep(tmpdir): # removing the staticmethod wrapper in the definition of # ReloaderLoop._sleep works most of the time, since `sleep` is a c # function, and unlike python functions which are descriptors, doesn't From 61e83a6dcd0ef338f570729455ab11592570c978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarjei=20Hus=C3=B8y?= Date: Fri, 18 May 2018 14:03:28 -0700 Subject: [PATCH 042/280] Only set CONTENT_TYPE/CONTENT_LENGTH if present This makes the test environment consistent with the rest. --- tests/test_formparser.py | 4 ---- tests/test_test.py | 5 +++-- werkzeug/test.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_formparser.py b/tests/test_formparser.py index c140476b7..9e226f2d5 100644 --- a/tests/test_formparser.py +++ b/tests/test_formparser.py @@ -120,8 +120,6 @@ def test_parse_form_data_put_without_content(self): # defining the media type of that body." In the case where either # headers are omitted, parse_form_data should still work. env = create_environ('/foo', 'http://example.org/', method='PUT') - del env['CONTENT_TYPE'] - del env['CONTENT_LENGTH'] stream, form, files = formparser.parse_form_data(env) strict_eq(stream.read(), b'') @@ -130,8 +128,6 @@ def test_parse_form_data_put_without_content(self): def test_parse_form_data_get_without_content(self): env = create_environ('/foo', 'http://example.org/', method='GET') - del env['CONTENT_TYPE'] - del env['CONTENT_LENGTH'] stream, form, files = formparser.parse_form_data(env) strict_eq(stream.read(), b'') diff --git a/tests/test_test.py b/tests/test_test.py index 2651ace88..b78a19f00 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -207,6 +207,9 @@ def test_environ_builder_headers_content_type(): headers={'Content-Type': 'text/plain'}) env = b.get_environ() assert env['CONTENT_TYPE'] == 'text/html' + b = EnvironBuilder() + env = b.get_environ() + assert 'CONTENT_TYPE' not in env def test_environ_builder_paths(): @@ -311,8 +314,6 @@ def test_create_environ(): 'wsgi.multithread': False, 'wsgi.url_scheme': 'http', 'SCRIPT_NAME': '', - 'CONTENT_TYPE': '', - 'CONTENT_LENGTH': '0', 'SERVER_NAME': 'example.org', 'REQUEST_METHOD': 'GET', 'HTTP_HOST': 'example.org', diff --git a/werkzeug/test.py b/werkzeug/test.py index 0c98db511..25a7e2450 100644 --- a/werkzeug/test.py +++ b/werkzeug/test.py @@ -614,8 +614,6 @@ def _path_encode(x): 'SERVER_PORT': str(self.server_port), 'HTTP_HOST': self.host, 'SERVER_PROTOCOL': self.server_protocol, - 'CONTENT_TYPE': content_type or '', - 'CONTENT_LENGTH': str(content_length or '0'), 'wsgi.version': self.wsgi_version, 'wsgi.url_scheme': self.url_scheme, 'wsgi.input': input_stream, @@ -624,6 +622,15 @@ def _path_encode(x): 'wsgi.multiprocess': self.multiprocess, 'wsgi.run_once': self.run_once }) + if content_type: + result.update({ + 'CONTENT_TYPE': content_type, + }) + if content_length: + result.update({ + 'CONTENT_LENGTH': str(content_length), + }) + for key, value in self.headers.to_wsgi_list(): result['HTTP_%s' % key.upper().replace('-', '_')] = value if self.environ_overrides: From 7a4e8b834ee81aeeaa5dd0458b3986d33bb69de8 Mon Sep 17 00:00:00 2001 From: Adam Williamson Date: Wed, 9 May 2018 15:50:50 -0700 Subject: [PATCH 043/280] Skip tests that use 'xprocess' fixture when not installed There's a trick in conftest.py intended to allow tests to use a fixture named 'subprocess', which will be the 'xprocess' fixture if that's available, or will cause the test to be skipped it it's not available. Some tests, however, just use the 'xprocess' fixture directly, so all those tests fail if it is not available. We don't really need this 'subprocess' fixture at all, it turns out - we can just do the same trick directly on the 'xprocess' fixture, so all tests can use that directly (and also there's no confusion between this wrapper fixture and the commonly-used Python module called...subprocess). This simplifies things and makes the whole test suite run OK when xprocess isn't available. I noticed this when trying to run the test suite during build of the Fedora package - xprocess isn't packaged for Fedora yet, so there's no way to run the tests that use it unfortunately. Signed-off-by: Adam Williamson --- tests/conftest.py | 12 ++++-------- tests/contrib/cache/test_cache.py | 12 ++++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ce885777a..cd78d8ceb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,12 +27,8 @@ __import__('pytest_xprocess') except ImportError: @pytest.fixture(scope='session') - def subprocess(): + def xprocess(): pytest.skip('pytest-xprocess not installed.') -else: - @pytest.fixture(scope='session') - def subprocess(xprocess): - return xprocess port_generator = count(13220) @@ -117,7 +113,7 @@ def wait_for_reloader_loop(self): @pytest.fixture -def dev_server(tmpdir, subprocess, request, monkeypatch): +def dev_server(tmpdir, xprocess, request, monkeypatch): '''Run werkzeug.serving.run_simple in its own process. :param application: String for the module that will be created. The module @@ -144,7 +140,7 @@ def run_dev_server(application): url_base = 'http://localhost:{0}'.format(port) info = _ServerInfo( - subprocess, + xprocess, 'localhost:{0}'.format(port), url_base, port @@ -154,7 +150,7 @@ def preparefunc(cwd): args = [sys.executable, __file__, str(tmpdir)] return lambda: 'pid=%s' % info.request_pid(), args - subprocess.ensure('dev_server', preparefunc, restart=True) + xprocess.ensure('dev_server', preparefunc, restart=True) def teardown(): # Killing the process group that runs the server, not just the diff --git a/tests/contrib/cache/test_cache.py b/tests/contrib/cache/test_cache.py index 3e6ee7f36..0a2dac14a 100644 --- a/tests/contrib/cache/test_cache.py +++ b/tests/contrib/cache/test_cache.py @@ -219,7 +219,7 @@ class TestRedisCache(GenericCacheTests): _can_use_fast_sleep = False @pytest.fixture(scope='class', autouse=True) - def requirements(self, subprocess): + def requirements(self, xprocess): if redis is None: pytest.skip('Python package "redis" is not installed.') @@ -227,7 +227,7 @@ def prepare(cwd): return '[Rr]eady to accept connections', ['redis-server'] try: - subprocess.ensure('redis_server', prepare) + xprocess.ensure('redis_server', prepare) except IOError as e: # xprocess raises FileNotFoundError if e.errno == errno.ENOENT: @@ -236,7 +236,7 @@ def prepare(cwd): raise yield - subprocess.getinfo('redis_server').terminate() + xprocess.getinfo('redis_server').terminate() @pytest.fixture(params=(None, False, True)) def make_cache(self, request): @@ -271,7 +271,7 @@ class TestMemcachedCache(GenericCacheTests): _guaranteed_deletes = False @pytest.fixture(scope='class', autouse=True) - def requirements(self, subprocess): + def requirements(self, xprocess): if memcache is None: pytest.skip( 'Python package for memcache is not installed. Need one of ' @@ -282,7 +282,7 @@ def prepare(cwd): return '', ['memcached'] try: - subprocess.ensure('memcached', prepare) + xprocess.ensure('memcached', prepare) except IOError as e: # xprocess raises FileNotFoundError if e.errno == errno.ENOENT: @@ -291,7 +291,7 @@ def prepare(cwd): raise yield - subprocess.getinfo('memcached').terminate() + xprocess.getinfo('memcached').terminate() @pytest.fixture def make_cache(self): From 365012b3226313d05d3f83261cbe616c728b0927 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 21 May 2018 13:43:41 -0700 Subject: [PATCH 044/280] only omit headers if None add changelog --- CHANGES.rst | 9 +++++++++ werkzeug/test.py | 12 ++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 26cf3e61b..608114e29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ +.. currentmodule:: werkzeug + Werkzeug Changelog ================== + Version 0.15 ------------ @@ -11,9 +14,15 @@ Release Date not Decided - Add 412 status code. - Cleanup ``werkzeug.security`` module, remove predated hashlib support. (`#1282`_) +- :class:`~test.EnvironBuilder` doesn't set ``CONTENT_TYPE`` or + ``CONTENT_LENGTH`` in the environ if they aren't set. Previously + these used default values if they weren't set. Now it's possible to + distinguish between empty and unset values. (`#1308`_) .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 +.. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 + Version 0.14.1 -------------- diff --git a/werkzeug/test.py b/werkzeug/test.py index 25a7e2450..86ec0244a 100644 --- a/werkzeug/test.py +++ b/werkzeug/test.py @@ -622,14 +622,10 @@ def _path_encode(x): 'wsgi.multiprocess': self.multiprocess, 'wsgi.run_once': self.run_once }) - if content_type: - result.update({ - 'CONTENT_TYPE': content_type, - }) - if content_length: - result.update({ - 'CONTENT_LENGTH': str(content_length), - }) + if content_type is not None: + result['CONTENT_TYPE'] = content_type + if content_length is not None: + result['CONTENT_LENGTH'] = str(content_length) for key, value in self.headers.to_wsgi_list(): result['HTTP_%s' % key.upper().replace('-', '_')] = value From 59e3497e8bc592edd27a27cdb754951ad11633c2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Feb 2018 21:35:07 +0100 Subject: [PATCH 045/280] Always send bodies for 412 responses --- tests/test_wrappers.py | 3 ++- werkzeug/wrappers.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index f59723652..3640d704c 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -597,7 +597,8 @@ def test_etag_response_412(): # headers left and the status code would have to be 412 resp = wrappers.Response.from_app(response, env) assert resp.status_code == 412 - assert 'content-length' not in resp.headers + # Make sure there is a body still + assert resp.data != b'' # make sure date is not overriden response = wrappers.Response('Hello World') diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 5815560f9..34a0306c9 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -1251,7 +1251,7 @@ def get_wsgi_headers(self, environ): # Per section 3.3.2 of RFC 7230, "a server MUST NOT send a Content-Length header field # in any response with a status code of 1xx (Informational) or 204 (No Content)." headers.remove('Content-Length') - elif status in (304, 412): + elif status == 304: remove_entity_headers(headers) # if we can determine the content length automatically, we @@ -1291,7 +1291,7 @@ def get_app_iter(self, environ): """ status = self.status_code if environ['REQUEST_METHOD'] == 'HEAD' or \ - 100 <= status < 200 or status in (204, 304, 412): + 100 <= status < 200 or status in (204, 304): iterable = () elif self.direct_passthrough: if __debug__: From 13efd656fe6e880e2291a86973077f36039d60ba Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 22 May 2018 09:08:45 -0700 Subject: [PATCH 046/280] add changelog for 412 body --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 608114e29..73ad6ff04 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,8 +18,15 @@ Release Date not Decided ``CONTENT_LENGTH`` in the environ if they aren't set. Previously these used default values if they weren't set. Now it's possible to distinguish between empty and unset values. (`#1308`_) +- 412 responses once again include entity headers and an error message + in the body. They were originally omitted when implementing + ``If-Match`` (`#1233`_), but the spec doesn't seem to disallow it. + (`#1231`_, `#1255`_) +.. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 +.. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 +.. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 From 64fb22fde232bfea15aa5bc903513133a6d927bb Mon Sep 17 00:00:00 2001 From: Markus Oehme Date: Tue, 7 Apr 2015 11:07:56 +0200 Subject: [PATCH 047/280] routing: Transparently convert a MultiDict containing parameters. This allows to pass a MultiDict and get the intuitively expected result (that is multiple parameters, where multiple values are present). This allows to recreate a URL as follows: * create a URL with multiple values for a parameter * use Map.match() to extract them into a MultiDict * recreate the URL with Map.build() handing in the MultiDict The last step would fail before this commit (in that it would reproduce only one of the values for each parameter). Also this avoids the collateral damage introduced by the last attempt at allowing MultiDicts. This zaps one of the test cases in test_basic_building to reflect the changed behaviour. --- tests/test_routing.py | 24 +++++++++++++++++------- werkzeug/routing.py | 19 +++++++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/test_routing.py b/tests/test_routing.py index 8bf5f15ad..1fb071cc6 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -145,8 +145,6 @@ def test_basic_building(): assert adapter.build('bar', {'baz': 'blub'}) == \ 'http://example.org/bar/blub' assert adapter.build('bari', {'bazi': 50}) == 'http://example.org/bar/50' - multivalues = MultiDict([('bazi', 50), ('bazi', None)]) - assert adapter.build('bari', multivalues) == 'http://example.org/bar/50' assert adapter.build('barf', {'bazf': 0.815}) == \ 'http://example.org/bar/0.815' assert adapter.build('barp', {'bazp': 'la/di'}) == \ @@ -555,13 +553,25 @@ def test_build_append_unknown(): def test_build_append_multiple(): map = r.Map([ - r.Rule('/bar/', endpoint='barf') + r.Rule('/bar/', endpoint='endp') ]) - adapter = map.bind('example.org', '/', subdomain='blah') - params = {'bazf': 0.815, 'bif': [1.0, 3.0], 'pof': 2.0} - a, b = adapter.build('barf', params).split('?') + adapter = map.bind('example.org', '/', subdomain='subd') + params = {'foo': 0.815, 'x': [1.0, 3.0], 'y': 2.0} + a, b = adapter.build('endp', params).split('?') + assert a == 'http://example.org/bar/0.815' + assert set(b.split('&')) == set('y=2.0&x=1.0&x=3.0'.split('&')) + + +def test_build_append_multidict(): + map = r.Map([ + r.Rule('/bar/', endpoint='endp') + ]) + adapter = map.bind('example.org', '/', subdomain='subd') + params = MultiDict( + (('foo', 0.815), ('x', 1.0), ('x', 3.0), ('y', 2.0))) + a, b = adapter.build('endp', params).split('?') assert a == 'http://example.org/bar/0.815' - assert set(b.split('&')) == set('pof=2.0&bif=1.0&bif=3.0'.split('&')) + assert set(b.split('&')) == set('y=2.0&x=1.0&x=3.0'.split('&')) def test_method_fallback(): diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 2e4e2f42c..9e63e0250 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -110,7 +110,7 @@ from werkzeug._internal import _get_environ, _encode_idna from werkzeug._compat import itervalues, iteritems, to_unicode, to_bytes, \ text_type, string_types, native_string_result, \ - implements_to_string, wsgi_decoding_dance + implements_to_string, wsgi_decoding_dance, iterlists from werkzeug.datastructures import ImmutableDict, MultiDict from werkzeug.utils import cached_property @@ -1739,6 +1739,12 @@ def build(self, endpoint, values=None, method=None, force_external=False, >>> urls.build("index", {'q': ['a', 'b', 'c']}) '/?q=a&q=b&q=c' + If an actual :py:class:`werkzeug.datastructures.MultiDict` is passed + in it is automatically expanded to do the correct thing: + + >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b')))) + '/?p=z&q=a&q=b' + If a rule does not exist when building a `BuildError` exception is raised. @@ -1764,10 +1770,15 @@ def build(self, endpoint, values=None, method=None, force_external=False, self.map.update() if values: if isinstance(values, MultiDict): - valueiter = iteritems(values, multi=True) + temp = {} + for key, list_value in iterlists(values): + if len(list_value) == 1: + temp[key] = list_value[0] + else: + temp[key] = list_value else: - valueiter = iteritems(values) - values = dict((k, v) for k, v in valueiter if v is not None) + temp = values + values = dict((k, v) for k, v in iteritems(temp) if v is not None) else: values = {} From ca8f1608c340105b3fb85b74f58f30a96b4e4a1d Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 22 May 2018 13:44:32 -0700 Subject: [PATCH 048/280] avoid intermediate lists and dicts when converting multidict --- CHANGES.rst | 5 +++++ werkzeug/routing.py | 30 ++++++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 73ad6ff04..837cbb53c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,12 @@ Release Date not Decided in the body. They were originally omitted when implementing ``If-Match`` (`#1233`_), but the spec doesn't seem to disallow it. (`#1231`_, `#1255`_) +- :meth:`MapAdapter.build() ` can be passed + a :class:`~datastructures.MultiDict` to represent multiple values + for a key. It already did this when passing a dict with a list + value. (`#724`_) +.. _`#724`: https://github.com/pallets/werkzeug/pull/724 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 9e63e0250..b8cb2994a 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -110,7 +110,7 @@ from werkzeug._internal import _get_environ, _encode_idna from werkzeug._compat import itervalues, iteritems, to_unicode, to_bytes, \ text_type, string_types, native_string_result, \ - implements_to_string, wsgi_decoding_dance, iterlists + implements_to_string, wsgi_decoding_dance from werkzeug.datastructures import ImmutableDict, MultiDict from werkzeug.utils import cached_property @@ -855,7 +855,7 @@ def suitable_for(self, values, method=None): if key not in defaults and key not in values: return False - # in case defaults are given we ensure taht either the value was + # in case defaults are given we ensure that either the value was # skipped or the value is the same as the default value. if defaults: for key, value in iteritems(defaults): @@ -1739,8 +1739,7 @@ def build(self, endpoint, values=None, method=None, force_external=False, >>> urls.build("index", {'q': ['a', 'b', 'c']}) '/?q=a&q=b&q=c' - If an actual :py:class:`werkzeug.datastructures.MultiDict` is passed - in it is automatically expanded to do the correct thing: + Passing a ``MultiDict`` will also add multiple values: >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b')))) '/?p=z&q=a&q=b' @@ -1768,17 +1767,24 @@ def build(self, endpoint, values=None, method=None, force_external=False, if you want the builder to ignore those. """ self.map.update() + if values: if isinstance(values, MultiDict): - temp = {} - for key, list_value in iterlists(values): - if len(list_value) == 1: - temp[key] = list_value[0] - else: - temp[key] = list_value + temp_values = {} + # iteritems(dict, values) is like `values.lists()` + # without the call or `list()` coercion overhead. + for key, value in iteritems(dict, values): + if not value: + continue + if len(value) == 1: # flatten single item lists + value = value[0] + if value is None: # drop None + continue + temp_values[key] = value + values = temp_values else: - temp = values - values = dict((k, v) for k, v in iteritems(temp) if v is not None) + # drop None + values = dict(i for i in iteritems(values) if i[0] is not None) else: values = {} From 7e0d6244c8d677a84ea0e8445d771ca9ec92396f Mon Sep 17 00:00:00 2001 From: Ed Kellett Date: Wed, 23 May 2018 10:24:13 +0100 Subject: [PATCH 049/280] add failing test for None-dropping --- tests/test_routing.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_routing.py b/tests/test_routing.py index 1fb071cc6..9be2dc79f 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -574,6 +574,20 @@ def test_build_append_multidict(): assert set(b.split('&')) == set('y=2.0&x=1.0&x=3.0'.split('&')) +def test_build_drop_none(): + map = r.Map([ + r.Rule('/flob/', endpoint='endp') + ]) + adapter = map.bind('', '/') + params = {'flub': None, 'flop': None} + with pytest.raises(r.BuildError): + x = adapter.build('endp', params) + assert not x + params = {'flub': 'x', 'flop': None} + url = adapter.build('endp', params) + assert 'flop' not in url + + def test_method_fallback(): map = r.Map([ r.Rule('/', endpoint='index', methods=['GET']), From 24c49f1b1bffa48fbffe297f026d641754059d4e Mon Sep 17 00:00:00 2001 From: Ed Kellett Date: Wed, 23 May 2018 10:30:16 +0100 Subject: [PATCH 050/280] routing: fix None-dropping --- werkzeug/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/routing.py b/werkzeug/routing.py index b8cb2994a..2dcf50285 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -1784,7 +1784,7 @@ def build(self, endpoint, values=None, method=None, force_external=False, values = temp_values else: # drop None - values = dict(i for i in iteritems(values) if i[0] is not None) + values = dict(i for i in iteritems(values) if i[1] is not None) else: values = {} From c520ec8b61b08d79e543ef62d031ea0d10e35ffc Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 15 May 2018 22:11:54 +0300 Subject: [PATCH 051/280] `ProxyFix` now reads also `X-Forwarded-Port` --- werkzeug/contrib/fixers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index b88a861b2..887c3881b 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -136,6 +136,7 @@ def __call__(self, environ, start_response): forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '') forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') + forwarded_port = getter('HTTP_X_FORWARDED_PORT', '') environ.update({ 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), @@ -147,6 +148,15 @@ def __call__(self, environ, start_response): environ['REMOTE_ADDR'] = remote_addr if forwarded_host: environ['HTTP_HOST'] = forwarded_host + if forwarded_port: + if environ.get('HTTP_HOST'): + parts = environ['HTTP_HOST'].split(':', 1) + if len(parts) == 2: + environ['HTTP_HOST'] = parts[0] + ':' + forwarded_port + else: + environ['HTTP_HOST'] += ':' + forwarded_port + else: + environ['SERVER_PORT'] = forwarded_port if forwarded_proto: environ['wsgi.url_scheme'] = forwarded_proto return self.app(environ, start_response) From ac5eb18318f2331aa2c51bc4cf45f3aa8e622907 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 15 May 2018 22:12:22 +0300 Subject: [PATCH 052/280] Improve `ProxyFix` tests and add `X-Forwarded-Port` test --- tests/contrib/test_fixers.py | 84 ++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py index 6998d15ee..bfaf51be4 100644 --- a/tests/contrib/test_fixers.py +++ b/tests/contrib/test_fixers.py @@ -8,7 +8,10 @@ :copyright: (c) 2014 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import pytest + from tests import strict_eq +from werkzeug._compat import to_bytes from werkzeug.datastructures import ResponseCacheControl from werkzeug.http import parse_cache_control_header @@ -16,6 +19,7 @@ from werkzeug.wrappers import Request, Response from werkzeug.contrib import fixers from werkzeug.utils import redirect +from werkzeug.wsgi import get_host @Request.application @@ -55,27 +59,88 @@ def test_path_info_from_request_uri_fix(self): response = Response.from_app(app, env) assert response.get_data() == b'PATH_INFO: /foo%bar\nSCRIPT_NAME: /test' - def test_proxy_fix(self): + @pytest.mark.parametrize('environ,assumed_addr,assumed_host', [ + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1' + }, '127.0.0.1', 'http://internal', id='No proxy, with Host'), + pytest.param({ + 'SERVER_NAME': 'internal', + 'SERVER_PORT': '80', + 'REMOTE_ADDR': '127.0.0.1' + }, '127.0.0.1', 'http://internal', id='No proxy, no Host'), + pytest.param({ + 'HTTP_HOST': 'internal:80', + 'REMOTE_ADDR': '127.0.0.1' + }, '127.0.0.1', 'http://internal', id='Sanitize HTTP port'), + pytest.param({ + 'wsgi.url_scheme': 'https', + 'HTTP_HOST': 'internal:443', + 'REMOTE_ADDR': '127.0.0.1' + }, '127.0.0.1', 'https://internal', id='Sanitize HTTPS port'), + pytest.param({ + 'HTTP_HOST': 'internal:8080', + 'REMOTE_ADDR': '127.0.0.1' + }, '127.0.0.1', 'http://internal:8080', id='Custom port'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_FOR': '1.2.3.4, 5.6.7.8' + }, '1.2.3.4', 'http://internal', id='X-Forwarded-For'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_PROTO': 'https', + 'HTTP_X_FORWARDED_HOST': 'example.com', + 'HTTP_X_FORWARDED_PORT': '8443', + }, '127.0.0.1', 'https://example.com:8443', id='X-Forwarded-*'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_PORT': '8080', + }, '127.0.0.1', 'http://internal:8080', id='HTTP X-Port, no X-Host'), + pytest.param({ + 'SERVER_NAME': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_PORT': '8080', + }, '127.0.0.1', 'http://internal:8080', id='HTTP X-Port, no Host'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_PROTO': 'https', + 'HTTP_X_FORWARDED_PORT': '8443', + }, '127.0.0.1', 'https://internal:8443', id='HTTPS X-Port, no X-Host'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_X_FORWARDED_PROTO': 'https', + 'HTTP_X_FORWARDED_HOST': 'example.com', + 'HTTP_X_FORWARDED_PORT': '443', + 'HTTP_X_FORWARDED_FOR': '1.2.3.4, 5.6.7.8' + }, '1.2.3.4', 'https://example.com', id='All together'), + ]) + def test_proxy_fix(self, environ, assumed_addr, assumed_host): @Request.application def app(request): return Response('%s|%s' % ( request.remote_addr, # do not use request.host as this fixes too :) - request.environ['HTTP_HOST'] + request.environ['wsgi.url_scheme'] + '://' + + get_host(request.environ) )) app = fixers.ProxyFix(app, num_proxies=2) + has_host = 'HTTP_HOST' in environ environ = dict( create_environ(), - HTTP_X_FORWARDED_PROTO="https", - HTTP_X_FORWARDED_HOST='example.com', - HTTP_X_FORWARDED_FOR='1.2.3.4, 5.6.7.8', - REMOTE_ADDR='127.0.0.1', - HTTP_HOST='fake' + **environ ) + if not has_host: + del environ['HTTP_HOST'] # create_environ() defaults to 'localhost' response = Response.from_app(app, environ) - assert response.get_data() == b'1.2.3.4|example.com' + assert response.get_data() == to_bytes('{}|{}'.format( + assumed_addr, assumed_host)) # And we must check that if it is a redirection it is # correctly done: @@ -84,7 +149,8 @@ def app(request): response = Response.from_app(redirect_app, environ) wsgi_headers = response.get_wsgi_headers(environ) - assert wsgi_headers['Location'] == 'https://example.com/foo/bar.hml' + assert wsgi_headers['Location'] == '{}/foo/bar.hml'.format( + assumed_host) def test_proxy_fix_weird_enum(self): @fixers.ProxyFix From 9f7ddfd18ded3c254c1939d8a71d43dd329dd701 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 23 May 2018 08:11:43 -0700 Subject: [PATCH 053/280] store original server_port add changelog --- CHANGES.rst | 8 ++++++++ werkzeug/contrib/fixers.py | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 837cbb53c..ea9da6b4e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,13 +26,21 @@ Release Date not Decided a :class:`~datastructures.MultiDict` to represent multiple values for a key. It already did this when passing a dict with a list value. (`#724`_) +- :meth:`wsgi.get_host` no longer looks at ``X-Forwarded-For``. Use + :class:`~fixers.ProxyFix` to handle that. (`#609`_, `#1303`_) +- :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Port`` header + set by some proxies. (`#1023`_, `#1304`_) +.. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 +.. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 +.. _`#1303`: https://github.com/pallets/werkzeug/pull/1303 +.. _`#1304`: https://github.com/pallets/werkzeug/pull/1304 .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index 887c3881b..494039088 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -138,9 +138,10 @@ def __call__(self, environ, start_response): forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') forwarded_port = getter('HTTP_X_FORWARDED_PORT', '') environ.update({ - 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), - 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), - 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST') + 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), + 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), + 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST'), + 'werkzeug.proxy_fix.orig_server_port': getter('SERVER_PORT'), }) forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] remote_addr = self.get_remote_addr(forwarded_for) From 8eb0b7399518f1da702b5dda08ae3d693027f8ee Mon Sep 17 00:00:00 2001 From: James Alexander Date: Mon, 14 May 2018 11:15:57 -0400 Subject: [PATCH 054/280] Ignore empty cookie keys --- CHANGES.rst | 6 +++++- tests/test_http.py | 12 ++++++++++++ werkzeug/http.py | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ea9da6b4e..609cefef0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,19 +26,23 @@ Release Date not Decided a :class:`~datastructures.MultiDict` to represent multiple values for a key. It already did this when passing a dict with a list value. (`#724`_) -- :meth:`wsgi.get_host` no longer looks at ``X-Forwarded-For``. Use +- :func:`wsgi.get_host` no longer looks at ``X-Forwarded-For``. Use :class:`~fixers.ProxyFix` to handle that. (`#609`_, `#1303`_) - :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Port`` header set by some proxies. (`#1023`_, `#1304`_) +- :func:`http.parse_cookie` ignores empty segments rather than + producing a cookie with no key or value. (`#1245`_, `#1301`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 +.. _`#1245`: https://github.com/pallets/werkzeug/pull/1245 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 +.. _`#1301`: https://github.com/pallets/werkzeug/pull/1301 .. _`#1303`: https://github.com/pallets/werkzeug/pull/1303 .. _`#1304`: https://github.com/pallets/werkzeug/pull/1304 .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 diff --git a/tests/test_http.py b/tests/test_http.py index 6f28f1536..32d0a270d 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -396,6 +396,18 @@ def test_bad_cookies(self): } ) + def test_empty_keys_are_ignored(self): + strict_eq( + dict(http.parse_cookie( + 'first=IamTheFirst ; a=1; a=2 ;second=andMeTwo; ; ' + )), + { + 'first': u'IamTheFirst', + 'a': u'2', + 'second': u'andMeTwo' + } + ) + def test_cookie_quoting(self): val = http.dump_cookie("foo", "?foo") strict_eq(val, 'foo="?foo"; Path=/') diff --git a/werkzeug/http.py b/werkzeug/http.py index 720d39160..8843a8632 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -999,6 +999,8 @@ def parse_cookie(header, charset='utf-8', errors='replace', cls=None): def _parse_pairs(): for key, val in _cookie_parse_impl(header): key = to_unicode(key, charset, errors, allow_none_charset=True) + if not key: + continue val = to_unicode(val, charset, errors, allow_none_charset=True) yield try_coerce_native(key), val From f4c8d12cfff7c91d5946423b1277feb043b40374 Mon Sep 17 00:00:00 2001 From: Ed Kellett Date: Wed, 23 May 2018 17:49:21 +0100 Subject: [PATCH 055/280] routing: optimize URL building --- tests/test_routing.py | 12 ++ werkzeug/routing.py | 336 ++++++++++++++++++++++++++++++++++++++---- werkzeug/urls.py | 25 +++- 3 files changed, 344 insertions(+), 29 deletions(-) diff --git a/tests/test_routing.py b/tests/test_routing.py index 9be2dc79f..c80f806bd 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -170,6 +170,18 @@ def test_basic_building(): assert adapter.build('foo', {}, force_external=True) == '//example.org/foo' +def test_long_build(): + long_args = dict(('v%d' % x, x) for x in range(10000)) + map = r.Map([ + r.Rule(''.join('/<%s>' % k for k in long_args.keys()), endpoint='bleep', build_only=True) + ]) + adapter = map.bind('localhost', '/') + url = adapter.build('bleep', long_args) + url += '/' + for v in long_args.values(): + assert '/%d' % v in url + + def test_defaults(): map = r.Map([ r.Rule('/foo/', defaults={'page': 1}, endpoint='foo'), diff --git a/werkzeug/routing.py b/werkzeug/routing.py index e7b567ee7..625244fe7 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -99,11 +99,15 @@ import re import uuid import posixpath +import dis +import sys +import types +from functools import partial from pprint import pformat from threading import Lock -from werkzeug.urls import url_encode, url_quote, url_join +from werkzeug.urls import url_encode, url_quote, url_join, fast_url_quote from werkzeug.utils import redirect, format_string from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed, \ BadHost @@ -741,6 +745,9 @@ def _build_regex(rule): if not self.is_leaf: self._trace.append((False, '/')) + self._build = self._compile_builder(False) + self._build_unknown = self._compile_builder(True) + if self.build_only: return regex = r'^%s%s$' % ( @@ -794,38 +801,311 @@ def match(self, path, method=None): return result + class BuilderCompiler: + JOIN_EMPTY = ''.join + if sys.version_info >= (3, 6): + OPARG_SIZE = 256 + OPARG_VARI = False + else: + OPARG_SIZE = 65536 + OPARG_VARI = True + + def __init__(self, rule): + self.rule = rule + self.consts = [] + self.const_table = {} + self.var = [] + self.var_table = {} + self.argdefs = () + self.defaults = dict(iteritems(self.rule.defaults or {})) + + def get_const(self, x): + """ + Return a constant ID for an object, adding it to the pool if not + already present. + """ + if x not in self.const_table: + self.const_table[x] = len(self.consts) + self.consts.append(x) + return self.const_table[x] + + def get_var(self, x): + """ + Return a local variable ID for a name, adding it to the pool if + not already present. + Our only use for local variables is as function arguments: any + variable name that exists before the call to add_defaults() will + become one. + """ + x = str(x) + if x not in self.var_table: + self.var_table[x] = len(self.var) + self.var.append(x) + return self.var_table[x] + + def add_defaults(self): + """ + It's allowed for a rule builder to receive any of its defaults as + arguments. We don't bother to check that they match anywhere, + since suitable_for() should have already done that, but we do + need them to be optional arguments. + Since their values are known at compile-time, the builder will + never refer to these arguments. + """ + # ensure every default exists + for k in self.defaults.keys(): + self.get_var(k) + # nb. reorder to put anything with a default at the end + req = [] + opt = [] + defs = [] + for k in self.var: + if k in self.defaults: + opt.append(k) + defs.append(self.defaults[k]) + else: + req.append(k) + self.var = req + opt + self.argdefs = tuple(defs) + for i, k in enumerate(self.var): + self.var_table[k] = i + + def collapse_constants(self, opl): + """ + Given a list of build operations, spit out a new list with runs + of constant elements joined. + """ + new = [] + for op, elem in opl: + if op is not None: + new.append((op, elem)) + continue + if elem == '': + continue + if not new or new[-1][0] is not None: + new.append((op, elem)) + continue + new[-1] = (None, new[-1][1] + elem) + if not new: + new.append((None, '')) + return new + + def build_op(self, op, arg=None): + """ + Return a byte representation of a Python instruction. + """ + if isinstance(op, str): + op = dis.opmap[op] + if arg is None and op >= dis.HAVE_ARGUMENT: + raise ValueError("Operation requires an argument: %s" % dis.opname[op]) + if arg is not None and op < dis.HAVE_ARGUMENT: + raise ValueError("Operation takes no argument: %s" % dis.opname[op]) + if arg is None: + arg = 0 + # Python 3.6 changed the argument to an 8-bit integer, so this + # could be a practical consideration + if arg >= self.OPARG_SIZE: + return (self.build_op('EXTENDED_ARG', arg // self.OPARG_SIZE) + + self.build_op(op, arg % self.OPARG_SIZE)) + if not self.OPARG_VARI: + return bytearray((op, arg)) + elif op >= dis.HAVE_ARGUMENT: + return bytearray((op, arg % 256, arg // 256)) + else: + return bytearray((op,)) + + def build_string(self, n): + """ + Return the correct opcode(s) for building a string from n elements. + If the ''.join crutch is needed, it must already be immediately + below the string elements on the stack. + """ + if 'BUILD_STRING' in dis.opmap: + return self.build_op('BUILD_STRING', n) + else: + return (self.build_op('BUILD_TUPLE', n) + + self.build_op('CALL_FUNCTION', 1)) + + def emit_build(self, ind, opl, append_unknown=False, encode_query_vars=None, kwargs=None): + ops = b'' + n = len(opl) + stack = 0 + stack_overhead = 0 + + for op, elem in opl: + if op is None: + ops += self.build_op('LOAD_CONST', self.get_const(elem)) + stack_overhead = 0 + continue + ops += self.build_op('LOAD_CONST', self.get_const(op)) + ops += self.build_op('LOAD_FAST', self.get_var(elem)) + ops += self.build_op('CALL_FUNCTION', 1) + stack_overhead = 2 + + stack += len(opl) + peak_stack = stack + stack_overhead + dont_build_string = False + needs_build_string = 'BUILD_STRING' not in dis.opmap + + if n <= 1: + dont_build_string = True + needs_build_string = False + + if append_unknown: + if 'BUILD_STRING' not in dis.opmap: + needs_build_string = True + ops = self.build_op('LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops + ops += self.build_op('LOAD_FAST', kwargs) + + # assemble this in its own buffers because we need to jump over it + uops = bytearray() # run if kwargs. TOS=kwargs + uops += self.build_op('LOAD_CONST', self.get_const(encode_query_vars)) + uops += self.build_op('ROT_TWO') + uops += self.build_op('CALL_FUNCTION', 1) + uops += self.build_op('LOAD_CONST', self.get_const('?')) + uops += self.build_op('ROT_TWO') + if dont_build_string: + uops += self.build_string(n + 2) + + nops = bytearray() # otherwise + if not dont_build_string: + # if we're going to build a string, we need to pad out to + # a constant length + nops += self.build_op('LOAD_CONST', self.get_const('')) + nops += self.build_op('DUP_TOP') + elif needs_build_string: + # we inserted the ''.join reference at the bottom of the + # stack, but we don't want to call it: throw it away + nops += self.build_op('ROT_TWO') + nops += self.build_op('POP_TOP') + nops += self.build_op('JUMP_FORWARD', len(uops)) + + # this jump needs to take its own length into account. the + # simple way to do that is to compute a minimal guess for the + # length of the jump instruction, and keep revising it upward + jump_op = self.build_op('JUMP_IF_TRUE_OR_POP', 0) + while True: + jump_len = len(jump_op) + jump_target = ind + len(ops) + jump_len + len(nops) + jump_op = self.build_op('JUMP_IF_TRUE_OR_POP', jump_target) + assert len(jump_op) >= jump_len + if len(jump_op) == jump_len: + break + + ops += jump_op + ops += nops + ops += uops + stack += 1 + n += 2 + peak_stack = max(peak_stack, stack + 2) + elif needs_build_string: + ops = self.build_op('LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops + peak_stack += 1 + if not dont_build_string: + ops += self.build_string(n) + return peak_stack, ops + + def compile(self, append_unknown=True): + flags = 0x08 + dom_ops = [] + url_ops = [] + opl = dom_ops + if append_unknown: + encode_query_vars = partial(url_encode, + charset=self.rule.map.charset, + sort=self.rule.map.sort_parameters, + key=self.rule.map.sort_key) + for is_dynamic, data in self.rule._trace: + if data == '|' and opl is dom_ops: + opl = url_ops + continue + # this seems like a silly case to ever come up but: + # if a default is given for a value that appears in the rule, + # resolve it to a constant ahead of time + if is_dynamic and data in self.defaults: + data = self.rule._converters[data].to_url(self.defaults[data]) + is_dynamic = False + if not is_dynamic: + opl.append((None, url_quote(to_bytes(data, self.rule.map.charset), + safe='/:|+'))) + continue + opl.append((self.rule._converters[data].to_url, data)) + dom_ops = self.collapse_constants(dom_ops) + url_ops = self.collapse_constants(url_ops) + for op, elem in (dom_ops + url_ops): + if op is not None: + self.get_var(elem) + self.add_defaults() + argcount = len(self.var) + # invalid name for paranoia reasons + self.get_var('.keyword_arguments') + stack = 0 + peak_stack = 0 + ops = b'' + if (not append_unknown and + len(dom_ops) == len(url_ops) == 1 and + dom_ops[0][0] is url_ops[0][0] is None): + # shortcut: just return the constant + stack = peak_stack = 1 + constant_value = (dom_ops[0][1], url_ops[0][1]) + ops += self.build_op('LOAD_CONST', self.get_const(constant_value)) + else: + ps, rv = self.emit_build(len(ops), dom_ops) + ops += rv + peak_stack = max(stack + ps, peak_stack) + stack += 1 + if append_unknown: + ps, rv = self.emit_build(len(ops), + url_ops, + append_unknown, + encode_query_vars, + argcount) + else: + ps, rv = self.emit_build(len(ops), url_ops) + ops += rv + peak_stack = max(stack + ps, peak_stack) + ops += self.build_op('BUILD_TUPLE', 2) + ops += self.build_op('RETURN_VALUE') + code_args = [argcount, + len(self.var), + peak_stack + len(self.var), + flags, + ops, + tuple(self.consts), + (), + tuple(self.var), + 'generated', + '' % self.rule.rule, + 1, + b''] + if sys.version_info >= (3,): + code_args[1:1] = [0] + else: + code_args[4] = str(code_args[4]) + co = types.CodeType(*code_args) + fn = types.FunctionType(co, {}, None, self.argdefs) + return fn + + def _compile_builder(self, append_unknown=True): + """Generate a function that builds this rule. + + :internal: + """ + return self.BuilderCompiler(self).compile(append_unknown) + def build(self, values, append_unknown=True): """Assembles the relative url for that rule and the subdomain. If building doesn't work for some reasons `None` is returned. :internal: """ - tmp = [] - add = tmp.append - processed = set(self.arguments) - for is_dynamic, data in self._trace: - if is_dynamic: - try: - add(self._converters[data].to_url(values[data])) - except ValidationError: - return - processed.add(data) + try: + if append_unknown: + return self._build_unknown(**values) else: - add(url_quote(to_bytes(data, self.map.charset), safe='/:|+')) - domain_part, url = (u''.join(tmp)).split(u'|', 1) - - if append_unknown: - query_vars = MultiDict(values) - for key in processed: - if key in query_vars: - del query_vars[key] - - if query_vars: - url += u'?' + url_encode(query_vars, charset=self.map.charset, - sort=self.map.sort_parameters, - key=self.map.sort_key) - - return domain_part, url + return self._build(**values) + except ValidationError: + return None def provides_defaults_for(self, rule): """Check if this rule has defaults for a given rule. @@ -938,7 +1218,7 @@ def to_python(self, value): return value def to_url(self, value): - return url_quote(value, charset=self.map.charset) + return fast_url_quote(text_type(value).encode(self.map.charset)) class UnicodeConverter(BaseConverter): @@ -1784,7 +2064,7 @@ def build(self, endpoint, values=None, method=None, force_external=False, (self.map.host_matching and host == self.server_name) or (not self.map.host_matching and domain_part == self.subdomain) ): - return str(url_join(self.script_name, './' + path.lstrip('/'))) + return '%s/%s' % (self.script_name.rstrip('/'), path.lstrip('/')) return str('%s//%s%s/%s' % ( self.url_scheme + ':' if self.url_scheme else '', host, diff --git a/werkzeug/urls.py b/werkzeug/urls.py index 5bd9a40d8..c0c1b9a67 100644 --- a/werkzeug/urls.py +++ b/werkzeug/urls.py @@ -390,7 +390,7 @@ def _url_encode_impl(obj, charset, encode_keys, sort, key): key = text_type(key).encode(charset) if not isinstance(value, bytes): value = text_type(value).encode(charset) - yield url_quote_plus(key) + '=' + url_quote_plus(value) + yield fast_url_quote_plus(key) + '=' + fast_url_quote_plus(value) def _url_unquote_legacy(value, unsafe=''): @@ -449,6 +449,29 @@ def url_parse(url, scheme=None, allow_fragments=True): return result_type(scheme, netloc, url, query, fragment) +def _make_url_encoder(charset='utf-8', errors='strict', safe='/:', unsafe=''): + if isinstance(safe, text_type): + safe = safe.encode(charset, errors) + if isinstance(unsafe, text_type): + unsafe = unsafe.encode(charset, errors) + safe = (frozenset(safe) | frozenset(_always_safe)) - frozenset(unsafe) + if not isinstance(next(iter(safe)), int): + safe = frozenset(map(ord, safe)) + table = [chr(c) if c in safe else '%%%02X' % c for c in range(256)] + quote = lambda s: ''.join(map(table.__getitem__, s)) + if isinstance(''.encode(), str): + return lambda s: quote(bytearray(s)) + return quote + + +fast_url_quote = _make_url_encoder() +_quote_plus = _make_url_encoder(safe=' ', unsafe='+') + + +def fast_url_quote_plus(string): + return _quote_plus(string).replace(' ', '+') + + def url_quote(string, charset='utf-8', errors='strict', safe='/:', unsafe=''): """URL encode a single string with a given encoding. From 9151ce60c56190307a5b4b9c66f811ddf3336139 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 23 May 2018 12:13:17 -0700 Subject: [PATCH 056/280] style, docs, changelog for #1281 --- CHANGES.rst | 3 + werkzeug/routing.py | 146 ++++++++++++++++++++++++-------------------- werkzeug/urls.py | 20 ++++-- 3 files changed, 98 insertions(+), 71 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 609cefef0..6a93c36a5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,6 +32,8 @@ Release Date not Decided set by some proxies. (`#1023`_, `#1304`_) - :func:`http.parse_cookie` ignores empty segments rather than producing a cookie with no key or value. (`#1245`_, `#1301`_) +- Building URLs is ~7x faster. Each :class:`~routing.Rule` compiles + an optimized function for building itself. (`#1281`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 @@ -41,6 +43,7 @@ Release Date not Decided .. _`#1245`: https://github.com/pallets/werkzeug/pull/1245 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 +.. _`#1281`: https://github.com/pallets/werkzeug/pull/1281 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 .. _`#1301`: https://github.com/pallets/werkzeug/pull/1301 .. _`#1303`: https://github.com/pallets/werkzeug/pull/1303 diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 625244fe7..ea70be2c5 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -817,12 +817,11 @@ def __init__(self, rule): self.var = [] self.var_table = {} self.argdefs = () - self.defaults = dict(iteritems(self.rule.defaults or {})) + self.defaults = dict(self.rule.defaults or {}) def get_const(self, x): - """ - Return a constant ID for an object, adding it to the pool if not - already present. + """Return a constant ID for an object, adding it to the pool + if not already present. """ if x not in self.const_table: self.const_table[x] = len(self.consts) @@ -830,12 +829,12 @@ def get_const(self, x): return self.const_table[x] def get_var(self, x): - """ - Return a local variable ID for a name, adding it to the pool if - not already present. - Our only use for local variables is as function arguments: any - variable name that exists before the call to add_defaults() will - become one. + """Return a local variable ID for a name, adding it to the + pool if not already present. + + Our only use for local variables is as function arguments: + any variable name that exists before the call to + ``add_defaults()`` will become one. """ x = str(x) if x not in self.var_table: @@ -844,18 +843,17 @@ def get_var(self, x): return self.var_table[x] def add_defaults(self): - """ - It's allowed for a rule builder to receive any of its defaults as - arguments. We don't bother to check that they match anywhere, - since suitable_for() should have already done that, but we do - need them to be optional arguments. - Since their values are known at compile-time, the builder will + """A rule builder is allowed to receive any of its defaults + as arguments. We don't bother to check that they match + anywhere, since ``suitable_for()`` should have already done + that, but we do need them to be optional arguments. Since + their values are known at compile-time, the builder will never refer to these arguments. """ # ensure every default exists for k in self.defaults.keys(): self.get_var(k) - # nb. reorder to put anything with a default at the end + # reorder to put anything with a default at the end req = [] opt = [] defs = [] @@ -871,10 +869,8 @@ def add_defaults(self): self.var_table[k] = i def collapse_constants(self, opl): - """ - Given a list of build operations, spit out a new list with runs - of constant elements joined. - """ + """Given a list of build operations, spit out a new list + with runs of constant elements joined.""" new = [] for op, elem in opl: if op is not None: @@ -891,22 +887,24 @@ def collapse_constants(self, opl): return new def build_op(self, op, arg=None): - """ - Return a byte representation of a Python instruction. - """ + """Return a byte representation of a Python instruction.""" if isinstance(op, str): op = dis.opmap[op] if arg is None and op >= dis.HAVE_ARGUMENT: - raise ValueError("Operation requires an argument: %s" % dis.opname[op]) + raise ValueError( + "Operation requires an argument: %s" % dis.opname[op]) if arg is not None and op < dis.HAVE_ARGUMENT: - raise ValueError("Operation takes no argument: %s" % dis.opname[op]) + raise ValueError( + "Operation takes no argument: %s" % dis.opname[op]) if arg is None: arg = 0 # Python 3.6 changed the argument to an 8-bit integer, so this # could be a practical consideration if arg >= self.OPARG_SIZE: - return (self.build_op('EXTENDED_ARG', arg // self.OPARG_SIZE) + - self.build_op(op, arg % self.OPARG_SIZE)) + return ( + self.build_op('EXTENDED_ARG', arg // self.OPARG_SIZE) + + self.build_op(op, arg % self.OPARG_SIZE) + ) if not self.OPARG_VARI: return bytearray((op, arg)) elif op >= dis.HAVE_ARGUMENT: @@ -915,18 +913,23 @@ def build_op(self, op, arg=None): return bytearray((op,)) def build_string(self, n): - """ - Return the correct opcode(s) for building a string from n elements. - If the ''.join crutch is needed, it must already be immediately - below the string elements on the stack. + """Return the correct opcode(s) for building a string from + ``n`` elements. If the ``''.join`` crutch is needed, it must + already be immediately below the string elements on the + stack. """ if 'BUILD_STRING' in dis.opmap: return self.build_op('BUILD_STRING', n) else: - return (self.build_op('BUILD_TUPLE', n) + - self.build_op('CALL_FUNCTION', 1)) + return ( + self.build_op('BUILD_TUPLE', n) + + self.build_op('CALL_FUNCTION', 1) + ) - def emit_build(self, ind, opl, append_unknown=False, encode_query_vars=None, kwargs=None): + def emit_build( + self, ind, opl, append_unknown=False, encode_query_vars=None, + kwargs=None + ): ops = b'' n = len(opl) stack = 0 @@ -954,12 +957,15 @@ def emit_build(self, ind, opl, append_unknown=False, encode_query_vars=None, kwa if append_unknown: if 'BUILD_STRING' not in dis.opmap: needs_build_string = True - ops = self.build_op('LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops + ops = self.build_op( + 'LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops ops += self.build_op('LOAD_FAST', kwargs) - # assemble this in its own buffers because we need to jump over it + # assemble this in its own buffers because we need to + # jump over it uops = bytearray() # run if kwargs. TOS=kwargs - uops += self.build_op('LOAD_CONST', self.get_const(encode_query_vars)) + uops += self.build_op( + 'LOAD_CONST', self.get_const(encode_query_vars)) uops += self.build_op('ROT_TWO') uops += self.build_op('CALL_FUNCTION', 1) uops += self.build_op('LOAD_CONST', self.get_const('?')) @@ -999,7 +1005,8 @@ def emit_build(self, ind, opl, append_unknown=False, encode_query_vars=None, kwa n += 2 peak_stack = max(peak_stack, stack + 2) elif needs_build_string: - ops = self.build_op('LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops + ops = self.build_op( + 'LOAD_CONST', self.get_const(self.JOIN_EMPTY)) + ops peak_stack += 1 if not dont_build_string: ops += self.build_string(n) @@ -1011,10 +1018,11 @@ def compile(self, append_unknown=True): url_ops = [] opl = dom_ops if append_unknown: - encode_query_vars = partial(url_encode, - charset=self.rule.map.charset, - sort=self.rule.map.sort_parameters, - key=self.rule.map.sort_key) + encode_query_vars = partial( + url_encode, + charset=self.rule.map.charset, + sort=self.rule.map.sort_parameters, + key=self.rule.map.sort_key) for is_dynamic, data in self.rule._trace: if data == '|' and opl is dom_ops: opl = url_ops @@ -1023,11 +1031,12 @@ def compile(self, append_unknown=True): # if a default is given for a value that appears in the rule, # resolve it to a constant ahead of time if is_dynamic and data in self.defaults: - data = self.rule._converters[data].to_url(self.defaults[data]) + data = self.rule._converters[data].to_url( + self.defaults[data]) is_dynamic = False if not is_dynamic: - opl.append((None, url_quote(to_bytes(data, self.rule.map.charset), - safe='/:|+'))) + opl.append((None, url_quote( + to_bytes(data, self.rule.map.charset), safe='/:|+'))) continue opl.append((self.rule._converters[data].to_url, data)) dom_ops = self.collapse_constants(dom_ops) @@ -1042,42 +1051,45 @@ def compile(self, append_unknown=True): stack = 0 peak_stack = 0 ops = b'' - if (not append_unknown and - len(dom_ops) == len(url_ops) == 1 and - dom_ops[0][0] is url_ops[0][0] is None): + if ( + not append_unknown + and len(dom_ops) == len(url_ops) == 1 + and dom_ops[0][0] is url_ops[0][0] is None + ): # shortcut: just return the constant stack = peak_stack = 1 constant_value = (dom_ops[0][1], url_ops[0][1]) - ops += self.build_op('LOAD_CONST', self.get_const(constant_value)) + ops += self.build_op( + 'LOAD_CONST', self.get_const(constant_value)) else: ps, rv = self.emit_build(len(ops), dom_ops) ops += rv peak_stack = max(stack + ps, peak_stack) stack += 1 if append_unknown: - ps, rv = self.emit_build(len(ops), - url_ops, - append_unknown, - encode_query_vars, - argcount) + ps, rv = self.emit_build( + len(ops), url_ops, append_unknown, encode_query_vars, + argcount) else: ps, rv = self.emit_build(len(ops), url_ops) ops += rv peak_stack = max(stack + ps, peak_stack) ops += self.build_op('BUILD_TUPLE', 2) ops += self.build_op('RETURN_VALUE') - code_args = [argcount, - len(self.var), - peak_stack + len(self.var), - flags, - ops, - tuple(self.consts), - (), - tuple(self.var), - 'generated', - '' % self.rule.rule, - 1, - b''] + code_args = [ + argcount, + len(self.var), + peak_stack + len(self.var), + flags, + ops, + tuple(self.consts), + (), + tuple(self.var), + 'generated', + '' % self.rule.rule, + 1, + b'' + ] if sys.version_info >= (3,): code_args[1:1] = [0] else: diff --git a/werkzeug/urls.py b/werkzeug/urls.py index c0c1b9a67..3aa3da714 100644 --- a/werkzeug/urls.py +++ b/werkzeug/urls.py @@ -15,6 +15,8 @@ :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +import functools +from functools import update_wrapper import os import re from werkzeug._compat import text_type, PY2, to_unicode, \ @@ -449,7 +451,17 @@ def url_parse(url, scheme=None, allow_fragments=True): return result_type(scheme, netloc, url, query, fragment) -def _make_url_encoder(charset='utf-8', errors='strict', safe='/:', unsafe=''): +def _make_fast_url_quote(charset='utf-8', errors='strict', safe='/:', unsafe=''): + """Precompile the translation table for a URL encoding function. + + Unlike :func:`url_quote`, the generated function only takes the + string to quote. + + :param charset: The charset to encode the result with. + :param errors: How to handle encoding errors. + :param safe: An optional sequence of safe characters to never encode. + :param unsafe: An optional sequence of unsafe characters to always encode. + """ if isinstance(safe, text_type): safe = safe.encode(charset, errors) if isinstance(unsafe, text_type): @@ -464,12 +476,12 @@ def _make_url_encoder(charset='utf-8', errors='strict', safe='/:', unsafe=''): return quote -fast_url_quote = _make_url_encoder() -_quote_plus = _make_url_encoder(safe=' ', unsafe='+') +fast_url_quote = _make_fast_url_quote() +_fast_quote_plus = _make_fast_url_quote(safe=' ', unsafe='+') def fast_url_quote_plus(string): - return _quote_plus(string).replace(' ', '+') + return _fast_quote_plus(string).replace(' ', '+') def url_quote(string, charset='utf-8', errors='strict', safe='/:', unsafe=''): From d2a35e18393e38fcd06696ec42410c0c9cce66f6 Mon Sep 17 00:00:00 2001 From: Chris Carini Date: Fri, 20 Apr 2018 22:33:18 -0700 Subject: [PATCH 057/280] customize the filenames generated by ProfilerMiddleware --- werkzeug/contrib/profiler.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/werkzeug/contrib/profiler.py b/werkzeug/contrib/profiler.py index be860afbc..1ae140717 100644 --- a/werkzeug/contrib/profiler.py +++ b/werkzeug/contrib/profiler.py @@ -9,7 +9,6 @@ stream provided (defaults to stderr). Example usage:: - from werkzeug.contrib.profiler import ProfilerMiddleware app = ProfilerMiddleware(app) @@ -60,22 +59,36 @@ class ProfilerMiddleware(object): directory, one file per request. Without it, a summary is printed to `stream` instead. + By giving the `profile_file_name_format` argument, the file name format + of the resulting files can be customized. The following are options that + can be part of the file name format: + - `%(method)s` - the request method; GET, POST, etc + - `%(path)s` - the request path or 'root' should one not exist + - `%(elapsed)06d` - the elapsed time of the request + - `%(time)d` - the time of the request + The default format is: '%(method)s.%(path)s.%(elapsed)06dms.%(time)d' + For the exact meaning of `sort_by` and `restrictions` consult the :mod:`profile` documentation. .. versionadded:: 0.9 Added support for `restrictions` and `profile_dir`. + .. versionadded:: 0.15 + Added support for `profile_file_name_format`. + :param app: the WSGI application to profile. :param stream: the stream for the profiled stats. defaults to stderr. :param sort_by: a tuple of columns to sort the result by. :param restrictions: a tuple of profiling strictions, not used if dumping to `profile_dir`. :param profile_dir: directory name to save pstat files + :param profile_file_name_format: format of the filename excluding the extension. """ def __init__(self, app, stream=None, - sort_by=('time', 'calls'), restrictions=(), profile_dir=None): + sort_by=('time', 'calls'), restrictions=(), profile_dir=None, + profile_file_name_format='%(method)s.%(path)s.%(elapsed)06dms.%(time)d'): if not available: raise RuntimeError('the profiler is not available because ' 'profile or pstat is not installed.') @@ -84,6 +97,7 @@ def __init__(self, app, stream=None, self._sort_by = sort_by self._restrictions = restrictions self._profile_dir = profile_dir + self._profile_file_name_format = profile_file_name_format def __call__(self, environ, start_response): response_body = [] @@ -105,14 +119,14 @@ def runapp(): elapsed = time.time() - start if self._profile_dir is not None: + data = { + 'method': environ['REQUEST_METHOD'], + 'path': environ.get('PATH_INFO').strip('/').replace('/', '.') or 'root', + 'elapsed': elapsed * 1000.0, + 'time': time.time() + } prof_filename = os.path.join(self._profile_dir, - '%s.%s.%06dms.%d.prof' % ( - environ['REQUEST_METHOD'], - environ.get('PATH_INFO').strip( - '/').replace('/', '.') or 'root', - elapsed * 1000.0, - time.time() - )) + (self._profile_file_name_format + '.prof') % data) p.dump_stats(prof_filename) else: From 037e92a3fe69c284e3ba851ac598b271d01e0ecd Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 23 May 2018 12:57:18 -0700 Subject: [PATCH 058/280] allow callable for filename_format use str.format instead of printf style update docs, add changelog --- CHANGES.rst | 4 +++ werkzeug/contrib/profiler.py | 57 +++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6a93c36a5..fc237e502 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -34,6 +34,9 @@ Release Date not Decided producing a cookie with no key or value. (`#1245`_, `#1301`_) - Building URLs is ~7x faster. Each :class:`~routing.Rule` compiles an optimized function for building itself. (`#1281`_) +- The filenames generated by + :class:`~contrib.profiler.ProfilerMiddleware` can be customized. + (`#1283`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 @@ -45,6 +48,7 @@ Release Date not Decided .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 .. _`#1281`: https://github.com/pallets/werkzeug/pull/1281 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 +.. _`#1283`: https://github.com/pallets/werkzeug/issues/1283 .. _`#1301`: https://github.com/pallets/werkzeug/pull/1301 .. _`#1303`: https://github.com/pallets/werkzeug/pull/1303 .. _`#1304`: https://github.com/pallets/werkzeug/pull/1304 diff --git a/werkzeug/contrib/profiler.py b/werkzeug/contrib/profiler.py index 1ae140717..8958907cf 100644 --- a/werkzeug/contrib/profiler.py +++ b/werkzeug/contrib/profiler.py @@ -59,14 +59,19 @@ class ProfilerMiddleware(object): directory, one file per request. Without it, a summary is printed to `stream` instead. - By giving the `profile_file_name_format` argument, the file name format - of the resulting files can be customized. The following are options that - can be part of the file name format: - - `%(method)s` - the request method; GET, POST, etc - - `%(path)s` - the request path or 'root' should one not exist - - `%(elapsed)06d` - the elapsed time of the request - - `%(time)d` - the time of the request - The default format is: '%(method)s.%(path)s.%(elapsed)06dms.%(time)d' + The file name format can be customized by passing + ``filename_format``. If it is a string, it will be formatted using + :meth:`str.format` with the following fields available: + + - ``{method}`` - the request method; GET, POST, etc + - ``{path}`` - the request path or 'root' should one not exist + - ``{elapsed}`` - the elapsed time of the request + - ``{time}`` - the time of the request + + If it is a callable, it will be called with the WSGI ``environ`` + dict and should return a filename. Either way, the ``'.prof'`` + extension will be appended to the name. The default format is + ``'{method}.{path}.{elapsed:06d}ms.{time:d}'``. For the exact meaning of `sort_by` and `restrictions` consult the :mod:`profile` documentation. @@ -75,20 +80,22 @@ class ProfilerMiddleware(object): Added support for `restrictions` and `profile_dir`. .. versionadded:: 0.15 - Added support for `profile_file_name_format`. + Added ``profile_file_name_format``. :param app: the WSGI application to profile. :param stream: the stream for the profiled stats. defaults to stderr. :param sort_by: a tuple of columns to sort the result by. - :param restrictions: a tuple of profiling strictions, not used if dumping + :param restrictions: a tuple of profiling restrictions, not used if dumping to `profile_dir`. :param profile_dir: directory name to save pstat files - :param profile_file_name_format: format of the filename excluding the extension. + :param filename_format: format of the filename excluding the extension. """ - def __init__(self, app, stream=None, - sort_by=('time', 'calls'), restrictions=(), profile_dir=None, - profile_file_name_format='%(method)s.%(path)s.%(elapsed)06dms.%(time)d'): + def __init__( + self, app, stream=None, + sort_by=('time', 'calls'), restrictions=(), profile_dir=None, + filename_format='%(method)s.%(path)s.%(elapsed)06dms.%(time)d' + ): if not available: raise RuntimeError('the profiler is not available because ' 'profile or pstat is not installed.') @@ -97,7 +104,7 @@ def __init__(self, app, stream=None, self._sort_by = sort_by self._restrictions = restrictions self._profile_dir = profile_dir - self._profile_file_name_format = profile_file_name_format + self._filename_format = filename_format def __call__(self, environ, start_response): response_body = [] @@ -119,14 +126,18 @@ def runapp(): elapsed = time.time() - start if self._profile_dir is not None: - data = { - 'method': environ['REQUEST_METHOD'], - 'path': environ.get('PATH_INFO').strip('/').replace('/', '.') or 'root', - 'elapsed': elapsed * 1000.0, - 'time': time.time() - } - prof_filename = os.path.join(self._profile_dir, - (self._profile_file_name_format + '.prof') % data) + if callable(self._filename_format): + filename = self._filename_format(environ) + else: + filename = self._filename_format.format( + method=environ['REQUEST_METHOD'], + path=( + environ.get('PATH_INFO').strip('/').replace('/', '.') + or 'root'), + elapsed=elapsed * 1000.0, + time=time.time(), + ) + prof_filename = os.path.join(self._profile_dir, filename + '.prof') p.dump_stats(prof_filename) else: From 8f3db63ecd814efcb28fce2ee57d930ebe42be72 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 23 May 2018 13:19:15 -0700 Subject: [PATCH 059/280] fix unused imports --- werkzeug/urls.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/werkzeug/urls.py b/werkzeug/urls.py index 3aa3da714..8a95b4792 100644 --- a/werkzeug/urls.py +++ b/werkzeug/urls.py @@ -15,18 +15,16 @@ :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -import functools -from functools import update_wrapper +from collections import namedtuple import os import re -from werkzeug._compat import text_type, PY2, to_unicode, \ - to_native, implements_to_string, try_coerce_native, \ - normalize_string_tuple, make_literal_wrapper, \ - fix_tuple_repr -from werkzeug._internal import _encode_idna, _decode_idna -from werkzeug.datastructures import MultiDict, iter_multi_items -from collections import namedtuple +from werkzeug._compat import ( + PY2, fix_tuple_repr, implements_to_string, make_literal_wrapper, + normalize_string_tuple, text_type, to_native, to_unicode, try_coerce_native +) +from werkzeug._internal import _decode_idna, _encode_idna +from werkzeug.datastructures import MultiDict, iter_multi_items # A regular expression for what a valid schema looks like _scheme_re = re.compile(r'^[a-zA-Z0-9+-.]+$') From f075a41e6f5bc15ab1a36022120a16cf5e2c74c6 Mon Sep 17 00:00:00 2001 From: Ilan Shamir Date: Mon, 15 Jan 2018 15:14:57 +0200 Subject: [PATCH 060/280] Implemented x-forwarded-prefix proxy fix + test --- tests/contrib/test_fixers.py | 15 +++++++++++++++ werkzeug/contrib/fixers.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py index bfaf51be4..eb174d9f6 100644 --- a/tests/contrib/test_fixers.py +++ b/tests/contrib/test_fixers.py @@ -152,6 +152,21 @@ def app(request): assert wsgi_headers['Location'] == '{}/foo/bar.hml'.format( assumed_host) + def test_proxy_fix_forwarded_prefix(self): + @fixers.ProxyFix + @Request.application + def app(request): + return Response('%s' % ( + request.script_root + )) + environ = dict( + create_environ(), + HTTP_X_FORWARDED_PREFIX="/foo/bar", + ) + + response = Response.from_app(app, environ) + assert response.get_data() == b'/foo/bar' + def test_proxy_fix_weird_enum(self): @fixers.ProxyFix @Request.application diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index 494039088..b4bcd3c08 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -137,6 +137,7 @@ def __call__(self, environ, start_response): forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') forwarded_port = getter('HTTP_X_FORWARDED_PORT', '') + forwarded_prefix = getter('HTTP_X_FORWARDED_PREFIX', '') environ.update({ 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), @@ -160,6 +161,8 @@ def __call__(self, environ, start_response): environ['SERVER_PORT'] = forwarded_port if forwarded_proto: environ['wsgi.url_scheme'] = forwarded_proto + if forwarded_prefix: + environ['SCRIPT_NAME'] = forwarded_prefix return self.app(environ, start_response) From d1454a8f06e71cfd0f3c1fb44c99daa0ad0e09ae Mon Sep 17 00:00:00 2001 From: Ilan Shamir Date: Tue, 16 Jan 2018 16:26:00 +0200 Subject: [PATCH 061/280] Add test for url build/match --- tests/contrib/test_fixers.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py index eb174d9f6..e3296d66e 100644 --- a/tests/contrib/test_fixers.py +++ b/tests/contrib/test_fixers.py @@ -20,6 +20,7 @@ from werkzeug.contrib import fixers from werkzeug.utils import redirect from werkzeug.wsgi import get_host +from werkzeug.routing import Map, Rule @Request.application @@ -156,8 +157,15 @@ def test_proxy_fix_forwarded_prefix(self): @fixers.ProxyFix @Request.application def app(request): - return Response('%s' % ( - request.script_root + m = Map([Rule('/downloads', endpoint='downloads/index')]) + urls = m.bind_to_environ(request.environ) + # make sure: + # 1. urls are built correctly with the given prefix header + # 2. urls are matched correctly - not affected by header + return Response('%s|%s|%s' % ( + request.script_root, + urls.build("downloads/index"), + urls.match('/downloads')[0] )) environ = dict( create_environ(), @@ -165,7 +173,7 @@ def app(request): ) response = Response.from_app(app, environ) - assert response.get_data() == b'/foo/bar' + assert response.get_data() == b'/foo/bar|/foo/bar/downloads|downloads/index' def test_proxy_fix_weird_enum(self): @fixers.ProxyFix From b4382f9d47d78ccf3bdb168f3d2e0f310fdefde4 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 24 May 2018 12:21:24 -0700 Subject: [PATCH 062/280] store original script_name add changelog --- CHANGES.rst | 4 ++++ werkzeug/contrib/fixers.py | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fc237e502..25bf30028 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,9 @@ Release Date not Decided :class:`~fixers.ProxyFix` to handle that. (`#609`_, `#1303`_) - :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Port`` header set by some proxies. (`#1023`_, `#1304`_) +- :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Prefix`` header + set by some proxies by changing the WSGI environ ``SCRIPT_NAME``. + (`#1237`_) - :func:`http.parse_cookie` ignores empty segments rather than producing a cookie with no key or value. (`#1245`_, `#1301`_) - Building URLs is ~7x faster. Each :class:`~routing.Rule` compiles @@ -43,6 +46,7 @@ Release Date not Decided .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 +.. _`#1237`: https://github.com/pallets/werkzeug/pull/1237 .. _`#1245`: https://github.com/pallets/werkzeug/pull/1245 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index b4bcd3c08..d8d4f9113 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -143,6 +143,7 @@ def __call__(self, environ, start_response): 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST'), 'werkzeug.proxy_fix.orig_server_port': getter('SERVER_PORT'), + 'werkzeug.proxy_fix.orig_script_name': getter('SCRIPT_NAME'), }) forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] remote_addr = self.get_remote_addr(forwarded_for) From a6a619e54f86f10eba8bc2b0c7ac92153a79e111 Mon Sep 17 00:00:00 2001 From: Marcin Paciulan Date: Wed, 23 May 2018 13:07:14 +0200 Subject: [PATCH 063/280] Added support for CSV x-forwarded-proto header in ProxyFix --- werkzeug/contrib/fixers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index d8d4f9113..86e977baa 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -133,7 +133,7 @@ def get_remote_addr(self, forwarded_for): def __call__(self, environ, start_response): getter = environ.get - forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '') + forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '').split(',') forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') forwarded_port = getter('HTTP_X_FORWARDED_PORT', '') @@ -146,6 +146,7 @@ def __call__(self, environ, start_response): 'werkzeug.proxy_fix.orig_script_name': getter('SCRIPT_NAME'), }) forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] + forwarded_proto = [x for x in [x.strip() for x in forwarded_proto] if x] remote_addr = self.get_remote_addr(forwarded_for) if remote_addr is not None: environ['REMOTE_ADDR'] = remote_addr @@ -161,7 +162,7 @@ def __call__(self, environ, start_response): else: environ['SERVER_PORT'] = forwarded_port if forwarded_proto: - environ['wsgi.url_scheme'] = forwarded_proto + environ['wsgi.url_scheme'] = forwarded_proto[0] if forwarded_prefix: environ['SCRIPT_NAME'] = forwarded_prefix return self.app(environ, start_response) From 4f4ff122f2025773d6e7ae8b56095a4d3ace2614 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 24 May 2018 13:05:08 -0700 Subject: [PATCH 064/280] add changelog and test --- CHANGES.rst | 3 +++ tests/contrib/test_fixers.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 25bf30028..2a0952b50 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,8 @@ Release Date not Decided - :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Prefix`` header set by some proxies by changing the WSGI environ ``SCRIPT_NAME``. (`#1237`_) +- :class:`~fixers.ProxyFix` handles chained ``X-Forwarded-Proto`` + headers. (`#1312`_) - :func:`http.parse_cookie` ignores empty segments rather than producing a cookie with no key or value. (`#1245`_, `#1301`_) - Building URLs is ~7x faster. Each :class:`~routing.Rule` compiles @@ -57,6 +59,7 @@ Release Date not Decided .. _`#1303`: https://github.com/pallets/werkzeug/pull/1303 .. _`#1304`: https://github.com/pallets/werkzeug/pull/1304 .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 +.. _`#1312`: https://github.com/pallets/werkzeug/pull/1312 Version 0.14.1 diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py index e3296d66e..0ff91a93f 100644 --- a/tests/contrib/test_fixers.py +++ b/tests/contrib/test_fixers.py @@ -119,6 +119,11 @@ def test_path_info_from_request_uri_fix(self): 'HTTP_X_FORWARDED_PORT': '443', 'HTTP_X_FORWARDED_FOR': '1.2.3.4, 5.6.7.8' }, '1.2.3.4', 'https://example.com', id='All together'), + pytest.param({ + 'HTTP_HOST': 'internal', + 'REMOTE_ADDR': '192.168.0.1', + 'HTTP_X_FORWARDED_PROTO': 'https, http', + }, '192.168.0.1', 'https://internal', id='Multiple Proto'), ]) def test_proxy_fix(self, environ, assumed_addr, assumed_host): @Request.application From 6cbed92d7820cd0dda46b6120d26515da7fd1ef1 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 28 May 2018 09:59:01 -0700 Subject: [PATCH 065/280] refactor ProxyFix middleware support multiple values for all headers configure trust for each header separately store original values in dict in environ parametrize tests for ProxyFix and get_host rewrite docs for ProxyFix and get_host --- CHANGES.rst | 39 +++--- tests/contrib/test_fixers.py | 229 ++++++++++++++++------------------- tests/test_wsgi.py | 66 ++++++---- werkzeug/contrib/fixers.py | 219 ++++++++++++++++++++++++--------- werkzeug/routing.py | 3 +- werkzeug/wsgi.py | 25 ++-- 6 files changed, 347 insertions(+), 234 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2a0952b50..c4e1836f1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,13 +7,12 @@ Werkzeug Changelog Version 0.15 ------------ -Release Date not Decided +Unreleased -- Fix a bug in ``werkzeug.wsgi.ProxyMiddleware`` with query string. - (`#1252`_) -- Add 412 status code. -- Cleanup ``werkzeug.security`` module, remove predated hashlib support. - (`#1282`_) +- :class:`~werkzeug.wsgi.ProxyMiddleware` proxies the query string. + (`#1252`_) +- Cleanup ``werkzeug.security`` module, remove predated hashlib + support. (`#1282`_) - :class:`~test.EnvironBuilder` doesn't set ``CONTENT_TYPE`` or ``CONTENT_LENGTH`` in the environ if they aren't set. Previously these used default values if they weren't set. Now it's possible to @@ -27,14 +26,25 @@ Release Date not Decided for a key. It already did this when passing a dict with a list value. (`#724`_) - :func:`wsgi.get_host` no longer looks at ``X-Forwarded-For``. Use - :class:`~fixers.ProxyFix` to handle that. (`#609`_, `#1303`_) -- :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Port`` header - set by some proxies. (`#1023`_, `#1304`_) -- :class:`~fixers.ProxyFix` handles the ``X-Forwarded-Prefix`` header - set by some proxies by changing the WSGI environ ``SCRIPT_NAME``. - (`#1237`_) -- :class:`~fixers.ProxyFix` handles chained ``X-Forwarded-Proto`` - headers. (`#1312`_) + :class:`~contrib.fixers.ProxyFix` to handle that. (`#609`_, + `#1303`_) +- :class:`~contrib.fixers.ProxyFix` is refactored to support more + headers, multiple values, and more secure configuration. + + - Each header supports multiple values. The trusted number of + proxies is configured separately for each header. The + ``num_proxies`` argument is deprecated. (`#1314`_) + - Sets ``SERVER_NAME`` and ``SERVER_PORT`` based on + ``X-Forwarded-Host``. (`#1314`_) + - Sets ``SERVER_PORT`` and modifies ``HTTP_HOST`` based on + ``X-Forwarded-Port``. (`#1023`_, `#1304`_) + - Sets ``SCRIPT_NAME`` based on ``X-Forwarded-Prefix``. (`#1237`_) + - The original WSGI environment values are stored in the + ``werkzeug.proxy_fix.orig`` key, a dict. The individual keys + ``werkzeug.proxy_fix.orig_remote_addr``, + ``werkzeug.proxy_fix.orig_wsgi_url_scheme``, and + ``werkzeug.proxy_fix.orig_http_host`` are deprecated. + - :func:`http.parse_cookie` ignores empty segments rather than producing a cookie with no key or value. (`#1245`_, `#1301`_) - Building URLs is ~7x faster. Each :class:`~routing.Rule` compiles @@ -60,6 +70,7 @@ Release Date not Decided .. _`#1304`: https://github.com/pallets/werkzeug/pull/1304 .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 .. _`#1312`: https://github.com/pallets/werkzeug/pull/1312 +.. _`#1314`: https://github.com/pallets/werkzeug/pull/1314 Version 0.14.1 diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py index 0ff91a93f..1edf69a93 100644 --- a/tests/contrib/test_fixers.py +++ b/tests/contrib/test_fixers.py @@ -10,17 +10,13 @@ """ import pytest -from tests import strict_eq -from werkzeug._compat import to_bytes +from werkzeug.contrib import fixers from werkzeug.datastructures import ResponseCacheControl from werkzeug.http import parse_cache_control_header - -from werkzeug.test import create_environ, Client -from werkzeug.wrappers import Request, Response -from werkzeug.contrib import fixers -from werkzeug.utils import redirect -from werkzeug.wsgi import get_host from werkzeug.routing import Map, Rule +from werkzeug.test import Client, create_environ +from werkzeug.utils import redirect +from werkzeug.wrappers import Request, Response @Request.application @@ -60,139 +56,120 @@ def test_path_info_from_request_uri_fix(self): response = Response.from_app(app, env) assert response.get_data() == b'PATH_INFO: /foo%bar\nSCRIPT_NAME: /test' - @pytest.mark.parametrize('environ,assumed_addr,assumed_host', [ - pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1' - }, '127.0.0.1', 'http://internal', id='No proxy, with Host'), - pytest.param({ - 'SERVER_NAME': 'internal', - 'SERVER_PORT': '80', - 'REMOTE_ADDR': '127.0.0.1' - }, '127.0.0.1', 'http://internal', id='No proxy, no Host'), - pytest.param({ - 'HTTP_HOST': 'internal:80', - 'REMOTE_ADDR': '127.0.0.1' - }, '127.0.0.1', 'http://internal', id='Sanitize HTTP port'), - pytest.param({ - 'wsgi.url_scheme': 'https', - 'HTTP_HOST': 'internal:443', - 'REMOTE_ADDR': '127.0.0.1' - }, '127.0.0.1', 'https://internal', id='Sanitize HTTPS port'), - pytest.param({ - 'HTTP_HOST': 'internal:8080', - 'REMOTE_ADDR': '127.0.0.1' - }, '127.0.0.1', 'http://internal:8080', id='Custom port'), - pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_X_FORWARDED_FOR': '1.2.3.4, 5.6.7.8' - }, '1.2.3.4', 'http://internal', id='X-Forwarded-For'), - pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1', + @pytest.mark.parametrize(('kwargs', 'base', 'url_root'), ( + pytest.param({}, { + 'REMOTE_ADDR': '192.168.0.2', + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': '192.168.0.1', + }, 'http://spam/', id='for'), + pytest.param({'x_proto': 1}, { + 'HTTP_HOST': 'spam', 'HTTP_X_FORWARDED_PROTO': 'https', - 'HTTP_X_FORWARDED_HOST': 'example.com', - 'HTTP_X_FORWARDED_PORT': '8443', - }, '127.0.0.1', 'https://example.com:8443', id='X-Forwarded-*'), - pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1', + }, 'https://spam/', id='proto'), + pytest.param({'x_host': 1}, { + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_HOST': 'eggs', + }, 'http://eggs/', id='host'), + pytest.param({'x_port': 1}, { + 'HTTP_HOST': 'spam', 'HTTP_X_FORWARDED_PORT': '8080', - }, '127.0.0.1', 'http://internal:8080', id='HTTP X-Port, no X-Host'), - pytest.param({ - 'SERVER_NAME': 'internal', - 'REMOTE_ADDR': '127.0.0.1', + }, 'http://spam:8080/', id='port, host without port'), + pytest.param({'x_port': 1}, { + 'HTTP_HOST': 'spam:9000', 'HTTP_X_FORWARDED_PORT': '8080', - }, '127.0.0.1', 'http://internal:8080', id='HTTP X-Port, no Host'), - pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_X_FORWARDED_PROTO': 'https', - 'HTTP_X_FORWARDED_PORT': '8443', - }, '127.0.0.1', 'https://internal:8443', id='HTTPS X-Port, no X-Host'), + }, 'http://spam:8080/', id='port, host with port'), + pytest.param({'x_port': 1}, { + 'SERVER_NAME': 'spam', + 'SERVER_PORT': '9000', + 'HTTP_X_FORWARDED_PORT': '8080', + }, 'http://spam:8080/', id='port, name'), + pytest.param({'x_prefix': 1}, { + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_PREFIX': '/eggs', + }, 'http://spam/eggs/', id='prefix'), pytest.param({ - 'HTTP_HOST': 'internal', - 'REMOTE_ADDR': '127.0.0.1', + 'x_for': 1, 'x_proto': 1, 'x_host': 1, 'x_port': 1, 'x_prefix': 1 + }, { + 'REMOTE_ADDR': '192.168.0.2', + 'HTTP_HOST': 'spam:9000', + 'HTTP_X_FORWARDED_FOR': '192.168.0.1', 'HTTP_X_FORWARDED_PROTO': 'https', - 'HTTP_X_FORWARDED_HOST': 'example.com', + 'HTTP_X_FORWARDED_HOST': 'eggs', 'HTTP_X_FORWARDED_PORT': '443', - 'HTTP_X_FORWARDED_FOR': '1.2.3.4, 5.6.7.8' - }, '1.2.3.4', 'https://example.com', id='All together'), - pytest.param({ - 'HTTP_HOST': 'internal', + 'HTTP_X_FORWARDED_PREFIX': '/ham', + }, 'https://eggs/ham/', id='all'), + pytest.param({'x_for': 2}, { + 'REMOTE_ADDR': '192.168.0.3', + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': '192.168.0.1, 192.168.0.2', + }, 'http://spam/', id='multiple for'), + pytest.param({'x_for': 0}, { + 'REMOTE_ADDR': '192.168.0.1', + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': '192.168.0.2', + }, 'http://spam/', id='ignore 0'), + pytest.param({'x_for': 3}, { + 'REMOTE_ADDR': '192.168.0.1', + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': '192.168.0.3, 192.168.0.2', + }, 'http://spam/', id='ignore len < trusted'), + pytest.param({}, { + 'REMOTE_ADDR': '192.168.0.2', + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': '192.168.0.3, 192.168.0.1', + }, 'http://spam/', id='ignore untrusted'), + pytest.param({'x_for': 2}, { 'REMOTE_ADDR': '192.168.0.1', - 'HTTP_X_FORWARDED_PROTO': 'https, http', - }, '192.168.0.1', 'https://internal', id='Multiple Proto'), - ]) - def test_proxy_fix(self, environ, assumed_addr, assumed_host): + 'HTTP_HOST': 'spam', + 'HTTP_X_FORWARDED_FOR': ', 192.168.0.3' + }, 'http://spam/', id='ignore empty') + )) + def test_proxy_fix_new(self, kwargs, base, url_root): @Request.application def app(request): - return Response('%s|%s' % ( - request.remote_addr, - # do not use request.host as this fixes too :) - request.environ['wsgi.url_scheme'] + '://' + - get_host(request.environ) - )) - app = fixers.ProxyFix(app, num_proxies=2) - has_host = 'HTTP_HOST' in environ - environ = dict( - create_environ(), - **environ - ) - if not has_host: - del environ['HTTP_HOST'] # create_environ() defaults to 'localhost' - + # for header + assert request.remote_addr == '192.168.0.1' + # proto, host, port, prefix headers + assert request.url_root == url_root + + urls = url_map.bind_to_environ(request.environ) + # build includes prefix + assert urls.build('parrot') == '/'.join(( + request.script_root, 'parrot')) + # match doesn't include prefix + assert urls.match('/parrot')[0] == 'parrot' + + return Response('success') + + url_map = Map([Rule('/parrot', endpoint='parrot')]) + app = fixers.ProxyFix(app, **kwargs) + + base.setdefault('REMOTE_ADDR', '192.168.0.1') + environ = create_environ(environ_overrides=base) + # host is always added, remove it if the test doesn't set it + if 'HTTP_HOST' not in base: + del environ['HTTP_HOST'] + + # ensure app request has correct headers response = Response.from_app(app, environ) + assert response.get_data() == b'success' - assert response.get_data() == to_bytes('{}|{}'.format( - assumed_addr, assumed_host)) - - # And we must check that if it is a redirection it is - # correctly done: - - redirect_app = redirect('/foo/bar.hml') + # ensure redirect location is correct + redirect_app = redirect( + url_map.bind_to_environ(environ).build('parrot')) response = Response.from_app(redirect_app, environ) + location = response.headers['Location'] + assert location == url_root + 'parrot' - wsgi_headers = response.get_wsgi_headers(environ) - assert wsgi_headers['Location'] == '{}/foo/bar.hml'.format( - assumed_host) + def test_proxy_fix_deprecations(self): + app = pytest.deprecated_call(fixers.ProxyFix, None, 2) + assert app.x_for == 2 - def test_proxy_fix_forwarded_prefix(self): - @fixers.ProxyFix - @Request.application - def app(request): - m = Map([Rule('/downloads', endpoint='downloads/index')]) - urls = m.bind_to_environ(request.environ) - # make sure: - # 1. urls are built correctly with the given prefix header - # 2. urls are matched correctly - not affected by header - return Response('%s|%s|%s' % ( - request.script_root, - urls.build("downloads/index"), - urls.match('/downloads')[0] - )) - environ = dict( - create_environ(), - HTTP_X_FORWARDED_PREFIX="/foo/bar", - ) - - response = Response.from_app(app, environ) - assert response.get_data() == b'/foo/bar|/foo/bar/downloads|downloads/index' + with pytest.deprecated_call(): + assert app.num_proxies == 2 - def test_proxy_fix_weird_enum(self): - @fixers.ProxyFix - @Request.application - def app(request): - return Response(request.remote_addr) - environ = dict( - create_environ(), - HTTP_X_FORWARDED_FOR=',', - REMOTE_ADDR='127.0.0.1', - ) - - response = Response.from_app(app, environ) - strict_eq(response.get_data(), b'127.0.0.1') + with pytest.deprecated_call(): + assert app.get_remote_addr(['spam', 'eggs']) == 'spam' def test_header_rewriter_fix(self): @Request.application diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 4fd99ed15..1e3c5ba84 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -98,32 +98,46 @@ def dummy_application(environ, start_response): assert b''.join(app_iter).strip() == b'NOT FOUND' -def test_get_host_by_http_host(): - env = {'HTTP_HOST': 'example.org', 'wsgi.url_scheme': 'http'} - assert wsgi.get_host(env) == 'example.org' - env['HTTP_HOST'] = 'example.org:8080' - assert wsgi.get_host(env) == 'example.org:8080' - env['HOST_NAME'] = 'ignore me' - assert wsgi.get_host(env) == 'example.org:8080' - - -def test_get_host_by_server_name_and_port(): - env = {'SERVER_NAME': 'example.org', 'SERVER_PORT': '80', - 'wsgi.url_scheme': 'http'} - assert wsgi.get_host(env) == 'example.org' - env['wsgi.url_scheme'] = 'https' - assert wsgi.get_host(env) == 'example.org:80' - env['SERVER_PORT'] = '8080' - assert wsgi.get_host(env) == 'example.org:8080' - env['SERVER_PORT'] = '443' - assert wsgi.get_host(env) == 'example.org' - - -def test_get_host_ignore_x_forwarded_for(): - env = {'HTTP_X_FORWARDED_HOST': 'forwarded', - 'HTTP_HOST': 'example.org', - 'wsgi.url_scheme': 'http'} - assert wsgi.get_host(env) == 'example.org' +@pytest.mark.parametrize(('environ', 'expect'), ( + pytest.param({ + 'HTTP_HOST': 'spam', + }, 'spam', id='host'), + pytest.param({ + 'HTTP_HOST': 'spam:80', + }, 'spam', id='host, strip http port'), + pytest.param({ + 'wsgi.url_scheme': 'https', + 'HTTP_HOST': 'spam:443', + }, 'spam', id='host, strip https port'), + pytest.param({ + 'HTTP_HOST': 'spam:8080', + }, 'spam:8080', id='host, custom port'), + pytest.param({ + 'HTTP_HOST': 'spam', + 'SERVER_NAME': 'eggs', + 'SERVER_PORT': '80', + }, 'spam', id='prefer host'), + pytest.param({ + 'SERVER_NAME': 'eggs', + 'SERVER_PORT': '80' + }, 'eggs', id='name, ignore http port'), + pytest.param({ + 'wsgi.url_scheme': 'https', + 'SERVER_NAME': 'eggs', + 'SERVER_PORT': '443' + }, 'eggs', id='name, ignore https port'), + pytest.param({ + 'SERVER_NAME': 'eggs', + 'SERVER_PORT': '8080' + }, 'eggs:8080', id='name, custom port'), + pytest.param({ + 'HTTP_HOST': 'ham', + 'HTTP_X_FORWARDED_HOST': 'eggs' + }, 'ham', id='ignore x-forwarded-host'), +)) +def test_get_host(environ, expect): + environ.setdefault('wsgi.url_scheme', 'http') + assert wsgi.get_host(environ) == expect def test_get_host_validate_trusted_hosts(): diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py index 86e977baa..1f08f9c79 100644 --- a/werkzeug/contrib/fixers.py +++ b/werkzeug/contrib/fixers.py @@ -16,6 +16,8 @@ :copyright: Copyright 2009 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +import warnings + try: from urllib import unquote except ImportError: @@ -95,76 +97,181 @@ def __call__(self, environ, start_response): class ProxyFix(object): + """Adjust the WSGI environ based on ``Forwarded`` headers that + proxies in front of the application may set. + + When the application is running behind a server like Nginx (or + another server or proxy), WSGI will see the request as coming from + that server rather than the real client. Proxies set various headers + to track where the request actually came from. + + This middleware should only be applied if the application is + actually behind such a proxy, and should be configured with the + number of proxies that are chained in front of it. Not all proxies + set all the headers. Since incoming headers can be faked, you must + set how many proxies are setting each header so the middleware knows + what to trust. + + The original values of the headers are stored in the WSGI + environ as ``werkzeug.proxy_fix.orig``, a dict. + + :param app: The WSGI application. + :param x_for: Number of values to trust for ``X-Forwarded-For``. + :param x_proto: Number of values to trust for ``X-Forwarded-Proto``. + :param x_host: Number of values to trust for ``X-Forwarded-Host``. + :param x_port: Number of values to trust for ``X-Forwarded-Port``. + :param x_prefix: Number of values to trust for + ``X-Forwarded-Prefix``. + :param num_proxies: Deprecated, use ``x_for`` instead. + + .. versionchanged:: 0.15 + Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``. + + .. versionchanged:: 0.15 + All headers support multiple values. The ``num_proxies`` + argument is deprecated. Each header is configured with a + separate number of trusted proxies. + + .. versionchanged:: 0.15 + Original WSGI environ values are stored in the + ``werkzeug.proxy_fix.orig`` dict. ``orig_remote_addr``, + ``orig_wsgi_url_scheme``, and ``orig_http_host`` are deprecated. + + .. versionchanged:: 0.15 + ``X-Fowarded-Host`` and ``X-Forwarded-Port`` modify + ``SERVER_NAME`` and ``SERVER_PORT``. + """ - """This middleware can be applied to add HTTP proxy support to an - application that was not designed with HTTP proxies in mind. It - sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers. While - Werkzeug-based applications already can use - :py:func:`werkzeug.wsgi.get_host` to retrieve the current host even if - behind proxy setups, this middleware can be used for applications which - access the WSGI environment directly. - - If you have more than one proxy server in front of your app, set - `num_proxies` accordingly. + def __init__( + self, app, num_proxies=None, + x_for=1, x_proto=0, x_host=0, x_port=0, x_prefix=0 + ): + self.app = app + self.x_for = x_for + self.x_proto = x_proto + self.x_host = x_host + self.x_port = x_port + self.x_prefix = x_prefix + self.num_proxies = num_proxies - Do not use this middleware in non-proxy setups for security reasons. + @property + def num_proxies(self): + """The number of proxies setting ``X-Forwarded-For`` in front + of the application. - The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in - the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and - `werkzeug.proxy_fix.orig_http_host`. + .. deprecated:: 0.15 + A separate number of trusted proxies is configured for each + header. ``num_proxies`` maps to ``x_for``. - :param app: the WSGI application - :param num_proxies: the number of proxy servers in front of the app. - """ + :internal: + """ + warnings.warn(DeprecationWarning( + "num_proxies is deprecated. Use x_for instead.")) + return self.x_for - def __init__(self, app, num_proxies=1): - self.app = app - self.num_proxies = num_proxies + @num_proxies.setter + def num_proxies(self, value): + if value is not None: + warnings.warn(DeprecationWarning( + 'num_proxies is deprecated. Use x_for instead.')) + self.x_for = value def get_remote_addr(self, forwarded_for): - """Selects the new remote addr from the given list of ips in - X-Forwarded-For. By default it picks the one that the `num_proxies` - proxy server provides. Before 0.9 it would always pick the first. + """Get the real ``remote_addr`` by looking backwards ``x_for`` + number of values in the ``X-Forwarded-For`` header. + + :param forwarded_for: List of values parsed from the + ``X-Forwarded-For`` header. + :return: The real ``remote_addr``, or ``None`` if there were not + at least ``x_for`` values. + + .. deprecated:: 0.15 + This is handled internally for each header. + + .. versionchanged:: 0.9 + Use ``num_proxies`` instead of always picking the first + value. .. versionadded:: 0.8 """ - if len(forwarded_for) >= self.num_proxies: - return forwarded_for[-self.num_proxies] + warnings.warn(DeprecationWarning("get_remote_addr is deprecated.")) + return self._get_trusted_comma(self.x_for, ','.join(forwarded_for)) + + def _get_trusted_comma(self, trusted, value): + """Get the real value from a comma-separated header based on the + configured number of trusted proxies. + + :param trusted: Number of values to trust in the header. + :param value: Header value to parse. + :return: The real value, or ``None`` if there are fewer values + than the number of trusted proxies. + + .. versionadded:: 0.15 + """ + if not (trusted and value): + return + values = [x.strip() for x in value.split(',')] + if len(values) >= trusted: + return values[-trusted] def __call__(self, environ, start_response): - getter = environ.get - forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '').split(',') - forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') - forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') - forwarded_port = getter('HTTP_X_FORWARDED_PORT', '') - forwarded_prefix = getter('HTTP_X_FORWARDED_PREFIX', '') + """Modify the WSGI environ based on the various ``Forwarded`` + headers before calling the wrapped application. Store the + original environ values in ``werkzeug.proxy_fix.orig_{key}``. + """ + environ_get = environ.get + orig_remote_addr = environ_get('REMOTE_ADDR') + orig_wsgi_url_scheme = environ_get('wsgi.url_scheme') + orig_http_host = environ_get('HTTP_HOST') environ.update({ - 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), - 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), - 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST'), - 'werkzeug.proxy_fix.orig_server_port': getter('SERVER_PORT'), - 'werkzeug.proxy_fix.orig_script_name': getter('SCRIPT_NAME'), + 'werkzeug.proxy_fix.orig': { + 'REMOTE_ADDR': orig_remote_addr, + 'wsgi.url_scheme': orig_wsgi_url_scheme, + 'HTTP_HOST': orig_http_host, + 'SERVER_NAME': environ_get('SERVER_NAME'), + 'SERVER_PORT': environ_get('SERVER_PORT'), + 'SCRIPT_NAME': environ_get('SCRIPT_NAME'), + }, + # todo: remove deprecated keys + 'werkzeug.proxy_fix.orig_remote_addr': orig_remote_addr, + 'werkzeug.proxy_fix.orig_wsgi_url_scheme': orig_wsgi_url_scheme, + 'werkzeug.proxy_fix.orig_http_host': orig_http_host, }) - forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] - forwarded_proto = [x for x in [x.strip() for x in forwarded_proto] if x] - remote_addr = self.get_remote_addr(forwarded_for) - if remote_addr is not None: - environ['REMOTE_ADDR'] = remote_addr - if forwarded_host: - environ['HTTP_HOST'] = forwarded_host - if forwarded_port: - if environ.get('HTTP_HOST'): - parts = environ['HTTP_HOST'].split(':', 1) - if len(parts) == 2: - environ['HTTP_HOST'] = parts[0] + ':' + forwarded_port - else: - environ['HTTP_HOST'] += ':' + forwarded_port - else: - environ['SERVER_PORT'] = forwarded_port - if forwarded_proto: - environ['wsgi.url_scheme'] = forwarded_proto[0] - if forwarded_prefix: - environ['SCRIPT_NAME'] = forwarded_prefix + + x_for = self._get_trusted_comma( + self.x_for, environ_get('HTTP_X_FORWARDED_FOR')) + if x_for: + environ['REMOTE_ADDR'] = x_for + + x_proto = self._get_trusted_comma( + self.x_proto, environ_get('HTTP_X_FORWARDED_PROTO')) + if x_proto: + environ['wsgi.url_scheme'] = x_proto + + x_host = self._get_trusted_comma( + self.x_host, environ_get('HTTP_X_FORWARDED_HOST')) + if x_host: + environ['HTTP_HOST'] = x_host + parts = x_host.split(':', 1) + environ['SERVER_NAME'] = parts[0] + if len(parts) == 2: + environ['SERVER_PORT'] = parts[1] + + x_port = self._get_trusted_comma( + self.x_port, environ_get('HTTP_X_FORWARDED_PORT')) + if x_port: + host = environ.get('HTTP_HOST') + if host: + parts = host.split(':', 1) + host = parts[0] if len(parts) == 2 else host + environ['HTTP_HOST'] = '%s:%s' % (host, x_port) + environ['SERVER_PORT'] = x_port + + x_prefix = self._get_trusted_comma( + self.x_for, environ_get('HTTP_X_FORWARDED_PREFIX')) + if x_prefix: + environ['SCRIPT_NAME'] = x_prefix + return self.app(environ, start_response) diff --git a/werkzeug/routing.py b/werkzeug/routing.py index ea70be2c5..e123d1c4e 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -1435,8 +1435,9 @@ class Map(object): `encoding_errors` and `host_matching` was added. """ + #: A dict of default converters to use. + #: #: .. versionadded:: 0.6 - #: a dict of default converters to be used. default_converters = ImmutableDict(DEFAULT_CONVERTERS) def __init__(self, rules=None, default_subdomain='', charset='utf-8', diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index ec7bb0a9a..1f58c18b9 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -143,17 +143,20 @@ def _normalize(hostname): def get_host(environ, trusted_hosts=None): - """Return the real host for the given WSGI environment. This first checks - the normal `Host` header, and if it's not present, then `SERVER_NAME` - and `SERVER_PORT` environment variables. - - Optionally it verifies that the host is in a list of trusted hosts. - If the host is not in there it will raise a - :exc:`~werkzeug.exceptions.SecurityError`. - - :param environ: the WSGI environment to get the host of. - :param trusted_hosts: a list of trusted hosts, see :func:`host_is_trusted` - for more information. + """Return the host for the given WSGI environment. This first checks + the ``Host`` header. If it's not present, then ``SERVER_NAME`` and + ``SERVER_PORT`` are used. The host will only contain the port if it + is different than the standard port for the protocol. + + Optionally, verify that the host is trusted using + :func:`host_is_trusted` and raise a + :exc:`~werkzeug.exceptions.SecurityError` if it is not. + + :param environ: The WSGI environment to get the host from. + :param trusted_hosts: A list of trusted hosts. + :return: Host, with port if necessary. + :raise ~werkzeug.exceptions.SecurityError: If the host is not + trusted. """ if 'HTTP_HOST' in environ: rv = environ['HTTP_HOST'] From ad8a3889f790dfc785f11483f40f8c3a41761937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rn=20Lode?= Date: Thu, 26 Mar 2015 01:20:15 +0100 Subject: [PATCH 066/280] allow relative location header --- tests/test_wrappers.py | 7 ++++++- werkzeug/wrappers.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 3640d704c..34567f6da 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1147,7 +1147,7 @@ class MyResponse(wrappers.Response): def test_location_header_autocorrect(): - env = create_environ() + env = create_environ('/a/b/c') class MyResponse(wrappers.Response): autocorrect_location_header = False @@ -1159,6 +1159,11 @@ class MyResponse(wrappers.Response): resp.headers['Location'] = '/test' assert resp.get_wsgi_headers(env)['Location'] == 'http://localhost/test' + resp = wrappers.Response('Hello World!') + resp.headers['Location'] = '../test' + assert resp.get_wsgi_headers(env)['Location'] == 'http://localhost/a/test' + + def test_204_and_1XX_response_has_no_content_length(): response = wrappers.Response(status=204) diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index f0aa78669..45e609386 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -1235,7 +1235,7 @@ def get_wsgi_headers(self, environ): location = iri_to_uri(location, safe_conversion=True) if self.autocorrect_location_header: - current_url = get_current_url(environ, root_only=True) + current_url = get_current_url(environ) if isinstance(current_url, text_type): current_url = iri_to_uri(current_url) location = url_join(current_url, location) From 8f8ced0086591fa2c95a367374797c4c62375eb9 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 28 May 2018 12:07:13 -0700 Subject: [PATCH 067/280] exclude current querystring from location parametrize location autocorrect test --- CHANGES.rst | 6 ++++++ tests/test_wrappers.py | 26 +++++++++++--------------- werkzeug/wrappers.py | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c4e1836f1..e0f19aa76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -52,8 +52,13 @@ Unreleased - The filenames generated by :class:`~contrib.profiler.ProfilerMiddleware` can be customized. (`#1283`_) +- ``Location`` autocorrection in :func:`Response.get_wsgi_headers() + ` is relative to the current + path rather than the root path. (`#693`_, `#718`_, `#1315`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 +.. _`#693`: https://github.com/pallets/werkzeug/pull/693 +.. _`#718`: https://github.com/pallets/werkzeug/pull/718 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 @@ -71,6 +76,7 @@ Unreleased .. _`#1308`: https://github.com/pallets/werkzeug/pull/1308 .. _`#1312`: https://github.com/pallets/werkzeug/pull/1312 .. _`#1314`: https://github.com/pallets/werkzeug/pull/1314 +.. _`#1315`: https://github.com/pallets/werkzeug/pull/1315 Version 0.14.1 diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 34567f6da..ca3cbc648 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1146,23 +1146,19 @@ class MyResponse(wrappers.Response): assert 'Content-Length' not in resp.get_wsgi_headers({}) -def test_location_header_autocorrect(): +@pytest.mark.parametrize(('auto', 'location', 'expect'), ( + (False, '/test', '/test'), + (True, '/test', 'http://localhost/test'), + (True, 'test', 'http://localhost/a/b/test'), + (True, './test', 'http://localhost/a/b/test'), + (True, '../test', 'http://localhost/a/test'), +)) +def test_location_header_autocorrect(monkeypatch, auto, location, expect): + monkeypatch.setattr(wrappers.Response, 'autocorrect_location_header', auto) env = create_environ('/a/b/c') - - class MyResponse(wrappers.Response): - autocorrect_location_header = False - resp = MyResponse('Hello World!') - resp.headers['Location'] = '/test' - assert resp.get_wsgi_headers(env)['Location'] == '/test' - resp = wrappers.Response('Hello World!') - resp.headers['Location'] = '/test' - assert resp.get_wsgi_headers(env)['Location'] == 'http://localhost/test' - - resp = wrappers.Response('Hello World!') - resp.headers['Location'] = '../test' - assert resp.get_wsgi_headers(env)['Location'] == 'http://localhost/a/test' - + resp.headers['Location'] = location + assert resp.get_wsgi_headers(env)['Location'] == expect def test_204_and_1XX_response_has_no_content_length(): diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 45e609386..093df36a2 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -1235,7 +1235,7 @@ def get_wsgi_headers(self, environ): location = iri_to_uri(location, safe_conversion=True) if self.autocorrect_location_header: - current_url = get_current_url(environ) + current_url = get_current_url(environ, strip_querystring=True) if isinstance(current_url, text_type): current_url = iri_to_uri(current_url) location = url_join(current_url, location) From 492b315d95b8ebb598eb0651e63dc540d77eb635 Mon Sep 17 00:00:00 2001 From: Zhaogui Xu Date: Mon, 8 Jun 2015 03:52:00 -0400 Subject: [PATCH 068/280] path_info defaults to / when binding map --- CHANGES.rst | 5 +++++ tests/test_routing.py | 6 ++++++ werkzeug/routing.py | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e0f19aa76..1d88a3413 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -55,11 +55,15 @@ Unreleased - ``Location`` autocorrection in :func:`Response.get_wsgi_headers() ` is relative to the current path rather than the root path. (`#693`_, `#718`_, `#1315`_) +- ``path_info`` defaults to ``'/'`` for + :meth:`Map.bind() `. (`#740`_, `#768`_, `#1316`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#693`: https://github.com/pallets/werkzeug/pull/693 .. _`#718`: https://github.com/pallets/werkzeug/pull/718 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 +.. _`#740`: https://github.com/pallets/werkzeug/issues/740 +.. _`#768`: https://github.com/pallets/werkzeug/pull/768 .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 @@ -77,6 +81,7 @@ Unreleased .. _`#1312`: https://github.com/pallets/werkzeug/pull/1312 .. _`#1314`: https://github.com/pallets/werkzeug/pull/1314 .. _`#1315`: https://github.com/pallets/werkzeug/pull/1315 +.. _`#1316`: https://github.com/pallets/werkzeug/pull/1316 Version 0.14.1 diff --git a/tests/test_routing.py b/tests/test_routing.py index c80f806bd..f6b9ebe1d 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -879,6 +879,12 @@ def test_empty_path_info(): assert excinfo.value.new_url == "http://example.com/" +def test_both_bind_and_match_path_info_are_none(): + m = r.Map([r.Rule(u'/', endpoint='index')]) + ma = m.bind('example.org') + strict_eq(ma.match(), ('index', {})) + + def test_map_repr(): m = r.Map([ r.Rule(u'/wat', endpoint='enter'), diff --git a/werkzeug/routing.py b/werkzeug/routing.py index e123d1c4e..1b044c47b 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -1534,6 +1534,9 @@ def bind(self, server_name, script_name=None, subdomain=None, .. versionadded:: 0.8 `query_args` can now also be a string. + + .. versionchanged:: 0.15 + ``path_info`` defaults to ``'/'`` if ``None``. """ server_name = server_name.lower() if self.host_matching: @@ -1544,6 +1547,8 @@ def bind(self, server_name, script_name=None, subdomain=None, subdomain = self.default_subdomain if script_name is None: script_name = '/' + if path_info is None: + path_info = '/' try: server_name = _encode_idna(server_name) except UnicodeError: From 56b7f115a25a8e99053a53b4c5ed9c94ccd94dda Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Tue, 29 May 2018 11:19:05 -0400 Subject: [PATCH 069/280] Keep terminal input when triggering code reload during breakpoint --- werkzeug/_reloader.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/werkzeug/_reloader.py b/werkzeug/_reloader.py index 0d23dba4e..0258cb20c 100644 --- a/werkzeug/_reloader.py +++ b/werkzeug/_reloader.py @@ -259,6 +259,27 @@ def run(self): reloader_loops['auto'] = reloader_loops['watchdog'] +try: + import termios +except ImportError: + termios = None + + +def ensure_echo_on(): + if termios is None: + return + + # tcgetattr will fail if stdin isn't a tty (e.g. test_serving.py test cases) + if not sys.stdin.isatty(): + return + + file_descriptor = sys.stdin.fileno() + attributes = termios.tcgetattr(file_descriptor) + if not attributes[3] & termios.ECHO: + attributes[3] |= termios.ECHO + termios.tcsetattr(file_descriptor, termios.TCSANOW, attributes) + + def run_with_reloader(main_func, extra_files=None, interval=1, reloader_type='auto'): """Run the given function in an independent python interpreter.""" @@ -267,6 +288,7 @@ def run_with_reloader(main_func, extra_files=None, interval=1, signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) try: if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + ensure_echo_on() t = threading.Thread(target=main_func, args=()) t.setDaemon(True) t.start() From e19a175b849a3aae25d447bfa9dbd75e5ddc4791 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 29 May 2018 12:48:12 -0700 Subject: [PATCH 070/280] refactor, add changelog --- CHANGES.rst | 3 +++ werkzeug/_reloader.py | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1d88a3413..75a22ac29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -57,6 +57,8 @@ Unreleased path rather than the root path. (`#693`_, `#718`_, `#1315`_) - ``path_info`` defaults to ``'/'`` for :meth:`Map.bind() `. (`#740`_, `#768`_, `#1316`_) +- Triggering a reload while using a tool such as PDB no longer hides + input. (`#1318`_) .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#693`: https://github.com/pallets/werkzeug/pull/693 @@ -82,6 +84,7 @@ Unreleased .. _`#1314`: https://github.com/pallets/werkzeug/pull/1314 .. _`#1315`: https://github.com/pallets/werkzeug/pull/1315 .. _`#1316`: https://github.com/pallets/werkzeug/pull/1316 +.. _`#1318`: https://github.com/pallets/werkzeug/pull/1318 Version 0.14.1 diff --git a/werkzeug/_reloader.py b/werkzeug/_reloader.py index 0258cb20c..e8d7643d7 100644 --- a/werkzeug/_reloader.py +++ b/werkzeug/_reloader.py @@ -259,25 +259,20 @@ def run(self): reloader_loops['auto'] = reloader_loops['watchdog'] -try: - import termios -except ImportError: - termios = None - - def ensure_echo_on(): - if termios is None: - return - - # tcgetattr will fail if stdin isn't a tty (e.g. test_serving.py test cases) + """Ensure that echo mode is enabled. Some tools such as PDB disable + it which causes usability issues after reload.""" + # tcgetattr will fail if stdin isn't a tty if not sys.stdin.isatty(): return - - file_descriptor = sys.stdin.fileno() - attributes = termios.tcgetattr(file_descriptor) + try: + import termios + except ImportError: + return + attributes = termios.tcgetattr(sys.stdin) if not attributes[3] & termios.ECHO: attributes[3] |= termios.ECHO - termios.tcsetattr(file_descriptor, termios.TCSANOW, attributes) + termios.tcsetattr(sys.stdin, termios.TCSANOW, attributes) def run_with_reloader(main_func, extra_files=None, interval=1, From 5dded5e084fdf0ca7fb9c33b8539f2101e0ad807 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 29 May 2018 15:39:40 -0700 Subject: [PATCH 071/280] add unix socket capability to dev_server fixture use requests_unixsocket --- CHANGES.rst | 4 ++++ docs/serving.rst | 15 +++++++-------- tests/conftest.py | 43 +++++++++++++++++++++++-------------------- tests/test_serving.py | 34 +++++++++++++--------------------- tox.ini | 1 + werkzeug/serving.py | 17 +++++++++++------ 6 files changed, 59 insertions(+), 55 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 75a22ac29..7d74773d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -59,13 +59,17 @@ Unreleased :meth:`Map.bind() `. (`#740`_, `#768`_, `#1316`_) - Triggering a reload while using a tool such as PDB no longer hides input. (`#1318`_) +- The dev server can bind to a Unix socket by passing a hostname like + ``unix://app.socket``. (`#209`_, `#1019`_) +.. _`#209`: https://github.com/pallets/werkzeug/pull/209 .. _`#609`: https://github.com/pallets/werkzeug/pull/609 .. _`#693`: https://github.com/pallets/werkzeug/pull/693 .. _`#718`: https://github.com/pallets/werkzeug/pull/718 .. _`#724`: https://github.com/pallets/werkzeug/pull/724 .. _`#740`: https://github.com/pallets/werkzeug/issues/740 .. _`#768`: https://github.com/pallets/werkzeug/pull/768 +.. _`#1019`: https://github.com/pallets/werkzeug/issues/1019 .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 .. _`#1233`: https://github.com/pallets/werkzeug/pull/1233 diff --git a/docs/serving.rst b/docs/serving.rst index 61da22ca3..2dbc56c2b 100644 --- a/docs/serving.rst +++ b/docs/serving.rst @@ -76,7 +76,7 @@ Colored Logging --------------- Werkzeug is able to color the output of request logs when ran from a terminal, just install the `termcolor `_ package. Windows users need to install `colorama -`_ in addition to termcolor for this to work. +`_ in addition to termcolor for this to work. Virtual Hosts ------------- @@ -226,14 +226,13 @@ security reasons. This feature requires the pyOpenSSL library to be installed. + Unix Sockets ------------ -The builtin server supports Unix socket binding. This means that it is -possible to start the server in a way that it will listen to a unix -socket instead of a TCP socket. -In order to use a unix socket the `hostname` parameter of :func:`run_simple` -method must start with `'unix://'`:: +The dev server can bind to a Unix socket instead of a TCP socket. +:func:`run_simple` will bind to a Unix socket if the ``hostname`` +parameter starts with ``'unix://'``. :: - from werkzeug.serving import run_simple - run_simple('unix://example.sock', 0, app) + from werkzeug.serving import run_simple + run_simple('unix://example.sock', 0, app) diff --git a/tests/conftest.py b/tests/conftest.py index ee37c3956..247f69d63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,22 +6,21 @@ :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement, print_function +from __future__ import print_function, with_statement +from itertools import count import os import signal import sys import textwrap import time -import requests import pytest from werkzeug import serving -from werkzeug.utils import cached_property from werkzeug._compat import to_bytes -from itertools import count - +from werkzeug.urls import url_quote +from werkzeug.utils import cached_property try: __import__('pytest_xprocess') @@ -61,8 +60,7 @@ def _dev_server(): sys.path.insert(0, sys.argv[1]) import testsuite_app app = _get_pid_middleware(testsuite_app.app) - serving.run_simple(hostname='localhost', application=app, - **testsuite_app.kwargs) + serving.run_simple(application=app, **testsuite_app.kwargs) class _ServerInfo(object): @@ -83,11 +81,16 @@ def logfile(self): return self.xprocess.getinfo('dev_server').logpath.open() def request_pid(self): - for i in range(20): + if self.url.startswith('http+unix://'): + from requests_unixsocket import get as rget + else: + from requests import get as rget + + for i in range(10): time.sleep(0.1 * i) try: - self.last_pid = int(requests.get(self.url + '/_getpid', - verify=False).text) + response = rget(self.url + '/_getpid', verify=False) + self.last_pid = int(response.text) return self.last_pid except Exception as e: # urllib also raises socketerrors print(self.url) @@ -133,19 +136,19 @@ def run_dev_server(application): monkeypatch.delitem(sys.modules, 'testsuite_app', raising=False) monkeypatch.syspath_prepend(str(tmpdir)) import testsuite_app + hostname = testsuite_app.kwargs['hostname'] port = testsuite_app.kwargs['port'] + addr = '{}:{}'.format(hostname, port) - if testsuite_app.kwargs.get('ssl_context', None): - url_base = 'https://localhost:{0}'.format(port) + if hostname.startswith('unix://'): + addr = hostname.split('unix://', 1)[1] + requests_url = 'http+unix://' + url_quote(addr, safe='') + elif testsuite_app.kwargs.get('ssl_context', None): + requests_url = 'https://localhost:{0}'.format(port) else: - url_base = 'http://localhost:{0}'.format(port) + requests_url = 'http://localhost:{0}'.format(port) - info = _ServerInfo( - xprocess, - 'localhost:{0}'.format(port), - url_base, - port - ) + info = _ServerInfo(xprocess, addr, requests_url, port) def preparefunc(cwd): args = [sys.executable, __file__, str(tmpdir)] @@ -153,6 +156,7 @@ def preparefunc(cwd): xprocess.ensure('dev_server', preparefunc, restart=True) + @request.addfinalizer def teardown(): # Killing the process group that runs the server, not just the # parent process attached. xprocess is confused about Werkzeug's @@ -160,7 +164,6 @@ def teardown(): pid = info.request_pid() if pid: os.killpg(os.getpgid(pid), signal.SIGTERM) - request.addfinalizer(teardown) return info diff --git a/tests/test_serving.py b/tests/test_serving.py index 7ba21bd4d..c59315eb5 100644 --- a/tests/test_serving.py +++ b/tests/test_serving.py @@ -471,29 +471,21 @@ def app(environ, start_response): conn.close() -@pytest.mark.skipif( - not hasattr(socket, 'AF_UNIX'), reason='Only works on UNIX') +def can_test_unix_socket(): + if not hasattr(socket, 'AF_UNIX'): + return False + try: + import requests_unixsocket # noqa: F401 + except ImportError: + return False + return True + + +@pytest.mark.skipif(not can_test_unix_socket(), reason='Only works on UNIX') def test_unix_socket(tmpdir, dev_server): socket_f = str(tmpdir.join('socket')) dev_server(''' - def app(environ, start_response): - start_response('200 OK', [('Content-Type', 'text/html')]) - return [b'hello'] + app = None kwargs['hostname'] = {socket!r} '''.format(socket='unix://' + socket_f)) - - for i in reversed(range(10)): - try: - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(socket_f) - s.send(b'GET / HTTP/1.0\n\n\n') - data = s.recv(1024) - assert b'hello' in data - s.shutdown(1) - s.close() - except socket.error: - if i == 0: - raise - time.sleep(0.1) - else: - break + assert os.path.exists(socket_f) diff --git a/tox.ini b/tox.ini index 9a9e69a1b..2aa434524 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ deps = pytest-xprocess coverage requests + requests_unixsocket pyopenssl greenlet redis diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 307d03a2f..6fb43fb72 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -567,7 +567,8 @@ def is_ssl_error(error=None): def select_address_family(host, port): - """Returns AF_INET4 or AF_INET6 depending on where to connect to.""" + """Return ``AF_INET4``, ``AF_INET6``, or ``AF_UNIX`` depending on + the host and port.""" # disabled due to problems with current ipv6 implementations # and various operating systems. Probably this code also is # not supposed to work, but I can't come up with any other @@ -588,8 +589,8 @@ def select_address_family(host, port): def get_sockaddr(host, port, family): - """Returns a fully qualified socket address, that can properly used by - socket.bind""" + """Return a fully qualified socket address that can be passed to + :func:`socket.bind`.""" if family == socket.AF_UNIX: return host.split('://', 1)[1] try: @@ -764,9 +765,13 @@ def run_simple(hostname, port, application, use_reloader=False, through the `reloader_type` parameter. See :ref:`reloader` for more information. - :param hostname: The host for the application. eg: ``'localhost'``. - In order to use an unix socket instead of a TCP socket - ``hostname`` must start with ``'unix://'``. + .. versionchanged:: 0.15 + Bind to a Unix socket by passing a path that starts with + ``unix://`` as the ``hostname``. + + :param hostname: The host to bind to, for example ``'localhost'``. + If the value is a path that starts with ``unix://`` it will bind + to a Unix socket instead of a TCP socket.. :param port: The port for the server. eg: ``8080`` :param application: the WSGI application to execute :param use_reloader: should the server automatically restart the python From 977ba0ec7de798b8abe580dd064e0a23782219ec Mon Sep 17 00:00:00 2001 From: Roman Konoval Date: Fri, 2 Mar 2018 10:43:36 +0100 Subject: [PATCH 072/280] ClosingIterator closes iterable passed to it. This is a fix for https://github.com/pallets/werkzeug/issues/1259. According to [wsgi specification](https://www.python.org/dev/peps/pep-3333): > If the iterable returned by the application has a close() method, the > server or gateway must call that method upon completion of the current > request This commit adds test for this and fixes the issue. --- tests/test_wsgi.py | 50 ++++++++++++++++++++++++++++++++++++++++++++-- werkzeug/wsgi.py | 9 +++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 1e3c5ba84..bac63b2b0 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -19,12 +19,12 @@ from tests import strict_eq from werkzeug import wsgi from werkzeug._compat import BytesIO, NativeStringIO, StringIO, to_bytes, \ - to_native + to_native from werkzeug.exceptions import BadRequest, ClientDisconnected from werkzeug.test import Client, create_environ, run_wsgi_app from werkzeug.wrappers import BaseResponse from werkzeug.urls import url_parse -from werkzeug.wsgi import _RangeWrapper, wrap_file +from werkzeug.wsgi import _RangeWrapper, ClosingIterator, wrap_file def test_shareddatamiddleware_get_file_loader(): @@ -555,3 +555,49 @@ def app(request): # test query string rv = client.get('/bar/baz?a=a&b=b') assert rv.data.decode('ascii') == 'bar|localhost|/baz?a=a&b=b' + + +def test_closing_iterator(): + # calls close on the iterable that is wrapped + got_close = [] + + class App(object): + + def __init__(self, environ, start_response): + self.start = start_response + + def __iter__(self): + self.start('200 OK', [('Content-Type', 'text/plain')]) + yield 'some content' + + def close(self): + got_close.append(None) + + def wrap(callback=None): + def wrapped(environ, start_response): + if callback: + callbacks = [callback] + else: + callbacks = [] + return ClosingIterator(App(environ, start_response), *callbacks) + + return wrapped + + app_iter, status, headers = run_wsgi_app(wrap(), + create_environ(), + buffered=True) + + strict_eq(''.join(app_iter), 'some content') + assert len(got_close) == 1 + + # calls close on the iterable that is wrapped and callbacks + got_additional = [] + + def additional(): + got_additional.append(None) + got_close = [] + app_iter, status, headers = run_wsgi_app(wrap(additional), + create_environ(), + buffered=True) + assert len(got_close) == 1 + assert len(got_additional) == 1 diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 1f58c18b9..991482a0e 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -841,9 +841,10 @@ def __call__(self, environ, start_response): class ClosingIterator(object): """The WSGI specification requires that all middlewares and gateways - respect the `close` callback of an iterator. Because it is useful to add - another close action to a returned iterator and adding a custom iterator - is a boring task this class can be used for that:: + respect the `close` callback of the iterable returned by the application. + Because it is useful to add another close action to a returned iterable + and adding a custom iterable is a boring task this class can be used for + that:: return ClosingIterator(app(environ, start_response), [cleanup_session, cleanup_locals]) @@ -869,7 +870,7 @@ def __init__(self, iterable, callbacks=None): callbacks = [callbacks] else: callbacks = list(callbacks) - iterable_close = getattr(iterator, 'close', None) + iterable_close = getattr(iterable, 'close', None) if iterable_close: callbacks.insert(0, iterable_close) self._callbacks = callbacks From f048c0bd1f1aa2ac111fe8f6fe05c1ec436e9739 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 29 May 2018 16:57:01 -0700 Subject: [PATCH 073/280] refactor test, add changelog --- CHANGES.rst | 6 ++++++ tests/test_wsgi.py | 47 +++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7d74773d3..33dc41163 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -61,6 +61,10 @@ Unreleased input. (`#1318`_) - The dev server can bind to a Unix socket by passing a hostname like ``unix://app.socket``. (`#209`_, `#1019`_) +- :class:`~wsgi.ClosingIterator` calls ``close`` on the wrapped + *iterable*, not the internal iterator. This doesn't affect objects + where ``__iter__`` returned ``self``. For other objects, the method + was not called before. (`#1259`_, `#1260`_) .. _`#209`: https://github.com/pallets/werkzeug/pull/209 .. _`#609`: https://github.com/pallets/werkzeug/pull/609 @@ -77,6 +81,8 @@ Unreleased .. _`#1245`: https://github.com/pallets/werkzeug/pull/1245 .. _`#1252`: https://github.com/pallets/werkzeug/pull/1252 .. _`#1255`: https://github.com/pallets/werkzeug/pull/1255 +.. _`#1259`: https://github.com/pallets/werkzeug/pull/1259 +.. _`#1260`: https://github.com/pallets/werkzeug/pull/1260 .. _`#1281`: https://github.com/pallets/werkzeug/pull/1281 .. _`#1282`: https://github.com/pallets/werkzeug/pull/1282 .. _`#1283`: https://github.com/pallets/werkzeug/issues/1283 diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index bac63b2b0..81b88c81a 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -558,46 +558,33 @@ def app(request): def test_closing_iterator(): - # calls close on the iterable that is wrapped - got_close = [] - - class App(object): + class Namespace(object): + got_close = False + got_additional = False + class Response(object): def __init__(self, environ, start_response): self.start = start_response + # Return a generator instead of making the object its own + # iterator. This ensures that ClosingIterator calls close on + # the iterable (the object), not the iterator. def __iter__(self): self.start('200 OK', [('Content-Type', 'text/plain')]) yield 'some content' def close(self): - got_close.append(None) - - def wrap(callback=None): - def wrapped(environ, start_response): - if callback: - callbacks = [callback] - else: - callbacks = [] - return ClosingIterator(App(environ, start_response), *callbacks) - - return wrapped + Namespace.got_close = True - app_iter, status, headers = run_wsgi_app(wrap(), - create_environ(), - buffered=True) + def additional(): + Namespace.got_additional = True - strict_eq(''.join(app_iter), 'some content') - assert len(got_close) == 1 + def app(environ, start_response): + return ClosingIterator(Response(environ, start_response), additional) - # calls close on the iterable that is wrapped and callbacks - got_additional = [] + app_iter, status, headers = run_wsgi_app( + app, create_environ(), buffered=True) - def additional(): - got_additional.append(None) - got_close = [] - app_iter, status, headers = run_wsgi_app(wrap(additional), - create_environ(), - buffered=True) - assert len(got_close) == 1 - assert len(got_additional) == 1 + assert ''.join(app_iter) == 'some content' + assert Namespace.got_close + assert Namespace.got_additional From 5e2b8cd850c235bd06a83cdf7f40e343a30acb81 Mon Sep 17 00:00:00 2001 From: dobesv Date: Tue, 3 Nov 2015 18:04:30 -0800 Subject: [PATCH 074/280] Add support for WWW-Authenticate header to Unauthorized In the HTTP spec, the Unauthorized response is required to provide at least one WWW-Authenticate challenge. This makes it easier to comply with that spec with Werkzeug exceptions. --- tests/test_exceptions.py | 7 ++++++- werkzeug/exceptions.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index f9d78217b..862656552 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -30,7 +30,7 @@ def test_proxy_exception(): @pytest.mark.parametrize('test', [ (exceptions.BadRequest, 400), - (exceptions.Unauthorized, 401), + (exceptions.Unauthorized, 401, ['Basic "test realm"']), (exceptions.Forbidden, 403), (exceptions.NotFound, 404), (exceptions.MethodNotAllowed, 405, ['GET', 'HEAD']), @@ -90,3 +90,8 @@ def test_special_exceptions(): h = dict(exc.get_headers({})) assert h['Allow'] == 'GET, HEAD, POST' assert 'The method is not allowed' in exc.get_description() + + exc = exceptions.Unauthorized(['Basic realm1', 'Digest realm=...']) + h = dict(exc.get_headers({})) + assert h['WWW-Authenticate'] == 'Basic realm1, Digest realm=...' + assert 'authorized' in exc.get_description() diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index 1d68185a9..0b0238a5b 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -217,6 +217,10 @@ class Unauthorized(HTTPException): Raise if the user is not authorized. Also used if you want to use HTTP basic auth. + + The first argument for this exception should be a list of WWW-Authenticate + challenges. Strictly speaking the response would be invalid if you don't + provide at least one challenge. """ code = 401 description = ( @@ -225,6 +229,17 @@ class Unauthorized(HTTPException): 'a bad password), or your browser doesn\'t understand how to supply ' 'the credentials required.' ) + + def __init__(self, challenges=None, description=None): + """Takes an optional list of WWW-Authenticate challenges.""" + HTTPException.__init__(self, description) + self.challenges = challenges + + def get_headers(self, environ): + headers = HTTPException.get_headers(self, environ) + if self.challenges: + headers.append(('WWW-Authenticate', ', '.join(self.challenges))) + return headers class Forbidden(HTTPException): From 8ed5b3f9a285eca756c3ab33f8c370a88eab3842 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 31 May 2018 08:57:34 -0700 Subject: [PATCH 075/280] handle WWWAuthenticate object for header allow single value or list of headers update docs and test, add changelog --- CHANGES.rst | 6 ++++++ docs/exceptions.rst | 6 ++---- tests/test_exceptions.py | 21 ++++++++++++++----- werkzeug/exceptions.py | 45 +++++++++++++++++++++++++--------------- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 33dc41163..37224c60f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -65,6 +65,10 @@ Unreleased *iterable*, not the internal iterator. This doesn't affect objects where ``__iter__`` returned ``self``. For other objects, the method was not called before. (`#1259`_, `#1260`_) +- :class:`~exceptions.Unauthorized` takes a ``www_authenticate`` + parameter to set the ``WWW-Authenticate`` header for the response, + which is technically required for a valid 401 response. (`#772`_, + `#795`_) .. _`#209`: https://github.com/pallets/werkzeug/pull/209 .. _`#609`: https://github.com/pallets/werkzeug/pull/609 @@ -73,6 +77,8 @@ Unreleased .. _`#724`: https://github.com/pallets/werkzeug/pull/724 .. _`#740`: https://github.com/pallets/werkzeug/issues/740 .. _`#768`: https://github.com/pallets/werkzeug/pull/768 +.. _`#772`: https://github.com/pallets/werkzeug/pull/772 +.. _`#795`: https://github.com/pallets/werkzeug/pull/795 .. _`#1019`: https://github.com/pallets/werkzeug/issues/1019 .. _`#1023`: https://github.com/pallets/werkzeug/issues/1023 .. _`#1231`: https://github.com/pallets/werkzeug/issues/1231 diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 6f978c4c7..0ee9669a4 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -140,8 +140,6 @@ methods. In any case you should have a look at the sourcecode of the exceptions module. You can override the default description in the constructor with the -`description` parameter (it's the first argument for all exceptions -except of the :exc:`MethodNotAllowed` which accepts a list of allowed methods -as first argument):: +``description`` parameter:: - raise BadRequest('Request failed because X was not present') + raise BadRequest(description='Request failed because X was not present') diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 862656552..5a00f1002 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -15,6 +15,7 @@ import pytest from werkzeug import exceptions +from werkzeug.datastructures import WWWAuthenticate from werkzeug.wrappers import Response from werkzeug._compat import text_type @@ -30,7 +31,7 @@ def test_proxy_exception(): @pytest.mark.parametrize('test', [ (exceptions.BadRequest, 400), - (exceptions.Unauthorized, 401, ['Basic "test realm"']), + (exceptions.Unauthorized, 401, 'Basic "test realm"'), (exceptions.Forbidden, 403), (exceptions.NotFound, 404), (exceptions.MethodNotAllowed, 405, ['GET', 'HEAD']), @@ -85,13 +86,23 @@ def test_exception_repr(): assert repr(exc) == "" -def test_special_exceptions(): +def test_method_not_allowed_methods(): exc = exceptions.MethodNotAllowed(['GET', 'HEAD', 'POST']) h = dict(exc.get_headers({})) assert h['Allow'] == 'GET, HEAD, POST' assert 'The method is not allowed' in exc.get_description() - exc = exceptions.Unauthorized(['Basic realm1', 'Digest realm=...']) + +def test_unauthorized_www_authenticate(): + basic = WWWAuthenticate() + basic.set_basic("test") + digest = WWWAuthenticate() + digest.set_digest("test", "test") + + exc = exceptions.Unauthorized(www_authenticate=basic) + h = dict(exc.get_headers({})) + assert h['WWW-Authenticate'] == str(basic) + + exc = exceptions.Unauthorized(www_authenticate=[digest, basic]) h = dict(exc.get_headers({})) - assert h['WWW-Authenticate'] == 'Basic realm1, Digest realm=...' - assert 'authorized' in exc.get_description() + assert h['WWW-Authenticate'] == ', '.join((str(digest), str(basic))) diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index 0b0238a5b..f934de388 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -213,32 +213,43 @@ class BadHost(BadRequest): class Unauthorized(HTTPException): - """*401* `Unauthorized` + """*401* ``Unauthorized`` - Raise if the user is not authorized. Also used if you want to use HTTP - basic auth. + Raise if the user is not authorized to access a resource. - The first argument for this exception should be a list of WWW-Authenticate - challenges. Strictly speaking the response would be invalid if you don't - provide at least one challenge. + The ``www_authenticate`` argument should be used to set the + ``WWW-Authenticate`` header. This is used for HTTP basic auth and + other schemes. Use :class:`~werkzeug.datastructures.WWWAuthenticate` + to create correctly formatted values. Strictly speaking a 401 + response is invalid if it doesn't provide at least one value for + this header, although real clients typically don't care. + + :param www-authenticate: A single value, or list of values, for the + WWW-Authenticate header. + :param description: Override the default message used for the body + of the response. """ code = 401 description = ( - 'The server could not verify that you are authorized to access ' - 'the URL requested. You either supplied the wrong credentials (e.g. ' - 'a bad password), or your browser doesn\'t understand how to supply ' - 'the credentials required.' + 'The server could not verify that you are authorized to access' + ' the URL requested. You either supplied the wrong credentials' + " (e.g. a bad password), or your browser doesn't understand" + ' how to supply the credentials required.' ) - - def __init__(self, challenges=None, description=None): - """Takes an optional list of WWW-Authenticate challenges.""" + + def __init__(self, www_authenticate=None, description=None): HTTPException.__init__(self, description) - self.challenges = challenges + if not isinstance(www_authenticate, (tuple, list)): + www_authenticate = (www_authenticate,) + self.www_authenticate = www_authenticate - def get_headers(self, environ): + def get_headers(self, environ=None): headers = HTTPException.get_headers(self, environ) - if self.challenges: - headers.append(('WWW-Authenticate', ', '.join(self.challenges))) + if self.www_authenticate: + headers.append(( + 'WWW-Authenticate', + ', '.join([str(x) for x in self.www_authenticate]) + )) return headers From 7ddc8b850535f4e9af87f6a7298935f08a9754d0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 7 Jun 2018 11:06:58 +0800 Subject: [PATCH 076/280] Fix key encoding in windows reloader. ref: https://github.com/pallets/werkzeug/pull/1320 --- werkzeug/_reloader.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/werkzeug/_reloader.py b/werkzeug/_reloader.py index e8d7643d7..1dcef963a 100644 --- a/werkzeug/_reloader.py +++ b/werkzeug/_reloader.py @@ -117,17 +117,22 @@ def restart_with_reloader(self): while 1: _log('info', ' * Restarting with %s' % self.name) args = _get_args_for_reloading() - new_environ = os.environ.copy() - new_environ['WERKZEUG_RUN_MAIN'] = 'true' # a weird bug on windows. sometimes unicode strings end up in the # environment and subprocess.call does not like this, encode them # to latin1 and continue. if os.name == 'nt' and PY2: - for key, value in iteritems(new_environ): + new_environ = {} + for key, value in iteritems(os.environ): + if isinstance(key, text_type): + key = key.encode('iso-8859-1') if isinstance(value, text_type): - new_environ[key] = value.encode('iso-8859-1') + value = value.encode('iso-8859-1') + new_environ[key] = value + else: + new_environ = os.environ.copy() + new_environ['WERKZEUG_RUN_MAIN'] = 'true' exit_code = subprocess.call(args, env=new_environ, close_fds=False) if exit_code != 3: From a8334a8f80b7c75e851f3dc3f4403d32127a7e55 Mon Sep 17 00:00:00 2001 From: Jeffrey Warren Date: Wed, 11 Jul 2018 16:49:56 -0700 Subject: [PATCH 077/280] Add support for Chrome on iOS user agent --- werkzeug/useragents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/useragents.py b/werkzeug/useragents.py index 4d7d41f1c..8aebb278f 100644 --- a/werkzeug/useragents.py +++ b/werkzeug/useragents.py @@ -49,7 +49,7 @@ class UserAgentParser(object): (r'aol|america\s+online\s+browser', 'aol'), ('opera', 'opera'), ('edge', 'edge'), - ('chrome', 'chrome'), + ('chrome|crios', 'chrome'), ('seamonkey', 'seamonkey'), ('firefox|firebird|phoenix|iceweasel', 'firefox'), ('galeon', 'galeon'), From 4d72f8dd4166522338f3e1c47c7639e34156747e Mon Sep 17 00:00:00 2001 From: pgjones Date: Sun, 29 Jul 2018 11:44:27 +0100 Subject: [PATCH 078/280] Raise an error if query string has multiple definitions Previously to this commit if when creating an environ (i.e. for testing) a query string is defined in the path and as an argument the path-query-string would be ignored. This results in the path not matching any routes and hence an unexpected result. This commit fixes this issue by raising a ValueError if someone should do this. Raising ValueError is considered the correct approach as it is unclear which individual query string is the user's intention or if they should be combined. --- tests/test_test.py | 5 +++++ werkzeug/test.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/tests/test_test.py b/tests/test_test.py index b78a19f00..19f651873 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -328,6 +328,11 @@ def test_create_environ(): strict_eq(create_environ('/foo', 'http://example.com/')['SCRIPT_NAME'], '') +def test_create_environ_query_string_error(): + with pytest.raises(ValueError): + create_environ('/foo?bar=baz', query_string={'a': 'b'}) + + def test_file_closing(): closed = [] diff --git a/werkzeug/test.py b/werkzeug/test.py index 86ec0244a..e81fd48d0 100644 --- a/werkzeug/test.py +++ b/werkzeug/test.py @@ -296,6 +296,8 @@ def __init__(self, path='/', base_url=None, query_string=None, environ_base=None, environ_overrides=None, charset='utf-8', mimetype=None): path_s = make_literal_wrapper(path) + if query_string is not None and path_s('?') in path: + raise ValueError('Query string is defined in the path and as an argument') if query_string is None and path_s('?') in path: path, query_string = path.split(path_s('?'), 1) self.charset = charset From 359830094f36b458f00a628c59eb8b28956e1a14 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 29 Jul 2018 07:08:43 -0700 Subject: [PATCH 079/280] update changelog --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 37224c60f..ecaa68d5b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -69,6 +69,8 @@ Unreleased parameter to set the ``WWW-Authenticate`` header for the response, which is technically required for a valid 401 response. (`#772`_, `#795`_) +- The test client raises a ``ValueError`` if a query string argument + would overwrite a query string in the path. (`#1338`_) .. _`#209`: https://github.com/pallets/werkzeug/pull/209 .. _`#609`: https://github.com/pallets/werkzeug/pull/609 @@ -101,6 +103,7 @@ Unreleased .. _`#1315`: https://github.com/pallets/werkzeug/pull/1315 .. _`#1316`: https://github.com/pallets/werkzeug/pull/1316 .. _`#1318`: https://github.com/pallets/werkzeug/pull/1318 +.. _`#1338`: https://github.com/pallets/werkzeug/pull/1338 Version 0.14.1 From baf20510cb1112b27dd659034980e295fb0b5563 Mon Sep 17 00:00:00 2001 From: Max Goodman Date: Fri, 3 Aug 2018 11:37:47 -0700 Subject: [PATCH 080/280] Replace shared global _empty_stream with separate instances If a test closed the _empty_stream, subsequent requests could reuse this closed stream, causing request.get_data() and other methods to fail. --- CHANGES.rst | 6 ++++++ werkzeug/_internal.py | 5 ++--- werkzeug/datastructures.py | 10 +++++----- werkzeug/test.py | 4 ++-- werkzeug/wsgi.py | 4 ++-- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ecaa68d5b..dfeb64fbd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -71,6 +71,11 @@ Unreleased `#795`_) - The test client raises a ``ValueError`` if a query string argument would overwrite a query string in the path. (`#1338`_) +- :class:`~test.EnvironBuilder`, :class:`~datastructures.FileStorage`, + and :func:`wsgi.get_input_stream` no longer share a global + ``_empty_stream`` instance. This improves test isolation by preventing + cases where closing the stream in one request would affect other usages. + (`#1340`_) .. _`#209`: https://github.com/pallets/werkzeug/pull/209 .. _`#609`: https://github.com/pallets/werkzeug/pull/609 @@ -104,6 +109,7 @@ Unreleased .. _`#1316`: https://github.com/pallets/werkzeug/pull/1316 .. _`#1318`: https://github.com/pallets/werkzeug/pull/1318 .. _`#1338`: https://github.com/pallets/werkzeug/pull/1338 +.. _`#1340`: https://github.com/pallets/werkzeug/pull/1340 Version 0.14.1 diff --git a/werkzeug/_internal.py b/werkzeug/_internal.py index 28bfd9fe4..5a4283cf7 100644 --- a/werkzeug/_internal.py +++ b/werkzeug/_internal.py @@ -15,12 +15,11 @@ from datetime import datetime, date from itertools import chain -from werkzeug._compat import iter_bytes, text_type, BytesIO, int_to_byte, \ - range_type, integer_types +from werkzeug._compat import iter_bytes, text_type, int_to_byte, range_type, \ + integer_types _logger = None -_empty_stream = BytesIO() _signature_cache = WeakKeyDictionary() _epoch_ord = date(1970, 1, 1).toordinal() _cookie_params = set((b'expires', b'path', b'comment', diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index 5634dc749..c1a1ac06e 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -15,10 +15,10 @@ from itertools import repeat from collections import Container, Iterable, MutableSet -from werkzeug._internal import _missing, _empty_stream -from werkzeug._compat import iterkeys, itervalues, iteritems, iterlists, \ - PY2, text_type, integer_types, string_types, make_literal_wrapper, \ - to_native +from werkzeug._internal import _missing +from werkzeug._compat import BytesIO, iterkeys, itervalues, iteritems, \ + iterlists, PY2, text_type, integer_types, string_types, \ + make_literal_wrapper, to_native from werkzeug.filesystem import get_filesystem_encoding @@ -2639,7 +2639,7 @@ def __init__(self, stream=None, filename=None, name=None, content_type=None, content_length=None, headers=None): self.name = name - self.stream = stream or _empty_stream + self.stream = stream or BytesIO() # if no filename is provided we can attempt to get the filename # from the stream object passed. There we have to be careful to diff --git a/werkzeug/test.py b/werkzeug/test.py index e81fd48d0..1e1301139 100644 --- a/werkzeug/test.py +++ b/werkzeug/test.py @@ -28,7 +28,7 @@ from werkzeug._compat import iterlists, iteritems, itervalues, to_bytes, \ string_types, text_type, reraise, wsgi_encoding_dance, \ make_literal_wrapper -from werkzeug._internal import _empty_stream, _get_environ +from werkzeug._internal import _get_environ from werkzeug.wrappers import BaseRequest from werkzeug.urls import url_encode, url_fix, iri_to_uri, url_unquote, \ url_unparse, url_parse @@ -596,7 +596,7 @@ def get_environ(self): content_length = len(values) input_stream = BytesIO(values) else: - input_stream = _empty_stream + input_stream = BytesIO() result = {} if self.environ_base: diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 991482a0e..04e339e33 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -27,7 +27,7 @@ from werkzeug._compat import BytesIO, PY2, implements_iterator, iteritems, \ make_literal_wrapper, string_types, text_type, to_bytes, to_unicode, \ try_coerce_native, wsgi_get_bytes -from werkzeug._internal import _empty_stream, _encode_idna +from werkzeug._internal import _encode_idna from werkzeug.filesystem import get_filesystem_encoding from werkzeug.http import http_date, is_resource_modified, \ is_hop_by_hop_header @@ -226,7 +226,7 @@ def get_input_stream(environ, safe_fallback=True): # potentially dangerous because it could be infinite, malicious or not. If # safe_fallback is true, return an empty stream instead for safety. if content_length is None: - return safe_fallback and _empty_stream or stream + return safe_fallback and BytesIO() or stream # Otherwise limit the stream to the content length return LimitedStream(stream, content_length) From 083bc65d27f5a7650f0fdfb734434c4ae2bca82a Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Aug 2018 21:19:48 -0700 Subject: [PATCH 081/280] update for latest pallets-sphinx-theme remove unused static files generate latest sphinx make files --- docs/Makefile | 118 +------- docs/_static/background.png | Bin 73096 -> 0 bytes docs/_static/codebackground.png | Bin 8307 -> 0 bytes docs/_static/contents.png | Bin 5527 -> 0 bytes docs/_static/header.png | Bin 86871 -> 0 bytes docs/_static/navigation.png | Bin 218 -> 0 bytes docs/_static/navigation_active.png | Bin 231 -> 0 bytes docs/_static/shorty-screenshot.png | Bin 85078 -> 0 bytes docs/_static/style.css | 423 ----------------------------- docs/_static/werkzeug.js | 10 - docs/conf.py | 78 +----- docs/make.bat | 101 ++----- docs/makearchive.py | 7 - werkzeug/routing.py | 3 +- 14 files changed, 44 insertions(+), 696 deletions(-) delete mode 100644 docs/_static/background.png delete mode 100644 docs/_static/codebackground.png delete mode 100644 docs/_static/contents.png delete mode 100644 docs/_static/header.png delete mode 100644 docs/_static/navigation.png delete mode 100644 docs/_static/navigation_active.png delete mode 100644 docs/_static/shorty-screenshot.png delete mode 100644 docs/_static/style.css delete mode 100644 docs/_static/werkzeug.js delete mode 100644 docs/makearchive.py diff --git a/docs/Makefile b/docs/Makefile index 52d78d9ef..2ca70ea46 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,118 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SPHINXPROJ = Werkzeug +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp epub latex changes linkcheck doctest - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) _build/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask" - @echo "# ln -s _build/devhelp $$HOME/.local/share/devhelp/Flask" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -latexpdf: latex - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex - @echo "Running LaTeX files through pdflatex..." - make -C _build/latex all-pdf - @echo "pdflatex finished; the PDF files are in _build/latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: help Makefile -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/background.png b/docs/_static/background.png deleted file mode 100644 index 928957aa3c01701a9b842138a93ee8474afb779d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73096 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 LNkvXXu0mjf{+y>$ diff --git a/docs/_static/codebackground.png b/docs/_static/codebackground.png deleted file mode 100644 index 3368da2ea931348a1fd8ac0dbec71bd8de564509..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8307 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 EFaDB{7ytkO diff --git a/docs/_static/contents.png b/docs/_static/contents.png deleted file mode 100644 index 6f993b5e3da27708d913115f46f9e2f9d80b2f39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5527 zcmV;I6=>>-P)IG>*vuMf}P*W>r!f4}~oSLSanIInPJ?tWeT`ulo|i~BC~;cJA~$L5uJYreuW zBe_2I@4x?EFJ51{o@eB91wJ{S`TF(i^&;cEUgRz_r0at7t&gvGQFxxKU(XD5zVGSH zN-^|@9$aMZ#QL)0+-qjS@_EItKlm=M`vY8mk7w3v7Lm{MnJ=G@`3!O}OE%ApbDpmS zeR#KLtaJOh$}I4VkM+8?f`ynbW~G?YYz4QU@y#C=8KyCNa9#g;ab`HXJ9J39@tMmEU}WmgKmTOCSXnOX>C7Pi`RAY6NTm4QnYj}Zi1=KKn)xHo{uuXQ$-IvK zm+>J2^U8H2u5(>{HjG_i&*l{_=)26|y_DCx*MzStGtPN&_TYNV-sG^ed2_G1?0xaf z#5?27=iG8$7&Ie@;LHtp1tl_n|MABk=$b7Q+|=?opSiy4u_68$aE8iyd0jj^4`A+l zjnLP*S9cbIbI0vxEf_AR2{qw4L^q-4_%4Hva+#e+yNxVwGr}tEACj>(v}p61~5>by9b&kOY9^D&EJ`JZcY9@Gpih>#`?<&wB5b~`MCzSpea-+%wjc;a}o z@!vlmBYGc~nxP;mRzv$N#F5)O<9plpLs`x^yy1S##-Nz(5X@fs1dOsr3D{3&cyr9{ z=*ASxUe2XhW#R-*?me%uhIom2&S{#*C4lv_+SP8`JKHYSUb7bnN@2-1bdh9?uh>3;vJ~JR&D+G`z)NZ;xozZ|k0zOl0 zm{l)k*TJk^c~9FbGWW7ChP}=%GEML3?8;ZWpb2ZNvt!_*|9oIMM63(1k02wvINiU_AEdC zr}fJSl!JH{D^73Dqj3d>fm`4qv2CMgE#`inE%%Ds%}&hc6Mb951Y9G2cHvOi+xBgT z#hw8^JyrrQ#&bYd+QubDNrdPRu$tqMz5^+@CxyVEb!;j2~5h>3BC8RQ%Yhd2Rh(2w&LVXURq!i8lY`C=XT`$MiT-wf+(Tf<$6 zczkd+5`yA01c-}o-<@40qQaYHpu|s!DEYh(vXK&|7rnu=wMyJzgN1Cvi{i&YM-DNu z4s@9FlPmjjB z+|F3kP;wBsLJS@IkS`j3NEDVdQe1Gmn#q`;D5CzYYu>pg=tT5P>;T>mmiH@)!zVv@ zHph6rX}t9aT@oc2?VC-rK00}?a7h>=}zD2Aaf`;kz{DBSjqWj`Dmva$o10;s~ZanU5Gzbxc zV*k{uMMmn`E!wYI~YY(9HnJP&<8DrF}x3{PfvUvNaU zoz;Bc4hKrM^hvw2=M&Rk(t$(lge9X-Ry_m&pV=rcC-LLiScTiL5>6OL-C8mbadO~~ zSQbQg;w%p+$qN?90E5l{F(ks(aL{j^GP4~SG^{?}nf>g^R?H{8e;ek<1V>Kyxrw`n z1l(T8bqvuRdm)J60~mpX7}B#l8ZwMg(6}?Pi53kz7KshEsqu<)vK%v7!QmcR8g_0e z$+u^|-yzsBEoirE-o*~uu31O;Y|hYweDQC@)?eS*cC35w(tWt%+&YSwKBhdMYe7}!c5iiU1(x2h2iGCG?r3G)fBIQnF#r33#T!`N@nKO)@%g z2Q)~Htr!NvK8SNro6Kl$KH?pg%Z}`mDBd6{iqeMCwur%p++eJ6X1cv|vq3j6=%xU& zy`G_=qVTXno--!HJgqOFAC8WXE^r7k$_xJ>UPa(jZxv)lAaY0GHmUJZ?KvCZcOJ;v z=61(}b!2aSm88q(d0bGF$Ba4}|1&@u1jZZVtWwAd$9Qwznz+Oejnn$X19%UPHyNZY zctZsPCds7kpfoOzBZCljLs{@tqusIJhUA?A45^H5@Hq2bV;k>~5(9ZhE;ha{dh?MS zcN76;VvdP{`5Xk>Ae2aOF3Z$r$Km4H!3nNpcrxDcf=$pzPTyU$_pL&Yj)Cc#T1I z8V33DS<8r1VS&_KQRe^;8$~$6UN(6S(2_%gJ7^m{gP$Wtkl;9M!+TbghPHP)`5DZ| zgP)NN_O?SX7R%{xZf0;Q>dZGLOX^{n2fi27bQQbG25k(7g?blt9D>w~+xb(YZNK5p z?j)WffUq@jBxNx+a=%feoC~Ihyx>VX42(Co)BTQz?L)dI;7Gl*dlk0K5uCx)-10bq zG>MJmH8W^0HmPja2U!GC@?n`b{BNTZ4`A~G9UD8d@C%!0Xz0h!2a_e13d=00|E-t) z`s=Tm9(r{9X;zaV&U&_(&Ty7zQ4*%usJkR6vkl;7xpQnE+OGY6ho6Blj%lH!hY{G1 z7ZZGJ+Fzi!`3$`C4OG0t7@RJ1Mj#|g3mpn>%QfbbcahU*$&HWiB0Qtgey&G?l=@~N zx|kJ(=?MxN1_L2$EDO=8abfRaBJhKjU0Lpg`lf5(7#=}dyHqTV`ud^E%J&e>&AynF<`Ig)cb^Mi5C%!o5&0u#+M;=VWxd!_|!8Vf|*=2l5EGD#!8UUS!p_ky~%9By^Qa~3@5dhSU*sz z%dLHyOpkJGkqxkdkL%>IC3E9IFe)bNVousBrYi;zcdad9#(c?ZYFVVQE)LlkiANVwbe~l_S;D(31TZ z55kIoV8FiHkc2jc#OEUDjlCArNjL!{G!4TgW$9|3kP3Wg?wz(mb$pXpJaK>*bmQ5Z z1!?6?a?Ch~Sv1ZjY$6RwogqS*h0f}v`o)^`sHcw1r?(VpIAq!F9CD7fJGR?Yq!PqI%P~9qtldek0c-a(SNO=C{oK8%o=x?H zui)jtS~)H3C?wqBAbfn@(aDD1aC$cn$zUvy0W&3iV+iC)v@*!Q)2pd!k2*A~j|4%g zV8eAl^}0B~!Ky76*xAl*`DojXNNy>rDyaj+ZHU+y4I_HsKOg^Gt^>uh)(#j%?@R(< zMqtQcm$7fid^buVU@{uvP|>KY_4l!yJ?;qNy_nNY94rk2!kp@}`qcrVMPawaILWk#9fCsQJd@oom=V zXVMVIWZZpB!d}ej*!92f_&`L^%p%5A)dv zEn}L1Bpz7izNSKg7UK_ZC4(J*N(Zea7Nlf<=%q>SK&Cb>xrCOst$<)XS&OeC;!nu|@|HTz+! z=Pd7(Qg$M>`6=nmTA1~kV-e(sG|xvkJRPPuuH=Mwk0zX*WC?d&DgLoZTZYwN@=GcX z2;A)jzH4;k!j-w18FAslp-p!+ zrr8o{Ops=7&QOHiys=gfFSua(t39n2PN{*5UF#TC@?7j!d;hLOiE-rgH%hBCLvh>n z+sTfE6X7r&TPhs08=ID8JoeUxM{9K~;OA7k<3JKzI4mP^N5Y>gh=1Wpx*}`0C`gIaY02EQM|2z!vIDBK`(1g*|p6pVh@shSAy zvCeh!9k`9Vi{Y_?C*!;+T{xU(Cv1!3c-t+7=wLP@p8Pf>H+FCCjZqJ7<;)!$uuFig zgWddZ$RG)ZvytiWW~J$p9glWnlMffZAiZATokLDL*UjG#!I^Kl$t7QO9104Q4mQbs zZD`-zh20L%X>;y^6CcW$z5f`EM>KSxkIg#tmb$MXnv%XhGpyZ8@z}`Wn21g`001By zdef{zpWLN>5!Lt{nPyhtPMnw{el&||{F^#1bNhbL+V&lOw5jdyn)`%Qk{x|lM4!&9 z@kq8H^_d-Y6SyC8^%>vQoku$UVW$qluj@ZHh@zU%DARCW+ z&OKpmI?3m^8jQoFca`CeIU01H!ShNlIdU0Bn!JcXjdPA-(UO!32`R+6kVWH;9TPOq zz77`MWS+Mir9j^BP=crM|Ey(p5iJfdmKWS>Kit4@66+Gm>4zMh;EwQragLo<0Txy6p!q z?rn-{f&jDSoa1p{2Z$9xMRpT`{7lBK9rhQEqBt9Nq{&hIoEbUr2$P)NeC{rl-!ge| zA)gcxfpC`R)E`;rMYSiyNhUQV?M#4~yJpUCdt8bnGFYH@Nc7HTCQa*K$1n>lu;|C| zFihJiP5;&$klp(t%-1x*AcFw$JA*#9Ge`%~UntODc)Ys?!kT*b^bJk@85@v}4(TO8 z+GeaKbLUzzHxt>N?Kk1T)sAsuL*VLURGKw!d5LeaJ3-qY+@wgycB!jNR9pDd@uYuKCfw5k7CHs-OrsW_bjYQ%`GQp+9U~(zGoK(EBMq3>}<2Q2q4TF zdRnPE;-`fxwiVAlJYth)0;q1*hLIe;;)-y93a4Fg>+KKa0R8s*V_+{0K!$+po`KFm zq}T9oufKG+72Iyqv^mIGrqHHrH{x^Vhp)&mc~{boIF09bzqA1krvSppTAbS#3_d}o z6=CEkUCYcOa+i!~R+4N1g_GH(oqT>Q=g8vq7-9`Jr-2sIGe|eKi)KvI-9NZg|g?D+g`&*3xP4Z&?#b)is@M*&2`XR4r7)YDwkiPG3@!|@5tH1vR Z7yz0-u}002ovPDHLkV1gzQ={5iW diff --git a/docs/_static/header.png b/docs/_static/header.png deleted file mode 100644 index 30d4e3adc827796f447f3069adef1aa36a432723..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86871 zcmV)OK(@b$P)Px#32;bRa{vGvuK)lWuK`{fksJU300(qQO+^RT0S*Q&7}(9!Y5)Kr07*naRCwC7 zz1fo_*>&dkJNNn$OJ?L&TkqXixXmwS&M+#}ML>mHE}PMMWeR%S+o`}Mng=R4o|j{GnG^S@J6 zSr!GkSF+Nx$qFX1WtJw)CJC`oVjGbp2}YoFo;XTCOD5?Nw|8&3+2qXT$J|}7IZZ8- z#e~hKBu!#O9LGl|7!T?l-U_$^uO4F(R_i;Ko02y-OJ3bBd3(3vhu{4GB*wTk(@9F{ z9Fxc(BCLxX=OB#}iXy{_vfg-{!sX3|Pd|Ld&E1lV(^H&tJbQFOS!Spz))-L17{hFO zMCm0fw2=5(EmAmB)Md1*y++`a!MTtnnd^#b| zH)OuRIVFu^PUlBVCn+b3Gfp49NBQ2L@Yl0X`H!1ZUfg;#SMUm@qNRee`+s#YRDZly z|EhwozenS1sb6cVZ|{D-{aR%EO=SDC*6+4{82Fxqf7X1>(9g$yR~pwRfUWPr)c-dI ztRNP!00j~FU;Kps>&Jh`>fiiN{HIU;ip#Y}3q^}+9%$K^3$Fg_6?OF-@b$l1 zKRc}-pud0Q_kVQ!p&atO2LCkPyE)%>UrOWu5}0VTe^(l>*O8ALyUpMj484Hy&uWfo z;H4^^Z)$r?CGex&8XEh|?emoGXUga!=>4mm50cLPO6NU?uB-Pt>2C$QF_HU=lFr=o z19MIqH=+-mf}Y>+j|RSe$eNHtz9$cVkAI09(toMX*gnh7B$1IfHV2NK1m@}X+IMG@ z4%~v?|6H4&DS>s=ScEFO-_^!`BQi7(w0U9gDl#-V@ z#zveU$4nGBL=L2^WzC;$1~o`Px;MP zZvewOD|mhRmd9tOh%wj+#A!rq61*!&lN9d?EO2^y%&N$Ed~!x4%6bD%4bLxcP%+#t zmpnaRV6;FGk~k)cV>X)=iZDs$I4vk#i4g;0h~fnAN=jc6r4y{N)Q1J^EAr)9iv5s!%8x zMPfyrRx`uaKwWB3ZW{+sw4Ge_=HfMcxJr8i6&f3dNPokU_J*~06Oqp0t`7j|(%HPF z0m_>fY&w5M?*AIu0e3MF8zFKz6ZLrS$%_oHiiu`KaltfAD80i*25SWM;6?B#)PTFl z9Z?FX2QlQO!x%-K;8k(XVXOfM#wdcIb?j6
P|`ot07-Lo^^Ie$c6=Dd3I5^EHf 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

<0AC*Q< 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

Nick Search

<% if results %>

The nickname "$escape(user)" is currently playing on the - following ${len(results) == 1 and 'server' or 'servers'}: + following ${'server' if len(results) == 1 else 'servers'}:

    <% for server in results %> diff --git a/examples/cupoftee/templates/serverlist.html b/examples/cupoftee/templates/serverlist.html index 8562e417f..a49bb4bd9 100644 --- a/examples/cupoftee/templates/serverlist.html +++ b/examples/cupoftee/templates/serverlist.html @@ -27,7 +27,7 @@

    Server List

    $escape(server.map) $server.gametype $server.player_count / $server.max_players - ${server.progression >= 0 and '%d%%' % server.progression or '?'} + ${'%d%%' % server.progression if server.progression >= 0 else '?'} <% endfor %> diff --git a/examples/cupoftee/utils.py b/examples/cupoftee/utils.py index 92d45871e..c46620f04 100644 --- a/examples/cupoftee/utils.py +++ b/examples/cupoftee/utils.py @@ -16,4 +16,4 @@ def unicodecmp(a, b): x, y = map(_sort_re.search, [a, b]) - return cmp((x and x.group() or a).lower(), (y and y.group() or b).lower()) + return cmp((x.group() if x else a).lower(), (y.group() if y else b).lower()) diff --git a/examples/i18nurls/application.py b/examples/i18nurls/application.py index dce2ec1ab..74ffafbd7 100644 --- a/examples/i18nurls/application.py +++ b/examples/i18nurls/application.py @@ -72,7 +72,7 @@ def __call__(self, environ, start_response): req.matched_url = (endpoint, args) if endpoint == '#language_select': lng = req.accept_languages.best - lng = lng and lng.split('-')[0].lower() or 'en' + lng = lng.split('-')[0].lower() if lng else 'en' index_url = urls.build('index', {'lang_code': lng}) resp = Response('Moved to %s' % index_url, status=302) resp.headers['Location'] = index_url diff --git a/examples/plnt/sync.py b/examples/plnt/sync.py index 8d2ea9a84..2aa3ba85f 100644 --- a/examples/plnt/sync.py +++ b/examples/plnt/sync.py @@ -54,7 +54,7 @@ def sync(): else: title = entry.get('title') url = entry.get('link') or blog.blog_url - text = 'content' in entry and entry.content[0] or \ + text = entry.content[0] if 'content' in entry else \ entry.get('summary_detail') if not title or not text: diff --git a/examples/simplewiki/templates/action_edit.html b/examples/simplewiki/templates/action_edit.html index d1efbcb2d..84d74fab6 100644 --- a/examples/simplewiki/templates/action_edit.html +++ b/examples/simplewiki/templates/action_edit.html @@ -5,12 +5,12 @@ - ${new and 'Create' or 'Edit'} Page + ${'Create' if new else 'Edit'} Page -

    ${new and 'Create' or 'Edit'} “${page.title or page_name}”

    +

    ${'Create' if new else 'Edit'} “${page.title or page_name}”

    - You can now ${new and 'create' or 'modify'} the page contents. To + You can now ${'create' if new else 'modify'} the page contents. To format your text you can use creole markup.

    ${error}

    diff --git a/examples/simplewiki/templates/action_log.html b/examples/simplewiki/templates/action_log.html index 9cef6f63c..05a97c06a 100644 --- a/examples/simplewiki/templates/action_log.html +++ b/examples/simplewiki/templates/action_log.html @@ -21,14 +21,14 @@

    Revisions for “${page.title}

    Actions + class="${'even' if idx % 2 == 1 else 'odd'}"> ${format_datetime(revision.timestamp)} ${revision.change_note} + checked="${'checked' if idx == 1 else None}" /> + checked="${'checked' if idx == 0 else None}" /> show diff --git a/examples/simplewiki/templates/layout.html b/examples/simplewiki/templates/layout.html index 0762cab9f..d63bb65c6 100644 --- a/examples/simplewiki/templates/layout.html +++ b/examples/simplewiki/templates/layout.html @@ -27,7 +27,7 @@

    Simple Wiki

    ('edit', href(page.name, action='edit'), 'edit'), ('log', href(page.name, action='log'), 'log') )"> - ${title} | diff --git a/examples/simplewiki/templates/recent_changes.html b/examples/simplewiki/templates/recent_changes.html index 51657923b..46bd7a197 100644 --- a/examples/simplewiki/templates/recent_changes.html +++ b/examples/simplewiki/templates/recent_changes.html @@ -15,7 +15,7 @@

    Recent Changes

    Change Note + class="${'even' if idx % 2 == 1 else 'odd'}"> ${format_datetime(entry.timestamp)} ${entry.title} ${entry.change_note} diff --git a/examples/simplewiki/utils.py b/examples/simplewiki/utils.py index 1f24a19e4..bc7b0a40e 100644 --- a/examples/simplewiki/utils.py +++ b/examples/simplewiki/utils.py @@ -65,9 +65,9 @@ def href(*args, **kw): Simple function for URL generation. Position arguments are used for the URL path and keyword arguments are used for the url parameters. """ - result = [(request and request.script_root or '') + '/'] + result = [(request.script_root if request else '') + '/'] for idx, arg in enumerate(args): - result.append((idx and '/' or '') + url_quote(arg)) + result.append(('/' if idx else '') + url_quote(arg)) if kw: result.append('?' + url_encode(kw)) return ''.join(result) diff --git a/werkzeug/contrib/atom.py b/werkzeug/contrib/atom.py index 51d1a4e64..ec2149f8d 100644 --- a/werkzeug/contrib/atom.py +++ b/werkzeug/contrib/atom.py @@ -120,7 +120,7 @@ def __init__(self, title=None, entries=None, **kwargs): if self.generator is None: self.generator = self.default_generator self.links = kwargs.get('links', []) - self.entries = entries and list(entries) or [] + self.entries = list(entries) if entries else [] if not hasattr(self.author, '__iter__') \ or isinstance(self.author, string_types + (dict,)): @@ -164,7 +164,7 @@ def generate(self): if not self.updated: dates = sorted([entry.updated for entry in self.entries]) - self.updated = dates and dates[-1] or datetime.utcnow() + self.updated = dates[-1] if dates else datetime.utcnow() yield u'\n' yield u'\n' diff --git a/werkzeug/contrib/jsrouting.py b/werkzeug/contrib/jsrouting.py index d8158927e..dec633433 100644 --- a/werkzeug/contrib/jsrouting.py +++ b/werkzeug/contrib/jsrouting.py @@ -211,7 +211,7 @@ def generate_map(map, name='url_map'): u'defaults': rule.defaults }) - return render_template(name_parts=name and name.split('.') or [], + return render_template(name_parts=name.split('.') if name else [], rules=dumps(rules), converters=converters) diff --git a/werkzeug/contrib/securecookie.py b/werkzeug/contrib/securecookie.py index 5927e2b0b..cad1d2d58 100644 --- a/werkzeug/contrib/securecookie.py +++ b/werkzeug/contrib/securecookie.py @@ -160,7 +160,7 @@ def __repr__(self): return '<%s %s%s>' % ( self.__class__.__name__, dict.__repr__(self), - self.should_save and '*' or '' + '*' if self.should_save else '' ) @property diff --git a/werkzeug/contrib/sessions.py b/werkzeug/contrib/sessions.py index b8969dd6b..c8232a42d 100644 --- a/werkzeug/contrib/sessions.py +++ b/werkzeug/contrib/sessions.py @@ -128,7 +128,7 @@ def __repr__(self): return '<%s %s%s>' % ( self.__class__.__name__, dict.__repr__(self), - self.should_save and '*' or '' + '*' if self.should_save else '' ) @property diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index c1a1ac06e..505c65df9 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -1782,7 +1782,7 @@ def _specificity(self, value): def _value_matches(self, value, item): def _normalize(x): x = x.lower() - return x == '*' and ('*', '*') or x.split('/', 1) + return ('*', '*') if x == '*' else x.split('/', 1) # this is from the application which is trusted. to avoid developer # frustration we actually check these for valid values @@ -2334,7 +2334,7 @@ def to_header(self): ranges = [] for begin, end in self.ranges: if end is None: - ranges.append(begin >= 0 and '%s-' % begin or str(begin)) + ranges.append('%s-' % begin if begin >= 0 else str(begin)) else: ranges.append('%s-%s' % (begin, end - 1)) return '%s=%s' % (self.units, ','.join(ranges)) @@ -2614,7 +2614,7 @@ def _set_stale(self, value): if value is None: self.pop('stale', None) else: - self['stale'] = value and 'TRUE' or 'FALSE' + self['stale'] = 'TRUE' if value else 'FALSE' stale = property(_get_stale, _set_stale, doc=''' A flag, indicating that the previous request from the client was rejected because the nonce value was stale.''') diff --git a/werkzeug/debug/__init__.py b/werkzeug/debug/__init__.py index 2622df740..a7e456a51 100644 --- a/werkzeug/debug/__init__.py +++ b/werkzeug/debug/__init__.py @@ -380,7 +380,7 @@ def check_pin_trust(self, environ): return (time.time() - PIN_TIME) < int(ts) def _fail_pin_auth(self): - time.sleep(self._failed_pin_auth > 5 and 5.0 or 0.5) + time.sleep(5.0 if self._failed_pin_auth > 5 else 0.5) self._failed_pin_auth += 1 def pin_auth(self, request): diff --git a/werkzeug/debug/console.py b/werkzeug/debug/console.py index 30e89063f..6ed3955c0 100644 --- a/werkzeug/debug/console.py +++ b/werkzeug/debug/console.py @@ -161,7 +161,7 @@ def __init__(self, globals, locals): def runsource(self, source): source = source.rstrip() + '\n' ThreadedStream.push() - prompt = self.more and '... ' or '>>> ' + prompt = '... ' if self.more else '>>> ' try: source_to_eval = ''.join(self.buffer + [source]) if code.InteractiveInterpreter.runsource(self, diff --git a/werkzeug/debug/repr.py b/werkzeug/debug/repr.py index 0a024576e..4861082e3 100644 --- a/werkzeug/debug/repr.py +++ b/werkzeug/debug/repr.py @@ -275,6 +275,6 @@ def render_object_dump(self, items, title, repr=None): html_items.append('Nothing') return OBJECT_DUMP_HTML % { 'title': escape(title), - 'repr': repr and '
    %s
    ' % repr or '', + 'repr': '
    %s
    ' % repr if repr else '', 'items': '\n'.join(html_items) } diff --git a/werkzeug/debug/tbtools.py b/werkzeug/debug/tbtools.py index bc2d825bb..bcac62c78 100644 --- a/werkzeug/debug/tbtools.py +++ b/werkzeug/debug/tbtools.py @@ -330,7 +330,7 @@ def render_summary(self, include_title=True): for frame in self.frames: frames.append(u'%s' % ( - frame.info and u' title="%s"' % escape(frame.info) or u'', + u' title="%s"' % escape(frame.info) if frame.info else u'', frame.render() )) @@ -341,7 +341,7 @@ def render_summary(self, include_title=True): return SUMMARY_HTML % { 'classes': u' '.join(classes), - 'title': title and u'

    %s

    ' % title or u'', + 'title': u'

    %s

    ' % title if title else u'', 'frames': u'\n'.join(frames), 'description': description_wrapper % escape(self.exception) } @@ -351,8 +351,8 @@ def render_full(self, evalex=False, secret=None, """Render the Full HTML page with the traceback info.""" exc = escape(self.exception) return PAGE_HTML % { - 'evalex': evalex and 'true' or 'false', - 'evalex_trusted': evalex_trusted and 'true' or 'false', + 'evalex': 'true' if evalex else 'false', + 'evalex_trusted': 'true' if evalex_trusted else 'false', 'console': 'false', 'title': exc, 'exception': exc, diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 2c3e8d1bc..aede43299 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -741,7 +741,7 @@ def _build_regex(rule): _build_regex(domain_rule) regex_parts.append('\\|') self._trace.append((False, '|')) - _build_regex(self.is_leaf and self.rule or self.rule.rstrip('/')) + _build_regex(self.rule if self.is_leaf else self.rule.rstrip('/')) if not self.is_leaf: self._trace.append((False, '/')) @@ -1182,7 +1182,7 @@ def build_compare_key(self): :internal: """ - return self.alias and 1 or 0, -len(self.arguments), \ + return 1 if self.alias else 0, -len(self.arguments), \ -len(self.defaults or ()) def __eq__(self, other): @@ -1848,7 +1848,7 @@ def _handle_match(match): redirect_url = rule.redirect_to(self, **rv) raise RequestRedirect(str(url_join('%s://%s%s%s' % ( self.url_scheme or 'http', - self.subdomain and self.subdomain + '.' or '', + self.subdomain + '.' if self.subdomain else '', self.server_name, self.script_name ), redirect_url))) @@ -1906,7 +1906,7 @@ def get_host(self, domain_part): subdomain = self.subdomain else: subdomain = to_unicode(subdomain, 'ascii') - return (subdomain and subdomain + u'.' or u'') + self.server_name + return (subdomain + u'.' if subdomain else u'') + self.server_name def get_default_redirect(self, rule, method, values, query_args): """A helper that returns the URL to redirect to if it finds one. diff --git a/werkzeug/security.py b/werkzeug/security.py index 9cd81d087..59bf85067 100644 --- a/werkzeug/security.py +++ b/werkzeug/security.py @@ -191,7 +191,7 @@ def generate_password_hash(password, method='pbkdf2:sha256', salt_length=8): to enable PBKDF2. :param salt_length: the length of the salt in letters. """ - salt = method != 'plain' and gen_salt(salt_length) or '' + salt = gen_salt(salt_length) if method != 'plain' else '' h, actual_method = _hash_internal(method, salt, password) return '%s$%s$%s' % (actual_method, salt, h) diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 6fb43fb72..c3a8a6ce4 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -165,7 +165,7 @@ def make_environ(self): def shutdown_server(): self.server.shutdown_signal = True - url_scheme = self.server.ssl_context is None and 'http' or 'https' + url_scheme = 'http' if self.server.ssl_context is None else 'https' if not self.client_address: self.client_address = '' if isinstance(self.client_address, str): @@ -818,7 +818,7 @@ def run_simple(hostname, port, application, use_reloader=False, application = SharedDataMiddleware(application, static_files) def log_startup(sock): - display_hostname = hostname not in ('', '*') and hostname or 'localhost' + display_hostname = hostname if hostname not in ('', '*') else 'localhost' quit_msg = '(Press CTRL+C to quit)' if sock.family is socket.AF_UNIX: _log('info', ' * Running on %s %s', display_hostname, quit_msg) @@ -827,7 +827,7 @@ def log_startup(sock): display_hostname = '[%s]' % display_hostname port = sock.getsockname()[1] _log('info', ' * Running on %s://%s:%d/ %s', - ssl_context is None and 'http' or 'https', + 'http' if ssl_context is None else 'https', display_hostname, port, quit_msg) def inner(): diff --git a/werkzeug/testapp.py b/werkzeug/testapp.py index 595555a09..d5246a26c 100644 --- a/werkzeug/testapp.py +++ b/werkzeug/testapp.py @@ -186,7 +186,7 @@ def render_testapp(req): if expanded: class_.append('exp') sys_path.append('%s' % ( - class_ and ' class="%s"' % ' '.join(class_) or '', + ' class="%s"' % ' '.join(class_) if class_ else '', escape(item) )) diff --git a/werkzeug/urls.py b/werkzeug/urls.py index 8a95b4792..8ef7ec6dd 100644 --- a/werkzeug/urls.py +++ b/werkzeug/urls.py @@ -445,7 +445,7 @@ def url_parse(url, scheme=None, allow_fragments=True): if s('?') in url: url, query = url.split(s('?'), 1) - result_type = is_text_based and URL or BytesURL + result_type = URL if is_text_based else BytesURL return result_type(scheme, netloc, url, query, fragment) diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 04e339e33..0d10e9501 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -226,7 +226,7 @@ def get_input_stream(environ, safe_fallback=True): # potentially dangerous because it could be infinite, malicious or not. If # safe_fallback is true, return an empty stream instead for safety. if content_length is None: - return safe_fallback and BytesIO() or stream + return BytesIO() if safe_fallback else stream # Otherwise limit the stream to the content length return LimitedStream(stream, content_length) From 656d6c7cb77da7ebc39f83fbea401fcd258f871e Mon Sep 17 00:00:00 2001 From: chengkang <1412950785@qq.com> Date: Wed, 12 Sep 2018 18:00:19 +0800 Subject: [PATCH 085/280] revert some changes --- examples/i18nurls/application.py | 2 +- examples/plnt/sync.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/i18nurls/application.py b/examples/i18nurls/application.py index 74ffafbd7..dce2ec1ab 100644 --- a/examples/i18nurls/application.py +++ b/examples/i18nurls/application.py @@ -72,7 +72,7 @@ def __call__(self, environ, start_response): req.matched_url = (endpoint, args) if endpoint == '#language_select': lng = req.accept_languages.best - lng = lng.split('-')[0].lower() if lng else 'en' + lng = lng and lng.split('-')[0].lower() or 'en' index_url = urls.build('index', {'lang_code': lng}) resp = Response('Moved to %s' % index_url, status=302) resp.headers['Location'] = index_url diff --git a/examples/plnt/sync.py b/examples/plnt/sync.py index 2aa3ba85f..8d2ea9a84 100644 --- a/examples/plnt/sync.py +++ b/examples/plnt/sync.py @@ -54,7 +54,7 @@ def sync(): else: title = entry.get('title') url = entry.get('link') or blog.blog_url - text = entry.content[0] if 'content' in entry else \ + text = 'content' in entry and entry.content[0] or \ entry.get('summary_detail') if not title or not text: From 069102b5c5b2ee2867f25ca5c1fe98f050b7d523 Mon Sep 17 00:00:00 2001 From: Rovshan Musayev Date: Thu, 13 Sep 2018 10:54:26 +0200 Subject: [PATCH 086/280] removed trailing white space --- werkzeug/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index 9d24f146b..b36b41088 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -520,11 +520,11 @@ class Locked(HTTPException): ) -class Depended(HTTPException): +class FailedDependency(HTTPException): """*424* `Failed Dependency` - Used if the method could not be performed on the resource + Used if the method could not be performed on the resource because the requested action depended on another action and that action failed. """ code = 424 From 32ec4bf7469c06e2998fecb877d06eb7ecf59566 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 16 Sep 2018 10:44:11 +0800 Subject: [PATCH 087/280] Add documentation link to README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index d92386f27..4e7d448fa 100644 --- a/README.rst +++ b/README.rst @@ -66,6 +66,7 @@ Links ----- * Website: https://www.palletsprojects.com/p/werkzeug/ +* Documentation: http://werkzeug.pocoo.org/docs/ * Releases: https://pypi.org/project/Werkzeug/ * Code: https://github.com/pallets/werkzeug * Issue tracker: https://github.com/pallets/werkzeug/issues From 759aa6836d3734ae8587656941185d4d6c947966 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Sep 2018 12:29:00 -0700 Subject: [PATCH 088/280] Import abc classes from collections.abc Fixes warnings that look like this: ``` werkzeug/datastructures.py:16: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working ``` You can replicate these warnings by running `python -Wonce -m werkzeug.datastructures` --- werkzeug/_compat.py | 4 ++++ werkzeug/datastructures.py | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/werkzeug/_compat.py b/werkzeug/_compat.py index fda038b97..f9c5b3434 100644 --- a/werkzeug/_compat.py +++ b/werkzeug/_compat.py @@ -33,6 +33,8 @@ int_to_byte = chr iter_bytes = iter + import collections as collections_abc + exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') def fix_tuple_repr(obj): @@ -132,6 +134,8 @@ def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): int_to_byte = operator.methodcaller('to_bytes', 1, 'big') iter_bytes = functools.partial(map, int_to_byte) + import collections.abc as collections_abc + def reraise(tp, value, tb=None): if value.__traceback__ is not tb: raise value.with_traceback(tb) diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index c1a1ac06e..52fe6bdd9 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -13,11 +13,10 @@ import mimetypes from copy import deepcopy from itertools import repeat -from collections import Container, Iterable, MutableSet from werkzeug._internal import _missing -from werkzeug._compat import BytesIO, iterkeys, itervalues, iteritems, \ - iterlists, PY2, text_type, integer_types, string_types, \ +from werkzeug._compat import BytesIO, collections_abc, iterkeys, itervalues, \ + iteritems, iterlists, PY2, text_type, integer_types, string_types, \ make_literal_wrapper, to_native from werkzeug.filesystem import get_filesystem_encoding @@ -2020,7 +2019,7 @@ def __repr__(self): ) -class HeaderSet(MutableSet): +class HeaderSet(collections_abc.MutableSet): """Similar to the :class:`ETags` class this implements a set-like structure. Unlike :class:`ETags` this is case insensitive and used for vary, allow, and @@ -2174,7 +2173,7 @@ def __repr__(self): ) -class ETags(Container, Iterable): +class ETags(collections_abc.Container, collections_abc.Iterable): """A set that can be used to check if one etag is present in a collection of etags. From 442514022aa81b0b36230baf0b282e9d2f82654e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 17 Sep 2018 16:31:22 -0700 Subject: [PATCH 089/280] Fix signature of get_headers in subclasses I noticed that `HTTPException.get_headers` has a default value which isn't present in some of the subclasses. This fixes it. --- werkzeug/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index f934de388..8dfa91209 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -301,7 +301,7 @@ def __init__(self, valid_methods=None, description=None): HTTPException.__init__(self, description) self.valid_methods = valid_methods - def get_headers(self, environ): + def get_headers(self, environ=None): headers = HTTPException.get_headers(self, environ) if self.valid_methods: headers.append(('Allow', ', '.join(self.valid_methods))) @@ -457,7 +457,7 @@ def __init__(self, length=None, units="bytes", description=None): self.length = length self.units = units - def get_headers(self, environ): + def get_headers(self, environ=None): headers = HTTPException.get_headers(self, environ) if self.length is not None: headers.append( From 524092d5743a44b6434302ccf625616bd7ac0fe3 Mon Sep 17 00:00:00 2001 From: chengkang <1412950785@qq.com> Date: Tue, 18 Sep 2018 18:48:55 +0800 Subject: [PATCH 090/280] useragents docs omitted edge browser --- werkzeug/useragents.py | 1 + 1 file changed, 1 insertion(+) diff --git a/werkzeug/useragents.py b/werkzeug/useragents.py index 8aebb278f..6f004eda2 100644 --- a/werkzeug/useragents.py +++ b/werkzeug/useragents.py @@ -148,6 +148,7 @@ class UserAgent(object): - `bing` * - `camino` - `chrome` + - `edge` - `firefox` - `galeon` - `google` * From 5c409d5b8fd1d19265616ba71e0488b8c5c047ac Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 24 Oct 2018 22:41:00 +0300 Subject: [PATCH 091/280] Fix a couple of "W605 invalid escape sequence" --- scripts/make-release.py | 2 +- werkzeug-import-rewrite.py | 2 +- werkzeug/routing.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/make-release.py b/scripts/make-release.py index 7da886549..614c710e5 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -15,7 +15,7 @@ def parse_changelog(): with open('CHANGES.rst') as f: lineiter = iter(f) for line in lineiter: - match = re.search('^Version\s+(.*)', line.strip()) + match = re.search(r'^Version\s+(.*)', line.strip()) if match is None: continue diff --git a/werkzeug-import-rewrite.py b/werkzeug-import-rewrite.py index e651aace7..3d587147d 100644 --- a/werkzeug-import-rewrite.py +++ b/werkzeug-import-rewrite.py @@ -19,7 +19,7 @@ _from_import_re = re.compile(r'(\s*(>>>|\.\.\.)?\s*)from werkzeug import\s+') -_direct_usage = re.compile('(? Date: Wed, 24 Oct 2018 22:41:37 +0300 Subject: [PATCH 092/280] Fix "F841 local variable 'e' is assigned to but never used" --- werkzeug/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 5719a2348..33934b3cb 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -1888,7 +1888,7 @@ def allowed_methods(self, path_info=None): self.match(path_info, method='--') except MethodNotAllowed as e: return e.valid_methods - except HTTPException as e: + except HTTPException: pass return [] From 593eaeae86e36c31a5626c04269c807b1fafa525 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 24 Oct 2018 22:53:18 +0300 Subject: [PATCH 093/280] Fix all "W504 line break after binary operator" --- werkzeug/_internal.py | 6 +++--- werkzeug/contrib/cache.py | 4 ++-- werkzeug/datastructures.py | 24 ++++++++++++------------ werkzeug/posixemulation.py | 8 ++++---- werkzeug/routing.py | 12 ++++++------ werkzeug/serving.py | 6 +++--- werkzeug/utils.py | 12 ++++++------ werkzeug/wsgi.py | 8 ++++---- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/werkzeug/_internal.py b/werkzeug/_internal.py index 5a4283cf7..b93f83694 100644 --- a/werkzeug/_internal.py +++ b/werkzeug/_internal.py @@ -25,9 +25,9 @@ _cookie_params = set((b'expires', b'path', b'comment', b'max-age', b'secure', b'httponly', b'version')) -_legal_cookie_chars = (string.ascii_letters + - string.digits + - u"/=!#$%&'*+-.^_`|~:").encode('ascii') +_legal_cookie_chars = (string.ascii_letters + + string.digits + + u"/=!#$%&'*+-.^_`|~:").encode('ascii') _cookie_quoting_map = { b',': b'\\054', diff --git a/werkzeug/contrib/cache.py b/werkzeug/contrib/cache.py index 179ba24a8..fd704390c 100644 --- a/werkzeug/contrib/cache.py +++ b/werkzeug/contrib/cache.py @@ -635,8 +635,8 @@ def add(self, key, value, timeout=None): timeout = self._normalize_timeout(timeout) dump = self.dump_object(value) return ( - self._client.setnx(name=self.key_prefix + key, value=dump) and - self._client.expire(name=self.key_prefix + key, time=timeout) + self._client.setnx(name=self.key_prefix + key, value=dump) + and self._client.expire(name=self.key_prefix + key, time=timeout) ) def set_many(self, mapping, timeout=None): diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py index 52fe6bdd9..61fe5d66e 100644 --- a/werkzeug/datastructures.py +++ b/werkzeug/datastructures.py @@ -1797,28 +1797,28 @@ def _normalize(x): if item_type == '*' and item_subtype != '*': return False return ( - (item_type == item_subtype == '*' or - value_type == value_subtype == '*') or - (item_type == value_type and (item_subtype == '*' or - value_subtype == '*' or - item_subtype == value_subtype)) + (item_type == item_subtype == '*' + or value_type == value_subtype == '*') + or (item_type == value_type and (item_subtype == '*' + or value_subtype == '*' + or item_subtype == value_subtype)) ) @property def accept_html(self): """True if this object accepts HTML.""" return ( - 'text/html' in self or - 'application/xhtml+xml' in self or - self.accept_xhtml + 'text/html' in self + or 'application/xhtml+xml' in self + or self.accept_xhtml ) @property def accept_xhtml(self): """True if this object accepts XHTML.""" return ( - 'application/xhtml+xml' in self or - 'application/xml' in self + 'application/xhtml+xml' in self + or 'application/xml' in self ) @property @@ -2226,8 +2226,8 @@ def to_header(self): if self.star_tag: return '*' return ', '.join( - ['"%s"' % x for x in self._strong] + - ['W/"%s"' % x for x in self._weak] + ['"%s"' % x for x in self._strong] + + ['W/"%s"' % x for x in self._weak] ) def __call__(self, etag=None, data=None, include_weak=False): diff --git a/werkzeug/posixemulation.py b/werkzeug/posixemulation.py index f83b63ed5..e97db95fb 100644 --- a/werkzeug/posixemulation.py +++ b/werkzeug/posixemulation.py @@ -47,8 +47,8 @@ def _rename(src, dst): retry = 0 rv = False while not rv and retry < 100: - rv = _MoveFileEx(src, dst, _MOVEFILE_REPLACE_EXISTING | - _MOVEFILE_WRITE_THROUGH) + rv = _MoveFileEx(src, dst, _MOVEFILE_REPLACE_EXISTING + | _MOVEFILE_WRITE_THROUGH) if not rv: time.sleep(0.001) retry += 1 @@ -70,8 +70,8 @@ def _rename_atomic(src, dst): rv = False while not rv and retry < 100: rv = _MoveFileTransacted(src, dst, None, None, - _MOVEFILE_REPLACE_EXISTING | - _MOVEFILE_WRITE_THROUGH, ta) + _MOVEFILE_REPLACE_EXISTING + | _MOVEFILE_WRITE_THROUGH, ta) if rv: rv = _CommitTransaction(ta) break diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 33934b3cb..31b41b047 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -752,8 +752,8 @@ def _build_regex(rule): return regex = r'^%s%s$' % ( u''.join(regex_parts), - (not self.is_leaf or not self.strict_slashes) and - '(?/?)' or '' + (not self.is_leaf or not self.strict_slashes) + and '(?/?)' or '' ) self._regex = re.compile(regex, re.UNICODE) @@ -778,8 +778,8 @@ def match(self, path, method=None): # trailing slash if self.strict_slashes and not self.is_leaf and \ not groups.pop('__suffix__') and \ - (method is None or self.methods is None or - method in self.methods): + (method is None or self.methods is None + or method in self.methods): raise RequestSlash() # if we are not in strict slashes mode we have to remove # a __suffix__ @@ -2079,8 +2079,8 @@ def build(self, endpoint, values=None, method=None, force_external=False, # shortcut this. if not force_external and ( - (self.map.host_matching and host == self.server_name) or - (not self.map.host_matching and domain_part == self.subdomain) + (self.map.host_matching and host == self.server_name) + or (not self.map.host_matching and domain_part == self.subdomain) ): return '%s/%s' % (self.script_name.rstrip('/'), path.lstrip('/')) return str('%s//%s%s/%s' % ( diff --git a/werkzeug/serving.py b/werkzeug/serving.py index 6fb43fb72..a361367f8 100644 --- a/werkzeug/serving.py +++ b/werkzeug/serving.py @@ -235,9 +235,9 @@ def write(data): self.send_header(key, value) key = key.lower() header_keys.add(key) - if not ('content-length' in header_keys or - environ['REQUEST_METHOD'] == 'HEAD' or - code < 200 or code in (204, 304)): + if not ('content-length' in header_keys + or environ['REQUEST_METHOD'] == 'HEAD' + or code < 200 or code in (204, 304)): self.close_connection = True self.send_header('Connection', 'close') if 'server' not in header_keys: diff --git a/werkzeug/utils.py b/werkzeug/utils.py index 2051f684f..154e24a07 100644 --- a/werkzeug/utils.py +++ b/werkzeug/utils.py @@ -224,8 +224,8 @@ def get_content_type(mimetype, charset): """ if mimetype.startswith('text/') or \ mimetype == 'application/xml' or \ - (mimetype.startswith('application/') and - mimetype.endswith('+xml')): + (mimetype.startswith('application/') + and mimetype.endswith('+xml')): mimetype += '; charset=' + charset return mimetype @@ -552,12 +552,12 @@ def bind_arguments(func, args, kwargs): if kwarg_var is not None: multikw = set(extra) & set([x[0] for x in arg_spec]) if multikw: - raise TypeError('got multiple values for keyword argument ' + - repr(next(iter(multikw)))) + raise TypeError('got multiple values for keyword argument ' + + repr(next(iter(multikw)))) values[kwarg_var] = extra elif extra: - raise TypeError('got unexpected keyword argument ' + - repr(next(iter(extra)))) + raise TypeError('got unexpected keyword argument ' + + repr(next(iter(extra)))) return values diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py index 04e339e33..1662466cc 100644 --- a/werkzeug/wsgi.py +++ b/werkzeug/wsgi.py @@ -434,8 +434,8 @@ def _normalize_netloc(scheme, netloc): if scheme not in (u'http', u'https'): return None else: - if not (base_scheme in (u'http', u'https') and - base_scheme == cur_scheme): + if not (base_scheme in (u'http', u'https') + and base_scheme == cur_scheme): return None # are the netlocs compatible? @@ -513,8 +513,8 @@ def proxy_to(self, opts, path, prefix): def application(environ, start_response): headers = list(EnvironHeaders(environ).items()) headers[:] = [(k, v) for k, v in headers - if not is_hop_by_hop_header(k) and - k.lower() not in ('content-length', 'host')] + if not is_hop_by_hop_header(k) + and k.lower() not in ('content-length', 'host')] headers.append(('Connection', 'close')) if opts['host'] == '': headers.append(('Host', target.ascii_host)) From a9147e300160bd2523a516e5409d1d7c01b058fc Mon Sep 17 00:00:00 2001 From: Aditya Date: Fri, 26 Oct 2018 18:27:17 +0530 Subject: [PATCH 094/280] Use https for external links wherever possible --- CONTRIBUTING.rst | 2 +- docs/deployment/cgi.rst | 2 +- docs/deployment/fastcgi.rst | 12 ++++++------ docs/deployment/mod_wsgi.rst | 12 ++++++------ docs/make.bat | 2 +- docs/routing.rst | 2 +- docs/serving.rst | 2 +- docs/transition.rst | 2 +- docs/tutorial.rst | 4 ++-- examples/README | 4 ++-- examples/couchy/README | 4 ++-- examples/cupoftee/templates/layout.html | 4 ++-- examples/plnt/templates/about.html | 2 +- tests/test_debug.py | 2 +- werkzeug/contrib/fixers.py | 2 +- werkzeug/contrib/limiter.py | 4 ++-- werkzeug/contrib/wrappers.py | 2 +- werkzeug/http.py | 4 ++-- werkzeug/testapp.py | 2 +- 19 files changed, 35 insertions(+), 35 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c98d40b94..cf88893b7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -131,7 +131,7 @@ reports from all runs. .. _email: https://help.github.com/articles/setting-your-email-in-git/ .. _Fork: https://github.com/pallets/werkzeug/pull/2305#fork-destination-box .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork -.. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes .. _PEP8: https://pep8.org/ .. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ .. _coverage: https://coverage.readthedocs.io diff --git a/docs/deployment/cgi.rst b/docs/deployment/cgi.rst index 5c79d5afc..a9f2eb7da 100644 --- a/docs/deployment/cgi.rst +++ b/docs/deployment/cgi.rst @@ -10,7 +10,7 @@ This is also the way you can use a Werkzeug application on Google's `AppEngine`_, there however the execution does happen in a CGI-like environment. The application's performance is unaffected because of that. -.. _AppEngine: http://code.google.com/appengine/ +.. _AppEngine: https://cloud.google.com/appengine/ Creating a `.cgi` file ====================== diff --git a/docs/deployment/fastcgi.rst b/docs/deployment/fastcgi.rst index 84d09877f..baf01605b 100644 --- a/docs/deployment/fastcgi.rst +++ b/docs/deployment/fastcgi.rst @@ -18,7 +18,7 @@ First you need to create the FastCGI server file. Let's call it #!/usr/bin/python from flup.server.fcgi import WSGIServer from yourapplication import make_app - + if __name__ == '__main__': application = make_app() WSGIServer(application).run() @@ -71,8 +71,8 @@ work in the URL root you have to work around a lighttpd bug with the Make sure to apply it only if you are mounting the application the URL root. Also, see the Lighty docs for more information on `FastCGI and Python -`_ (note that -explicitly passing a socket to run() is no longer necessary). +`_ (note +that explicitly passing a socket to `run()` is no longer necessary). Configuring nginx ================= @@ -137,6 +137,6 @@ path. Common problems are: web server. - different python interpreters being used. -.. _lighttpd: http://www.lighttpd.net/ -.. _nginx: http://nginx.net/ -.. _flup: http://trac.saddi.com/flup +.. _lighttpd: https://www.lighttpd.net/ +.. _nginx: https://nginx.org/ +.. _flup: https://www.saddi.com/software/flup/ diff --git a/docs/deployment/mod_wsgi.rst b/docs/deployment/mod_wsgi.rst index 85eb965b9..cbf401dc5 100644 --- a/docs/deployment/mod_wsgi.rst +++ b/docs/deployment/mod_wsgi.rst @@ -4,7 +4,7 @@ If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. -.. _Apache: http://httpd.apache.org/ +.. _Apache: https://httpd.apache.org/ Installing `mod_wsgi` ===================== @@ -74,9 +74,9 @@ application under a different user for security reasons: -For more information consult the `mod_wsgi wiki`_. +For more information consult the `mod_wsgi docs`_. -.. _mod_wsgi: http://code.google.com/p/modwsgi/ -.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide -.. _virtual python: http://pypi.python.org/pypi/virtualenv -.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ +.. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi +.. _installation instructions: https://modwsgi.readthedocs.io/en/latest/user-guides/quick-installation-guide.html +.. _virtual python: https://pypi.org/project/virtualenv/ +.. _mod_wsgi docs: https://modwsgi.readthedocs.io/ diff --git a/docs/make.bat b/docs/make.bat index 8bf6fb87c..5c2f98a29 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -22,7 +22,7 @@ if errorlevel 9009 ( echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://www.sphinx-doc.org/ exit /b 1 ) diff --git a/docs/routing.rst b/docs/routing.rst index 0b1d8269d..85dafe81d 100644 --- a/docs/routing.rst +++ b/docs/routing.rst @@ -19,7 +19,7 @@ Werkzeug provides a much more powerful system, similar to `Routes`_. All the objects mentioned on this page must be imported from :mod:`werkzeug.routing`, not from :mod:`werkzeug`! -.. _Routes: http://routes.groovie.org/ +.. _Routes: https://routes.readthedocs.io/ Quickstart diff --git a/docs/serving.rst b/docs/serving.rst index 2dbc56c2b..54adae9c6 100644 --- a/docs/serving.rst +++ b/docs/serving.rst @@ -151,7 +151,7 @@ preferring ipv6, you will be unable to connect to your server. In that situation, you can either remove the localhost entry for ``::1`` or explicitly bind the hostname to an ipv4 address (`127.0.0.1`) -.. _hosts file: http://en.wikipedia.org/wiki/Hosts_file +.. _hosts file: https://en.wikipedia.org/wiki/Hosts_file SSL --- diff --git a/docs/transition.rst b/docs/transition.rst index 54cf2a0ea..aa1a1ecd1 100644 --- a/docs/transition.rst +++ b/docs/transition.rst @@ -26,7 +26,7 @@ was this:: With Werkzeug 0.7, the recommended way to import this function is directly from the utils module (and with 1.0 this will become mandatory). To automatically rewrite all imports one can use the -`werkzeug-import-rewrite `_ script. +`werkzeug-import-rewrite `_ script. You can use it by executing it with Python and with a list of folders with Werkzeug based code. It will then spit out a hg/git compatible patch diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 9435e34e3..f75a67429 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -45,9 +45,9 @@ The final result will look something like this: .. image:: _static/shortly.png :alt: a screenshot of shortly -.. _TinyURL: http://tinyurl.com/ +.. _TinyURL: https://tinyurl.com/ .. _Jinja: http://jinja.pocoo.org/ -.. _redis: http://redis.io/ +.. _redis: https://redis.io/ Step 0: A Basic WSGI Introduction --------------------------------- diff --git a/examples/README b/examples/README index 8f74b4f09..64ab0f788 100644 --- a/examples/README +++ b/examples/README @@ -93,10 +93,10 @@ find in real life :-) Requirements : - werkzeug : http://werkzeug.pocoo.org - jinja : http://jinja.pocoo.org - - couchdb 0.72 & above : http://www.couchdb.org + - couchdb 0.72 & above : https://couchdb.apache.org/ `cupoftee` - A `Teeworlds `_ server browser. This application + A `Teeworlds `_ server browser. This application works best in a non forking environment and won't work for CGI. Usage:: diff --git a/examples/couchy/README b/examples/couchy/README index 1bfc3a977..1821ed117 100644 --- a/examples/couchy/README +++ b/examples/couchy/README @@ -3,6 +3,6 @@ couchy README Requirements : - werkzeug : http://werkzeug.pocoo.org - jinja : http://jinja.pocoo.org -- couchdb 0.72 & above : http://www.couchdb.org -- couchdb-python 0.3 & above : http://code.google.com/p/couchdb-python +- couchdb 0.72 & above : https://couchdb.apache.org/ +- couchdb-python 0.3 & above : https://github.com/djc/couchdb-python diff --git a/examples/cupoftee/templates/layout.html b/examples/cupoftee/templates/layout.html index 3aa88a20e..ec76bfc89 100644 --- a/examples/cupoftee/templates/layout.html +++ b/examples/cupoftee/templates/layout.html @@ -7,12 +7,12 @@ -

    Teeworlds Server Browser

    +

    Teeworlds Server Browser

    ${body}

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 a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -a.headerlink:hover { - background-color: #B4B4B4; - color: #F0F0F0!important; -} - -table { - border-collapse: collapse; - margin: 0 -0.5em 0 -0.5em; -} - -table td, table th { - padding: 0.2em 0.5em 0.2em 0.5em; -} - -div.footer { - background-color: #E3EFF1; - color: #86989B; - padding: 3px 8px 3px 0; - clear: both; - font-size: 0.8em; - text-align: right; -} - -div.footer a { - color: #86989B; - text-decoration: underline; -} - -div.toc { - float: right; - background-color: white; - border: 1px solid #86989B; - padding: 0; - margin: 0 0 1em 1em; - width: 10em; -} - -div.toc h4 { - margin: 0; - font-size: 0.9em; - padding: 0.1em 0 0.1em 0.6em; - margin: 0; - color: white; - border-bottom: 1px solid #86989B; - background-color: #AFC1C4; -} - -div.toc ul { - margin: 1em 0 1em 0; - padding: 0 0 0 1em; - list-style: none; -} - -div.toc ul li { - margin: 0.5em 0 0.5em 0; - font-size: 0.9em; - line-height: 130%; -} - -div.toc ul li p { - margin: 0; - padding: 0; -} - -div.toc ul ul { - margin: 0.2em 0 0.2em 0; - padding: 0 0 0 1.8em; -} - -div.toc ul ul li { - padding: 0; -} - -div.admonition, div.warning, div#toc { - font-size: 0.9em; - margin: 1em 0 0 0; - border: 1px solid #86989B; - background-color: #f7f7f7; -} - -div.admonition p, div.warning p, div#toc p { - margin: 0.5em 1em 0.5em 1em; - padding: 0; -} - -div.admonition pre, div.warning pre, div#toc pre { - margin: 0.4em 1em 0.4em 1em; -} - -div.admonition p.admonition-title, -div.warning p.admonition-title, -div#toc h3 { - margin: 0; - padding: 0.1em 0 0.1em 0.5em; - color: white; - border-bottom: 1px solid #86989B; - font-weight: bold; - background-color: #AFC1C4; -} - -div.warning { - border: 1px solid #940000; -} - -div.warning p.admonition-title { - background-color: #CF0000; - border-bottom-color: #940000; -} - -div.admonition ul, div.admonition ol, -div.warning ul, div.warning ol, -div#toc ul, div#toc ol { - margin: 0.1em 0.5em 0.5em 3em; - padding: 0; -} - -div#toc div.inner { - border-top: 1px solid #86989B; - padding: 10px; -} - -div#toc h3 { - border-bottom: none; - cursor: pointer; - font-size: 13px; -} - -div#toc h3:hover { - background-color: #86989B; -} - -div#toc ul { - margin: 2px 0 2px 20px; - padding: 0; -} - -div#toc ul li { - line-height: 125%; -} - -dl.function dt, -dl.class dt, -dl.exception dt, -dl.method dt, -dl.attribute dt { - font-weight: normal; -} - -dt .descname { - font-weight: bold; - margin-right: 4px; -} - -dt .descname, dt .descclassname { - padding: 0; - background: transparent; - border-bottom: 1px solid #111; -} - -dt .descclassname { - margin-left: 2px; -} - -dl dt big { - font-size: 100%; -} - -dl p { - margin: 0; -} - -dl p + p { - margin-top: 10px; -} - -span.versionmodified { - color: #4B4A49; - font-weight: bold; -} - -span.versionadded { - color: #30691A; - font-weight: bold; -} - -table.field-list td.field-body ul.simple { - margin: 0; - padding: 0!important; - list-style: none; -} - -table.indextable td { - width: 50%; - vertical-align: top; -} - -table.indextable dt { - margin: 0; -} - -table.indextable dd dt a { - color: black!important; - font-size: 0.8em; -} - -div.jumpbox { - padding: 1em 0 0.4em 0; - border-bottom: 1px solid #ddd; - color: #aaa; -} diff --git a/docs/_static/werkzeug.js b/docs/_static/werkzeug.js deleted file mode 100644 index 6ab549a33..000000000 --- a/docs/_static/werkzeug.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { - Werkzeug = {}; - - $(function() { - $('#toc h3').click(function() { - $(this).next().slideToggle(); - $(this).parent().toggleClass('toc-collapsed'); - }).next().hide().parent().addClass('toc-collapsed'); - }); -})(); diff --git a/docs/conf.py b/docs/conf.py index fcb4e1fe2..cbcd7b66c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,15 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function - -import inspect -import re - from pallets_sphinx_themes import ProjectLink, get_version # Project -------------------------------------------------------------- project = 'Werkzeug' -copyright = '2011 Pallets Team' +copyright = '2007 Pallets Team' author = 'Pallets Team' release, version = get_version('Werkzeug') @@ -21,6 +16,7 @@ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.doctest', + 'pallets_sphinx_themes', ] intersphinx_mapping = { @@ -34,42 +30,42 @@ html_theme = 'werkzeug' html_context = { 'project_links': [ - ProjectLink('Donate to Pallets', 'https://psfmember.org/civicrm/contribute/transact?reset=1&id=20'), + ProjectLink('Donate to Pallets', 'https://www.palletsprojects.com/donate'), ProjectLink('Werkzeug Website', 'https://palletsprojects.com/p/werkzeug/'), ProjectLink('PyPI releases', 'https://pypi.org/project/Werkzeug/'), ProjectLink('Source Code', 'https://github.com/pallets/werkzeug/'), ProjectLink('Issue Tracker', 'https://github.com/pallets/werkzeug/issues/'), ], - 'canonical_url': 'http://werkzeug.pocoo.org/docs/{}/'.format(version), - 'carbon_ads_args': 'zoneid=1673&serve=C6AILKT&placement=pocooorg', } html_sidebars = { 'index': [ 'project.html', 'versions.html', - 'carbon_ads.html', 'searchbox.html', ], '**': [ 'localtoc.html', 'relations.html', 'versions.html', - 'carbon_ads.html', 'searchbox.html', ] } +singlehtml_sidebars = { + "index": [ + "project.html", + "versions.html", + "localtoc.html", + ] +} html_static_path = ['_static'] html_favicon = '_static/favicon.ico' html_logo = '_static/werkzeug.png' -html_additional_pages = { - '404': '404.html', -} html_show_sourcelink = False # LaTeX ---------------------------------------------------------------- latex_documents = [ - (master_doc, 'Werkzeug.tex', 'Werkzeug Documentation', 'Pallets Team', 'manual'), + (master_doc, 'Werkzeug.tex', 'Werkzeug Documentation', author, 'manual'), ] latex_use_modindex = False latex_elements = { @@ -80,55 +76,3 @@ } latex_use_parts = True latex_additional_files = ['werkzeugstyle.sty', 'logo.pdf'] - -# linkcheck ------------------------------------------------------------ - -linkcheck_anchors = False - -# Local Extensions ----------------------------------------------------- - -def unwrap_decorators(): - import sphinx.util.inspect as inspect - import functools - - old_getargspec = inspect.getargspec - def getargspec(x): - return old_getargspec(getattr(x, '_original_function', x)) - inspect.getargspec = getargspec - - old_update_wrapper = functools.update_wrapper - def update_wrapper(wrapper, wrapped, *a, **kw): - rv = old_update_wrapper(wrapper, wrapped, *a, **kw) - rv._original_function = wrapped - return rv - functools.update_wrapper = update_wrapper - - -unwrap_decorators() -del unwrap_decorators - - -_internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)', re.M) - - -def skip_internal(app, what, name, obj, skip, options): - docstring = inspect.getdoc(obj) or '' - - if skip or _internal_mark_re.search(docstring) is not None: - return True - - -def cut_module_meta(app, what, name, obj, options, lines): - """Remove metadata from autodoc output.""" - if what != 'module': - return - - lines[:] = [ - line for line in lines - if not line.startswith((':copyright:', ':license:')) - ] - - -def setup(app): - app.connect('autodoc-skip-member', skip_internal) - app.connect('autodoc-process-docstring', cut_module_meta) diff --git a/docs/make.bat b/docs/make.bat index c0f34eb5b..8bf6fb87c 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,95 +1,36 @@ @ECHO OFF +pushd %~dp0 + REM Command file for Sphinx documentation -set SPHINXBUILD=sphinx-build -set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build ) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=Werkzeug if "%1" == "" goto help -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - goto end -) - -if "%1" == "clean" ( - for /d %%i in (_build\*) do rmdir /q /s %%i - del /q /s _build\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html - echo. - echo.Build finished. The HTML pages are in _build/html. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. - echo.Build finished; now you can process the JSON files. - goto end + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 ) -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% _build/htmlhelp - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in _build/htmlhelp. - goto end -) +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% _build/qthelp - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in _build/qthelp, like this: - echo.^> qcollectiongenerator _build\qthelp\Werkzeug.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile _build\qthelp\Werkzeug.ghc - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex - echo. - echo.Build finished; the LaTeX files are in _build/latex. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes - echo. - echo.The overview file is in _build/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% _build/linkcheck - echo. - echo.Link check complete; look for any errors in the above output ^ -or in _build/linkcheck/output.txt. - goto end -) +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end +popd diff --git a/docs/makearchive.py b/docs/makearchive.py deleted file mode 100644 index 62e208e35..000000000 --- a/docs/makearchive.py +++ /dev/null @@ -1,7 +0,0 @@ -import os -import conf -name = "werkzeug-docs-" + conf.version -os.chdir("_build") -os.rename("html", name) -os.system("tar czf %s.tar.gz %s" % (name, name)) -os.rename(name, "html") diff --git a/werkzeug/routing.py b/werkzeug/routing.py index 2e4e2f42c..22db8888a 100644 --- a/werkzeug/routing.py +++ b/werkzeug/routing.py @@ -1143,8 +1143,9 @@ class Map(object): `encoding_errors` and `host_matching` was added. """ + #: A dict of default converters to be used. + #: #: .. versionadded:: 0.6 - #: a dict of default converters to be used. default_converters = ImmutableDict(DEFAULT_CONVERTERS) def __init__(self, rules=None, default_subdomain='', charset='utf-8', From 3306a84445f3c421caf0b2da22278050fdb78bcd Mon Sep 17 00:00:00 2001 From: Rovshan Musayev Date: Tue, 11 Sep 2018 15:54:57 +0200 Subject: [PATCH 082/280] Added http code 424 --- werkzeug/exceptions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index f934de388..9d24f146b 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -520,6 +520,20 @@ class Locked(HTTPException): ) +class Depended(HTTPException): + + """*424* `Failed Dependency` + + Used if the method could not be performed on the resource + because the requested action depended on another action and that action failed. + """ + code = 424 + description = ( + 'The method could not be performed on the resource because the requested action ' + 'depended on another action and that action failed.' + ) + + class PreconditionRequired(HTTPException): """*428* `Precondition Required` From d10649704ac26b6c1667610dd97ff2423967b874 Mon Sep 17 00:00:00 2001 From: Arlington1985 Date: Tue, 11 Sep 2018 20:58:24 +0200 Subject: [PATCH 083/280] Updated class name based on recommendation --- werkzeug/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py index 9d24f146b..456b290dc 100644 --- a/werkzeug/exceptions.py +++ b/werkzeug/exceptions.py @@ -520,7 +520,7 @@ class Locked(HTTPException): ) -class Depended(HTTPException): +class FailedDependency(HTTPException): """*424* `Failed Dependency` From 7f872043bd5c0cd11970b18ee3a1eaf6b48a10a6 Mon Sep 17 00:00:00 2001 From: chengkang <1412950785@qq.com> Date: Wed, 12 Sep 2018 15:08:50 +0800 Subject: [PATCH 084/280] Replace the "ternary operator" (and or) to (if else) 1. (if else) is more readable than (and or). 2. (if else) is faster than (and or). --- examples/cupoftee/pages.py | 2 +- examples/cupoftee/templates/search.html | 2 +- examples/cupoftee/templates/serverlist.html | 2 +- examples/cupoftee/utils.py | 2 +- examples/i18nurls/application.py | 2 +- examples/plnt/sync.py | 2 +- examples/simplewiki/templates/action_edit.html | 6 +++--- examples/simplewiki/templates/action_log.html | 6 +++--- examples/simplewiki/templates/layout.html | 2 +- examples/simplewiki/templates/recent_changes.html | 2 +- examples/simplewiki/utils.py | 4 ++-- werkzeug/contrib/atom.py | 4 ++-- werkzeug/contrib/jsrouting.py | 2 +- werkzeug/contrib/securecookie.py | 2 +- werkzeug/contrib/sessions.py | 2 +- werkzeug/datastructures.py | 6 +++--- werkzeug/debug/__init__.py | 2 +- werkzeug/debug/console.py | 2 +- werkzeug/debug/repr.py | 2 +- werkzeug/debug/tbtools.py | 8 ++++---- werkzeug/routing.py | 8 ++++---- werkzeug/security.py | 2 +- werkzeug/serving.py | 6 +++--- werkzeug/testapp.py | 2 +- werkzeug/urls.py | 2 +- werkzeug/wsgi.py | 2 +- 26 files changed, 42 insertions(+), 42 deletions(-) diff --git a/examples/cupoftee/pages.py b/examples/cupoftee/pages.py index 92b2046b3..8f679d32d 100644 --- a/examples/cupoftee/pages.py +++ b/examples/cupoftee/pages.py @@ -24,7 +24,7 @@ def order_link(self, name, title): desc = False if name == self.order_by: desc = not self.order_desc - cls = ' class="%s"' % (desc and 'down' or 'up') + cls = ' class="%s"' % ('down' if desc else 'up') if desc: link += '&dir=desc' return '%s' % (link, cls, title) diff --git a/examples/cupoftee/templates/search.html b/examples/cupoftee/templates/search.html index 25cfec854..4639790ed 100644 --- a/examples/cupoftee/templates/search.html +++ b/examples/cupoftee/templates/search.html @@ -16,7 +16,7 @@