conflict rez + pip3 install cherrypy

This commit is contained in:
carl 2024-01-18 14:51:16 -04:00
commit cbcdd3e14c
671 changed files with 102136 additions and 32 deletions

8
awesome_venv/bin/calc-prorate Executable file
View File

@ -0,0 +1,8 @@
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from tempora import calculate_prorated_values
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(calculate_prorated_values())

8
awesome_venv/bin/cheroot Executable file
View File

@ -0,0 +1,8 @@
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from cheroot.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
awesome_venv/bin/cherryd Executable file
View File

@ -0,0 +1,8 @@
#!/home/carl/disp/yoloserv/awesome_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from cherrypy.__main__ import run
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(run())

View File

@ -0,0 +1,30 @@
Copyright © 2004-2019, CherryPy Team (team@cherrypy.dev)
All rights reserved.
* * *
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of CherryPy nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,201 @@
Metadata-Version: 2.1
Name: CherryPy
Version: 18.9.0
Summary: Object-Oriented HTTP framework
Home-page: https://www.cherrypy.dev
Author: CherryPy Team
Author-email: team@cherrypy.dev
Project-URL: CI: AppVeyor, https://ci.appveyor.com/project/cherrypy/cherrypy
Project-URL: CI: Travis, https://travis-ci.org/cherrypy/cherrypy
Project-URL: CI: Circle, https://circleci.com/gh/cherrypy/cherrypy
Project-URL: CI: GitHub, https://github.com/cherrypy/cherrypy/actions
Project-URL: Docs: RTD, https://docs.cherrypy.dev
Project-URL: GitHub: issues, https://github.com/cherrypy/cherrypy/issues
Project-URL: GitHub: repo, https://github.com/cherrypy/cherrypy
Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: Freely Distributable
Classifier: Operating System :: OS Independent
Classifier: Framework :: CherryPy
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: Jython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.6
License-File: LICENSE.md
Requires-Dist: cheroot >=8.2.1
Requires-Dist: portend >=2.1.1
Requires-Dist: more-itertools
Requires-Dist: zc.lockfile
Requires-Dist: jaraco.collections
Requires-Dist: pywin32 >=227 ; sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10"
Provides-Extra: docs
Requires-Dist: sphinx ; extra == 'docs'
Requires-Dist: docutils ; extra == 'docs'
Requires-Dist: alabaster ; extra == 'docs'
Requires-Dist: sphinxcontrib-apidoc >=0.3.0 ; extra == 'docs'
Requires-Dist: rst.linker >=1.11 ; extra == 'docs'
Requires-Dist: jaraco.packaging >=3.2 ; extra == 'docs'
Requires-Dist: setuptools ; extra == 'docs'
Provides-Extra: json
Requires-Dist: simplejson ; extra == 'json'
Provides-Extra: memcached_session
Requires-Dist: python-memcached >=1.58 ; extra == 'memcached_session'
Provides-Extra: routes_dispatcher
Requires-Dist: routes >=2.3.1 ; extra == 'routes_dispatcher'
Provides-Extra: ssl
Requires-Dist: pyOpenSSL ; extra == 'ssl'
Provides-Extra: testing
Requires-Dist: coverage ; extra == 'testing'
Requires-Dist: codecov ; extra == 'testing'
Requires-Dist: objgraph ; extra == 'testing'
Requires-Dist: pytest >=5.3.5 ; extra == 'testing'
Requires-Dist: pytest-cov ; extra == 'testing'
Requires-Dist: pytest-forked ; extra == 'testing'
Requires-Dist: pytest-sugar ; extra == 'testing'
Requires-Dist: path.py ; extra == 'testing'
Requires-Dist: requests-toolbelt ; extra == 'testing'
Requires-Dist: pytest-services >=2 ; extra == 'testing'
Requires-Dist: setuptools ; extra == 'testing'
Provides-Extra: xcgi
Requires-Dist: flup ; extra == 'xcgi'
.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg
:target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md
:alt: SWUbanner
.. image:: https://img.shields.io/pypi/v/cherrypy.svg
:target: https://pypi.org/project/cherrypy
.. image:: https://tidelift.com/badges/package/pypi/CherryPy
:target: https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=readme
:alt: CherryPy is available as part of the Tidelift Subscription
.. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg
:target: https://python3statement.org/
.. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg
:target: https://python3statement.org/#sections40-timeline
.. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest
:target: https://docs.cherrypy.dev/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg
:target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy
.. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg
:target: https://groups.google.com/group/cherrypy-users
.. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg
:target: https://gitter.im/cherrypy/cherrypy
.. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI
:target: https://travis-ci.org/cherrypy/cherrypy
.. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg
:target: https://circleci.com/gh/cherrypy/cherrypy/tree/master
.. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor
:target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master
.. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600
:target: https://pypi.org/project/cheroot
.. image:: https://img.shields.io/pypi/pyversions/cherrypy.svg
:target: https://pypi.org/project/cherrypy
.. image:: https://badges.github.io/stability-badges/dist/stable.svg
:target: https://github.com/badges/stability-badges
:alt: stable
.. image:: https://api.codacy.com/project/badge/Grade/48b11060b5d249dc86e52dac2be2c715
:target: https://www.codacy.com/app/webknjaz/cherrypy-upstream?utm_source=github.com&utm_medium=referral&utm_content=cherrypy/cherrypy&utm_campaign=Badge_Grade
.. image:: https://codecov.io/gh/cherrypy/cherrypy/branch/master/graph/badge.svg
:target: https://codecov.io/gh/cherrypy/cherrypy
:alt: codecov
Welcome to the GitHub repository of `CherryPy <https://cherrypy.dev>`_!
CherryPy is a pythonic, object-oriented HTTP framework.
1. It allows building web applications in much the same way one would
build any other object-oriented program.
2. This design results in more concise and readable code developed faster.
It's all just properties and methods.
3. It is now more than ten years old and has proven fast and very
stable.
4. It is being used in production by many sites, from the simplest to
the most demanding.
5. And perhaps most importantly, it is fun to work with :-)
Here's how easy it is to write "Hello World" in CherryPy:
.. code:: python
import cherrypy
class HelloWorld(object):
@cherrypy.expose
def index(self):
return "Hello World!"
cherrypy.quickstart(HelloWorld())
And it continues to work that intuitively when systems grow, allowing
for the Python object model to be dynamically presented as a website
and/or API.
While CherryPy is one of the easiest and most intuitive frameworks out
there, the prerequisite for understanding the `CherryPy
documentation <https://docs.cherrypy.dev>`_ is that you have
a general understanding of Python and web development.
Additionally:
- Tutorials are included in the repository:
https://github.com/cherrypy/cherrypy/tree/master/cherrypy/tutorial
- A general wiki at:
https://github.com/cherrypy/cherrypy/wiki
If the docs are insufficient to address your needs, the CherryPy
community has several `avenues for support
<https://docs.cherrypy.dev/en/latest/support.html>`_.
For Enterprise
--------------
CherryPy is available as part of the Tidelift Subscription.
The CherryPy maintainers and the maintainers of thousands of other packages
are working with Tidelift to deliver one enterprise subscription that covers
all of the open source you use.
`Learn more <https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=github>`_.
Contributing
------------
Please follow the `contribution guidelines
<https://docs.cherrypy.dev/en/latest/contribute.html>`_.
And by all means, absorb the `Zen of
CherryPy <https://github.com/cherrypy/cherrypy/wiki/The-Zen-of-CherryPy>`_.

View File

