diff --git a/.travis.yml b/.travis.yml index 97c578aa..1d569a76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ matrix: env: TOXENV=py37 dist: xenial sudo: true + - python: pypy3 + env: TOXENV=pypy3 + dist: xenial - python: 3.8-dev env: TOXENV=py38-dev dist: xenial diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a6880bb..7bd82abd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,7 @@ The relevant maintainer for a pull request is assigned in 3 steps: * Step 2: Find the MAINTAINERS file which affects this directory. If the directory itself does not have a MAINTAINERS file, work your way up the the repo hierarchy until you find one. -* Step 3: The first maintainer listed is the primary maintainer. The pull request is assigned to him. He may assign it to other listed maintainers, at his discretion. +* Step 3: The first maintainer listed is the primary maintainer who is assigned the Pull Request. The primary maintainer can reassign a Pull Request to other listed maintainers. ### I'm a maintainer, should I make pull requests too? diff --git a/README.rst b/README.rst index 6b9bcaf1..c9e3ebdf 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,12 @@ Example with test app:: $ gunicorn --workers=2 test:app +Contributing +------------ + +See `our complete contributor's guide `_ for more details. + + License ------- diff --git a/docs/source/custom.rst b/docs/source/custom.rst index 0b8c366c..0fb39250 100644 --- a/docs/source/custom.rst +++ b/docs/source/custom.rst @@ -13,7 +13,8 @@ Here is a small example where we create a very small WSGI app and load it with a custom Application: .. literalinclude:: ../../examples/standalone_app.py - :lines: 11-60 + :start-after: # See the NOTICE for more information + :lines: 2- Direct Usage of Existing WSGI Apps ---------------------------------- diff --git a/docs/source/install.rst b/docs/source/install.rst index 3002a611..d6d146d2 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -52,6 +52,28 @@ want to consider one of the alternate worker types. installed, this is the most likely reason. +Extra Packages +============== +Some Gunicorn options require additional packages. You can use the ``[extra]`` +syntax to install these at the same time as Gunicorn. + +Most extra packages are needed for alternate worker types. See the +`design docs`_ for more information on when you'll want to consider an +alternate worker type. + +* ``gunicorn[eventlet]`` - Eventlet-based greenlets workers +* ``gunicorn[gevent]`` - Gevent-based greenlets workers +* ``gunicorn[gthread]`` - Threaded workers +* ``gunicorn[tornado]`` - Tornado-based workers, not recommended + +If you are running more than one instance of Gunicorn, the :ref:`proc-name` +setting will help distinguish between them in tools like ``ps`` and ``top``. + +* ``gunicorn[setproctitle]`` - Enables setting the process name + +Multiple extras can be combined, like +``pip install gunicorn[gevent,setproctitle]``. + Debian GNU/Linux ================ diff --git a/examples/echo.py b/examples/echo.py index 06f61602..e10332d8 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -5,12 +5,9 @@ # # Example code from Eventlet sources -from wsgiref.validate import validator - from gunicorn import __version__ -@validator def app(environ, start_response): """Simplest possible application object""" diff --git a/gunicorn/__init__.py b/gunicorn/__init__.py index 78204797..7b38ab04 100644 --- a/gunicorn/__init__.py +++ b/gunicorn/__init__.py @@ -3,6 +3,6 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. -version_info = (19, 9, 0) +version_info = (20, 0, 0) __version__ = ".".join([str(v) for v in version_info]) SERVER_SOFTWARE = "gunicorn/%s" % __version__ diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index 7eaa2c17..bca671d1 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -223,9 +223,7 @@ class Arbiter(object): self.log.info("Handling signal: %s", signame) handler() self.wakeup() - except StopIteration: - self.halt() - except KeyboardInterrupt: + except (StopIteration, KeyboardInterrupt): self.halt() except HaltServer as inst: self.halt(reason=inst.reason, exit_status=inst.exit_status) diff --git a/gunicorn/glogging.py b/gunicorn/glogging.py index 3f7b4ac7..a096f967 100644 --- a/gunicorn/glogging.py +++ b/gunicorn/glogging.py @@ -445,7 +445,7 @@ class Logger(object): def _get_user(self, environ): user = None http_auth = environ.get("HTTP_AUTHORIZATION") - if http_auth and http_auth.startswith('Basic'): + if http_auth and http_auth.lower().startswith('basic'): auth = http_auth.split(" ", 1) if len(auth) == 2: try: diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py index e75d72de..afde3685 100644 --- a/gunicorn/http/body.py +++ b/gunicorn/http/body.py @@ -7,7 +7,7 @@ import io import sys from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, - InvalidChunkSize) + InvalidChunkSize) class ChunkedReader(object): @@ -187,6 +187,7 @@ class Body(object): if not ret: raise StopIteration() return ret + next = __next__ def getsize(self, size): diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index 32e7a2ac..3524471f 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -253,10 +253,13 @@ class Response(object): if HEADER_RE.search(name): raise InvalidHeaderName('%r' % name) + if not isinstance(value, str): + raise TypeError('%r is not a string' % value) + if HEADER_VALUE_RE.search(value): raise InvalidHeader('%r' % value) - value = str(value).strip() + value = value.strip() lname = name.lower().strip() if lname == "content-length": self.response_length = int(value) diff --git a/gunicorn/workers/workertmp.py b/gunicorn/workers/workertmp.py index 22aaef34..a37ed155 100644 --- a/gunicorn/workers/workertmp.py +++ b/gunicorn/workers/workertmp.py @@ -21,11 +21,13 @@ class WorkerTmp(object): if fdir and not os.path.isdir(fdir): raise RuntimeError("%s doesn't exist. Can't create workertmp." % fdir) fd, name = tempfile.mkstemp(prefix="wgunicorn-", dir=fdir) - - # allows the process to write to the file - util.chown(name, cfg.uid, cfg.gid) os.umask(old_umask) + # change the owner and group of the file if the worker will run as + # a different user or group, so that the worker can modify the file + if cfg.uid != os.geteuid() or cfg.gid != os.getegid(): + util.chown(name, cfg.uid, cfg.gid) + # unlink the file so we don't leak tempory files try: if not IS_CYGWIN: diff --git a/setup.py b/setup.py index ee898d88..8d79fb7a 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,8 @@ CLASSIFIERS = [ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet', 'Topic :: Utilities', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -78,6 +80,7 @@ extras_require = { 'eventlet': ['eventlet>=0.9.7'], 'tornado': ['tornado>=0.2'], 'gthread': [], + 'setproctitle': ['setproctitle'], } setup( diff --git a/tests/test_logger.py b/tests/test_logger.py index 5b8c0d42..54801266 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,6 +1,8 @@ import datetime from types import SimpleNamespace +import pytest + from gunicorn.config import Config from gunicorn.glogging import Logger @@ -47,7 +49,13 @@ def test_atoms_zero_bytes(): assert atoms['B'] == 0 -def test_get_username_from_basic_auth_header(): +@pytest.mark.parametrize('auth', [ + # auth type is case in-sensitive + 'Basic YnJrMHY6', + 'basic YnJrMHY6', + 'BASIC YnJrMHY6', +]) +def test_get_username_from_basic_auth_header(auth): request = SimpleNamespace(headers=()) response = SimpleNamespace( status='200', response_length=1024, sent=1024, @@ -57,7 +65,7 @@ def test_get_username_from_basic_auth_header(): 'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar', 'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar', 'SERVER_PROTOCOL': 'HTTP/1.1', - 'HTTP_AUTHORIZATION': 'Basic YnJrMHY6', + 'HTTP_AUTHORIZATION': auth, } logger = Logger(Config()) atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1)) diff --git a/tests/test_util.py b/tests/test_util.py index 3b8688a2..2494d2c5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -29,15 +29,15 @@ def test_parse_address(test_input, expected): def test_parse_address_invalid(): - with pytest.raises(RuntimeError) as err: + with pytest.raises(RuntimeError) as exc_info: util.parse_address('127.0.0.1:test') - assert "'test' is not a valid port number." in str(err) + assert "'test' is not a valid port number." in str(exc_info.value) def test_parse_fd_invalid(): - with pytest.raises(RuntimeError) as err: + with pytest.raises(RuntimeError) as exc_info: util.parse_address('fd://asd') - assert "'asd' is not a valid file descriptor." in str(err) + assert "'asd' is not a valid file descriptor." in str(exc_info.value) def test_http_date(): @@ -63,24 +63,24 @@ def test_warn(capsys): def test_import_app(): assert util.import_app('support:app') - with pytest.raises(ImportError) as err: + with pytest.raises(ImportError) as exc_info: util.import_app('a:app') - assert 'No module' in str(err) + assert 'No module' in str(exc_info.value) - with pytest.raises(AppImportError) as err: + with pytest.raises(AppImportError) as exc_info: util.import_app('support:wrong_app') msg = "Failed to find application object 'wrong_app' in 'support'" - assert msg in str(err) + assert msg in str(exc_info.value) def test_to_bytestring(): assert util.to_bytestring('test_str', 'ascii') == b'test_str' assert util.to_bytestring('test_strĀ®') == b'test_str\xc2\xae' assert util.to_bytestring(b'byte_test_str') == b'byte_test_str' - with pytest.raises(TypeError) as err: + with pytest.raises(TypeError) as exc_info: util.to_bytestring(100) msg = '100 is not a string' - assert msg in str(err) + assert msg in str(exc_info.value) @pytest.mark.parametrize('test_input, expected', [ diff --git a/tox.ini b/tox.ini index 47249d6e..96388fa7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, py36, py37, py38-dev, pypy, lint +envlist = py34, py35, py36, py37, py38-dev, pypy3, lint skipsdist = True [testenv]