Merge branch 'nightly' into ntfy_notifier

This commit is contained in:
Nate Harris 2024-06-19 07:17:21 +00:00 committed by GitHub
commit a4ea0249fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 2403 additions and 1344 deletions

View file

@ -70,7 +70,7 @@ jobs:
password: ${{ secrets.GHCR_TOKEN }} password: ${{ secrets.GHCR_TOKEN }}
- name: Docker Build and Push - name: Docker Build and Push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: success() if: success()
with: with:
context: . context: .

View file

@ -13,8 +13,6 @@ DOCUMENTATION :: END
import datetime import datetime
import plexpy import plexpy
from plexpy import common, helpers from plexpy import common, helpers
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
%> %>
<table class="config-scheduler-table small-muted"> <table class="config-scheduler-table small-muted">
@ -29,16 +27,15 @@ DOCUMENTATION :: END
</thead> </thead>
<tbody> <tbody>
% for job, job_type in common.SCHEDULER_LIST.items(): % for job, job_type in common.SCHEDULER_LIST.items():
% if job in scheduled_jobs:
<% <%
sched_job = plexpy.SCHED.get_job(job) sched_job = plexpy.SCHED.get_job(job)
now = datetime.datetime.now(sched_job.next_run_time.tzinfo)
%> %>
% if sched_job:
<tr> <tr>
<td>${sched_job.id}</td> <td>${sched_job.id}</td>
<td><i class="fa fa-sm fa-fw fa-check"></i> Active</td> <td><i class="fa fa-sm fa-fw fa-check"></i> Active</td>
<td>${helpers.format_timedelta_Hms(sched_job.trigger.interval)}</td> <td>${helpers.format_timedelta_Hms(sched_job.trigger.interval)}</td>
<td>${helpers.format_timedelta_Hms(sched_job.next_run_time - now)}</td> <td>${helpers.format_timedelta_Hms(sched_job.next_run_time - datetime.datetime.now(sched_job.next_run_time.tzinfo))}</td>
<td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td> <td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td>
</tr> </tr>
% elif job_type == 'websocket' and plexpy.WS_CONNECTED: % elif job_type == 'websocket' and plexpy.WS_CONNECTED:

View file