@ -0,0 +1,235 @@
../../../bin/cherryd,sha256=L7hGGldd0BmJ_lR4GGKlOimkNa-HMk7b9cUOmX9jk1Q,247
CherryPy-18.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
CherryPy-18.9.0.dist-info/LICENSE.md,sha256=Ra3pM8KA7ON-PgHTqRr-7ZUFdGpFb3KtELCJfEnmUQQ,1511
CherryPy-18.9.0.dist-info/METADATA,sha256=n2c4v1oJ39Pk7RfnGD3efAoXB3QDX-f7z3kWYR1VtAQ,8750
CherryPy-18.9.0.dist-info/RECORD,,
CherryPy-18.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
CherryPy-18.9.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
CherryPy-18.9.0.dist-info/entry_points.txt,sha256=2iMzTbV4_iDQUVEBHZGAuXaFAbW9naJoL4O9Gn7eDks,50
CherryPy-18.9.0.dist-info/top_level.txt,sha256=mOBE-r7Ej1kFrXNKlOj3yY9QfGA6Xkz6vZK7VNJF3YE,9
cherrypy/__init__.py,sha256=BHFq6fz2ILLDGq7OCALzmX8MaxPGI6AiR-fYLJo1kIw,11285
cherrypy/__main__.py,sha256=gb89ZhhQ1kjYvU61axGuZt5twA4q3kfBa3WKrPT6D2k,107
cherrypy/__pycache__/__init__.cpython-310.pyc,,
cherrypy/__pycache__/__main__.cpython-310.pyc,,
cherrypy/__pycache__/_cpchecker.cpython-310.pyc,,
cherrypy/__pycache__/_cpcompat.cpython-310.pyc,,
cherrypy/__pycache__/_cpconfig.cpython-310.pyc,,
cherrypy/__pycache__/_cpdispatch.cpython-310.pyc,,
cherrypy/__pycache__/_cperror.cpython-310.pyc,,
cherrypy/__pycache__/_cplogging.cpython-310.pyc,,
cherrypy/__pycache__/_cpmodpy.cpython-310.pyc,,
cherrypy/__pycache__/_cpnative_server.cpython-310.pyc,,
cherrypy/__pycache__/_cpreqbody.cpython-310.pyc,,
cherrypy/__pycache__/_cprequest.cpython-310.pyc,,
cherrypy/__pycache__/_cpserver.cpython-310.pyc,,
cherrypy/__pycache__/_cptools.cpython-310.pyc,,
cherrypy/__pycache__/_cptree.cpython-310.pyc,,
cherrypy/__pycache__/_cpwsgi.cpython-310.pyc,,
cherrypy/__pycache__/_cpwsgi_server.cpython-310.pyc,,
cherrypy/__pycache__/_helper.cpython-310.pyc,,
cherrypy/__pycache__/_json.cpython-310.pyc,,
cherrypy/__pycache__/daemon.cpython-310.pyc,,
cherrypy/_cpchecker.py,sha256=aAZfQTmW7_f0MfKZmmaHKs_7IjeOi8j-voeDmSbLEA0,14566
cherrypy/_cpcompat.py,sha256=2gZtFj0h6RRGlkY-R6WoEl-S8BBSIwCeuj7F2Tf3mU0,1992
cherrypy/_cpconfig.py,sha256=abftUSXmOWoqpak3VHOR50XBBo6wxy8qngz5VmsRXDQ,9647
cherrypy/_cpdispatch.py,sha256=BRpBxNHxyPmfuRCdjwUNiwgkwyPnpQQ1p4_htSDicoU,25216
cherrypy/_cperror.py,sha256=Maxb8U0CJfEHkwI678a-H8PXnZvWcObnr7_GW1nVvNc,23103
cherrypy/_cplogging.py,sha256=Y5eBiAKW1jdL2ikzGfawWKYVkKHvfMU92I-vsnnbSZQ,16438
cherrypy/_cpmodpy.py,sha256=lUqckVgqRr972jrFyA0y2TD79gA8NaRj5nboDuuARDY,11096
cherrypy/_cpnative_server.py,sha256=BtmMuMtdBmjb_kavTt6NdoREmqkHRhi1bUAG9EdhQuU,6677
cherrypy/_cpreqbody.py,sha256=85XtrtUq6wd1XeyRK2i2wiSY3QvJfaPnoLGMhhaks68,36382
cherrypy/_cprequest.py,sha256=fIGuIbJ7Eb_-zriV4300VG7x7f0f7xXzTzxVez6fZVA,34408
cherrypy/_cpserver.py,sha256=sai48_tDCkUrhqLKaSZSyk3aSIRxgUiMv6FNUe03PBY,8320
cherrypy/_cptools.py,sha256=Y35JizxKfVy3JNCbVido0rSXlyIzv4MrYDxdRuYG0bQ,18163
cherrypy/_cptree.py,sha256=_DLxtDfnPksYk5wlkLzsyCIk5TqrwE4wgkF1yFPdbko,10977
cherrypy/_cpwsgi.py,sha256=W3u6BKh1P_TZw9d5DzuwZhINXeQcMGvUVshN0iaFEOk,16394
cherrypy/_cpwsgi_server.py,sha256=ZILHc_ouGC_eARqrVTA1z5pdm9T5QLGURN4Idr0eiJQ,4187
cherrypy/_helper.py,sha256=bTZvfBpsZ0FDAGqtuuSblaiDloogxCpOyBy-XJF39vE,11653
cherrypy/_json.py,sha256=zxl6rLuW4xE5aiXJ0UPeI22NhdeLG-gDN_FFOMGikg0,440
cherrypy/daemon.py,sha256=kgiqlnWFx-PkLEcPPRJmXmB6zbI7X0cN2cCRtosow4c,3950
cherrypy/favicon.ico,sha256=jrNK5SnKfbnSFgX_xQyLX3khmeImw8IbbHgJVIsGci0,1406
cherrypy/lib/__init__.py,sha256=5_heysJFsUQMDLVNEFuCUbH03wWbA3lqsErmXLvPfFU,2745
cherrypy/lib/__pycache__/__init__.cpython-310.pyc,,
cherrypy/lib/__pycache__/auth_basic.cpython-310.pyc,,
cherrypy/lib/__pycache__/auth_digest.cpython-310.pyc,,
cherrypy/lib/__pycache__/caching.cpython-310.pyc,,
cherrypy/lib/__pycache__/covercp.cpython-310.pyc,,
cherrypy/lib/__pycache__/cpstats.cpython-310.pyc,,
cherrypy/lib/__pycache__/cptools.cpython-310.pyc,,
cherrypy/lib/__pycache__/encoding.cpython-310.pyc,,
cherrypy/lib/__pycache__/gctools.cpython-310.pyc,,
cherrypy/lib/__pycache__/httputil.cpython-310.pyc,,
cherrypy/lib/__pycache__/jsontools.cpython-310.pyc,,
cherrypy/lib/__pycache__/locking.cpython-310.pyc,,
cherrypy/lib/__pycache__/profiler.cpython-310.pyc,,
cherrypy/lib/__pycache__/reprconf.cpython-310.pyc,,
cherrypy/lib/__pycache__/sessions.cpython-310.pyc,,
cherrypy/lib/__pycache__/static.cpython-310.pyc,,
cherrypy/lib/__pycache__/xmlrpcutil.cpython-310.pyc,,
cherrypy/lib/auth_basic.py,sha256=FkySKMk0WC7OeLIefylkNyGORLpr9Rf98jQuxdBGHsA,4421
cherrypy/lib/auth_digest.py,sha256=WO9aXkVvqj0nAsD1jRYN4zy9OvhUo7FdiXkgvfx3iYE,15351
cherrypy/lib/caching.py,sha256=rJ_fPGlZ3Dpbg-BEDZQHL-m4_LJHJg69tBrAqgfzOW8,17517
cherrypy/lib/covercp.py,sha256=l3zwygAxDBZ8x617-HPk4lUXN20wRqXdblTm_v2dPOc,11599
cherrypy/lib/cpstats.py,sha256=luAeI7Dttzb3HhWUNsZUrNCrFp9LDXiBIOeVEzVmxiU,22854
cherrypy/lib/cptools.py,sha256=wSn80P3to8QhHzgL2Q2OC6rmd0qOAFLb4ZQn-wpkLKk,23530
cherrypy/lib/encoding.py,sha256=pnUVBiRioH9LpLU-K81u47yCviPebPNyX3P_Pzvv6Lw,17047
cherrypy/lib/gctools.py,sha256=5SI3w507J3JJConDjp8Jmgo6FI9uR59S_mm44cGB_Lw,7346
cherrypy/lib/httputil.py,sha256=304ixadmh6Tm5CGRqk7JaDKruKzeDJ7vJCdwRXPRBzQ,18007
cherrypy/lib/jsontools.py,sha256=YQQmGQN4XKnSgB4u0lRl2MB9YkVeNR3-6YWyU76jUFQ,3641
cherrypy/lib/locking.py,sha256=RLUwVj07-xNnBavmcRkjs-fmg712S7wSk3Gnox9TyEg,1224
cherrypy/lib/profiler.py,sha256=_IegAnVjU5H2CVx8AFQzGOlYwKWVg42VE2rsMj5-zQA,6556
cherrypy/lib/reprconf.py,sha256=zcAtqk5LSSnKgQHlT_IW5krbshsCBKhcFFaqJfSld1Q,12329
cherrypy/lib/sessions.py,sha256=CEruNzLRCPiDbHWVL_FUTJHr0xVWvWdy05-ATTJNk60,30896
cherrypy/lib/static.py,sha256=mdBINEDzuNQUdG07Hd6wh8DckugKNAgDBoq88nFFMVM,16612
cherrypy/lib/xmlrpcutil.py,sha256=UZqJsoBboSHjgsSiOfFcDwlubtSruhYyxpKdxutazQk,1684
cherrypy/process/__init__.py,sha256=RjaRqUG5U-ZhxAs7GBWN9PFR5hIK-9a9x3ZFwFjyW4Y,547
cherrypy/process/__pycache__/__init__.cpython-310.pyc,,
cherrypy/process/__pycache__/plugins.cpython-310.pyc,,
cherrypy/process/__pycache__/servers.cpython-310.pyc,,
cherrypy/process/__pycache__/win32.cpython-310.pyc,,
cherrypy/process/__pycache__/wspbus.cpython-310.pyc,,
cherrypy/process/plugins.py,sha256=hZ0r1cpK5qQZCFmTG28GC6GTS1Gcd01bU06DYQc2ucw,26846
cherrypy/process/servers.py,sha256=KojCzP2AxBV6D3URQX_blU8dfpkIL78OpfdwcQwRtt8,13442
cherrypy/process/win32.py,sha256=o_aT4XrUL6WcyVPti4O59apFd5ARoJz75VbiktdqqJk,5787
cherrypy/process/wspbus.py,sha256=_AFV7s2hmZngJF79reVWMD4rKK5i5wouLETfWiezV7U,21459
cherrypy/scaffold/__init__.py,sha256=sLK5vjy-_f6ikJ7-Lhj7HT8VGAIi6olqPuZMTD-RpAQ,1997
cherrypy/scaffold/__pycache__/__init__.cpython-310.pyc,,
cherrypy/scaffold/apache-fcgi.conf,sha256=0M10HHX8i2Or3r-gHoDglSQK4dZHd4Jhx4WhamJtuwc,930
cherrypy/scaffold/example.conf,sha256=EAqr2Sb1B1osc198dY1FV2A0wgnBmGsJf99_-GGexVU,62
cherrypy/scaffold/site.conf,sha256=pjUhF-ir1xzSsV7LqXGfyR6Ns_r_n3ATWw8OlfbgT3w,426
cherrypy/scaffold/static/made_with_cherrypy_small.png,sha256=VlSRvYj-pZzls-peicQhWpbqkdsZHtNhPtSfZV12BFQ,6347
cherrypy/test/__init__.py,sha256=jWQJbuVAcOuCITN7V8yrRz7U4rcK_ACx35Ephl4xwqY,396
cherrypy/test/__pycache__/__init__.cpython-310.pyc,,
cherrypy/test/__pycache__/_test_decorators.cpython-310.pyc,,
cherrypy/test/__pycache__/_test_states_demo.cpython-310.pyc,,
cherrypy/test/__pycache__/benchmark.cpython-310.pyc,,
cherrypy/test/__pycache__/checkerdemo.cpython-310.pyc,,
cherrypy/test/__pycache__/helper.cpython-310.pyc,,
cherrypy/test/__pycache__/logtest.cpython-310.pyc,,
cherrypy/test/__pycache__/modfastcgi.cpython-310.pyc,,
cherrypy/test/__pycache__/modfcgid.cpython-310.pyc,,
cherrypy/test/__pycache__/modpy.cpython-310.pyc,,
cherrypy/test/__pycache__/modwsgi.cpython-310.pyc,,
cherrypy/test/__pycache__/sessiondemo.cpython-310.pyc,,
cherrypy/test/__pycache__/test_auth_basic.cpython-310.pyc,,
cherrypy/test/__pycache__/test_auth_digest.cpython-310.pyc,,
cherrypy/test/__pycache__/test_bus.cpython-310.pyc,,
cherrypy/test/__pycache__/test_caching.cpython-310.pyc,,
cherrypy/test/__pycache__/test_config.cpython-310.pyc,,
cherrypy/test/__pycache__/test_config_server.cpython-310.pyc,,
cherrypy/test/__pycache__/test_conn.cpython-310.pyc,,
cherrypy/test/__pycache__/test_core.cpython-310.pyc,,
cherrypy/test/__pycache__/test_dynamicobjectmapping.cpython-310.pyc,,
cherrypy/test/__pycache__/test_encoding.cpython-310.pyc,,
cherrypy/test/__pycache__/test_etags.cpython-310.pyc,,
cherrypy/test/__pycache__/test_http.cpython-310.pyc,,
cherrypy/test/__pycache__/test_httputil.cpython-310.pyc,,
cherrypy/test/__pycache__/test_iterator.cpython-310.pyc,,
cherrypy/test/__pycache__/test_json.cpython-310.pyc,,
cherrypy/test/__pycache__/test_logging.cpython-310.pyc,,
cherrypy/test/__pycache__/test_mime.cpython-310.pyc,,
cherrypy/test/__pycache__/test_misc_tools.cpython-310.pyc,,
cherrypy/test/__pycache__/test_native.cpython-310.pyc,,
cherrypy/test/__pycache__/test_objectmapping.cpython-310.pyc,,
cherrypy/test/__pycache__/test_params.cpython-310.pyc,,
cherrypy/test/__pycache__/test_plugins.cpython-310.pyc,,
cherrypy/test/__pycache__/test_proxy.cpython-310.pyc,,
cherrypy/test/__pycache__/test_refleaks.cpython-310.pyc,,
cherrypy/test/__pycache__/test_request_obj.cpython-310.pyc,,
cherrypy/test/__pycache__/test_routes.cpython-310.pyc,,
cherrypy/test/__pycache__/test_session.cpython-310.pyc,,
cherrypy/test/__pycache__/test_sessionauthenticate.cpython-310.pyc,,
cherrypy/test/__pycache__/test_states.cpython-310.pyc,,
cherrypy/test/__pycache__/test_static.cpython-310.pyc,,
cherrypy/test/__pycache__/test_tools.cpython-310.pyc,,
cherrypy/test/__pycache__/test_tutorials.cpython-310.pyc,,
cherrypy/test/__pycache__/test_virtualhost.cpython-310.pyc,,
cherrypy/test/__pycache__/test_wsgi_ns.cpython-310.pyc,,
cherrypy/test/__pycache__/test_wsgi_unix_socket.cpython-310.pyc,,
cherrypy/test/__pycache__/test_wsgi_vhost.cpython-310.pyc,,
cherrypy/test/__pycache__/test_wsgiapps.cpython-310.pyc,,
cherrypy/test/__pycache__/test_xmlrpc.cpython-310.pyc,,
cherrypy/test/__pycache__/webtest.cpython-310.pyc,,
cherrypy/test/_test_decorators.py,sha256=U5y0mhWwTcCrYLjY3EWSkkz_Vw-z0bPSP5ujwb6b79Y,951
cherrypy/test/_test_states_demo.py,sha256=lVpbqHgHcIfdz1-i6xDbmzmD87V3JbwD_e4lK0Bagnw,1876
cherrypy/test/benchmark.py,sha256=GoTeD98I5tFaDDSQTxmy1OrmTM5XS8bybg41i2j3rB4,12563
cherrypy/test/checkerdemo.py,sha256=H8muNg9uHSAlDVYc-rICkcUKtJqzlTQfjYmMzC1Fuv4,1861
cherrypy/test/fastcgi.conf,sha256=0YsIPLmOg-NdGGqCCPpBERKGYy_zBU6LkRDyR41nvBE,686
cherrypy/test/fcgi.conf,sha256=neiD1sjiFblAJLUdlOSKiZ1uVl1eK3zM2_7LZQigkTs,486
cherrypy/test/helper.py,sha256=y7PwhV4kZoJsXTUMXDtqGmZxk-7RfqWec7SvXWCOYto,16398
cherrypy/test/logtest.py,sha256=zAMSL0AGGYwXUFLm56ztev6yb9GR94hL_wArtXC0Sic,8325
cherrypy/test/modfastcgi.py,sha256=SblRPAhbo9uJLHISamWE7AyW2slrlWms76VvpnCqzLo,4607
cherrypy/test/modfcgid.py,sha256=7a5iRZ9VQyr5O2Pajgo_73j6rbMr_i02FG6sO1-Q-lM,4192
cherrypy/test/modpy.py,sha256=F6Ar3uxr1ckp5V5l6FNJEZnNHSB0SDtR5hGmYU010oY,4933
cherrypy/test/modwsgi.py,sha256=DnKeIzaYSY2_fBgGb2ojjWS_A9A2UcuZUgneSalPO08,4789
cherrypy/test/sessiondemo.py,sha256=_vVruGha_8f6Fim_n9LgxwNxebbM6JgiqBvyX0WaEQc,5425
cherrypy/test/static/404.html,sha256=9jnU0KKbdzHVq9EvhkGOEly92qNJbtCSXOsH0KFTL4U,92
cherrypy/test/static/dirback.jpg,sha256=eS_X3BSeu8OSu-GTYndM1tJkWoW_oVJp1O_mmUUGeo8,16585
cherrypy/test/static/index.html,sha256=cB6ALrLhcxEGyMNgOHzmnBvmRnPCW_u3ebZUqdCiHkQ,14
cherrypy/test/style.css,sha256=2Ypw_ziOWlY4dTZJlwsrorDLqLA1z485lgagaGemtKQ,17
cherrypy/test/test.pem,sha256=x6LrLPw2dBRyZwHXk6FhdSDNM3-Cv7DBXc8o4A19RhI,2254
cherrypy/test/test_auth_basic.py,sha256=JlLdHeyaEutMzWK7LX3HfSOVQWAKZXoUNL67sSgUMZQ,4499
cherrypy/test/test_auth_digest.py,sha256=pp49xOWFS_i8N_s8MQ6h_exbOxlQZjfjT-cxDvi701E,4454
cherrypy/test/test_bus.py,sha256=K_6pYUz4q6xBOh5tMK67d3n_X1VF1LJhS1mRsGh0JnQ,9960
cherrypy/test/test_caching.py,sha256=s6aA_P6mcasaEfPwX50c9A-UqwSiYdzMVp5GFQH08uQ,14386
cherrypy/test/test_config.py,sha256=lUxRUCBmDVBo8LK7yi8w5qvFcS3vw4YpFwl66TdRskQ,8836
cherrypy/test/test_config_server.py,sha256=D7jLqZawCOh2z6vGU-WjkupiJA0BxPywb8LuILL2JGA,4037
cherrypy/test/test_conn.py,sha256=g2e2CCaB_2UiFWVLniVZbk2YNrXqN9_J0M1FymMZ_F8,30744
cherrypy/test/test_core.py,sha256=u9lxQENZoTczEzx677_MxyV3i2tmZxl0Wd9yQ-mtIUI,30395
cherrypy/test/test_dynamicobjectmapping.py,sha256=0VpMCJq1APhGjqwYoYvx0irFJ4yb3NWqdE5RtK11uDM,12331
cherrypy/test/test_encoding.py,sha256=lGUiNKrWQlHma4fV-mjR0vv-xylrw_v1E6j61CT6vBc,17535
cherrypy/test/test_etags.py,sha256=mzuKNjFXx67gHoqS_jaGEzjxJ025mzlLgepsW2DYETI,3093
cherrypy/test/test_http.py,sha256=Tg1WoG7R03tP8ljwmFUNMG21EVBh6mTBudWmQ0VgfeY,11167
cherrypy/test/test_httputil.py,sha256=gA3u7bt1vV2Av3T1I0CaEGEdYV9kGDEPD80al1IbPE8,2412
cherrypy/test/test_iterator.py,sha256=siygtCR27EoDoOwyr8Bvk-Gt31dJTLAsS-V3WRYE4PM,5754
cherrypy/test/test_json.py,sha256=rVfzyCwSMf79bcZ8aYBA_180FJxcHY9jFT5_0M6-pSc,2860
cherrypy/test/test_logging.py,sha256=OtjY-Q87VNPB6rRVDKn0tOScWkFpcSEkZZa4rRmyT4o,9155
cherrypy/test/test_mime.py,sha256=-6HpcAIGtN56nWRQclxvSKhNLW5e_bYRO6kDtM3DlN8,4538
cherrypy/test/test_misc_tools.py,sha256=Ixjq2IAJZ1BTuV-i_AUKw_OsjYru_i9RPm1gIWyWt_E,7094
cherrypy/test/test_native.py,sha256=rtow-ShYRkd2oEBtDksU6e06_L0BvZToFJFngeeGF34,971
cherrypy/test/test_objectmapping.py,sha256=KTMqAizhWBGCgDFp1n8msno4ylYPnmvWZ1cWHzzUgO0,14504
cherrypy/test/test_params.py,sha256=p4DfugiKWxF9nPX5Gs7arGhUmpx_eeZhWyS5yCXCUj4,1862
cherrypy/test/test_plugins.py,sha256=afO6r6XOLYaWPO-Crbei1W0xe_VDA1ippMkCtuo_ypc,334
cherrypy/test/test_proxy.py,sha256=XPdi3O_izRLtvu3UTJE-WTBVmn4DEng7irrfUD69srU,5630
cherrypy/test/test_refleaks.py,sha256=HK55E9JtRFc28FhnnTLV9DrM1k82ZmPEVdHYyp525K0,1555
cherrypy/test/test_request_obj.py,sha256=C3dOHeeqVGxXQjAhDS3Q_7dIzfpyR4WFo6eZ61W8vKI,37404
cherrypy/test/test_routes.py,sha256=m0MvSqurFk42PuSp5vF8ue40-vnhPNwC2EGTqkDozo4,2583
cherrypy/test/test_session.py,sha256=Fxkm1kuth5tRb6FKiToFLETDmFwnQeVQKmDYLdo6pU0,17949
cherrypy/test/test_sessionauthenticate.py,sha256=zhVUpN3FWPaZbMKQcTrDQiaI-RXjlwrJi7ssqbzhmU8,2013
cherrypy/test/test_states.py,sha256=2E9S0L_JvJUtVBoJLOCpLgAICoOvE4HpFp3EP0gCLy8,16744
cherrypy/test/test_static.py,sha256=4VQpLumcxWGNFwaLuCgOS11pyla1gpGgQNlKhMOopws,16702
cherrypy/test/test_tools.py,sha256=cQELdifYvLfwJPg_0JoGQwyFvsgbuFXIJLUd8ejrgZA,17851
cherrypy/test/test_tutorials.py,sha256=IszX5SdX3Vlnoq45b9vkdrAIWgc37um4DERFVpP6KcI,6964
cherrypy/test/test_virtualhost.py,sha256=ap_e1gM7PERVN4mU70zc5RD1pVoSdN-te-B_uIAlV8g,4021
cherrypy/test/test_wsgi_ns.py,sha256=PuoUe2EUwZk5z0yLIw9NdRkd_P4VKU7Cckj8n0QKSJo,2812
cherrypy/test/test_wsgi_unix_socket.py,sha256=8guEkMHcOBhOGXYBfAThqCzpIBmUTNpqsilOpaRgD2c,2228
cherrypy/test/test_wsgi_vhost.py,sha256=4uZ8_luFHiQJ6uxQeHFJtjemug8UiPcKmnwwclj0dkw,1034
cherrypy/test/test_wsgiapps.py,sha256=1SdQGuWVcVCTiSvizDdIOekuvQLCybRXUKF2dpV2NTs,3997
cherrypy/test/test_xmlrpc.py,sha256=DQfgzjIMcQP_gOi8el1QQ5dkfPaRbyZ6CDdPL9ZIgWo,4584
cherrypy/test/webtest.py,sha256=uRwMw_why3KeXGZXdHE7-GfJag4ziL9KZmDGx4Q7Jbg,262
cherrypy/tutorial/README.rst,sha256=v77BbhuiK44TxqeYPk3PwqV09Dg5AKWFdp7re04KdEo,617
cherrypy/tutorial/__init__.py,sha256=cmLXfvQI0L6trCXwDzR0WE1bu4JZYt301HJRNhjZOBc,85
cherrypy/tutorial/__pycache__/__init__.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut01_helloworld.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut02_expose_methods.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut03_get_and_post.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut04_complex_site.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut05_derived_objects.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut06_default_method.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut07_sessions.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut08_generators_and_yield.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut09_files.cpython-310.pyc,,
cherrypy/tutorial/__pycache__/tut10_http_errors.cpython-310.pyc,,
cherrypy/tutorial/custom_error.html,sha256=9cMEb83zwct9i-fJlyl7yvBSNexF7yEIWOoxH8lpllQ,404
cherrypy/tutorial/pdf_file.pdf,sha256=-WuAfJ9i1vbUT9EcKKvUaNGfbPQsNeFx-qsB4gsxWUg,11961
cherrypy/tutorial/tut01_helloworld.py,sha256=Au6IDz0Kd1XMPzb-vJL-1y_BWz0GgrljOtqOpzDZeOk,1015
cherrypy/tutorial/tut02_expose_methods.py,sha256=ikz6QOGLknEZm0k-f9BR18deItQ3UO8yYg1NW13kB8g,801
cherrypy/tutorial/tut03_get_and_post.py,sha256=bY_cTha4zIkokv585yioQkM-S2a7GfetTE3ovl3-7cw,1587
cherrypy/tutorial/tut04_complex_site.py,sha256=PCxyUVKG-L_YUgt1D5Q5t0_IUiGXbcGyf_L00K8cH8c,2948
cherrypy/tutorial/tut05_derived_objects.py,sha256=u0LBnUTW8DnexEAtowhJ0sjeAqp6rS065TM2QXfDY1I,2141
cherrypy/tutorial/tut06_default_method.py,sha256=3Wx34fL_4P3M_dhu6RQdpXQeSriIJpSsy72IqpNQ6ns,2264
cherrypy/tutorial/tut07_sessions.py,sha256=fQo-v_ol5CjXiq4vdsm7Dh1us6DVDzbqAIouRkmLeR8,1228
cherrypy/tutorial/tut08_generators_and_yield.py,sha256=m5GfOtNDoGxMd1rw5tCRg3o9cTyt-e1gR-ALoLRLiQw,1288
cherrypy/tutorial/tut09_files.py,sha256=qcelN09_k62zWVC-daId5I4i-fw6TWA4WddUbG4j8B8,3463
cherrypy/tutorial/tut10_http_errors.py,sha256=6GllO8SI-6Xs6R8hRwHe7jUzGxYJxLlxwxgaOJC9i8Y,2706
cherrypy/tutorial/tutorial.conf,sha256=9ENgfRDyopHuignr_aHeMaWoC562xThbmlgF6zg4oEE,96

View File

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.42.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1,2 @@
[console_scripts]
cherryd = cherrypy.__main__:run

View File

