Skip to content

Commit

Permalink
WIP: typing
Browse files Browse the repository at this point in the history
  • Loading branch information
pajod committed Dec 25, 2023
1 parent f4adf2f commit 0368e0c
Show file tree
Hide file tree
Showing 42 changed files with 1,035 additions and 397 deletions.
32 changes: 26 additions & 6 deletions .github/workflows/buildpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
( cd workaround/ && tar --extract --file gunicorn_21.2.0.orig.tar.gz gunicorn-21.2.0 )
test -s workaround/gunicorn-21.2.0/pyproject.toml
rsync -vrlt source/.github/packaging/debian/ workaround/gunicorn-21.2.0/debian
echo 'extend-diff-ignore = "^setup\.cfg$"' > workaround/gunicorn-21.2.0/debian/source/options
echo 'extend-diff-ignore = "^setup\.cfg$"' >> workaround/gunicorn-21.2.0/debian/source/options
mv --verbose workaround/gunicorn-21.2.0/debian/setup.cfg workaround/gunicorn-21.2.0/
chmod --changes +x workaround/gunicorn-21.2.0/debian/control
ls -l workaround/gunicorn-21.2.0/
Expand Down Expand Up @@ -88,16 +88,36 @@ jobs:
test -s workaround/gunicorn-21.2.0/debian/control
test -d workaround/gunicorn-21.2.0/tests
( cd workaround/gunicorn-21.2.0/ && dpkg-buildpackage --unsigned-source --unsigned-changes )
cp --verbose --archive workaround/*.deb upload/workaround/
# note that Ubuntu 22.04 does not allow zstd in dpkg tools
cp --verbose --archive workaround/*.{deb,tar.gz,tar.xz,tar.zstd,buildinfo,changes,dsc} upload/workaround/
- name: build deb (clean)
continue-on-error: true
run: |
test -s debian/gunicorn-21.2.0/pyproject.toml
test -s debian/gunicorn-21.2.0/debian/control
test -d debian/gunicorn-21.2.0/tests
( cd debian/gunicorn-21.2.0/ && dpkg-buildpackage --unsigned-source --unsigned-changes )
cp --verbose --archive debian/*.deb upload/
- uses: actions/upload-artifact@v3
# note that Ubuntu 22.04 does not allow zstd in dpkg tools
cp --verbose --archive debian/*.{deb,tar.gz,tar.xz,tar.zstd,buildinfo,changes,dsc} upload/
- uses: actions/upload-artifact@v4
with:
path: upload
name: dist
path: |
upload/workaround/*.deb
upload/workaround/*.tar.gz
upload/workaround/*.tar.xz
upload/workaround/*.tar.zstd
upload/workaround/*.buildinfo
upload/workaround/*.changes
upload/workaround/*.dsc
upload/*.deb
upload/*.tar.gz
upload/*.tar.xz
upload/*.tar.zstd
upload/*.buildinfo
upload/*.changes
upload/*.dsc
name: deb
retention-days: 5
# deb and source tarball are already compressed
compression-level: 0
if-no-files-found: error
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ jobs:
- uses: actions/checkout@v4
- name: Using Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
id: setup-python
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install Dependencies
cache-dependency-path: pyproject.toml
- name: Install Dependencies (cache hit: ${{ steps.setup-python.outputs.cache-hit }})
run: |
python -m pip install --upgrade pip
python -m pip install tox
Expand Down
33 changes: 27 additions & 6 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ jobs:
os: [ubuntu-latest, macos-latest]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10" ]
unsupported: [false]
mindep: [false]
include:
- os: ubuntu-20.04
python-version: "3.8"
mindep: true
- os: ubuntu-latest
python-version: "3.13"
unsupported: true
- os: windows-latest
python-version: "3.12"
unsupported: true
Expand All @@ -41,18 +48,26 @@ jobs:
- uses: actions/checkout@v4
- name: Using Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
id: "setup-python"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: ${{ matrix.unsupported }}
cache: pip
cache-dependency-path: requirements_test.txt
check-latest: true
- name: Install test Dependencies
cache-dependency-path: pyproject.toml
- name: Install test Dependencies (cache hit: ${{ steps.setup-python.outputs.cache-hit }})
run: |
python -m pip install --upgrade pip
python -m pip install tox
- run: tox -e run-module
timeout-minutes: 2
- run: tox -e run-entrypoint
timeout-minutes: 2
- run: tox -e py
timeout-minutes: 5
continue-on-error: ${{ matrix.unsupported }}
- if: ${{ mindep }}
timeout-minutes: 8
run: tox -e py-mindep
continue-on-error: ${{ matrix.unsupported }}
- name: Install dist Dependencies
run: |
Expand All @@ -69,7 +84,13 @@ jobs:
# windows doe not do --archive --verbose on cp
cp dist/*.tar.gz upload/
cp dist/*.whl upload/
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
path: upload
name: dist
path: |
upload/*.tar.gz
upload/*.whl
name: dist-${{ matrix.os }}-${{ matrix.python-version }}
retention-days: 5
# sdist.tar.gz and bdist.whl are compressed by default
compression-level: 0
if-no-files-found: error
2 changes: 1 addition & 1 deletion gunicorn/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from _typeshed import Incomplete

version_info: Incomplete
version_info: tuple[int, int, int]
__version__: str
SERVER: str
SERVER_SOFTWARE: str
10 changes: 8 additions & 2 deletions gunicorn/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import os
import sys
import traceback
import logging

from gunicorn import util
from gunicorn.arbiter import Arbiter
from gunicorn.config import Config, get_default_config_file
from gunicorn import debug


logger = logging.getLogger(__name__)

class BaseApplication:
"""
An application interface for configuring and loading
Expand Down Expand Up @@ -101,8 +104,11 @@ def get_config_from_filename(self, filename):
if ext in [".py", ".pyc"]:
spec = importlib.util.spec_from_file_location(module_name, filename)
else:
msg = "configuration file should have a valid Python extension.\n"
util.warn(msg)
if filename == os.devnull:
logger.debug("configuration file %r path matches os.devnull", filename)
else:
msg = "configuration file should have a valid Python extension.\n"
util.warn(msg)
loader_ = importlib.machinery.SourceFileLoader(module_name, filename)
spec = importlib.util.spec_from_file_location(module_name, filename, loader=loader_)
mod = importlib.util.module_from_spec(spec)
Expand Down
31 changes: 18 additions & 13 deletions gunicorn/app/base.pyi
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
from argparse import ArgumentParser, Namespace

from _typeshed import Incomplete
from _typeshed.wsgi import WSGIApplication as _WSGIApplication

from gunicorn import debug as debug
from gunicorn import util as util
from gunicorn.arbiter import Arbiter as Arbiter
from gunicorn.config import Config as Config
from gunicorn.config import Config
from gunicorn.config import get_default_config_file as get_default_config_file

# from abc import abstractmethod, ABCMeta

class BaseApplication:
usage: Incomplete
cfg: Incomplete
callable: Incomplete
prog: Incomplete
usage: str | None
cfg: Config
callable: None | _WSGIApplication
prog: str | None
logger: Incomplete
def __init__(self, usage: Incomplete | None = ..., prog: Incomplete | None = ...) -> None: ...
def __init__(self, usage: str | None = ..., prog: str | None = ...) -> None: ...
def do_load_config(self) -> None: ...
def load_default_config(self) -> None: ...
def init(self, parser, opts, args) -> None: ...
def load(self) -> None: ...
def init(self, parser: ArgumentParser, opts: Namespace, args: list[str]) -> None: ...
def load(self) -> _WSGIApplication: ...
def load_config(self) -> None: ...
def reload(self) -> None: ...
def wsgi(self): ...
def wsgi(self) -> _WSGIApplication: ...
def run(self) -> None: ...

class Application(BaseApplication):
def chdir(self) -> None: ...
def get_config_from_filename(self, filename): ...
def get_config_from_module_name(self, module_name): ...
def load_config_from_module_name_or_filename(self, location): ...
def load_config_from_file(self, filename): ...
def get_config_from_filename(self, filename: str) -> Config: ...
def get_config_from_module_name(self, module_name: str) -> Config: ...
def load_config_from_module_name_or_filename(self, location: str) -> Config: ...
def load_config_from_file(self, filename: str) -> Config: ...
def load_config(self) -> None: ...
def run(self) -> None: ...
9 changes: 6 additions & 3 deletions gunicorn/app/pasterapp.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from _typeshed import Incomplete
from _typeshed.wsgi import WSGIApplication as _WSGIApplication

from gunicorn.app.wsgiapp import WSGIApplication as WSGIApplication
from gunicorn.config import get_default_config_file as get_default_config_file

def get_wsgi_app(config_uri, name: Incomplete | None = ..., defaults: Incomplete | None = ...): ...
def has_logging_config(config_file): ...
def serve(app, global_conf, **local_conf): ...
def get_wsgi_app(
config_uri: str, name: Incomplete | None = ..., defaults: Incomplete | None = ...
) -> _WSGIApplication: ...
def has_logging_config(config_file: str) -> bool: ...
def serve(app: _WSGIApplication, global_conf: dict[str, Incomplete], **local_conf: Incomplete) -> None: ...
13 changes: 8 additions & 5 deletions gunicorn/app/wsgiapp.pyi
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from argparse import ArgumentParser, Namespace

from _typeshed import Incomplete
from _typeshed.wsgi import WSGIApplication as _WSGIApplication

from gunicorn import util as util
from gunicorn.app.base import Application as Application
from gunicorn.errors import ConfigError as ConfigError

class WSGIApplication(Application):
app_uri: Incomplete
def init(self, parser, opts, args) -> None: ...
def init(self, parser: ArgumentParser, opts: Namespace, args: list[str]) -> None: ...
def load_config(self) -> None: ...
def load_wsgiapp(self): ...
def load_pasteapp(self): ...
def load(self): ...
def load_wsgiapp(self) -> _WSGIApplication: ...
def load_pasteapp(self) -> _WSGIApplication: ...
def load(self) -> _WSGIApplication: ...

def run() -> None: ...
def run(prog: str | None = ...) -> None: ...
17 changes: 6 additions & 11 deletions gunicorn/arbiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,12 @@ class Arbiter:

# I love dynamic languages
SIG_QUEUE = []
_want_signals_unix = set("HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH".split())
SIGNALS = [getattr(_signal, "SIG%s" % x)
for x in "HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH".split()]
SIG_NAMES = dict(
(getattr(_signal, name), name[3:]) for name in dir(_signal)
(getattr(_signal, name), name[3:].lower()) for name in dir(_signal)
if name[:3] == "SIG" and name[3] != "_"
)
# FIXME: nonsense. just testing some unrelated stuff that IS available in wondows
SIGNALS = {getattr(_signal, "SIG%s" % x, None) for x in _want_signals_unix}
SIGNALS = [sig for sig in SIGNALS if sig is not None]

# FIXME: nonsense. just testing some unrelated stuff that IS available in wondows
SIGKILL = getattr(_signal, "SIGKILL", _signal.SIGTERM)

def __init__(self, app):
os.environ["SERVER_SOFTWARE"] = SERVER_SOFTWARE
Expand Down Expand Up @@ -396,7 +391,7 @@ def stop(self, graceful=True):
while self.WORKERS and time.time() < limit:
time.sleep(0.1)

self.kill_workers(self.SIGKILL)
self.kill_workers(_signal.SIGKILL)

def reexec(self):
"""\
Expand Down Expand Up @@ -506,7 +501,7 @@ def murder_workers(self):
worker.aborted = True
self.kill_worker(pid, _signal.SIGABRT)
else:
self.kill_worker(pid, self.SIGKILL)
self.kill_worker(pid, _signal.SIGKILL)

def reap_workers(self):
"""\
Expand Down Expand Up @@ -550,7 +545,7 @@ def reap_workers(self):
wpid, sig_name)

# Additional hint for SIGKILL
if status == self.SIGKILL:
if status == _signal.SIGKILL:
msg += " Perhaps out of memory?"
self.log.error(msg)

Expand Down
41 changes: 41 additions & 0 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2378,3 +2378,44 @@ class TolerateDangerousFraming(Setting):
.. versionadded:: 22.0.0
"""

def validate_fatal_behaviour(val):
# FIXME: refactor all of this subclassing stdlib argparse

if val is None:
return

if not isinstance(val, str):
raise TypeError("Invalid type for casting: %s" % val)
if val.lower().strip() == "world-readable":
return "world-readable"
elif val.lower().strip() == "refuse":
return "refuse"
elif val.lower().strip() == "quiet":
return "quiet"
elif val.lower().strip() == "guess":
return "guess"
else:
raise ValueError("Invalid header map behaviour: %s" % val)


class OnFatal(Setting):
name = "on_fatal"
section = "Server Mechanics"
cli = ["--on-fatal"]
validator = validate_fatal_behaviour
default = "guess"
desc = """\
Configure what to do if loading the application failes
If set to ``world-readable``, always send the traceback too the client.
The defaule behaviour ``guess`` is to share the traceback with the world
if the reloader is enabled. If set to ``refuse``, stop processing requests.
If set to ``quiet``, respond with error status but do not share internals.
The behaviour of ``world-readable`` (or, by extension ``guess``) risks exposing
sensitive data and is not recommended for production use.
.. versionadded:: 22.0.0
"""
13 changes: 10 additions & 3 deletions gunicorn/debug.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from collections.abc import Container
from types import FrameType
from typing import Any, Literal

from _typeshed import Incomplete
from typing_extensions import Self

class Spew:
trace_names: Incomplete
show_values: Incomplete
def __init__(self, trace_names: Incomplete | None = ..., show_values: bool = ...) -> None: ...
def __call__(self, frame, event, arg): ...
def __init__(self, trace_names: Container[str] | None = ..., show_values: bool = ...) -> None: ...
def __call__(
self, frame: FrameType, event: Literal["call", "line", "return", "exception", "opcode"], arg: Any
) -> Self: ...

def spew(trace_names: Incomplete | None = ..., show_values: bool = ...) -> None: ...
def spew(trace_names: Container[str] | None = ..., show_values: bool = ...) -> None: ...
def unspew() -> None: ...
4 changes: 2 additions & 2 deletions gunicorn/errors.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from _typeshed import Incomplete

class HaltServer(BaseException):
reason: Incomplete
exit_status: Incomplete
reason: str
exit_status: int
def __init__(self, reason: str, exit_status: int = ...) -> None: ...

class ConfigError(Exception): ...
Expand Down
Loading

0 comments on commit 0368e0c

Please sign in to comment.