@ -1 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__) __path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
#------------------------------------------------------------------- #-------------------------------------------------------------------
# tarfile.py # tarfile.py
#------------------------------------------------------------------- #-------------------------------------------------------------------
@ -46,7 +45,6 @@ import time
import struct import struct
import copy import copy
import re import re
import warnings
from .compat.py38 import removesuffix from .compat.py38 import removesuffix
@ -639,6 +637,10 @@ class _FileInFile(object):
def flush(self): def flush(self):
pass pass
@property
def mode(self):
return 'rb'
def readable(self): def readable(self):
return True return True
@ -875,7 +877,7 @@ class TarInfo(object):
pax_headers = ('A dictionary containing key-value pairs of an ' pax_headers = ('A dictionary containing key-value pairs of an '
'associated pax extended header.'), 'associated pax extended header.'),
sparse = 'Sparse member information.', sparse = 'Sparse member information.',
tarfile = None, _tarfile = None,
_sparse_structs = None, _sparse_structs = None,
_link_target = None, _link_target = None,
) )
@ -904,6 +906,24 @@ class TarInfo(object):
self.sparse = None # sparse member information self.sparse = None # sparse member information
self.pax_headers = {} # pax header information self.pax_headers = {} # pax header information
@property
def tarfile(self):
import warnings
warnings.warn(
'The undocumented "tarfile" attribute of TarInfo objects '
+ 'is deprecated and will be removed in Python 3.16',
DeprecationWarning, stacklevel=2)
return self._tarfile
@tarfile.setter
def tarfile(self, tarfile):
import warnings
warnings.warn(
'The undocumented "tarfile" attribute of TarInfo objects '
+ 'is deprecated and will be removed in Python 3.16',
DeprecationWarning, stacklevel=2)
self._tarfile = tarfile
@property @property
def path(self): def path(self):
'In pax headers, "name" is called "path".' 'In pax headers, "name" is called "path".'
@ -1198,7 +1218,7 @@ class TarInfo(object):
for keyword, value in pax_headers.items(): for keyword, value in pax_headers.items():
keyword = keyword.encode("utf-8") keyword = keyword.encode("utf-8")
if binary: if binary:
# Try to restore the original byte representation of `value'. # Try to restore the original byte representation of 'value'.
# Needless to say, that the encoding must match the string. # Needless to say, that the encoding must match the string.
value = value.encode(encoding, "surrogateescape") value = value.encode(encoding, "surrogateescape")
else: else:
@ -1643,14 +1663,14 @@ class TarFile(object):
def __init__(self, name=None, mode="r", fileobj=None, format=None, def __init__(self, name=None, mode="r", fileobj=None, format=None,
tarinfo=None, dereference=None, ignore_zeros=None, encoding=None, tarinfo=None, dereference=None, ignore_zeros=None, encoding=None,
errors="surrogateescape", pax_headers=None, debug=None, errors="surrogateescape", pax_headers=None, debug=None,
errorlevel=None, copybufsize=None): errorlevel=None, copybufsize=None, stream=False):
"""Open an (uncompressed) tar archive `name'. `mode' is either 'r' to """Open an (uncompressed) tar archive 'name'. 'mode' is either 'r' to
read from an existing archive, 'a' to append data to an existing read from an existing archive, 'a' to append data to an existing
file or 'w' to create a new file overwriting an existing one. `mode' file or 'w' to create a new file overwriting an existing one. 'mode'
defaults to 'r'. defaults to 'r'.
If `fileobj' is given, it is used for reading or writing data. If it If 'fileobj' is given, it is used for reading or writing data. If it
can be determined, `mode' is overridden by `fileobj's mode. can be determined, 'mode' is overridden by 'fileobj's mode.
`fileobj' is not closed, when TarFile is closed. 'fileobj' is not closed, when TarFile is closed.
""" """
modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"} modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"}
if mode not in modes: if mode not in modes:
@ -1675,6 +1695,8 @@ class TarFile(object):
self.name = os.path.abspath(name) if name else None self.name = os.path.abspath(name) if name else None
self.fileobj = fileobj self.fileobj = fileobj
self.stream = stream
# Init attributes. # Init attributes.
if format is not None: if format is not None:
self.format = format self.format = format
@ -1977,7 +1999,7 @@ class TarFile(object):
self.fileobj.close() self.fileobj.close()
def getmember(self, name): def getmember(self, name):
"""Return a TarInfo object for member ``name``. If ``name`` can not be """Return a TarInfo object for member 'name'. If 'name' can not be
found in the archive, KeyError is raised. If a member occurs more found in the archive, KeyError is raised. If a member occurs more
than once in the archive, its last occurrence is assumed to be the than once in the archive, its last occurrence is assumed to be the
most up-to-date version. most up-to-date version.
@ -2005,9 +2027,9 @@ class TarFile(object):
def gettarinfo(self, name=None, arcname=None, fileobj=None): def gettarinfo(self, name=None, arcname=None, fileobj=None):
"""Create a TarInfo object from the result of os.stat or equivalent """Create a TarInfo object from the result of os.stat or equivalent
on an existing file. The file is either named by ``name``, or on an existing file. The file is either named by 'name', or
specified as a file object ``fileobj`` with a file descriptor. If specified as a file object 'fileobj' with a file descriptor. If
given, ``arcname`` specifies an alternative name for the file in the given, 'arcname' specifies an alternative name for the file in the
archive, otherwise, the name is taken from the 'name' attribute of archive, otherwise, the name is taken from the 'name' attribute of
'fileobj', or the 'name' argument. The name should be a text 'fileobj', or the 'name' argument. The name should be a text
string. string.
@ -2031,7 +2053,7 @@ class TarFile(object):
# Now, fill the TarInfo object with # Now, fill the TarInfo object with
# information specific for the file. # information specific for the file.
tarinfo = self.tarinfo() tarinfo = self.tarinfo()
tarinfo.tarfile = self # Not needed tarinfo._tarfile = self # To be removed in 3.16.
# Use os.stat or os.lstat, depending on if symlinks shall be resolved. # Use os.stat or os.lstat, depending on if symlinks shall be resolved.
if fileobj is None: if fileobj is None:
@ -2103,11 +2125,15 @@ class TarFile(object):
return tarinfo return tarinfo
def list(self, verbose=True, *, members=None): def list(self, verbose=True, *, members=None):
"""Print a table of contents to sys.stdout. If ``verbose`` is False, only """Print a table of contents to sys.stdout. If 'verbose' is False, only
the names of the members are printed. If it is True, an `ls -l'-like the names of the members are printed. If it is True, an 'ls -l'-like
output is produced. ``members`` is optional and must be a subset of the output is produced. 'members' is optional and must be a subset of the
list returned by getmembers(). list returned by getmembers().
""" """
# Convert tarinfo type to stat type.
type2mode = {REGTYPE: stat.S_IFREG, SYMTYPE: stat.S_IFLNK,
FIFOTYPE: stat.S_IFIFO, CHRTYPE: stat.S_IFCHR,
DIRTYPE: stat.S_IFDIR, BLKTYPE: stat.S_IFBLK}
self._check() self._check()
if members is None: if members is None:
@ -2117,7 +2143,8 @@ class TarFile(object):
if tarinfo.mode is None: if tarinfo.mode is None:
_safe_print("??????????") _safe_print("??????????")
else: else:
_safe_print(stat.filemode(tarinfo.mode)) modetype = type2mode.get(tarinfo.type, 0)
_safe_print(stat.filemode(modetype | tarinfo.mode))
_safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid, _safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid,
tarinfo.gname or tarinfo.gid)) tarinfo.gname or tarinfo.gid))
if tarinfo.ischr() or tarinfo.isblk(): if tarinfo.ischr() or tarinfo.isblk():
@ -2141,11 +2168,11 @@ class TarFile(object):
print() print()
def add(self, name, arcname=None, recursive=True, *, filter=None): def add(self, name, arcname=None, recursive=True, *, filter=None):
"""Add the file ``name`` to the archive. ``name`` may be any type of file """Add the file 'name' to the archive. 'name' may be any type of file
(directory, fifo, symbolic link, etc.). If given, ``arcname`` (directory, fifo, symbolic link, etc.). If given, 'arcname'
specifies an alternative name for the file in the archive. specifies an alternative name for the file in the archive.
Directories are added recursively by default. This can be avoided by Directories are added recursively by default. This can be avoided by
setting ``recursive`` to False. ``filter`` is a function setting 'recursive' to False. 'filter' is a function
that expects a TarInfo object argument and returns the changed that expects a TarInfo object argument and returns the changed
TarInfo object, if it returns None the TarInfo object will be TarInfo object, if it returns None the TarInfo object will be
excluded from the archive. excluded from the archive.
@ -2192,13 +2219,16 @@ class TarFile(object):
self.addfile(tarinfo) self.addfile(tarinfo)
def addfile(self, tarinfo, fileobj=None): def addfile(self, tarinfo, fileobj=None):
"""Add the TarInfo object ``tarinfo`` to the archive. If ``fileobj`` is """Add the TarInfo object 'tarinfo' to the archive. If 'tarinfo' represents
given, it should be a binary file, and tarinfo.size bytes are read a non zero-size regular file, the 'fileobj' argument should be a binary file,
from it and added to the archive. You can create TarInfo objects and tarinfo.size bytes are read from it and added to the archive.
directly, or by using gettarinfo(). You can create TarInfo objects directly, or by using gettarinfo().
""" """
self._check("awx") self._check("awx")
if fileobj is None and tarinfo.isreg() and tarinfo.size != 0:
raise ValueError("fileobj not provided for non zero-size regular file")
tarinfo = copy.copy(tarinfo) tarinfo = copy.copy(tarinfo)
buf = tarinfo.tobuf(self.format, self.encoding, self.errors) buf = tarinfo.tobuf(self.format, self.encoding, self.errors)
@ -2220,11 +2250,12 @@ class TarFile(object):
if filter is None: if filter is None:
filter = self.extraction_filter filter = self.extraction_filter
if filter is None: if filter is None:
import warnings
warnings.warn( warnings.warn(
'Python 3.14 will, by default, filter extracted tar ' 'Python 3.14 will, by default, filter extracted tar '
+ 'archives and reject files or modify their metadata. ' + 'archives and reject files or modify their metadata. '
+ 'Use the filter argument to control this behavior.', + 'Use the filter argument to control this behavior.',
DeprecationWarning) DeprecationWarning, stacklevel=3)
return fully_trusted_filter return fully_trusted_filter
if isinstance(filter, str): if isinstance(filter, str):
raise TypeError( raise TypeError(
@ -2243,12 +2274,12 @@ class TarFile(object):
filter=None): filter=None):
"""Extract all members from the archive to the current working """Extract all members from the archive to the current working
directory and set owner, modification time and permissions on directory and set owner, modification time and permissions on
directories afterwards. `path' specifies a different directory directories afterwards. 'path' specifies a different directory
to extract to. `members' is optional and must be a subset of the to extract to. 'members' is optional and must be a subset of the
list returned by getmembers(). If `numeric_owner` is True, only list returned by getmembers(). If 'numeric_owner' is True, only
the numbers for user/group names are used and not the names. the numbers for user/group names are used and not the names.
The `filter` function will be called on each member just The 'filter' function will be called on each member just
before extraction. before extraction.
It can return a changed TarInfo or None to skip the member. It can return a changed TarInfo or None to skip the member.
String names of common filters are accepted. String names of common filters are accepted.
@ -2288,13 +2319,13 @@ class TarFile(object):
filter=None): filter=None):
"""Extract a member from the archive to the current working directory, """Extract a member from the archive to the current working directory,
using its full name. Its file information is extracted as accurately using its full name. Its file information is extracted as accurately
as possible. `member' may be a filename or a TarInfo object. You can as possible. 'member' may be a filename or a TarInfo object. You can
specify a different directory using `path'. File attributes (owner, specify a different directory using 'path'. File attributes (owner,
mtime, mode) are set unless `set_attrs' is False. If `numeric_owner` mtime, mode) are set unless 'set_attrs' is False. If 'numeric_owner'
is True, only the numbers for user/group names are used and not is True, only the numbers for user/group names are used and not
the names. the names.
The `filter` function will be called before extraction. The 'filter' function will be called before extraction.
It can return a changed TarInfo or None to skip the member. It can return a changed TarInfo or None to skip the member.
String names of common filters are accepted. String names of common filters are accepted.
""" """
@ -2359,10 +2390,10 @@ class TarFile(object):
self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e)) self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e))
def extractfile(self, member): def extractfile(self, member):
"""Extract a member from the archive as a file object. ``member`` may be """Extract a member from the archive as a file object. 'member' may be
a filename or a TarInfo object. If ``member`` is a regular file or a filename or a TarInfo object. If 'member' is a regular file or
a link, an io.BufferedReader object is returned. For all other a link, an io.BufferedReader object is returned. For all other
existing members, None is returned. If ``member`` does not appear existing members, None is returned. If 'member' does not appear
in the archive, KeyError is raised. in the archive, KeyError is raised.
""" """
self._check("r") self._check("r")
@ -2406,7 +2437,7 @@ class TarFile(object):
if upperdirs and not os.path.exists(upperdirs): if upperdirs and not os.path.exists(upperdirs):
# Create directories that are not part of the archive with # Create directories that are not part of the archive with
# default permissions. # default permissions.
os.makedirs(upperdirs) os.makedirs(upperdirs, exist_ok=True)
if tarinfo.islnk() or tarinfo.issym(): if tarinfo.islnk() or tarinfo.issym():
self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname)) self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname))
@ -2559,7 +2590,8 @@ class TarFile(object):
os.lchown(targetpath, u, g) os.lchown(targetpath, u, g)
else: else:
os.chown(targetpath, u, g) os.chown(targetpath, u, g)
except OSError as e: except (OSError, OverflowError) as e:
# OverflowError can be raised if an ID doesn't fit in 'id_t'
raise ExtractError("could not change owner") from e raise ExtractError("could not change owner") from e
def chmod(self, tarinfo, targetpath): def chmod(self, tarinfo, targetpath):
@ -2642,7 +2674,9 @@ class TarFile(object):
break break
if tarinfo is not None: if tarinfo is not None:
self.members.append(tarinfo) # if streaming the file we do not want to cache the tarinfo
if not self.stream:
self.members.append(tarinfo)
else: else:
self._loaded = True self._loaded = True
@ -2693,11 +2727,12 @@ class TarFile(object):
def _load(self): def _load(self):
"""Read through the entire archive file and look for readable """Read through the entire archive file and look for readable
members. members. This should not run if the file is set to stream.
""" """
while self.next() is not None: if not self.stream:
pass while self.next() is not None:
self._loaded = True pass
self._loaded = True
def _check(self, mode=None): def _check(self, mode=None):
"""Check if TarFile is still open, and if the operation's mode """Check if TarFile is still open, and if the operation's mode

View file

@ -1,4 +1,4 @@
from .core import contents, where from .core import contents, where
__all__ = ["contents", "where"] __all__ = ["contents", "where"]
__version__ = "2024.02.02" __version__ = "2024.06.02"

View file

@ -4812,3 +4812,27 @@ X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q
ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm
dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= dTdmQRCsu/WU48IxK63nI1bMNSWSs1A=
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA
# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA
# Label: "FIRMAPROFESIONAL CA ROOT-A WEB"
# Serial: 65916896770016886708751106294915943533
# MD5 Fingerprint: 82:b2:ad:45:00:82:b0:66:63:f8:5f:c3:67:4e:ce:a3
# SHA1 Fingerprint: a8:31:11:74:a6:14:15:0d:ca:77:dd:0e:e4:0c:5d:58:fc:a0:72:a5
# SHA256 Fingerprint: be:f2:56:da:f2:6e:9c:69:bd:ec:16:02:35:97:98:f3:ca:f7:18:21:a0:3e:01:82:57:c5:3c:65:61:7f:3d:4a
-----BEGIN CERTIFICATE-----
MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw
CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE
YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB
IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw
CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE
YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB
IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf
e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C
cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB
/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O
BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO
PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw
hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG
XSaQpYXFuXqUPoeovQA=
-----END CERTIFICATE-----

View file

@ -57,9 +57,11 @@ These API's are described in the `CherryPy specification
""" """
try: try:
import pkg_resources import importlib.metadata as importlib_metadata
except ImportError: except ImportError:
pass # fall back for python <= 3.7
# This try/except can be removed with py <= 3.7 support
import importlib_metadata
from threading import local as _local from threading import local as _local
@ -109,7 +111,7 @@ tree = _cptree.Tree()
try: try:
__version__ = pkg_resources.require('cherrypy')[0].version __version__ = importlib_metadata.version('cherrypy')
except Exception: except Exception:
__version__ = 'unknown' __version__ = 'unknown'
@ -181,24 +183,28 @@ def quickstart(root=None, script_name='', config=None):
class _Serving(_local): class _Serving(_local):
"""An interface for registering request and response objects. """An interface for registering request and response objects.
Rather than have a separate "thread local" object for the request and Rather than have a separate "thread local" object for the request
the response, this class works as a single threadlocal container for and the response, this class works as a single threadlocal container
both objects (and any others which developers wish to define). In this for both objects (and any others which developers wish to define).
way, we can easily dump those objects when we stop/start a new HTTP In this way, we can easily dump those objects when we stop/start a
conversation, yet still refer to them as module-level globals in a new HTTP conversation, yet still refer to them as module-level
thread-safe way. globals in a thread-safe way.
""" """
request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
_httputil.Host('127.0.0.1', 1111)) _httputil.Host('127.0.0.1', 1111))
"""The request object for the current thread.
In the main thread, and any threads which are not receiving HTTP
requests, this is None.
""" """
The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
response = _cprequest.Response() response = _cprequest.Response()
"""The response object for the current thread.
In the main thread, and any threads which are not receiving HTTP
requests, this is None.
""" """
The response object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
def load(self, request, response): def load(self, request, response):
self.request = request self.request = request
@ -316,8 +322,8 @@ class _GlobalLogManager(_cplogging.LogManager):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log. """Log the given message to the app.log or global log.
Log the given message to the app.log or global Log the given message to the app.log or global log as
log as appropriate. appropriate.
""" """
# Do NOT use try/except here. See # Do NOT use try/except here. See
# https://github.com/cherrypy/cherrypy/issues/945 # https://github.com/cherrypy/cherrypy/issues/945
@ -330,8 +336,8 @@ class _GlobalLogManager(_cplogging.LogManager):
def access(self): def access(self):
"""Log an access message to the app.log or global log. """Log an access message to the app.log or global log.
Log the given message to the app.log or global Log the given message to the app.log or global log as
log as appropriate. appropriate.
""" """
try: try:
return request.app.log.access() return request.app.log.access()

View file

@ -313,7 +313,10 @@ class Checker(object):
# -------------------- Specific config warnings -------------------- # # -------------------- Specific config warnings -------------------- #
def check_localhost(self): def check_localhost(self):
"""Warn if any socket_host is 'localhost'. See #711.""" """Warn if any socket_host is 'localhost'.
See #711.
"""
for k, v in cherrypy.config.items(): for k, v in cherrypy.config.items():
if k == 'server.socket_host' and v == 'localhost': if k == 'server.socket_host' and v == 'localhost':
warnings.warn("The use of 'localhost' as a socket host can " warnings.warn("The use of 'localhost' as a socket host can "

View file

@ -1,5 +1,4 @@
""" """Configuration system for CherryPy.
Configuration system for CherryPy.
Configuration in CherryPy is implemented via dictionaries. Keys are strings Configuration in CherryPy is implemented via dictionaries. Keys are strings
which name the mapped value, which may be of any type. which name the mapped value, which may be of any type.
@ -132,8 +131,8 @@ def _if_filename_register_autoreload(ob):
def merge(base, other): def merge(base, other):
"""Merge one app config (from a dict, file, or filename) into another. """Merge one app config (from a dict, file, or filename) into another.
If the given config is a filename, it will be appended to If the given config is a filename, it will be appended to the list
the list of files to monitor for "autoreload" changes. of files to monitor for "autoreload" changes.
""" """
_if_filename_register_autoreload(other) _if_filename_register_autoreload(other)

View file

@ -1,9 +1,10 @@
"""CherryPy dispatchers. """CherryPy dispatchers.
A 'dispatcher' is the object which looks up the 'page handler' callable A 'dispatcher' is the object which looks up the 'page handler' callable
and collects config for the current request based on the path_info, other and collects config for the current request based on the path_info,
request attributes, and the application architecture. The core calls the other request attributes, and the application architecture. The core
dispatcher as early as possible, passing it a 'path_info' argument. calls the dispatcher as early as possible, passing it a 'path_info'
argument.
The default dispatcher discovers the page handler by matching path_info The default dispatcher discovers the page handler by matching path_info
to a hierarchical arrangement of objects, starting at request.app.root. to a hierarchical arrangement of objects, starting at request.app.root.
@ -21,7 +22,6 @@ import cherrypy
class PageHandler(object): class PageHandler(object):
"""Callable which sets response.body.""" """Callable which sets response.body."""
def __init__(self, callable, *args, **kwargs): def __init__(self, callable, *args, **kwargs):
@ -64,8 +64,7 @@ class PageHandler(object):
def test_callable_spec(callable, callable_args, callable_kwargs): def test_callable_spec(callable, callable_args, callable_kwargs):
""" """Inspect callable and test to see if the given args are suitable for it.
Inspect callable and test to see if the given args are suitable for it.
When an error occurs during the handler's invoking stage there are 2 When an error occurs during the handler's invoking stage there are 2
erroneous cases: erroneous cases:
@ -252,16 +251,16 @@ else:
class Dispatcher(object): class Dispatcher(object):
"""CherryPy Dispatcher which walks a tree of objects to find a handler. """CherryPy Dispatcher which walks a tree of objects to find a handler.
The tree is rooted at cherrypy.request.app.root, and each hierarchical The tree is rooted at cherrypy.request.app.root, and each
component in the path_info argument is matched to a corresponding nested hierarchical component in the path_info argument is matched to a
attribute of the root object. Matching handlers must have an 'exposed' corresponding nested attribute of the root object. Matching handlers
attribute which evaluates to True. The special method name "index" must have an 'exposed' attribute which evaluates to True. The
matches a URI which ends in a slash ("/"). The special method name special method name "index" matches a URI which ends in a slash
"default" may match a portion of the path_info (but only when no longer ("/"). The special method name "default" may match a portion of the
substring of the path_info matches some other object). path_info (but only when no longer substring of the path_info
matches some other object).
This is the default, built-in dispatcher for CherryPy. This is the default, built-in dispatcher for CherryPy.
""" """
@ -306,9 +305,9 @@ class Dispatcher(object):
The second object returned will be a list of names which are The second object returned will be a list of names which are
'virtual path' components: parts of the URL which are dynamic, 'virtual path' components: parts of the URL which are dynamic,
and were not used when looking up the handler. and were not used when looking up the handler. These virtual
These virtual path components are passed to the handler as path components are passed to the handler as positional
positional arguments. arguments.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
app = request.app app = request.app
@ -448,13 +447,11 @@ class Dispatcher(object):
class MethodDispatcher(Dispatcher): class MethodDispatcher(Dispatcher):
"""Additional dispatch based on cherrypy.request.method.upper(). """Additional dispatch based on cherrypy.request.method.upper().
Methods named GET, POST, etc will be called on an exposed class. Methods named GET, POST, etc will be called on an exposed class. The
The method names must be all caps; the appropriate Allow header method names must be all caps; the appropriate Allow header will be
will be output showing all capitalized method names as allowable output showing all capitalized method names as allowable HTTP verbs.
HTTP verbs.
Note that the containing class must be exposed, not the methods. Note that the containing class must be exposed, not the methods.
""" """
@ -492,16 +489,14 @@ class MethodDispatcher(Dispatcher):
class RoutesDispatcher(object): class RoutesDispatcher(object):
"""A Routes based dispatcher for CherryPy.""" """A Routes based dispatcher for CherryPy."""
def __init__(self, full_result=False, **mapper_options): def __init__(self, full_result=False, **mapper_options):
""" """Routes dispatcher.
Routes dispatcher
Set full_result to True if you wish the controller Set full_result to True if you wish the controller and the
and the action to be passed on to the page handler action to be passed on to the page handler parameters. By
parameters. By default they won't be. default they won't be.
""" """
import routes import routes
self.full_result = full_result self.full_result = full_result
@ -617,8 +612,7 @@ def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
**domains): **domains):
""" """Select a different handler based on the Host header.
Select a different handler based on the Host header.
This can be useful when running multiple sites within one CP server. This can be useful when running multiple sites within one CP server.
It allows several domains to point to different parts of a single It allows several domains to point to different parts of a single

View file

@ -136,19 +136,17 @@ from cherrypy.lib import httputil as _httputil
class CherryPyException(Exception): class CherryPyException(Exception):
"""A base class for CherryPy exceptions.""" """A base class for CherryPy exceptions."""
pass pass
class InternalRedirect(CherryPyException): class InternalRedirect(CherryPyException):
"""Exception raised to switch to the handler for a different URL. """Exception raised to switch to the handler for a different URL.
This exception will redirect processing to another path within the site This exception will redirect processing to another path within the
(without informing the client). Provide the new path as an argument when site (without informing the client). Provide the new path as an
raising the exception. Provide any params in the querystring for the new argument when raising the exception. Provide any params in the
URL. querystring for the new URL.
""" """
def __init__(self, path, query_string=''): def __init__(self, path, query_string=''):
@ -173,7 +171,6 @@ class InternalRedirect(CherryPyException):
class HTTPRedirect(CherryPyException): class HTTPRedirect(CherryPyException):
"""Exception raised when the request should be redirected. """Exception raised when the request should be redirected.
This exception will force a HTTP redirect to the URL or URL's you give it. This exception will force a HTTP redirect to the URL or URL's you give it.
@ -202,7 +199,7 @@ class HTTPRedirect(CherryPyException):
"""The list of URL's to emit.""" """The list of URL's to emit."""
encoding = 'utf-8' encoding = 'utf-8'
"""The encoding when passed urls are not native strings""" """The encoding when passed urls are not native strings."""
def __init__(self, urls, status=None, encoding=None): def __init__(self, urls, status=None, encoding=None):
self.urls = abs_urls = [ self.urls = abs_urls = [
@ -230,8 +227,7 @@ class HTTPRedirect(CherryPyException):
@classproperty @classproperty
def default_status(cls): def default_status(cls):
""" """The default redirect status for the request.
The default redirect status for the request.
RFC 2616 indicates a 301 response code fits our goal; however, RFC 2616 indicates a 301 response code fits our goal; however,
browser support for 301 is quite messy. Use 302/303 instead. See browser support for 301 is quite messy. Use 302/303 instead. See
@ -249,8 +245,9 @@ class HTTPRedirect(CherryPyException):
"""Modify cherrypy.response status, headers, and body to represent """Modify cherrypy.response status, headers, and body to represent
self. self.
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create
HTTPRedirect object and set its output without *raising* the exception. an HTTPRedirect object and set its output without *raising* the
exception.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
response.status = status = self.status response.status = status = self.status
@ -339,7 +336,6 @@ def clean_headers(status):
class HTTPError(CherryPyException): class HTTPError(CherryPyException):
"""Exception used to return an HTTP error code (4xx-5xx) to the client. """Exception used to return an HTTP error code (4xx-5xx) to the client.
This exception can be used to automatically send a response using a This exception can be used to automatically send a response using a
@ -358,7 +354,9 @@ class HTTPError(CherryPyException):
""" """
status = None status = None
"""The HTTP status code. May be of type int or str (with a Reason-Phrase). """The HTTP status code.
May be of type int or str (with a Reason-Phrase).
""" """
code = None code = None
@ -386,8 +384,9 @@ class HTTPError(CherryPyException):
"""Modify cherrypy.response status, headers, and body to represent """Modify cherrypy.response status, headers, and body to represent
self. self.
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create
HTTPError object and set its output without *raising* the exception. an HTTPError object and set its output without *raising* the
exception.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
@ -426,11 +425,10 @@ class HTTPError(CherryPyException):
class NotFound(HTTPError): class NotFound(HTTPError):
"""Exception raised when a URL could not be mapped to any handler (404). """Exception raised when a URL could not be mapped to any handler (404).
This is equivalent to raising This is equivalent to raising :class:`HTTPError("404 Not Found")
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`. <cherrypy._cperror.HTTPError>`.
""" """
def __init__(self, path=None): def __init__(self, path=None):
@ -477,8 +475,8 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
def get_error_page(status, **kwargs): def get_error_page(status, **kwargs):
"""Return an HTML page, containing a pretty error response. """Return an HTML page, containing a pretty error response.
status should be an int or a str. status should be an int or a str. kwargs will be interpolated into
kwargs will be interpolated into the page template. the page template.
""" """
try: try:
code, reason, message = _httputil.valid_status(status) code, reason, message = _httputil.valid_status(status)
@ -595,8 +593,8 @@ def bare_error(extrabody=None):
"""Produce status, headers, body for a critical error. """Produce status, headers, body for a critical error.
Returns a triple without calling any other questionable functions, Returns a triple without calling any other questionable functions,
so it should be as error-free as possible. Call it from an HTTP server so it should be as error-free as possible. Call it from an HTTP
if you get errors outside of the request. server if you get errors outside of the request.
If extrabody is None, a friendly but rather unhelpful error message If extrabody is None, a friendly but rather unhelpful error message
is set in the body. If extrabody is a string, it will be appended is set in the body. If extrabody is a string, it will be appended

View file

@ -123,7 +123,6 @@ logfmt = logging.Formatter('%(message)s')
class NullHandler(logging.Handler): class NullHandler(logging.Handler):
"""A no-op logging handler to silence the logging.lastResort handler.""" """A no-op logging handler to silence the logging.lastResort handler."""
def handle(self, record): def handle(self, record):
@ -137,15 +136,16 @@ class NullHandler(logging.Handler):
class LogManager(object): class LogManager(object):
"""An object to assist both simple and advanced logging. """An object to assist both simple and advanced logging.
``cherrypy.log`` is an instance of this class. ``cherrypy.log`` is an instance of this class.
""" """
appid = None appid = None
"""The id() of the Application object which owns this log manager. If this """The id() of the Application object which owns this log manager.
is a global log manager, appid is None."""
If this is a global log manager, appid is None.
"""
error_log = None error_log = None
"""The actual :class:`logging.Logger` instance for error messages.""" """The actual :class:`logging.Logger` instance for error messages."""
@ -317,8 +317,8 @@ class LogManager(object):
def screen(self): def screen(self):
"""Turn stderr/stdout logging on or off. """Turn stderr/stdout logging on or off.
If you set this to True, it'll add the appropriate StreamHandler for If you set this to True, it'll add the appropriate StreamHandler
you. If you set it to False, it will remove the handler. for you. If you set it to False, it will remove the handler.
""" """
h = self._get_builtin_handler h = self._get_builtin_handler
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
@ -414,7 +414,6 @@ class LogManager(object):
class WSGIErrorHandler(logging.Handler): class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']." "A handler class which writes logging records to environ['wsgi.errors']."
def flush(self): def flush(self):
@ -452,6 +451,8 @@ class WSGIErrorHandler(logging.Handler):
class LazyRfc3339UtcTime(object): class LazyRfc3339UtcTime(object):
def __str__(self): def __str__(self):
"""Return utcnow() in RFC3339 UTC Format.""" """Return datetime in RFC3339 UTC Format."""
iso_formatted_now = datetime.datetime.utcnow().isoformat('T') iso_formatted_now = datetime.datetime.now(
datetime.timezone.utc,
).isoformat('T')
return f'{iso_formatted_now!s}Z' return f'{iso_formatted_now!s}Z'

View file

@ -1,4 +1,4 @@
"""Native adapter for serving CherryPy via mod_python """Native adapter for serving CherryPy via mod_python.
Basic usage: Basic usage:

View file

@ -120,10 +120,10 @@ class NativeGateway(cheroot.server.Gateway):
class CPHTTPServer(cheroot.server.HTTPServer): class CPHTTPServer(cheroot.server.HTTPServer):
"""Wrapper for cheroot.server.HTTPServer. """Wrapper for cheroot.server.HTTPServer.
cheroot has been designed to not reference CherryPy in any way, cheroot has been designed to not reference CherryPy in any way, so
so that it can be used in other frameworks and applications. that it can be used in other frameworks and applications. Therefore,
Therefore, we wrap it here, so we can apply some attributes we wrap it here, so we can apply some attributes from config ->
from config -> cherrypy.server -> HTTPServer. cherrypy.server -> HTTPServer.
""" """
def __init__(self, server_adapter=cherrypy.server): def __init__(self, server_adapter=cherrypy.server):

View file

@ -248,7 +248,10 @@ def process_multipart_form_data(entity):
def _old_process_multipart(entity): def _old_process_multipart(entity):
"""The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" """The behavior of 3.2 and lower.
Deprecated and will be changed in 3.3.
"""
process_multipart(entity) process_multipart(entity)
params = entity.params params = entity.params
@ -277,7 +280,6 @@ def _old_process_multipart(entity):
# -------------------------------- Entities --------------------------------- # # -------------------------------- Entities --------------------------------- #
class Entity(object): class Entity(object):
"""An HTTP request body, or MIME multipart body. """An HTTP request body, or MIME multipart body.
This class collects information about the HTTP request entity. When a This class collects information about the HTTP request entity. When a
@ -346,13 +348,15 @@ class Entity(object):
content_type = None content_type = None
"""The value of the Content-Type request header. """The value of the Content-Type request header.
If the Entity is part of a multipart payload, this will be the Content-Type If the Entity is part of a multipart payload, this will be the
given in the MIME headers for this part. Content-Type given in the MIME headers for this part.
""" """
default_content_type = 'application/x-www-form-urlencoded' default_content_type = 'application/x-www-form-urlencoded'
"""This defines a default ``Content-Type`` to use if no Content-Type header """This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the is given.
The empty string is used for RequestBody, which results in the
request body not being read or parsed at all. This is by design; a missing request body not being read or parsed at all. This is by design; a missing
``Content-Type`` header in the HTTP request entity is an error at best, ``Content-Type`` header in the HTTP request entity is an error at best,
and a security hole at worst. For multipart parts, however, the MIME spec and a security hole at worst. For multipart parts, however, the MIME spec
@ -402,8 +406,8 @@ class Entity(object):
part_class = None part_class = None
"""The class used for multipart parts. """The class used for multipart parts.
You can replace this with custom subclasses to alter the processing of You can replace this with custom subclasses to alter the processing
multipart parts. of multipart parts.
""" """
def __init__(self, fp, headers, params=None, parts=None): def __init__(self, fp, headers, params=None, parts=None):
@ -509,7 +513,8 @@ class Entity(object):
"""Return a file-like object into which the request body will be read. """Return a file-like object into which the request body will be read.
By default, this will return a TemporaryFile. Override as needed. By default, this will return a TemporaryFile. Override as needed.
See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.
"""
return tempfile.TemporaryFile() return tempfile.TemporaryFile()
def fullvalue(self): def fullvalue(self):
@ -525,7 +530,7 @@ class Entity(object):
return value return value
def decode_entity(self, value): def decode_entity(self, value):
"""Return a given byte encoded value as a string""" """Return a given byte encoded value as a string."""
for charset in self.attempt_charsets: for charset in self.attempt_charsets:
try: try:
value = value.decode(charset) value = value.decode(charset)
@ -569,7 +574,6 @@ class Entity(object):
class Part(Entity): class Part(Entity):
"""A MIME part entity, part of a multipart entity.""" """A MIME part entity, part of a multipart entity."""
# "The default character set, which must be assumed in the absence of a # "The default character set, which must be assumed in the absence of a
@ -653,8 +657,8 @@ class Part(Entity):
def read_lines_to_boundary(self, fp_out=None): def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file. """Read bytes from self.fp and return or write them to a file.
If the 'fp_out' argument is None (the default), all bytes read are If the 'fp_out' argument is None (the default), all bytes read
returned in a single byte string. are returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like If the 'fp_out' argument is not None, it must be a file-like
object that supports the 'write' method; all bytes read will be object that supports the 'write' method; all bytes read will be
@ -755,15 +759,15 @@ class SizedReader:
def read(self, size=None, fp_out=None): def read(self, size=None, fp_out=None):
"""Read bytes from the request body and return or write them to a file. """Read bytes from the request body and return or write them to a file.
A number of bytes less than or equal to the 'size' argument are read A number of bytes less than or equal to the 'size' argument are
off the socket. The actual number of bytes read are tracked in read off the socket. The actual number of bytes read are tracked
self.bytes_read. The number may be smaller than 'size' when 1) the in self.bytes_read. The number may be smaller than 'size' when
client sends fewer bytes, 2) the 'Content-Length' request header 1) the client sends fewer bytes, 2) the 'Content-Length' request
specifies fewer bytes than requested, or 3) the number of bytes read header specifies fewer bytes than requested, or 3) the number of
exceeds self.maxbytes (in which case, 413 is raised). bytes read exceeds self.maxbytes (in which case, 413 is raised).
If the 'fp_out' argument is None (the default), all bytes read are If the 'fp_out' argument is None (the default), all bytes read
returned in a single byte string. are returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like If the 'fp_out' argument is not None, it must be a file-like
object that supports the 'write' method; all bytes read will be object that supports the 'write' method; all bytes read will be
@ -918,7 +922,6 @@ class SizedReader:
class RequestBody(Entity): class RequestBody(Entity):
"""The entity of the HTTP request.""" """The entity of the HTTP request."""
bufsize = 8 * 1024 bufsize = 8 * 1024

View file

@ -16,7 +16,6 @@ from cherrypy.lib import httputil, reprconf, encoding
class Hook(object): class Hook(object):
"""A callback and its metadata: failsafe, priority, and kwargs.""" """A callback and its metadata: failsafe, priority, and kwargs."""
callback = None callback = None
@ -30,10 +29,12 @@ class Hook(object):
from the same call point raise exceptions.""" from the same call point raise exceptions."""
priority = 50 priority = 50
"""Defines the order of execution for a list of Hooks.
Priority numbers should be limited to the closed interval [0, 100],
but values outside this range are acceptable, as are fractional
values.
""" """
Defines the order of execution for a list of Hooks. Priority numbers
should be limited to the closed interval [0, 100], but values outside
this range are acceptable, as are fractional values."""
kwargs = {} kwargs = {}
""" """
@ -74,7 +75,6 @@ class Hook(object):
class HookMap(dict): class HookMap(dict):
"""A map of call points to lists of callbacks (Hook objects).""" """A map of call points to lists of callbacks (Hook objects)."""
def __new__(cls, points=None): def __new__(cls, points=None):
@ -190,23 +190,23 @@ hookpoints = ['on_start_resource', 'before_request_body',
class Request(object): class Request(object):
"""An HTTP request. """An HTTP request.
This object represents the metadata of an HTTP request message; This object represents the metadata of an HTTP request message; that
that is, it contains attributes which describe the environment is, it contains attributes which describe the environment in which
in which the request URL, headers, and body were sent (if you the request URL, headers, and body were sent (if you want tools to
want tools to interpret the headers and body, those are elsewhere, interpret the headers and body, those are elsewhere, mostly in
mostly in Tools). This 'metadata' consists of socket data, Tools). This 'metadata' consists of socket data, transport
transport characteristics, and the Request-Line. This object characteristics, and the Request-Line. This object also contains
also contains data regarding the configuration in effect for data regarding the configuration in effect for the given URL, and
the given URL, and the execution plan for generating a response. the execution plan for generating a response.
""" """
prev = None prev = None
"""The previous Request object (if any).
This should be None unless we are processing an InternalRedirect.
""" """
The previous Request object (if any). This should be None
unless we are processing an InternalRedirect."""
# Conversation/connection attributes # Conversation/connection attributes
local = httputil.Host('127.0.0.1', 80) local = httputil.Host('127.0.0.1', 80)
@ -216,9 +216,10 @@ class Request(object):
'An httputil.Host(ip, port, hostname) object for the client socket.' 'An httputil.Host(ip, port, hostname) object for the client socket.'
scheme = 'http' scheme = 'http'
"""The protocol used between client and server.
In most cases, this will be either 'http' or 'https'.
""" """
The protocol used between client and server. In most cases,
this will be either 'http' or 'https'."""
server_protocol = 'HTTP/1.1' server_protocol = 'HTTP/1.1'
""" """
@ -227,25 +228,30 @@ class Request(object):
base = '' base = ''
"""The (scheme://host) portion of the requested URL. """The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but path segments which cherrypy.url uses when constructing url's, but
which otherwise are ignored by CherryPy. Regardless, this value which otherwise are ignored by CherryPy. Regardless, this value MUST
MUST NOT end in a slash.""" NOT end in a slash.
"""
# Request-Line attributes # Request-Line attributes
request_line = '' request_line = ''
"""The complete Request-Line received from the client.
This is a single string consisting of the request method, URI, and
protocol version (joined by spaces). Any final CRLF is removed.
""" """
The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed."""
method = 'GET' method = 'GET'
"""Indicates the HTTP method to be performed on the resource identified by
the Request-URI.
Common methods include GET, HEAD, POST, PUT, and DELETE. CherryPy
allows any extension method; however, various HTTP servers and
gateways may restrict the set of allowable methods. CherryPy
applications SHOULD restrict the set (on a per-URI basis).
""" """
Indicates the HTTP method to be performed on the resource identified
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
DELETE. CherryPy allows any extension method; however, various HTTP
servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = '' query_string = ''
""" """
@ -277,22 +283,26 @@ class Request(object):
A dict which combines query string (GET) and request entity (POST) A dict which combines query string (GET) and request entity (POST)
variables. This is populated in two stages: GET params are added variables. This is populated in two stages: GET params are added
before the 'on_start_resource' hook, and POST params are added before the 'on_start_resource' hook, and POST params are added
between the 'before_request_body' and 'before_handler' hooks.""" between the 'before_request_body' and 'before_handler' hooks.
"""
# Message attributes # Message attributes
header_list = [] header_list = []
"""A list of the HTTP request headers as (name, value) tuples.
In general, you should use request.headers (a dict) instead.
""" """
A list of the HTTP request headers as (name, value) tuples.
In general, you should use request.headers (a dict) instead."""
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
""" """A dict-like object containing the request headers.
A dict-like object containing the request headers. Keys are header
Keys are header
names (in Title-Case format); however, you may get and set them in names (in Title-Case format); however, you may get and set them in
a case-insensitive manner. That is, headers['Content-Type'] and a case-insensitive manner. That is, headers['Content-Type'] and
headers['content-type'] refer to the same value. Values are header headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary). See also: values (decoded according to :rfc:`2047` if necessary). See also:
httputil.HeaderMap, httputil.HeaderElement.""" httputil.HeaderMap, httputil.HeaderElement.
"""
cookie = SimpleCookie() cookie = SimpleCookie()
"""See help(Cookie).""" """See help(Cookie)."""
@ -336,7 +346,8 @@ class Request(object):
or multipart, this will be None. Otherwise, this will be an instance or multipart, this will be None. Otherwise, this will be an instance
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
can .read()); this value is set between the 'before_request_body' and can .read()); this value is set between the 'before_request_body' and
'before_handler' hooks (assuming that process_request_body is True).""" 'before_handler' hooks (assuming that process_request_body is True).
"""
# Dispatch attributes # Dispatch attributes
dispatch = cherrypy.dispatch.Dispatcher() dispatch = cherrypy.dispatch.Dispatcher()
@ -347,23 +358,24 @@ class Request(object):
calls the dispatcher as early as possible, passing it a 'path_info' calls the dispatcher as early as possible, passing it a 'path_info'
argument. argument.
The default dispatcher discovers the page handler by matching path_info The default dispatcher discovers the page handler by matching
to a hierarchical arrangement of objects, starting at request.app.root. path_info to a hierarchical arrangement of objects, starting at
See help(cherrypy.dispatch) for more information.""" request.app.root. See help(cherrypy.dispatch) for more information.
"""
script_name = '' script_name = ''
""" """The 'mount point' of the application which is handling this request.
The 'mount point' of the application which is handling this request.
This attribute MUST NOT end in a slash. If the script_name refers to This attribute MUST NOT end in a slash. If the script_name refers to
the root of the URI, it MUST be an empty string (not "/"). the root of the URI, it MUST be an empty string (not "/").
""" """
path_info = '/' path_info = '/'
"""The 'relative path' portion of the Request-URI.
This is relative to the script_name ('mount point') of the
application which is handling this request.
""" """
The 'relative path' portion of the Request-URI. This is relative
to the script_name ('mount point') of the application which is
handling this request."""
login = None login = None
""" """
@ -391,14 +403,16 @@ class Request(object):
of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
config = None config = None
"""A flat dict of all configuration entries which apply to the current
request.
These entries are collected from global config, application config
(based on request.path_info), and from handler config (exactly how
is governed by the request.dispatch object in effect for this
request; by default, handler config can be attached anywhere in the
tree between request.app.root and the final handler, and inherits
downward).
""" """
A flat dict of all configuration entries which apply to the
current request. These entries are collected from global config,
application config (based on request.path_info), and from handler
config (exactly how is governed by the request.dispatch object in
effect for this request; by default, handler config can be attached
anywhere in the tree between request.app.root and the final handler,
and inherits downward)."""
is_index = None is_index = None
""" """
@ -409,13 +423,14 @@ class Request(object):
the trailing slash. See cherrypy.tools.trailing_slash.""" the trailing slash. See cherrypy.tools.trailing_slash."""
hooks = HookMap(hookpoints) hooks = HookMap(hookpoints)
""" """A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
Each key is a str naming the hook point, and each value is a list Each key is a str naming the hook point, and each value is a list
of hooks which will be called at that hook point during this request. of hooks which will be called at that hook point during this request.
The list of hooks is generally populated as early as possible (mostly The list of hooks is generally populated as early as possible (mostly
from Tools specified in config), but may be extended at any time. from Tools specified in config), but may be extended at any time.
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.
"""
error_response = cherrypy.HTTPError(500).set_response error_response = cherrypy.HTTPError(500).set_response
""" """
@ -428,12 +443,11 @@ class Request(object):
error response to the user-agent.""" error response to the user-agent."""
error_page = {} error_page = {}
""" """A dict of {error code: response filename or callable} pairs.
A dict of {error code: response filename or callable} pairs.
The error code must be an int representing a given HTTP error code, The error code must be an int representing a given HTTP error code,
or the string 'default', which will be used if no matching entry or the string 'default', which will be used if no matching entry is
is found for a given numeric code. found for a given numeric code.
If a filename is provided, the file should contain a Python string- If a filename is provided, the file should contain a Python string-
formatting template, and can expect by default to receive format formatting template, and can expect by default to receive format
@ -447,8 +461,8 @@ class Request(object):
iterable of strings which will be set to response.body. It may also iterable of strings which will be set to response.body. It may also
override headers or perform any other processing. override headers or perform any other processing.
If no entry is given for an error code, and no 'default' entry exists, If no entry is given for an error code, and no 'default' entry
a default template will be used. exists, a default template will be used.
""" """
show_tracebacks = True show_tracebacks = True
@ -473,9 +487,10 @@ class Request(object):
"""True once the close method has been called, False otherwise.""" """True once the close method has been called, False otherwise."""
stage = None stage = None
"""A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests.
""" """
A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests."""
unique_id = None unique_id = None
"""A lazy object generating and memorizing UUID4 on ``str()`` render.""" """A lazy object generating and memorizing UUID4 on ``str()`` render."""
@ -492,9 +507,10 @@ class Request(object):
server_protocol='HTTP/1.1'): server_protocol='HTTP/1.1'):
"""Populate a new Request object. """Populate a new Request object.
local_host should be an httputil.Host object with the server info. local_host should be an httputil.Host object with the server
remote_host should be an httputil.Host object with the client info. info. remote_host should be an httputil.Host object with the
scheme should be a string, either "http" or "https". client info. scheme should be a string, either "http" or
"https".
""" """
self.local = local_host self.local = local_host
self.remote = remote_host self.remote = remote_host
@ -514,7 +530,10 @@ class Request(object):
self.unique_id = LazyUUID4() self.unique_id = LazyUUID4()
def close(self): def close(self):
"""Run cleanup code. (Core)""" """Run cleanup code.
(Core)
"""
if not self.closed: if not self.closed:
self.closed = True self.closed = True
self.stage = 'on_end_request' self.stage = 'on_end_request'
@ -551,7 +570,6 @@ class Request(object):
Consumer code (HTTP servers) should then access these response Consumer code (HTTP servers) should then access these response
attributes to build the outbound stream. attributes to build the outbound stream.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
self.stage = 'run' self.stage = 'run'
@ -631,7 +649,10 @@ class Request(object):
return response return response
def respond(self, path_info): def respond(self, path_info):
"""Generate a response for the resource at self.path_info. (Core)""" """Generate a response for the resource at self.path_info.
(Core)
"""
try: try:
try: try:
try: try:
@ -702,7 +723,10 @@ class Request(object):
response.finalize() response.finalize()
def process_query_string(self): def process_query_string(self):
"""Parse the query string into Python structures. (Core)""" """Parse the query string into Python structures.
(Core)
"""
try: try:
p = httputil.parse_query_string( p = httputil.parse_query_string(
self.query_string, encoding=self.query_string_encoding) self.query_string, encoding=self.query_string_encoding)
@ -715,7 +739,10 @@ class Request(object):
self.params.update(p) self.params.update(p)
def process_headers(self): def process_headers(self):
"""Parse HTTP header data into Python structures. (Core)""" """Parse HTTP header data into Python structures.
(Core)
"""
# Process the headers into self.headers # Process the headers into self.headers
headers = self.headers headers = self.headers
for name, value in self.header_list: for name, value in self.header_list:
@ -751,7 +778,10 @@ class Request(object):
self.base = '%s://%s' % (self.scheme, host) self.base = '%s://%s' % (self.scheme, host)
def get_resource(self, path): def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)""" """Call a dispatcher (which sets self.handler and .config).
(Core)
"""
# First, see if there is a custom dispatch at this URI. Custom # First, see if there is a custom dispatch at this URI. Custom
# dispatchers can only be specified in app.config, not in _cp_config # dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root). # (since custom dispatchers may not even have an app.root).
@ -762,7 +792,10 @@ class Request(object):
dispatch(path) dispatch(path)
def handle_error(self): def handle_error(self):
"""Handle the last unanticipated exception. (Core)""" """Handle the last unanticipated exception.
(Core)
"""
try: try:
self.hooks.run('before_error_response') self.hooks.run('before_error_response')
if self.error_response: if self.error_response:
@ -776,7 +809,6 @@ class Request(object):
class ResponseBody(object): class ResponseBody(object):
"""The body of the HTTP response (the response entity).""" """The body of the HTTP response (the response entity)."""
unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
@ -802,18 +834,18 @@ class ResponseBody(object):
class Response(object): class Response(object):
"""An HTTP Response, including status, headers, and body.""" """An HTTP Response, including status, headers, and body."""
status = '' status = ''
"""The HTTP Status-Code and Reason-Phrase.""" """The HTTP Status-Code and Reason-Phrase."""
header_list = [] header_list = []
""" """A list of the HTTP response headers as (name, value) tuples.
A list of the HTTP response headers as (name, value) tuples.
In general, you should use response.headers (a dict) instead. This In general, you should use response.headers (a dict) instead. This
attribute is generated from response.headers and is not valid until attribute is generated from response.headers and is not valid until
after the finalize phase.""" after the finalize phase.
"""
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
""" """
@ -833,7 +865,10 @@ class Response(object):
"""The body (entity) of the HTTP response.""" """The body (entity) of the HTTP response."""
time = None time = None
"""The value of time.time() when created. Use in HTTP dates.""" """The value of time.time() when created.
Use in HTTP dates.
"""
stream = False stream = False
"""If False, buffer the response body.""" """If False, buffer the response body."""
@ -861,15 +896,15 @@ class Response(object):
return new_body return new_body
def _flush_body(self): def _flush_body(self):
""" """Discard self.body but consume any generator such that any
Discard self.body but consume any generator such that finalization can occur, such as is required by caching.tee_output()."""
any finalization can occur, such as is required by
caching.tee_output().
"""
consume(iter(self.body)) consume(iter(self.body))
def finalize(self): def finalize(self):
"""Transform headers (and cookies) into self.header_list. (Core)""" """Transform headers (and cookies) into self.header_list.
(Core)
"""
try: try:
code, reason, _ = httputil.valid_status(self.status) code, reason, _ = httputil.valid_status(self.status)
except ValueError: except ValueError:

View file

@ -50,7 +50,8 @@ class Server(ServerAdapter):
"""If given, the name of the UNIX socket to use instead of TCP/IP. """If given, the name of the UNIX socket to use instead of TCP/IP.
When this option is not None, the `socket_host` and `socket_port` options When this option is not None, the `socket_host` and `socket_port` options
are ignored.""" are ignored.
"""
socket_queue_size = 5 socket_queue_size = 5
"""The 'backlog' argument to socket.listen(); specifies the maximum number """The 'backlog' argument to socket.listen(); specifies the maximum number
@ -79,17 +80,24 @@ class Server(ServerAdapter):
"""The number of worker threads to start up in the pool.""" """The number of worker threads to start up in the pool."""
thread_pool_max = -1 thread_pool_max = -1
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit. """The maximum size of the worker-thread pool.
Use -1 to indicate no limit.
""" """
max_request_header_size = 500 * 1024 max_request_header_size = 500 * 1024
"""The maximum number of bytes allowable in the request headers. """The maximum number of bytes allowable in the request headers.
If exceeded, the HTTP server should return "413 Request Entity Too Large".
If exceeded, the HTTP server should return "413 Request Entity Too
Large".
""" """
max_request_body_size = 100 * 1024 * 1024 max_request_body_size = 100 * 1024 * 1024
"""The maximum number of bytes allowable in the request body. If exceeded, """The maximum number of bytes allowable in the request body.
the HTTP server should return "413 Request Entity Too Large"."""
If exceeded, the HTTP server should return "413 Request Entity Too
Large".
"""
instance = None instance = None
"""If not None, this should be an HTTP server instance (such as """If not None, this should be an HTTP server instance (such as
@ -119,7 +127,8 @@ class Server(ServerAdapter):
the builtin WSGI server. Builtin options are: 'builtin' (to the builtin WSGI server. Builtin options are: 'builtin' (to
use the SSL library built into recent versions of Python). use the SSL library built into recent versions of Python).
You may also register your own classes in the You may also register your own classes in the
cheroot.server.ssl_adapters dict.""" cheroot.server.ssl_adapters dict.
"""
statistics = False statistics = False
"""Turns statistics-gathering on or off for aware HTTP servers.""" """Turns statistics-gathering on or off for aware HTTP servers."""
@ -129,11 +138,13 @@ class Server(ServerAdapter):
wsgi_version = (1, 0) wsgi_version = (1, 0)
"""The WSGI version tuple to use with the builtin WSGI server. """The WSGI version tuple to use with the builtin WSGI server.
The provided options are (1, 0) [which includes support for PEP 3333,
which declares it covers WSGI version 1.0.1 but still mandates the The provided options are (1, 0) [which includes support for PEP
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. 3333, which declares it covers WSGI version 1.0.1 but still mandates
You may create and register your own experimental versions of the WSGI the wsgi.version (1, 0)] and ('u', 0), an experimental unicode
protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. version. You may create and register your own experimental versions
of the WSGI protocol by adding custom classes to the
cheroot.server.wsgi_gateways dict.
""" """
peercreds = False peercreds = False
@ -184,7 +195,8 @@ class Server(ServerAdapter):
def bind_addr(self): def bind_addr(self):
"""Return bind address. """Return bind address.
A (host, port) tuple for TCP sockets or a str for Unix domain sockts. A (host, port) tuple for TCP sockets or a str for Unix domain
sockets.
""" """
if self.socket_file: if self.socket_file:
return self.socket_file return self.socket_file

View file

@ -1,7 +1,7 @@
"""CherryPy tools. A "tool" is any helper, adapted to CP. """CherryPy tools. A "tool" is any helper, adapted to CP.
Tools are usually designed to be used in a variety of ways (although some Tools are usually designed to be used in a variety of ways (although
may only offer one if they choose): some may only offer one if they choose):
Library calls Library calls
All tools are callables that can be used wherever needed. All tools are callables that can be used wherever needed.
@ -48,10 +48,10 @@ _attr_error = (
class Tool(object): class Tool(object):
"""A registered function for use with CherryPy request-processing hooks. """A registered function for use with CherryPy request-processing hooks.
help(tool.callable) should give you more information about this Tool. help(tool.callable) should give you more information about this
Tool.
""" """
namespace = 'tools' namespace = 'tools'
@ -135,8 +135,8 @@ class Tool(object):
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call
method when the tool is "turned on" in config. this method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop('priority', None) p = conf.pop('priority', None)
@ -147,15 +147,15 @@ class Tool(object):
class HandlerTool(Tool): class HandlerTool(Tool):
"""Tool which is called 'before main', that may skip normal handlers. """Tool which is called 'before main', that may skip normal handlers.
If the tool successfully handles the request (by setting response.body), If the tool successfully handles the request (by setting
if should return True. This will cause CherryPy to skip any 'normal' page response.body), if should return True. This will cause CherryPy to
handler. If the tool did not handle the request, it should return False skip any 'normal' page handler. If the tool did not handle the
to tell CherryPy to continue on and call the normal page handler. If the request, it should return False to tell CherryPy to continue on and
tool is declared AS a page handler (see the 'handler' method), returning call the normal page handler. If the tool is declared AS a page
False will raise NotFound. handler (see the 'handler' method), returning False will raise
NotFound.
""" """
def __init__(self, callable, name=None): def __init__(self, callable, name=None):
@ -185,8 +185,8 @@ class HandlerTool(Tool):
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call
method when the tool is "turned on" in config. this method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop('priority', None) p = conf.pop('priority', None)
@ -197,7 +197,6 @@ class HandlerTool(Tool):
class HandlerWrapperTool(Tool): class HandlerWrapperTool(Tool):
"""Tool which wraps request.handler in a provided wrapper function. """Tool which wraps request.handler in a provided wrapper function.
The 'newhandler' arg must be a handler wrapper function that takes a The 'newhandler' arg must be a handler wrapper function that takes a
@ -232,7 +231,6 @@ class HandlerWrapperTool(Tool):
class ErrorTool(Tool): class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response.""" """Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None): def __init__(self, callable, name=None):
@ -244,8 +242,8 @@ class ErrorTool(Tool):
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call
method when the tool is "turned on" in config. this method when the tool is "turned on" in config.
""" """
cherrypy.serving.request.error_response = self._wrapper cherrypy.serving.request.error_response = self._wrapper
@ -254,7 +252,6 @@ class ErrorTool(Tool):
class SessionTool(Tool): class SessionTool(Tool):
"""Session Tool for CherryPy. """Session Tool for CherryPy.
sessions.locking sessions.locking
@ -282,8 +279,8 @@ class SessionTool(Tool):
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call
method when the tool is "turned on" in config. this method when the tool is "turned on" in config.
""" """
hooks = cherrypy.serving.request.hooks hooks = cherrypy.serving.request.hooks
@ -325,7 +322,6 @@ class SessionTool(Tool):
class XMLRPCController(object): class XMLRPCController(object):
"""A Controller (page handler collection) for XML-RPC. """A Controller (page handler collection) for XML-RPC.
To use it, have your controllers subclass this base class (it will To use it, have your controllers subclass this base class (it will
@ -392,7 +388,6 @@ class SessionAuthTool(HandlerTool):
class CachingTool(Tool): class CachingTool(Tool):
"""Caching Tool for CherryPy.""" """Caching Tool for CherryPy."""
def _wrapper(self, **kwargs): def _wrapper(self, **kwargs):
@ -416,11 +411,11 @@ class CachingTool(Tool):
class Toolbox(object): class Toolbox(object):
"""A collection of Tools. """A collection of Tools.
This object also functions as a config namespace handler for itself. This object also functions as a config namespace handler for itself.
Custom toolboxes should be added to each Application's toolboxes dict. Custom toolboxes should be added to each Application's toolboxes
dict.
""" """
def __init__(self, namespace): def __init__(self, namespace):

View file

@ -10,19 +10,22 @@ from cherrypy.lib import httputil, reprconf
class Application(object): class Application(object):
"""A CherryPy Application. """A CherryPy Application.
Servers and gateways should not instantiate Request objects directly. Servers and gateways should not instantiate Request objects
Instead, they should ask an Application object for a request object. directly. Instead, they should ask an Application object for a
request object.
An instance of this class may also be used as a WSGI callable An instance of this class may also be used as a WSGI callable (WSGI
(WSGI application object) for itself. application object) for itself.
""" """
root = None root = None
"""The top-most container of page handlers for this app. Handlers should """The top-most container of page handlers for this app.
be arranged in a hierarchy of attributes, matching the expected URI
hierarchy; the default dispatcher then searches this hierarchy for a Handlers should be arranged in a hierarchy of attributes, matching
matching handler. When using a dispatcher other than the default, the expected URI hierarchy; the default dispatcher then searches
this value may be None.""" this hierarchy for a matching handler. When using a dispatcher other
than the default, this value may be None.
"""
config = {} config = {}
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
@ -32,10 +35,16 @@ class Application(object):
toolboxes = {'tools': cherrypy.tools} toolboxes = {'tools': cherrypy.tools}
log = None log = None
"""A LogManager instance. See _cplogging.""" """A LogManager instance.
See _cplogging.
"""
wsgiapp = None wsgiapp = None
"""A CPWSGIApp instance. See _cpwsgi.""" """A CPWSGIApp instance.
See _cpwsgi.
"""
request_class = _cprequest.Request request_class = _cprequest.Request
response_class = _cprequest.Response response_class = _cprequest.Response
@ -82,12 +91,15 @@ class Application(object):
def script_name(self): # noqa: D401; irrelevant for properties def script_name(self): # noqa: D401; irrelevant for properties
"""The URI "mount point" for this app. """The URI "mount point" for this app.
A mount point is that portion of the URI which is constant for all URIs A mount point is that portion of the URI which is constant for
that are serviced by this application; it does not include scheme, all URIs that are serviced by this application; it does not
host, or proxy ("virtual host") portions of the URI. include scheme, host, or proxy ("virtual host") portions of the
URI.
For example, if script_name is "/my/cool/app", then the URL For example, if script_name is "/my/cool/app", then the URL "
"http://www.example.com/my/cool/app/page1" might be handled by a
http://www.example.com/my/cool/app/page1"
might be handled by a
"page1" method on the root object. "page1" method on the root object.
The value of script_name MUST NOT end in a slash. If the script_name The value of script_name MUST NOT end in a slash. If the script_name
@ -171,9 +183,9 @@ class Application(object):
class Tree(object): class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points. """A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable An instance of this class may also be used as a WSGI callable (WSGI
(WSGI application object), in which case it dispatches to all application object), in which case it dispatches to all mounted
mounted apps. apps.
""" """
apps = {} apps = {}

View file

@ -1,10 +1,10 @@
"""WSGI interface (see PEP 333 and 3333). """WSGI interface (see PEP 333 and 3333).
Note that WSGI environ keys and values are 'native strings'; that is, Note that WSGI environ keys and values are 'native strings'; that is,
whatever the type of "" is. For Python 2, that's a byte string; for Python 3, whatever the type of "" is. For Python 2, that's a byte string; for
it's a unicode string. But PEP 3333 says: "even if Python's str type is Python 3, it's a unicode string. But PEP 3333 says: "even if Python's
actually Unicode "under the hood", the content of native strings must str type is actually Unicode "under the hood", the content of native
still be translatable to bytes via the Latin-1 encoding!" strings must still be translatable to bytes via the Latin-1 encoding!"
""" """
import sys as _sys import sys as _sys
@ -34,7 +34,6 @@ def downgrade_wsgi_ux_to_1x(environ):
class VirtualHost(object): class VirtualHost(object):
"""Select a different WSGI application based on the Host header. """Select a different WSGI application based on the Host header.
This can be useful when running multiple sites within one CP server. This can be useful when running multiple sites within one CP server.
@ -56,7 +55,10 @@ class VirtualHost(object):
cherrypy.tree.graft(vhost) cherrypy.tree.graft(vhost)
""" """
default = None default = None
"""Required. The default WSGI application.""" """Required.
The default WSGI application.
"""
use_x_forwarded_host = True use_x_forwarded_host = True
"""If True (the default), any "X-Forwarded-Host" """If True (the default), any "X-Forwarded-Host"
@ -65,11 +67,12 @@ class VirtualHost(object):
domains = {} domains = {}
"""A dict of {host header value: application} pairs. """A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding WSGI application The incoming "Host" request header is looked up in this dict, and,
will be called instead of the default. Note that you often need if a match is found, the corresponding WSGI application will be
separate entries for "example.com" and "www.example.com". called instead of the default. Note that you often need separate
In addition, "Host" headers may contain the port number. entries for "example.com" and "www.example.com". In addition, "Host"
headers may contain the port number.
""" """
def __init__(self, default, domains=None, use_x_forwarded_host=True): def __init__(self, default, domains=None, use_x_forwarded_host=True):
@ -89,7 +92,6 @@ class VirtualHost(object):
class InternalRedirector(object): class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect.""" """WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False): def __init__(self, nextapp, recursive=False):
@ -137,7 +139,6 @@ class InternalRedirector(object):
class ExceptionTrapper(object): class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions.""" """WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
@ -226,7 +227,6 @@ class _TrappedResponse(object):
class AppResponse(object): class AppResponse(object):
"""WSGI response iterable for CherryPy applications.""" """WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp): def __init__(self, environ, start_response, cpapp):
@ -277,7 +277,10 @@ class AppResponse(object):
return next(self.iter_response) return next(self.iter_response)
def close(self): def close(self):
"""Close and de-reference the current request and response. (Core)""" """Close and de-reference the current request and response.
(Core)
"""
streaming = _cherrypy.serving.response.stream streaming = _cherrypy.serving.response.stream
self.cpapp.release_serving() self.cpapp.release_serving()
@ -380,18 +383,20 @@ class AppResponse(object):
class CPWSGIApp(object): class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application.""" """A WSGI application object for a CherryPy Application."""
pipeline = [ pipeline = [
('ExceptionTrapper', ExceptionTrapper), ('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector), ('InternalRedirector', InternalRedirector),
] ]
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a """A list of (name, wsgiapp) pairs.
constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application Each 'wsgiapp' MUST be a constructor that takes an initial,
(that takes environ and start_response arguments). The 'name' can positional 'nextapp' argument, plus optional keyword arguments, and
be any you choose, and will correspond to keys in self.config.""" returns a WSGI application (that takes environ and start_response
arguments). The 'name' can be any you choose, and will correspond to
keys in self.config.
"""
head = None head = None
"""Rather than nest all apps in the pipeline on each call, it's only """Rather than nest all apps in the pipeline on each call, it's only
@ -399,9 +404,12 @@ class CPWSGIApp(object):
this to None again if you change self.pipeline after calling self.""" this to None again if you change self.pipeline after calling self."""
config = {} config = {}
"""A dict whose keys match names listed in the pipeline. Each """A dict whose keys match names listed in the pipeline.
value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments.""" Each value is a further dict which will be passed to the
corresponding named WSGI callable (from the pipeline) as keyword
arguments.
"""
response_class = AppResponse response_class = AppResponse
"""The class to instantiate and return as the next app in the WSGI chain. """The class to instantiate and return as the next app in the WSGI chain.
@ -417,8 +425,8 @@ class CPWSGIApp(object):
def tail(self, environ, start_response): def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application. """WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead, You probably shouldn't call this; call self.__call__ instead, so
so that any WSGI middleware in self.pipeline can run first. that any WSGI middleware in self.pipeline can run first.
""" """
return self.response_class(environ, start_response, self.cpapp) return self.response_class(environ, start_response, self.cpapp)

View file

@ -1,7 +1,7 @@
""" """WSGI server interface (see PEP 333).
WSGI server interface (see PEP 333).
This adds some CP-specific bits to the framework-agnostic cheroot package. This adds some CP-specific bits to the framework-agnostic cheroot
package.
""" """
import sys import sys
@ -35,10 +35,11 @@ class CPWSGIHTTPRequest(cheroot.server.HTTPRequest):
class CPWSGIServer(cheroot.wsgi.Server): class CPWSGIServer(cheroot.wsgi.Server):
"""Wrapper for cheroot.wsgi.Server. """Wrapper for cheroot.wsgi.Server.
cheroot has been designed to not reference CherryPy in any way, cheroot has been designed to not reference CherryPy in any way, so
so that it can be used in other frameworks and applications. Therefore, that it can be used in other frameworks and applications. Therefore,
we wrap it here, so we can set our own mount points from cherrypy.tree we wrap it here, so we can set our own mount points from
and apply some attributes from config -> cherrypy.server -> wsgi.Server. cherrypy.tree and apply some attributes from config ->
cherrypy.server -> wsgi.Server.
""" """
fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}'

View file

@ -137,7 +137,6 @@ def popargs(*args, **kwargs):
class Root: class Root:
def index(self): def index(self):
#... #...
""" """
# Since keyword arg comes after *args, we have to process it ourselves # Since keyword arg comes after *args, we have to process it ourselves
# for lower versions of python. # for lower versions of python.
@ -201,16 +200,17 @@ def url(path='', qs='', script_name=None, base=None, relative=None):
If it does not start with a slash, this returns If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs). (base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used If script_name is None, cherrypy.request will be used to find a
to find a script_name, if available. script_name, if available.
If base is None, cherrypy.request.base will be used (if available). If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this. Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL Finally, note that this function can be used to obtain an absolute
for the current request path (minus the querystring) by passing no args. URL for the current request path (minus the querystring) by passing
If you call url(qs=cherrypy.request.query_string), you should get the no args. If you call url(qs=cherrypy.request.query_string), you
original browser URL (assuming no internal redirections). should get the original browser URL (assuming no internal
redirections).
If relative is None or not provided, request.app.relative_urls will If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an be used (if available, else False). If False, the output will be an
@ -320,8 +320,8 @@ def normalize_path(path):
class _ClassPropertyDescriptor(object): class _ClassPropertyDescriptor(object):
"""Descript for read-only class-based property. """Descript for read-only class-based property.
Turns a classmethod-decorated func into a read-only property of that class Turns a classmethod-decorated func into a read-only property of that
type (means the value cannot be set). class type (means the value cannot be set).
""" """
def __init__(self, fget, fset=None): def __init__(self, fget, fset=None):

View file

@ -1,5 +1,4 @@
""" """JSON support.
JSON support.
Expose preferred json module as json and provide encode/decode Expose preferred json module as json and provide encode/decode
convenience functions. convenience functions.

View file

@ -6,8 +6,8 @@ def is_iterator(obj):
(i.e. like a generator). (i.e. like a generator).
This will return False for objects which are iterable, This will return False for objects which are iterable, but not
but not iterators themselves. iterators themselves.
""" """
from types import GeneratorType from types import GeneratorType
if isinstance(obj, GeneratorType): if isinstance(obj, GeneratorType):

View file

@ -18,7 +18,6 @@ as the credentials store::
'tools.auth_basic.accept_charset': 'UTF-8', 'tools.auth_basic.accept_charset': 'UTF-8',
} }
app_config = { '/' : basic_auth } app_config = { '/' : basic_auth }
""" """
import binascii import binascii

View file

@ -55,7 +55,7 @@ def TRACE(msg):
def get_ha1_dict_plain(user_password_dict): def get_ha1_dict_plain(user_password_dict):
"""Returns a get_ha1 function which obtains a plaintext password from a """Return a get_ha1 function which obtains a plaintext password from a
dictionary of the form: {username : password}. dictionary of the form: {username : password}.
If you want a simple dictionary-based authentication scheme, with plaintext If you want a simple dictionary-based authentication scheme, with plaintext
@ -72,7 +72,7 @@ def get_ha1_dict_plain(user_password_dict):
def get_ha1_dict(user_ha1_dict): def get_ha1_dict(user_ha1_dict):
"""Returns a get_ha1 function which obtains a HA1 password hash from a """Return a get_ha1 function which obtains a HA1 password hash from a
dictionary of the form: {username : HA1}. dictionary of the form: {username : HA1}.
If you want a dictionary-based authentication scheme, but with If you want a dictionary-based authentication scheme, but with
@ -87,7 +87,7 @@ def get_ha1_dict(user_ha1_dict):
def get_ha1_file_htdigest(filename): def get_ha1_file_htdigest(filename):
"""Returns a get_ha1 function which obtains a HA1 password hash from a """Return a get_ha1 function which obtains a HA1 password hash from a
flat file with lines of the same format as that produced by the Apache flat file with lines of the same format as that produced by the Apache
htdigest utility. For example, for realm 'wonderland', username 'alice', htdigest utility. For example, for realm 'wonderland', username 'alice',
and password '4x5istwelve', the htdigest line would be:: and password '4x5istwelve', the htdigest line would be::
@ -135,7 +135,7 @@ def synthesize_nonce(s, key, timestamp=None):
def H(s): def H(s):
"""The hash function H""" """The hash function H."""
return md5_hex(s) return md5_hex(s)
@ -259,10 +259,11 @@ class HttpDigestAuthorization(object):
return False return False
def is_nonce_stale(self, max_age_seconds=600): def is_nonce_stale(self, max_age_seconds=600):
"""Returns True if a validated nonce is stale. The nonce contains a """Return True if a validated nonce is stale.
timestamp in plaintext and also a secure hash of the timestamp.
You should first validate the nonce to ensure the plaintext The nonce contains a timestamp in plaintext and also a secure
timestamp is not spoofed. hash of the timestamp. You should first validate the nonce to
ensure the plaintext timestamp is not spoofed.
""" """
try: try:
timestamp, hashpart = self.nonce.split(':', 1) timestamp, hashpart = self.nonce.split(':', 1)
@ -275,7 +276,10 @@ class HttpDigestAuthorization(object):
return True return True
def HA2(self, entity_body=''): def HA2(self, entity_body=''):
"""Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" """Return the H(A2) string.
See :rfc:`2617` section 3.2.2.3.
"""
# RFC 2617 3.2.2.3 # RFC 2617 3.2.2.3
# If the "qop" directive's value is "auth" or is unspecified, # If the "qop" directive's value is "auth" or is unspecified,
# then A2 is: # then A2 is:
@ -306,7 +310,6 @@ class HttpDigestAuthorization(object):
4.3. This refers to the entity the user agent sent in the 4.3. This refers to the entity the user agent sent in the
request which has the Authorization header. Typically GET request which has the Authorization header. Typically GET
requests don't have an entity, and POST requests do. requests don't have an entity, and POST requests do.
""" """
ha2 = self.HA2(entity_body) ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1 # Request-Digest -- RFC 2617 3.2.2.1
@ -395,7 +398,6 @@ def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
key key
A secret string known only to the server, used in the synthesis A secret string known only to the server, used in the synthesis
of nonces. of nonces.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
@ -447,9 +449,7 @@ def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
def _respond_401(realm, key, accept_charset, debug, **kwargs): def _respond_401(realm, key, accept_charset, debug, **kwargs):
""" """Respond with 401 status and a WWW-Authenticate header."""
Respond with 401 status and a WWW-Authenticate header
"""
header = www_authenticate( header = www_authenticate(
realm, key, realm, key,
accept_charset=accept_charset, accept_charset=accept_charset,

View file

@ -42,7 +42,6 @@ from cherrypy.lib import cptools, httputil
class Cache(object): class Cache(object):
"""Base class for Cache implementations.""" """Base class for Cache implementations."""
def get(self): def get(self):
@ -64,17 +63,16 @@ class Cache(object):
# ------------------------------ Memory Cache ------------------------------- # # ------------------------------ Memory Cache ------------------------------- #
class AntiStampedeCache(dict): class AntiStampedeCache(dict):
"""A storage system for cached items which reduces stampede collisions.""" """A storage system for cached items which reduces stampede collisions."""
def wait(self, key, timeout=5, debug=False): def wait(self, key, timeout=5, debug=False):
"""Return the cached value for the given key, or None. """Return the cached value for the given key, or None.
If timeout is not None, and the value is already If timeout is not None, and the value is already being
being calculated by another thread, wait until the given timeout has calculated by another thread, wait until the given timeout has
elapsed. If the value is available before the timeout expires, it is elapsed. If the value is available before the timeout expires,
returned. If not, None is returned, and a sentinel placed in the cache it is returned. If not, None is returned, and a sentinel placed
to signal other threads to wait. in the cache to signal other threads to wait.
If timeout is None, no waiting is performed nor sentinels used. If timeout is None, no waiting is performed nor sentinels used.
""" """
@ -127,7 +125,6 @@ class AntiStampedeCache(dict):
class MemoryCache(Cache): class MemoryCache(Cache):
"""An in-memory cache for varying response content. """An in-memory cache for varying response content.
Each key in self.store is a URI, and each value is an AntiStampedeCache. Each key in self.store is a URI, and each value is an AntiStampedeCache.
@ -381,7 +378,10 @@ def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
def tee_output(): def tee_output():
"""Tee response output to cache storage. Internal.""" """Tee response output to cache storage.
Internal.
"""
# Used by CachingTool by attaching to request.hooks # Used by CachingTool by attaching to request.hooks
request = cherrypy.serving.request request = cherrypy.serving.request
@ -441,7 +441,6 @@ def expires(secs=0, force=False, debug=False):
* Expires * Expires
If any are already present, none of the above response headers are set. If any are already present, none of the above response headers are set.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response

View file

@ -22,7 +22,7 @@ it will call ``serve()`` for you.
import re import re
import sys import sys
import cgi import html
import os import os
import os.path import os.path
import urllib.parse import urllib.parse
@ -352,9 +352,9 @@ class CoverStats(object):
buffer.append((lineno, line)) buffer.append((lineno, line))
if empty_the_buffer: if empty_the_buffer:
for lno, pastline in buffer: for lno, pastline in buffer:
yield template % (lno, cgi.escape(pastline)) yield template % (lno, html.escape(pastline))
buffer = [] buffer = []
yield template % (lineno, cgi.escape(line)) yield template % (lineno, html.escape(line))
@cherrypy.expose @cherrypy.expose
def report(self, name): def report(self, name):

View file

@ -184,7 +184,6 @@ To report statistics::
To format statistics reports:: To format statistics reports::
See 'Reporting', above. See 'Reporting', above.
""" """
import logging import logging
@ -254,7 +253,6 @@ def proc_time(s):
class ByteCountWrapper(object): class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read.""" """Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile): def __init__(self, rfile):
@ -307,7 +305,6 @@ def _get_threading_ident():
class StatsTool(cherrypy.Tool): class StatsTool(cherrypy.Tool):
"""Record various information about the current request.""" """Record various information about the current request."""
def __init__(self): def __init__(self):
@ -316,8 +313,8 @@ class StatsTool(cherrypy.Tool):
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call
method when the tool is "turned on" in config. this method when the tool is "turned on" in config.
""" """
if appstats.get('Enabled', False): if appstats.get('Enabled', False):
cherrypy.Tool._setup(self) cherrypy.Tool._setup(self)

View file

@ -94,8 +94,8 @@ def validate_etags(autotags=False, debug=False):
def validate_since(): def validate_since():
"""Validate the current Last-Modified against If-Modified-Since headers. """Validate the current Last-Modified against If-Modified-Since headers.
If no code has set the Last-Modified response header, then no validation If no code has set the Last-Modified response header, then no
will be performed. validation will be performed.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
lastmod = response.headers.get('Last-Modified') lastmod = response.headers.get('Last-Modified')
@ -123,9 +123,9 @@ def validate_since():
def allow(methods=None, debug=False): def allow(methods=None, debug=False):
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']). """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
The given methods are case-insensitive, and may be in any order. The given methods are case-insensitive, and may be in any order. If
If only one method is allowed, you may supply a single string; only one method is allowed, you may supply a single string; if more
if more than one, supply a list of strings. than one, supply a list of strings.
Regardless of whether the current method is allowed or not, this Regardless of whether the current method is allowed or not, this
also emits an 'Allow' response header, containing the given methods. also emits an 'Allow' response header, containing the given methods.
@ -154,22 +154,23 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme='X-Forwarded-Proto', debug=False): scheme='X-Forwarded-Proto', debug=False):
"""Change the base URL (scheme://host[:port][/path]). """Change the base URL (scheme://host[:port][/path]).
For running a CP server behind Apache, lighttpd, or other HTTP server. For running a CP server behind Apache, lighttpd, or other HTTP
server.
For Apache and lighttpd, you should leave the 'local' argument at the For Apache and lighttpd, you should leave the 'local' argument at
default value of 'X-Forwarded-Host'. For Squid, you probably want to set the default value of 'X-Forwarded-Host'. For Squid, you probably
tools.proxy.local = 'Origin'. want to set tools.proxy.local = 'Origin'.
If you want the new request.base to include path info (not just the host), If you want the new request.base to include path info (not just the
you must explicitly set base to the full base path, and ALSO set 'local' host), you must explicitly set base to the full base path, and ALSO
to '', so that the X-Forwarded-Host request header (which never includes set 'local' to '', so that the X-Forwarded-Host request header
path info) does not override it. Regardless, the value for 'base' MUST (which never includes path info) does not override it. Regardless,
NOT end in a slash. the value for 'base' MUST NOT end in a slash.
cherrypy.request.remote.ip (the IP address of the client) will be cherrypy.request.remote.ip (the IP address of the client) will be
rewritten if the header specified by the 'remote' arg is valid. rewritten if the header specified by the 'remote' arg is valid. By
By default, 'remote' is set to 'X-Forwarded-For'. If you do not default, 'remote' is set to 'X-Forwarded-For'. If you do not want to
want to rewrite remote.ip, set the 'remote' arg to an empty string. rewrite remote.ip, set the 'remote' arg to an empty string.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
@ -217,8 +218,8 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
def ignore_headers(headers=('Range',), debug=False): def ignore_headers(headers=('Range',), debug=False):
"""Delete request headers whose field names are included in 'headers'. """Delete request headers whose field names are included in 'headers'.
This is a useful tool for working behind certain HTTP servers; This is a useful tool for working behind certain HTTP servers; for
for example, Apache duplicates the work that CP does for 'Range' example, Apache duplicates the work that CP does for 'Range'
headers, and will doubly-truncate the response. headers, and will doubly-truncate the response.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
@ -281,7 +282,6 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
class SessionAuth(object): class SessionAuth(object):
"""Assert that the user is logged in.""" """Assert that the user is logged in."""
session_key = 'username' session_key = 'username'
@ -319,7 +319,10 @@ Message: %(error_msg)s
</body></html>""") % vars()).encode('utf-8') </body></html>""") % vars()).encode('utf-8')
def do_login(self, username, password, from_page='..', **kwargs): def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled.""" """Login.
May raise redirect, or return True if request handled.
"""
response = cherrypy.serving.response response = cherrypy.serving.response
error_msg = self.check_username_and_password(username, password) error_msg = self.check_username_and_password(username, password)
if error_msg: if error_msg:
@ -336,7 +339,10 @@ Message: %(error_msg)s
raise cherrypy.HTTPRedirect(from_page or '/') raise cherrypy.HTTPRedirect(from_page or '/')
def do_logout(self, from_page='..', **kwargs): def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled.""" """Logout.
May raise redirect, or return True if request handled.
"""
sess = cherrypy.session sess = cherrypy.session
username = sess.get(self.session_key) username = sess.get(self.session_key)
sess[self.session_key] = None sess[self.session_key] = None
@ -346,7 +352,9 @@ Message: %(error_msg)s
raise cherrypy.HTTPRedirect(from_page) raise cherrypy.HTTPRedirect(from_page)
def do_check(self): def do_check(self):
"""Assert username. Raise redirect, or return True if request handled. """Assert username.
Raise redirect, or return True if request handled.
""" """
sess = cherrypy.session sess = cherrypy.session
request = cherrypy.serving.request request = cherrypy.serving.request
@ -408,8 +416,7 @@ def session_auth(**kwargs):
Any attribute of the SessionAuth class may be overridden Any attribute of the SessionAuth class may be overridden
via a keyword arg to this function: via a keyword arg to this function:
""" + '\n' + '\n '.join(
""" + '\n '.join(
'{!s}: {!s}'.format(k, type(getattr(SessionAuth, k)).__name__) '{!s}: {!s}'.format(k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) for k in dir(SessionAuth)
if not k.startswith('__') if not k.startswith('__')
@ -490,8 +497,8 @@ def trailing_slash(missing=True, extra=False, status=None, debug=False):
def flatten(debug=False): def flatten(debug=False):
"""Wrap response.body in a generator that recursively iterates over body. """Wrap response.body in a generator that recursively iterates over body.
This allows cherrypy.response.body to consist of 'nested generators'; This allows cherrypy.response.body to consist of 'nested
that is, a set of generators that yield generators. generators'; that is, a set of generators that yield generators.
""" """
def flattener(input): def flattener(input):
numchunks = 0 numchunks = 0

View file

@ -261,9 +261,7 @@ class ResponseEncoder:
def prepare_iter(value): def prepare_iter(value):
""" """Ensure response body is iterable and resolves to False when empty."""
Ensure response body is iterable and resolves to False when empty.
"""
if isinstance(value, text_or_bytes): if isinstance(value, text_or_bytes):
# strings get wrapped in a list because iterating over a single # strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character # item list is much faster than iterating over every character
@ -360,7 +358,6 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
* The 'identity' value is given with a qvalue > 0. * The 'identity' value is given with a qvalue > 0.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response

View file

@ -14,7 +14,6 @@ from cherrypy.process.plugins import SimplePlugin
class ReferrerTree(object): class ReferrerTree(object):
"""An object which gathers all referrers of an object to a given depth.""" """An object which gathers all referrers of an object to a given depth."""
peek_length = 40 peek_length = 40
@ -132,7 +131,6 @@ def get_context(obj):
class GCRoot(object): class GCRoot(object):
"""A CherryPy page handler for testing reference leaks.""" """A CherryPy page handler for testing reference leaks."""
classes = [ classes = [

View file

@ -0,0 +1,39 @@
"""headers."""
def _parse_param(s):
while s[:1] == ';':
s = s[1:]
end = s.find(';')
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
end = s.find(';', end + 1)
if end < 0:
end = len(s)
f = s[:end]
yield f.strip()
s = s[end:]
def parse_header(line):
"""Parse a Content-type like header.
Return the main content-type and a dictionary of options.
Copied from removed stdlib cgi module. See
`cherrypy/cherrypy#2014 (comment)
<https://github.com/cherrypy/cherrypy/issues/2014#issuecomment-1883774891>`_
for background.
"""
parts = _parse_param(';' + line)
key = parts.__next__()
pdict = {}
for p in parts:
i = p.find('=')
if i >= 0:
name = p[:i].strip().lower()
value = p[i + 1:].strip()
if len(value) >= 2 and value[0] == value[-1] == '"':
value = value[1:-1]
value = value.replace('\\\\', '\\').replace('\\"', '"')
pdict[name] = value
return key, pdict

View file

@ -12,7 +12,6 @@ import email.utils
import re import re
import builtins import builtins
from binascii import b2a_base64 from binascii import b2a_base64
from cgi import parse_header
from email.header import decode_header from email.header import decode_header
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from urllib.parse import unquote_plus from urllib.parse import unquote_plus
@ -21,6 +20,7 @@ import jaraco.collections
import cherrypy import cherrypy
from cherrypy._cpcompat import ntob, ntou from cherrypy._cpcompat import ntob, ntou
from .headers import parse_header
response_codes = BaseHTTPRequestHandler.responses.copy() response_codes = BaseHTTPRequestHandler.responses.copy()
@ -71,10 +71,10 @@ def protocol_from_http(protocol_str):
def get_ranges(headervalue, content_length): def get_ranges(headervalue, content_length):
"""Return a list of (start, stop) indices from a Range header, or None. """Return a list of (start, stop) indices from a Range header, or None.
Each (start, stop) tuple will be composed of two ints, which are suitable Each (start, stop) tuple will be composed of two ints, which are
for use in a slicing operation. That is, the header "Range: bytes=3-6", suitable for use in a slicing operation. That is, the header "Range:
if applied against a Python string, is requesting resource[3:7]. This bytes=3-6", if applied against a Python string, is requesting
function will return the list [(3, 7)]. resource[3:7]. This function will return the list [(3, 7)].
If this function returns an empty list, you should return HTTP 416. If this function returns an empty list, you should return HTTP 416.
""" """
@ -127,7 +127,6 @@ def get_ranges(headervalue, content_length):
class HeaderElement(object): class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list.""" """An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None): def __init__(self, value, params=None):
@ -169,14 +168,14 @@ q_separator = re.compile(r'; *q *=')
class AcceptElement(HeaderElement): class AcceptElement(HeaderElement):
"""An element (with parameters) from an Accept* header's element list. """An element (with parameters) from an Accept* header's element list.
AcceptElement objects are comparable; the more-preferred object will be AcceptElement objects are comparable; the more-preferred object will
"less than" the less-preferred object. They are also therefore sortable; be "less than" the less-preferred object. They are also therefore
if you sort a list of AcceptElement objects, they will be listed in sortable; if you sort a list of AcceptElement objects, they will be
priority order; the most preferred value will be first. Yes, it should listed in priority order; the most preferred value will be first.
have been the other way around, but it's too late to fix now. Yes, it should have been the other way around, but it's too late to
fix now.
""" """
@classmethod @classmethod
@ -249,8 +248,7 @@ def header_elements(fieldname, fieldvalue):
def decode_TEXT(value): def decode_TEXT(value):
r""" r"""Decode :rfc:`2047` TEXT.
Decode :rfc:`2047` TEXT
>>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1')
True True
@ -265,9 +263,7 @@ def decode_TEXT(value):
def decode_TEXT_maybe(value): def decode_TEXT_maybe(value):
""" """Decode the text but only if '=?' appears in it."""
Decode the text but only if '=?' appears in it.
"""
return decode_TEXT(value) if '=?' in value else value return decode_TEXT(value) if '=?' in value else value
@ -388,7 +384,6 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict): class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict):
"""A case-insensitive dict subclass. """A case-insensitive dict subclass.
Each key is changed on entry to title case. Each key is changed on entry to title case.
@ -417,7 +412,6 @@ else:
class HeaderMap(CaseInsensitiveDict): class HeaderMap(CaseInsensitiveDict):
"""A dict subclass for HTTP request and response headers. """A dict subclass for HTTP request and response headers.
Each key is changed on entry to str(key).title(). This allows headers Each key is changed on entry to str(key).title(). This allows headers
@ -494,7 +488,6 @@ class HeaderMap(CaseInsensitiveDict):
class Host(object): class Host(object):
"""An internet address. """An internet address.
name name