@ -0,0 +1,242 @@
Metadata-Version: 2.1
Name: annotated-types
Version: 0.6.0
Summary: Reusable constraint types to use with typing.Annotated
Author-email: Samuel Colvin <s@muelcolvin.com>, Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Zac Hatfield-Dodds <zac@zhd.dev>
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: MacOS X
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Unix
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
Description-Content-Type: text/markdown
# annotated-types
[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types)
[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types)
[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
adding context-specific metadata to existing types, and specifies that
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
logic for `x`.
This package provides metadata objects which can be used to represent common
constraints such as upper and lower bounds on scalar values and collection sizes,
a `Predicate` marker for runtime checks, and
descriptions of how we intend these metadata to be interpreted. In some cases,
we also note alternative representations which do not require this package.
## Install
```bash
pip install annotated-types
```
## Examples
```python
from typing import Annotated
from annotated_types import Gt, Len, Predicate
class MyClass:
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
# Invalid: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
# Invalid: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
# Invalid: (1, 2), ["abc"], [0] * 20
```
## Documentation
_While `annotated-types` avoids runtime checks for performance, users should not
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
Downstream implementors may choose to raise an error, emit a warning, silently ignore
a metadata item, etc., if the metadata objects described below are used with an
incompatible type - or for any other reason!_
### Gt, Ge, Lt, Le
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
dates, times, strings, sets, etc. Note that the boundary value need not be of the
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
the `annotated-types` package.
To be explicit, these types have the following meanings:
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
### Interval
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
metadata object. `None` attributes should be ignored, and non-`None` attributes
treated as per the single bounds above.
### MultipleOf
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
1. Python semantics, implying `value % multiple_of == 0`, or
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
where `int(value / multiple_of) == value / multiple_of`.
We encourage users to be aware of these two common interpretations and their
distinct behaviours, especially since very large or non-integer numbers make
it easy to cause silent data corruption due to floating-point imprecision.
We encourage libraries to carefully document which interpretation they implement.
### MinLen, MaxLen, Len
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
As well as `Len()` which can optionally include upper and lower bounds, we also
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
and `Len(max_length=y)` respectively.
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
Examples of usage:
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
#### Changed in v0.4.0
* `min_inclusive` has been renamed to `min_length`, no change in meaning
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
meaning of the upper bound in slices vs. `Len`
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
### Timezone
`Timezone` can be used with a `datetime` or a `time` to express which timezones
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
expresses that any timezone-aware datetime is allowed. You may also pass a specific
timezone string or `timezone` object such as `Timezone(timezone.utc)` or
`Timezone("Africa/Abidjan")` to express that you only allow a specific timezone,
though we note that this is often a symptom of fragile design.
### Predicate
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
Users should prefer the statically inspectable metadata above, but if you need
the full power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints:
* `IsLower = Predicate(str.islower)`
* `IsUpper = Predicate(str.isupper)`
* `IsDigit = Predicate(str.isdigit)`
* `IsFinite = Predicate(math.isfinite)`
* `IsNotFinite = Predicate(Not(math.isfinite))`
* `IsNan = Predicate(math.isnan)`
* `IsNotNan = Predicate(Not(math.isnan))`
* `IsInfinite = Predicate(math.isinf)`
* `IsNotInfinite = Predicate(Not(math.isinf))`
Some libraries might have special logic to handle known or understandable predicates,
for example by checking for `str.isdigit` and using its presence to both call custom
logic to enforce digit-only strings, and customise some generated external schema.
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propogate or discard the resulting
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
exception. We encourage libraries to document the behaviour they choose.
### Doc
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
### Integrating downstream types with `GroupedMetadata`
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
This can help reduce verbosity and cognitive overhead for users.
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
```python
from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge
@dataclass
class Field(GroupedMetadata):
ge: int | None = None
description: str | None = None
def __iter__(self) -> Iterator[object]:
# Iterating over a GroupedMetadata object should yield annotated-types
# constraint metadata objects which describe it as fully as possible,
# and may include other unknown objects too.
if self.ge is not None:
yield Ge(self.ge)
if self.description is not None:
yield Description(self.description)
```
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
### Consuming metadata
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
It is up to the implementer to determine how this metadata is used.
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
## Design & History
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
and Hypothesis, with the goal of making it as easy as possible for end-users to
provide more informative annotations for use by runtime libraries.
It is deliberately minimal, and following PEP-593 allows considerable downstream
discretion in what (if anything!) they choose to support. Nonetheless, we expect
that staying simple and covering _only_ the most common use-cases will give users
and maintainers the best experience we can. If you'd like more constraints for your
types - follow our lead, by defining them and documenting them downstream!

View File

@ -0,0 +1,10 @@
annotated_types-0.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
annotated_types-0.6.0.dist-info/METADATA,sha256=78YeruT3b_doh8TbsK7qcbqY4cvCMz_QQRCGUbyAo6M,12879
annotated_types-0.6.0.dist-info/RECORD,,
annotated_types-0.6.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
annotated_types-0.6.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
annotated_types/__init__.py,sha256=txLoRt8iSiAc-Su37esWpMI0wFjN92V7gE5HdFIeeKI,12151
annotated_types/__pycache__/__init__.cpython-310.pyc,,
annotated_types/__pycache__/test_cases.cpython-310.pyc,,
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
annotated_types/test_cases.py,sha256=LfFyURZwr7X3NVfoCrSXSMMxTxJD4o8Xz-Y8qbmY7JU,6327

View File

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.18.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 the contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,396 @@
import math
import sys
from dataclasses import dataclass
from datetime import timezone
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
if sys.version_info < (3, 8):
from typing_extensions import Protocol, runtime_checkable
else:
from typing import Protocol, runtime_checkable
if sys.version_info < (3, 9):
from typing_extensions import Annotated, Literal
else:
from typing import Annotated, Literal
if sys.version_info < (3, 10):
EllipsisType = type(Ellipsis)
KW_ONLY = {}
SLOTS = {}
else:
from types import EllipsisType
KW_ONLY = {"kw_only": True}
SLOTS = {"slots": True}
__all__ = (
'BaseMetadata',
'GroupedMetadata',
'Gt',
'Ge',
'Lt',
'Le',
'Interval',
'MultipleOf',
'MinLen',
'MaxLen',
'Len',
'Timezone',
'Predicate',
'LowerCase',
'UpperCase',
'IsDigits',
'IsFinite',
'IsNotFinite',
'IsNan',
'IsNotNan',
'IsInfinite',
'IsNotInfinite',
'doc',
'DocInfo',
'__version__',
)
__version__ = '0.6.0'
T = TypeVar('T')
# arguments that start with __ are considered
# positional only
# see https://peps.python.org/pep-0484/#positional-only-arguments
class SupportsGt(Protocol):
def __gt__(self: T, __other: T) -> bool:
...
class SupportsGe(Protocol):
def __ge__(self: T, __other: T) -> bool:
...
class SupportsLt(Protocol):
def __lt__(self: T, __other: T) -> bool:
...
class SupportsLe(Protocol):
def __le__(self: T, __other: T) -> bool:
...
class SupportsMod(Protocol):
def __mod__(self: T, __other: T) -> T:
...
class SupportsDiv(Protocol):
def __div__(self: T, __other: T) -> T:
...
class BaseMetadata:
"""Base class for all metadata.
This exists mainly so that implementers
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
"""
__slots__ = ()
@dataclass(frozen=True, **SLOTS)
class Gt(BaseMetadata):
"""Gt(gt=x) implies that the value must be greater than x.
It can be used with any type that supports the ``>`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
gt: SupportsGt
@dataclass(frozen=True, **SLOTS)
class Ge(BaseMetadata):
"""Ge(ge=x) implies that the value must be greater than or equal to x.
It can be used with any type that supports the ``>=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
ge: SupportsGe
@dataclass(frozen=True, **SLOTS)
class Lt(BaseMetadata):
"""Lt(lt=x) implies that the value must be less than x.
It can be used with any type that supports the ``<`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
lt: SupportsLt
@dataclass(frozen=True, **SLOTS)
class Le(BaseMetadata):
"""Le(le=x) implies that the value must be less than or equal to x.
It can be used with any type that supports the ``<=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
le: SupportsLe
@runtime_checkable
class GroupedMetadata(Protocol):
"""A grouping of multiple BaseMetadata objects.
`GroupedMetadata` on its own is not metadata and has no meaning.
All it the the constraint and metadata should be fully expressable
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
Concrete implementations should override `GroupedMetadata.__iter__()`
to add their own metadata.
For example:
>>> @dataclass
>>> class Field(GroupedMetadata):
>>> gt: float | None = None
>>> description: str | None = None
...
>>> def __iter__(self) -> Iterable[BaseMetadata]:
>>> if self.gt is not None:
>>> yield Gt(self.gt)
>>> if self.description is not None:
>>> yield Description(self.gt)
Also see the implementation of `Interval` below for an example.
Parsers should recognize this and unpack it so that it can be used
both with and without unpacking:
- `Annotated[int, Field(...)]` (parser must unpack Field)
- `Annotated[int, *Field(...)]` (PEP-646)
""" # noqa: trailing-whitespace
@property
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
return True
def __iter__(self) -> Iterator[BaseMetadata]:
...
if not TYPE_CHECKING:
__slots__ = () # allow subclasses to use slots
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
# Basic ABC like functionality without the complexity of an ABC
super().__init_subclass__(*args, **kwargs)
if cls.__iter__ is GroupedMetadata.__iter__:
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
def __iter__(self) -> Iterator[BaseMetadata]: # noqa: F811
raise NotImplementedError # more helpful than "None has no attribute..." type errors
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
class Interval(GroupedMetadata):
"""Interval can express inclusive or exclusive bounds with a single object.
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
are interpreted the same way as the single-bound constraints.
"""
gt: Union[SupportsGt, None] = None
ge: Union[SupportsGe, None] = None
lt: Union[SupportsLt, None] = None
le: Union[SupportsLe, None] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack an Interval into zero or more single-bounds."""
if self.gt is not None:
yield Gt(self.gt)
if self.ge is not None:
yield Ge(self.ge)
if self.lt is not None:
yield Lt(self.lt)
if self.le is not None:
yield Le(self.le)
@dataclass(frozen=True, **SLOTS)
class MultipleOf(BaseMetadata):
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
1. Python semantics, implying ``value % multiple_of == 0``, or
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
We encourage users to be aware of these two common interpretations,
and libraries to carefully document which they implement.
"""
multiple_of: Union[SupportsDiv, SupportsMod]
@dataclass(frozen=True, **SLOTS)
class MinLen(BaseMetadata):
"""
MinLen() implies minimum inclusive length,
e.g. ``len(value) >= min_length``.
"""
min_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class MaxLen(BaseMetadata):
"""
MaxLen() implies maximum inclusive length,
e.g. ``len(value) <= max_length``.
"""
max_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class Len(GroupedMetadata):
"""
Len() implies that ``min_length <= len(value) <= max_length``.
Upper bound may be omitted or ``None`` to indicate no upper length bound.
"""
min_length: Annotated[int, Ge(0)] = 0
max_length: Optional[Annotated[int, Ge(0)]] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack a Len into zone or more single-bounds."""
if self.min_length > 0:
yield MinLen(self.min_length)
if self.max_length is not None:
yield MaxLen(self.max_length)
@dataclass(frozen=True, **SLOTS)
class Timezone(BaseMetadata):
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
tz-aware but any timezone is allowed.
You may also pass a specific timezone string or timezone object such as
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
you only allow a specific timezone, though we note that this is often
a symptom of poor design.
"""
tz: Union[str, timezone, EllipsisType, None]
@dataclass(frozen=True, **SLOTS)
class Predicate(BaseMetadata):
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
Users should prefer statically inspectable metadata, but if you need the full
power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints:
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
``IsDigit = Predicate(str.isdigit)``. Users are encouraged to use methods which
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
Some libraries might have special logic to handle certain predicates, e.g. by
checking for `str.isdigit` and using its presence to both call custom logic to
enforce digit-only strings, and customise some generated external schema.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propogate or discard the resulting exception.
"""
func: Callable[[Any], bool]
@dataclass
class Not:
func: Callable[[Any], bool]
def __call__(self, __v: Any) -> bool:
return not self.func(__v)
_StrType = TypeVar("_StrType", bound=str)
LowerCase = Annotated[_StrType, Predicate(str.islower)]
"""
Return True if the string is a lowercase string, False otherwise.
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
""" # noqa: E501
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
"""
Return True if the string is an uppercase string, False otherwise.
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
""" # noqa: E501
IsDigits = Annotated[_StrType, Predicate(str.isdigit)]
"""
Return True if the string is a digit string, False otherwise.
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
""" # noqa: E501
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
"""
Return True if all characters in the string are ASCII, False otherwise.
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
"""
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
"""Return True if x is one of infinity or NaN, and False otherwise"""
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
"""Return True if x is a NaN (not a number), and False otherwise."""
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
"""Return True if x is anything but NaN (not a number), and False otherwise."""
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
"""Return True if x is a positive or negative infinity, and False otherwise."""
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
try:
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
except ImportError:
@dataclass(frozen=True, **SLOTS)
class DocInfo: # type: ignore [no-redef]
""" "
The return value of doc(), mainly to be used by tools that want to extract the
Annotated documentation at runtime.
"""
documentation: str
"""The documentation string passed to doc()."""
def doc(
documentation: str,
) -> DocInfo:
"""
Add documentation to a type annotation inside of Annotated.
For example:
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
"""
return DocInfo(documentation)

View File

@ -0,0 +1,147 @@
import math
import sys
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
import annotated_types as at
class Case(NamedTuple):
"""
A test case for `annotated_types`.
"""
annotation: Any
valid_cases: Iterable[Any]
invalid_cases: Iterable[Any]
def cases() -> Iterable[Case]:
# Gt, Ge, Lt, Le
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(date(2000, 1, 1))],
[date(2000, 1, 2), date(2000, 1, 3)],
[date(2000, 1, 1), date(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(Decimal('1.123'))],
[Decimal('1.1231'), Decimal('123')],
[Decimal('1.123'), Decimal('0')],
)
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
)
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
# Interval
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
yield Case(
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
)
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
# lengths
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
# Timezone
yield Case(
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
)
yield Case(
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
)
yield Case(
Annotated[datetime, at.Timezone(timezone.utc)],
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
yield Case(
Annotated[datetime, at.Timezone('Europe/London')],
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
# predicate types
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
yield Case(at.IsDigits[str], ['123'], ['', 'ab', 'a1b2'])
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
# check stacked predicates
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
# doc
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
# custom GroupedMetadata
class MyCustomGroupedMetadata(at.GroupedMetadata):
def __iter__(self) -> Iterator[at.Predicate]:
yield at.Predicate(lambda x: float(x).is_integer())
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])

View File

@ -0,0 +1,166 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -0,0 +1,420 @@
Metadata-Version: 2.1
Name: autocommand
Version: 2.2.2
Summary: A library to create a command-line program from a function
Home-page: https://github.com/Lucretiel/autocommand
Author: Nathan West
License: LGPLv3
Project-URL: Homepage, https://github.com/Lucretiel/autocommand
Project-URL: Bug Tracker, https://github.com/Lucretiel/autocommand/issues
Platform: any
Classifier: Development Status :: 6 - Mature
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
[![PyPI version](https://badge.fury.io/py/autocommand.svg)](https://badge.fury.io/py/autocommand)
# autocommand
A library to automatically generate and run simple argparse parsers from function signatures.
## Installation
Autocommand is installed via pip:
```
$ pip install autocommand
```
## Usage
Autocommand turns a function into a command-line program. It converts the function's parameter signature into command-line arguments, and automatically runs the function if the module was called as `__main__`. In effect, it lets your create a smart main function.
```python
from autocommand import autocommand
# This program takes exactly one argument and echos it.
@autocommand(__name__)
def echo(thing):
print(thing)
```
```
$ python echo.py hello
hello
$ python echo.py -h
usage: echo [-h] thing
positional arguments:
thing
optional arguments:
-h, --help show this help message and exit
$ python echo.py hello world # too many arguments
usage: echo.py [-h] thing
echo.py: error: unrecognized arguments: world
```
As you can see, autocommand converts the signature of the function into an argument spec. When you run the file as a program, autocommand collects the command-line arguments and turns them into function arguments. The function is executed with these arguments, and then the program exits with the return value of the function, via `sys.exit`. Autocommand also automatically creates a usage message, which can be invoked with `-h` or `--help`, and automatically prints an error message when provided with invalid arguments.
### Types
You can use a type annotation to give an argument a type. Any type (or in fact any callable) that returns an object when given a string argument can be used, though there are a few special cases that are described later.
```python
@autocommand(__name__)
def net_client(host, port: int):
...
```
Autocommand will catch `TypeErrors` raised by the type during argument parsing, so you can supply a callable and do some basic argument validation as well.
### Trailing Arguments
You can add a `*args` parameter to your function to give it trailing arguments. The command will collect 0 or more trailing arguments and supply them to `args` as a tuple. If a type annotation is supplied, the type is applied to each argument.
```python
# Write the contents of each file, one by one
@autocommand(__name__)
def cat(*files):
for filename in files:
with open(filename) as file:
for line in file:
print(line.rstrip())
```
```
$ python cat.py -h
usage: ipython [-h] [file [file ...]]
positional arguments:
file
optional arguments:
-h, --help show this help message and exit
```
### Options
To create `--option` switches, just assign a default. Autocommand will automatically create `--long` and `-s`hort switches.
```python
@autocommand(__name__)
def do_with_config(argument, config='~/foo.conf'):
pass
```
```
$ python example.py -h
usage: example.py [-h] [-c CONFIG] argument
positional arguments:
argument
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
```
The option's type is automatically deduced from the default, unless one is explicitly given in an annotation:
```python
@autocommand(__name__)
def http_connect(host, port=80):
print('{}:{}'.format(host, port))
```
```
$ python http.py -h
usage: http.py [-h] [-p PORT] host
positional arguments:
host
optional arguments:
-h, --help show this help message and exit
-p PORT, --port PORT
$ python http.py localhost
localhost:80
$ python http.py localhost -p 8080
localhost:8080
$ python http.py localhost -p blah
usage: http.py [-h] [-p PORT] host
http.py: error: argument -p/--port: invalid int value: 'blah'
```
#### None
If an option is given a default value of `None`, it reads in a value as normal, but supplies `None` if the option isn't provided.
#### Switches
If an argument is given a default value of `True` or `False`, or
given an explicit `bool` type, it becomes an option switch.
```python
@autocommand(__name__)
def example(verbose=False, quiet=False):
pass
```
```
$ python example.py -h
usage: example.py [-h] [-v] [-q]
optional arguments:
-h, --help show this help message and exit
-v, --verbose
-q, --quiet
```
Autocommand attempts to do the "correct thing" in these cases- if the default is `True`, then supplying the switch makes the argument `False`; if the type is `bool` and the default is some other `True` value, then supplying the switch makes the argument `False`, while not supplying the switch makes the argument the default value.
Autocommand also supports the creation of switch inverters. Pass `add_nos=True` to `autocommand` to enable this.
```
@autocommand(__name__, add_nos=True)
def example(verbose=False):
pass
```
```
$ python example.py -h
usage: ipython [-h] [-v] [--no-verbose]
optional arguments:
-h, --help show this help message and exit
-v, --verbose
--no-verbose
```
Using the `--no-` version of a switch will pass the opposite value in as a function argument. If multiple switches are present, the last one takes precedence.
#### Files
If the default value is a file object, such as `sys.stdout`, then autocommand just looks for a string, for a file path. It doesn't do any special checking on the string, though (such as checking if the file exists); it's better to let the client decide how to handle errors in this case. Instead, it provides a special context manager called `smart_open`, which behaves exactly like `open` if a filename or other openable type is provided, but also lets you use already open files:
```python
from autocommand import autocommand, smart_open
import sys
# Write the contents of stdin, or a file, to stdout
@autocommand(__name__)
def write_out(infile=sys.stdin):
with smart_open(infile) as f:
for line in f:
print(line.rstrip())
# If a file was opened, it is closed here. If it was just stdin, it is untouched.
```
```
$ echo "Hello World!" | python write_out.py | tee hello.txt
Hello World!
$ python write_out.py --infile hello.txt
Hello World!
```
### Descriptions and docstrings
The `autocommand` decorator accepts `description` and `epilog` kwargs, corresponding to the `description <https://docs.python.org/3/library/argparse.html#description>`_ and `epilog <https://docs.python.org/3/library/argparse.html#epilog>`_ of the `ArgumentParser`. If no description is given, but the decorated function has a docstring, then it is taken as the `description` for the `ArgumentParser`. You can also provide both the description and epilog in the docstring by splitting it into two sections with 4 or more - characters.
```python
@autocommand(__name__)
def copy(infile=sys.stdin, outfile=sys.stdout):
'''
Copy an the contents of a file (or stdin) to another file (or stdout)
----------
Some extra documentation in the epilog
'''
with smart_open(infile) as istr:
with smart_open(outfile, 'w') as ostr:
for line in istr:
ostr.write(line)
```
```
$ python copy.py -h
usage: copy.py [-h] [-i INFILE] [-o OUTFILE]
Copy an the contents of a file (or stdin) to another file (or stdout)
optional arguments:
-h, --help show this help message and exit
-i INFILE, --infile INFILE
-o OUTFILE, --outfile OUTFILE
Some extra documentation in the epilog
$ echo "Hello World" | python copy.py --outfile hello.txt
$ python copy.py --infile hello.txt --outfile hello2.txt
$ python copy.py --infile hello2.txt
Hello World
```
### Parameter descriptions
You can also attach description text to individual parameters in the annotation. To attach both a type and a description, supply them both in any order in a tuple
```python
@autocommand(__name__)
def copy_net(
infile: 'The name of the file to send',
host: 'The host to send the file to',
port: (int, 'The port to connect to')):
'''
Copy a file over raw TCP to a remote destination.
'''
# Left as an exercise to the reader
```
### Decorators and wrappers
Autocommand automatically follows wrapper chains created by `@functools.wraps`. This means that you can apply other wrapping decorators to your main function, and autocommand will still correctly detect the signature.
```python
from functools import wraps
from autocommand import autocommand
def print_yielded(func):
'''
Convert a generator into a function that prints all yielded elements
'''
@wraps(func)
def wrapper(*args, **kwargs):
for thing in func(*args, **kwargs):
print(thing)
return wrapper
@autocommand(__name__,
description= 'Print all the values from START to STOP, inclusive, in steps of STEP',
epilog= 'STOP and STEP default to 1')
@print_yielded
def seq(stop, start=1, step=1):
for i in range(start, stop + 1, step):
yield i
```
```
$ seq.py -h
usage: seq.py [-h] [-s START] [-S STEP] stop
Print all the values from START to STOP, inclusive, in steps of STEP
positional arguments:
stop
optional arguments:
-h, --help show this help message and exit
-s START, --start START
-S STEP, --step STEP
STOP and STEP default to 1
```
Even though autocommand is being applied to the `wrapper` returned by `print_yielded`, it still retreives the signature of the underlying `seq` function to create the argument parsing.
### Custom Parser
While autocommand's automatic parser generator is a powerful convenience, it doesn't cover all of the different features that argparse provides. If you need these features, you can provide your own parser as a kwarg to `autocommand`:
```python
from argparse import ArgumentParser
from autocommand import autocommand
parser = ArgumentParser()
# autocommand can't do optional positonal parameters
parser.add_argument('arg', nargs='?')
# or mutually exclusive options
group = parser.add_mutually_exclusive_group()
group.add_argument('-v', '--verbose', action='store_true')
group.add_argument('-q', '--quiet', action='store_true')
@autocommand(__name__, parser=parser)
def main(arg, verbose, quiet):
print(arg, verbose, quiet)
```
```
$ python parser.py -h
usage: write_file.py [-h] [-v | -q] [arg]
positional arguments:
arg
optional arguments:
-h, --help show this help message and exit
-v, --verbose
-q, --quiet
$ python parser.py
None False False
$ python parser.py hello
hello False False
$ python parser.py -v
None True False
$ python parser.py -q
None False True
$ python parser.py -vq
usage: parser.py [-h] [-v | -q] [arg]
parser.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
```
Any parser should work fine, so long as each of the parser's arguments has a corresponding parameter in the decorated main function. The order of parameters doesn't matter, as long as they are all present. Note that when using a custom parser, autocommand doesn't modify the parser or the retrieved arguments. This means that no description/epilog will be added, and the function's type annotations and defaults (if present) will be ignored.
## Testing and Library use
The decorated function is only called and exited from if the first argument to `autocommand` is `'__main__'` or `True`. If it is neither of these values, or no argument is given, then a new main function is created by the decorator. This function has the signature `main(argv=None)`, and is intended to be called with arguments as if via `main(sys.argv[1:])`. The function has the attributes `parser` and `main`, which are the generated `ArgumentParser` and the original main function that was decorated. This is to facilitate testing and library use of your main. Calling the function triggers a `parse_args()` with the supplied arguments, and returns the result of the main function. Note that, while it returns instead of calling `sys.exit`, the `parse_args()` function will raise a `SystemExit` in the event of a parsing error or `-h/--help` argument.
```python
@autocommand()
def test_prog(arg1, arg2: int, quiet=False, verbose=False):
if not quiet:
print(arg1, arg2)
if verbose:
print("LOUD NOISES")
return 0
print(test_prog(['-v', 'hello', '80']))
```
```
$ python test_prog.py
hello 80
LOUD NOISES
0
```
If the function is called with no arguments, `sys.argv[1:]` is used. This is to allow the autocommand function to be used as a setuptools entry point.
## Exceptions and limitations
- There are a few possible exceptions that `autocommand` can raise. All of them derive from `autocommand.AutocommandError`.
- If an invalid annotation is given (that is, it isn't a `type`, `str`, `(type, str)`, or `(str, type)`, an `AnnotationError` is raised. The `type` may be any callable, as described in the `Types`_ section.
- If the function has a `**kwargs` parameter, a `KWargError` is raised.
- If, somehow, the function has a positional-only parameter, a `PositionalArgError` is raised. This means that the argument doesn't have a name, which is currently not possible with a plain `def` or `lambda`, though many built-in functions have this kind of parameter.
- There are a few argparse features that are not supported by autocommand.
- It isn't possible to have an optional positional argument (as opposed to a `--option`). POSIX thinks this is bad form anyway.
- It isn't possible to have mutually exclusive arguments or options
- It isn't possible to have subcommands or subparsers, though I'm working on a few solutions involving classes or nested function definitions to allow this.
## Development
Autocommand cannot be important from the project root; this is to enforce separation of concerns and prevent accidental importing of `setup.py` or tests. To develop, install the project in editable mode:
```
$ python setup.py develop
```
This will create a link to the source files in the deployment directory, so that any source changes are reflected when it is imported.

View File

@ -0,0 +1,18 @@
autocommand-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
autocommand-2.2.2.dist-info/LICENSE,sha256=reeNBJgtaZctREqOFKlPh6IzTdOFXMgDSOqOJAqg3y0,7634
autocommand-2.2.2.dist-info/METADATA,sha256=OADZuR3O6iBlpu1ieTgzYul6w4uOVrk0P0BO5TGGAJk,15006
autocommand-2.2.2.dist-info/RECORD,,
autocommand-2.2.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
autocommand-2.2.2.dist-info/top_level.txt,sha256=AzfhgKKS8EdAwWUTSF8mgeVQbXOY9kokHB6kSqwwqu0,12
autocommand/__init__.py,sha256=zko5Rnvolvb-UXjCx_2ArPTGBWwUK5QY4LIQIKYR7As,1037
autocommand/__pycache__/__init__.cpython-310.pyc,,
autocommand/__pycache__/autoasync.cpython-310.pyc,,
autocommand/__pycache__/autocommand.cpython-310.pyc,,
autocommand/__pycache__/automain.cpython-310.pyc,,
autocommand/__pycache__/autoparse.cpython-310.pyc,,
autocommand/__pycache__/errors.cpython-310.pyc,,
autocommand/autoasync.py,sha256=AMdyrxNS4pqWJfP_xuoOcImOHWD-qT7x06wmKN1Vp-U,5680
autocommand/autocommand.py,sha256=hmkEmQ72HtL55gnURVjDOnsfYlGd5lLXbvT4KG496Qw,2505
autocommand/automain.py,sha256=A2b8i754Mxc_DjU9WFr6vqYDWlhz0cn8miu8d8EsxV8,2076
autocommand/autoparse.py,sha256=WVWmZJPcbzUKXP40raQw_0HD8qPJ2V9VG1eFFmmnFxw,11642
autocommand/errors.py,sha256=7aa3roh9Herd6nIKpQHNWEslWE8oq7GiHYVUuRqORnA,886

View File

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.38.4)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1,27 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
# flake8 flags all these imports as unused, hence the NOQAs everywhere.
from .automain import automain # NOQA
from .autoparse import autoparse, smart_open # NOQA
from .autocommand import autocommand # NOQA
try:
from .autoasync import autoasync # NOQA
except ImportError: # pragma: no cover
pass

View File

@ -0,0 +1,142 @@
# Copyright 2014-2015 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
from asyncio import get_event_loop, iscoroutine
from functools import wraps
from inspect import signature
async def _run_forever_coro(coro, args, kwargs, loop):
'''
This helper function launches an async main function that was tagged with
forever=True. There are two possibilities:
- The function is a normal function, which handles initializing the event
loop, which is then run forever
- The function is a coroutine, which needs to be scheduled in the event
loop, which is then run forever
- There is also the possibility that the function is a normal function
wrapping a coroutine function
The function is therefore called unconditionally and scheduled in the event
loop if the return value is a coroutine object.
The reason this is a separate function is to make absolutely sure that all
the objects created are garbage collected after all is said and done; we
do this to ensure that any exceptions raised in the tasks are collected
ASAP.
'''
# Personal note: I consider this an antipattern, as it relies on the use of
# unowned resources. The setup function dumps some stuff into the event
# loop where it just whirls in the ether without a well defined owner or
# lifetime. For this reason, there's a good chance I'll remove the
# forever=True feature from autoasync at some point in the future.
thing = coro(*args, **kwargs)
if iscoroutine(thing):
await thing
def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
'''
Convert an asyncio coroutine into a function which, when called, is
evaluted in an event loop, and the return value returned. This is intented
to make it easy to write entry points into asyncio coroutines, which
otherwise need to be explictly evaluted with an event loop's
run_until_complete.
If `loop` is given, it is used as the event loop to run the coro in. If it
is None (the default), the loop is retreived using asyncio.get_event_loop.
This call is defered until the decorated function is called, so that
callers can install custom event loops or event loop policies after
@autoasync is applied.
If `forever` is True, the loop is run forever after the decorated coroutine
is finished. Use this for servers created with asyncio.start_server and the
like.
If `pass_loop` is True, the event loop object is passed into the coroutine
as the `loop` kwarg when the wrapper function is called. In this case, the
wrapper function's __signature__ is updated to remove this parameter, so
that autoparse can still be used on it without generating a parameter for
`loop`.
This coroutine can be called with ( @autoasync(...) ) or without
( @autoasync ) arguments.
Examples:
@autoasync
def get_file(host, port):
reader, writer = yield from asyncio.open_connection(host, port)
data = reader.read()
sys.stdout.write(data.decode())
get_file(host, port)
@autoasync(forever=True, pass_loop=True)
def server(host, port, loop):
yield_from loop.create_server(Proto, host, port)
server('localhost', 8899)
'''
if coro is None:
return lambda c: autoasync(
c, loop=loop,
forever=forever,
pass_loop=pass_loop)
# The old and new signatures are required to correctly bind the loop
# parameter in 100% of cases, even if it's a positional parameter.
# NOTE: A future release will probably require the loop parameter to be
# a kwonly parameter.
if pass_loop:
old_sig = signature(coro)
new_sig = old_sig.replace(parameters=(
param for name, param in old_sig.parameters.items()
if name != "loop"))
@wraps(coro)
def autoasync_wrapper(*args, **kwargs):
# Defer the call to get_event_loop so that, if a custom policy is
# installed after the autoasync decorator, it is respected at call time
local_loop = get_event_loop() if loop is None else loop
# Inject the 'loop' argument. We have to use this signature binding to
# ensure it's injected in the correct place (positional, keyword, etc)
if pass_loop:
bound_args = old_sig.bind_partial()
bound_args.arguments.update(
loop=local_loop,
**new_sig.bind(*args, **kwargs).arguments)
args, kwargs = bound_args.args, bound_args.kwargs
if forever:
local_loop.create_task(_run_forever_coro(
coro, args, kwargs, local_loop
))
local_loop.run_forever()
else:
return local_loop.run_until_complete(coro(*args, **kwargs))
# Attach the updated signature. This allows 'pass_loop' to be used with
# autoparse
if pass_loop:
autoasync_wrapper.__signature__ = new_sig
return autoasync_wrapper

View File

@ -0,0 +1,70 @@
# Copyright 2014-2015 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
from .autoparse import autoparse
from .automain import automain
try:
from .autoasync import autoasync
except ImportError: # pragma: no cover
pass
def autocommand(
module, *,
description=None,
epilog=None,
add_nos=False,
parser=None,
loop=None,
forever=False,
pass_loop=False):
if callable(module):
raise TypeError('autocommand requires a module name argument')
def autocommand_decorator(func):
# Step 1: if requested, run it all in an asyncio event loop. autoasync
# patches the __signature__ of the decorated function, so that in the
# event that pass_loop is True, the `loop` parameter of the original
# function will *not* be interpreted as a command-line argument by
# autoparse
if loop is not None or forever or pass_loop:
func = autoasync(
func,
loop=None if loop is True else loop,
pass_loop=pass_loop,
forever=forever)
# Step 2: create parser. We do this second so that the arguments are
# parsed and passed *before* entering the asyncio event loop, if it
# exists. This simplifies the stack trace and ensures errors are
# reported earlier. It also ensures that errors raised during parsing &
# passing are still raised if `forever` is True.
func = autoparse(
func,
description=description,
epilog=epilog,
add_nos=add_nos,
parser=parser)
# Step 3: call the function automatically if __name__ == '__main__' (or
# if True was provided)
func = automain(module)(func)
return func
return autocommand_decorator

View File

@ -0,0 +1,59 @@
# Copyright 2014-2015 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
import sys
from .errors import AutocommandError
class AutomainRequiresModuleError(AutocommandError, TypeError):
pass
def automain(module, *, args=(), kwargs=None):
'''
This decorator automatically invokes a function if the module is being run
as the "__main__" module. Optionally, provide args or kwargs with which to
call the function. If `module` is "__main__", the function is called, and
the program is `sys.exit`ed with the return value. You can also pass `True`
to cause the function to be called unconditionally. If the function is not
called, it is returned unchanged by the decorator.
Usage:
@automain(__name__) # Pass __name__ to check __name__=="__main__"
def main():
...
If __name__ is "__main__" here, the main function is called, and then
sys.exit called with the return value.
'''
# Check that @automain(...) was called, rather than @automain
if callable(module):
raise AutomainRequiresModuleError(module)
if module == '__main__' or module is True:
if kwargs is None:
kwargs = {}
# Use a function definition instead of a lambda for a neater traceback
def automain_decorator(main):
sys.exit(main(*args, **kwargs))
return automain_decorator
else:
return lambda main: main

View File

@ -0,0 +1,333 @@
# Copyright 2014-2015 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
import sys
from re import compile as compile_regex
from inspect import signature, getdoc, Parameter
from argparse import ArgumentParser
from contextlib import contextmanager
from functools import wraps
from io import IOBase
from autocommand.errors import AutocommandError
_empty = Parameter.empty
class AnnotationError(AutocommandError):
'''Annotation error: annotation must be a string, type, or tuple of both'''
class PositionalArgError(AutocommandError):
'''
Postional Arg Error: autocommand can't handle postional-only parameters
'''
class KWArgError(AutocommandError):
'''kwarg Error: autocommand can't handle a **kwargs parameter'''
class DocstringError(AutocommandError):
'''Docstring error'''
class TooManySplitsError(DocstringError):
'''
The docstring had too many ---- section splits. Currently we only support
using up to a single split, to split the docstring into description and
epilog parts.
'''
def _get_type_description(annotation):
'''
Given an annotation, return the (type, description) for the parameter.
If you provide an annotation that is somehow both a string and a callable,
the behavior is undefined.
'''
if annotation is _empty:
return None, None
elif callable(annotation):
return annotation, None
elif isinstance(annotation, str):
return None, annotation
elif isinstance(annotation, tuple):
try:
arg1, arg2 = annotation
except ValueError as e:
raise AnnotationError(annotation) from e
else:
if callable(arg1) and isinstance(arg2, str):
return arg1, arg2
elif isinstance(arg1, str) and callable(arg2):
return arg2, arg1
raise AnnotationError(annotation)
def _add_arguments(param, parser, used_char_args, add_nos):
'''
Add the argument(s) to an ArgumentParser (using add_argument) for a given
parameter. used_char_args is the set of -short options currently already in
use, and is updated (if necessary) by this function. If add_nos is True,
this will also add an inverse switch for all boolean options. For
instance, for the boolean parameter "verbose", this will create --verbose
and --no-verbose.
'''
# Impl note: This function is kept separate from make_parser because it's
# already very long and I wanted to separate out as much as possible into
# its own call scope, to prevent even the possibility of suble mutation
# bugs.
if param.kind is param.POSITIONAL_ONLY:
raise PositionalArgError(param)
elif param.kind is param.VAR_KEYWORD:
raise KWArgError(param)
# These are the kwargs for the add_argument function.
arg_spec = {}
is_option = False
# Get the type and default from the annotation.
arg_type, description = _get_type_description(param.annotation)
# Get the default value
default = param.default
# If there is no explicit type, and the default is present and not None,
# infer the type from the default.
if arg_type is None and default not in {_empty, None}:
arg_type = type(default)
# Add default. The presence of a default means this is an option, not an
# argument.
if default is not _empty:
arg_spec['default'] = default
is_option = True
# Add the type
if arg_type is not None:
# Special case for bool: make it just a --switch
if arg_type is bool:
if not default or default is _empty:
arg_spec['action'] = 'store_true'
else:
arg_spec['action'] = 'store_false'
# Switches are always options
is_option = True
# Special case for file types: make it a string type, for filename
elif isinstance(default, IOBase):
arg_spec['type'] = str
# TODO: special case for list type.
# - How to specificy type of list members?
# - param: [int]
# - param: int =[]
# - action='append' vs nargs='*'
else:
arg_spec['type'] = arg_type
# nargs: if the signature includes *args, collect them as trailing CLI
# arguments in a list. *args can't have a default value, so it can never be
# an option.
if param.kind is param.VAR_POSITIONAL:
# TODO: consider depluralizing metavar/name here.
arg_spec['nargs'] = '*'
# Add description.
if description is not None:
arg_spec['help'] = description
# Get the --flags
flags = []
name = param.name
if is_option:
# Add the first letter as a -short option.
for letter in name[0], name[0].swapcase():
if letter not in used_char_args:
used_char_args.add(letter)
flags.append('-{}'.format(letter))
break
# If the parameter is a --long option, or is a -short option that
# somehow failed to get a flag, add it.
if len(name) > 1 or not flags:
flags.append('--{}'.format(name))
arg_spec['dest'] = name
else:
flags.append(name)
parser.add_argument(*flags, **arg_spec)
# Create the --no- version for boolean switches
if add_nos and arg_type is bool:
parser.add_argument(
'--no-{}'.format(name),
action='store_const',
dest=name,
const=default if default is not _empty else False)
def make_parser(func_sig, description, epilog, add_nos):
'''
Given the signature of a function, create an ArgumentParser
'''
parser = ArgumentParser(description=description, epilog=epilog)
used_char_args = {'h'}
# Arange the params so that single-character arguments are first. This
# esnures they don't have to get --long versions. sorted is stable, so the
# parameters will otherwise still be in relative order.
params = sorted(
func_sig.parameters.values(),
key=lambda param: len(param.name) > 1)
for param in params:
_add_arguments(param, parser, used_char_args, add_nos)
return parser
_DOCSTRING_SPLIT = compile_regex(r'\n\s*-{4,}\s*\n')
def parse_docstring(docstring):
'''
Given a docstring, parse it into a description and epilog part
'''
if docstring is None:
return '', ''
parts = _DOCSTRING_SPLIT.split(docstring)
if len(parts) == 1:
return docstring, ''
elif len(parts) == 2:
return parts[0], parts[1]
else:
raise TooManySplitsError()
def autoparse(
func=None, *,
description=None,
epilog=None,
add_nos=False,
parser=None):
'''
This decorator converts a function that takes normal arguments into a
function which takes a single optional argument, argv, parses it using an
argparse.ArgumentParser, and calls the underlying function with the parsed
arguments. If it is not given, sys.argv[1:] is used. This is so that the
function can be used as a setuptools entry point, as well as a normal main
function. sys.argv[1:] is not evaluated until the function is called, to
allow injecting different arguments for testing.
It uses the argument signature of the function to create an
ArgumentParser. Parameters without defaults become positional parameters,
while parameters *with* defaults become --options. Use annotations to set
the type of the parameter.
The `desctiption` and `epilog` parameters corrospond to the same respective
argparse parameters. If no description is given, it defaults to the
decorated functions's docstring, if present.
If add_nos is True, every boolean option (that is, every parameter with a
default of True/False or a type of bool) will have a --no- version created
as well, which inverts the option. For instance, the --verbose option will
have a --no-verbose counterpart. These are not mutually exclusive-
whichever one appears last in the argument list will have precedence.
If a parser is given, it is used instead of one generated from the function
signature. In this case, no parser is created; instead, the given parser is
used to parse the argv argument. The parser's results' argument names must
match up with the parameter names of the decorated function.
The decorated function is attached to the result as the `func` attribute,
and the parser is attached as the `parser` attribute.
'''
# If @autoparse(...) is used instead of @autoparse
if func is None:
return lambda f: autoparse(
f, description=description,
epilog=epilog,
add_nos=add_nos,
parser=parser)
func_sig = signature(func)
docstr_description, docstr_epilog = parse_docstring(getdoc(func))
if parser is None:
parser = make_parser(
func_sig,
description or docstr_description,
epilog or docstr_epilog,
add_nos)
@wraps(func)
def autoparse_wrapper(argv=None):
if argv is None:
argv = sys.argv[1:]
# Get empty argument binding, to fill with parsed arguments. This
# object does all the heavy lifting of turning named arguments into
# into correctly bound *args and **kwargs.
parsed_args = func_sig.bind_partial()
parsed_args.arguments.update(vars(parser.parse_args(argv)))
return func(*parsed_args.args, **parsed_args.kwargs)
# TODO: attach an updated __signature__ to autoparse_wrapper, just in case.
# Attach the wrapped function and parser, and return the wrapper.
autoparse_wrapper.func = func
autoparse_wrapper.parser = parser
return autoparse_wrapper
@contextmanager
def smart_open(filename_or_file, *args, **kwargs):
'''
This context manager allows you to open a filename, if you want to default
some already-existing file object, like sys.stdout, which shouldn't be
closed at the end of the context. If the filename argument is a str, bytes,
or int, the file object is created via a call to open with the given *args
and **kwargs, sent to the context, and closed at the end of the context,
just like "with open(filename) as f:". If it isn't one of the openable
types, the object simply sent to the context unchanged, and left unclosed
at the end of the context. Example:
def work_with_file(name=sys.stdout):
with smart_open(name) as f:
# Works correctly if name is a str filename or sys.stdout
print("Some stuff", file=f)
# If it was a filename, f is closed at the end here.
'''
if isinstance(filename_or_file, (str, bytes, int)):
with open(filename_or_file, *args, **kwargs) as file:
yield file
else:
yield filename_or_file

View File

@ -0,0 +1,23 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
class AutocommandError(Exception):
'''Base class for autocommand exceptions'''
pass
# Individual modules will define errors specific to that module.

View File

@ -0,0 +1,30 @@
Copyright © 2004-2020, CherryPy Team (team@cherrypy.dev)
All rights reserved.
* * *
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of CherryPy nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,150 @@
Metadata-Version: 2.1
Name: cheroot
Version: 10.0.0
Summary: Highly-optimized, pure-python HTTP server
Home-page: https://cheroot.cherrypy.dev
Author: CherryPy Team
Author-email: team@cherrypy.dev
License: UNKNOWN
Project-URL: CI: GitHub, https://github.com/cherrypy/cheroot/actions
Project-URL: Docs: RTD, https://cheroot.cherrypy.dev
Project-URL: GitHub: issues, https://github.com/cherrypy/cheroot/issues
Project-URL: GitHub: repo, https://github.com/cherrypy/cheroot
Project-URL: Tidelift: funding, https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=referral&utm_campaign=pypi
Keywords: http,server,ssl,wsgi
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Framework :: CherryPy
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: Jython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
Classifier: Typing :: Typed
Requires-Python: >=3.6
Description-Content-Type: text/x-rst
License-File: LICENSE.md
Requires-Dist: more-itertools (>=2.6)
Requires-Dist: jaraco.functools
Requires-Dist: importlib-metadata ; python_version < "3.8"
Provides-Extra: docs
Requires-Dist: sphinx (>=1.8.2) ; extra == 'docs'
Requires-Dist: jaraco.packaging (>=3.2) ; extra == 'docs'
Requires-Dist: sphinx-tabs (>=1.1.0) ; extra == 'docs'
Requires-Dist: furo ; extra == 'docs'
Requires-Dist: python-dateutil ; extra == 'docs'
Requires-Dist: sphinxcontrib-apidoc (>=0.3.0) ; extra == 'docs'
.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg
:target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md
:alt: SWUbanner
.. image:: https://img.shields.io/pypi/v/cheroot.svg
:target: https://pypi.org/project/cheroot
.. image:: https://tidelift.com/badges/package/pypi/cheroot
:target: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=readme
:alt: Cheroot is available as part of the Tidelift Subscription
.. image:: https://github.com/cherrypy/cheroot/actions/workflows/ci-cd.yml/badge.svg
:target: https://github.com/cherrypy/cheroot/actions/workflows/ci-cd.yml
:alt: GitHub Actions CI/CD Workflow
.. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600
:target: https://pypi.org/project/cheroot
.. image:: https://img.shields.io/pypi/pyversions/cheroot.svg
:target: https://pypi.org/project/cheroot
.. image:: https://codecov.io/gh/cherrypy/cheroot/branch/master/graph/badge.svg
:target: https://codecov.io/gh/cherrypy/cheroot
:alt: codecov
.. image:: https://readthedocs.org/projects/cheroot/badge/?version=latest
:target: https://cheroot.cherrypy.dev/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/StackOverflow-Cheroot-blue.svg
:target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy
.. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg
:target: https://gitter.im/cherrypy/cherrypy
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
:target: http://makeapullrequest.com/
.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot.svg?type=shield
:target: https://app.fossa.io/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot?ref=badge_shield
:alt: FOSSA Status
Cheroot is the high-performance, pure-Python HTTP server used by CherryPy.
Status
======
The test suite currently relies on pytest. It's being run via GitHub
Actions CI/CD workflows.
For Enterprise
==============
.. list-table::
:widths: 10 100
* - |tideliftlogo|
- Professional support for Cheroot is available as part of the
`Tidelift Subscription`_. The CherryPy maintainers and the
maintainers of thousands of other packages are working with
Tidelift to deliver one enterprise subscription that covers all
of the open source you use.
Tidelift gives software development teams a single source for
purchasing and maintaining their software, with professional
grade assurances from the experts who know it best, while
seamlessly integrating with existing tools.
`Learn more <Tidelift Subscription_>`_.
.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=referral&utm_campaign=readme
.. |tideliftlogo| image:: https://cdn2.hubspot.net/hubfs/4008838/website/logos/logos_for_download/Tidelift_primary-shorthand-logo.png
:target: https://tidelift.com/subscription/pkg/pypi-cheroot?utm_source=pypi-cheroot&utm_medium=readme
:width: 75
:alt: Tidelift
Contribute Cheroot
==================
**Want to add something to upstream?** Feel free to submit a PR or file an issue
if unsure. Please follow `CherryPy's common contribution guidelines
<https://github.com/cherrypy/cherrypy/blob/master/.github/CONTRIBUTING.rst>`_.
Note that PR is more likely to be accepted if it includes tests and detailed
description helping maintainers to understand it better 🎉
Oh, and be pythonic, please 🐍
**Don't know how?** Check out `How to Contribute to Open Source
<https://opensource.guide/how-to-contribute/>`_ article by GitHub 🚀
License
=======
.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot.svg?type=large
:target: https://app.fossa.io/projects/git%2Bgithub.com%2Fcherrypy%2Fcheroot?ref=badge_large
:alt: FOSSA Status

View File

@ -0,0 +1,83 @@
../../../bin/cheroot,sha256=E8JQfKtdh5qRqSjMrCA02q8ZRqP-O-sRTc4SsgO4-3s,243
cheroot-10.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
cheroot-10.0.0.dist-info/LICENSE.md,sha256=4g_utJGn6YCE8VcZNJ6YV6rUHEUDxeR5-IFbBj2_dWQ,1511
cheroot-10.0.0.dist-info/METADATA,sha256=NEVNG_PxvbSNFehtACADPaqBN5jArY3OBntPlbPrAcE,6423
cheroot-10.0.0.dist-info/RECORD,,
cheroot-10.0.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
cheroot-10.0.0.dist-info/entry_points.txt,sha256=BapQtPZPUE38-YhhtJUBwlkNRNY_yg0s3_bt5yVqqG8,46
cheroot-10.0.0.dist-info/top_level.txt,sha256=P8VZfrem5gTRS34X6Thu7jyEoj_zSaPNI_3P0fShbAI,8
cheroot/__init__.py,sha256=ZZgcItIBeyj6d3xlBw_o4uFxtBwL86Y9MwzFSfmd1_c,284
cheroot/__init__.pyi,sha256=Y25n44pyE3vp92MiABKrcK3IWRyQ1JG1rZ4Ufqy2nC0,17
cheroot/__main__.py,sha256=jLOqwD221LYbg1ksPWhjOKYu4M4yV7y3mteM6CJi-Oc,109
cheroot/__pycache__/__init__.cpython-310.pyc,,
cheroot/__pycache__/__main__.cpython-310.pyc,,
cheroot/__pycache__/_compat.cpython-310.pyc,,
cheroot/__pycache__/cli.cpython-310.pyc,,
cheroot/__pycache__/connections.cpython-310.pyc,,
cheroot/__pycache__/errors.cpython-310.pyc,,
cheroot/__pycache__/makefile.cpython-310.pyc,,
cheroot/__pycache__/server.cpython-310.pyc,,
cheroot/__pycache__/testing.cpython-310.pyc,,
cheroot/__pycache__/wsgi.cpython-310.pyc,,
cheroot/_compat.py,sha256=t1y0uqANmAv4SfkFrqHUk6NCgWOXkt2W8_1r0u7EQTA,2083
cheroot/_compat.pyi,sha256=jrBkeGVNS6B6TXzP0NyyBQyGvf-ucCFbLi5RitP1eJs,605
cheroot/cli.py,sha256=5qxJ0tK7nUPwTI2Q97VyjlPNdVwznoj-gTT7DJ_y_S8,6987
cheroot/cli.pyi,sha256=LIKNaRFyZVRRl2n3Jm_VTJjYaNsDSJQ7IcEr5qcjZR0,828
cheroot/connections.py,sha256=a04R6pFRn9uvtgO_gDXvJwEy0Asqrxuy4ks5rkG7gY4,14395
cheroot/connections.pyi,sha256=r-I9Mkn-PHcjqQvekmMmQNbh8a5-sJyfLp6d5sP7T48,714
cheroot/errors.py,sha256=vOHGdmaJwk9eUu-XqTyYjYxCdC6DgdTJHSm0bE3soIc,2753
cheroot/errors.pyi,sha256=WPJaht4vEcELxTStNPMYuEmztK52aLbOcYE8MwGTmSU,425
cheroot/makefile.py,sha256=XVLIM1ngS_XbLzZryUPgs5SSnV8CHyyRGzU3J4QpVPE,2306
cheroot/makefile.pyi,sha256=39bXRM-9N1jOAEgfudimuqoOkLFAShg-naldaQDKnGM,542
cheroot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cheroot/server.py,sha256=0_mLoRq7evFYoJVxKV0ia6wU2D4DuVGj49oVDv6FPHs,77336
cheroot/server.pyi,sha256=_r4KsLNMGvh6UZHN-hYrz7VAhut4CfrTHmNoyWauYoE,5064
cheroot/ssl/__init__.py,sha256=U2aplZGnvXibKBSGN3Y9cje_zFS-mA_bJz5jO9h7JaY,1416
cheroot/ssl/__init__.pyi,sha256=liQgp4uYOywZfAGCqcaxVYEM6OBFCWOS-bCGtDI2q2w,581
cheroot/ssl/__pycache__/__init__.cpython-310.pyc,,
cheroot/ssl/__pycache__/builtin.cpython-310.pyc,,
cheroot/ssl/__pycache__/pyopenssl.cpython-310.pyc,,
cheroot/ssl/builtin.py,sha256=2apU5DsSFCTVysj53Ek-zKb3YH5V01j5MCWwTd3GK7M,17906
cheroot/ssl/builtin.pyi,sha256=nriJS-4eEkyw04oOZW8dslIXLfr_Kzkb7izy6TavEIw,561
cheroot/ssl/pyopenssl.py,sha256=3aVW3aQYW6YOHoQyPYaJ46Ts1EQ5_yZCM5g9SnvFQzs,13231
cheroot/ssl/pyopenssl.pyi,sha256=YlpXT8Rpj_mvBtOBiFuBep758p618JSegch9x2OCP38,1086
cheroot/test/__init__.py,sha256=_hgyWgeLHsLLScMhGwMOsevD3Wg3pssK535YfNJFoeU,26
cheroot/test/__pycache__/__init__.cpython-310.pyc,,
cheroot/test/__pycache__/_pytest_plugin.cpython-310.pyc,,
cheroot/test/__pycache__/conftest.cpython-310.pyc,,
cheroot/test/__pycache__/helper.cpython-310.pyc,,
cheroot/test/__pycache__/test__compat.cpython-310.pyc,,
cheroot/test/__pycache__/test_cli.cpython-310.pyc,,
cheroot/test/__pycache__/test_conn.cpython-310.pyc,,
cheroot/test/__pycache__/test_core.cpython-310.pyc,,
cheroot/test/__pycache__/test_dispatch.cpython-310.pyc,,
cheroot/test/__pycache__/test_errors.cpython-310.pyc,,
cheroot/test/__pycache__/test_makefile.cpython-310.pyc,,
cheroot/test/__pycache__/test_server.cpython-310.pyc,,
cheroot/test/__pycache__/test_ssl.cpython-310.pyc,,
cheroot/test/__pycache__/test_wsgi.cpython-310.pyc,,
cheroot/test/__pycache__/webtest.cpython-310.pyc,,
cheroot/test/_pytest_plugin.py,sha256=u_13sGqGbIDP7z9ZC1_SRr_DWBoDQDR3O37IPqOxwyI,1768
cheroot/test/conftest.py,sha256=pyMICEAvtvWw1Q-sqhr-VVcal3NbWlDD3rKe9_ROsGM,2051
cheroot/test/helper.py,sha256=A5HlkxxyswxYRT_0IjHja_TWvd2VxDY44Elzni0uke0,4755
cheroot/test/test__compat.py,sha256=uC4QuuYSDDMyErGij-vHfGV1fKTA_ylnZEmWXMLXUvc,1661
cheroot/test/test_cli.py,sha256=7bmQNozJ8cakCFw-ogUzAZq7yGUaSJYwzgXQx8tQfLg,2664
cheroot/test/test_conn.py,sha256=kpk30-tHHRIgeLWEaAC7iF39FoOv42WnPsdmCZkdTkY,43780
cheroot/test/test_core.py,sha256=6MOYMEvXZhx4wnwOt8uTO-1Cu-37n5qqLiB0hicCeD8,14698
cheroot/test/test_dispatch.py,sha256=NJtth_qaHlKa7V83j-ekrf_8G2-7HI8EQiZdKTUcCx4,1210
cheroot/test/test_errors.py,sha256=i4-o3CTwluDN_70N9c_9QI4OY1imIMHSqOHpmyqML_8,924
cheroot/test/test_makefile.py,sha256=j3ZVMMDJI_JPTun-agdaJSKa8z3kH5HhXv_95Lg9T2I,1191
cheroot/test/test_server.py,sha256=vBZ35OJOGEgBcPmjpomwMIuCBoIlGSKc0OHS1EnhQqY,16697
cheroot/test/test_ssl.py,sha256=CkbF164i-yV5Infp8zE7sXSRZQOxlIILulu3eWRhM90,22268
cheroot/test/test_wsgi.py,sha256=vdyH1JN2K8qbfx4olnda-VHOxErtemaVVEUjVlNC5mo,2823
cheroot/test/webtest.py,sha256=3tjaCGiotSoEX9s1ULVKQojqLm82z3CDKZSrZQLzSTI,18398
cheroot/testing.py,sha256=ilu4oOPR8Qap64mO5G6PyLjXD0ix6yJiRY5K0Ldqi6s,3969
cheroot/testing.pyi,sha256=HlbbwoROyn5DSAuCQxcUw58v2cCKu1MG7ySzwbH0ZXk,448
cheroot/workers/__init__.py,sha256=-ziyw7iPWHs2dN4R_Q7AQZ7r0dQPTes1nVCzAg2LOc8,25
cheroot/workers/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cheroot/workers/__pycache__/__init__.cpython-310.pyc,,
cheroot/workers/__pycache__/threadpool.cpython-310.pyc,,
cheroot/workers/threadpool.py,sha256=F6KusLUcVQmooIV35hXhDc3ckaTrZkonJORU74p6vcE,10967
cheroot/workers/threadpool.pyi,sha256=9xF6s4LAwnURJbdG1K8f97zGk2YVb2kEULJMaxsUXiI,925
cheroot/wsgi.py,sha256=_eUOnbMUvc6wdzaSjEyQrUuu6Q6ye5yYNC7z9rovnCw,14239
cheroot/wsgi.pyi,sha256=4DVIwCgcYtIxIA7wd-ZHcBNKOSiaXwaAAwrsUcg0h0M,1516

View File

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.37.1)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1,3 @@
[console_scripts]
cheroot = cheroot.cli:main

View File

@ -0,0 +1,12 @@
"""High-performance, pure-Python HTTP server used by CherryPy."""
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata # noqa: WPS440
try:
__version__ = metadata.version('cheroot')
except Exception:
__version__ = 'unknown'

View File

@ -0,0 +1 @@
__version__: str

View File

@ -0,0 +1,6 @@
"""Stub for accessing the Cheroot CLI tool."""
from .cli import main
if __name__ == '__main__':
main()

View File

@ -0,0 +1,84 @@
# pylint: disable=unused-import
"""Compatibility code for using Cheroot with various versions of Python."""
import os
import platform
try:
import ssl
IS_ABOVE_OPENSSL10 = ssl.OPENSSL_VERSION_INFO >= (1, 1)
del ssl
except ImportError:
IS_ABOVE_OPENSSL10 = None
IS_CI = bool(os.getenv('CI'))
IS_GITHUB_ACTIONS_WORKFLOW = bool(os.getenv('GITHUB_WORKFLOW'))
IS_PYPY = platform.python_implementation() == 'PyPy'
SYS_PLATFORM = platform.system()
IS_WINDOWS = SYS_PLATFORM == 'Windows'
IS_LINUX = SYS_PLATFORM == 'Linux'
IS_MACOS = SYS_PLATFORM == 'Darwin'
IS_SOLARIS = SYS_PLATFORM == 'SunOS'
PLATFORM_ARCH = platform.machine()
IS_PPC = PLATFORM_ARCH.startswith('ppc')
def ntob(n, encoding='ISO-8859-1'):
"""Return the native string as bytes in the given encoding."""
assert_native(n)
# In Python 3, the native string type is unicode
return n.encode(encoding)
def ntou(n, encoding='ISO-8859-1'):
"""Return the native string as Unicode with the given encoding."""
assert_native(n)
# In Python 3, the native string type is unicode
return n
def bton(b, encoding='ISO-8859-1'):
"""Return the byte string as native string in the given encoding."""
return b.decode(encoding)
def assert_native(n):
"""Check whether the input is of native :py:class:`str` type.
Raises:
TypeError: in case of failed check
"""
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
def extract_bytes(mv):
r"""Retrieve bytes out of the given input buffer.
:param mv: input :py:func:`buffer`
:type mv: memoryview or bytes
:return: unwrapped bytes
:rtype: bytes
:raises ValueError: if the input is not one of \
:py:class:`memoryview`/:py:func:`buffer` \
or :py:class:`bytes`
"""
if isinstance(mv, memoryview):
return mv.tobytes()
if isinstance(mv, bytes):
return mv
raise ValueError(
'extract_bytes() only accepts bytes and memoryview/buffer',
)

View File

@ -0,0 +1,22 @@
from typing import Any, ContextManager, Optional, Type, Union
def suppress(*exceptions: Type[BaseException]) -> ContextManager[None]: ...
IS_ABOVE_OPENSSL10: Optional[bool]
IS_CI: bool
IS_GITHUB_ACTIONS_WORKFLOW: bool
IS_PYPY: bool
SYS_PLATFORM: str
IS_WINDOWS: bool
IS_LINUX: bool
IS_MACOS: bool
IS_SOLARIS: bool
PLATFORM_ARCH: str
IS_PPC: bool
def ntob(n: str, encoding: str = ...) -> bytes: ...
def ntou(n: str, encoding: str = ...) -> str: ...
def bton(b: bytes, encoding: str = ...) -> str: ...
def assert_native(n: str) -> None: ...
def extract_bytes(mv: Union[memoryview, bytes]) -> bytes: ...

View File

@ -0,0 +1,243 @@
"""Command line tool for starting a Cheroot WSGI/HTTP server instance.
Basic usage:
.. code-block:: shell-session
$ # Start a server on 127.0.0.1:8000 with the default settings
$ # for the WSGI app myapp/wsgi.py:application()
$ cheroot myapp.wsgi
$ # Start a server on 0.0.0.0:9000 with 8 threads
$ # for the WSGI app myapp/wsgi.py:main_app()
$ cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8
$ # Start a server for the cheroot.server.Gateway subclass
$ # myapp/gateway.py:HTTPGateway
$ cheroot myapp.gateway:HTTPGateway
$ # Start a server on the UNIX socket /var/spool/myapp.sock
$ cheroot myapp.wsgi --bind /var/spool/myapp.sock
$ # Start a server on the abstract UNIX socket CherootServer
$ cheroot myapp.wsgi --bind @CherootServer
.. spelling::
cli
"""
import argparse
import os
import sys
import urllib.parse # noqa: WPS301
from importlib import import_module
from contextlib import suppress
from . import server
from . import wsgi
class BindLocation:
"""A class for storing the bind location for a Cheroot instance."""
class TCPSocket(BindLocation):
"""TCPSocket."""
def __init__(self, address, port):
"""Initialize.
Args:
address (str): Host name or IP address
port (int): TCP port number
"""
self.bind_addr = address, port
class UnixSocket(BindLocation):
"""UnixSocket."""
def __init__(self, path):
"""Initialize."""
self.bind_addr = path
class AbstractSocket(BindLocation):
"""AbstractSocket."""
def __init__(self, abstract_socket):
"""Initialize."""
self.bind_addr = '\x00{sock_path}'.format(sock_path=abstract_socket)
class Application:
"""Application."""
@classmethod
def resolve(cls, full_path):
"""Read WSGI app/Gateway path string and import application module."""
mod_path, _, app_path = full_path.partition(':')
app = getattr(import_module(mod_path), app_path or 'application')
# suppress the `TypeError` exception, just in case `app` is not a class
with suppress(TypeError):
if issubclass(app, server.Gateway):
return GatewayYo(app)
return cls(app)
def __init__(self, wsgi_app):
"""Initialize."""
if not callable(wsgi_app):
raise TypeError(
'Application must be a callable object or '
'cheroot.server.Gateway subclass',
)
self.wsgi_app = wsgi_app
def server_args(self, parsed_args):
"""Return keyword args for Server class."""
args = {
arg: value
for arg, value in vars(parsed_args).items()
if not arg.startswith('_') and value is not None
}
args.update(vars(self))
return args
def server(self, parsed_args):
"""Server."""
return wsgi.Server(**self.server_args(parsed_args))
class GatewayYo:
"""Gateway."""
def __init__(self, gateway):
"""Init."""
self.gateway = gateway
def server(self, parsed_args):
"""Server."""
server_args = vars(self)
server_args['bind_addr'] = parsed_args['bind_addr']
if parsed_args.max is not None:
server_args['maxthreads'] = parsed_args.max
if parsed_args.numthreads is not None:
server_args['minthreads'] = parsed_args.numthreads
return server.HTTPServer(**server_args)
def parse_wsgi_bind_location(bind_addr_string):
"""Convert bind address string to a BindLocation."""
# if the string begins with an @ symbol, use an abstract socket,
# this is the first condition to verify, otherwise the urlparse
# validation would detect //@<value> as a valid url with a hostname
# with value: "<value>" and port: None
if bind_addr_string.startswith('@'):
return AbstractSocket(bind_addr_string[1:])
# try and match for an IP/hostname and port
match = urllib.parse.urlparse(
'//{addr}'.format(addr=bind_addr_string),
)
try:
addr = match.hostname
port = match.port
if addr is not None or port is not None:
return TCPSocket(addr, port)
except ValueError:
pass
# else, assume a UNIX socket path
return UnixSocket(path=bind_addr_string)
def parse_wsgi_bind_addr(bind_addr_string):
"""Convert bind address string to bind address parameter."""
return parse_wsgi_bind_location(bind_addr_string).bind_addr
_arg_spec = {
'_wsgi_app': {
'metavar': 'APP_MODULE',
'type': Application.resolve,
'help': 'WSGI application callable or cheroot.server.Gateway subclass',
},
'--bind': {
'metavar': 'ADDRESS',
'dest': 'bind_addr',
'type': parse_wsgi_bind_addr,
'default': '[::1]:8000',
'help': 'Network interface to listen on (default: [::1]:8000)',
},
'--chdir': {
'metavar': 'PATH',
'type': os.chdir,
'help': 'Set the working directory',
},
'--server-name': {
'dest': 'server_name',
'type': str,
'help': 'Web server name to be advertised via Server HTTP header',
},
'--threads': {
'metavar': 'INT',
'dest': 'numthreads',
'type': int,
'help': 'Minimum number of worker threads',
},
'--max-threads': {
'metavar': 'INT',
'dest': 'max',
'type': int,
'help': 'Maximum number of worker threads',
},
'--timeout': {
'metavar': 'INT',
'dest': 'timeout',
'type': int,
'help': 'Timeout in seconds for accepted connections',
},
'--shutdown-timeout': {
'metavar': 'INT',
'dest': 'shutdown_timeout',
'type': int,
'help': 'Time in seconds to wait for worker threads to cleanly exit',
},
'--request-queue-size': {
'metavar': 'INT',
'dest': 'request_queue_size',
'type': int,
'help': 'Maximum number of queued connections',
},
'--accepted-queue-size': {
'metavar': 'INT',
'dest': 'accepted_queue_size',
'type': int,
'help': 'Maximum number of active requests in queue',
},
'--accepted-queue-timeout': {
'metavar': 'INT',
'dest': 'accepted_queue_timeout',
'type': int,
'help': 'Timeout in seconds for putting requests into queue',
},
}
def main():
"""Create a new Cheroot instance with arguments from the command line."""
parser = argparse.ArgumentParser(
description='Start an instance of the Cheroot WSGI/HTTP server.',
)
for arg, spec in _arg_spec.items():
parser.add_argument(arg, **spec)
raw_args = parser.parse_args()
# ensure cwd in sys.path
'' in sys.path or sys.path.insert(0, '')
# create a server based on the arguments provided
raw_args._wsgi_app.server(raw_args).safe_start()

View File

@ -0,0 +1,32 @@
from typing import Any
class BindLocation: ...
class TCPSocket(BindLocation):
bind_addr: Any
def __init__(self, address, port) -> None: ...
class UnixSocket(BindLocation):
bind_addr: Any
def __init__(self, path) -> None: ...
class AbstractSocket(BindLocation):
bind_addr: Any
def __init__(self, abstract_socket) -> None: ...
class Application:
@classmethod
def resolve(cls, full_path): ...
wsgi_app: Any
def __init__(self, wsgi_app) -> None: ...
def server_args(self, parsed_args): ...
def server(self, parsed_args): ...
class GatewayYo:
gateway: Any
def __init__(self, gateway) -> None: ...
def server(self, parsed_args): ...
def parse_wsgi_bind_location(bind_addr_string: str): ...
def parse_wsgi_bind_addr(bind_addr_string: str): ...
def main() -> None: ...

View File

@ -0,0 +1,387 @@
"""Utilities to manage open connections."""
import io
import os
import socket
import threading
import time
import selectors
from contextlib import suppress
from . import errors
from ._compat import IS_WINDOWS
from .makefile import MakeFile
try:
import fcntl
except ImportError:
try:
from ctypes import windll, WinError
import ctypes.wintypes
_SetHandleInformation = windll.kernel32.SetHandleInformation
_SetHandleInformation.argtypes = [
ctypes.wintypes.HANDLE,
ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD,
]
_SetHandleInformation.restype = ctypes.wintypes.BOOL
except ImportError:
def prevent_socket_inheritance(sock):
"""Stub inheritance prevention.
Dummy function, since neither fcntl nor ctypes are available.
"""
pass
else:
def prevent_socket_inheritance(sock):
"""Mark the given socket fd as non-inheritable (Windows)."""
if not _SetHandleInformation(sock.fileno(), 1, 0):
raise WinError()
else:
def prevent_socket_inheritance(sock):
"""Mark the given socket fd as non-inheritable (POSIX)."""
fd = sock.fileno()
old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
class _ThreadsafeSelector:
"""Thread-safe wrapper around a DefaultSelector.
There are 2 thread contexts in which it may be accessed:
* the selector thread
* one of the worker threads in workers/threadpool.py
The expected read/write patterns are:
* :py:func:`~iter`: selector thread
* :py:meth:`register`: selector thread and threadpool,
via :py:meth:`~cheroot.workers.threadpool.ThreadPool.put`
* :py:meth:`unregister`: selector thread only
Notably, this means :py:class:`_ThreadsafeSelector` never needs to worry
that connections will be removed behind its back.
The lock is held when iterating or modifying the selector but is not
required when :py:meth:`select()ing <selectors.BaseSelector.select>` on it.
"""
def __init__(self):
self._selector = selectors.DefaultSelector()
self._lock = threading.Lock()
def __len__(self):
with self._lock:
return len(self._selector.get_map() or {})
@property
def connections(self):
"""Retrieve connections registered with the selector."""
with self._lock:
mapping = self._selector.get_map() or {}
for _, (_, sock_fd, _, conn) in mapping.items():
yield (sock_fd, conn)
def register(self, fileobj, events, data=None):
"""Register ``fileobj`` with the selector."""
with self._lock:
return self._selector.register(fileobj, events, data)
def unregister(self, fileobj):
"""Unregister ``fileobj`` from the selector."""
with self._lock:
return self._selector.unregister(fileobj)
def select(self, timeout=None):
"""Return socket fd and data pairs from selectors.select call.
Returns entries ready to read in the form:
(socket_file_descriptor, connection)
"""
return (
(key.fd, key.data)
for key, _ in self._selector.select(timeout=timeout)
)
def close(self):
"""Close the selector."""
with self._lock:
self._selector.close()
class ConnectionManager:
"""Class which manages HTTPConnection objects.
This is for connections which are being kept-alive for follow-up requests.
"""
def __init__(self, server):
"""Initialize ConnectionManager object.
Args:
server (cheroot.server.HTTPServer): web server object
that uses this ConnectionManager instance.
"""
self._serving = False
self._stop_requested = False
self.server = server
self._selector = _ThreadsafeSelector()
self._selector.register(
server.socket.fileno(),
selectors.EVENT_READ, data=server,
)
def put(self, conn):
"""Put idle connection into the ConnectionManager to be managed.
:param conn: HTTP connection to be managed
:type conn: cheroot.server.HTTPConnection
"""
conn.last_used = time.time()
# if this conn doesn't have any more data waiting to be read,
# register it with the selector.
if conn.rfile.has_data():
self.server.process_conn(conn)
else:
self._selector.register(
conn.socket.fileno(), selectors.EVENT_READ, data=conn,
)
def _expire(self, threshold):
r"""Expire least recently used connections.
:param threshold: Connections that have not been used within this \
duration (in seconds), are considered expired and \
are closed and removed.
:type threshold: float
This should be called periodically.
"""
# find any connections still registered with the selector
# that have not been active recently enough.
timed_out_connections = [
(sock_fd, conn)
for (sock_fd, conn) in self._selector.connections
if conn != self.server and conn.last_used < threshold
]
for sock_fd, conn in timed_out_connections:
self._selector.unregister(sock_fd)
conn.close()
def stop(self):
"""Stop the selector loop in run() synchronously.
May take up to half a second.
"""
self._stop_requested = True
while self._serving:
time.sleep(0.01)
def run(self, expiration_interval):
"""Run the connections selector indefinitely.
Args:
expiration_interval (float): Interval, in seconds, at which
connections will be checked for expiration.
Connections that are ready to process are submitted via
self.server.process_conn()
Connections submitted for processing must be `put()`
back if they should be examined again for another request.
Can be shut down by calling `stop()`.
"""
self._serving = True
try:
self._run(expiration_interval)
finally:
self._serving = False
def _run(self, expiration_interval):
r"""Run connection handler loop until stop was requested.
:param expiration_interval: Interval, in seconds, at which \
connections will be checked for \
expiration.
:type expiration_interval: float
Use ``expiration_interval`` as ``select()`` timeout
to assure expired connections are closed in time.
On Windows cap the timeout to 0.05 seconds
as ``select()`` does not return when a socket is ready.
"""
last_expiration_check = time.time()
if IS_WINDOWS:
# 0.05 seconds are used as an empirically obtained balance between
# max connection delay and idle system load. Benchmarks show a
# mean processing time per connection of ~0.03 seconds on Linux
# and with 0.01 seconds timeout on Windows:
# https://github.com/cherrypy/cheroot/pull/352
# While this highly depends on system and hardware, 0.05 seconds
# max delay should hence usually not significantly increase the
# mean time/delay per connection, but significantly reduce idle
# system load by reducing socket loops to 1/5 with 0.01 seconds.
select_timeout = min(expiration_interval, 0.05)
else:
select_timeout = expiration_interval
while not self._stop_requested:
try:
active_list = self._selector.select(timeout=select_timeout)
except OSError:
self._remove_invalid_sockets()
continue
for (sock_fd, conn) in active_list:
if conn is self.server:
# New connection
new_conn = self._from_server_socket(self.server.socket)
if new_conn is not None:
self.server.process_conn(new_conn)
else:
# unregister connection from the selector until the server
# has read from it and returned it via put()
self._selector.unregister(sock_fd)
self.server.process_conn(conn)
now = time.time()
if (now - last_expiration_check) > expiration_interval:
self._expire(threshold=now - self.server.timeout)
last_expiration_check = now
def _remove_invalid_sockets(self):
"""Clean up the resources of any broken connections.
This method attempts to detect any connections in an invalid state,
unregisters them from the selector and closes the file descriptors of
the corresponding network sockets where possible.
"""
invalid_conns = []
for sock_fd, conn in self._selector.connections:
if conn is self.server:
continue
try:
os.fstat(sock_fd)
except OSError:
invalid_conns.append((sock_fd, conn))
for sock_fd, conn in invalid_conns:
self._selector.unregister(sock_fd)
# One of the reason on why a socket could cause an error
# is that the socket is already closed, ignore the
# socket error if we try to close it at this point.
with suppress(OSError):
conn.close()
def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
try:
s, addr = server_socket.accept()
if self.server.stats['Enabled']:
self.server.stats['Accepts'] += 1
prevent_socket_inheritance(s)
if hasattr(s, 'settimeout'):
s.settimeout(self.server.timeout)
mf = MakeFile
ssl_env = {}
# if ssl cert and key are set, we try to be a secure HTTP server
if self.server.ssl_adapter is not None:
try:
s, ssl_env = self.server.ssl_adapter.wrap(s)
except errors.NoSSLError:
msg = (
'The client sent a plain HTTP request, but '
'this server only speaks HTTPS on this port.'
)
buf = [
'%s 400 Bad Request\r\n' % self.server.protocol,
'Content-Length: %s\r\n' % len(msg),
'Content-Type: text/plain\r\n\r\n',
msg,
]
wfile = mf(s, 'wb', io.DEFAULT_BUFFER_SIZE)
try:
wfile.write(''.join(buf).encode('ISO-8859-1'))
except OSError as ex:
if ex.args[0] not in errors.socket_errors_to_ignore:
raise
return
if not s:
return
mf = self.server.ssl_adapter.makefile
# Re-apply our timeout since we may have a new socket object
if hasattr(s, 'settimeout'):
s.settimeout(self.server.timeout)
conn = self.server.ConnectionClass(self.server, s, mf)
if not isinstance(self.server.bind_addr, (str, bytes)):
# optional values
# Until we do DNS lookups, omit REMOTE_HOST
if addr is None: # sometimes this can happen
# figure out if AF_INET or AF_INET6.
if len(s.getsockname()) == 2:
# AF_INET
addr = ('0.0.0.0', 0)
else:
# AF_INET6
addr = ('::', 0)
conn.remote_addr = addr[0]
conn.remote_port = addr[1]
conn.ssl_env = ssl_env
return conn
except socket.timeout:
# The only reason for the timeout in start() is so we can
# notice keyboard interrupts on Win32, which don't interrupt
# accept() by default
return
except OSError as ex:
if self.server.stats['Enabled']:
self.server.stats['Socket Errors'] += 1
if ex.args[0] in errors.socket_error_eintr:
# I *think* this is right. EINTR should occur when a signal
# is received during the accept() call; all docs say retry
# the call, and I *think* I'm reading it right that Python
# will then go ahead and poll for and handle the signal
# elsewhere. See
# https://github.com/cherrypy/cherrypy/issues/707.
return
if ex.args[0] in errors.socket_errors_nonblocking:
# Just try again. See
# https://github.com/cherrypy/cherrypy/issues/479.
return
if ex.args[0] in errors.socket_errors_to_ignore:
# Our socket was closed.
# See https://github.com/cherrypy/cherrypy/issues/686.
return
raise
def close(self):
"""Close all monitored connections."""
for (_, conn) in self._selector.connections:
if conn is not self.server: # server closes its own socket
conn.close()
self._selector.close()
@property
def _num_connections(self):
"""Return the current number of connections.
Includes all connections registered with the selector,
minus one for the server socket, which is always registered
with the selector.
"""
return len(self._selector) - 1
@property
def can_add_keepalive_connection(self):
"""Flag whether it is allowed to add a new keep-alive connection."""
ka_limit = self.server.keep_alive_conn_limit
return ka_limit is None or self._num_connections < ka_limit

View File

@ -0,0 +1,23 @@
from typing import Any
def prevent_socket_inheritance(sock) -> None: ...
class _ThreadsafeSelector:
def __init__(self) -> None: ...
def __len__(self): ...
@property
def connections(self) -> None: ...
def register(self, fileobj, events, data: Any | None = ...): ...
def unregister(self, fileobj): ...
def select(self, timeout: Any | None = ...): ...
def close(self) -> None: ...
class ConnectionManager:
server: Any
def __init__(self, server) -> None: ...
def put(self, conn) -> None: ...
def stop(self) -> None: ...
def run(self, expiration_interval) -> None: ...
def close(self) -> None: ...
@property
def can_add_keepalive_connection(self): ...

View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""Collection of exceptions raised and/or processed by Cheroot."""
import errno
import sys
class MaxSizeExceeded(Exception):
"""Exception raised when a client sends more data then allowed under limit.
Depends on ``request.body.maxbytes`` config option if used within CherryPy.
"""
class NoSSLError(Exception):
"""Exception raised when a client speaks HTTP to an HTTPS socket."""
class FatalSSLAlert(Exception):
"""Exception raised when the SSL implementation signals a fatal alert."""
def plat_specific_errors(*errnames):
"""Return error numbers for all errors in ``errnames`` on this platform.
The :py:mod:`errno` module contains different global constants
depending on the specific platform (OS). This function will return
the list of numeric values for a given list of potential names.
"""
missing_attr = {None}
unique_nums = {getattr(errno, k, None) for k in errnames}
return list(unique_nums - missing_attr)
socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR')
socket_errors_to_ignore = plat_specific_errors(
'EPIPE',
'EBADF', 'WSAEBADF',
'ENOTSOCK', 'WSAENOTSOCK',
'ETIMEDOUT', 'WSAETIMEDOUT',
'ECONNREFUSED', 'WSAECONNREFUSED',
'ECONNRESET', 'WSAECONNRESET',
'ECONNABORTED', 'WSAECONNABORTED',
'ENETRESET', 'WSAENETRESET',
'EHOSTDOWN', 'EHOSTUNREACH',
)
socket_errors_to_ignore.append('timed out')
socket_errors_to_ignore.append('The read operation timed out')
socket_errors_nonblocking = plat_specific_errors(
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK',
)
if sys.platform == 'darwin':
socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE'))
socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE'))
acceptable_sock_shutdown_error_codes = {
errno.ENOTCONN,
errno.EPIPE, errno.ESHUTDOWN, # corresponds to BrokenPipeError in Python 3
errno.ECONNRESET, # corresponds to ConnectionResetError in Python 3
}
"""Errors that may happen during the connection close sequence.
* ENOTCONN client is no longer connected
* EPIPE write on a pipe while the other end has been closed
* ESHUTDOWN write on a socket which has been shutdown for writing
* ECONNRESET connection is reset by the peer, we received a TCP RST packet
Refs:
* https://github.com/cherrypy/cheroot/issues/341#issuecomment-735884889
* https://bugs.python.org/issue30319
* https://bugs.python.org/issue30329
* https://github.com/python/cpython/commit/83a2c28
* https://github.com/python/cpython/blob/c39b52f/Lib/poplib.py#L297-L302
* https://docs.microsoft.com/windows/win32/api/winsock/nf-winsock-shutdown
"""
acceptable_sock_shutdown_exceptions = (BrokenPipeError, ConnectionResetError)

View File

@ -0,0 +1,13 @@
from typing import List, Set, Tuple, Type
class MaxSizeExceeded(Exception): ...
class NoSSLError(Exception): ...
class FatalSSLAlert(Exception): ...
def plat_specific_errors(*errnames: str) -> List[int]: ...
socket_error_eintr: List[int]
socket_errors_to_ignore: List[int]
socket_errors_nonblocking: List[int]
acceptable_sock_shutdown_error_codes: Set[int]
acceptable_sock_shutdown_exceptions: Tuple[Type[Exception], ...]

View File

@ -0,0 +1,76 @@
"""Socket file object."""
import socket
# prefer slower Python-based io module
import _pyio as io
# Write only 16K at a time to sockets
SOCK_WRITE_BLOCKSIZE = 16384
class BufferedWriter(io.BufferedWriter):
"""Faux file object attached to a socket object."""
def write(self, b):
"""Write bytes to buffer."""
self._checkClosed()
if isinstance(b, str):
raise TypeError("can't write str to binary stream")
with self._write_lock:
self._write_buf.extend(b)
self._flush_unlocked()
return len(b)
def _flush_unlocked(self):
self._checkClosed('flush of closed file')
while self._write_buf:
try:
# ssl sockets only except 'bytes', not bytearrays
# so perhaps we should conditionally wrap this for perf?
n = self.raw.write(bytes(self._write_buf))
except io.BlockingIOError as e:
n = e.characters_written
del self._write_buf[:n]
class StreamReader(io.BufferedReader):
"""Socket stream reader."""
def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
"""Initialize socket stream reader."""
super().__init__(socket.SocketIO(sock, mode), bufsize)
self.bytes_read = 0
def read(self, *args, **kwargs):
"""Capture bytes read."""
val = super().read(*args, **kwargs)
self.bytes_read += len(val)
return val
def has_data(self):
"""Return true if there is buffered data to read."""
return len(self._read_buf) > self._read_pos
class StreamWriter(BufferedWriter):
"""Socket stream writer."""
def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE):
"""Initialize socket stream writer."""
super().__init__(socket.SocketIO(sock, mode), bufsize)
self.bytes_written = 0
def write(self, val, *args, **kwargs):
"""Capture bytes written."""
res = super().write(val, *args, **kwargs)
self.bytes_written += len(val)
return res
def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
"""File object attached to a socket object."""
cls = StreamReader if 'r' in mode else StreamWriter
return cls(sock, mode, bufsize)

View File

@ -0,0 +1,19 @@
import io
SOCK_WRITE_BLOCKSIZE: int
class BufferedWriter(io.BufferedWriter):
def write(self, b): ...
class StreamReader(io.BufferedReader):
bytes_read: int
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
def read(self, *args, **kwargs): ...
def has_data(self): ...
class StreamWriter(BufferedWriter):
bytes_written: int
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
def write(self, val, *args, **kwargs): ...
def MakeFile(sock, mode: str = ..., bufsize=...): ...

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,175 @@
from typing import Any
class HeaderReader:
def __call__(self, rfile, hdict: Any | None = ...): ...
class DropUnderscoreHeaderReader(HeaderReader): ...
class SizeCheckWrapper:
rfile: Any
maxlen: Any
bytes_read: int
def __init__(self, rfile, maxlen) -> None: ...
def read(self, size: Any | None = ...): ...
def readline(self, size: Any | None = ...): ...
def readlines(self, sizehint: int = ...): ...
def close(self) -> None: ...
def __iter__(self): ...
def __next__(self): ...
next: Any
class KnownLengthRFile:
rfile: Any
remaining: Any
def __init__(self, rfile, content_length) -> None: ...
def read(self, size: Any | None = ...): ...
def readline(self, size: Any | None = ...): ...
def readlines(self, sizehint: int = ...): ...
def close(self) -> None: ...
def __iter__(self): ...
def __next__(self): ...
next: Any
class ChunkedRFile:
rfile: Any
maxlen: Any
bytes_read: int
buffer: Any
bufsize: Any
closed: bool
def __init__(self, rfile, maxlen, bufsize: int = ...) -> None: ...
def read(self, size: Any | None = ...): ...
def readline(self, size: Any | None = ...): ...
def readlines(self, sizehint: int = ...): ...
def read_trailer_lines(self) -> None: ...
def close(self) -> None: ...
class HTTPRequest:
server: Any
conn: Any
inheaders: Any
outheaders: Any
ready: bool
close_connection: bool
chunked_write: bool
header_reader: Any
started_request: bool
scheme: bytes
response_protocol: str
status: str
sent_headers: bool
chunked_read: bool
proxy_mode: Any
strict_mode: Any
def __init__(self, server, conn, proxy_mode: bool = ..., strict_mode: bool = ...) -> None: ...
rfile: Any
def parse_request(self) -> None: ...
uri: Any
method: Any
authority: Any
path: Any
qs: Any
request_protocol: Any
def read_request_line(self): ...
def read_request_headers(self): ...
def respond(self) -> None: ...
def simple_response(self, status, msg: str = ...) -> None: ...
def ensure_headers_sent(self) -> None: ...
def write(self, chunk) -> None: ...
def send_headers(self) -> None: ...
class HTTPConnection:
remote_addr: Any
remote_port: Any
ssl_env: Any
rbufsize: Any
wbufsize: Any
RequestHandlerClass: Any
peercreds_enabled: bool
peercreds_resolve_enabled: bool
last_used: Any
server: Any
socket: Any
rfile: Any
wfile: Any
requests_seen: int
def __init__(self, server, sock, makefile=...) -> None: ...
def communicate(self): ...
linger: bool
def close(self) -> None: ...
def get_peer_creds(self): ...
@property
def peer_pid(self): ...
@property
def peer_uid(self): ...
@property
def peer_gid(self): ...
def resolve_peer_creds(self): ...
@property
def peer_user(self): ...
@property
def peer_group(self): ...
class HTTPServer:
gateway: Any
minthreads: Any
maxthreads: Any
server_name: Any
protocol: str
request_queue_size: int
shutdown_timeout: int
timeout: int
expiration_interval: float
version: Any
software: Any
ready: bool
max_request_header_size: int
max_request_body_size: int
nodelay: bool
ConnectionClass: Any
ssl_adapter: Any
peercreds_enabled: bool
peercreds_resolve_enabled: bool
reuse_port: bool
keep_alive_conn_limit: int
requests: Any
def __init__(self, bind_addr, gateway, minthreads: int = ..., maxthreads: int = ..., server_name: Any | None = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ..., reuse_port: bool = ...) -> None: ...
stats: Any
def clear_stats(self): ...
def runtime(self): ...
@property
def bind_addr(self): ...
@bind_addr.setter
def bind_addr(self, value) -> None: ...
def safe_start(self) -> None: ...
socket: Any
def prepare(self) -> None: ...
def serve(self) -> None: ...
def start(self) -> None: ...
@property
def can_add_keepalive_connection(self): ...
def put_conn(self, conn) -> None: ...
def error_log(self, msg: str = ..., level: int = ..., traceback: bool = ...) -> None: ...
def bind(self, family, type, proto: int = ...): ...
def bind_unix_socket(self, bind_addr): ...
@staticmethod
def _make_socket_reusable(socket_, bind_addr) -> None: ...
@classmethod
def prepare_socket(cls, bind_addr, family, type, proto, nodelay, ssl_adapter, reuse_port: bool = ...): ...
@staticmethod
def bind_socket(socket_, bind_addr): ...
@staticmethod
def resolve_real_bind_addr(socket_): ...
def process_conn(self, conn) -> None: ...
@property
def interrupt(self): ...
@interrupt.setter
def interrupt(self, interrupt) -> None: ...
def stop(self) -> None: ...
class Gateway:
req: Any
def __init__(self, req) -> None: ...
def respond(self) -> None: ...
def get_ssl_adapter_class(name: str = ...): ...