View file

@ -7,22 +7,22 @@ class NeverExpires(object):
class Timer(object): class Timer(object):
""" """A simple timer that will indicate when an expiration time has passed."""
A simple timer that will indicate when an expiration time has passed.
"""
def __init__(self, expiration): def __init__(self, expiration):
'Create a timer that expires at `expiration` (UTC datetime)' 'Create a timer that expires at `expiration` (UTC datetime)'
self.expiration = expiration self.expiration = expiration
@classmethod @classmethod
def after(cls, elapsed): def after(cls, elapsed):
""" """Return a timer that will expire after `elapsed` passes."""
Return a timer that will expire after `elapsed` passes. return cls(
""" datetime.datetime.now(datetime.timezone.utc) + elapsed,
return cls(datetime.datetime.utcnow() + elapsed) )
def expired(self): def expired(self):
return datetime.datetime.utcnow() >= self.expiration return datetime.datetime.now(
datetime.timezone.utc,
) >= self.expiration
class LockTimeout(Exception): class LockTimeout(Exception):
@ -30,9 +30,7 @@ class LockTimeout(Exception):
class LockChecker(object): class LockChecker(object):
""" """Keep track of the time and detect if a timeout has expired."""
Keep track of the time and detect if a timeout has expired
"""
def __init__(self, session_id, timeout): def __init__(self, session_id, timeout):
self.session_id = session_id self.session_id = session_id
if timeout: if timeout:

View file

@ -30,7 +30,6 @@ to get a quick sanity-check on overall CP performance. Use the
``--profile`` flag when running the test suite. Then, use the ``serve()`` ``--profile`` flag when running the test suite. Then, use the ``serve()``
function to browse the results in a web browser. If you run this function to browse the results in a web browser. If you run this
module from the command line, it will call ``serve()`` for you. module from the command line, it will call ``serve()`` for you.
""" """
import io import io

View file