View File

@ -0,0 +1,46 @@
"""Implementation of the SSL adapter base interface."""
from abc import ABCMeta, abstractmethod
class Adapter(metaclass=ABCMeta):
"""Base class for SSL driver library adapters.
Required methods:
* ``wrap(sock) -> (wrapped socket, ssl environ dict)``
* ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
socket file object``
"""
@abstractmethod
def __init__(
self, certificate, private_key, certificate_chain=None,
ciphers=None,
):
"""Set up certificates, private key ciphers and reset context."""
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
self.ciphers = ciphers
self.context = None
@abstractmethod
def bind(self, sock):
"""Wrap and return the given socket."""
return sock
@abstractmethod
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
raise NotImplementedError # pragma: no cover
@abstractmethod
def get_environ(self):
"""Return WSGI environ entries to be merged into each request."""
raise NotImplementedError # pragma: no cover
@abstractmethod
def makefile(self, sock, mode='r', bufsize=-1):
"""Return socket file object."""
raise NotImplementedError # pragma: no cover

View File

@ -0,0 +1,19 @@
from abc import abstractmethod, ABCMeta
from typing import Any
class Adapter(metaclass=ABCMeta):
certificate: Any
private_key: Any
certificate_chain: Any
ciphers: Any
context: Any
@abstractmethod
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...): ...
@abstractmethod
def bind(self, sock): ...
@abstractmethod
def wrap(self, sock): ...
@abstractmethod
def get_environ(self): ...
@abstractmethod
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...

View File

@ -0,0 +1,478 @@
"""
A library for integrating Python's builtin :py:mod:`ssl` library with Cheroot.
The :py:mod:`ssl` module must be importable for SSL functionality.
To use this module, set ``HTTPServer.ssl_adapter`` to an instance of
``BuiltinSSLAdapter``.
"""
import socket
import sys
import threading
from contextlib import suppress
try:
import ssl
except ImportError:
ssl = None
try:
from _pyio import DEFAULT_BUFFER_SIZE
except ImportError:
try:
from io import DEFAULT_BUFFER_SIZE
except ImportError:
DEFAULT_BUFFER_SIZE = -1
from . import Adapter
from .. import errors
from .._compat import IS_ABOVE_OPENSSL10
from ..makefile import StreamReader, StreamWriter
from ..server import HTTPServer
generic_socket_error = OSError
def _assert_ssl_exc_contains(exc, *msgs):
"""Check whether SSL exception contains either of messages provided."""
if len(msgs) < 1:
raise TypeError(
'_assert_ssl_exc_contains() requires '
'at least one message to be passed.',
)
err_msg_lower = str(exc).lower()
return any(m.lower() in err_msg_lower for m in msgs)
def _loopback_for_cert_thread(context, server):
"""Wrap a socket in ssl and perform the server-side handshake."""
# As we only care about parsing the certificate, the failure of
# which will cause an exception in ``_loopback_for_cert``,
# we can safely ignore connection and ssl related exceptions. Ref:
# https://github.com/cherrypy/cheroot/issues/302#issuecomment-662592030
with suppress(ssl.SSLError, OSError):
with context.wrap_socket(
server, do_handshake_on_connect=True, server_side=True,
) as ssl_sock:
# in TLS 1.3 (Python 3.7+, OpenSSL 1.1.1+), the server
# sends the client session tickets that can be used to
# resume the TLS session on a new connection without
# performing the full handshake again. session tickets are
# sent as a post-handshake message at some _unspecified_
# time and thus a successful connection may be closed
# without the client having received the tickets.
# Unfortunately, on Windows (Python 3.8+), this is treated
# as an incomplete handshake on the server side and a
# ``ConnectionAbortedError`` is raised.
# TLS 1.3 support is still incomplete in Python 3.8;
# there is no way for the client to wait for tickets.
# While not necessary for retrieving the parsed certificate,
# we send a tiny bit of data over the connection in an
# attempt to give the server a chance to send the session
# tickets and close the connection cleanly.
# Note that, as this is essentially a race condition,
# the error may still occur ocasionally.
ssl_sock.send(b'0000')
def _loopback_for_cert(certificate, private_key, certificate_chain):
"""Create a loopback connection to parse a cert with a private key."""
context = ssl.create_default_context(cafile=certificate_chain)
context.load_cert_chain(certificate, private_key)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Python 3+ Unix, Python 3.5+ Windows
client, server = socket.socketpair()
try:
# `wrap_socket` will block until the ssl handshake is complete.
# it must be called on both ends at the same time -> thread
# openssl will cache the peer's cert during a successful handshake
# and return it via `getpeercert` even after the socket is closed.
# when `close` is called, the SSL shutdown notice will be sent
# and then python will wait to receive the corollary shutdown.
thread = threading.Thread(
target=_loopback_for_cert_thread, args=(context, server),
)
try:
thread.start()
with context.wrap_socket(
client, do_handshake_on_connect=True,
server_side=False,
) as ssl_sock:
ssl_sock.recv(4)
return ssl_sock.getpeercert()
finally:
thread.join()
finally:
client.close()
server.close()
def _parse_cert(certificate, private_key, certificate_chain):
"""Parse a certificate."""
# loopback_for_cert uses socket.socketpair which was only
# introduced in Python 3.0 for *nix and 3.5 for Windows
# and requires OS support (AttributeError, OSError)
# it also requires a private key either in its own file
# or combined with the cert (SSLError)
with suppress(AttributeError, ssl.SSLError, OSError):
return _loopback_for_cert(certificate, private_key, certificate_chain)
# KLUDGE: using an undocumented, private, test method to parse a cert
# unfortunately, it is the only built-in way without a connection
# as a private, undocumented method, it may change at any time
# so be tolerant of *any* possible errors it may raise
with suppress(Exception):
return ssl._ssl._test_decode_cert(certificate)
return {}
def _sni_callback(sock, sni, context):
"""Handle the SNI callback to tag the socket with the SNI."""
sock.sni = sni
# return None to allow the TLS negotiation to continue
class BuiltinSSLAdapter(Adapter):
"""Wrapper for integrating Python's builtin :py:mod:`ssl` with Cheroot."""
certificate = None
"""The file name of the server SSL certificate."""
private_key = None
"""The file name of the server's private key file."""
certificate_chain = None
"""The file name of the certificate chain file."""
ciphers = None
"""The ciphers list of SSL."""
# from mod_ssl/pkg.sslmod/ssl_engine_vars.c ssl_var_lookup_ssl_cert
CERT_KEY_TO_ENV = {
'version': 'M_VERSION',
'serialNumber': 'M_SERIAL',
'notBefore': 'V_START',
'notAfter': 'V_END',
'subject': 'S_DN',
'issuer': 'I_DN',
'subjectAltName': 'SAN',
# not parsed by the Python standard library
# - A_SIG
# - A_KEY
# not provided by mod_ssl
# - OCSP
# - caIssuers
# - crlDistributionPoints
}
# from mod_ssl/pkg.sslmod/ssl_engine_vars.c ssl_var_lookup_ssl_cert_dn_rec
CERT_KEY_TO_LDAP_CODE = {
'countryName': 'C',
'stateOrProvinceName': 'ST',
# NOTE: mod_ssl also provides 'stateOrProvinceName' as 'SP'
# for compatibility with SSLeay
'localityName': 'L',
'organizationName': 'O',
'organizationalUnitName': 'OU',
'commonName': 'CN',
'title': 'T',
'initials': 'I',
'givenName': 'G',
'surname': 'S',
'description': 'D',
'userid': 'UID',
'emailAddress': 'Email',
# not provided by mod_ssl
# - dnQualifier: DNQ
# - domainComponent: DC
# - postalCode: PC
# - streetAddress: STREET
# - serialNumber
# - generationQualifier
# - pseudonym
# - jurisdictionCountryName
# - jurisdictionLocalityName
# - jurisdictionStateOrProvince
# - businessCategory
}
def __init__(
self, certificate, private_key, certificate_chain=None,
ciphers=None,
):
"""Set up context in addition to base class properties if available."""
if ssl is None:
raise ImportError('You must install the ssl module to use HTTPS.')
super(BuiltinSSLAdapter, self).__init__(
certificate, private_key, certificate_chain, ciphers,
)
self.context = ssl.create_default_context(
purpose=ssl.Purpose.CLIENT_AUTH,
cafile=certificate_chain,
)
self.context.load_cert_chain(certificate, private_key)
if self.ciphers is not None:
self.context.set_ciphers(ciphers)
self._server_env = self._make_env_cert_dict(
'SSL_SERVER',
_parse_cert(certificate, private_key, self.certificate_chain),
)
if not self._server_env:
return
cert = None
with open(certificate, mode='rt') as f:
cert = f.read()
# strip off any keys by only taking the first certificate
cert_start = cert.find(ssl.PEM_HEADER)
if cert_start == -1:
return
cert_end = cert.find(ssl.PEM_FOOTER, cert_start)
if cert_end == -1:
return
cert_end += len(ssl.PEM_FOOTER)
self._server_env['SSL_SERVER_CERT'] = cert[cert_start:cert_end]
@property
def context(self):
""":py:class:`~ssl.SSLContext` that will be used to wrap sockets."""
return self._context
@context.setter
def context(self, context):
"""Set the ssl ``context`` to use."""
self._context = context
# Python 3.7+
# if a context is provided via `cherrypy.config.update` then
# `self.context` will be set after `__init__`
# use a property to intercept it to add an SNI callback
# but don't override the user's callback
# TODO: chain callbacks
with suppress(AttributeError):
if ssl.HAS_SNI and context.sni_callback is None:
context.sni_callback = _sni_callback
def bind(self, sock):
"""Wrap and return the given socket."""
return super(BuiltinSSLAdapter, self).bind(sock)
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
EMPTY_RESULT = None, {}
try:
s = self.context.wrap_socket(
sock, do_handshake_on_connect=True, server_side=True,
)
except ssl.SSLError as ex:
if ex.errno == ssl.SSL_ERROR_EOF:
# This is almost certainly due to the cherrypy engine
# 'pinging' the socket to assert it's connectable;
# the 'ping' isn't SSL.
return EMPTY_RESULT
elif ex.errno == ssl.SSL_ERROR_SSL:
if _assert_ssl_exc_contains(ex, 'http request'):
# The client is speaking HTTP to an HTTPS server.
raise errors.NoSSLError
# Check if it's one of the known errors
# Errors that are caught by PyOpenSSL, but thrown by
# built-in ssl
_block_errors = (
'unknown protocol', 'unknown ca', 'unknown_ca',
'unknown error',
'https proxy request', 'inappropriate fallback',
'wrong version number',
'no shared cipher', 'certificate unknown',
'ccs received early',
'certificate verify failed', # client cert w/o trusted CA
'version too low', # caused by SSL3 connections
'unsupported protocol', # caused by TLS1 connections
)
if _assert_ssl_exc_contains(ex, *_block_errors):
# Accepted error, let's pass
return EMPTY_RESULT
elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'):
# This error is thrown by builtin SSL after a timeout
# when client is speaking HTTP to an HTTPS server.
# The connection can safely be dropped.
return EMPTY_RESULT
raise
except generic_socket_error as exc:
"""It is unclear why exactly this happens.
It's reproducible only with openssl>1.0 and stdlib
:py:mod:`ssl` wrapper.
In CherryPy it's triggered by Checker plugin, which connects
to the app listening to the socket port in TLS mode via plain
HTTP during startup (from the same process).
Ref: https://github.com/cherrypy/cherrypy/issues/1618
"""
is_error0 = exc.args == (0, 'Error')
if is_error0 and IS_ABOVE_OPENSSL10:
return EMPTY_RESULT
raise
return s, self.get_environ(s)
def get_environ(self, sock):
"""Create WSGI environ entries to be merged into each request."""
cipher = sock.cipher()
ssl_environ = {
'wsgi.url_scheme': 'https',
'HTTPS': 'on',
'SSL_PROTOCOL': cipher[1],
'SSL_CIPHER': cipher[0],
'SSL_CIPHER_EXPORT': '',
'SSL_CIPHER_USEKEYSIZE': cipher[2],
'SSL_VERSION_INTERFACE': '%s Python/%s' % (
HTTPServer.version, sys.version,
),
'SSL_VERSION_LIBRARY': ssl.OPENSSL_VERSION,
'SSL_CLIENT_VERIFY': 'NONE',
# 'NONE' - client did not provide a cert (overriden below)
}
# Python 3.3+
with suppress(AttributeError):
compression = sock.compression()
if compression is not None:
ssl_environ['SSL_COMPRESS_METHOD'] = compression
# Python 3.6+
with suppress(AttributeError):
ssl_environ['SSL_SESSION_ID'] = sock.session.id.hex()
with suppress(AttributeError):
target_cipher = cipher[:2]
for cip in sock.context.get_ciphers():
if target_cipher == (cip['name'], cip['protocol']):
ssl_environ['SSL_CIPHER_ALGKEYSIZE'] = cip['alg_bits']
break
# Python 3.7+ sni_callback
with suppress(AttributeError):
ssl_environ['SSL_TLS_SNI'] = sock.sni
if self.context and self.context.verify_mode != ssl.CERT_NONE:
client_cert = sock.getpeercert()
if client_cert:
# builtin ssl **ALWAYS** validates client certificates
# and terminates the connection on failure
ssl_environ['SSL_CLIENT_VERIFY'] = 'SUCCESS'
ssl_environ.update(
self._make_env_cert_dict('SSL_CLIENT', client_cert),
)
ssl_environ['SSL_CLIENT_CERT'] = ssl.DER_cert_to_PEM_cert(
sock.getpeercert(binary_form=True),
).strip()
ssl_environ.update(self._server_env)
# not supplied by the Python standard library (as of 3.8)
# - SSL_SESSION_RESUMED
# - SSL_SECURE_RENEG
# - SSL_CLIENT_CERT_CHAIN_n
# - SRP_USER
# - SRP_USERINFO
return ssl_environ
def _make_env_cert_dict(self, env_prefix, parsed_cert):
"""Return a dict of WSGI environment variables for a certificate.
E.g. SSL_CLIENT_M_VERSION, SSL_CLIENT_M_SERIAL, etc.
See https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
"""
if not parsed_cert:
return {}
env = {}
for cert_key, env_var in self.CERT_KEY_TO_ENV.items():
key = '%s_%s' % (env_prefix, env_var)
value = parsed_cert.get(cert_key)
if env_var == 'SAN':
env.update(self._make_env_san_dict(key, value))
elif env_var.endswith('_DN'):
env.update(self._make_env_dn_dict(key, value))
else:
env[key] = str(value)
# mod_ssl 2.1+; Python 3.2+
# number of days until the certificate expires
if 'notBefore' in parsed_cert:
remain = ssl.cert_time_to_seconds(parsed_cert['notAfter'])
remain -= ssl.cert_time_to_seconds(parsed_cert['notBefore'])
remain /= 60 * 60 * 24
env['%s_V_REMAIN' % (env_prefix,)] = str(int(remain))
return env
def _make_env_san_dict(self, env_prefix, cert_value):
"""Return a dict of WSGI environment variables for a certificate DN.
E.g. SSL_CLIENT_SAN_Email_0, SSL_CLIENT_SAN_DNS_0, etc.
See SSL_CLIENT_SAN_* at
https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
"""
if not cert_value:
return {}
env = {}
dns_count = 0
email_count = 0
for attr_name, val in cert_value:
if attr_name == 'DNS':
env['%s_DNS_%i' % (env_prefix, dns_count)] = val
dns_count += 1
elif attr_name == 'Email':
env['%s_Email_%i' % (env_prefix, email_count)] = val
email_count += 1
# other mod_ssl SAN vars:
# - SAN_OTHER_msUPN_n
return env
def _make_env_dn_dict(self, env_prefix, cert_value):
"""Return a dict of WSGI environment variables for a certificate DN.
E.g. SSL_CLIENT_S_DN_CN, SSL_CLIENT_S_DN_C, etc.
See SSL_CLIENT_S_DN_x509 at
https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars.
"""
if not cert_value:
return {}
dn = []
dn_attrs = {}
for rdn in cert_value:
for attr_name, val in rdn:
attr_code = self.CERT_KEY_TO_LDAP_CODE.get(attr_name)
dn.append('%s=%s' % (attr_code or attr_name, val))
if not attr_code:
continue
dn_attrs.setdefault(attr_code, [])
dn_attrs[attr_code].append(val)
env = {
env_prefix: ','.join(dn),
}
for attr_code, values in dn_attrs.items():
env['%s_%s' % (env_prefix, attr_code)] = ','.join(values)
if len(values) == 1:
continue
for i, val in enumerate(values):
env['%s_%s_%i' % (env_prefix, attr_code, i)] = val
return env
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
"""Return socket file object."""
cls = StreamReader if 'r' in mode else StreamWriter
return cls(sock, mode, bufsize)