@ -27,18 +27,17 @@ from cherrypy._cpcompat import text_or_bytes
class NamespaceSet(dict): class NamespaceSet(dict):
"""A dict of config namespace names and handlers. """A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding Each config entry should begin with a namespace name; the
namespace handler will be called once for each config entry in that corresponding namespace handler will be called once for each config
namespace, and will be passed two arguments: the config key (with the entry in that namespace, and will be passed two arguments: the
namespace removed) and the config value. config key (with the namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be Namespace handlers may be any Python callable; they may also be
context managers, in which case their __enter__ context managers, in which case their __enter__ method should return
method should return a callable to be used as the handler. a callable to be used as the handler. See cherrypy.tools (the
See cherrypy.tools (the Toolbox class) for an example. Toolbox class) for an example.
""" """
def __call__(self, config): def __call__(self, config):
@ -48,9 +47,10 @@ class NamespaceSet(dict):
A flat dict, where keys use dots to separate A flat dict, where keys use dots to separate
namespaces, and values are arbitrary. namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding The first name in each config key is used to look up the
namespace handler. For example, a config entry of {'tools.gzip.on': v} corresponding namespace handler. For example, a config entry of
will call the 'tools' namespace handler with the args: ('gzip.on', v) {'tools.gzip.on': v} will call the 'tools' namespace handler
with the args: ('gzip.on', v)
""" """
# Separate the given config into namespaces # Separate the given config into namespaces
ns_confs = {} ns_confs = {}
@ -103,7 +103,6 @@ class NamespaceSet(dict):
class Config(dict): class Config(dict):
"""A dict-like set of configuration data, with defaults and namespaces. """A dict-like set of configuration data, with defaults and namespaces.
May take a file, filename, or dict. May take a file, filename, or dict.
@ -167,7 +166,7 @@ class Parser(configparser.ConfigParser):
self._read(fp, filename) self._read(fp, filename)
def as_dict(self, raw=False, vars=None): def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary""" """Convert an INI file to a dictionary."""
# Load INI file into a dict # Load INI file into a dict
result = {} result = {}
for section in self.sections(): for section in self.sections():

View file

@ -120,7 +120,6 @@ missing = object()
class Session(object): class Session(object):
"""A CherryPy dict-like Session object (one per request).""" """A CherryPy dict-like Session object (one per request)."""
_id = None _id = None
@ -148,9 +147,11 @@ class Session(object):
to session data.""" to session data."""
loaded = False loaded = False
"""If True, data has been retrieved from storage.
This should happen automatically on the first attempt to access
session data.
""" """
If True, data has been retrieved from storage. This should happen
automatically on the first attempt to access session data."""
clean_thread = None clean_thread = None
'Class-level Monitor which calls self.clean_up.' 'Class-level Monitor which calls self.clean_up.'
@ -165,9 +166,10 @@ class Session(object):
'True if the session requested by the client did not exist.' 'True if the session requested by the client did not exist.'
regenerated = False regenerated = False
"""True if the application called session.regenerate().
This is not set by internal calls to regenerate the session id.
""" """
True if the application called session.regenerate(). This is not set by
internal calls to regenerate the session id."""
debug = False debug = False
'If True, log debug information.' 'If True, log debug information.'
@ -335,8 +337,9 @@ class Session(object):
def pop(self, key, default=missing): def pop(self, key, default=missing):
"""Remove the specified key and return the corresponding value. """Remove the specified key and return the corresponding value.
If key is not found, default is returned if given,
otherwise KeyError is raised. If key is not found, default is returned if given, otherwise
KeyError is raised.
""" """
if not self.loaded: if not self.loaded:
self.load() self.load()
@ -351,13 +354,19 @@ class Session(object):
return key in self._data return key in self._data
def get(self, key, default=None): def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" """D.get(k[,d]) -> D[k] if k in D, else d.
d defaults to None.
"""
if not self.loaded: if not self.loaded:
self.load() self.load()
return self._data.get(key, default) return self._data.get(key, default)
def update(self, d): def update(self, d):
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" """D.update(E) -> None.
Update D from E: for k in E: D[k] = E[k].
"""
if not self.loaded: if not self.loaded:
self.load() self.load()
self._data.update(d) self._data.update(d)
@ -369,7 +378,10 @@ class Session(object):
return self._data.setdefault(key, default) return self._data.setdefault(key, default)
def clear(self): def clear(self):
"""D.clear() -> None. Remove all items from D.""" """D.clear() -> None.
Remove all items from D.
"""
if not self.loaded: if not self.loaded:
self.load() self.load()
self._data.clear() self._data.clear()
@ -492,7 +504,8 @@ class FileSession(Session):
"""Set up the storage system for file-based sessions. """Set up the storage system for file-based sessions.
This should only be called once per process; this will be done This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does). automatically when using sessions.init (as the built-in Tool
does).
""" """
# The 'storage_path' arg is required for file-based sessions. # The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
@ -616,7 +629,8 @@ class MemcachedSession(Session):
"""Set up the storage system for memcached-based sessions. """Set up the storage system for memcached-based sessions.
This should only be called once per process; this will be done This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does). automatically when using sessions.init (as the built-in Tool
does).
""" """
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)

View file

@ -56,15 +56,15 @@ def serve_file(path, content_type=None, disposition=None, name=None,
debug=False): debug=False):
"""Set status, headers, and body in order to serve the given path. """Set status, headers, and body in order to serve the given path.
The Content-Type header will be set to the content_type arg, if provided. The Content-Type header will be set to the content_type arg, if
If not provided, the Content-Type will be guessed by the file extension provided. If not provided, the Content-Type will be guessed by the
of the 'path' argument. file extension of the 'path' argument.
If disposition is not None, the Content-Disposition header will be set If disposition is not None, the Content-Disposition header will be
to "<disposition>; filename=<name>; filename*=utf-8''<name>" set to "<disposition>; filename=<name>; filename*=utf-8''<name>" as
as described in :rfc:`6266#appendix-D`. described in :rfc:`6266#appendix-D`. If name is None, it will be set
If name is None, it will be set to the basename of path. to the basename of path. If disposition is None, no Content-
If disposition is None, no Content-Disposition header will be written. Disposition header will be written.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response

View file

@ -31,7 +31,6 @@ _module__file__base = os.getcwd()
class SimplePlugin(object): class SimplePlugin(object):
"""Plugin base class which auto-subscribes methods for known channels.""" """Plugin base class which auto-subscribes methods for known channels."""
bus = None bus = None
@ -59,7 +58,6 @@ class SimplePlugin(object):
class SignalHandler(object): class SignalHandler(object):
"""Register bus channels (and listeners) for system signals. """Register bus channels (and listeners) for system signals.
You can modify what signals your application listens for, and what it does You can modify what signals your application listens for, and what it does
@ -171,8 +169,8 @@ class SignalHandler(object):
If the optional 'listener' argument is provided, it will be If the optional 'listener' argument is provided, it will be
subscribed as a listener for the given signal's channel. subscribed as a listener for the given signal's channel.
If the given signal name or number is not available on the current If the given signal name or number is not available on the
platform, ValueError is raised. current platform, ValueError is raised.
""" """
if isinstance(signal, text_or_bytes): if isinstance(signal, text_or_bytes):
signum = getattr(_signal, signal, None) signum = getattr(_signal, signal, None)
@ -218,11 +216,10 @@ except ImportError:
class DropPrivileges(SimplePlugin): class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows. """Drop privileges. uid/gid arguments not available on Windows.
Special thanks to `Gavin Baker Special thanks to `Gavin Baker
<http://antonym.org/2005/12/dropping-privileges-in-python.html>`_ <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_.
""" """
def __init__(self, bus, umask=None, uid=None, gid=None): def __init__(self, bus, umask=None, uid=None, gid=None):
@ -234,7 +231,10 @@ class DropPrivileges(SimplePlugin):
@property @property
def uid(self): def uid(self):
"""The uid under which to run. Availability: Unix.""" """The uid under which to run.
Availability: Unix.
"""
return self._uid return self._uid
@uid.setter @uid.setter
@ -250,7 +250,10 @@ class DropPrivileges(SimplePlugin):
@property @property
def gid(self): def gid(self):
"""The gid under which to run. Availability: Unix.""" """The gid under which to run.
Availability: Unix.
"""
return self._gid return self._gid
@gid.setter @gid.setter
@ -332,7 +335,6 @@ class DropPrivileges(SimplePlugin):
class Daemonizer(SimplePlugin): class Daemonizer(SimplePlugin):
"""Daemonize the running script. """Daemonize the running script.
Use this with a Web Site Process Bus via:: Use this with a Web Site Process Bus via::
@ -423,7 +425,6 @@ class Daemonizer(SimplePlugin):
class PIDFile(SimplePlugin): class PIDFile(SimplePlugin):
"""Maintain a PID file via a WSPBus.""" """Maintain a PID file via a WSPBus."""
def __init__(self, bus, pidfile): def __init__(self, bus, pidfile):
@ -453,12 +454,11 @@ class PIDFile(SimplePlugin):
class PerpetualTimer(threading.Timer): class PerpetualTimer(threading.Timer):
"""A responsive subclass of threading.Timer whose run() method repeats. """A responsive subclass of threading.Timer whose run() method repeats.
Use this timer only when you really need a very interruptible timer; Use this timer only when you really need a very interruptible timer;
this checks its 'finished' condition up to 20 times a second, which can this checks its 'finished' condition up to 20 times a second, which
results in pretty high CPU usage can results in pretty high CPU usage
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -483,14 +483,14 @@ class PerpetualTimer(threading.Timer):
class BackgroundTask(threading.Thread): class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats. """A subclass of threading.Thread whose run() method repeats.
Use this class for most repeating tasks. It uses time.sleep() to wait Use this class for most repeating tasks. It uses time.sleep() to
for each interval, which isn't very responsive; that is, even if you call wait for each interval, which isn't very responsive; that is, even
self.cancel(), you'll have to wait until the sleep() call finishes before if you call self.cancel(), you'll have to wait until the sleep()
the thread stops. To compensate, it defaults to being daemonic, which means call finishes before the thread stops. To compensate, it defaults to
it won't delay stopping the whole process. being daemonic, which means it won't delay stopping the whole
process.
""" """
def __init__(self, interval, function, args=[], kwargs={}, bus=None): def __init__(self, interval, function, args=[], kwargs={}, bus=None):
@ -525,7 +525,6 @@ class BackgroundTask(threading.Thread):
class Monitor(SimplePlugin): class Monitor(SimplePlugin):
"""WSPBus listener to periodically run a callback in its own thread.""" """WSPBus listener to periodically run a callback in its own thread."""
callback = None callback = None
@ -582,7 +581,6 @@ class Monitor(SimplePlugin):
class Autoreloader(Monitor): class Autoreloader(Monitor):
"""Monitor which re-executes the process when files change. """Monitor which re-executes the process when files change.
This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`) This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
@ -699,20 +697,20 @@ class Autoreloader(Monitor):
class ThreadManager(SimplePlugin): class ThreadManager(SimplePlugin):
"""Manager for HTTP request threads. """Manager for HTTP request threads.
If you have control over thread creation and destruction, publish to If you have control over thread creation and destruction, publish to
the 'acquire_thread' and 'release_thread' channels (for each thread). the 'acquire_thread' and 'release_thread' channels (for each
This will register/unregister the current thread and publish to thread). This will register/unregister the current thread and
'start_thread' and 'stop_thread' listeners in the bus as needed. publish to 'start_thread' and 'stop_thread' listeners in the bus as
needed.
If threads are created and destroyed by code you do not control If threads are created and destroyed by code you do not control
(e.g., Apache), then, at the beginning of every HTTP request, (e.g., Apache), then, at the beginning of every HTTP request,
publish to 'acquire_thread' only. You should not publish to publish to 'acquire_thread' only. You should not publish to
'release_thread' in this case, since you do not know whether 'release_thread' in this case, since you do not know whether the
the thread will be re-used or not. The bus will call thread will be re-used or not. The bus will call 'stop_thread'
'stop_thread' listeners for you when it stops. listeners for you when it stops.
""" """
threads = None threads = None

View file

@ -132,7 +132,6 @@ class Timeouts:
class ServerAdapter(object): class ServerAdapter(object):
"""Adapter for an HTTP server. """Adapter for an HTTP server.
If you need to start more than one HTTP server (to serve on multiple If you need to start more than one HTTP server (to serve on multiple
@ -188,9 +187,7 @@ class ServerAdapter(object):
@property @property
def description(self): def description(self):
""" """A description about where this server is bound."""
A description about where this server is bound.
"""
if self.bind_addr is None: if self.bind_addr is None:
on_what = 'unknown interface (dynamic?)' on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple): elif isinstance(self.bind_addr, tuple):
@ -292,7 +289,6 @@ class ServerAdapter(object):
class FlupCGIServer(object): class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer.""" """Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -316,7 +312,6 @@ class FlupCGIServer(object):
class FlupFCGIServer(object): class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer.""" """Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -362,7 +357,6 @@ class FlupFCGIServer(object):
class FlupSCGIServer(object): class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer.""" """Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View file

@ -1,4 +1,7 @@
"""Windows service. Requires pywin32.""" """Windows service.
Requires pywin32.
"""
import os import os
import win32api import win32api
@ -11,7 +14,6 @@ from cherrypy.process import wspbus, plugins
class ConsoleCtrlHandler(plugins.SimplePlugin): class ConsoleCtrlHandler(plugins.SimplePlugin):
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
def __init__(self, bus): def __init__(self, bus):
@ -69,10 +71,10 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
class Win32Bus(wspbus.Bus): class Win32Bus(wspbus.Bus):
"""A Web Site Process Bus implementation for Win32. """A Web Site Process Bus implementation for Win32.
Instead of time.sleep, this bus blocks using native win32event objects. Instead of time.sleep, this bus blocks using native win32event
objects.
""" """
def __init__(self): def __init__(self):
@ -120,7 +122,6 @@ class Win32Bus(wspbus.Bus):
class _ControlCodes(dict): class _ControlCodes(dict):
"""Control codes used to "signal" a service via ControlService. """Control codes used to "signal" a service via ControlService.
User-defined control codes are in the range 128-255. We generally use User-defined control codes are in the range 128-255. We generally use
@ -152,7 +153,6 @@ def signal_child(service, command):
class PyWebService(win32serviceutil.ServiceFramework): class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service.""" """Python Web Service."""
_svc_name_ = 'Python Web Service' _svc_name_ = 'Python Web Service'

View file

@ -57,7 +57,6 @@ the new state.::
| \ | | \ |
| V V | V V
STARTED <-- STARTING STARTED <-- STARTING
""" """
import atexit import atexit
@ -65,7 +64,7 @@ import atexit
try: try:
import ctypes import ctypes
except ImportError: except ImportError:
"""Google AppEngine is shipped without ctypes """Google AppEngine is shipped without ctypes.
:seealso: http://stackoverflow.com/a/6523777/70170 :seealso: http://stackoverflow.com/a/6523777/70170
""" """
@ -165,8 +164,8 @@ class Bus(object):
All listeners for a given channel are guaranteed to be called even All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the processing from inside a listener is to raise SystemExit and stop
whole server. the whole server.
""" """
states = states states = states
@ -312,8 +311,9 @@ class Bus(object):
def restart(self): def restart(self):
"""Restart the process (may close connections). """Restart the process (may close connections).
This method does not restart the process from the calling thread; This method does not restart the process from the calling
instead, it stops the bus and asks the main thread to call execv. thread; instead, it stops the bus and asks the main thread to
call execv.
""" """
self.execv = True self.execv = True
self.exit() self.exit()
@ -327,10 +327,11 @@ class Bus(object):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit. """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread. This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads After waiting for the EXITING state, it also waits for all
to terminate, and then calls os.execv if self.execv is True. This threads to terminate, and then calls os.execv if self.execv is
design allows another thread to call bus.restart, yet have the main True. This design allows another thread to call bus.restart, yet
thread perform the actual execv call (required on some platforms). have the main thread perform the actual execv call (required on
some platforms).
""" """
try: try:
self.wait(states.EXITING, interval=interval, channel='main') self.wait(states.EXITING, interval=interval, channel='main')
@ -379,13 +380,14 @@ class Bus(object):
def _do_execv(self): def _do_execv(self):
"""Re-execute the current process. """Re-execute the current process.
This must be called from the main thread, because certain platforms This must be called from the main thread, because certain
(OS X) don't allow execv to be called in a child thread very well. platforms (OS X) don't allow execv to be called in a child
thread very well.
""" """
try: try:
args = self._get_true_argv() args = self._get_true_argv()
except NotImplementedError: except NotImplementedError:
"""It's probably win32 or GAE""" """It's probably win32 or GAE."""
args = [sys.executable] + self._get_interpreter_argv() + sys.argv args = [sys.executable] + self._get_interpreter_argv() + sys.argv
self.log('Re-spawning %s' % ' '.join(args)) self.log('Re-spawning %s' % ' '.join(args))
@ -472,7 +474,7 @@ class Bus(object):
c_ind = None c_ind = None
if is_module: if is_module:
"""It's containing `-m -m` sequence of arguments""" """It's containing `-m -m` sequence of arguments."""
if is_command and c_ind < m_ind: if is_command and c_ind < m_ind:
"""There's `-c -c` before `-m`""" """There's `-c -c` before `-m`"""
raise RuntimeError( raise RuntimeError(
@ -481,7 +483,7 @@ class Bus(object):
# Survive module argument here # Survive module argument here
original_module = sys.argv[0] original_module = sys.argv[0]
if not os.access(original_module, os.R_OK): if not os.access(original_module, os.R_OK):
"""There's no such module exist""" """There's no such module exist."""
raise AttributeError( raise AttributeError(
"{} doesn't seem to be a module " "{} doesn't seem to be a module "
'accessible by current user'.format(original_module)) 'accessible by current user'.format(original_module))
@ -489,7 +491,7 @@ class Bus(object):
# ... and substitute it with the original module path: # ... and substitute it with the original module path:
_argv.insert(m_ind, original_module) _argv.insert(m_ind, original_module)
elif is_command: elif is_command:
"""It's containing just `-c -c` sequence of arguments""" """It's containing just `-c -c` sequence of arguments."""
raise RuntimeError( raise RuntimeError(
"Cannot reconstruct command from '-c'. " "Cannot reconstruct command from '-c'. "
'Ref: https://github.com/cherrypy/cherrypy/issues/1545') 'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
@ -512,13 +514,13 @@ class Bus(object):
"""Prepend current working dir to PATH environment variable if needed. """Prepend current working dir to PATH environment variable if needed.
If sys.path[0] is an empty string, the interpreter was likely If sys.path[0] is an empty string, the interpreter was likely
invoked with -m and the effective path is about to change on invoked with -m and the effective path is about to change on re-
re-exec. Add the current directory to $PYTHONPATH to ensure exec. Add the current directory to $PYTHONPATH to ensure that
that the new process sees the same path. the new process sees the same path.
This issue cannot be addressed in the general case because This issue cannot be addressed in the general case because
Python cannot reliably reconstruct the Python cannot reliably reconstruct the original command line (
original command line (http://bugs.python.org/issue14208). http://bugs.python.org/issue14208).
(This idea filched from tornado.autoreload) (This idea filched from tornado.autoreload)
""" """
@ -536,10 +538,10 @@ class Bus(object):
"""Set the CLOEXEC flag on all open files (except stdin/out/err). """Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting platforms which support it, it represents the max open files
for the operating system. This function will be called just before setting for the operating system. This function will be called
the process is restarted via os.execv() to prevent open files just before the process is restarted via os.execv() to prevent
from persisting into the new process. open files from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior. Set self.max_cloexec_files to 0 to disable this behavior.
""" """
@ -578,7 +580,10 @@ class Bus(object):
return t return t
def log(self, msg='', level=20, traceback=False): def log(self, msg='', level=20, traceback=False):
"""Log the given message. Append the last traceback if requested.""" """Log the given message.
Append the last traceback if requested.
"""
if traceback: if traceback:
msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
self.publish('log', msg, level) self.publish('log', msg, level)

View file

@ -9,7 +9,6 @@ Even before any tweaking, this should serve a few demonstration pages.
Change to this directory and run: Change to this directory and run:
cherryd -c site.conf cherryd -c site.conf
""" """
import cherrypy import cherrypy

View file

@ -1,6 +1,4 @@
""" """Regression test suite for CherryPy."""
Regression test suite for CherryPy.
"""
import os import os
import sys import sys

View file

@ -1,4 +1,4 @@
"""Test module for the @-decorator syntax, which is version-specific""" """Test module for the @-decorator syntax, which is version-specific."""
import cherrypy import cherrypy
from cherrypy import expose, tools from cherrypy import expose, tools

View file

@ -1,24 +1,24 @@
"""CherryPy Benchmark Tool """CherryPy Benchmark Tool.
Usage: Usage:
benchmark.py [options] benchmark.py [options]
--null: use a null Request object (to bench the HTTP server only) --null: use a null Request object (to bench the HTTP server only)
--notests: start the server but do not run the tests; this allows --notests: start the server but do not run the tests; this allows
you to check the tested pages with a browser you to check the tested pages with a browser
--help: show this help message --help: show this help message
--cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy)
--modpython: run tests via apache on 54583 (with modpython_gateway) --modpython: run tests via apache on 54583 (with modpython_gateway)
--ab=path: Use the ab script/executable at 'path' (see below) --ab=path: Use the ab script/executable at 'path' (see below)
--apache=path: Use the apache script/exe at 'path' (see below) --apache=path: Use the apache script/exe at 'path' (see below)
To run the benchmarks, the Apache Benchmark tool "ab" must either be on To run the benchmarks, the Apache Benchmark tool "ab" must either be on
your system path, or specified via the --ab=path option. your system path, or specified via the --ab=path option.
To run the modpython tests, the "apache" executable or script must be To run the modpython tests, the "apache" executable or script must be
on your system path, or provided via the --apache=path option. On some on your system path, or provided via the --apache=path option. On some
platforms, "apache" may be called "apachectl" or "apache2ctl"--create platforms, "apache" may be called "apachectl" or "apache2ctl"--create
a symlink to them if needed. a symlink to them if needed.
""" """
import getopt import getopt
@ -106,7 +106,6 @@ def init():
class NullRequest: class NullRequest:
"""A null HTTP request class, returning 200 and an empty body.""" """A null HTTP request class, returning 200 and an empty body."""
def __init__(self, local, remote, scheme='http'): def __init__(self, local, remote, scheme='http'):
@ -131,65 +130,66 @@ class NullResponse:
class ABSession: class ABSession:
"""A session of 'ab', the Apache HTTP server benchmarking tool. """A session of 'ab', the Apache HTTP server benchmarking tool.
Example output from ab: Example output from ab:
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd,
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation,
http://www.apache.org/
Benchmarking 127.0.0.1 (be patient) Benchmarking 127.0.0.1 (be patient)
Completed 100 requests Completed 100 requests
Completed 200 requests Completed 200 requests
Completed 300 requests Completed 300 requests
Completed 400 requests Completed 400 requests
Completed 500 requests Completed 500 requests
Completed 600 requests Completed 600 requests
Completed 700 requests Completed 700 requests
Completed 800 requests Completed 800 requests
Completed 900 requests Completed 900 requests
Server Software: CherryPy/3.1beta Server Software: CherryPy/3.1beta
Server Hostname: 127.0.0.1 Server Hostname: 127.0.0.1
Server Port: 54583 Server Port: 54583
Document Path: /static/index.html Document Path: /static/index.html
Document Length: 14 bytes Document Length: 14 bytes
Concurrency Level: 10 Concurrency Level: 10
Time taken for tests: 9.643867 seconds Time taken for tests: 9.643867 seconds
Complete requests: 1000 Complete requests: 1000
Failed requests: 0 Failed requests: 0
Write errors: 0 Write errors: 0
Total transferred: 189000 bytes Total transferred: 189000 bytes
HTML transferred: 14000 bytes HTML transferred: 14000 bytes
Requests per second: 103.69 [#/sec] (mean) Requests per second: 103.69 [#/sec] (mean)
Time per request: 96.439 [ms] (mean) Time per request: 96.439 [ms] (mean)
Time per request: 9.644 [ms] (mean, across all concurrent requests) Time per request: 9.644 [ms] (mean, across all concurrent requests)
Transfer rate: 19.08 [Kbytes/sec] received Transfer rate: 19.08 [Kbytes/sec] received
Connection Times (ms) Connection Times (ms)
min mean[+/-sd] median max min mean[+/-sd] median max
Connect: 0 0 2.9 0 10 Connect: 0 0 2.9 0 10
Processing: 20 94 7.3 90 130 Processing: 20 94 7.3 90 130
Waiting: 0 43 28.1 40 100 Waiting: 0 43 28.1 40 100
Total: 20 95 7.3 100 130 Total: 20 95 7.3 100 130
Percentage of the requests served within a certain time (ms) Percentage of the requests served within a certain time (ms)
50% 100 50% 100
66% 100 66% 100
75% 100 75% 100
80% 100 80% 100
90% 100 90% 100
95% 100 95% 100
98% 100 98% 100
99% 110 99% 110
100% 130 (longest request) 100% 130 (longest request)
Finished 1000 requests Finished 1000 requests
""" """
parse_patterns = [ parse_patterns = [
('complete_requests', 'Completed', ('complete_requests', 'Completed',
@ -403,7 +403,6 @@ if __name__ == '__main__':
print('Starting CherryPy app server...') print('Starting CherryPy app server...')
class NullWriter(object): class NullWriter(object):
"""Suppresses the printing of socket errors.""" """Suppresses the printing of socket errors."""
def write(self, data): def write(self, data):

View file

@ -1,8 +1,8 @@
"""Demonstration app for cherrypy.checker. """Demonstration app for cherrypy.checker.
This application is intentionally broken and badly designed. This application is intentionally broken and badly designed. To
To demonstrate the output of the CherryPy Checker, simply execute demonstrate the output of the CherryPy Checker, simply execute this
this module. module.
""" """
import os import os

View file

@ -28,7 +28,6 @@ serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
class Supervisor(object): class Supervisor(object):
"""Base class for modeling and controlling servers during testing.""" """Base class for modeling and controlling servers during testing."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -43,14 +42,13 @@ def log_to_stderr(msg, level):
class LocalSupervisor(Supervisor): class LocalSupervisor(Supervisor):
"""Base class for modeling/controlling servers which run in the same """Base class for modeling/controlling servers which run in the same
process. process.
When the server side runs in a different process, start/stop can dump all When the server side runs in a different process, start/stop can
state between each test module easily. When the server side runs in the dump all state between each test module easily. When the server side
same process as the client, however, we have to do a bit more work to runs in the same process as the client, however, we have to do a bit
ensure config and mounted apps are reset between tests. more work to ensure config and mounted apps are reset between tests.
""" """
using_apache = False using_apache = False
@ -99,7 +97,6 @@ class LocalSupervisor(Supervisor):
class NativeServerSupervisor(LocalSupervisor): class NativeServerSupervisor(LocalSupervisor):
"""Server supervisor for the builtin HTTP server.""" """Server supervisor for the builtin HTTP server."""
httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
@ -111,7 +108,6 @@ class NativeServerSupervisor(LocalSupervisor):
class LocalWSGISupervisor(LocalSupervisor): class LocalWSGISupervisor(LocalSupervisor):
"""Server supervisor for the builtin WSGI server.""" """Server supervisor for the builtin WSGI server."""
httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
@ -311,8 +307,7 @@ class CPWebCase(webtest.WebCase):
sys.exit() sys.exit()
def getPage(self, url, *args, **kwargs): def getPage(self, url, *args, **kwargs):
"""Open the url. """Open the url."""
"""
if self.script_name: if self.script_name:
url = httputil.urljoin(self.script_name, url) url = httputil.urljoin(self.script_name, url)
return webtest.WebCase.getPage(self, url, *args, **kwargs) return webtest.WebCase.getPage(self, url, *args, **kwargs)
@ -323,8 +318,9 @@ class CPWebCase(webtest.WebCase):
def assertErrorPage(self, status, message=None, pattern=''): def assertErrorPage(self, status, message=None, pattern=''):
"""Compare the response body with a built in error page. """Compare the response body with a built in error page.
The function will optionally look for the regexp pattern, The function will optionally look for the regexp pattern, within
within the exception embedded in the error page.""" the exception embedded in the error page.
"""
# This will never contain a traceback # This will never contain a traceback
page = cherrypy._cperror.get_error_page(status, message=message) page = cherrypy._cperror.get_error_page(status, message=message)
@ -453,19 +449,17 @@ server.ssl_private_key: r'%s'
'-c', self.config_file, '-c', self.config_file,
'-p', self.pid_file, '-p', self.pid_file,
] ]
r""" r"""Command for running cherryd server with autoreload enabled.
Command for running cherryd server with autoreload enabled
Using Using
``` ```
['-c', ['-c',
"__requires__ = 'CherryPy'; \ "__requires__ = 'CherryPy'; \
import pkg_resources, re, sys; \ import importlib.metadata, re, sys; \
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
sys.exit(\ sys.exit(\
pkg_resources.load_entry_point(\ importlib.metadata.distribution('cherrypy').entry_points[0])"]
'CherryPy', 'console_scripts', 'cherryd')())"]
``` ```
doesn't work as it's impossible to reconstruct the `-c`'s contents. doesn't work as it's impossible to reconstruct the `-c`'s contents.

View file

@ -1,4 +1,4 @@
"""logtest, a unittest.TestCase helper for testing log output.""" """Logtest, a unittest.TestCase helper for testing log output."""
import sys import sys
import time import time
@ -32,7 +32,6 @@ except ImportError:
class LogCase(object): class LogCase(object):
"""unittest.TestCase mixin for testing log messages. """unittest.TestCase mixin for testing log messages.
logfile: a filename for the desired log. Yes, I know modes are evil, logfile: a filename for the desired log. Yes, I know modes are evil,
@ -116,7 +115,8 @@ class LogCase(object):
"""Return lines from self.logfile in the marked region. """Return lines from self.logfile in the marked region.
If marker is None, self.lastmarker is used. If the log hasn't If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be returned. been marked (using self.markLog), the entire log will be
returned.
""" """
# Give the logger time to finish writing? # Give the logger time to finish writing?
# time.sleep(0.5) # time.sleep(0.5)
@ -146,9 +146,10 @@ class LogCase(object):
def assertInLog(self, line, marker=None): def assertInLog(self, line, marker=None):
"""Fail if the given (partial) line is not in the log. """Fail if the given (partial) line is not in the log.
The log will be searched from the given marker to the next marker. The log will be searched from the given marker to the next
If marker is None, self.lastmarker is used. If the log hasn't marker. If marker is None, self.lastmarker is used. If the log
been marked (using self.markLog), the entire log will be searched. hasn't been marked (using self.markLog), the entire log will be
searched.
""" """
data = self._read_marked_region(marker) data = self._read_marked_region(marker)
for logline in data: for logline in data:
@ -160,9 +161,10 @@ class LogCase(object):
def assertNotInLog(self, line, marker=None): def assertNotInLog(self, line, marker=None):
"""Fail if the given (partial) line is in the log. """Fail if the given (partial) line is in the log.
The log will be searched from the given marker to the next marker. The log will be searched from the given marker to the next
If marker is None, self.lastmarker is used. If the log hasn't marker. If marker is None, self.lastmarker is used. If the log
been marked (using self.markLog), the entire log will be searched. hasn't been marked (using self.markLog), the entire log will be
searched.
""" """
data = self._read_marked_region(marker) data = self._read_marked_region(marker)
for logline in data: for logline in data:
@ -173,9 +175,10 @@ class LogCase(object):
def assertValidUUIDv4(self, marker=None): def assertValidUUIDv4(self, marker=None):
"""Fail if the given UUIDv4 is not valid. """Fail if the given UUIDv4 is not valid.
The log will be searched from the given marker to the next marker. The log will be searched from the given marker to the next
If marker is None, self.lastmarker is used. If the log hasn't marker. If marker is None, self.lastmarker is used. If the log
been marked (using self.markLog), the entire log will be searched. hasn't been marked (using self.markLog), the entire log will be
searched.
""" """
data = self._read_marked_region(marker) data = self._read_marked_region(marker)
data = [ data = [
@ -200,9 +203,10 @@ class LogCase(object):
def assertLog(self, sliceargs, lines, marker=None): def assertLog(self, sliceargs, lines, marker=None):
"""Fail if log.readlines()[sliceargs] is not contained in 'lines'. """Fail if log.readlines()[sliceargs] is not contained in 'lines'.
The log will be searched from the given marker to the next marker. The log will be searched from the given marker to the next
If marker is None, self.lastmarker is used. If the log hasn't marker. If marker is None, self.lastmarker is used. If the log
been marked (using self.markLog), the entire log will be searched. hasn't been marked (using self.markLog), the entire log will be
searched.
""" """
data = self._read_marked_region(marker) data = self._read_marked_region(marker)
if isinstance(sliceargs, int): if isinstance(sliceargs, int):

View file

@ -94,7 +94,6 @@ SetEnv testmod %(testmod)s
class ModWSGISupervisor(helper.Supervisor): class ModWSGISupervisor(helper.Supervisor):
"""Server Controller for ModWSGI and CherryPy.""" """Server Controller for ModWSGI and CherryPy."""
using_apache = True using_apache = True

View file

@ -3,6 +3,7 @@
import calendar import calendar
from datetime import datetime from datetime import datetime
from datetime import timezone as _timezone
import sys import sys
import cherrypy import cherrypy
@ -123,9 +124,12 @@ class Root(object):
'reqcookie': cherrypy.request.cookie.output(), 'reqcookie': cherrypy.request.cookie.output(),
'sessiondata': list(cherrypy.session.items()), 'sessiondata': list(cherrypy.session.items()),
'servertime': ( 'servertime': (
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' datetime.now(_timezone.utc).strftime('%Y/%m/%d %H:%M UTC')
),
'serverunixtime':
calendar.timegm(
datetime.utcnow(_timezone.utc).timetuple(),
), ),
'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
'cpversion': cherrypy.__version__, 'cpversion': cherrypy.__version__,
'pyversion': sys.version, 'pyversion': sys.version,
'expires': expires, 'expires': expires,

View file

@ -1,5 +1,4 @@
# coding: utf-8 # coding: utf-8
"""Basic tests for the CherryPy core: request handling.""" """Basic tests for the CherryPy core: request handling."""
import os import os
@ -48,7 +47,6 @@ class CoreRequestHandlingTest(helper.CPWebCase):
root.expose_dec = ExposeExamples() root.expose_dec = ExposeExamples()
class TestType(type): class TestType(type):
"""Metaclass which automatically exposes all functions in each """Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute subclass, and adds an instance of the subclass as an attribute
of root. of root.

View file

@ -97,9 +97,7 @@ def setup_server():
class UserContainerNode(object): class UserContainerNode(object):
def POST(self, name): def POST(self, name):
""" """Allow the creation of a new Object."""
Allow the creation of a new Object
"""
return 'POST %d' % make_user(name) return 'POST %d' % make_user(name)
def GET(self): def GET(self):
@ -125,15 +123,11 @@ def setup_server():
raise cherrypy.HTTPError(404) raise cherrypy.HTTPError(404)
def GET(self, *args, **kwargs): def GET(self, *args, **kwargs):
""" """Return the appropriate representation of the instance."""
Return the appropriate representation of the instance.
"""
return str(self.user) return str(self.user)
def POST(self, name): def POST(self, name):
""" """Update the fields of the user instance."""
Update the fields of the user instance.
"""
self.user.name = name self.user.name = name
return 'POST %d' % self.user.id return 'POST %d' % self.user.id
@ -151,9 +145,7 @@ def setup_server():
return 'PUT %d' % make_user(name, self.id) return 'PUT %d' % make_user(name, self.id)
def DELETE(self): def DELETE(self):
""" """Delete the user specified at the id."""
Delete the user specified at the id.
"""
id = self.user.id id = self.user.id
del user_lookup[self.user.id] del user_lookup[self.user.id]
del self.user del self.user
@ -199,7 +191,6 @@ def setup_server():
return 'IndexOnly index' return 'IndexOnly index'
class DecoratedPopArgs: class DecoratedPopArgs:
"""Test _cp_dispatch with @cherrypy.popargs.""" """Test _cp_dispatch with @cherrypy.popargs."""
@cherrypy.expose @cherrypy.expose
@ -213,7 +204,6 @@ def setup_server():
'a', 'b', handler=ABHandler())(DecoratedPopArgs) 'a', 'b', handler=ABHandler())(DecoratedPopArgs)
class NonDecoratedPopArgs: class NonDecoratedPopArgs:
"""Test _cp_dispatch = cherrypy.popargs()""" """Test _cp_dispatch = cherrypy.popargs()"""
_cp_dispatch = cherrypy.popargs('a') _cp_dispatch = cherrypy.popargs('a')
@ -223,8 +213,7 @@ def setup_server():
return 'index: ' + str(a) return 'index: ' + str(a)
class ParameterizedHandler: class ParameterizedHandler:
"""Special handler created for each request."""
"""Special handler created for each request"""
def __init__(self, a): def __init__(self, a):
self.a = a self.a = a
@ -238,8 +227,7 @@ def setup_server():
return self.a return self.a
class ParameterizedPopArgs: class ParameterizedPopArgs:
"""Test cherrypy.popargs() with a function call handler."""
"""Test cherrypy.popargs() with a function call handler"""
ParameterizedPopArgs = cherrypy.popargs( ParameterizedPopArgs = cherrypy.popargs(
'a', handler=ParameterizedHandler)(ParameterizedPopArgs) 'a', handler=ParameterizedHandler)(ParameterizedPopArgs)

View file

@ -16,9 +16,7 @@ from cherrypy.test import helper
def is_ascii(text): def is_ascii(text):
""" """Return True if the text encodes as ascii."""
Return True if the text encodes as ascii.
"""
try: try:
text.encode('ascii') text.encode('ascii')
return True return True
@ -28,9 +26,9 @@ def is_ascii(text):
def encode_filename(filename): def encode_filename(filename):
""" """Given a filename to be used in a multipart/form-data, encode the name.
Given a filename to be used in a multipart/form-data,
encode the name. Return the key and encoded filename. Return the key and encoded filename.
""" """
if is_ascii(filename): if is_ascii(filename):
return 'filename', '"{filename}"'.format(**locals()) return 'filename', '"{filename}"'.format(**locals())
@ -114,7 +112,7 @@ class HTTPTests(helper.CPWebCase):
@cherrypy.expose @cherrypy.expose
def post_filename(self, myfile): def post_filename(self, myfile):
'''Return the name of the file which was uploaded.''' """Return the name of the file which was uploaded."""
return myfile.filename return myfile.filename
cherrypy.tree.mount(Root()) cherrypy.tree.mount(Root())

View file

@ -199,7 +199,7 @@ def test_custom_log_format(log_tracker, monkeypatch, server):
def test_utc_in_timez(monkeypatch): def test_utc_in_timez(monkeypatch):
"""Test that ``LazyRfc3339UtcTime`` is rendered as ``str`` using UTC timestamp.""" """Test ``LazyRfc3339UtcTime`` renders as ``str`` UTC timestamp."""
utcoffset8_local_time_in_naive_utc = ( utcoffset8_local_time_in_naive_utc = (
datetime.datetime( datetime.datetime(
year=2020, year=2020,
@ -216,7 +216,7 @@ def test_utc_in_timez(monkeypatch):
class mock_datetime: class mock_datetime:
@classmethod @classmethod
def utcnow(cls): def now(cls, tz):
return utcoffset8_local_time_in_naive_utc return utcoffset8_local_time_in_naive_utc
monkeypatch.setattr('datetime.datetime', mock_datetime) monkeypatch.setattr('datetime.datetime', mock_datetime)

View file

@ -6,8 +6,7 @@ __metaclass__ = type
class TestAutoreloader: class TestAutoreloader:
def test_file_for_file_module_when_None(self): def test_file_for_file_module_when_None(self):
"""No error when module.__file__ is None. """No error when ``module.__file__`` is :py:data:`None`."""
"""
class test_module: class test_module:
__file__ = None __file__ = None

View file

@ -275,7 +275,6 @@ class RequestObjectTests(helper.CPWebCase):
return 'success' return 'success'
class Divorce(Test): class Divorce(Test):
"""HTTP Method handlers shouldn't collide with normal method names. """HTTP Method handlers shouldn't collide with normal method names.
For example, a GET-handler shouldn't collide with a method named For example, a GET-handler shouldn't collide with a method named
'get'. 'get'.
@ -757,8 +756,8 @@ class RequestObjectTests(helper.CPWebCase):
self.assertBody('application/json') self.assertBody('application/json')
def test_dangerous_host(self): def test_dangerous_host(self):
""" """Dangerous characters like newlines should be elided.
Dangerous characters like newlines should be elided.
Ref #1974. Ref #1974.
""" """
# foo\nbar # foo\nbar

View file

@ -4,7 +4,7 @@ import threading
import time import time
from http.client import HTTPConnection from http.client import HTTPConnection
from distutils.spawn import find_executable from shutil import which
import pytest import pytest
from path import Path from path import Path
from more_itertools import consume from more_itertools import consume
@ -146,9 +146,14 @@ class SessionTest(helper.CPWebCase):
def teardown_class(cls): def teardown_class(cls):
"""Clean up sessions.""" """Clean up sessions."""
super(cls, cls).teardown_class() super(cls, cls).teardown_class()
try:
files_to_clean = localDir.iterdir() # Python 3.8+
except AttributeError:
files_to_clean = localDir.listdir() # Python 3.6-3.7
consume( consume(
file.remove_p() file.remove_p()
for file in localDir.listdir() for file in files_to_clean
if file.basename().startswith( if file.basename().startswith(
sessions.FileSession.SESSION_PREFIX sessions.FileSession.SESSION_PREFIX
) )
@ -402,7 +407,7 @@ class SessionTest(helper.CPWebCase):
def is_memcached_present(): def is_memcached_present():
executable = find_executable('memcached') executable = which('memcached')
return bool(executable) return bool(executable)
@ -418,9 +423,7 @@ def memcached_client_present():
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def memcached_instance(request, watcher_getter, memcached_server_present): def memcached_instance(request, watcher_getter, memcached_server_present):
""" """Start up an instance of memcached."""
Start up an instance of memcached.
"""
port = portend.find_available_local_port() port = portend.find_available_local_port()

View file

@ -433,14 +433,13 @@ test_case_name: "test_signal_handler_unsubscribe"
def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name
""" """Wait on INADDR_ANY should not raise IOError.
Wait on INADDR_ANY should not raise IOError
In cases where the loopback interface does not exist, CherryPy cannot In cases where the loopback interface does not exist, CherryPy
effectively determine if a port binding to INADDR_ANY was effected. cannot effectively determine if a port binding to INADDR_ANY was
In this situation, CherryPy should assume that it failed to detect effected. In this situation, CherryPy should assume that it failed
the binding (not that the binding failed) and only warn that it could to detect the binding (not that the binding failed) and only warn
not verify it. that it could not verify it.
""" """
# At such a time that CherryPy can reliably determine one or more # At such a time that CherryPy can reliably determine one or more
# viable IP addresses of the host, this test may be removed. # viable IP addresses of the host, this test may be removed.

View file

@ -460,9 +460,7 @@ class SessionAuthTest(unittest.TestCase):
class TestHooks: class TestHooks:
def test_priorities(self): def test_priorities(self):
""" """Hooks should sort by priority order."""
Hooks should sort by priority order.
"""
Hook = cherrypy._cprequest.Hook Hook = cherrypy._cprequest.Hook
hooks = [ hooks = [
Hook(None, priority=48), Hook(None, priority=48),

View file

@ -9,18 +9,14 @@ class TutorialTest(helper.CPWebCase):
@classmethod @classmethod
def setup_server(cls): def setup_server(cls):
""" """Mount something so the engine starts."""
Mount something so the engine starts.
"""
class Dummy: class Dummy:
pass pass
cherrypy.tree.mount(Dummy()) cherrypy.tree.mount(Dummy())
@staticmethod @staticmethod
def load_module(name): def load_module(name):
""" """Import or reload tutorial module as needed."""
Import or reload tutorial module as needed.
"""
target = 'cherrypy.tutorial.' + name target = 'cherrypy.tutorial.' + name
if target in sys.modules: if target in sys.modules:
module = importlib.reload(sys.modules[target]) module = importlib.reload(sys.modules[target])

View file

@ -21,9 +21,7 @@ USOCKET_PATH = usocket_path()
class USocketHTTPConnection(HTTPConnection): class USocketHTTPConnection(HTTPConnection):
""" """HTTPConnection over a unix socket."""
HTTPConnection over a unix socket.
"""
def __init__(self, path): def __init__(self, path):
HTTPConnection.__init__(self, 'localhost') HTTPConnection.__init__(self, 'localhost')

View file

@ -11,8 +11,7 @@ import cherrypy
class HelloWorld: class HelloWorld:
"""Sample request handler class."""
""" Sample request handler class. """
# Expose the index method through the web. CherryPy will never # Expose the index method through the web. CherryPy will never
# publish methods that don't have the exposed attribute set to True. # publish methods that don't have the exposed attribute set to True.

View file

@ -3,4 +3,4 @@
from .more import * # noqa from .more import * # noqa
from .recipes import * # noqa from .recipes import * # noqa
__version__ = '10.2.0' __version__ = '10.3.0'

View file

@ -1,3 +1,4 @@
import math
import warnings import warnings
from collections import Counter, defaultdict, deque, abc from collections import Counter, defaultdict, deque, abc
@ -6,6 +7,7 @@ from functools import cached_property, partial, reduce, wraps
from heapq import heapify, heapreplace, heappop from heapq import heapify, heapreplace, heappop
from itertools import ( from itertools import (
chain, chain,
combinations,
compress, compress,
count, count,
cycle, cycle,
@ -19,7 +21,7 @@ from itertools import (
zip_longest, zip_longest,
product, product,
) )
from math import exp, factorial, floor, log, perm, comb from math import comb, e, exp, factorial, floor, fsum, log, perm, tau
from queue import Empty, Queue from queue import Empty, Queue
from random import random, randrange, uniform from random import random, randrange, uniform
from operator import itemgetter, mul, sub, gt, lt, ge, le from operator import itemgetter, mul, sub, gt, lt, ge, le
@ -61,11 +63,13 @@ __all__ = [
'consumer', 'consumer',
'count_cycle', 'count_cycle',
'countable', 'countable',
'dft',
'difference', 'difference',
'distinct_combinations', 'distinct_combinations',
'distinct_permutations', 'distinct_permutations',
'distribute', 'distribute',
'divide', 'divide',
'doublestarmap',
'duplicates_everseen', 'duplicates_everseen',
'duplicates_justseen', 'duplicates_justseen',
'classify_unique', 'classify_unique',
@ -77,6 +81,7 @@ __all__ = [
'groupby_transform', 'groupby_transform',
'ichunked', 'ichunked',
'iequals', 'iequals',
'idft',
'ilen', 'ilen',
'interleave', 'interleave',
'interleave_evenly', 'interleave_evenly',
@ -86,6 +91,7 @@ __all__ = [
'islice_extended', 'islice_extended',
'iterate', 'iterate',
'iter_suppress', 'iter_suppress',
'join_mappings',
'last', 'last',
'locate', 'locate',
'longest_common_prefix', 'longest_common_prefix',
@ -109,6 +115,7 @@ __all__ = [
'partitions', 'partitions',
'peekable', 'peekable',
'permutation_index', 'permutation_index',
'powerset_of_sets',
'product_index', 'product_index',
'raise_', 'raise_',
'repeat_each', 'repeat_each',
@ -148,6 +155,9 @@ __all__ = [
'zip_offset', 'zip_offset',
] ]
# math.sumprod is available for Python 3.12+
_fsumprod = getattr(math, 'sumprod', lambda x, y: fsum(map(mul, x, y)))
def chunked(iterable, n, strict=False): def chunked(iterable, n, strict=False):
"""Break *iterable* into lists of length *n*: """Break *iterable* into lists of length *n*:
@ -550,10 +560,10 @@ def one(iterable, too_short=None, too_long=None):
try: try:
first_value = next(it) first_value = next(it)
except StopIteration as e: except StopIteration as exc:
raise ( raise (
too_short or ValueError('too few items in iterable (expected 1)') too_short or ValueError('too few items in iterable (expected 1)')
) from e ) from exc
try: try:
second_value = next(it) second_value = next(it)
@ -840,26 +850,31 @@ def windowed(seq, n, fillvalue=None, step=1):
if n < 0: if n < 0:
raise ValueError('n must be >= 0') raise ValueError('n must be >= 0')
if n == 0: if n == 0:
yield tuple() yield ()
return return
if step < 1: if step < 1:
raise ValueError('step must be >= 1') raise ValueError('step must be >= 1')
window = deque(maxlen=n) iterable = iter(seq)
i = n
for _ in map(window.append, seq):
i -= 1
if not i:
i = step
yield tuple(window)
size = len(window) # Generate first window
if size == 0: window = deque(islice(iterable, n), maxlen=n)
# Deal with the first window not being full
if not window:
return return
elif size < n: if len(window) < n:
yield tuple(chain(window, repeat(fillvalue, n - size))) yield tuple(window) + ((fillvalue,) * (n - len(window)))
elif 0 < i < min(step, n): return
window += (fillvalue,) * i yield tuple(window)
# Create the filler for the next windows. The padding ensures
# we have just enough elements to fill the last window.
padding = (fillvalue,) * (n - 1 if step >= n else step - 1)
filler = map(window.append, chain(iterable, padding))
# Generate the rest of the windows
for _ in islice(filler, step - 1, None, step):
yield tuple(window) yield tuple(window)
@ -1151,8 +1166,8 @@ def interleave_evenly(iterables, lengths=None):
# those iterables for which the error is negative are yielded # those iterables for which the error is negative are yielded
# ("diagonal step" in Bresenham) # ("diagonal step" in Bresenham)
for i, e in enumerate(errors): for i, e_ in enumerate(errors):
if e < 0: if e_ < 0:
yield next(iters_secondary[i]) yield next(iters_secondary[i])
to_yield -= 1 to_yield -= 1
errors[i] += delta_primary errors[i] += delta_primary
@ -1184,26 +1199,38 @@ def collapse(iterable, base_type=None, levels=None):
['a', ['b'], 'c', ['d']] ['a', ['b'], 'c', ['d']]
""" """
stack = deque()
# Add our first node group, treat the iterable as a single node
stack.appendleft((0, repeat(iterable, 1)))
def walk(node, level): while stack:
if ( node_group = stack.popleft()
((levels is not None) and (level > levels)) level, nodes = node_group
or isinstance(node, (str, bytes))
or ((base_type is not None) and isinstance(node, base_type))
):
yield node
return
try: # Check if beyond max level
tree = iter(node) if levels is not None and level > levels:
except TypeError: yield from nodes
yield node continue
return
else:
for child in tree:
yield from walk(child, level + 1)
yield from walk(iterable, 0) for node in nodes:
# Check if done iterating
if isinstance(node, (str, bytes)) or (
(base_type is not None) and isinstance(node, base_type)
):
yield node
# Otherwise try to create child nodes
else:
try:
tree = iter(node)
except TypeError:
yield node
else:
# Save our current location
stack.appendleft(node_group)
# Append the new child node
stack.appendleft((level + 1, tree))
# Break to process child node
break
def side_effect(func, iterable, chunk_size=None, before=None, after=None): def side_effect(func, iterable, chunk_size=None, before=None, after=None):
@ -1516,28 +1543,41 @@ def padded(iterable, fillvalue=None, n=None, next_multiple=False):
[1, 2, 3, '?', '?'] [1, 2, 3, '?', '?']
If *next_multiple* is ``True``, *fillvalue* will be emitted until the If *next_multiple* is ``True``, *fillvalue* will be emitted until the
number of items emitted is a multiple of *n*:: number of items emitted is a multiple of *n*:
>>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True))
[1, 2, 3, 4, None, None] [1, 2, 3, 4, None, None]
If *n* is ``None``, *fillvalue* will be emitted indefinitely. If *n* is ``None``, *fillvalue* will be emitted indefinitely.
To create an *iterable* of exactly size *n*, you can truncate with
:func:`islice`.
>>> list(islice(padded([1, 2, 3], '?'), 5))
[1, 2, 3, '?', '?']
>>> list(islice(padded([1, 2, 3, 4, 5, 6, 7, 8], '?'), 5))
[1, 2, 3, 4, 5]
""" """
it = iter(iterable) iterable = iter(iterable)
iterable_with_repeat = chain(iterable, repeat(fillvalue))
if n is None: if n is None:
yield from chain(it, repeat(fillvalue)) return iterable_with_repeat
elif n < 1: elif n < 1:
raise ValueError('n must be at least 1') raise ValueError('n must be at least 1')
else: elif next_multiple:
item_count = 0
for item in it:
yield item
item_count += 1
remaining = (n - item_count) % n if next_multiple else n - item_count def slice_generator():
for _ in range(remaining): for first in iterable:
yield fillvalue yield (first,)
yield islice(iterable_with_repeat, n - 1)
# While elements exist produce slices of size n
return chain.from_iterable(slice_generator())
else:
# Ensure the first batch is at least size n then iterate
return chain(islice(iterable_with_repeat, n), iterable)
def repeat_each(iterable, n=2): def repeat_each(iterable, n=2):
@ -1592,7 +1632,9 @@ def distribute(n, iterable):
[[1], [2], [3], [], []] [[1], [2], [3], [], []]
This function uses :func:`itertools.tee` and may require significant This function uses :func:`itertools.tee` and may require significant
storage. If you need the order items in the smaller iterables to match the storage.
If you need the order items in the smaller iterables to match the
original iterable, see :func:`divide`. original iterable, see :func:`divide`.
""" """
@ -1840,9 +1882,9 @@ def divide(n, iterable):
>>> [list(c) for c in children] >>> [list(c) for c in children]
[[1], [2], [3], [], []] [[1], [2], [3], [], []]
This function will exhaust the iterable before returning and may require This function will exhaust the iterable before returning.
significant storage. If order is not important, see :func:`distribute`, If order is not important, see :func:`distribute`, which does not first
which does not first pull the iterable into memory. pull the iterable into memory.
""" """
if n < 1: if n < 1:
@ -3296,25 +3338,38 @@ def only(iterable, default=None, too_long=None):
return first_value return first_value
class _IChunk: def _ichunk(iterable, n):
def __init__(self, iterable, n): cache = deque()
self._it = islice(iterable, n) chunk = islice(iterable, n)
self._cache = deque()
def fill_cache(self): def generator():
self._cache.extend(self._it) while True:
if cache:
def __iter__(self): yield cache.popleft()
return self
def __next__(self):
try:
return next(self._it)
except StopIteration:
if self._cache:
return self._cache.popleft()
else: else:
raise try:
item = next(chunk)
except StopIteration:
return
else:
yield item
def materialize_next(n=1):
# if n not specified materialize everything
if n is None:
cache.extend(chunk)
return len(cache)
to_cache = n - len(cache)
# materialize up to n
if to_cache > 0:
cache.extend(islice(chunk, to_cache))
# return number materialized up to n
return min(n, len(cache))
return (generator(), materialize_next)
def ichunked(iterable, n): def ichunked(iterable, n):
@ -3338,19 +3393,19 @@ def ichunked(iterable, n):
[8, 9, 10, 11] [8, 9, 10, 11]
""" """
source = peekable(iter(iterable)) iterable = iter(iterable)
ichunk_marker = object()
while True: while True:
# Create new chunk
chunk, materialize_next = _ichunk(iterable, n)
# Check to see whether we're at the end of the source iterable # Check to see whether we're at the end of the source iterable
item = source.peek(ichunk_marker) if not materialize_next():
if item is ichunk_marker:
return return
chunk = _IChunk(source, n)
yield chunk yield chunk
# Advance the source iterable and fill previous chunk's cache # Fill previous chunk's cache
chunk.fill_cache() materialize_next(None)
def iequals(*iterables): def iequals(*iterables):
@ -3864,6 +3919,7 @@ def nth_permutation(iterable, r, index):
raise ValueError raise ValueError
else: else:
c = perm(n, r) c = perm(n, r)
assert c > 0 # factortial(n)>0, and r<n so perm(n,r) is never zero
if index < 0: if index < 0:
index += c index += c
@ -3871,9 +3927,6 @@ def nth_permutation(iterable, r, index):
if not 0 <= index < c: if not 0 <= index < c:
raise IndexError raise IndexError
if c == 0:
return tuple()
result = [0] * r result = [0] * r
q = index * factorial(n) // c if r < n else index q = index * factorial(n) // c if r < n else index
for d in range(1, n + 1): for d in range(1, n + 1):
@ -3946,6 +3999,12 @@ def value_chain(*args):
>>> list(value_chain('12', '34', ['56', '78'])) >>> list(value_chain('12', '34', ['56', '78']))
['12', '34', '56', '78'] ['12', '34', '56', '78']
Pre- or postpend a single element to an iterable:
>>> list(value_chain(1, [2, 3, 4, 5, 6]))
[1, 2, 3, 4, 5, 6]
>>> list(value_chain([1, 2, 3, 4, 5], 6))
[1, 2, 3, 4, 5, 6]
Multiple levels of nesting are not flattened. Multiple levels of nesting are not flattened.
@ -4154,53 +4213,41 @@ def chunked_even(iterable, n):
[[1, 2, 3], [4, 5, 6], [7]] [[1, 2, 3], [4, 5, 6], [7]]
""" """
iterable = iter(iterable)
len_method = getattr(iterable, '__len__', None) # Initialize a buffer to process the chunks while keeping
# some back to fill any underfilled chunks
min_buffer = (n - 1) * (n - 2)
buffer = list(islice(iterable, min_buffer))
if len_method is None: # Append items until we have a completed chunk
return _chunked_even_online(iterable, n) for _ in islice(map(buffer.append, iterable), n, None, n):
else: yield buffer[:n]
return _chunked_even_finite(iterable, len_method(), n) del buffer[:n]
# Check if any chunks need addition processing
def _chunked_even_online(iterable, n): if not buffer:
buffer = []
maxbuf = n + (n - 2) * (n - 1)
for x in iterable:
buffer.append(x)
if len(buffer) == maxbuf:
yield buffer[:n]
buffer = buffer[n:]
yield from _chunked_even_finite(buffer, len(buffer), n)
def _chunked_even_finite(iterable, N, n):
if N < 1:
return return
length = len(buffer)
# Lists are either size `full_size <= n` or `partial_size = full_size - 1` # Chunks are either size `full_size <= n` or `partial_size = full_size - 1`
q, r = divmod(N, n) q, r = divmod(length, n)
num_lists = q + (1 if r > 0 else 0) num_lists = q + (1 if r > 0 else 0)
q, r = divmod(N, num_lists) q, r = divmod(length, num_lists)
full_size = q + (1 if r > 0 else 0) full_size = q + (1 if r > 0 else 0)
partial_size = full_size - 1 partial_size = full_size - 1
num_full = N - partial_size * num_lists num_full = length - partial_size * num_lists
num_partial = num_lists - num_full
# Yield num_full lists of full_size # Yield chunks of full size
partial_start_idx = num_full * full_size partial_start_idx = num_full * full_size
if full_size > 0: if full_size > 0:
for i in range(0, partial_start_idx, full_size): for i in range(0, partial_start_idx, full_size):
yield list(islice(iterable, i, i + full_size)) yield buffer[i : i + full_size]
# Yield num_partial lists of partial_size # Yield chunks of partial size
if partial_size > 0: if partial_size > 0:
for i in range( for i in range(partial_start_idx, length, partial_size):
partial_start_idx, yield buffer[i : i + partial_size]
partial_start_idx + (num_partial * partial_size),
partial_size,
):
yield list(islice(iterable, i, i + partial_size))
def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False):
@ -4419,12 +4466,12 @@ def minmax(iterable_or_value, *others, key=None, default=_marker):
try: try:
lo = hi = next(it) lo = hi = next(it)
except StopIteration as e: except StopIteration as exc:
if default is _marker: if default is _marker:
raise ValueError( raise ValueError(
'`minmax()` argument is an empty iterable. ' '`minmax()` argument is an empty iterable. '
'Provide a `default` value to suppress this error.' 'Provide a `default` value to suppress this error.'
) from e ) from exc
return default return default
# Different branches depending on the presence of key. This saves a lot # Different branches depending on the presence of key. This saves a lot
@ -4654,3 +4701,106 @@ def filter_map(func, iterable):
y = func(x) y = func(x)
if y is not None: if y is not None:
yield y yield y
def powerset_of_sets(iterable):
"""Yields all possible subsets of the iterable.
>>> list(powerset_of_sets([1, 2, 3])) # doctest: +SKIP
[set(), {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}]
>>> list(powerset_of_sets([1, 1, 0])) # doctest: +SKIP
[set(), {1}, {0}, {0, 1}]
:func:`powerset_of_sets` takes care to minimize the number
of hash operations performed.
"""
sets = tuple(map(set, dict.fromkeys(map(frozenset, zip(iterable)))))
for r in range(len(sets) + 1):
yield from starmap(set().union, combinations(sets, r))
def join_mappings(**field_to_map):
"""
Joins multiple mappings together using their common keys.
>>> user_scores = {'elliot': 50, 'claris': 60}
>>> user_times = {'elliot': 30, 'claris': 40}
>>> join_mappings(score=user_scores, time=user_times)
{'elliot': {'score': 50, 'time': 30}, 'claris': {'score': 60, 'time': 40}}
"""
ret = defaultdict(dict)
for field_name, mapping in field_to_map.items():
for key, value in mapping.items():
ret[key][field_name] = value
return dict(ret)
def _complex_sumprod(v1, v2):
"""High precision sumprod() for complex numbers.
Used by :func:`dft` and :func:`idft`.
"""
r1 = chain((p.real for p in v1), (-p.imag for p in v1))
r2 = chain((q.real for q in v2), (q.imag for q in v2))
i1 = chain((p.real for p in v1), (p.imag for p in v1))
i2 = chain((q.imag for q in v2), (q.real for q in v2))
return complex(_fsumprod(r1, r2), _fsumprod(i1, i2))
def dft(xarr):
"""Discrete Fourier Tranform. *xarr* is a sequence of complex numbers.
Yields the components of the corresponding transformed output vector.
>>> import cmath
>>> xarr = [1, 2-1j, -1j, -1+2j]
>>> Xarr = [2, -2-2j, -2j, 4+4j]
>>> all(map(cmath.isclose, dft(xarr), Xarr))
True
See :func:`idft` for the inverse Discrete Fourier Transform.
"""
N = len(xarr)
roots_of_unity = [e ** (n / N * tau * -1j) for n in range(N)]
for k in range(N):
coeffs = [roots_of_unity[k * n % N] for n in range(N)]
yield _complex_sumprod(xarr, coeffs)
def idft(Xarr):
"""Inverse Discrete Fourier Tranform. *Xarr* is a sequence of
complex numbers. Yields the components of the corresponding
inverse-transformed output vector.
>>> import cmath
>>> xarr = [1, 2-1j, -1j, -1+2j]
>>> Xarr = [2, -2-2j, -2j, 4+4j]
>>> all(map(cmath.isclose, idft(Xarr), xarr))
True
See :func:`dft` for the Discrete Fourier Transform.
"""
N = len(Xarr)
roots_of_unity = [e ** (n / N * tau * 1j) for n in range(N)]
for k in range(N):
coeffs = [roots_of_unity[k * n % N] for n in range(N)]
yield _complex_sumprod(Xarr, coeffs) / N
def doublestarmap(func, iterable):
"""Apply *func* to every item of *iterable* by dictionary unpacking
the item into *func*.
The difference between :func:`itertools.starmap` and :func:`doublestarmap`
parallels the distinction between ``func(*a)`` and ``func(**a)``.
>>> iterable = [{'a': 1, 'b': 2}, {'a': 40, 'b': 60}]
>>> list(doublestarmap(lambda a, b: a + b, iterable))
[3, 100]
``TypeError`` will be raised if *func*'s signature doesn't match the
mapping contained in *iterable* or if *iterable* does not contain mappings.
"""
for item in iterable:
yield func(**item)

View file

@ -1,4 +1,5 @@
"""Stubs for more_itertools.more""" """Stubs for more_itertools.more"""
from __future__ import annotations from __future__ import annotations
from types import TracebackType from types import TracebackType
@ -9,8 +10,10 @@ from typing import (
ContextManager, ContextManager,
Generic, Generic,
Hashable, Hashable,
Mapping,
Iterable, Iterable,
Iterator, Iterator,
Mapping,
overload, overload,
Reversible, Reversible,
Sequence, Sequence,
@ -602,6 +605,7 @@ class countable(Generic[_T], Iterator[_T]):
def __init__(self, iterable: Iterable[_T]) -> None: ... def __init__(self, iterable: Iterable[_T]) -> None: ...
def __iter__(self) -> countable[_T]: ... def __iter__(self) -> countable[_T]: ...
def __next__(self) -> _T: ... def __next__(self) -> _T: ...
items_seen: int
def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[list[_T]]: ... def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[list[_T]]: ...
def zip_broadcast( def zip_broadcast(
@ -693,3 +697,13 @@ def filter_map(
func: Callable[[_T], _V | None], func: Callable[[_T], _V | None],
iterable: Iterable[_T], iterable: Iterable[_T],
) -> Iterator[_V]: ... ) -> Iterator[_V]: ...
def powerset_of_sets(iterable: Iterable[_T]) -> Iterator[set[_T]]: ...
def join_mappings(
**field_to_map: Mapping[_T, _V]
) -> dict[_T, dict[str, _V]]: ...
def doublestarmap(
func: Callable[..., _T],
iterable: Iterable[Mapping[str, Any]],
) -> Iterator[_T]: ...
def dft(xarr: Sequence[complex]) -> Iterator[complex]: ...
def idft(Xarr: Sequence[complex]) -> Iterator[complex]: ...

View file

@ -7,6 +7,7 @@ Some backward-compatible usability improvements have been made.
.. [1] http://docs.python.org/library/itertools.html#recipes .. [1] http://docs.python.org/library/itertools.html#recipes
""" """
import math import math
import operator import operator
@ -74,6 +75,7 @@ __all__ = [
'totient', 'totient',
'transpose', 'transpose',
'triplewise', 'triplewise',
'unique',
'unique_everseen', 'unique_everseen',
'unique_justseen', 'unique_justseen',
] ]
@ -198,7 +200,7 @@ def nth(iterable, n, default=None):
return next(islice(iterable, n, None), default) return next(islice(iterable, n, None), default)
def all_equal(iterable): def all_equal(iterable, key=None):
""" """
Returns ``True`` if all the elements are equal to each other. Returns ``True`` if all the elements are equal to each other.
@ -207,9 +209,16 @@ def all_equal(iterable):
>>> all_equal('aaab') >>> all_equal('aaab')
False False
A function that accepts a single argument and returns a transformed version
of each input item can be specified with *key*:
>>> all_equal('AaaA', key=str.casefold)
True
>>> all_equal([1, 2, 3], key=lambda x: x < 10)
True
""" """
g = groupby(iterable) return len(list(islice(groupby(iterable, key), 2))) <= 1
return next(g, True) and not next(g, False)
def quantify(iterable, pred=bool): def quantify(iterable, pred=bool):
@ -410,16 +419,11 @@ def roundrobin(*iterables):
iterables is small). iterables is small).
""" """
# Recipe credited to George Sakkis # Algorithm credited to George Sakkis
pending = len(iterables) iterators = map(iter, iterables)
nexts = cycle(iter(it).__next__ for it in iterables) for num_active in range(len(iterables), 0, -1):
while pending: iterators = cycle(islice(iterators, num_active))
try: yield from map(next, iterators)
for next in nexts:
yield next()
except StopIteration:
pending -= 1
nexts = cycle(islice(nexts, pending))
def partition(pred, iterable): def partition(pred, iterable):
@ -458,16 +462,14 @@ def powerset(iterable):
:func:`powerset` will operate on iterables that aren't :class:`set` :func:`powerset` will operate on iterables that aren't :class:`set`
instances, so repeated elements in the input will produce repeated elements instances, so repeated elements in the input will produce repeated elements
in the output. Use :func:`unique_everseen` on the input to avoid generating in the output.
duplicates:
>>> seq = [1, 1, 0] >>> seq = [1, 1, 0]
>>> list(powerset(seq)) >>> list(powerset(seq))
[(), (1,), (1,), (0,), (1, 1), (1, 0), (1, 0), (1, 1, 0)] [(), (1,), (1,), (0,), (1, 1), (1, 0), (1, 0), (1, 1, 0)]
>>> from more_itertools import unique_everseen
>>> list(powerset(unique_everseen(seq)))
[(), (1,), (0,), (1, 0)]
For a variant that efficiently yields actual :class:`set` instances, see
:func:`powerset_of_sets`.
""" """
s = list(iterable) s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
@ -533,6 +535,25 @@ def unique_justseen(iterable, key=None):
return map(next, map(operator.itemgetter(1), groupby(iterable, key))) return map(next, map(operator.itemgetter(1), groupby(iterable, key)))
def unique(iterable, key=None, reverse=False):
"""Yields unique elements in sorted order.
>>> list(unique([[1, 2], [3, 4], [1, 2]]))
[[1, 2], [3, 4]]
*key* and *reverse* are passed to :func:`sorted`.
>>> list(unique('ABBcCAD', str.casefold))
['A', 'B', 'c', 'D']
>>> list(unique('ABBcCAD', str.casefold, reverse=True))
['D', 'c', 'B', 'A']
The elements in *iterable* need not be hashable, but they must be
comparable for sorting to work.
"""
return unique_justseen(sorted(iterable, key=key, reverse=reverse), key=key)
def iter_except(func, exception, first=None): def iter_except(func, exception, first=None):
"""Yields results from a function repeatedly until an exception is raised. """Yields results from a function repeatedly until an exception is raised.
@ -827,8 +848,6 @@ def iter_index(iterable, value, start=0, stop=None):
"""Yield the index of each place in *iterable* that *value* occurs, """Yield the index of each place in *iterable* that *value* occurs,
beginning with index *start* and ending before index *stop*. beginning with index *start* and ending before index *stop*.
See :func:`locate` for a more general means of finding the indexes
associated with particular values.
>>> list(iter_index('AABCADEAF', 'A')) >>> list(iter_index('AABCADEAF', 'A'))
[0, 1, 4, 7] [0, 1, 4, 7]
@ -836,6 +855,19 @@ def iter_index(iterable, value, start=0, stop=None):
[1, 4, 7] [1, 4, 7]
>>> list(iter_index('AABCADEAF', 'A', 1, 7)) # stop index is not inclusive >>> list(iter_index('AABCADEAF', 'A', 1, 7)) # stop index is not inclusive
[1, 4] [1, 4]
The behavior for non-scalar *values* matches the built-in Python types.
>>> list(iter_index('ABCDABCD', 'AB'))
[0, 4]
>>> list(iter_index([0, 1, 2, 3, 0, 1, 2, 3], [0, 1]))
[]
>>> list(iter_index([[0, 1], [2, 3], [0, 1], [2, 3]], [0, 1]))
[0, 2]
See :func:`locate` for a more general means of finding the indexes
associated with particular values.
""" """
seq_index = getattr(iterable, 'index', None) seq_index = getattr(iterable, 'index', None)
if seq_index is None: if seq_index is None:
@ -1006,7 +1038,9 @@ def totient(n):
>>> totient(12) >>> totient(12)
4 4
""" """
for p in unique_justseen(factor(n)): # The itertools docs use unique_justseen instead of set; see
# https://github.com/more-itertools/more-itertools/issues/823
for p in set(factor(n)):
n = n // p * (p - 1) n = n // p * (p - 1)
return n return n

View file

@ -1,4 +1,5 @@
"""Stubs for more_itertools.recipes""" """Stubs for more_itertools.recipes"""
from __future__ import annotations from __future__ import annotations
from typing import ( from typing import (
@ -28,7 +29,9 @@ def consume(iterator: Iterable[_T], n: int | None = ...) -> None: ...
def nth(iterable: Iterable[_T], n: int) -> _T | None: ... def nth(iterable: Iterable[_T], n: int) -> _T | None: ...
@overload @overload
def nth(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ... def nth(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ...
def all_equal(iterable: Iterable[_T]) -> bool: ... def all_equal(
iterable: Iterable[_T], key: Callable[[_T], _U] | None = ...
) -> bool: ...
def quantify( def quantify(
iterable: Iterable[_T], pred: Callable[[_T], bool] = ... iterable: Iterable[_T], pred: Callable[[_T], bool] = ...
) -> int: ... ) -> int: ...
@ -58,6 +61,11 @@ def unique_everseen(
def unique_justseen( def unique_justseen(
iterable: Iterable[_T], key: Callable[[_T], object] | None = ... iterable: Iterable[_T], key: Callable[[_T], object] | None = ...
) -> Iterator[_T]: ... ) -> Iterator[_T]: ...
def unique(
iterable: Iterable[_T],
key: Callable[[_T], object] | None = ...,
reverse: bool = False,
) -> Iterator[_T]: ...
@overload @overload
def iter_except( def iter_except(
func: Callable[[], _T], func: Callable[[], _T],

View file

@ -6,7 +6,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages" __summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging" __uri__ = "https://github.com/pypa/packaging"
__version__ = "24.0" __version__ = "24.1"
__author__ = "Donald Stufft and individual contributors" __author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io" __email__ = "donald@stufft.io"

View file

@ -8,10 +8,12 @@ Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
""" """
from __future__ import annotations
import enum import enum
import os import os
import struct import struct
from typing import IO, Optional, Tuple from typing import IO
class ELFInvalid(ValueError): class ELFInvalid(ValueError):
@ -87,11 +89,11 @@ class ELFFile:
except struct.error as e: except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e raise ELFInvalid("unable to parse machine and section information") from e
def _read(self, fmt: str) -> Tuple[int, ...]: def _read(self, fmt: str) -> tuple[int, ...]:
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
@property @property
def interpreter(self) -> Optional[str]: def interpreter(self) -> str | None:
""" """
The path recorded in the ``PT_INTERP`` section header. The path recorded in the ``PT_INTERP`` section header.
""" """

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import collections import collections
import contextlib import contextlib
import functools import functools
@ -5,7 +7,7 @@ import os
import re import re
import sys import sys
import warnings import warnings
from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple from typing import Generator, Iterator, NamedTuple, Sequence
from ._elffile import EIClass, EIData, ELFFile, EMachine from ._elffile import EIClass, EIData, ELFFile, EMachine
@ -17,7 +19,7 @@ EF_ARM_ABI_FLOAT_HARD = 0x00000400
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str` # `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then. # as the type for `path` until then.
@contextlib.contextmanager @contextlib.contextmanager
def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
yield ELFFile(f) yield ELFFile(f)
@ -72,7 +74,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
# For now, guess what the highest minor version might be, assume it will # For now, guess what the highest minor version might be, assume it will
# be 50 for testing. Once this actually happens, update the dictionary # be 50 for testing. Once this actually happens, update the dictionary
# with the actual value. # with the actual value.
_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) _LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
class _GLibCVersion(NamedTuple): class _GLibCVersion(NamedTuple):
@ -80,7 +82,7 @@ class _GLibCVersion(NamedTuple):
minor: int minor: int
def _glibc_version_string_confstr() -> Optional[str]: def _glibc_version_string_confstr() -> str | None:
""" """
Primary implementation of glibc_version_string using os.confstr. Primary implementation of glibc_version_string using os.confstr.
""" """
@ -90,7 +92,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try: try:
# Should be a string like "glibc 2.17". # Should be a string like "glibc 2.17".
version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION") version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None assert version_string is not None
_, version = version_string.rsplit() _, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError): except (AssertionError, AttributeError, OSError, ValueError):
@ -99,7 +101,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
return version return version
def _glibc_version_string_ctypes() -> Optional[str]: def _glibc_version_string_ctypes() -> str | None:
""" """
Fallback implementation of glibc_version_string using ctypes. Fallback implementation of glibc_version_string using ctypes.
""" """
@ -143,12 +145,12 @@ def _glibc_version_string_ctypes() -> Optional[str]:
return version_str return version_str
def _glibc_version_string() -> Optional[str]: def _glibc_version_string() -> str | None:
"""Returns glibc version string, or None if not using glibc.""" """Returns glibc version string, or None if not using glibc."""
return _glibc_version_string_confstr() or _glibc_version_string_ctypes() return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> Tuple[int, int]: def _parse_glibc_version(version_str: str) -> tuple[int, int]:
"""Parse glibc version. """Parse glibc version.
We use a regexp instead of str.split because we want to discard any We use a regexp instead of str.split because we want to discard any
@ -167,8 +169,8 @@ def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
return int(m.group("major")), int(m.group("minor")) return int(m.group("major")), int(m.group("minor"))
@functools.lru_cache() @functools.lru_cache
def _get_glibc_version() -> Tuple[int, int]: def _get_glibc_version() -> tuple[int, int]:
version_str = _glibc_version_string() version_str = _glibc_version_string()
if version_str is None: if version_str is None:
return (-1, -1) return (-1, -1)

View file

@ -4,11 +4,13 @@ This module implements logic to detect if the currently running Python is
linked against musl, and what musl version is used. linked against musl, and what musl version is used.
""" """
from __future__ import annotations
import functools import functools
import re import re
import subprocess import subprocess
import sys import sys
from typing import Iterator, NamedTuple, Optional, Sequence from typing import Iterator, NamedTuple, Sequence
from ._elffile import ELFFile from ._elffile import ELFFile
@ -18,7 +20,7 @@ class _MuslVersion(NamedTuple):
minor: int minor: int
def _parse_musl_version(output: str) -> Optional[_MuslVersion]: def _parse_musl_version(output: str) -> _MuslVersion | None:
lines = [n for n in (n.strip() for n in output.splitlines()) if n] lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl": if len(lines) < 2 or lines[0][:4] != "musl":
return None return None
@ -28,8 +30,8 @@ def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
@functools.lru_cache() @functools.lru_cache
def _get_musl_version(executable: str) -> Optional[_MuslVersion]: def _get_musl_version(executable: str) -> _MuslVersion | None:
"""Detect currently-running musl runtime version. """Detect currently-running musl runtime version.
This is done by checking the specified executable's dynamic linking This is done by checking the specified executable's dynamic linking

View file

@ -1,11 +1,13 @@
"""Handwritten parser of dependency specifiers. """Handwritten parser of dependency specifiers.
The docstring for each __parse_* function contains ENBF-inspired grammar representing The docstring for each __parse_* function contains EBNF-inspired grammar representing
the implementation. the implementation.
""" """
from __future__ import annotations
import ast import ast
from typing import Any, List, NamedTuple, Optional, Tuple, Union from typing import NamedTuple, Sequence, Tuple, Union
from ._tokenizer import DEFAULT_RULES, Tokenizer from ._tokenizer import DEFAULT_RULES, Tokenizer
@ -41,20 +43,16 @@ class Op(Node):
MarkerVar = Union[Variable, Value] MarkerVar = Union[Variable, Value]
MarkerItem = Tuple[MarkerVar, Op, MarkerVar] MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]] MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
# MarkerList = List[Union["MarkerList", MarkerAtom, str]] MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
# mypy does not support recursive type definition
# https://github.com/python/mypy/issues/731
MarkerAtom = Any
MarkerList = List[Any]
class ParsedRequirement(NamedTuple): class ParsedRequirement(NamedTuple):
name: str name: str
url: str url: str
extras: List[str] extras: list[str]
specifier: str specifier: str
marker: Optional[MarkerList] marker: MarkerList | None
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
@ -87,7 +85,7 @@ def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
def _parse_requirement_details( def _parse_requirement_details(
tokenizer: Tokenizer, tokenizer: Tokenizer,
) -> Tuple[str, str, Optional[MarkerList]]: ) -> tuple[str, str, MarkerList | None]:
""" """
requirement_details = AT URL (WS requirement_marker?)? requirement_details = AT URL (WS requirement_marker?)?
| specifier WS? (requirement_marker)? | specifier WS? (requirement_marker)?
@ -156,7 +154,7 @@ def _parse_requirement_marker(
return marker return marker
def _parse_extras(tokenizer: Tokenizer) -> List[str]: def _parse_extras(tokenizer: Tokenizer) -> list[str]:
""" """
extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)? extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
""" """
@ -175,11 +173,11 @@ def _parse_extras(tokenizer: Tokenizer) -> List[str]:
return extras return extras
def _parse_extras_list(tokenizer: Tokenizer) -> List[str]: def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
""" """
extras_list = identifier (wsp* ',' wsp* identifier)* extras_list = identifier (wsp* ',' wsp* identifier)*
""" """
extras: List[str] = [] extras: list[str] = []
if not tokenizer.check("IDENTIFIER"): if not tokenizer.check("IDENTIFIER"):
return extras return extras

View file

@ -1,7 +1,9 @@
from __future__ import annotations
import contextlib import contextlib
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union from typing import Iterator, NoReturn
from .specifiers import Specifier from .specifiers import Specifier
@ -21,7 +23,7 @@ class ParserSyntaxError(Exception):
message: str, message: str,
*, *,
source: str, source: str,
span: Tuple[int, int], span: tuple[int, int],
) -> None: ) -> None:
self.span = span self.span = span
self.message = message self.message = message
@ -34,7 +36,7 @@ class ParserSyntaxError(Exception):
return "\n ".join([self.message, self.source, marker]) return "\n ".join([self.message, self.source, marker])
DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
"LEFT_PARENTHESIS": r"\(", "LEFT_PARENTHESIS": r"\(",
"RIGHT_PARENTHESIS": r"\)", "RIGHT_PARENTHESIS": r"\)",
"LEFT_BRACKET": r"\[", "LEFT_BRACKET": r"\[",
@ -96,13 +98,13 @@ class Tokenizer:
self, self,
source: str, source: str,
*, *,
rules: "Dict[str, Union[str, re.Pattern[str]]]", rules: dict[str, str | re.Pattern[str]],
) -> None: ) -> None:
self.source = source self.source = source
self.rules: Dict[str, re.Pattern[str]] = { self.rules: dict[str, re.Pattern[str]] = {
name: re.compile(pattern) for name, pattern in rules.items() name: re.compile(pattern) for name, pattern in rules.items()
} }
self.next_token: Optional[Token] = None self.next_token: Token | None = None
self.position = 0 self.position = 0
def consume(self, name: str) -> None: def consume(self, name: str) -> None:
@ -154,8 +156,8 @@ class Tokenizer:
self, self,
message: str, message: str,
*, *,
span_start: Optional[int] = None, span_start: int | None = None,
span_end: Optional[int] = None, span_end: int | None = None,
) -> NoReturn: ) -> NoReturn:
"""Raise ParserSyntaxError at the given position.""" """Raise ParserSyntaxError at the given position."""
span = ( span = (

View file

@ -2,20 +2,16 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details. # for complete details.
from __future__ import annotations
import operator import operator
import os import os
import platform import platform
import sys import sys
from typing import Any, Callable, Dict, List, Optional, Tuple, Union from typing import Any, Callable, TypedDict, cast
from ._parser import ( from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
MarkerAtom, from ._parser import parse_marker as _parse_marker
MarkerList,
Op,
Value,
Variable,
parse_marker as _parse_marker,
)
from ._tokenizer import ParserSyntaxError from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name from .utils import canonicalize_name
@ -50,6 +46,78 @@ class UndefinedEnvironmentName(ValueError):
""" """
class Environment(TypedDict):
implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``."""
implementation_version: str
"""
The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
``'7.3.13'`` for PyPy3.10 v7.3.13.
"""
os_name: str
"""
The value of :py:data:`os.name`. The name of the operating system dependent module
imported, e.g. ``'posix'``.
"""
platform_machine: str
"""
Returns the machine type, e.g. ``'i386'``.
An empty string if the value cannot be determined.
"""
platform_release: str
"""
The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
An empty string if the value cannot be determined.
"""
platform_system: str
"""
The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
An empty string if the value cannot be determined.
"""
platform_version: str
"""
The system's release version, e.g. ``'#3 on degas'``.
An empty string if the value cannot be determined.
"""
python_full_version: str
"""
The Python version as string ``'major.minor.patchlevel'``.
Note that unlike the Python :py:data:`sys.version`, this value will always include
the patchlevel (it defaults to 0).
"""
platform_python_implementation: str
"""
A string identifying the Python implementation, e.g. ``'CPython'``.
"""
python_version: str
"""The Python version as string ``'major.minor'``."""
sys_platform: str
"""
This string contains a platform identifier that can be used to append
platform-specific components to :py:data:`sys.path`, for instance.
For Unix systems, except on Linux and AIX, this is the lowercased OS name as
returned by ``uname -s`` with the first part of the version as returned by
``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
was built.
"""
def _normalize_extra_values(results: Any) -> Any: def _normalize_extra_values(results: Any) -> Any:
""" """
Normalize extra values. Normalize extra values.
@ -67,9 +135,8 @@ def _normalize_extra_values(results: Any) -> Any:
def _format_marker( def _format_marker(
marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True marker: list[str] | MarkerAtom | str, first: bool | None = True
) -> str: ) -> str:
assert isinstance(marker, (list, tuple, str)) assert isinstance(marker, (list, tuple, str))
# Sometimes we have a structure like [[...]] which is a single item list # Sometimes we have a structure like [[...]] which is a single item list
@ -95,7 +162,7 @@ def _format_marker(
return marker return marker
_operators: Dict[str, Operator] = { _operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs, "in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs, "not in": lambda lhs, rhs: lhs not in rhs,
"<": operator.lt, "<": operator.lt,
@ -115,14 +182,14 @@ def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
else: else:
return spec.contains(lhs, prereleases=True) return spec.contains(lhs, prereleases=True)
oper: Optional[Operator] = _operators.get(op.serialize()) oper: Operator | None = _operators.get(op.serialize())
if oper is None: if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
return oper(lhs, rhs) return oper(lhs, rhs)
def _normalize(*values: str, key: str) -> Tuple[str, ...]: def _normalize(*values: str, key: str) -> tuple[str, ...]:
# PEP 685 Comparison of extra names for optional distribution dependencies # PEP 685 Comparison of extra names for optional distribution dependencies
# https://peps.python.org/pep-0685/ # https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being # > When comparing extra names, tools MUST normalize the names being
@ -134,8 +201,8 @@ def _normalize(*values: str, key: str) -> Tuple[str, ...]:
return values return values
def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
groups: List[List[bool]] = [[]] groups: list[list[bool]] = [[]]
for marker in markers: for marker in markers:
assert isinstance(marker, (list, tuple, str)) assert isinstance(marker, (list, tuple, str))
@ -164,7 +231,7 @@ def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
return any(all(item) for item in groups) return any(all(item) for item in groups)
def format_full_version(info: "sys._version_info") -> str: def format_full_version(info: sys._version_info) -> str:
version = "{0.major}.{0.minor}.{0.micro}".format(info) version = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel kind = info.releaselevel
if kind != "final": if kind != "final":
@ -172,7 +239,7 @@ def format_full_version(info: "sys._version_info") -> str:
return version return version
def default_environment() -> Dict[str, str]: def default_environment() -> Environment:
iver = format_full_version(sys.implementation.version) iver = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name implementation_name = sys.implementation.name
return { return {
@ -231,7 +298,7 @@ class Marker:
return str(self) == str(other) return str(self) == str(other)
def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: def evaluate(self, environment: dict[str, str] | None = None) -> bool:
"""Evaluate a marker. """Evaluate a marker.
Return the boolean from evaluating the given marker against the Return the boolean from evaluating the given marker against the
@ -240,8 +307,14 @@ class Marker:
The environment is determined from the current Python process. The environment is determined from the current Python process.
""" """
current_environment = default_environment() current_environment = cast("dict[str, str]", default_environment())
current_environment["extra"] = "" current_environment["extra"] = ""
# Work around platform.python_version() returning something that is not PEP 440
# compliant for non-tagged Python builds. We preserve default_environment()'s
# behavior of returning platform.python_version() verbatim, and leave it to the
# caller to provide a syntactically valid version if they want to override it.
if current_environment["python_full_version"].endswith("+"):
current_environment["python_full_version"] += "local"
if environment is not None: if environment is not None:
current_environment.update(environment) current_environment.update(environment)
# The API used to allow setting extra to None. We need to handle this # The API used to allow setting extra to None. We need to handle this

View file

@ -1,50 +1,31 @@
from __future__ import annotations
import email.feedparser import email.feedparser
import email.header import email.header
import email.message import email.message
import email.parser import email.parser
import email.policy import email.policy
import sys
import typing import typing
from typing import ( from typing import (
Any, Any,
Callable, Callable,
Dict,
Generic, Generic,
List, Literal,
Optional, TypedDict,
Tuple,
Type,
Union,
cast, cast,
) )
from . import requirements, specifiers, utils, version as version_module from . import requirements, specifiers, utils
from . import version as version_module
T = typing.TypeVar("T") T = typing.TypeVar("T")
if sys.version_info[:2] >= (3, 8): # pragma: no cover
from typing import Literal, TypedDict
else: # pragma: no cover
if typing.TYPE_CHECKING:
from typing_extensions import Literal, TypedDict
else:
try:
from typing_extensions import Literal, TypedDict
except ImportError:
class Literal:
def __init_subclass__(*_args, **_kwargs):
pass
class TypedDict:
def __init_subclass__(*_args, **_kwargs):
pass
try: try:
ExceptionGroup ExceptionGroup
except NameError: # pragma: no cover except NameError: # pragma: no cover
class ExceptionGroup(Exception): # noqa: N818 class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11. """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself, If :external:exc:`ExceptionGroup` is already defined by Python itself,
@ -52,9 +33,9 @@ except NameError: # pragma: no cover
""" """
message: str message: str
exceptions: List[Exception] exceptions: list[Exception]
def __init__(self, message: str, exceptions: List[Exception]) -> None: def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message self.message = message
self.exceptions = exceptions self.exceptions = exceptions
@ -100,32 +81,32 @@ class RawMetadata(TypedDict, total=False):
metadata_version: str metadata_version: str
name: str name: str
version: str version: str
platforms: List[str] platforms: list[str]
summary: str summary: str
description: str description: str
keywords: List[str] keywords: list[str]
home_page: str home_page: str
author: str author: str
author_email: str author_email: str
license: str license: str
# Metadata 1.1 - PEP 314 # Metadata 1.1 - PEP 314
supported_platforms: List[str] supported_platforms: list[str]
download_url: str download_url: str
classifiers: List[str] classifiers: list[str]
requires: List[str] requires: list[str]
provides: List[str] provides: list[str]
obsoletes: List[str] obsoletes: list[str]
# Metadata 1.2 - PEP 345 # Metadata 1.2 - PEP 345
maintainer: str maintainer: str
maintainer_email: str maintainer_email: str
requires_dist: List[str] requires_dist: list[str]
provides_dist: List[str] provides_dist: list[str]
obsoletes_dist: List[str] obsoletes_dist: list[str]
requires_python: str requires_python: str
requires_external: List[str] requires_external: list[str]
project_urls: Dict[str, str] project_urls: dict[str, str]
# Metadata 2.0 # Metadata 2.0
# PEP 426 attempted to completely revamp the metadata format # PEP 426 attempted to completely revamp the metadata format
@ -138,10 +119,10 @@ class RawMetadata(TypedDict, total=False):
# Metadata 2.1 - PEP 566 # Metadata 2.1 - PEP 566
description_content_type: str description_content_type: str
provides_extra: List[str] provides_extra: list[str]
# Metadata 2.2 - PEP 643 # Metadata 2.2 - PEP 643
dynamic: List[str] dynamic: list[str]
# Metadata 2.3 - PEP 685 # Metadata 2.3 - PEP 685
# No new fields were added in PEP 685, just some edge case were # No new fields were added in PEP 685, just some edge case were
@ -185,12 +166,12 @@ _DICT_FIELDS = {
} }
def _parse_keywords(data: str) -> List[str]: def _parse_keywords(data: str) -> list[str]:
"""Split a string of comma-separate keyboards into a list of keywords.""" """Split a string of comma-separate keyboards into a list of keywords."""
return [k.strip() for k in data.split(",")] return [k.strip() for k in data.split(",")]
def _parse_project_urls(data: List[str]) -> Dict[str, str]: def _parse_project_urls(data: list[str]) -> dict[str, str]:
"""Parse a list of label/URL string pairings separated by a comma.""" """Parse a list of label/URL string pairings separated by a comma."""
urls = {} urls = {}
for pair in data: for pair in data:
@ -230,7 +211,7 @@ def _parse_project_urls(data: List[str]) -> Dict[str, str]:
return urls return urls
def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str: def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
"""Get the body of the message.""" """Get the body of the message."""
# If our source is a str, then our caller has managed encodings for us, # If our source is a str, then our caller has managed encodings for us,
# and we don't need to deal with it. # and we don't need to deal with it.
@ -292,7 +273,7 @@ _EMAIL_TO_RAW_MAPPING = {
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()} _RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]: def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
This function returns a two-item tuple of dicts. The first dict is of This function returns a two-item tuple of dicts. The first dict is of
@ -308,8 +289,8 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
included in this dict. included in this dict.
""" """
raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {} raw: dict[str, str | list[str] | dict[str, str]] = {}
unparsed: Dict[str, List[str]] = {} unparsed: dict[str, list[str]] = {}
if isinstance(data, str): if isinstance(data, str):
parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data) parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data)
@ -357,7 +338,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
# The Header object stores it's data as chunks, and each chunk # The Header object stores it's data as chunks, and each chunk
# can be independently encoded, so we'll need to check each # can be independently encoded, so we'll need to check each
# of them. # of them.
chunks: List[Tuple[bytes, Optional[str]]] = [] chunks: list[tuple[bytes, str | None]] = []
for bin, encoding in email.header.decode_header(h): for bin, encoding in email.header.decode_header(h):
try: try:
bin.decode("utf8", "strict") bin.decode("utf8", "strict")
@ -499,11 +480,11 @@ class _Validator(Generic[T]):
) -> None: ) -> None:
self.added = added self.added = added
def __set_name__(self, _owner: "Metadata", name: str) -> None: def __set_name__(self, _owner: Metadata, name: str) -> None:
self.name = name self.name = name
self.raw_name = _RAW_TO_EMAIL_MAPPING[name] self.raw_name = _RAW_TO_EMAIL_MAPPING[name]
def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T: def __get__(self, instance: Metadata, _owner: type[Metadata]) -> T:
# With Python 3.8, the caching can be replaced with functools.cached_property(). # With Python 3.8, the caching can be replaced with functools.cached_property().
# No need to check the cache as attribute lookup will resolve into the # No need to check the cache as attribute lookup will resolve into the
# instance's __dict__ before __get__ is called. # instance's __dict__ before __get__ is called.
@ -531,7 +512,7 @@ class _Validator(Generic[T]):
return cast(T, value) return cast(T, value)
def _invalid_metadata( def _invalid_metadata(
self, msg: str, cause: Optional[Exception] = None self, msg: str, cause: Exception | None = None
) -> InvalidMetadata: ) -> InvalidMetadata:
exc = InvalidMetadata( exc = InvalidMetadata(
self.raw_name, msg.format_map({"field": repr(self.raw_name)}) self.raw_name, msg.format_map({"field": repr(self.raw_name)})
@ -606,7 +587,7 @@ class _Validator(Generic[T]):
) )
return value return value
def _process_dynamic(self, value: List[str]) -> List[str]: def _process_dynamic(self, value: list[str]) -> list[str]:
for dynamic_field in map(str.lower, value): for dynamic_field in map(str.lower, value):
if dynamic_field in {"name", "version", "metadata-version"}: if dynamic_field in {"name", "version", "metadata-version"}:
raise self._invalid_metadata( raise self._invalid_metadata(
@ -618,8 +599,8 @@ class _Validator(Generic[T]):
def _process_provides_extra( def _process_provides_extra(
self, self,
value: List[str], value: list[str],
) -> List[utils.NormalizedName]: ) -> list[utils.NormalizedName]:
normalized_names = [] normalized_names = []
try: try:
for name in value: for name in value:
@ -641,8 +622,8 @@ class _Validator(Generic[T]):
def _process_requires_dist( def _process_requires_dist(
self, self,
value: List[str], value: list[str],
) -> List[requirements.Requirement]: ) -> list[requirements.Requirement]:
reqs = [] reqs = []
try: try:
for req in value: for req in value:
@ -665,7 +646,7 @@ class Metadata:
_raw: RawMetadata _raw: RawMetadata
@classmethod @classmethod
def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata": def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:
"""Create an instance from :class:`RawMetadata`. """Create an instance from :class:`RawMetadata`.
If *validate* is true, all metadata will be validated. All exceptions If *validate* is true, all metadata will be validated. All exceptions
@ -675,7 +656,7 @@ class Metadata:
ins._raw = data.copy() # Mutations occur due to caching enriched values. ins._raw = data.copy() # Mutations occur due to caching enriched values.
if validate: if validate:
exceptions: List[Exception] = [] exceptions: list[Exception] = []
try: try:
metadata_version = ins.metadata_version metadata_version = ins.metadata_version
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version) metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
@ -722,9 +703,7 @@ class Metadata:
return ins return ins
@classmethod @classmethod
def from_email( def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
cls, data: Union[bytes, str], *, validate: bool = True
) -> "Metadata":
"""Parse metadata from email headers. """Parse metadata from email headers.
If *validate* is true, the metadata will be validated. All exceptions If *validate* is true, the metadata will be validated. All exceptions
@ -760,66 +739,66 @@ class Metadata:
*validate* parameter)""" *validate* parameter)"""
version: _Validator[version_module.Version] = _Validator() version: _Validator[version_module.Version] = _Validator()
""":external:ref:`core-metadata-version` (required)""" """:external:ref:`core-metadata-version` (required)"""
dynamic: _Validator[Optional[List[str]]] = _Validator( dynamic: _Validator[list[str] | None] = _Validator(
added="2.2", added="2.2",
) )
""":external:ref:`core-metadata-dynamic` """:external:ref:`core-metadata-dynamic`
(validated against core metadata field names and lowercased)""" (validated against core metadata field names and lowercased)"""
platforms: _Validator[Optional[List[str]]] = _Validator() platforms: _Validator[list[str] | None] = _Validator()
""":external:ref:`core-metadata-platform`""" """:external:ref:`core-metadata-platform`"""
supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1") supported_platforms: _Validator[list[str] | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-supported-platform`""" """:external:ref:`core-metadata-supported-platform`"""
summary: _Validator[Optional[str]] = _Validator() summary: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-summary` (validated to contain no newlines)""" """:external:ref:`core-metadata-summary` (validated to contain no newlines)"""
description: _Validator[Optional[str]] = _Validator() # TODO 2.1: can be in body description: _Validator[str | None] = _Validator() # TODO 2.1: can be in body
""":external:ref:`core-metadata-description`""" """:external:ref:`core-metadata-description`"""
description_content_type: _Validator[Optional[str]] = _Validator(added="2.1") description_content_type: _Validator[str | None] = _Validator(added="2.1")
""":external:ref:`core-metadata-description-content-type` (validated)""" """:external:ref:`core-metadata-description-content-type` (validated)"""
keywords: _Validator[Optional[List[str]]] = _Validator() keywords: _Validator[list[str] | None] = _Validator()
""":external:ref:`core-metadata-keywords`""" """:external:ref:`core-metadata-keywords`"""
home_page: _Validator[Optional[str]] = _Validator() home_page: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-home-page`""" """:external:ref:`core-metadata-home-page`"""
download_url: _Validator[Optional[str]] = _Validator(added="1.1") download_url: _Validator[str | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-download-url`""" """:external:ref:`core-metadata-download-url`"""
author: _Validator[Optional[str]] = _Validator() author: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-author`""" """:external:ref:`core-metadata-author`"""
author_email: _Validator[Optional[str]] = _Validator() author_email: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-author-email`""" """:external:ref:`core-metadata-author-email`"""
maintainer: _Validator[Optional[str]] = _Validator(added="1.2") maintainer: _Validator[str | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-maintainer`""" """:external:ref:`core-metadata-maintainer`"""
maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2") maintainer_email: _Validator[str | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-maintainer-email`""" """:external:ref:`core-metadata-maintainer-email`"""
license: _Validator[Optional[str]] = _Validator() license: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-license`""" """:external:ref:`core-metadata-license`"""
classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1") classifiers: _Validator[list[str] | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-classifier`""" """:external:ref:`core-metadata-classifier`"""
requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator( requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator(
added="1.2" added="1.2"
) )
""":external:ref:`core-metadata-requires-dist`""" """:external:ref:`core-metadata-requires-dist`"""
requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator( requires_python: _Validator[specifiers.SpecifierSet | None] = _Validator(
added="1.2" added="1.2"
) )
""":external:ref:`core-metadata-requires-python`""" """:external:ref:`core-metadata-requires-python`"""
# Because `Requires-External` allows for non-PEP 440 version specifiers, we # Because `Requires-External` allows for non-PEP 440 version specifiers, we
# don't do any processing on the values. # don't do any processing on the values.
requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2") requires_external: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-requires-external`""" """:external:ref:`core-metadata-requires-external`"""
project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2") project_urls: _Validator[dict[str, str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-project-url`""" """:external:ref:`core-metadata-project-url`"""
# PEP 685 lets us raise an error if an extra doesn't pass `Name` validation # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation
# regardless of metadata version. # regardless of metadata version.
provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator( provides_extra: _Validator[list[utils.NormalizedName] | None] = _Validator(
added="2.1", added="2.1",
) )
""":external:ref:`core-metadata-provides-extra`""" """:external:ref:`core-metadata-provides-extra`"""
provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") provides_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-provides-dist`""" """:external:ref:`core-metadata-provides-dist`"""
obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-obsoletes-dist`""" """:external:ref:`core-metadata-obsoletes-dist`"""
requires: _Validator[Optional[List[str]]] = _Validator(added="1.1") requires: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Requires`` (deprecated)""" """``Requires`` (deprecated)"""
provides: _Validator[Optional[List[str]]] = _Validator(added="1.1") provides: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Provides`` (deprecated)""" """``Provides`` (deprecated)"""
obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1") obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Obsoletes`` (deprecated)""" """``Obsoletes`` (deprecated)"""

View file

@ -1,8 +1,9 @@
# This file is dual licensed under the terms of the Apache License, Version # This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details. # for complete details.
from __future__ import annotations
from typing import Any, Iterator, Optional, Set from typing import Any, Iterator
from ._parser import parse_requirement as _parse_requirement from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError from ._tokenizer import ParserSyntaxError
@ -37,10 +38,10 @@ class Requirement:
raise InvalidRequirement(str(e)) from e raise InvalidRequirement(str(e)) from e
self.name: str = parsed.name self.name: str = parsed.name
self.url: Optional[str] = parsed.url or None self.url: str | None = parsed.url or None
self.extras: Set[str] = set(parsed.extras or []) self.extras: set[str] = set(parsed.extras or [])
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
self.marker: Optional[Marker] = None self.marker: Marker | None = None
if parsed.marker is not None: if parsed.marker is not None:
self.marker = Marker.__new__(Marker) self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker) self.marker._markers = _normalize_extra_values(parsed.marker)

View file

@ -8,10 +8,12 @@
from packaging.version import Version from packaging.version import Version
""" """
from __future__ import annotations
import abc import abc
import itertools import itertools
import re import re
from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union from typing import Callable, Iterable, Iterator, TypeVar, Union
from .utils import canonicalize_version from .utils import canonicalize_version
from .version import Version from .version import Version
@ -64,7 +66,7 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
@property @property
@abc.abstractmethod @abc.abstractmethod
def prereleases(self) -> Optional[bool]: def prereleases(self) -> bool | None:
"""Whether or not pre-releases as a whole are allowed. """Whether or not pre-releases as a whole are allowed.
This can be set to either ``True`` or ``False`` to explicitly enable or disable This can be set to either ``True`` or ``False`` to explicitly enable or disable
@ -79,14 +81,14 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
""" """
@abc.abstractmethod @abc.abstractmethod
def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: def contains(self, item: str, prereleases: bool | None = None) -> bool:
""" """
Determines if the given item is contained within this specifier. Determines if the given item is contained within this specifier.
""" """
@abc.abstractmethod @abc.abstractmethod
def filter( def filter(
self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]: ) -> Iterator[UnparsedVersionVar]:
""" """
Takes an iterable of items and filters them so that only items which Takes an iterable of items and filters them so that only items which
@ -217,7 +219,7 @@ class Specifier(BaseSpecifier):
"===": "arbitrary", "===": "arbitrary",
} }
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: def __init__(self, spec: str = "", prereleases: bool | None = None) -> None:
"""Initialize a Specifier instance. """Initialize a Specifier instance.
:param spec: :param spec:
@ -234,7 +236,7 @@ class Specifier(BaseSpecifier):
if not match: if not match:
raise InvalidSpecifier(f"Invalid specifier: '{spec}'") raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
self._spec: Tuple[str, str] = ( self._spec: tuple[str, str] = (
match.group("operator").strip(), match.group("operator").strip(),
match.group("version").strip(), match.group("version").strip(),
) )
@ -318,7 +320,7 @@ class Specifier(BaseSpecifier):
return "{}{}".format(*self._spec) return "{}{}".format(*self._spec)
@property @property
def _canonical_spec(self) -> Tuple[str, str]: def _canonical_spec(self) -> tuple[str, str]:
canonical_version = canonicalize_version( canonical_version = canonicalize_version(
self._spec[1], self._spec[1],
strip_trailing_zero=(self._spec[0] != "~="), strip_trailing_zero=(self._spec[0] != "~="),
@ -364,7 +366,6 @@ class Specifier(BaseSpecifier):
return operator_callable return operator_callable
def _compare_compatible(self, prospective: Version, spec: str) -> bool: def _compare_compatible(self, prospective: Version, spec: str) -> bool:
# Compatible releases have an equivalent combination of >= and ==. That # Compatible releases have an equivalent combination of >= and ==. That
# is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
# implement this in terms of the other specifiers instead of # implement this in terms of the other specifiers instead of
@ -385,7 +386,6 @@ class Specifier(BaseSpecifier):
) )
def _compare_equal(self, prospective: Version, spec: str) -> bool: def _compare_equal(self, prospective: Version, spec: str) -> bool:
# We need special logic to handle prefix matching # We need special logic to handle prefix matching
if spec.endswith(".*"): if spec.endswith(".*"):
# In the case of prefix matching we want to ignore local segment. # In the case of prefix matching we want to ignore local segment.
@ -429,21 +429,18 @@ class Specifier(BaseSpecifier):
return not self._compare_equal(prospective, spec) return not self._compare_equal(prospective, spec)
def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version # NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from # specifier, so local version labels can be universally removed from
# the prospective version. # the prospective version.
return Version(prospective.public) <= Version(spec) return Version(prospective.public) <= Version(spec)
def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version # NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from # specifier, so local version labels can be universally removed from
# the prospective version. # the prospective version.
return Version(prospective.public) >= Version(spec) return Version(prospective.public) >= Version(spec)
def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
# Convert our spec to a Version instance, since we'll want to work with # Convert our spec to a Version instance, since we'll want to work with
# it as a version. # it as a version.
spec = Version(spec_str) spec = Version(spec_str)
@ -468,7 +465,6 @@ class Specifier(BaseSpecifier):
return True return True
def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
# Convert our spec to a Version instance, since we'll want to work with # Convert our spec to a Version instance, since we'll want to work with
# it as a version. # it as a version.
spec = Version(spec_str) spec = Version(spec_str)
@ -501,7 +497,7 @@ class Specifier(BaseSpecifier):
def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
return str(prospective).lower() == str(spec).lower() return str(prospective).lower() == str(spec).lower()
def __contains__(self, item: Union[str, Version]) -> bool: def __contains__(self, item: str | Version) -> bool:
"""Return whether or not the item is contained in this specifier. """Return whether or not the item is contained in this specifier.
:param item: The item to check for. :param item: The item to check for.
@ -522,9 +518,7 @@ class Specifier(BaseSpecifier):
""" """
return self.contains(item) return self.contains(item)
def contains( def contains(self, item: UnparsedVersion, prereleases: bool | None = None) -> bool:
self, item: UnparsedVersion, prereleases: Optional[bool] = None
) -> bool:
"""Return whether or not the item is contained in this specifier. """Return whether or not the item is contained in this specifier.
:param item: :param item:
@ -569,7 +563,7 @@ class Specifier(BaseSpecifier):
return operator_callable(normalized_item, self.version) return operator_callable(normalized_item, self.version)
def filter( def filter(
self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]: ) -> Iterator[UnparsedVersionVar]:
"""Filter items in the given iterable, that match the specifier. """Filter items in the given iterable, that match the specifier.
@ -633,7 +627,7 @@ class Specifier(BaseSpecifier):
_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
def _version_split(version: str) -> List[str]: def _version_split(version: str) -> list[str]:
"""Split version into components. """Split version into components.
The split components are intended for version comparison. The logic does The split components are intended for version comparison. The logic does
@ -641,7 +635,7 @@ def _version_split(version: str) -> List[str]:
components back with :func:`_version_join` may not produce the original components back with :func:`_version_join` may not produce the original
version string. version string.
""" """
result: List[str] = [] result: list[str] = []
epoch, _, rest = version.rpartition("!") epoch, _, rest = version.rpartition("!")
result.append(epoch or "0") result.append(epoch or "0")
@ -655,7 +649,7 @@ def _version_split(version: str) -> List[str]:
return result return result
def _version_join(components: List[str]) -> str: def _version_join(components: list[str]) -> str:
"""Join split version components into a version string. """Join split version components into a version string.
This function assumes the input came from :func:`_version_split`, where the This function assumes the input came from :func:`_version_split`, where the
@ -672,7 +666,7 @@ def _is_not_suffix(segment: str) -> bool:
) )
def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: def _pad_version(left: list[str], right: list[str]) -> tuple[list[str], list[str]]:
left_split, right_split = [], [] left_split, right_split = [], []
# Get the release segment of our versions # Get the release segment of our versions
@ -700,9 +694,7 @@ class SpecifierSet(BaseSpecifier):
specifiers (``>=3.0,!=3.1``), or no specifier at all. specifiers (``>=3.0,!=3.1``), or no specifier at all.
""" """
def __init__( def __init__(self, specifiers: str = "", prereleases: bool | None = None) -> None:
self, specifiers: str = "", prereleases: Optional[bool] = None
) -> None:
"""Initialize a SpecifierSet instance. """Initialize a SpecifierSet instance.
:param specifiers: :param specifiers:
@ -730,7 +722,7 @@ class SpecifierSet(BaseSpecifier):
self._prereleases = prereleases self._prereleases = prereleases
@property @property
def prereleases(self) -> Optional[bool]: def prereleases(self) -> bool | None:
# If we have been given an explicit prerelease modifier, then we'll # If we have been given an explicit prerelease modifier, then we'll
# pass that through here. # pass that through here.
if self._prereleases is not None: if self._prereleases is not None:
@ -787,7 +779,7 @@ class SpecifierSet(BaseSpecifier):
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self._specs) return hash(self._specs)
def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": def __and__(self, other: SpecifierSet | str) -> SpecifierSet:
"""Return a SpecifierSet which is a combination of the two sets. """Return a SpecifierSet which is a combination of the two sets.
:param other: The other object to combine with. :param other: The other object to combine with.
@ -883,8 +875,8 @@ class SpecifierSet(BaseSpecifier):
def contains( def contains(
self, self,
item: UnparsedVersion, item: UnparsedVersion,
prereleases: Optional[bool] = None, prereleases: bool | None = None,
installed: Optional[bool] = None, installed: bool | None = None,
) -> bool: ) -> bool:
"""Return whether or not the item is contained in this SpecifierSet. """Return whether or not the item is contained in this SpecifierSet.
@ -938,7 +930,7 @@ class SpecifierSet(BaseSpecifier):
return all(s.contains(item, prereleases=prereleases) for s in self._specs) return all(s.contains(item, prereleases=prereleases) for s in self._specs)
def filter( def filter(
self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]: ) -> Iterator[UnparsedVersionVar]:
"""Filter items in the given iterable, that match the specifiers in this set. """Filter items in the given iterable, that match the specifiers in this set.
@ -995,8 +987,8 @@ class SpecifierSet(BaseSpecifier):
# which will filter out any pre-releases, unless there are no final # which will filter out any pre-releases, unless there are no final
# releases. # releases.
else: else:
filtered: List[UnparsedVersionVar] = [] filtered: list[UnparsedVersionVar] = []
found_prereleases: List[UnparsedVersionVar] = [] found_prereleases: list[UnparsedVersionVar] = []
for item in iterable: for item in iterable:
parsed_version = _coerce_version(item) parsed_version = _coerce_version(item)

View file

@ -2,6 +2,8 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details. # for complete details.
from __future__ import annotations
import logging import logging
import platform import platform
import re import re
@ -11,15 +13,10 @@ import sys
import sysconfig import sysconfig
from importlib.machinery import EXTENSION_SUFFIXES from importlib.machinery import EXTENSION_SUFFIXES
from typing import ( from typing import (
Dict,
FrozenSet,
Iterable, Iterable,
Iterator, Iterator,
List,
Optional,
Sequence, Sequence,
Tuple, Tuple,
Union,
cast, cast,
) )
@ -30,7 +27,7 @@ logger = logging.getLogger(__name__)
PythonVersion = Sequence[int] PythonVersion = Sequence[int]
MacVersion = Tuple[int, int] MacVersion = Tuple[int, int]
INTERPRETER_SHORT_NAMES: Dict[str, str] = { INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic. "python": "py", # Generic.
"cpython": "cp", "cpython": "cp",
"pypy": "pp", "pypy": "pp",
@ -96,7 +93,7 @@ class Tag:
return f"<{self} @ {id(self)}>" return f"<{self} @ {id(self)}>"
def parse_tag(tag: str) -> FrozenSet[Tag]: def parse_tag(tag: str) -> frozenset[Tag]:
""" """
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
@ -112,8 +109,8 @@ def parse_tag(tag: str) -> FrozenSet[Tag]:
return frozenset(tags) return frozenset(tags)
def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: def _get_config_var(name: str, warn: bool = False) -> int | str | None:
value: Union[int, str, None] = sysconfig.get_config_var(name) value: int | str | None = sysconfig.get_config_var(name)
if value is None and warn: if value is None and warn:
logger.debug( logger.debug(
"Config variable '%s' is unset, Python ABI tag may be incorrect", name "Config variable '%s' is unset, Python ABI tag may be incorrect", name
@ -125,7 +122,7 @@ def _normalize_string(string: str) -> str:
return string.replace(".", "_").replace("-", "_").replace(" ", "_") return string.replace(".", "_").replace("-", "_").replace(" ", "_")
def _is_threaded_cpython(abis: List[str]) -> bool: def _is_threaded_cpython(abis: list[str]) -> bool:
""" """
Determine if the ABI corresponds to a threaded (`--disable-gil`) build. Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
@ -151,7 +148,7 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
py_version = tuple(py_version) # To allow for version comparison. py_version = tuple(py_version) # To allow for version comparison.
abis = [] abis = []
version = _version_nodot(py_version[:2]) version = _version_nodot(py_version[:2])
@ -185,9 +182,9 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
def cpython_tags( def cpython_tags(
python_version: Optional[PythonVersion] = None, python_version: PythonVersion | None = None,
abis: Optional[Iterable[str]] = None, abis: Iterable[str] | None = None,
platforms: Optional[Iterable[str]] = None, platforms: Iterable[str] | None = None,
*, *,
warn: bool = False, warn: bool = False,
) -> Iterator[Tag]: ) -> Iterator[Tag]:
@ -244,7 +241,7 @@ def cpython_tags(
yield Tag(interpreter, "abi3", platform_) yield Tag(interpreter, "abi3", platform_)
def _generic_abi() -> List[str]: def _generic_abi() -> list[str]:
""" """
Return the ABI tag based on EXT_SUFFIX. Return the ABI tag based on EXT_SUFFIX.
""" """
@ -286,9 +283,9 @@ def _generic_abi() -> List[str]:
def generic_tags( def generic_tags(
interpreter: Optional[str] = None, interpreter: str | None = None,
abis: Optional[Iterable[str]] = None, abis: Iterable[str] | None = None,
platforms: Optional[Iterable[str]] = None, platforms: Iterable[str] | None = None,
*, *,
warn: bool = False, warn: bool = False,
) -> Iterator[Tag]: ) -> Iterator[Tag]:
@ -332,9 +329,9 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
def compatible_tags( def compatible_tags(
python_version: Optional[PythonVersion] = None, python_version: PythonVersion | None = None,
interpreter: Optional[str] = None, interpreter: str | None = None,
platforms: Optional[Iterable[str]] = None, platforms: Iterable[str] | None = None,
) -> Iterator[Tag]: ) -> Iterator[Tag]:
""" """
Yields the sequence of tags that are compatible with a specific version of Python. Yields the sequence of tags that are compatible with a specific version of Python.
@ -366,7 +363,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
return "i386" return "i386"
def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
formats = [cpu_arch] formats = [cpu_arch]
if cpu_arch == "x86_64": if cpu_arch == "x86_64":
if version < (10, 4): if version < (10, 4):
@ -399,7 +396,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
def mac_platforms( def mac_platforms(
version: Optional[MacVersion] = None, arch: Optional[str] = None version: MacVersion | None = None, arch: str | None = None
) -> Iterator[str]: ) -> Iterator[str]:
""" """
Yields the platform tags for a macOS system. Yields the platform tags for a macOS system.

View file

@ -2,8 +2,10 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details. # for complete details.
from __future__ import annotations
import re import re
from typing import FrozenSet, NewType, Tuple, Union, cast from typing import NewType, Tuple, Union, cast
from .tags import Tag, parse_tag from .tags import Tag, parse_tag
from .version import InvalidVersion, Version from .version import InvalidVersion, Version
@ -53,7 +55,7 @@ def is_normalized_name(name: str) -> bool:
def canonicalize_version( def canonicalize_version(
version: Union[Version, str], *, strip_trailing_zero: bool = True version: Version | str, *, strip_trailing_zero: bool = True
) -> str: ) -> str:
""" """
This is very similar to Version.__str__, but has one subtle difference This is very similar to Version.__str__, but has one subtle difference
@ -102,7 +104,7 @@ def canonicalize_version(
def parse_wheel_filename( def parse_wheel_filename(
filename: str, filename: str,
) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: ) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
if not filename.endswith(".whl"): if not filename.endswith(".whl"):
raise InvalidWheelFilename( raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename}" f"Invalid wheel filename (extension must be '.whl'): {filename}"
@ -143,7 +145,7 @@ def parse_wheel_filename(
return (name, version, build, tags) return (name, version, build, tags)
def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
if filename.endswith(".tar.gz"): if filename.endswith(".tar.gz"):
file_stem = filename[: -len(".tar.gz")] file_stem = filename[: -len(".tar.gz")]
elif filename.endswith(".zip"): elif filename.endswith(".zip"):

View file

@ -7,9 +7,11 @@
from packaging.version import parse, Version from packaging.version import parse, Version
""" """
from __future__ import annotations
import itertools import itertools
import re import re
from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
@ -35,14 +37,14 @@ VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
class _Version(NamedTuple): class _Version(NamedTuple):
epoch: int epoch: int
release: Tuple[int, ...] release: tuple[int, ...]
dev: Optional[Tuple[str, int]] dev: tuple[str, int] | None
pre: Optional[Tuple[str, int]] pre: tuple[str, int] | None
post: Optional[Tuple[str, int]] post: tuple[str, int] | None
local: Optional[LocalType] local: LocalType | None
def parse(version: str) -> "Version": def parse(version: str) -> Version:
"""Parse the given version string. """Parse the given version string.
>>> parse('1.0.dev1') >>> parse('1.0.dev1')
@ -65,7 +67,7 @@ class InvalidVersion(ValueError):
class _BaseVersion: class _BaseVersion:
_key: Tuple[Any, ...] _key: tuple[Any, ...]
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self._key) return hash(self._key)
@ -73,13 +75,13 @@ class _BaseVersion:
# Please keep the duplicated `isinstance` check # Please keep the duplicated `isinstance` check
# in the six comparisons hereunder # in the six comparisons hereunder
# unless you find a way to avoid adding overhead function calls. # unless you find a way to avoid adding overhead function calls.
def __lt__(self, other: "_BaseVersion") -> bool: def __lt__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion): if not isinstance(other, _BaseVersion):
return NotImplemented return NotImplemented
return self._key < other._key return self._key < other._key
def __le__(self, other: "_BaseVersion") -> bool: def __le__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion): if not isinstance(other, _BaseVersion):
return NotImplemented return NotImplemented
@ -91,13 +93,13 @@ class _BaseVersion:
return self._key == other._key return self._key == other._key
def __ge__(self, other: "_BaseVersion") -> bool: def __ge__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion): if not isinstance(other, _BaseVersion):
return NotImplemented return NotImplemented
return self._key >= other._key return self._key >= other._key
def __gt__(self, other: "_BaseVersion") -> bool: def __gt__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion): if not isinstance(other, _BaseVersion):
return NotImplemented return NotImplemented
@ -274,7 +276,7 @@ class Version(_BaseVersion):
return self._version.epoch return self._version.epoch
@property @property
def release(self) -> Tuple[int, ...]: def release(self) -> tuple[int, ...]:
"""The components of the "release" segment of the version. """The components of the "release" segment of the version.
>>> Version("1.2.3").release >>> Version("1.2.3").release
@ -290,7 +292,7 @@ class Version(_BaseVersion):
return self._version.release return self._version.release
@property @property
def pre(self) -> Optional[Tuple[str, int]]: def pre(self) -> tuple[str, int] | None:
"""The pre-release segment of the version. """The pre-release segment of the version.
>>> print(Version("1.2.3").pre) >>> print(Version("1.2.3").pre)
@ -305,7 +307,7 @@ class Version(_BaseVersion):
return self._version.pre return self._version.pre
@property @property
def post(self) -> Optional[int]: def post(self) -> int | None:
"""The post-release number of the version. """The post-release number of the version.
>>> print(Version("1.2.3").post) >>> print(Version("1.2.3").post)
@ -316,7 +318,7 @@ class Version(_BaseVersion):
return self._version.post[1] if self._version.post else None return self._version.post[1] if self._version.post else None
@property @property
def dev(self) -> Optional[int]: def dev(self) -> int | None:
"""The development number of the version. """The development number of the version.
>>> print(Version("1.2.3").dev) >>> print(Version("1.2.3").dev)
@ -327,7 +329,7 @@ class Version(_BaseVersion):
return self._version.dev[1] if self._version.dev else None return self._version.dev[1] if self._version.dev else None
@property @property
def local(self) -> Optional[str]: def local(self) -> str | None:
"""The local version segment of the version. """The local version segment of the version.
>>> print(Version("1.2.3").local) >>> print(Version("1.2.3").local)
@ -450,9 +452,8 @@ class Version(_BaseVersion):
def _parse_letter_version( def _parse_letter_version(
letter: Optional[str], number: Union[str, bytes, SupportsInt, None] letter: str | None, number: str | bytes | SupportsInt | None
) -> Optional[Tuple[str, int]]: ) -> tuple[str, int] | None:
if letter: if letter:
# We consider there to be an implicit 0 in a pre-release if there is # We consider there to be an implicit 0 in a pre-release if there is
# not a numeral associated with it. # not a numeral associated with it.
@ -488,7 +489,7 @@ def _parse_letter_version(
_local_version_separators = re.compile(r"[\._-]") _local_version_separators = re.compile(r"[\._-]")
def _parse_local_version(local: Optional[str]) -> Optional[LocalType]: def _parse_local_version(local: str | None) -> LocalType | None:
""" """
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
""" """
@ -502,13 +503,12 @@ def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
def _cmpkey( def _cmpkey(
epoch: int, epoch: int,
release: Tuple[int, ...], release: tuple[int, ...],
pre: Optional[Tuple[str, int]], pre: tuple[str, int] | None,
post: Optional[Tuple[str, int]], post: tuple[str, int] | None,
dev: Optional[Tuple[str, int]], dev: tuple[str, int] | None,
local: Optional[LocalType], local: LocalType | None,
) -> CmpKey: ) -> CmpKey:
# When we compare a release version, we want to compare it with all of the # When we compare a release version, we want to compare it with all of the
# trailing zeros removed. So we'll use a reverse the list, drop all the now # trailing zeros removed. So we'll use a reverse the list, drop all the now
# leading zeros until we come to something non zero, then take the rest # leading zeros until we come to something non zero, then take the rest

View file

@ -83,7 +83,11 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
# charset_normalizer >= 2.0.0 < 4.0.0 # charset_normalizer >= 2.0.0 < 4.0.0
assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0) assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0)
else: else:
raise Exception("You need either charset_normalizer or chardet installed") warnings.warn(
"Unable to find acceptable character detection dependency "
"(chardet or charset_normalizer).",
RequestsDependencyWarning,
)
def _check_cryptography(cryptography_version): def _check_cryptography(cryptography_version):

View file

@ -5,10 +5,10 @@
__title__ = "requests" __title__ = "requests"
__description__ = "Python HTTP for Humans." __description__ = "Python HTTP for Humans."
__url__ = "https://requests.readthedocs.io" __url__ = "https://requests.readthedocs.io"
__version__ = "2.31.0" __version__ = "2.32.3"
__build__ = 0x023100 __build__ = 0x023203
__author__ = "Kenneth Reitz" __author__ = "Kenneth Reitz"
__author_email__ = "me@kennethreitz.org" __author_email__ = "me@kennethreitz.org"
__license__ = "Apache 2.0" __license__ = "Apache-2.0"
__copyright__ = "Copyright Kenneth Reitz" __copyright__ = "Copyright Kenneth Reitz"
__cake__ = "\u2728 \U0001f370 \u2728" __cake__ = "\u2728 \U0001f370 \u2728"

View file

@ -8,6 +8,8 @@ and maintain connections.
import os.path import os.path
import socket # noqa: F401 import socket # noqa: F401
import typing
import warnings
from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
from urllib3.exceptions import HTTPError as _HTTPError from urllib3.exceptions import HTTPError as _HTTPError
@ -25,6 +27,7 @@ from urllib3.poolmanager import PoolManager, proxy_from_url
from urllib3.util import Timeout as TimeoutSauce from urllib3.util import Timeout as TimeoutSauce
from urllib3.util import parse_url from urllib3.util import parse_url
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from urllib3.util.ssl_ import create_urllib3_context
from .auth import _basic_auth_str from .auth import _basic_auth_str
from .compat import basestring, urlparse from .compat import basestring, urlparse
@ -61,12 +64,76 @@ except ImportError:
raise InvalidSchema("Missing dependencies for SOCKS support.") raise InvalidSchema("Missing dependencies for SOCKS support.")
if typing.TYPE_CHECKING:
from .models import PreparedRequest
DEFAULT_POOLBLOCK = False DEFAULT_POOLBLOCK = False
DEFAULT_POOLSIZE = 10 DEFAULT_POOLSIZE = 10
DEFAULT_RETRIES = 0 DEFAULT_RETRIES = 0
DEFAULT_POOL_TIMEOUT = None DEFAULT_POOL_TIMEOUT = None
try:
import ssl # noqa: F401
_preloaded_ssl_context = create_urllib3_context()
_preloaded_ssl_context.load_verify_locations(
extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
)
except ImportError:
# Bypass default SSLContext creation when Python
# interpreter isn't built with the ssl module.
_preloaded_ssl_context = None
def _urllib3_request_context(
request: "PreparedRequest",
verify: "bool | str | None",
client_cert: "typing.Tuple[str, str] | str | None",
poolmanager: "PoolManager",
) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])":
host_params = {}
pool_kwargs = {}
parsed_request_url = urlparse(request.url)
scheme = parsed_request_url.scheme.lower()
port = parsed_request_url.port
# Determine if we have and should use our default SSLContext
# to optimize performance on standard requests.
poolmanager_kwargs = getattr(poolmanager, "connection_pool_kw", {})
has_poolmanager_ssl_context = poolmanager_kwargs.get("ssl_context")
should_use_default_ssl_context = (
_preloaded_ssl_context is not None and not has_poolmanager_ssl_context
)
cert_reqs = "CERT_REQUIRED"
if verify is False:
cert_reqs = "CERT_NONE"
elif verify is True and should_use_default_ssl_context:
pool_kwargs["ssl_context"] = _preloaded_ssl_context
elif isinstance(verify, str):
if not os.path.isdir(verify):
pool_kwargs["ca_certs"] = verify
else:
pool_kwargs["ca_cert_dir"] = verify
pool_kwargs["cert_reqs"] = cert_reqs
if client_cert is not None:
if isinstance(client_cert, tuple) and len(client_cert) == 2:
pool_kwargs["cert_file"] = client_cert[0]
pool_kwargs["key_file"] = client_cert[1]
else:
# According to our docs, we allow users to specify just the client
# cert path
pool_kwargs["cert_file"] = client_cert
host_params = {
"scheme": scheme,
"host": parsed_request_url.hostname,
"port": port,
}
return host_params, pool_kwargs
class BaseAdapter: class BaseAdapter:
"""The Base Transport Adapter""" """The Base Transport Adapter"""
@ -247,28 +314,26 @@ class HTTPAdapter(BaseAdapter):
:param cert: The SSL certificate to verify. :param cert: The SSL certificate to verify.
""" """
if url.lower().startswith("https") and verify: if url.lower().startswith("https") and verify:
cert_loc = None
# Allow self-specified cert location.
if verify is not True:
cert_loc = verify
if not cert_loc:
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
if not cert_loc or not os.path.exists(cert_loc):
raise OSError(
f"Could not find a suitable TLS CA certificate bundle, "
f"invalid path: {cert_loc}"
)
conn.cert_reqs = "CERT_REQUIRED" conn.cert_reqs = "CERT_REQUIRED"
if not os.path.isdir(cert_loc): # Only load the CA certificates if 'verify' is a string indicating the CA bundle to use.
conn.ca_certs = cert_loc # Otherwise, if verify is a boolean, we don't load anything since
else: # the connection will be using a context with the default certificates already loaded,
conn.ca_cert_dir = cert_loc # and this avoids a call to the slow load_verify_locations()
if verify is not True:
# `verify` must be a str with a path then
cert_loc = verify
if not os.path.exists(cert_loc):
raise OSError(
f"Could not find a suitable TLS CA certificate bundle, "
f"invalid path: {cert_loc}"
)
if not os.path.isdir(cert_loc):
conn.ca_certs = cert_loc
else:
conn.ca_cert_dir = cert_loc
else: else:
conn.cert_reqs = "CERT_NONE" conn.cert_reqs = "CERT_NONE"
conn.ca_certs = None conn.ca_certs = None
@ -328,8 +393,110 @@ class HTTPAdapter(BaseAdapter):
return response return response
def build_connection_pool_key_attributes(self, request, verify, cert=None):
"""Build the PoolKey attributes used by urllib3 to return a connection.
This looks at the PreparedRequest, the user-specified verify value,
and the value of the cert parameter to determine what PoolKey values
to use to select a connection from a given urllib3 Connection Pool.
The SSL related pool key arguments are not consistently set. As of
this writing, use the following to determine what keys may be in that
dictionary:
* If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the
default Requests SSL Context
* If ``verify`` is ``False``, ``"ssl_context"`` will not be set but
``"cert_reqs"`` will be set
* If ``verify`` is a string, (i.e., it is a user-specified trust bundle)
``"ca_certs"`` will be set if the string is not a directory recognized
by :py:func:`os.path.isdir`, otherwise ``"ca_certs_dir"`` will be
set.
* If ``"cert"`` is specified, ``"cert_file"`` will always be set. If
``"cert"`` is a tuple with a second item, ``"key_file"`` will also
be present
To override these settings, one may subclass this class, call this
method and use the above logic to change parameters as desired. For
example, if one wishes to use a custom :py:class:`ssl.SSLContext` one
must both set ``"ssl_context"`` and based on what else they require,
alter the other keys to ensure the desired behaviour.
:param request:
The PreparedReqest being sent over the connection.
:type request:
:class:`~requests.models.PreparedRequest`
:param verify:
Either a boolean, in which case it controls whether
we verify the server's TLS certificate, or a string, in which case it
must be a path to a CA bundle to use.
:param cert:
(optional) Any user-provided SSL certificate for client
authentication (a.k.a., mTLS). This may be a string (i.e., just
the path to a file which holds both certificate and key) or a
tuple of length 2 with the certificate file path and key file
path.
:returns:
A tuple of two dictionaries. The first is the "host parameters"
portion of the Pool Key including scheme, hostname, and port. The
second is a dictionary of SSLContext related parameters.
"""
return _urllib3_request_context(request, verify, cert, self.poolmanager)
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
"""Returns a urllib3 connection for the given request and TLS settings.
This should not be called from user code, and is only exposed for use
when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
:param request:
The :class:`PreparedRequest <PreparedRequest>` object to be sent
over the connection.
:param verify:
Either a boolean, in which case it controls whether we verify the
server's TLS certificate, or a string, in which case it must be a
path to a CA bundle to use.
:param proxies:
(optional) The proxies dictionary to apply to the request.
:param cert:
(optional) Any user-provided SSL certificate to be used for client
authentication (a.k.a., mTLS).
:rtype:
urllib3.ConnectionPool
"""
proxy = select_proxy(request.url, proxies)
try:
host_params, pool_kwargs = self.build_connection_pool_key_attributes(
request,
verify,
cert,
)
except ValueError as e:
raise InvalidURL(e, request=request)
if proxy:
proxy = prepend_scheme_if_needed(proxy, "http")
proxy_url = parse_url(proxy)
if not proxy_url.host:
raise InvalidProxyURL(
"Please check proxy URL. It is malformed "
"and could be missing the host."
)
proxy_manager = self.proxy_manager_for(proxy)
conn = proxy_manager.connection_from_host(
**host_params, pool_kwargs=pool_kwargs
)
else:
# Only scheme should be lower case
conn = self.poolmanager.connection_from_host(
**host_params, pool_kwargs=pool_kwargs
)
return conn
def get_connection(self, url, proxies=None): def get_connection(self, url, proxies=None):
"""Returns a urllib3 connection for the given URL. This should not be """DEPRECATED: Users should move to `get_connection_with_tls_context`
for all subclasses of HTTPAdapter using Requests>=2.32.2.
Returns a urllib3 connection for the given URL. This should not be
called from user code, and is only exposed for use when subclassing the called from user code, and is only exposed for use when subclassing the
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
@ -337,6 +504,15 @@ class HTTPAdapter(BaseAdapter):
:param proxies: (optional) A Requests-style dictionary of proxies used on this request. :param proxies: (optional) A Requests-style dictionary of proxies used on this request.
:rtype: urllib3.ConnectionPool :rtype: urllib3.ConnectionPool
""" """
warnings.warn(
(
"`get_connection` has been deprecated in favor of "
"`get_connection_with_tls_context`. Custom HTTPAdapter subclasses "
"will need to migrate for Requests>=2.32.2. Please see "
"https://github.com/psf/requests/pull/6710 for more details."
),
DeprecationWarning,
)
proxy = select_proxy(url, proxies) proxy = select_proxy(url, proxies)
if proxy: if proxy:
@ -391,6 +567,9 @@ class HTTPAdapter(BaseAdapter):
using_socks_proxy = proxy_scheme.startswith("socks") using_socks_proxy = proxy_scheme.startswith("socks")
url = request.path_url url = request.path_url
if url.startswith("//"): # Don't confuse urllib3
url = f"/{url.lstrip('/')}"
if is_proxied_http_request and not using_socks_proxy: if is_proxied_http_request and not using_socks_proxy:
url = urldefragauth(request.url) url = urldefragauth(request.url)
@ -451,7 +630,9 @@ class HTTPAdapter(BaseAdapter):
""" """
try: try:
conn = self.get_connection(request.url, proxies) conn = self.get_connection_with_tls_context(
request, verify, proxies=proxies, cert=cert
)
except LocationValueError as e: except LocationValueError as e:
raise InvalidURL(e, request=request) raise InvalidURL(e, request=request)

View file

@ -25,7 +25,7 @@ def request(method, url, **kwargs):
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload. :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content_type'`` is a string
defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
to add for the file. to add for the file.
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.

View file

@ -258,7 +258,6 @@ class HTTPDigestAuth(AuthBase):
s_auth = r.headers.get("www-authenticate", "") s_auth = r.headers.get("www-authenticate", "")
if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2: if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
self._thread_local.num_401_calls += 1 self._thread_local.num_401_calls += 1
pat = re.compile(r"digest ", flags=re.IGNORECASE) pat = re.compile(r"digest ", flags=re.IGNORECASE)
self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1)) self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))

View file

@ -7,13 +7,28 @@ between Python 2 and Python 3. It remains for backwards
compatibility until the next major version. compatibility until the next major version.
""" """
try: import importlib
import chardet
except ImportError:
import charset_normalizer as chardet
import sys import sys
# -------------------
# Character Detection
# -------------------
def _resolve_char_detection():
"""Find supported character detection libraries."""
chardet = None
for lib in ("chardet", "charset_normalizer"):
if chardet is None:
try:
chardet = importlib.import_module(lib)
except ImportError:
pass
return chardet
chardet = _resolve_char_detection()
# ------- # -------
# Pythons # Pythons
# ------- # -------

View file

@ -2,7 +2,7 @@
requests.cookies requests.cookies
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
Compatibility code to be able to use `cookielib.CookieJar` with requests. Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.
requests.utils imports from here, so be careful with imports. requests.utils imports from here, so be careful with imports.
""" """
@ -23,7 +23,7 @@ except ImportError:
class MockRequest: class MockRequest:
"""Wraps a `requests.Request` to mimic a `urllib2.Request`. """Wraps a `requests.Request` to mimic a `urllib2.Request`.
The code in `cookielib.CookieJar` expects this interface in order to correctly The code in `http.cookiejar.CookieJar` expects this interface in order to correctly
manage cookie policies, i.e., determine whether a cookie can be set, given the manage cookie policies, i.e., determine whether a cookie can be set, given the
domains of the request and the cookie. domains of the request and the cookie.
@ -76,7 +76,7 @@ class MockRequest:
return self._r.headers.get(name, self._new_headers.get(name, default)) return self._r.headers.get(name, self._new_headers.get(name, default))
def add_header(self, key, val): def add_header(self, key, val):
"""cookielib has no legitimate use for this method; add it back if you find one.""" """cookiejar has no legitimate use for this method; add it back if you find one."""
raise NotImplementedError( raise NotImplementedError(
"Cookie headers should be added with add_unredirected_header()" "Cookie headers should be added with add_unredirected_header()"
) )
@ -104,11 +104,11 @@ class MockResponse:
"""Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`. """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
...what? Basically, expose the parsed HTTP headers from the server response ...what? Basically, expose the parsed HTTP headers from the server response
the way `cookielib` expects to see them. the way `http.cookiejar` expects to see them.
""" """
def __init__(self, headers): def __init__(self, headers):
"""Make a MockResponse for `cookielib` to read. """Make a MockResponse for `cookiejar` to read.
:param headers: a httplib.HTTPMessage or analogous carrying the headers :param headers: a httplib.HTTPMessage or analogous carrying the headers
""" """
@ -124,7 +124,7 @@ class MockResponse:
def extract_cookies_to_jar(jar, request, response): def extract_cookies_to_jar(jar, request, response):
"""Extract the cookies from the response into a CookieJar. """Extract the cookies from the response into a CookieJar.
:param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar) :param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar)
:param request: our own requests.Request object :param request: our own requests.Request object
:param response: urllib3.HTTPResponse object :param response: urllib3.HTTPResponse object
""" """
@ -174,7 +174,7 @@ class CookieConflictError(RuntimeError):
class RequestsCookieJar(cookielib.CookieJar, MutableMapping): class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict """Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict
interface. interface.
This is the CookieJar we create by default for requests and sessions that This is the CookieJar we create by default for requests and sessions that
@ -341,7 +341,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
self.set(name, value) self.set(name, value)
def __delitem__(self, name): def __delitem__(self, name):
"""Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s """Deletes a cookie given a name. Wraps ``http.cookiejar.CookieJar``'s
``remove_cookie_by_name()``. ``remove_cookie_by_name()``.
""" """
remove_cookie_by_name(self, name) remove_cookie_by_name(self, name)

View file

@ -41,6 +41,16 @@ class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
CompatJSONDecodeError.__init__(self, *args) CompatJSONDecodeError.__init__(self, *args)
InvalidJSONError.__init__(self, *self.args, **kwargs) InvalidJSONError.__init__(self, *self.args, **kwargs)
def __reduce__(self):
"""
The __reduce__ method called when pickling the object must
be the one from the JSONDecodeError (be it json/simplejson)
as it expects all the arguments for instantiation, not just
one like the IOError, and the MRO would by default call the
__reduce__ method from the IOError due to the inheritance order.
"""
return CompatJSONDecodeError.__reduce__(self)
class HTTPError(RequestException): class HTTPError(RequestException):
"""An HTTP error occurred.""" """An HTTP error occurred."""

View file

@ -170,7 +170,7 @@ class RequestEncodingMixin:
) )
) )
for (k, v) in files: for k, v in files:
# support for explicit filename # support for explicit filename
ft = None ft = None
fh = None fh = None
@ -268,7 +268,6 @@ class Request(RequestHooksMixin):
hooks=None, hooks=None,
json=None, json=None,
): ):
# Default empty dicts for dict params. # Default empty dicts for dict params.
data = [] if data is None else data data = [] if data is None else data
files = [] if files is None else files files = [] if files is None else files
@ -277,7 +276,7 @@ class Request(RequestHooksMixin):
hooks = {} if hooks is None else hooks hooks = {} if hooks is None else hooks
self.hooks = default_hooks() self.hooks = default_hooks()
for (k, v) in list(hooks.items()): for k, v in list(hooks.items()):
self.register_hook(event=k, hook=v) self.register_hook(event=k, hook=v)
self.method = method self.method = method
@ -790,7 +789,12 @@ class Response:
@property @property
def apparent_encoding(self): def apparent_encoding(self):
"""The apparent encoding, provided by the charset_normalizer or chardet libraries.""" """The apparent encoding, provided by the charset_normalizer or chardet libraries."""
return chardet.detect(self.content)["encoding"] if chardet is not None:
return chardet.detect(self.content)["encoding"]
else:
# If no character detection library is available, we'll fall back
# to a standard Python utf-8 str.
return "utf-8"
def iter_content(self, chunk_size=1, decode_unicode=False): def iter_content(self, chunk_size=1, decode_unicode=False):
"""Iterates over the response data. When stream=True is set on the """Iterates over the response data. When stream=True is set on the
@ -865,7 +869,6 @@ class Response:
for chunk in self.iter_content( for chunk in self.iter_content(
chunk_size=chunk_size, decode_unicode=decode_unicode chunk_size=chunk_size, decode_unicode=decode_unicode
): ):
if pending is not None: if pending is not None:
chunk = pending + chunk chunk = pending + chunk