View File

@ -0,0 +1,17 @@
from typing import Any
from . import Adapter
DEFAULT_BUFFER_SIZE: int
class BuiltinSSLAdapter(Adapter):
CERT_KEY_TO_ENV: Any
CERT_KEY_TO_LDAP_CODE: Any
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
@property
def context(self): ...
@context.setter
def context(self, context) -> None: ...
def bind(self, sock): ...
def wrap(self, sock): ...
def get_environ(self, sock): ...
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...

View File

@ -0,0 +1,376 @@
"""
A library for integrating :doc:`pyOpenSSL <pyopenssl:index>` with Cheroot.
The :py:mod:`OpenSSL <pyopenssl:OpenSSL>` module must be importable
for SSL/TLS/HTTPS functionality.
You can obtain it from `here <https://github.com/pyca/pyopenssl>`_.
To use this module, set :py:attr:`HTTPServer.ssl_adapter
<cheroot.server.HTTPServer.ssl_adapter>` to an instance of
:py:class:`ssl.Adapter <cheroot.ssl.Adapter>`.
There are two ways to use :abbr:`TLS (Transport-Level Security)`:
Method One
----------
* :py:attr:`ssl_adapter.context
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.context>`: an instance of
:py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`.
If this is not None, it is assumed to be an :py:class:`SSL.Context
<pyopenssl:OpenSSL.SSL.Context>` instance, and will be passed to
:py:class:`SSL.Connection <pyopenssl:OpenSSL.SSL.Connection>` on bind().
The developer is responsible for forming a valid :py:class:`Context
<pyopenssl:OpenSSL.SSL.Context>` object. This
approach is to be preferred for more flexibility, e.g. if the cert and
key are streams instead of files, or need decryption, or
:py:data:`SSL.SSLv3_METHOD <pyopenssl:OpenSSL.SSL.SSLv3_METHOD>`
is desired instead of the default :py:data:`SSL.SSLv23_METHOD
<pyopenssl:OpenSSL.SSL.SSLv3_METHOD>`, etc. Consult
the :doc:`pyOpenSSL <pyopenssl:api/ssl>` documentation for
complete options.
Method Two (shortcut)
---------------------
* :py:attr:`ssl_adapter.certificate
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.certificate>`: the file name
of the server's TLS certificate.
* :py:attr:`ssl_adapter.private_key
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.private_key>`: the file name
of the server's private key file.
Both are :py:data:`None` by default. If :py:attr:`ssl_adapter.context
<cheroot.ssl.pyopenssl.pyOpenSSLAdapter.context>` is :py:data:`None`,
but ``.private_key`` and ``.certificate`` are both given and valid, they
will be read, and the context will be automatically created from them.
.. spelling::
pyopenssl
"""
import socket
import sys
import threading
import time
try:
import OpenSSL.version
from OpenSSL import SSL
from OpenSSL import crypto
try:
ssl_conn_type = SSL.Connection
except AttributeError:
ssl_conn_type = SSL.ConnectionType
except ImportError:
SSL = None
from . import Adapter
from .. import errors, server as cheroot_server
from ..makefile import StreamReader, StreamWriter
class SSLFileobjectMixin:
"""Base mixin for a TLS socket stream."""
ssl_timeout = 3
ssl_retry = .01
# FIXME:
def _safe_call(self, is_reader, call, *args, **kwargs): # noqa: C901
"""Wrap the given call with TLS error-trapping.
is_reader: if False EOF errors will be raised. If True, EOF errors
will return "" (to emulate normal sockets).
"""
start = time.time()
while True:
try:
return call(*args, **kwargs)
except SSL.WantReadError:
# Sleep and try again. This is dangerous, because it means
# the rest of the stack has no way of differentiating
# between a "new handshake" error and "client dropped".
# Note this isn't an endless loop: there's a timeout below.
# Ref: https://stackoverflow.com/a/5133568/595220
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return b''
errnum = e.args[0]
if is_reader and errnum in errors.socket_errors_to_ignore:
return b''
raise socket.error(errnum)
except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return b''
thirdarg = None
try:
thirdarg = e.args[0][0][2]
except IndexError:
pass
if thirdarg == 'http request':
# The client is talking HTTP to an HTTPS server.
raise errors.NoSSLError()
raise errors.FatalSSLAlert(*e.args)
if time.time() - start > self.ssl_timeout:
raise socket.timeout('timed out')
def recv(self, size):
"""Receive message of a size from the socket."""
return self._safe_call(
True,
super(SSLFileobjectMixin, self).recv,
size,
)
def readline(self, size=-1):
"""Receive message of a size from the socket.
Matches the following interface:
https://docs.python.org/3/library/io.html#io.IOBase.readline
"""
return self._safe_call(
True,
super(SSLFileobjectMixin, self).readline,
size,
)
def sendall(self, *args, **kwargs):
"""Send whole message to the socket."""
return self._safe_call(
False,
super(SSLFileobjectMixin, self).sendall,
*args, **kwargs
)
def send(self, *args, **kwargs):
"""Send some part of message to the socket."""
return self._safe_call(
False,
super(SSLFileobjectMixin, self).send,
*args, **kwargs
)
class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader):
"""SSL file object attached to a socket object."""
class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter):
"""SSL file object attached to a socket object."""
class SSLConnectionProxyMeta:
"""Metaclass for generating a bunch of proxy methods."""
def __new__(mcl, name, bases, nmspc):
"""Attach a list of proxy methods to a new class."""
proxy_methods = (
'get_context', 'pending', 'send', 'write', 'recv', 'read',
'renegotiate', 'bind', 'listen', 'connect', 'accept',
'setblocking', 'fileno', 'close', 'get_cipher_list',
'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
'makefile', 'get_app_data', 'set_app_data', 'state_string',
'sock_shutdown', 'get_peer_certificate', 'want_read',
'want_write', 'set_connect_state', 'set_accept_state',
'connect_ex', 'sendall', 'settimeout', 'gettimeout',
'shutdown',
)
proxy_methods_no_args = (
'shutdown',
)
proxy_props = (
'family',
)
def lock_decorator(method):
"""Create a proxy method for a new class."""
def proxy_wrapper(self, *args):
self._lock.acquire()
try:
new_args = (
args[:] if method not in proxy_methods_no_args else []
)
return getattr(self._ssl_conn, method)(*new_args)
finally:
self._lock.release()
return proxy_wrapper
for m in proxy_methods:
nmspc[m] = lock_decorator(m)
nmspc[m].__name__ = m
def make_property(property_):
"""Create a proxy method for a new class."""
def proxy_prop_wrapper(self):
return getattr(self._ssl_conn, property_)
proxy_prop_wrapper.__name__ = property_
return property(proxy_prop_wrapper)
for p in proxy_props:
nmspc[p] = make_property(p)
# Doesn't work via super() for some reason.
# Falling back to type() instead:
return type(name, bases, nmspc)
class SSLConnection(metaclass=SSLConnectionProxyMeta):
r"""A thread-safe wrapper for an ``SSL.Connection``.
:param tuple args: the arguments to create the wrapped \
:py:class:`SSL.Connection(*args) \
<pyopenssl:OpenSSL.SSL.Connection>`
"""
def __init__(self, *args):
"""Initialize SSLConnection instance."""
self._ssl_conn = SSL.Connection(*args)
self._lock = threading.RLock()
class pyOpenSSLAdapter(Adapter):
"""A wrapper for integrating pyOpenSSL with Cheroot."""
certificate = None
"""The file name of the server's TLS certificate."""
private_key = None
"""The file name of the server's private key file."""
certificate_chain = None
"""Optional. The file name of CA's intermediate certificate bundle.
This is needed for cheaper "chained root" TLS certificates,
and should be left as :py:data:`None` if not required."""
context = None
"""
An instance of :py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`.
"""
ciphers = None
"""The ciphers list of TLS."""
def __init__(
self, certificate, private_key, certificate_chain=None,
ciphers=None,
):
"""Initialize OpenSSL Adapter instance."""
if SSL is None:
raise ImportError('You must install pyOpenSSL to use HTTPS.')
super(pyOpenSSLAdapter, self).__init__(
certificate, private_key, certificate_chain, ciphers,
)
self._environ = None
def bind(self, sock):
"""Wrap and return the given socket."""
if self.context is None:
self.context = self.get_context()
conn = SSLConnection(self.context, sock)
self._environ = self.get_environ()
return conn
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
# pyOpenSSL doesn't perform the handshake until the first read/write
# forcing the handshake to complete tends to result in the connection
# closing so we can't reliably access protocol/client cert for the env
return sock, self._environ.copy()
def get_context(self):
"""Return an ``SSL.Context`` from self attributes.
Ref: :py:class:`SSL.Context <pyopenssl:OpenSSL.SSL.Context>`
"""
# See https://code.activestate.com/recipes/442473/
c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
return c
def get_environ(self):
"""Return WSGI environ entries to be merged into each request."""
ssl_environ = {
'wsgi.url_scheme': 'https',
'HTTPS': 'on',
'SSL_VERSION_INTERFACE': '%s %s/%s Python/%s' % (
cheroot_server.HTTPServer.version,
OpenSSL.version.__title__, OpenSSL.version.__version__,
sys.version,
),
'SSL_VERSION_LIBRARY': SSL.SSLeay_version(
SSL.SSLEAY_VERSION,
).decode(),
}
if self.certificate:
# Server certificate attributes
with open(self.certificate, 'rb') as cert_file:
cert = crypto.load_certificate(
crypto.FILETYPE_PEM, cert_file.read(),
)
ssl_environ.update({
'SSL_SERVER_M_VERSION': cert.get_version(),
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
# 'SSL_SERVER_V_START':
# Validity of server's certificate (start time),
# 'SSL_SERVER_V_END':
# Validity of server's certificate (end time),
})
for prefix, dn in [
('I', cert.get_issuer()),
('S', cert.get_subject()),
]:
# X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
dnstr = str(dn)[18:-2]
wsgikey = 'SSL_SERVER_%s_DN' % prefix
ssl_environ[wsgikey] = dnstr
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
# for any value to contain slashes itself (in a URL).
while dnstr:
pos = dnstr.rfind('=')
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind('/')
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
ssl_environ[wsgikey] = value
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
"""Return socket file object."""
cls = (
SSLFileobjectStreamReader
if 'r' in mode else
SSLFileobjectStreamWriter
)
if SSL and isinstance(sock, ssl_conn_type):
wrapped_socket = cls(sock, mode, bufsize)
wrapped_socket.ssl_timeout = sock.gettimeout()
return wrapped_socket
# This is from past:
# TODO: figure out what it's meant for
else:
return cheroot_server.CP_fileobject(sock, mode, bufsize)