View file

@ -1,13 +1,6 @@
import sys import sys
try: from .compat import chardet
import chardet
except ImportError:
import warnings
import charset_normalizer as chardet
warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer")
# This code exists for backwards compatibility reasons. # This code exists for backwards compatibility reasons.
# I don't like it either. Just look the other way. :) # I don't like it either. Just look the other way. :)
@ -20,9 +13,11 @@ for package in ("urllib3", "idna"):
if mod == package or mod.startswith(f"{package}."): if mod == package or mod.startswith(f"{package}."):
sys.modules[f"requests.packages.{mod}"] = sys.modules[mod] sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
target = chardet.__name__ if chardet is not None:
for mod in list(sys.modules): target = chardet.__name__
if mod == target or mod.startswith(f"{target}."): for mod in list(sys.modules):
target = target.replace(target, "chardet") if mod == target or mod.startswith(f"{target}."):
sys.modules[f"requests.packages.{target}"] = sys.modules[mod] imported_mod = sys.modules[mod]
# Kinda cool, though, right? sys.modules[f"requests.packages.{mod}"] = imported_mod
mod = mod.replace(target, "chardet")
sys.modules[f"requests.packages.{mod}"] = imported_mod

View file

@ -262,7 +262,6 @@ class SessionRedirectMixin:
if yield_requests: if yield_requests:
yield req yield req
else: else:
resp = self.send( resp = self.send(
req, req,
stream=stream, stream=stream,
@ -326,7 +325,7 @@ class SessionRedirectMixin:
# urllib3 handles proxy authorization for us in the standard adapter. # urllib3 handles proxy authorization for us in the standard adapter.
# Avoid appending this to TLS tunneled requests where it may be leaked. # Avoid appending this to TLS tunneled requests where it may be leaked.
if not scheme.startswith('https') and username and password: if not scheme.startswith("https") and username and password:
headers["Proxy-Authorization"] = _basic_auth_str(username, password) headers["Proxy-Authorization"] = _basic_auth_str(username, password)
return new_proxies return new_proxies
@ -389,7 +388,6 @@ class Session(SessionRedirectMixin):
] ]
def __init__(self): def __init__(self):
#: A case-insensitive dictionary of headers to be sent on each #: A case-insensitive dictionary of headers to be sent on each
#: :class:`Request <Request>` sent from this #: :class:`Request <Request>` sent from this
#: :class:`Session <Session>`. #: :class:`Session <Session>`.
@ -545,6 +543,8 @@ class Session(SessionRedirectMixin):
:type allow_redirects: bool :type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol or protocol and :param proxies: (optional) Dictionary mapping protocol or protocol and
hostname to the URL of the proxy. hostname to the URL of the proxy.
:param hooks: (optional) Dictionary mapping hook name to one event or
list of events, event must be callable.
:param stream: (optional) whether to immediately download the response :param stream: (optional) whether to immediately download the response
content. Defaults to ``False``. content. Defaults to ``False``.
:param verify: (optional) Either a boolean, in which case it controls whether we verify :param verify: (optional) Either a boolean, in which case it controls whether we verify
@ -711,7 +711,6 @@ class Session(SessionRedirectMixin):
# Persist cookies # Persist cookies
if r.history: if r.history:
# If the hooks create history then we want those cookies too # If the hooks create history then we want those cookies too
for resp in r.history: for resp in r.history:
extract_cookies_to_jar(self.cookies, resp.request, resp.raw) extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
@ -759,7 +758,7 @@ class Session(SessionRedirectMixin):
# Set environment's proxies. # Set environment's proxies.
no_proxy = proxies.get("no_proxy") if proxies is not None else None no_proxy = proxies.get("no_proxy") if proxies is not None else None
env_proxies = get_environ_proxies(url, no_proxy=no_proxy) env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
for (k, v) in env_proxies.items(): for k, v in env_proxies.items():
proxies.setdefault(k, v) proxies.setdefault(k, v)
# Look for requests environment configuration # Look for requests environment configuration
@ -785,8 +784,7 @@ class Session(SessionRedirectMixin):
:rtype: requests.adapters.BaseAdapter :rtype: requests.adapters.BaseAdapter
""" """
for (prefix, adapter) in self.adapters.items(): for prefix, adapter in self.adapters.items():
if url.lower().startswith(prefix.lower()): if url.lower().startswith(prefix.lower()):
return adapter return adapter

View file

@ -24,7 +24,7 @@ _codes = {
# Informational. # Informational.
100: ("continue",), 100: ("continue",),
101: ("switching_protocols",), 101: ("switching_protocols",),
102: ("processing",), 102: ("processing", "early-hints"),
103: ("checkpoint",), 103: ("checkpoint",),
122: ("uri_too_long", "request_uri_too_long"), 122: ("uri_too_long", "request_uri_too_long"),
200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", ""), 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", ""),
@ -65,8 +65,8 @@ _codes = {
410: ("gone",), 410: ("gone",),
411: ("length_required",), 411: ("length_required",),
412: ("precondition_failed", "precondition"), 412: ("precondition_failed", "precondition"),
413: ("request_entity_too_large",), 413: ("request_entity_too_large", "content_too_large"),
414: ("request_uri_too_large",), 414: ("request_uri_too_large", "uri_too_long"),
415: ("unsupported_media_type", "unsupported_media", "media_type"), 415: ("unsupported_media_type", "unsupported_media", "media_type"),
416: ( 416: (
"requested_range_not_satisfiable", "requested_range_not_satisfiable",
@ -76,10 +76,10 @@ _codes = {
417: ("expectation_failed",), 417: ("expectation_failed",),
418: ("im_a_teapot", "teapot", "i_am_a_teapot"), 418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
421: ("misdirected_request",), 421: ("misdirected_request",),
422: ("unprocessable_entity", "unprocessable"), 422: ("unprocessable_entity", "unprocessable", "unprocessable_content"),
423: ("locked",), 423: ("locked",),
424: ("failed_dependency", "dependency"), 424: ("failed_dependency", "dependency"),
425: ("unordered_collection", "unordered"), 425: ("unordered_collection", "unordered", "too_early"),
426: ("upgrade_required", "upgrade"), 426: ("upgrade_required", "upgrade"),
428: ("precondition_required", "precondition"), 428: ("precondition_required", "precondition"),
429: ("too_many_requests", "too_many"), 429: ("too_many_requests", "too_many"),

View file

@ -97,6 +97,8 @@ if sys.platform == "win32":
# '<local>' string by the localhost entry and the corresponding # '<local>' string by the localhost entry and the corresponding
# canonical entry. # canonical entry.
proxyOverride = proxyOverride.split(";") proxyOverride = proxyOverride.split(";")
# filter out empty strings to avoid re.match return true in the following code.
proxyOverride = filter(None, proxyOverride)
# now check if we match one of the registry values. # now check if we match one of the registry values.
for test in proxyOverride: for test in proxyOverride:
if test == "<local>": if test == "<local>":
@ -134,6 +136,9 @@ def super_len(o):
total_length = None total_length = None
current_position = 0 current_position = 0
if isinstance(o, str):
o = o.encode("utf-8")
if hasattr(o, "__len__"): if hasattr(o, "__len__"):
total_length = len(o) total_length = len(o)
@ -466,11 +471,7 @@ def dict_from_cookiejar(cj):
:rtype: dict :rtype: dict
""" """
cookie_dict = {} cookie_dict = {cookie.name: cookie.value for cookie in cj}
for cookie in cj:
cookie_dict[cookie.name] = cookie.value
return cookie_dict return cookie_dict
@ -767,6 +768,7 @@ def should_bypass_proxies(url, no_proxy):
:rtype: bool :rtype: bool
""" """
# Prioritize lowercase environment variables over uppercase # Prioritize lowercase environment variables over uppercase
# to keep a consistent behaviour with other http projects (curl, wget). # to keep a consistent behaviour with other http projects (curl, wget).
def get_proxy(key): def get_proxy(key):
@ -862,7 +864,7 @@ def select_proxy(url, proxies):
def resolve_proxies(request, proxies, trust_env=True): def resolve_proxies(request, proxies, trust_env=True):
"""This method takes proxy information from a request and configuration """This method takes proxy information from a request and configuration
input to resolve a mapping of target proxies. This will consider settings input to resolve a mapping of target proxies. This will consider settings
such a NO_PROXY to strip proxy configurations. such as NO_PROXY to strip proxy configurations.
:param request: Request or PreparedRequest :param request: Request or PreparedRequest
:param proxies: A dictionary of schemes or schemes and hosts to proxy URLs :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
@ -1054,7 +1056,7 @@ def _validate_header_part(header, header_part, header_validator_index):
if not validator.match(header_part): if not validator.match(header_part):
header_kind = "name" if header_validator_index == 0 else "value" header_kind = "name" if header_validator_index == 0 else "value"
raise InvalidHeader( raise InvalidHeader(
f"Invalid leading whitespace, reserved character(s), or return" f"Invalid leading whitespace, reserved character(s), or return "
f"character(s) in header {header_kind}: {header_part!r}" f"character(s) in header {header_kind}: {header_part!r}"
) )

View file

@ -3,14 +3,24 @@ Classes for calling functions a schedule. Has time zone support.
For example, to run a job at 08:00 every morning in 'Asia/Calcutta': For example, to run a job at 08:00 every morning in 'Asia/Calcutta':
>>> from tests.compat.py38 import zoneinfo
>>> job = lambda: print("time is now", datetime.datetime()) >>> job = lambda: print("time is now", datetime.datetime())
>>> time = datetime.time(8, tzinfo=pytz.timezone('Asia/Calcutta')) >>> time = datetime.time(8, tzinfo=zoneinfo.ZoneInfo('Asia/Calcutta'))
>>> cmd = PeriodicCommandFixedDelay.daily_at(time, job) >>> cmd = PeriodicCommandFixedDelay.daily_at(time, job)
>>> sched = InvokeScheduler() >>> sched = InvokeScheduler()
>>> sched.add(cmd) >>> sched.add(cmd)
>>> while True: # doctest: +SKIP >>> while True: # doctest: +SKIP
... sched.run_pending() ... sched.run_pending()
... time.sleep(.1) ... time.sleep(.1)
By default, the scheduler uses timezone-aware times in UTC. A
client may override the default behavior by overriding ``now``
and ``from_timestamp`` functions.
>>> now()
datetime.datetime(...utc)
>>> from_timestamp(1718723533.7685602)
datetime.datetime(...utc)
""" """
import datetime import datetime
@ -18,27 +28,7 @@ import numbers
import abc import abc
import bisect import bisect
import pytz from .utc import now, fromtimestamp as from_timestamp
def now():
"""
Provide the current timezone-aware datetime.
A client may override this function to change the default behavior,
such as to use local time or timezone-naïve times.
"""
return datetime.datetime.now(pytz.utc)
def from_timestamp(ts):
"""
Convert a numeric timestamp to a timezone-aware datetime.
A client may override this function to change the default behavior,
such as to use local time or timezone-naïve times.
"""
return datetime.datetime.fromtimestamp(ts, pytz.utc)
class DelayedCommand(datetime.datetime): class DelayedCommand(datetime.datetime):
@ -106,18 +96,7 @@ class PeriodicCommand(DelayedCommand):
""" """
Add delay to self, localized Add delay to self, localized
""" """
return self._localize(self + self.delay) return self + self.delay
@staticmethod
def _localize(dt):
"""
Rely on pytz.localize to ensure new result honors DST.
"""
try:
tz = dt.tzinfo
return tz.localize(dt.replace(tzinfo=None))
except AttributeError:
return dt
def next(self): def next(self):
cmd = self.__class__.from_datetime(self._next_time()) cmd = self.__class__.from_datetime(self._next_time())
@ -127,9 +106,7 @@ class PeriodicCommand(DelayedCommand):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key == 'delay' and not value > datetime.timedelta(): if key == 'delay' and not value > datetime.timedelta():
raise ValueError( raise ValueError("A PeriodicCommand must have a positive, non-zero delay.")
"A PeriodicCommand must have a positive, " "non-zero delay."
)
super().__setattr__(key, value) super().__setattr__(key, value)
@ -172,7 +149,7 @@ class PeriodicCommandFixedDelay(PeriodicCommand):
when -= daily when -= daily
while when < now(): while when < now():
when += daily when += daily
return cls.at_time(cls._localize(when), daily, target) return cls.at_time(when, daily, target)
class Scheduler: class Scheduler:

View file

@ -21,6 +21,13 @@ datetime.timezone.utc
>>> time(0, 0).tzinfo >>> time(0, 0).tzinfo
datetime.timezone.utc datetime.timezone.utc
Now should be affected by freezegun.
>>> freezer = getfixture('freezer')
>>> freezer.move_to('1999-12-31 17:00:00 -0700')
>>> print(now())
2000-01-01 00:00:00+00:00
""" """
import datetime as std import datetime as std
@ -30,7 +37,10 @@ import functools
__all__ = ['now', 'fromtimestamp', 'datetime', 'time'] __all__ = ['now', 'fromtimestamp', 'datetime', 'time']
now = functools.partial(std.datetime.now, std.timezone.utc) def now():
return std.datetime.now(std.timezone.utc)
fromtimestamp = functools.partial(std.datetime.fromtimestamp, tz=std.timezone.utc) fromtimestamp = functools.partial(std.datetime.fromtimestamp, tz=std.timezone.utc)
datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc) datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc)
time = functools.partial(std.time, tzinfo=std.timezone.utc) time = functools.partial(std.time, tzinfo=std.timezone.utc)

View file

@ -32,22 +32,24 @@ from typing import (
Union, Union,
) )
from unittest.mock import Mock from unittest.mock import Mock
from weakref import WeakKeyDictionary
try: try:
import typing_extensions import typing_extensions
except ImportError: except ImportError:
typing_extensions = None # type: ignore[assignment] typing_extensions = None # type: ignore[assignment]
# Must use this because typing.is_typeddict does not recognize
# TypedDict from typing_extensions, and as of version 4.12.0
# typing_extensions.TypedDict is different from typing.TypedDict
# on all versions.
from typing_extensions import is_typeddict
from ._config import ForwardRefPolicy from ._config import ForwardRefPolicy
from ._exceptions import TypeCheckError, TypeHintWarning from ._exceptions import TypeCheckError, TypeHintWarning
from ._memo import TypeCheckMemo from ._memo import TypeCheckMemo
from ._utils import evaluate_forwardref, get_stacklevel, get_type_name, qualified_name from ._utils import evaluate_forwardref, get_stacklevel, get_type_name, qualified_name
if sys.version_info >= (3, 13):
from typing import is_typeddict
else:
from typing_extensions import is_typeddict
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
from typing import ( from typing import (
Annotated, Annotated,
@ -87,6 +89,9 @@ generic_alias_types: tuple[type, ...] = (type(List), type(List[Any]))
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
generic_alias_types += (types.GenericAlias,) generic_alias_types += (types.GenericAlias,)
protocol_check_cache: WeakKeyDictionary[
type[Any], dict[type[Any], TypeCheckError | None]
] = WeakKeyDictionary()
# Sentinel # Sentinel
_missing = object() _missing = object()
@ -649,19 +654,96 @@ def check_protocol(
args: tuple[Any, ...], args: tuple[Any, ...],
memo: TypeCheckMemo, memo: TypeCheckMemo,
) -> None: ) -> None:
# TODO: implement proper compatibility checking and support non-runtime protocols subject: type[Any] = value if isclass(value) else type(value)
if getattr(origin_type, "_is_runtime_protocol", False):
if not isinstance(value, origin_type): if subject in protocol_check_cache:
raise TypeCheckError( result_map = protocol_check_cache[subject]
f"is not compatible with the {origin_type.__qualname__} protocol" if origin_type in result_map:
if exc := result_map[origin_type]:
raise exc
else:
return
# Collect a set of methods and non-method attributes present in the protocol
ignored_attrs = set(dir(typing.Protocol)) | {
"__annotations__",
"__non_callable_proto_members__",
}
expected_methods: dict[str, tuple[Any, Any]] = {}
expected_noncallable_members: dict[str, Any] = {}
for attrname in dir(origin_type):
# Skip attributes present in typing.Protocol
if attrname in ignored_attrs:
continue
member = getattr(origin_type, attrname)
if callable(member):
signature = inspect.signature(member)
argtypes = [
(p.annotation if p.annotation is not Parameter.empty else Any)
for p in signature.parameters.values()
if p.kind is not Parameter.KEYWORD_ONLY
] or Ellipsis
return_annotation = (
signature.return_annotation
if signature.return_annotation is not Parameter.empty
else Any
) )
expected_methods[attrname] = argtypes, return_annotation
else:
expected_noncallable_members[attrname] = member
for attrname, annotation in typing.get_type_hints(origin_type).items():
expected_noncallable_members[attrname] = annotation
subject_annotations = typing.get_type_hints(subject)
# Check that all required methods are present and their signatures are compatible
result_map = protocol_check_cache.setdefault(subject, {})
try:
for attrname, callable_args in expected_methods.items():
try:
method = getattr(subject, attrname)
except AttributeError:
if attrname in subject_annotations:
raise TypeCheckError(
f"is not compatible with the {origin_type.__qualname__} protocol "
f"because its {attrname!r} attribute is not a method"
) from None
else:
raise TypeCheckError(
f"is not compatible with the {origin_type.__qualname__} protocol "
f"because it has no method named {attrname!r}"
) from None
if not callable(method):
raise TypeCheckError(
f"is not compatible with the {origin_type.__qualname__} protocol "
f"because its {attrname!r} attribute is not a callable"
)
# TODO: raise exception on added keyword-only arguments without defaults
try:
check_callable(method, Callable, callable_args, memo)
except TypeCheckError as exc:
raise TypeCheckError(
f"is not compatible with the {origin_type.__qualname__} protocol "
f"because its {attrname!r} method {exc}"
) from None
# Check that all required non-callable members are present
for attrname in expected_noncallable_members:
# TODO: implement assignability checks for non-callable members
if attrname not in subject_annotations and not hasattr(subject, attrname):
raise TypeCheckError(
f"is not compatible with the {origin_type.__qualname__} protocol "
f"because it has no attribute named {attrname!r}"
)
except TypeCheckError as exc:
result_map[origin_type] = exc
raise
else: else:
warnings.warn( result_map[origin_type] = None
f"Typeguard cannot check the {origin_type.__qualname__} protocol because "
f"it is a non-runtime protocol. If you would like to type check this "
f"protocol, please use @typing.runtime_checkable",
stacklevel=get_stacklevel(),
)
def check_byteslike( def check_byteslike(
@ -852,7 +934,8 @@ def builtin_checker_lookup(
elif is_typeddict(origin_type): elif is_typeddict(origin_type):
return check_typed_dict return check_typed_dict
elif isclass(origin_type) and issubclass( elif isclass(origin_type) and issubclass(
origin_type, Tuple # type: ignore[arg-type] origin_type,
Tuple, # type: ignore[arg-type]
): ):
# NamedTuple # NamedTuple
return check_tuple return check_tuple

View file

@ -2,21 +2,22 @@ from __future__ import annotations
import sys import sys
import warnings import warnings
from typing import Any, Literal from typing import TYPE_CHECKING, Any, Literal
from pytest import Config, Parser
from typeguard._config import CollectionCheckStrategy, ForwardRefPolicy, global_config from typeguard._config import CollectionCheckStrategy, ForwardRefPolicy, global_config
from typeguard._exceptions import InstrumentationWarning from typeguard._exceptions import InstrumentationWarning
from typeguard._importhook import install_import_hook from typeguard._importhook import install_import_hook
from typeguard._utils import qualified_name, resolve_reference from typeguard._utils import qualified_name, resolve_reference
if TYPE_CHECKING:
from pytest import Config, Parser
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
def add_ini_option( def add_ini_option(
opt_type: ( opt_type: (
Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None
) ),
) -> None: ) -> None:
parser.addini( parser.addini(
group.options[-1].names()[0][2:], group.options[-1].names()[0][2:],

View file

@ -28,7 +28,7 @@ def suppress_type_checks() -> ContextManager[None]: ...
def suppress_type_checks( def suppress_type_checks(
func: Callable[P, T] | None = None func: Callable[P, T] | None = None,
) -> Callable[P, T] | ContextManager[None]: ) -> Callable[P, T] | ContextManager[None]:
""" """
Temporarily suppress all type checking. Temporarily suppress all type checking.

Some files were not shown because too many files have changed in this diff Show more