View File

@ -0,0 +1,31 @@
from . import Adapter
from ..makefile import StreamReader, StreamWriter
from OpenSSL import SSL
from typing import Any, Type
ssl_conn_type: Type[SSL.Connection]
class SSLFileobjectMixin:
ssl_timeout: int
ssl_retry: float
def recv(self, size): ...
def readline(self, size: int = ...): ...
def sendall(self, *args, **kwargs): ...
def send(self, *args, **kwargs): ...
class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ... # type:ignore[misc]
class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ... # type:ignore[misc]
class SSLConnectionProxyMeta:
def __new__(mcl, name, bases, nmspc): ...
class SSLConnection:
def __init__(self, *args) -> None: ...
class pyOpenSSLAdapter(Adapter):
def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
def bind(self, sock): ...
def wrap(self, sock): ...
def get_environ(self): ...
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
def get_context(self) -> SSL.Context: ...

View File

@ -0,0 +1 @@
"""Cheroot test suite."""

View File

@ -0,0 +1,43 @@
"""Local pytest plugin.
Contains hooks, which are tightly bound to the Cheroot framework
itself, useless for end-users' app testing.
"""
import pytest
pytest_version = tuple(map(int, pytest.__version__.split('.')))
def pytest_load_initial_conftests(early_config, parser, args):
"""Drop unfilterable warning ignores."""
if pytest_version < (6, 2, 0):
return
# pytest>=6.2.0 under Python 3.8:
# Refs:
# * https://docs.pytest.org/en/stable/usage.html#unraisable
# * https://github.com/pytest-dev/pytest/issues/5299
early_config._inicache['filterwarnings'].extend((
'ignore:Exception in thread CP Server Thread-:'
'pytest.PytestUnhandledThreadExceptionWarning:_pytest.threadexception',
'ignore:Exception in thread Thread-:'
'pytest.PytestUnhandledThreadExceptionWarning:_pytest.threadexception',
'ignore:Exception ignored in. '
'<socket.socket fd=-1, family=AddressFamily.AF_INET, '
'type=SocketKind.SOCK_STREAM, proto=.:'
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
'ignore:Exception ignored in. '
'<socket.socket fd=-1, family=AddressFamily.AF_INET6, '
'type=SocketKind.SOCK_STREAM, proto=.:'
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
'ignore:Exception ignored in. '
'<socket.socket fd=-1, family=AF_INET, '
'type=SocketKind.SOCK_STREAM, proto=.:'
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
'ignore:Exception ignored in. '
'<socket.socket fd=-1, family=AF_INET6, '
'type=SocketKind.SOCK_STREAM, proto=.:'
'pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception',
))

View File

@ -0,0 +1,83 @@
"""Pytest configuration module.
Contains fixtures, which are tightly bound to the Cheroot framework
itself, useless for end-users' app testing.
"""
import threading
import time
import pytest
from .._compat import IS_MACOS, IS_WINDOWS # noqa: WPS436
from ..server import Gateway, HTTPServer
from ..testing import ( # noqa: F401 # pylint: disable=unused-import
native_server, wsgi_server,
)
from ..testing import get_server_client
@pytest.fixture
def http_request_timeout():
"""Return a common HTTP request timeout for tests with queries."""
computed_timeout = 0.1
if IS_MACOS:
computed_timeout *= 2
if IS_WINDOWS:
computed_timeout *= 10
return computed_timeout
@pytest.fixture
# pylint: disable=redefined-outer-name
def wsgi_server_client(wsgi_server): # noqa: F811
"""Create a test client out of given WSGI server."""
return get_server_client(wsgi_server)
@pytest.fixture
# pylint: disable=redefined-outer-name
def native_server_client(native_server): # noqa: F811
"""Create a test client out of given HTTP server."""
return get_server_client(native_server)
@pytest.fixture
def http_server():
"""Provision a server creator as a fixture."""
def start_srv():
bind_addr = yield
if bind_addr is None:
return
httpserver = make_http_server(bind_addr)
yield httpserver
yield httpserver
srv_creator = iter(start_srv())
next(srv_creator) # pylint: disable=stop-iteration-return
yield srv_creator
try:
while True:
httpserver = next(srv_creator)
if httpserver is not None:
httpserver.stop()
except StopIteration:
pass
def make_http_server(bind_addr):
"""Create and start an HTTP server bound to ``bind_addr``."""
httpserver = HTTPServer(
bind_addr=bind_addr,
gateway=Gateway,
)
threading.Thread(target=httpserver.safe_start).start()
while not httpserver.ready:
time.sleep(0.1)
return httpserver

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