tutorial 3, from one-to-many db to many-to-many db
parent
2ff60e02e9
commit
a755b38fc9
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2010 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,123 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Flask
|
||||||
|
Version: 2.2.2
|
||||||
|
Summary: A simple framework for building complex web applications.
|
||||||
|
Home-page: https://palletsprojects.com/p/flask
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://flask.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://flask.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/flask/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Framework :: Flask
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
Requires-Dist: Werkzeug (>=2.2.2)
|
||||||
|
Requires-Dist: Jinja2 (>=3.0)
|
||||||
|
Requires-Dist: itsdangerous (>=2.0)
|
||||||
|
Requires-Dist: click (>=8.0)
|
||||||
|
Requires-Dist: importlib-metadata (>=3.6.0) ; python_version < "3.10"
|
||||||
|
Provides-Extra: async
|
||||||
|
Requires-Dist: asgiref (>=3.2) ; extra == 'async'
|
||||||
|
Provides-Extra: dotenv
|
||||||
|
Requires-Dist: python-dotenv ; extra == 'dotenv'
|
||||||
|
|
||||||
|
Flask
|
||||||
|
=====
|
||||||
|
|
||||||
|
Flask is a lightweight `WSGI`_ web application framework. It is designed
|
||||||
|
to make getting started quick and easy, with the ability to scale up to
|
||||||
|
complex applications. It began as a simple wrapper around `Werkzeug`_
|
||||||
|
and `Jinja`_ and has become one of the most popular Python web
|
||||||
|
application frameworks.
|
||||||
|
|
||||||
|
Flask offers suggestions, but doesn't enforce any dependencies or
|
||||||
|
project layout. It is up to the developer to choose the tools and
|
||||||
|
libraries they want to use. There are many extensions provided by the
|
||||||
|
community that make adding new functionality easy.
|
||||||
|
|
||||||
|
.. _WSGI: https://wsgi.readthedocs.io/
|
||||||
|
.. _Werkzeug: https://werkzeug.palletsprojects.com/
|
||||||
|
.. _Jinja: https://jinja.palletsprojects.com/
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U Flask
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
A Simple Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# save this as app.py
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def hello():
|
||||||
|
return "Hello, World!"
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ flask run
|
||||||
|
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
|
||||||
|
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
For guidance on setting up a development environment and how to make a
|
||||||
|
contribution to Flask, see the `contributing guidelines`_.
|
||||||
|
|
||||||
|
.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Flask and the libraries
|
||||||
|
it uses. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://flask.palletsprojects.com/
|
||||||
|
- Changes: https://flask.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Flask/
|
||||||
|
- Source Code: https://github.com/pallets/flask/
|
||||||
|
- Issue Tracker: https://github.com/pallets/flask/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/flask/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
@ -0,0 +1,31 @@
|
|||||||
|
../../../bin/flask,sha256=cmw1xCScGvcqcdnwyrltB4q4XB6htSvgihv5EVf2zR8,258
|
||||||
|
Flask-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
Flask-2.2.2.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||||
|
Flask-2.2.2.dist-info/METADATA,sha256=UXiwRLD1johd_tGlYOlOKXkJFIG82ehR3bxqh4XWFwA,3889
|
||||||
|
Flask-2.2.2.dist-info/RECORD,,
|
||||||
|
Flask-2.2.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
Flask-2.2.2.dist-info/entry_points.txt,sha256=s3MqQpduU25y4dq3ftBYD6bMVdVnbMpZP-sUNw0zw0k,41
|
||||||
|
Flask-2.2.2.dist-info/top_level.txt,sha256=dvi65F6AeGWVU0TBpYiC04yM60-FX1gJFkK31IKQr5c,6
|
||||||
|
flask/__init__.py,sha256=Y4mEWqAMxj_Oxq9eYv3tWyN-0nU9yVKBGK_t6BxqvvM,2890
|
||||||
|
flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
||||||
|
flask/app.py,sha256=VfBcGmEVveMcSajkUmDXCEOvAd-2mIBJ355KicvQ4gE,99025
|
||||||
|
flask/blueprints.py,sha256=Jbrt-2jlLiFklC3De9EWBioPtDjHYYbXlTDK9Z7L2nk,26936
|
||||||
|
flask/cli.py,sha256=foLlD8NiIXcxpxMmRQvvlZPbVM8pxOaJG3sa58c9dAA,33486
|
||||||
|
flask/config.py,sha256=IWqHecH4poDxNEUg4U_ZA1CTlL5BKZDX3ofG4UGYyi0,12584
|
||||||
|
flask/ctx.py,sha256=ZOGEWuFjsCIk3vm-C9pLME0e4saeBkeGpr2tTSvemSM,14851
|
||||||
|
flask/debughelpers.py,sha256=_RvAL3TW5lqMJeCVWtTU6rSDJC7jnRaBL6OEkVmooyU,5511
|
||||||
|
flask/globals.py,sha256=1DLZMi8Su-S1gf8zEiR3JPi6VXYIrYqm8C9__Ly66ss,3187
|
||||||
|
flask/helpers.py,sha256=ELq27745jihrdyAP9qY8KENlCVDdnWRWTIn35L9a-UU,25334
|
||||||
|
flask/json/__init__.py,sha256=TOwldHT3_kFaXHlORKi9yCWt7dbPNB0ovdHHQWlSRzY,11175
|
||||||
|
flask/json/provider.py,sha256=jXCNypf11PN4ngQjEt6LnSdCWQ1yHIAkNLHlXQlCB-A,10674
|
||||||
|
flask/json/tag.py,sha256=fys3HBLssWHuMAIJuTcf2K0bCtosePBKXIWASZEEjnU,8857
|
||||||
|
flask/logging.py,sha256=WYng0bLTRS_CJrocGcCLJpibHf1lygHE_pg-KoUIQ4w,2293
|
||||||
|
flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
flask/scaffold.py,sha256=tiQRK-vMY5nucoN6pewXF87GaxrltsCGOgTVsT6wm7s,33443
|
||||||
|
flask/sessions.py,sha256=66oGlE-v9iac-eb54tFN3ILAjJ1FeeuHHWw98UVaoxc,15847
|
||||||
|
flask/signals.py,sha256=H7QwDciK-dtBxinjKpexpglP0E6k0MJILiFWTItfmqU,2136
|
||||||
|
flask/templating.py,sha256=1P4OzvSnA2fsJTYgQT3G4owVKsuOz8XddCiR6jMHGJ0,7419
|
||||||
|
flask/testing.py,sha256=p51f9P7jDc_IDSiZug7jypnfVcxsQrMg3B2tnjlpEFw,10596
|
||||||
|
flask/typing.py,sha256=KgxegTF9v9WvuongeF8LooIvpZPauzGrq9ZXf3gBlYc,2969
|
||||||
|
flask/views.py,sha256=bveWilivkPP-4HB9w_fOusBz6sHNIl0QTqKUFMCltzE,6738
|
||||||
|
flask/wrappers.py,sha256=Wa-bhjNdPPveSHS1dpzD_r-ayZxIYFF1DoWncKOafrk,5695
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
flask = flask.cli:main
|
@ -0,0 +1 @@
|
|||||||
|
flask
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2007 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,113 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Jinja2
|
||||||
|
Version: 3.1.2
|
||||||
|
Summary: A very fast and expressive template engine.
|
||||||
|
Home-page: https://palletsprojects.com/p/jinja/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://jinja.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://jinja.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/jinja/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
Requires-Dist: MarkupSafe (>=2.0)
|
||||||
|
Provides-Extra: i18n
|
||||||
|
Requires-Dist: Babel (>=2.7) ; extra == 'i18n'
|
||||||
|
|
||||||
|
Jinja
|
||||||
|
=====
|
||||||
|
|
||||||
|
Jinja is a fast, expressive, extensible templating engine. Special
|
||||||
|
placeholders in the template allow writing code similar to Python
|
||||||
|
syntax. Then the template is passed data to render the final document.
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
|
||||||
|
- Template inheritance and inclusion.
|
||||||
|
- Define and import macros within templates.
|
||||||
|
- HTML templates can use autoescaping to prevent XSS from untrusted
|
||||||
|
user input.
|
||||||
|
- A sandboxed environment can safely render untrusted templates.
|
||||||
|
- AsyncIO support for generating templates and calling async
|
||||||
|
functions.
|
||||||
|
- I18N support with Babel.
|
||||||
|
- Templates are compiled to optimized Python code just-in-time and
|
||||||
|
cached, or can be compiled ahead-of-time.
|
||||||
|
- Exceptions point to the correct line in templates to make debugging
|
||||||
|
easier.
|
||||||
|
- Extensible filters, tests, functions, and even syntax.
|
||||||
|
|
||||||
|
Jinja's philosophy is that while application logic belongs in Python if
|
||||||
|
possible, it shouldn't make the template designer's job difficult by
|
||||||
|
restricting functionality too much.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U Jinja2
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
In A Nutshell
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Members{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<ul>
|
||||||
|
{% for user in users %}
|
||||||
|
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Jinja and other popular
|
||||||
|
packages. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://jinja.palletsprojects.com/
|
||||||
|
- Changes: https://jinja.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Jinja2/
|
||||||
|
- Source Code: https://github.com/pallets/jinja/
|
||||||
|
- Issue Tracker: https://github.com/pallets/jinja/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/jinja/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
Jinja2-3.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
Jinja2-3.1.2.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475
|
||||||
|
Jinja2-3.1.2.dist-info/METADATA,sha256=PZ6v2SIidMNixR7MRUX9f7ZWsPwtXanknqiZUmRbh4U,3539
|
||||||
|
Jinja2-3.1.2.dist-info/RECORD,,
|
||||||
|
Jinja2-3.1.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
Jinja2-3.1.2.dist-info/entry_points.txt,sha256=zRd62fbqIyfUpsRtU7EVIFyiu1tPwfgO7EvPErnxgTE,59
|
||||||
|
Jinja2-3.1.2.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7
|
||||||
|
jinja2/__init__.py,sha256=8vGduD8ytwgD6GDSqpYc2m3aU-T7PKOAddvVXgGr_Fs,1927
|
||||||
|
jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958
|
||||||
|
jinja2/async_utils.py,sha256=dHlbTeaxFPtAOQEYOGYh_PHcDT0rsDaUJAFDl_0XtTg,2472
|
||||||
|
jinja2/bccache.py,sha256=mhz5xtLxCcHRAa56azOhphIAe19u1we0ojifNMClDio,14061
|
||||||
|
jinja2/compiler.py,sha256=Gs-N8ThJ7OWK4-reKoO8Wh1ZXz95MVphBKNVf75qBr8,72172
|
||||||
|
jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433
|
||||||
|
jinja2/debug.py,sha256=iWJ432RadxJNnaMOPrjIDInz50UEgni3_HKuFXi2vuQ,6299
|
||||||
|
jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267
|
||||||
|
jinja2/environment.py,sha256=6uHIcc7ZblqOMdx_uYNKqRnnwAF0_nzbyeMP9FFtuh4,61349
|
||||||
|
jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071
|
||||||
|
jinja2/ext.py,sha256=ivr3P7LKbddiXDVez20EflcO3q2aHQwz9P_PgWGHVqE,31502
|
||||||
|
jinja2/filters.py,sha256=9js1V-h2RlyW90IhLiBGLM2U-k6SCy2F4BUUMgB3K9Q,53509
|
||||||
|
jinja2/idtracking.py,sha256=GfNmadir4oDALVxzn3DL9YInhJDr69ebXeA2ygfuCGA,10704
|
||||||
|
jinja2/lexer.py,sha256=DW2nX9zk-6MWp65YR2bqqj0xqCvLtD-u9NWT8AnFRxQ,29726
|
||||||
|
jinja2/loaders.py,sha256=BfptfvTVpClUd-leMkHczdyPNYFzp_n7PKOJ98iyHOg,23207
|
||||||
|
jinja2/meta.py,sha256=GNPEvifmSaU3CMxlbheBOZjeZ277HThOPUTf1RkppKQ,4396
|
||||||
|
jinja2/nativetypes.py,sha256=DXgORDPRmVWgy034H0xL8eF7qYoK3DrMxs-935d0Fzk,4226
|
||||||
|
jinja2/nodes.py,sha256=i34GPRAZexXMT6bwuf5SEyvdmS-bRCy9KMjwN5O6pjk,34550
|
||||||
|
jinja2/optimizer.py,sha256=tHkMwXxfZkbfA1KmLcqmBMSaz7RLIvvItrJcPoXTyD8,1650
|
||||||
|
jinja2/parser.py,sha256=nHd-DFHbiygvfaPtm9rcQXJChZG7DPsWfiEsqfwKerY,39595
|
||||||
|
jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jinja2/runtime.py,sha256=5CmD5BjbEJxSiDNTFBeKCaq8qU4aYD2v6q2EluyExms,33476
|
||||||
|
jinja2/sandbox.py,sha256=Y0xZeXQnH6EX5VjaV2YixESxoepnRbW_3UeQosaBU3M,14584
|
||||||
|
jinja2/tests.py,sha256=Am5Z6Lmfr2XaH_npIfJJ8MdXtWsbLjMULZJulTAj30E,5905
|
||||||
|
jinja2/utils.py,sha256=u9jXESxGn8ATZNVolwmkjUVu4SA-tLgV0W7PcSfPfdQ,23965
|
||||||
|
jinja2/visitor.py,sha256=MH14C6yq24G_KVtWzjwaI7Wg14PCJIYlWW1kpkxYak0,3568
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
[babel.extractors]
|
||||||
|
jinja2 = jinja2.ext:babel_extract[i18n]
|
@ -0,0 +1 @@
|
|||||||
|
jinja2
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2010 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,101 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: MarkupSafe
|
||||||
|
Version: 2.1.1
|
||||||
|
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||||
|
Home-page: https://palletsprojects.com/p/markupsafe/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/markupsafe/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/markupsafe/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
|
||||||
|
MarkupSafe
|
||||||
|
==========
|
||||||
|
|
||||||
|
MarkupSafe implements a text object that escapes characters so it is
|
||||||
|
safe to use in HTML and XML. Characters that have special meanings are
|
||||||
|
replaced so that they display as the actual characters. This mitigates
|
||||||
|
injection attacks, meaning untrusted user input can safely be displayed
|
||||||
|
on a page.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
pip install -U MarkupSafe
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
>>> from markupsafe import Markup, escape
|
||||||
|
|
||||||
|
>>> # escape replaces special characters and wraps in Markup
|
||||||
|
>>> escape("<script>alert(document.cookie);</script>")
|
||||||
|
Markup('<script>alert(document.cookie);</script>')
|
||||||
|
|
||||||
|
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||||
|
>>> Markup("<strong>Hello</strong>")
|
||||||
|
Markup('<strong>hello</strong>')
|
||||||
|
|
||||||
|
>>> escape(Markup("<strong>Hello</strong>"))
|
||||||
|
Markup('<strong>hello</strong>')
|
||||||
|
|
||||||
|
>>> # Markup is a str subclass
|
||||||
|
>>> # methods and operators escape their arguments
|
||||||
|
>>> template = Markup("Hello <em>{name}</em>")
|
||||||
|
>>> template.format(name='"World"')
|
||||||
|
Markup('Hello <em>"World"</em>')
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports MarkupSafe and other
|
||||||
|
popular packages. In order to grow the community of contributors and
|
||||||
|
users, and allow the maintainers to devote more time to the projects,
|
||||||
|
`please donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://markupsafe.palletsprojects.com/
|
||||||
|
- Changes: https://markupsafe.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/MarkupSafe/
|
||||||
|
- Source Code: https://github.com/pallets/markupsafe/
|
||||||
|
- Issue Tracker: https://github.com/pallets/markupsafe/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/markupsafe/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
MarkupSafe-2.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
MarkupSafe-2.1.1.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||||
|
MarkupSafe-2.1.1.dist-info/METADATA,sha256=DC93VszmzjLQcrVChRUjtW4XbUwjTdbaplpgdlbFdbs,3242
|
||||||
|
MarkupSafe-2.1.1.dist-info/RECORD,,
|
||||||
|
MarkupSafe-2.1.1.dist-info/WHEEL,sha256=m4Wk6eOy92yJY87gVAkQnn-qnRb0vl_v2zsmx0hgXfo,109
|
||||||
|
MarkupSafe-2.1.1.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||||
|
markupsafe/__init__.py,sha256=xfaUQkKNRTdYWe6HnnJ2HjguFmS-C_0H6g8-Q9VAfkQ,9284
|
||||||
|
markupsafe/_native.py,sha256=GR86Qvo_GcgKmKreA1WmYN9ud17OFwkww8E-fiW-57s,1713
|
||||||
|
markupsafe/_speedups.c,sha256=X2XvQVtIdcK4Usz70BvkzoOfjTCmQlDkkjYSn-swE0g,7083
|
||||||
|
markupsafe/_speedups.cpython-38-darwin.so,sha256=kvMcXRxVyUrlANCkt-nae5u0BcrieOKwAZL7m_Qmik8,35136
|
||||||
|
markupsafe/_speedups.pyi,sha256=vfMCsOgbAXRNLUXkyuyonG8uEWKYU4PDqNuMaDELAYw,229
|
||||||
|
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.0)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp38-cp38-macosx_10_9_x86_64
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
markupsafe
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2007 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,126 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Werkzeug
|
||||||
|
Version: 2.2.2
|
||||||
|
Summary: The comprehensive WSGI web application library.
|
||||||
|
Home-page: https://palletsprojects.com/p/werkzeug/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://werkzeug.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://werkzeug.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/werkzeug/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/werkzeug/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
Requires-Dist: MarkupSafe (>=2.1.1)
|
||||||
|
Provides-Extra: watchdog
|
||||||
|
Requires-Dist: watchdog ; extra == 'watchdog'
|
||||||
|
|
||||||
|
Werkzeug
|
||||||
|
========
|
||||||
|
|
||||||
|
*werkzeug* German noun: "tool". Etymology: *werk* ("work"), *zeug* ("stuff")
|
||||||
|
|
||||||
|
Werkzeug is a comprehensive `WSGI`_ web application library. It began as
|
||||||
|
a simple collection of various utilities for WSGI applications and has
|
||||||
|
become one of the most advanced WSGI utility libraries.
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
|
||||||
|
- An interactive debugger that allows inspecting stack traces and
|
||||||
|
source code in the browser with an interactive interpreter for any
|
||||||
|
frame in the stack.
|
||||||
|
- A full-featured request object with objects to interact with
|
||||||
|
headers, query args, form data, files, and cookies.
|
||||||
|
- A response object that can wrap other WSGI applications and handle
|
||||||
|
streaming data.
|
||||||
|
- A routing system for matching URLs to endpoints and generating URLs
|
||||||
|
for endpoints, with an extensible system for capturing variables
|
||||||
|
from URLs.
|
||||||
|
- HTTP utilities to handle entity tags, cache control, dates, user
|
||||||
|
agents, cookies, files, and more.
|
||||||
|
- A threaded WSGI server for use while developing applications
|
||||||
|
locally.
|
||||||
|
- A test client for simulating HTTP requests during testing without
|
||||||
|
requiring running a server.
|
||||||
|
|
||||||
|
Werkzeug doesn't enforce any dependencies. It is up to the developer to
|
||||||
|
choose a template engine, database adapter, and even how to handle
|
||||||
|
requests. It can be used to build all sorts of end user applications
|
||||||
|
such as blogs, wikis, or bulletin boards.
|
||||||
|
|
||||||
|
`Flask`_ wraps Werkzeug, using it to handle the details of WSGI while
|
||||||
|
providing more structure and patterns for defining powerful
|
||||||
|
applications.
|
||||||
|
|
||||||
|
.. _WSGI: https://wsgi.readthedocs.io/en/latest/
|
||||||
|
.. _Flask: https://www.palletsprojects.com/p/flask/
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
pip install -U Werkzeug
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
A Simple Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from werkzeug.wrappers import Request, Response
|
||||||
|
|
||||||
|
@Request.application
|
||||||
|
def application(request):
|
||||||
|
return Response('Hello, World!')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from werkzeug.serving import run_simple
|
||||||
|
run_simple('localhost', 4000, application)
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Werkzeug and other
|
||||||
|
popular packages. In order to grow the community of contributors and
|
||||||
|
users, and allow the maintainers to devote more time to the projects,
|
||||||
|
`please donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://werkzeug.palletsprojects.com/
|
||||||
|
- Changes: https://werkzeug.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Werkzeug/
|
||||||
|
- Source Code: https://github.com/pallets/werkzeug/
|
||||||
|
- Issue Tracker: https://github.com/pallets/werkzeug/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/werkzeug/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
@ -0,0 +1,56 @@
|
|||||||
|
Werkzeug-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
Werkzeug-2.2.2.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475
|
||||||
|
Werkzeug-2.2.2.dist-info/METADATA,sha256=hz42ndovEQQy3rwXKZDwR7LA4UNthKegxf_7xIQrjsM,4416
|
||||||
|
Werkzeug-2.2.2.dist-info/RECORD,,
|
||||||
|
Werkzeug-2.2.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
Werkzeug-2.2.2.dist-info/top_level.txt,sha256=QRyj2VjwJoQkrwjwFIOlB8Xg3r9un0NtqVHQF-15xaw,9
|
||||||
|
werkzeug/__init__.py,sha256=UP218Ddd2NYm1dUhTlhvGRQytzAx1Ms1A716UKiPOYk,188
|
||||||
|
werkzeug/_internal.py,sha256=g8PHJz2z39I3x0vwTvTKbXIg0eUQqGF9UtUzDMWT0Qw,16222
|
||||||
|
werkzeug/_reloader.py,sha256=lYStlIDduTxBOB8BSozy_44HQ7YT5fup-x3uuac1-2o,14331
|
||||||
|
werkzeug/datastructures.py,sha256=T1SRE_KRuNz9Q7P-Ck4PyKPyil1NOx9zDuNMLgrN1Z0,97083
|
||||||
|
werkzeug/datastructures.pyi,sha256=HRzDLc7A6qnwluhNqn6AT76CsLZIkAbVVqxn0AbfV-s,34506
|
||||||
|
werkzeug/debug/__init__.py,sha256=Gpq6OpS6mHwHk0mJkHc2fWvvjo6ccJVS9QJwJgoeb9I,18893
|
||||||
|
werkzeug/debug/console.py,sha256=dechqiCtHfs0AQZWZofUC1S97tCuvwDgT0gdha5KwWM,6208
|
||||||
|
werkzeug/debug/repr.py,sha256=FFczy4yhVfEQjW99HuZtUce-ebtJWMjp9GnfasXa0KA,9488
|
||||||
|
werkzeug/debug/shared/ICON_LICENSE.md,sha256=DhA6Y1gUl5Jwfg0NFN9Rj4VWITt8tUx0IvdGf0ux9-s,222
|
||||||
|
werkzeug/debug/shared/console.png,sha256=bxax6RXXlvOij_KeqvSNX0ojJf83YbnZ7my-3Gx9w2A,507
|
||||||
|
werkzeug/debug/shared/debugger.js,sha256=tg42SZs1SVmYWZ-_Fj5ELK5-FLHnGNQrei0K2By8Bw8,10521
|
||||||
|
werkzeug/debug/shared/less.png,sha256=-4-kNRaXJSONVLahrQKUxMwXGm9R4OnZ9SxDGpHlIR4,191
|
||||||
|
werkzeug/debug/shared/more.png,sha256=GngN7CioHQoV58rH6ojnkYi8c_qED2Aka5FO5UXrReY,200
|
||||||
|
werkzeug/debug/shared/style.css,sha256=-xSxzUEZGw_IqlDR5iZxitNl8LQUjBM-_Y4UAvXVH8g,6078
|
||||||
|
werkzeug/debug/tbtools.py,sha256=Fsmlk6Ao3CcXm9iX7i_8MhCp2SQZ8uHm8Cf5wacnlW4,13293
|
||||||
|
werkzeug/exceptions.py,sha256=5MFy6RyaU4nokoYzdDafloY51QUDIGVNKeK_FORUFS0,26543
|
||||||
|
werkzeug/formparser.py,sha256=rLEu_ZwVpvqshZg6E4Qiv36QsmzmCytTijBeGX3dDGk,16056
|
||||||
|
werkzeug/http.py,sha256=i_LrIU9KsOz27zfkwKIK6eifFuFMKgSuW15k57HbMiE,42162
|
||||||
|
werkzeug/local.py,sha256=1IRMV9MFrauLaZeliF0Md1n7ZOcOKLbS03bnQ8Gz5WY,22326
|
||||||
|
werkzeug/middleware/__init__.py,sha256=qfqgdT5npwG9ses3-FXQJf3aB95JYP1zchetH_T3PUw,500
|
||||||
|
werkzeug/middleware/dispatcher.py,sha256=Fh_w-KyWnTSYF-Lfv5dimQ7THSS7afPAZMmvc4zF1gg,2580
|
||||||
|
werkzeug/middleware/http_proxy.py,sha256=HE8VyhS7CR-E1O6_9b68huv8FLgGGR1DLYqkS3Xcp3Q,7558
|
||||||
|
werkzeug/middleware/lint.py,sha256=Sr6gV4royDs6ezkqv5trRAyKMDQ60KaEq3-tQ3opUvw,13968
|
||||||
|
werkzeug/middleware/profiler.py,sha256=QkXk7cqnaPnF8wQu-5SyPCIOT3_kdABUBorQOghVNOA,4899
|
||||||
|
werkzeug/middleware/proxy_fix.py,sha256=l7LC_LDu0Yd4SvUxS5SFigAJMzcIOGm6LNKl9IXJBSU,6974
|
||||||
|
werkzeug/middleware/shared_data.py,sha256=fXjrEkuqxUVLG1DLrOdQLc96QQdjftCBZ1oM5oK89h4,9528
|
||||||
|
werkzeug/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
werkzeug/routing/__init__.py,sha256=HpvahY7WwkLdV4Cq3Bsc3GrqNon4u6t8-vhbb9E5o00,4819
|
||||||
|
werkzeug/routing/converters.py,sha256=05bkekg64vLC6mqqK4ddBh589WH9yBsjtW8IJhdUBvw,6968
|
||||||
|
werkzeug/routing/exceptions.py,sha256=RklUDL9ajOv2fTcRNj4pb18Bs4Y-GKk4rIeTSfsqkks,4737
|
||||||
|
werkzeug/routing/map.py,sha256=XN4ZjzEF1SfMxtdov89SDE-1_U78KVnnobTfnHzqbmE,36757
|
||||||
|
werkzeug/routing/matcher.py,sha256=U8xZTB3e5f3TgbkxdDyVuyxK4w72l1lo_b3tdG2zNrc,7122
|
||||||
|
werkzeug/routing/rules.py,sha256=v27RaR5H3sIPRdJ_pdEfOBMN6EivFVpmFzJk7aizdyw,31072
|
||||||
|
werkzeug/sansio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
werkzeug/sansio/http.py,sha256=9eORg44CDxpmV9i_U_pZ_NR8gdc9UXFCdE7EAP1v-c0,5162
|
||||||
|
werkzeug/sansio/multipart.py,sha256=Uyrg2U6s2oft8LXOyuTvFCWTLOEr7INVW8zFTXNwZ7A,9756
|
||||||
|
werkzeug/sansio/request.py,sha256=SiGcx2cz-l81TlCCrKrT2fePqC64hN8fSg5Ig6J6vRs,20175
|
||||||
|
werkzeug/sansio/response.py,sha256=UTl-teQDDjovrZMkjj3ZQsHw-JtiFak5JfKEk1_vBYU,26026
|
||||||
|
werkzeug/sansio/utils.py,sha256=EjbqdHdT-JZWgjUQaaWSgBUIRprXUkrsMQQqJlJHpVU,4847
|
||||||
|
werkzeug/security.py,sha256=vrBofh4WZZoUo1eAdJ6F1DrzVRlYauGS2CUDYpbQKj8,4658
|
||||||
|
werkzeug/serving.py,sha256=18pfjrHw8b5UCgPPo1ZEoxlYZZ5UREl4jQ9f8LGWMAo,38458
|
||||||
|
werkzeug/test.py,sha256=t7T5G-HciIlv1ZXtlydFVpow0VrXnJ_Y3yyEB7T0_Ww,48125
|
||||||
|
werkzeug/testapp.py,sha256=RJhT_2JweNiMKe304N3bF1zaIeMpRx-CIMERdeydfTY,9404
|
||||||
|
werkzeug/urls.py,sha256=Q9Si-eVh7yxk3rwkzrwGRm146FXVXgg9lBP3k0HUfVM,36600
|
||||||
|
werkzeug/user_agent.py,sha256=WclZhpvgLurMF45hsioSbS75H1Zb4iMQGKN3_yZ2oKo,1420
|
||||||
|
werkzeug/utils.py,sha256=OYdB2cZPYYgq3C0EVKMIv05BrYzzr9xdefW0H00_IVo,24936
|
||||||
|
werkzeug/wrappers/__init__.py,sha256=kGyK7rOud3qCxll_jFyW15YarJhj1xtdf3ocx9ZheB8,120
|
||||||
|
werkzeug/wrappers/request.py,sha256=UQ559KkGS0Po6HTBgvKMlk1_AsNw5zstzm8o_dRrfdQ,23415
|
||||||
|
werkzeug/wrappers/response.py,sha256=c2HUXrrt5Sf8-XEB1fUXxm6jp7Lu80KR0A_tbQFvw1Q,34750
|
||||||
|
werkzeug/wsgi.py,sha256=sgkFCzhl23hlSmbvjxbI-hVEjSlPuEBGTDAHmXFcAts,34732
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
werkzeug
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2014 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,111 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: click
|
||||||
|
Version: 8.1.3
|
||||||
|
Summary: Composable command line interface toolkit
|
||||||
|
Home-page: https://palletsprojects.com/p/click/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://click.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://click.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/click/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/click/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
Requires-Dist: colorama ; platform_system == "Windows"
|
||||||
|
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||||
|
|
||||||
|
\$ click\_
|
||||||
|
==========
|
||||||
|
|
||||||
|
Click is a Python package for creating beautiful command line interfaces
|
||||||
|
in a composable way with as little code as necessary. It's the "Command
|
||||||
|
Line Interface Creation Kit". It's highly configurable but comes with
|
||||||
|
sensible defaults out of the box.
|
||||||
|
|
||||||
|
It aims to make the process of writing command line tools quick and fun
|
||||||
|
while also preventing any frustration caused by the inability to
|
||||||
|
implement an intended CLI API.
|
||||||
|
|
||||||
|
Click in three points:
|
||||||
|
|
||||||
|
- Arbitrary nesting of commands
|
||||||
|
- Automatic help page generation
|
||||||
|
- Supports lazy loading of subcommands at runtime
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U click
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
A Simple Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--count", default=1, help="Number of greetings.")
|
||||||
|
@click.option("--name", prompt="Your name", help="The person to greet.")
|
||||||
|
def hello(count, name):
|
||||||
|
"""Simple program that greets NAME for a total of COUNT times."""
|
||||||
|
for _ in range(count):
|
||||||
|
click.echo(f"Hello, {name}!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
hello()
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ python hello.py --count=3
|
||||||
|
Your name: Click
|
||||||
|
Hello, Click!
|
||||||
|
Hello, Click!
|
||||||
|
Hello, Click!
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Click and other popular
|
||||||
|
packages. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://click.palletsprojects.com/
|
||||||
|
- Changes: https://click.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/click/
|
||||||
|
- Source Code: https://github.com/pallets/click
|
||||||
|
- Issue Tracker: https://github.com/pallets/click/issues
|
||||||
|
- Website: https://palletsprojects.com/p/click
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
click-8.1.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
click-8.1.3.dist-info/LICENSE.rst,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
|
||||||
|
click-8.1.3.dist-info/METADATA,sha256=tFJIX5lOjx7c5LjZbdTPFVDJSgyv9F74XY0XCPp_gnc,3247
|
||||||
|
click-8.1.3.dist-info/RECORD,,
|
||||||
|
click-8.1.3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
click-8.1.3.dist-info/top_level.txt,sha256=J1ZQogalYS4pphY_lPECoNMfw0HzTSrZglC4Yfwo4xA,6
|
||||||
|
click/__init__.py,sha256=rQBLutqg-z6m8nOzivIfigDn_emijB_dKv9BZ2FNi5s,3138
|
||||||
|
click/_compat.py,sha256=JIHLYs7Jzz4KT9t-ds4o4jBzLjnwCiJQKqur-5iwCKI,18810
|
||||||
|
click/_termui_impl.py,sha256=qK6Cfy4mRFxvxE8dya8RBhLpSC8HjF-lvBc6aNrPdwg,23451
|
||||||
|
click/_textwrap.py,sha256=10fQ64OcBUMuK7mFvh8363_uoOxPlRItZBmKzRJDgoY,1353
|
||||||
|
click/_winconsole.py,sha256=5ju3jQkcZD0W27WEMGqmEP4y_crUVzPCqsX_FYb7BO0,7860
|
||||||
|
click/core.py,sha256=mz87bYEKzIoNYEa56BFAiOJnvt1Y0L-i7wD4_ZecieE,112782
|
||||||
|
click/decorators.py,sha256=yo3zvzgUm5q7h5CXjyV6q3h_PJAiUaem178zXwdWUFI,16350
|
||||||
|
click/exceptions.py,sha256=7gDaLGuFZBeCNwY9ERMsF2-Z3R9Fvq09Zc6IZSKjseo,9167
|
||||||
|
click/formatting.py,sha256=Frf0-5W33-loyY_i9qrwXR8-STnW3m5gvyxLVUdyxyk,9706
|
||||||
|
click/globals.py,sha256=TP-qM88STzc7f127h35TD_v920FgfOD2EwzqA0oE8XU,1961
|
||||||
|
click/parser.py,sha256=cAEt1uQR8gq3-S9ysqbVU-fdAZNvilxw4ReJ_T1OQMk,19044
|
||||||
|
click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
click/shell_completion.py,sha256=qOp_BeC9esEOSZKyu5G7RIxEUaLsXUX-mTb7hB1r4QY,18018
|
||||||
|
click/termui.py,sha256=ACBQVOvFCTSqtD5VREeCAdRtlHd-Imla-Lte4wSfMjA,28355
|
||||||
|
click/testing.py,sha256=ptpMYgRY7dVfE3UDgkgwayu9ePw98sQI3D7zZXiCpj4,16063
|
||||||
|
click/types.py,sha256=rEb1aZSQKq3ciCMmjpG2Uva9vk498XRL7ThrcK2GRss,35805
|
||||||
|
click/utils.py,sha256=33D6E7poH_nrKB-xr-UyDEXnxOcCiQqxuRLtrqeVv6o,18682
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
click
|
@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Click is a simple Python module inspired by the stdlib optparse to make
|
||||||
|
writing command line scripts fun. Unlike other modules, it's based
|
||||||
|
around a simple API that does not come with too much magic and is
|
||||||
|
composable.
|
||||||
|
"""
|
||||||
|
from .core import Argument as Argument
|
||||||
|
from .core import BaseCommand as BaseCommand
|
||||||
|
from .core import Command as Command
|
||||||
|
from .core import CommandCollection as CommandCollection
|
||||||
|
from .core import Context as Context
|
||||||
|
from .core import Group as Group
|
||||||
|
from .core import MultiCommand as MultiCommand
|
||||||
|
from .core import Option as Option
|
||||||
|
from .core import Parameter as Parameter
|
||||||
|
from .decorators import argument as argument
|
||||||
|
from .decorators import command as command
|
||||||
|
from .decorators import confirmation_option as confirmation_option
|
||||||
|
from .decorators import group as group
|
||||||
|
from .decorators import help_option as help_option
|
||||||
|
from .decorators import make_pass_decorator as make_pass_decorator
|
||||||
|
from .decorators import option as option
|
||||||
|
from .decorators import pass_context as pass_context
|
||||||
|
from .decorators import pass_obj as pass_obj
|
||||||
|
from .decorators import password_option as password_option
|
||||||
|
from .decorators import version_option as version_option
|
||||||
|
from .exceptions import Abort as Abort
|
||||||
|
from .exceptions import BadArgumentUsage as BadArgumentUsage
|
||||||
|
from .exceptions import BadOptionUsage as BadOptionUsage
|
||||||
|
from .exceptions import BadParameter as BadParameter
|
||||||
|
from .exceptions import ClickException as ClickException
|
||||||
|
from .exceptions import FileError as FileError
|
||||||
|
from .exceptions import MissingParameter as MissingParameter
|
||||||
|
from .exceptions import NoSuchOption as NoSuchOption
|
||||||
|
from .exceptions import UsageError as UsageError
|
||||||
|
from .formatting import HelpFormatter as HelpFormatter
|
||||||
|
from .formatting import wrap_text as wrap_text
|
||||||
|
from .globals import get_current_context as get_current_context
|
||||||
|
from .parser import OptionParser as OptionParser
|
||||||
|
from .termui import clear as clear
|
||||||
|
from .termui import confirm as confirm
|
||||||
|
from .termui import echo_via_pager as echo_via_pager
|
||||||
|
from .termui import edit as edit
|
||||||
|
from .termui import getchar as getchar
|
||||||
|
from .termui import launch as launch
|
||||||
|
from .termui import pause as pause
|
||||||
|
from .termui import progressbar as progressbar
|
||||||
|
from .termui import prompt as prompt
|
||||||
|
from .termui import secho as secho
|
||||||
|
from .termui import style as style
|
||||||
|
from .termui import unstyle as unstyle
|
||||||
|
from .types import BOOL as BOOL
|
||||||
|
from .types import Choice as Choice
|
||||||
|
from .types import DateTime as DateTime
|
||||||
|
from .types import File as File
|
||||||
|
from .types import FLOAT as FLOAT
|
||||||
|
from .types import FloatRange as FloatRange
|
||||||
|
from .types import INT as INT
|
||||||
|
from .types import IntRange as IntRange
|
||||||
|
from .types import ParamType as ParamType
|
||||||
|
from .types import Path as Path
|
||||||
|
from .types import STRING as STRING
|
||||||
|
from .types import Tuple as Tuple
|
||||||
|
from .types import UNPROCESSED as UNPROCESSED
|
||||||
|
from .types import UUID as UUID
|
||||||
|
from .utils import echo as echo
|
||||||
|
from .utils import format_filename as format_filename
|
||||||
|
from .utils import get_app_dir as get_app_dir
|
||||||
|
from .utils import get_binary_stream as get_binary_stream
|
||||||
|
from .utils import get_text_stream as get_text_stream
|
||||||
|
from .utils import open_file as open_file
|
||||||
|
|
||||||
|
__version__ = "8.1.3"
|
@ -0,0 +1,626 @@
|
|||||||
|
import codecs
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
|
CYGWIN = sys.platform.startswith("cygwin")
|
||||||
|
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
||||||
|
# Determine local App Engine environment, per Google's own suggestion
|
||||||
|
APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get(
|
||||||
|
"SERVER_SOFTWARE", ""
|
||||||
|
)
|
||||||
|
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
|
||||||
|
auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None
|
||||||
|
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
||||||
|
|
||||||
|
|
||||||
|
def get_filesystem_encoding() -> str:
|
||||||
|
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_text_stream(
|
||||||
|
stream: t.BinaryIO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
|
if encoding is None:
|
||||||
|
encoding = get_best_encoding(stream)
|
||||||
|
if errors is None:
|
||||||
|
errors = "replace"
|
||||||
|
return _NonClosingTextIOWrapper(
|
||||||
|
stream,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
line_buffering=True,
|
||||||
|
force_readable=force_readable,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ascii_encoding(encoding: str) -> bool:
|
||||||
|
"""Checks if a given encoding is ascii."""
|
||||||
|
try:
|
||||||
|
return codecs.lookup(encoding).name == "ascii"
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_encoding(stream: t.IO) -> str:
|
||||||
|
"""Returns the default stream encoding if not found."""
|
||||||
|
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||||
|
if is_ascii_encoding(rv):
|
||||||
|
return "utf-8"
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
stream: t.BinaryIO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
**extra: t.Any,
|
||||||
|
) -> None:
|
||||||
|
self._stream = stream = t.cast(
|
||||||
|
t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
|
||||||
|
)
|
||||||
|
super().__init__(stream, encoding, errors, **extra)
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
try:
|
||||||
|
self.detach()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||||
|
return self._stream.isatty()
|
||||||
|
|
||||||
|
|
||||||
|
class _FixupStream:
|
||||||
|
"""The new io interface needs more from streams than streams
|
||||||
|
traditionally implement. As such, this fix-up code is necessary in
|
||||||
|
some circumstances.
|
||||||
|
|
||||||
|
The forcing of readable and writable flags are there because some tools
|
||||||
|
put badly patched objects on sys (one such offender are certain version
|
||||||
|
of jupyter notebook).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
stream: t.BinaryIO,
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
):
|
||||||
|
self._stream = stream
|
||||||
|
self._force_readable = force_readable
|
||||||
|
self._force_writable = force_writable
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self._stream, name)
|
||||||
|
|
||||||
|
def read1(self, size: int) -> bytes:
|
||||||
|
f = getattr(self._stream, "read1", None)
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
return t.cast(bytes, f(size))
|
||||||
|
|
||||||
|
return self._stream.read(size)
|
||||||
|
|
||||||
|
def readable(self) -> bool:
|
||||||
|
if self._force_readable:
|
||||||
|
return True
|
||||||
|
x = getattr(self._stream, "readable", None)
|
||||||
|
if x is not None:
|
||||||
|
return t.cast(bool, x())
|
||||||
|
try:
|
||||||
|
self._stream.read(0)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def writable(self) -> bool:
|
||||||
|
if self._force_writable:
|
||||||
|
return True
|
||||||
|
x = getattr(self._stream, "writable", None)
|
||||||
|
if x is not None:
|
||||||
|
return t.cast(bool, x())
|
||||||
|
try:
|
||||||
|
self._stream.write("") # type: ignore
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self._stream.write(b"")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seekable(self) -> bool:
|
||||||
|
x = getattr(self._stream, "seekable", None)
|
||||||
|
if x is not None:
|
||||||
|
return t.cast(bool, x())
|
||||||
|
try:
|
||||||
|
self._stream.seek(self._stream.tell())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
|
||||||
|
try:
|
||||||
|
return isinstance(stream.read(0), bytes)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
# This happens in some cases where the stream was already
|
||||||
|
# closed. In this case, we assume the default.
|
||||||
|
|
||||||
|
|
||||||
|
def _is_binary_writer(stream: t.IO, default: bool = False) -> bool:
|
||||||
|
try:
|
||||||
|
stream.write(b"")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
stream.write("")
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]:
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detaching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_reader(stream, False):
|
||||||
|
return t.cast(t.BinaryIO, stream)
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_reader(buf, True):
|
||||||
|
return t.cast(t.BinaryIO, buf)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_binary_writer(stream: t.IO) -> t.Optional[t.BinaryIO]:
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detaching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_writer(stream, False):
|
||||||
|
return t.cast(t.BinaryIO, stream)
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_writer(buf, True):
|
||||||
|
return t.cast(t.BinaryIO, buf)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
|
||||||
|
"""A stream is misconfigured if its encoding is ASCII."""
|
||||||
|
# If the stream does not have an encoding set, we assume it's set
|
||||||
|
# to ASCII. This appears to happen in certain unittest
|
||||||
|
# environments. It's not quite clear what the correct behavior is
|
||||||
|
# but this at least will force Click to recover somehow.
|
||||||
|
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: t.Optional[str]) -> bool:
|
||||||
|
"""A stream attribute is compatible if it is equal to the
|
||||||
|
desired value or the desired value is unset and the attribute
|
||||||
|
has a value.
|
||||||
|
"""
|
||||||
|
stream_value = getattr(stream, attr, None)
|
||||||
|
return stream_value == value or (value is None and stream_value is not None)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_compatible_text_stream(
|
||||||
|
stream: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> bool:
|
||||||
|
"""Check if a stream's encoding and errors attributes are
|
||||||
|
compatible with the desired values.
|
||||||
|
"""
|
||||||
|
return _is_compat_stream_attr(
|
||||||
|
stream, "encoding", encoding
|
||||||
|
) and _is_compat_stream_attr(stream, "errors", errors)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_correct_text_stream(
|
||||||
|
text_stream: t.IO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
is_binary: t.Callable[[t.IO, bool], bool],
|
||||||
|
find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]],
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
|
if is_binary(text_stream, False):
|
||||||
|
binary_reader = t.cast(t.BinaryIO, text_stream)
|
||||||
|
else:
|
||||||
|
text_stream = t.cast(t.TextIO, text_stream)
|
||||||
|
# If the stream looks compatible, and won't default to a
|
||||||
|
# misconfigured ascii encoding, return it as-is.
|
||||||
|
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
||||||
|
encoding is None and _stream_is_misconfigured(text_stream)
|
||||||
|
):
|
||||||
|
return text_stream
|
||||||
|
|
||||||
|
# Otherwise, get the underlying binary reader.
|
||||||
|
possible_binary_reader = find_binary(text_stream)
|
||||||
|
|
||||||
|
# If that's not possible, silently use the original reader
|
||||||
|
# and get mojibake instead of exceptions.
|
||||||
|
if possible_binary_reader is None:
|
||||||
|
return text_stream
|
||||||
|
|
||||||
|
binary_reader = possible_binary_reader
|
||||||
|
|
||||||
|
# Default errors to replace instead of strict in order to get
|
||||||
|
# something that works.
|
||||||
|
if errors is None:
|
||||||
|
errors = "replace"
|
||||||
|
|
||||||
|
# Wrap the binary stream in a text stream with the correct
|
||||||
|
# encoding parameters.
|
||||||
|
return _make_text_stream(
|
||||||
|
binary_reader,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
force_readable=force_readable,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_correct_text_reader(
|
||||||
|
text_reader: t.IO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
force_readable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
|
return _force_correct_text_stream(
|
||||||
|
text_reader,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
_is_binary_reader,
|
||||||
|
_find_binary_reader,
|
||||||
|
force_readable=force_readable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_correct_text_writer(
|
||||||
|
text_writer: t.IO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
|
return _force_correct_text_stream(
|
||||||
|
text_writer,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
_is_binary_writer,
|
||||||
|
_find_binary_writer,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stdin() -> t.BinaryIO:
|
||||||
|
reader = _find_binary_reader(sys.stdin)
|
||||||
|
if reader is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
||||||
|
return reader
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stdout() -> t.BinaryIO:
|
||||||
|
writer = _find_binary_writer(sys.stdout)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stderr() -> t.BinaryIO:
|
||||||
|
writer = _find_binary_writer(sys.stderr)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stdin(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stdout(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stderr(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_io_open(
|
||||||
|
file: t.Union[str, os.PathLike, int],
|
||||||
|
mode: str,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
) -> t.IO:
|
||||||
|
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
|
||||||
|
if "b" in mode:
|
||||||
|
return open(file, mode)
|
||||||
|
|
||||||
|
return open(file, mode, encoding=encoding, errors=errors)
|
||||||
|
|
||||||
|
|
||||||
|
def open_stream(
|
||||||
|
filename: str,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
atomic: bool = False,
|
||||||
|
) -> t.Tuple[t.IO, bool]:
|
||||||
|
binary = "b" in mode
|
||||||
|
|
||||||
|
# Standard streams first. These are simple because they ignore the
|
||||||
|
# atomic flag. Use fsdecode to handle Path("-").
|
||||||
|
if os.fsdecode(filename) == "-":
|
||||||
|
if any(m in mode for m in ["w", "a", "x"]):
|
||||||
|
if binary:
|
||||||
|
return get_binary_stdout(), False
|
||||||
|
return get_text_stdout(encoding=encoding, errors=errors), False
|
||||||
|
if binary:
|
||||||
|
return get_binary_stdin(), False
|
||||||
|
return get_text_stdin(encoding=encoding, errors=errors), False
|
||||||
|
|
||||||
|
# Non-atomic writes directly go out through the regular open functions.
|
||||||
|
if not atomic:
|
||||||
|
return _wrap_io_open(filename, mode, encoding, errors), True
|
||||||
|
|
||||||
|
# Some usability stuff for atomic writes
|
||||||
|
if "a" in mode:
|
||||||
|
raise ValueError(
|
||||||
|
"Appending to an existing file is not supported, because that"
|
||||||
|
" would involve an expensive `copy`-operation to a temporary"
|
||||||
|
" file. Open the file in normal `w`-mode and copy explicitly"
|
||||||
|
" if that's what you're after."
|
||||||
|
)
|
||||||
|
if "x" in mode:
|
||||||
|
raise ValueError("Use the `overwrite`-parameter instead.")
|
||||||
|
if "w" not in mode:
|
||||||
|
raise ValueError("Atomic writes only make sense with `w`-mode.")
|
||||||
|
|
||||||
|
# Atomic writes are more complicated. They work by opening a file
|
||||||
|
# as a proxy in the same folder and then using the fdopen
|
||||||
|
# functionality to wrap it in a Python file. Then we wrap it in an
|
||||||
|
# atomic file that moves the file over on close.
|
||||||
|
import errno
|
||||||
|
import random
|
||||||
|
|
||||||
|
try:
|
||||||
|
perm: t.Optional[int] = os.stat(filename).st_mode
|
||||||
|
except OSError:
|
||||||
|
perm = None
|
||||||
|
|
||||||
|
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||||
|
|
||||||
|
if binary:
|
||||||
|
flags |= getattr(os, "O_BINARY", 0)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tmp_filename = os.path.join(
|
||||||
|
os.path.dirname(filename),
|
||||||
|
f".__atomic-write{random.randrange(1 << 32):08x}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
||||||
|
break
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.EEXIST or (
|
||||||
|
os.name == "nt"
|
||||||
|
and e.errno == errno.EACCES
|
||||||
|
and os.path.isdir(e.filename)
|
||||||
|
and os.access(e.filename, os.W_OK)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
if perm is not None:
|
||||||
|
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
||||||
|
|
||||||
|
f = _wrap_io_open(fd, mode, encoding, errors)
|
||||||
|
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
|
||||||
|
return t.cast(t.IO, af), True
|
||||||
|
|
||||||
|
|
||||||
|
class _AtomicFile:
|
||||||
|
def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None:
|
||||||
|
self._f = f
|
||||||
|
self._tmp_filename = tmp_filename
|
||||||
|
self._real_filename = real_filename
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._real_filename
|
||||||
|
|
||||||
|
def close(self, delete: bool = False) -> None:
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
self._f.close()
|
||||||
|
os.replace(self._tmp_filename, self._real_filename)
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self._f, name)
|
||||||
|
|
||||||
|
def __enter__(self) -> "_AtomicFile":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
|
self.close(delete=exc_type is not None)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return repr(self._f)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_ansi(value: str) -> str:
|
||||||
|
return _ansi_re.sub("", value)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_jupyter_kernel_output(stream: t.IO) -> bool:
|
||||||
|
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||||
|
stream = stream._stream
|
||||||
|
|
||||||
|
return stream.__class__.__module__.startswith("ipykernel.")
|
||||||
|
|
||||||
|
|
||||||
|
def should_strip_ansi(
|
||||||
|
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
|
||||||
|
) -> bool:
|
||||||
|
if color is None:
|
||||||
|
if stream is None:
|
||||||
|
stream = sys.stdin
|
||||||
|
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
|
||||||
|
return not color
|
||||||
|
|
||||||
|
|
||||||
|
# On Windows, wrap the output streams with colorama to support ANSI
|
||||||
|
# color codes.
|
||||||
|
# NOTE: double check is needed so mypy does not analyze this on Linux
|
||||||
|
if sys.platform.startswith("win") and WIN:
|
||||||
|
from ._winconsole import _get_windows_console_stream
|
||||||
|
|
||||||
|
def _get_argv_encoding() -> str:
|
||||||
|
import locale
|
||||||
|
|
||||||
|
return locale.getpreferredencoding()
|
||||||
|
|
||||||
|
_ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||||
|
|
||||||
|
def auto_wrap_for_ansi(
|
||||||
|
stream: t.TextIO, color: t.Optional[bool] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
"""Support ANSI color and style codes on Windows by wrapping a
|
||||||
|
stream with colorama.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cached = _ansi_stream_wrappers.get(stream)
|
||||||
|
except Exception:
|
||||||
|
cached = None
|
||||||
|
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
strip = should_strip_ansi(stream, color)
|
||||||
|
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||||
|
rv = t.cast(t.TextIO, ansi_wrapper.stream)
|
||||||
|
_write = rv.write
|
||||||
|
|
||||||
|
def _safe_write(s):
|
||||||
|
try:
|
||||||
|
return _write(s)
|
||||||
|
except BaseException:
|
||||||
|
ansi_wrapper.reset_all()
|
||||||
|
raise
|
||||||
|
|
||||||
|
rv.write = _safe_write
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ansi_stream_wrappers[stream] = rv
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def _get_argv_encoding() -> str:
|
||||||
|
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
||||||
|
|
||||||
|
def _get_windows_console_stream(
|
||||||
|
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> t.Optional[t.TextIO]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def term_len(x: str) -> int:
|
||||||
|
return len(strip_ansi(x))
|
||||||
|
|
||||||
|
|
||||||
|
def isatty(stream: t.IO) -> bool:
|
||||||
|
try:
|
||||||
|
return stream.isatty()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cached_stream_func(
|
||||||
|
src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO]
|
||||||
|
) -> t.Callable[[], t.TextIO]:
|
||||||
|
cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||||
|
|
||||||
|
def func() -> t.TextIO:
|
||||||
|
stream = src_func()
|
||||||
|
try:
|
||||||
|
rv = cache.get(stream)
|
||||||
|
except Exception:
|
||||||
|
rv = None
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
rv = wrapper_func()
|
||||||
|
try:
|
||||||
|
cache[stream] = rv
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rv
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
|
||||||
|
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
|
||||||
|
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
||||||
|
|
||||||
|
|
||||||
|
binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = {
|
||||||
|
"stdin": get_binary_stdin,
|
||||||
|
"stdout": get_binary_stdout,
|
||||||
|
"stderr": get_binary_stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
text_streams: t.Mapping[
|
||||||
|
str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO]
|
||||||
|
] = {
|
||||||
|
"stdin": get_text_stdin,
|
||||||
|
"stdout": get_text_stdout,
|
||||||
|
"stderr": get_text_stderr,
|
||||||
|
}
|
@ -0,0 +1,717 @@
|
|||||||
|
"""
|
||||||
|
This module contains implementations for the termui module. To keep the
|
||||||
|
import time of Click down, some infrequently used functionality is
|
||||||
|
placed in this module and only imported as needed.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from ._compat import _default_text_stdout
|
||||||
|
from ._compat import CYGWIN
|
||||||
|
from ._compat import get_best_encoding
|
||||||
|
from ._compat import isatty
|
||||||
|
from ._compat import open_stream
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import term_len
|
||||||
|
from ._compat import WIN
|
||||||
|
from .exceptions import ClickException
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
BEFORE_BAR = "\r"
|
||||||
|
AFTER_BAR = "\n"
|
||||||
|
else:
|
||||||
|
BEFORE_BAR = "\r\033[?25l"
|
||||||
|
AFTER_BAR = "\033[?25h\n"
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressBar(t.Generic[V]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
iterable: t.Optional[t.Iterable[V]],
|
||||||
|
length: t.Optional[int] = None,
|
||||||
|
fill_char: str = "#",
|
||||||
|
empty_char: str = " ",
|
||||||
|
bar_template: str = "%(bar)s",
|
||||||
|
info_sep: str = " ",
|
||||||
|
show_eta: bool = True,
|
||||||
|
show_percent: t.Optional[bool] = None,
|
||||||
|
show_pos: bool = False,
|
||||||
|
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||||
|
label: t.Optional[str] = None,
|
||||||
|
file: t.Optional[t.TextIO] = None,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
update_min_steps: int = 1,
|
||||||
|
width: int = 30,
|
||||||
|
) -> None:
|
||||||
|
self.fill_char = fill_char
|
||||||
|
self.empty_char = empty_char
|
||||||
|
self.bar_template = bar_template
|
||||||
|
self.info_sep = info_sep
|
||||||
|
self.show_eta = show_eta
|
||||||
|
self.show_percent = show_percent
|
||||||
|
self.show_pos = show_pos
|
||||||
|
self.item_show_func = item_show_func
|
||||||
|
self.label = label or ""
|
||||||
|
if file is None:
|
||||||
|
file = _default_text_stdout()
|
||||||
|
self.file = file
|
||||||
|
self.color = color
|
||||||
|
self.update_min_steps = update_min_steps
|
||||||
|
self._completed_intervals = 0
|
||||||
|
self.width = width
|
||||||
|
self.autowidth = width == 0
|
||||||
|
|
||||||
|
if length is None:
|
||||||
|
from operator import length_hint
|
||||||
|
|
||||||
|
length = length_hint(iterable, -1)
|
||||||
|
|
||||||
|
if length == -1:
|
||||||
|
length = None
|
||||||
|
if iterable is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("iterable or length is required")
|
||||||
|
iterable = t.cast(t.Iterable[V], range(length))
|
||||||
|
self.iter = iter(iterable)
|
||||||
|
self.length = length
|
||||||
|
self.pos = 0
|
||||||
|
self.avg: t.List[float] = []
|
||||||
|
self.start = self.last_eta = time.time()
|
||||||
|
self.eta_known = False
|
||||||
|
self.finished = False
|
||||||
|
self.max_width: t.Optional[int] = None
|
||||||
|
self.entered = False
|
||||||
|
self.current_item: t.Optional[V] = None
|
||||||
|
self.is_hidden = not isatty(self.file)
|
||||||
|
self._last_line: t.Optional[str] = None
|
||||||
|
|
||||||
|
def __enter__(self) -> "ProgressBar":
|
||||||
|
self.entered = True
|
||||||
|
self.render_progress()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
|
self.render_finish()
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[V]:
|
||||||
|
if not self.entered:
|
||||||
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
|
self.render_progress()
|
||||||
|
return self.generator()
|
||||||
|
|
||||||
|
def __next__(self) -> V:
|
||||||
|
# Iteration is defined in terms of a generator function,
|
||||||
|
# returned by iter(self); use that to define next(). This works
|
||||||
|
# because `self.iter` is an iterable consumed by that generator,
|
||||||
|
# so it is re-entry safe. Calling `next(self.generator())`
|
||||||
|
# twice works and does "what you want".
|
||||||
|
return next(iter(self))
|
||||||
|
|
||||||
|
def render_finish(self) -> None:
|
||||||
|
if self.is_hidden:
|
||||||
|
return
|
||||||
|
self.file.write(AFTER_BAR)
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pct(self) -> float:
|
||||||
|
if self.finished:
|
||||||
|
return 1.0
|
||||||
|
return min(self.pos / (float(self.length or 1) or 1), 1.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_per_iteration(self) -> float:
|
||||||
|
if not self.avg:
|
||||||
|
return 0.0
|
||||||
|
return sum(self.avg) / float(len(self.avg))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eta(self) -> float:
|
||||||
|
if self.length is not None and not self.finished:
|
||||||
|
return self.time_per_iteration * (self.length - self.pos)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def format_eta(self) -> str:
|
||||||
|
if self.eta_known:
|
||||||
|
t = int(self.eta)
|
||||||
|
seconds = t % 60
|
||||||
|
t //= 60
|
||||||
|
minutes = t % 60
|
||||||
|
t //= 60
|
||||||
|
hours = t % 24
|
||||||
|
t //= 24
|
||||||
|
if t > 0:
|
||||||
|
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||||
|
else:
|
||||||
|
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def format_pos(self) -> str:
|
||||||
|
pos = str(self.pos)
|
||||||
|
if self.length is not None:
|
||||||
|
pos += f"/{self.length}"
|
||||||
|
return pos
|
||||||
|
|
||||||
|
def format_pct(self) -> str:
|
||||||
|
return f"{int(self.pct * 100): 4}%"[1:]
|
||||||
|
|
||||||
|
def format_bar(self) -> str:
|
||||||
|
if self.length is not None:
|
||||||
|
bar_length = int(self.pct * self.width)
|
||||||
|
bar = self.fill_char * bar_length
|
||||||
|
bar += self.empty_char * (self.width - bar_length)
|
||||||
|
elif self.finished:
|
||||||
|
bar = self.fill_char * self.width
|
||||||
|
else:
|
||||||
|
chars = list(self.empty_char * (self.width or 1))
|
||||||
|
if self.time_per_iteration != 0:
|
||||||
|
chars[
|
||||||
|
int(
|
||||||
|
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||||
|
* self.width
|
||||||
|
)
|
||||||
|
] = self.fill_char
|
||||||
|
bar = "".join(chars)
|
||||||
|
return bar
|
||||||
|
|
||||||
|
def format_progress_line(self) -> str:
|
||||||
|
show_percent = self.show_percent
|
||||||
|
|
||||||
|
info_bits = []
|
||||||
|
if self.length is not None and show_percent is None:
|
||||||
|
show_percent = not self.show_pos
|
||||||
|
|
||||||
|
if self.show_pos:
|
||||||
|
info_bits.append(self.format_pos())
|
||||||
|
if show_percent:
|
||||||
|
info_bits.append(self.format_pct())
|
||||||
|
if self.show_eta and self.eta_known and not self.finished:
|
||||||
|
info_bits.append(self.format_eta())
|
||||||
|
if self.item_show_func is not None:
|
||||||
|
item_info = self.item_show_func(self.current_item)
|
||||||
|
if item_info is not None:
|
||||||
|
info_bits.append(item_info)
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.bar_template
|
||||||
|
% {
|
||||||
|
"label": self.label,
|
||||||
|
"bar": self.format_bar(),
|
||||||
|
"info": self.info_sep.join(info_bits),
|
||||||
|
}
|
||||||
|
).rstrip()
|
||||||
|
|
||||||
|
def render_progress(self) -> None:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if self.is_hidden:
|
||||||
|
# Only output the label as it changes if the output is not a
|
||||||
|
# TTY. Use file=stderr if you expect to be piping stdout.
|
||||||
|
if self._last_line != self.label:
|
||||||
|
self._last_line = self.label
|
||||||
|
echo(self.label, file=self.file, color=self.color)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
buf = []
|
||||||
|
# Update width in case the terminal has been resized
|
||||||
|
if self.autowidth:
|
||||||
|
old_width = self.width
|
||||||
|
self.width = 0
|
||||||
|
clutter_length = term_len(self.format_progress_line())
|
||||||
|
new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
|
||||||
|
if new_width < old_width:
|
||||||
|
buf.append(BEFORE_BAR)
|
||||||
|
buf.append(" " * self.max_width) # type: ignore
|
||||||
|
self.max_width = new_width
|
||||||
|
self.width = new_width
|
||||||
|
|
||||||
|
clear_width = self.width
|
||||||
|
if self.max_width is not None:
|
||||||
|
clear_width = self.max_width
|
||||||
|
|
||||||
|
buf.append(BEFORE_BAR)
|
||||||
|
line = self.format_progress_line()
|
||||||
|
line_len = term_len(line)
|
||||||
|
if self.max_width is None or self.max_width < line_len:
|
||||||
|
self.max_width = line_len
|
||||||
|
|
||||||
|
buf.append(line)
|
||||||
|
buf.append(" " * (clear_width - line_len))
|
||||||
|
line = "".join(buf)
|
||||||
|
# Render the line only if it changed.
|
||||||
|
|
||||||
|
if line != self._last_line:
|
||||||
|
self._last_line = line
|
||||||
|
echo(line, file=self.file, color=self.color, nl=False)
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
|
def make_step(self, n_steps: int) -> None:
|
||||||
|
self.pos += n_steps
|
||||||
|
if self.length is not None and self.pos >= self.length:
|
||||||
|
self.finished = True
|
||||||
|
|
||||||
|
if (time.time() - self.last_eta) < 1.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.last_eta = time.time()
|
||||||
|
|
||||||
|
# self.avg is a rolling list of length <= 7 of steps where steps are
|
||||||
|
# defined as time elapsed divided by the total progress through
|
||||||
|
# self.length.
|
||||||
|
if self.pos:
|
||||||
|
step = (time.time() - self.start) / self.pos
|
||||||
|
else:
|
||||||
|
step = time.time() - self.start
|
||||||
|
|
||||||
|
self.avg = self.avg[-6:] + [step]
|
||||||
|
|
||||||
|
self.eta_known = self.length is not None
|
||||||
|
|
||||||
|
def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None:
|
||||||
|
"""Update the progress bar by advancing a specified number of
|
||||||
|
steps, and optionally set the ``current_item`` for this new
|
||||||
|
position.
|
||||||
|
|
||||||
|
:param n_steps: Number of steps to advance.
|
||||||
|
:param current_item: Optional item to set as ``current_item``
|
||||||
|
for the updated position.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``current_item`` optional parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Only render when the number of steps meets the
|
||||||
|
``update_min_steps`` threshold.
|
||||||
|
"""
|
||||||
|
if current_item is not None:
|
||||||
|
self.current_item = current_item
|
||||||
|
|
||||||
|
self._completed_intervals += n_steps
|
||||||
|
|
||||||
|
if self._completed_intervals >= self.update_min_steps:
|
||||||
|
self.make_step(self._completed_intervals)
|
||||||
|
self.render_progress()
|
||||||
|
self._completed_intervals = 0
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
self.eta_known = False
|
||||||
|
self.current_item = None
|
||||||
|
self.finished = True
|
||||||
|
|
||||||
|
def generator(self) -> t.Iterator[V]:
|
||||||
|
"""Return a generator which yields the items added to the bar
|
||||||
|
during construction, and updates the progress bar *after* the
|
||||||
|
yielded block returns.
|
||||||
|
"""
|
||||||
|
# WARNING: the iterator interface for `ProgressBar` relies on
|
||||||
|
# this and only works because this is a simple generator which
|
||||||
|
# doesn't create or manage additional state. If this function
|
||||||
|
# changes, the impact should be evaluated both against
|
||||||
|
# `iter(bar)` and `next(bar)`. `next()` in particular may call
|
||||||
|
# `self.generator()` repeatedly, and this must remain safe in
|
||||||
|
# order for that interface to work.
|
||||||
|
if not self.entered:
|
||||||
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
|
|
||||||
|
if self.is_hidden:
|
||||||
|
yield from self.iter
|
||||||
|
else:
|
||||||
|
for rv in self.iter:
|
||||||
|
self.current_item = rv
|
||||||
|
|
||||||
|
# This allows show_item_func to be updated before the
|
||||||
|
# item is processed. Only trigger at the beginning of
|
||||||
|
# the update interval.
|
||||||
|
if self._completed_intervals == 0:
|
||||||
|
self.render_progress()
|
||||||
|
|
||||||
|
yield rv
|
||||||
|
self.update(1)
|
||||||
|
|
||||||
|
self.finish()
|
||||||
|
self.render_progress()
|
||||||
|
|
||||||
|
|
||||||
|
def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None:
|
||||||
|
"""Decide what method to use for paging through text."""
|
||||||
|
stdout = _default_text_stdout()
|
||||||
|
if not isatty(sys.stdin) or not isatty(stdout):
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
pager_cmd = (os.environ.get("PAGER", None) or "").strip()
|
||||||
|
if pager_cmd:
|
||||||
|
if WIN:
|
||||||
|
return _tempfilepager(generator, pager_cmd, color)
|
||||||
|
return _pipepager(generator, pager_cmd, color)
|
||||||
|
if os.environ.get("TERM") in ("dumb", "emacs"):
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
if WIN or sys.platform.startswith("os2"):
|
||||||
|
return _tempfilepager(generator, "more <", color)
|
||||||
|
if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
|
||||||
|
return _pipepager(generator, "less", color)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
fd, filename = tempfile.mkstemp()
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
if hasattr(os, "system") and os.system(f'more "{filename}"') == 0:
|
||||||
|
return _pipepager(generator, "more", color)
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
finally:
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None:
|
||||||
|
"""Page through text by feeding it to another program. Invoking a
|
||||||
|
pager through this might support colors.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
env = dict(os.environ)
|
||||||
|
|
||||||
|
# If we're piping to less we might support colors under the
|
||||||
|
# condition that
|
||||||
|
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
||||||
|
if color is None and cmd_detail[0] == "less":
|
||||||
|
less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}"
|
||||||
|
if not less_flags:
|
||||||
|
env["LESS"] = "-R"
|
||||||
|
color = True
|
||||||
|
elif "r" in less_flags or "R" in less_flags:
|
||||||
|
color = True
|
||||||
|
|
||||||
|
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
|
||||||
|
stdin = t.cast(t.BinaryIO, c.stdin)
|
||||||
|
encoding = get_best_encoding(stdin)
|
||||||
|
try:
|
||||||
|
for text in generator:
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
|
||||||
|
stdin.write(text.encode(encoding, "replace"))
|
||||||
|
except (OSError, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
stdin.close()
|
||||||
|
|
||||||
|
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||||
|
# search or other commands inside less).
|
||||||
|
#
|
||||||
|
# That means when the user hits ^C, the parent process (click) terminates,
|
||||||
|
# but less is still alive, paging the output and messing up the terminal.
|
||||||
|
#
|
||||||
|
# If the user wants to make the pager exit on ^C, they should set
|
||||||
|
# `LESS='-K'`. It's not our decision to make.
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
c.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _tempfilepager(
|
||||||
|
generator: t.Iterable[str], cmd: str, color: t.Optional[bool]
|
||||||
|
) -> None:
|
||||||
|
"""Page through text by invoking a program on a temporary file."""
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
fd, filename = tempfile.mkstemp()
|
||||||
|
# TODO: This never terminates if the passed generator never terminates.
|
||||||
|
text = "".join(generator)
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
encoding = get_best_encoding(sys.stdout)
|
||||||
|
with open_stream(filename, "wb")[0] as f:
|
||||||
|
f.write(text.encode(encoding))
|
||||||
|
try:
|
||||||
|
os.system(f'{cmd} "{filename}"')
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _nullpager(
|
||||||
|
stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool]
|
||||||
|
) -> None:
|
||||||
|
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||||
|
for text in generator:
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
class Editor:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
editor: t.Optional[str] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, str]] = None,
|
||||||
|
require_save: bool = True,
|
||||||
|
extension: str = ".txt",
|
||||||
|
) -> None:
|
||||||
|
self.editor = editor
|
||||||
|
self.env = env
|
||||||
|
self.require_save = require_save
|
||||||
|
self.extension = extension
|
||||||
|
|
||||||
|
def get_editor(self) -> str:
|
||||||
|
if self.editor is not None:
|
||||||
|
return self.editor
|
||||||
|
for key in "VISUAL", "EDITOR":
|
||||||
|
rv = os.environ.get(key)
|
||||||
|
if rv:
|
||||||
|
return rv
|
||||||
|
if WIN:
|
||||||
|
return "notepad"
|
||||||
|
for editor in "sensible-editor", "vim", "nano":
|
||||||
|
if os.system(f"which {editor} >/dev/null 2>&1") == 0:
|
||||||
|
return editor
|
||||||
|
return "vi"
|
||||||
|
|
||||||
|
def edit_file(self, filename: str) -> None:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
editor = self.get_editor()
|
||||||
|
environ: t.Optional[t.Dict[str, str]] = None
|
||||||
|
|
||||||
|
if self.env:
|
||||||
|
environ = os.environ.copy()
|
||||||
|
environ.update(self.env)
|
||||||
|
|
||||||
|
try:
|
||||||
|
c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
|
||||||
|
exit_code = c.wait()
|
||||||
|
if exit_code != 0:
|
||||||
|
raise ClickException(
|
||||||
|
_("{editor}: Editing failed").format(editor=editor)
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
raise ClickException(
|
||||||
|
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]:
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
data = b""
|
||||||
|
elif isinstance(text, (bytes, bytearray)):
|
||||||
|
data = text
|
||||||
|
else:
|
||||||
|
if text and not text.endswith("\n"):
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
if WIN:
|
||||||
|
data = text.replace("\n", "\r\n").encode("utf-8-sig")
|
||||||
|
else:
|
||||||
|
data = text.encode("utf-8")
|
||||||
|
|
||||||
|
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
||||||
|
f: t.BinaryIO
|
||||||
|
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# If the filesystem resolution is 1 second, like Mac OS
|
||||||
|
# 10.12 Extended, or 2 seconds, like FAT32, and the editor
|
||||||
|
# closes very fast, require_save can fail. Set the modified
|
||||||
|
# time to be 2 seconds in the past to work around this.
|
||||||
|
os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
|
||||||
|
# Depending on the resolution, the exact value might not be
|
||||||
|
# recorded, so get the new recorded value.
|
||||||
|
timestamp = os.path.getmtime(name)
|
||||||
|
|
||||||
|
self.edit_file(name)
|
||||||
|
|
||||||
|
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(name, "rb") as f:
|
||||||
|
rv = f.read()
|
||||||
|
|
||||||
|
if isinstance(text, (bytes, bytearray)):
|
||||||
|
return rv
|
||||||
|
|
||||||
|
return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore
|
||||||
|
finally:
|
||||||
|
os.unlink(name)
|
||||||
|
|
||||||
|
|
||||||
|
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def _unquote_file(url: str) -> str:
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
if url.startswith("file://"):
|
||||||
|
url = unquote(url[7:])
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
args = ["open"]
|
||||||
|
if wait:
|
||||||
|
args.append("-W")
|
||||||
|
if locate:
|
||||||
|
args.append("-R")
|
||||||
|
args.append(_unquote_file(url))
|
||||||
|
null = open("/dev/null", "w")
|
||||||
|
try:
|
||||||
|
return subprocess.Popen(args, stderr=null).wait()
|
||||||
|
finally:
|
||||||
|
null.close()
|
||||||
|
elif WIN:
|
||||||
|
if locate:
|
||||||
|
url = _unquote_file(url.replace('"', ""))
|
||||||
|
args = f'explorer /select,"{url}"'
|
||||||
|
else:
|
||||||
|
url = url.replace('"', "")
|
||||||
|
wait_str = "/WAIT" if wait else ""
|
||||||
|
args = f'start {wait_str} "" "{url}"'
|
||||||
|
return os.system(args)
|
||||||
|
elif CYGWIN:
|
||||||
|
if locate:
|
||||||
|
url = os.path.dirname(_unquote_file(url).replace('"', ""))
|
||||||
|
args = f'cygstart "{url}"'
|
||||||
|
else:
|
||||||
|
url = url.replace('"', "")
|
||||||
|
wait_str = "-w" if wait else ""
|
||||||
|
args = f'cygstart {wait_str} "{url}"'
|
||||||
|
return os.system(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if locate:
|
||||||
|
url = os.path.dirname(_unquote_file(url)) or "."
|
||||||
|
else:
|
||||||
|
url = _unquote_file(url)
|
||||||
|
c = subprocess.Popen(["xdg-open", url])
|
||||||
|
if wait:
|
||||||
|
return c.wait()
|
||||||
|
return 0
|
||||||
|
except OSError:
|
||||||
|
if url.startswith(("http://", "https://")) and not locate and not wait:
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
webbrowser.open(url)
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]:
|
||||||
|
if ch == "\x03":
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
|
||||||
|
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
|
||||||
|
raise EOFError()
|
||||||
|
|
||||||
|
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
||||||
|
raise EOFError()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if WIN:
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def raw_terminal() -> t.Iterator[int]:
|
||||||
|
yield -1
|
||||||
|
|
||||||
|
def getchar(echo: bool) -> str:
|
||||||
|
# The function `getch` will return a bytes object corresponding to
|
||||||
|
# the pressed character. Since Windows 10 build 1803, it will also
|
||||||
|
# return \x00 when called a second time after pressing a regular key.
|
||||||
|
#
|
||||||
|
# `getwch` does not share this probably-bugged behavior. Moreover, it
|
||||||
|
# returns a Unicode object by default, which is what we want.
|
||||||
|
#
|
||||||
|
# Either of these functions will return \x00 or \xe0 to indicate
|
||||||
|
# a special key, and you need to call the same function again to get
|
||||||
|
# the "rest" of the code. The fun part is that \u00e0 is
|
||||||
|
# "latin small letter a with grave", so if you type that on a French
|
||||||
|
# keyboard, you _also_ get a \xe0.
|
||||||
|
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
|
||||||
|
# resulting Unicode string reads as "a with grave" + "capital H".
|
||||||
|
# This is indistinguishable from when the user actually types
|
||||||
|
# "a with grave" and then "capital H".
|
||||||
|
#
|
||||||
|
# When \xe0 is returned, we assume it's part of a special-key sequence
|
||||||
|
# and call `getwch` again, but that means that when the user types
|
||||||
|
# the \u00e0 character, `getchar` doesn't return until a second
|
||||||
|
# character is typed.
|
||||||
|
# The alternative is returning immediately, but that would mess up
|
||||||
|
# cross-platform handling of arrow keys and others that start with
|
||||||
|
# \xe0. Another option is using `getch`, but then we can't reliably
|
||||||
|
# read non-ASCII characters, because return values of `getch` are
|
||||||
|
# limited to the current 8-bit codepage.
|
||||||
|
#
|
||||||
|
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
||||||
|
# is doing the right thing in more situations than with `getch`.
|
||||||
|
func: t.Callable[[], str]
|
||||||
|
|
||||||
|
if echo:
|
||||||
|
func = msvcrt.getwche # type: ignore
|
||||||
|
else:
|
||||||
|
func = msvcrt.getwch # type: ignore
|
||||||
|
|
||||||
|
rv = func()
|
||||||
|
|
||||||
|
if rv in ("\x00", "\xe0"):
|
||||||
|
# \x00 and \xe0 are control characters that indicate special key,
|
||||||
|
# see above.
|
||||||
|
rv += func()
|
||||||
|
|
||||||
|
_translate_ch_to_exc(rv)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
else:
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def raw_terminal() -> t.Iterator[int]:
|
||||||
|
f: t.Optional[t.TextIO]
|
||||||
|
fd: int
|
||||||
|
|
||||||
|
if not isatty(sys.stdin):
|
||||||
|
f = open("/dev/tty")
|
||||||
|
fd = f.fileno()
|
||||||
|
else:
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
f = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
yield fd
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
except termios.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getchar(echo: bool) -> str:
|
||||||
|
with raw_terminal() as fd:
|
||||||
|
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
|
||||||
|
|
||||||
|
if echo and isatty(sys.stdout):
|
||||||
|
sys.stdout.write(ch)
|
||||||
|
|
||||||
|
_translate_ch_to_exc(ch)
|
||||||
|
return ch
|
@ -0,0 +1,49 @@
|
|||||||
|
import textwrap
|
||||||
|
import typing as t
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
class TextWrapper(textwrap.TextWrapper):
|
||||||
|
def _handle_long_word(
|
||||||
|
self,
|
||||||
|
reversed_chunks: t.List[str],
|
||||||
|
cur_line: t.List[str],
|
||||||
|
cur_len: int,
|
||||||
|
width: int,
|
||||||
|
) -> None:
|
||||||
|
space_left = max(width - cur_len, 1)
|
||||||
|
|
||||||
|
if self.break_long_words:
|
||||||
|
last = reversed_chunks[-1]
|
||||||
|
cut = last[:space_left]
|
||||||
|
res = last[space_left:]
|
||||||
|
cur_line.append(cut)
|
||||||
|
reversed_chunks[-1] = res
|
||||||
|
elif not cur_line:
|
||||||
|
cur_line.append(reversed_chunks.pop())
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def extra_indent(self, indent: str) -> t.Iterator[None]:
|
||||||
|
old_initial_indent = self.initial_indent
|
||||||
|
old_subsequent_indent = self.subsequent_indent
|
||||||
|
self.initial_indent += indent
|
||||||
|
self.subsequent_indent += indent
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.initial_indent = old_initial_indent
|
||||||
|
self.subsequent_indent = old_subsequent_indent
|
||||||
|
|
||||||
|
def indent_only(self, text: str) -> str:
|
||||||
|
rv = []
|
||||||
|
|
||||||
|
for idx, line in enumerate(text.splitlines()):
|
||||||
|
indent = self.initial_indent
|
||||||
|
|
||||||
|
if idx > 0:
|
||||||
|
indent = self.subsequent_indent
|
||||||
|
|
||||||
|
rv.append(f"{indent}{line}")
|
||||||
|
|
||||||
|
return "\n".join(rv)
|
@ -0,0 +1,279 @@
|
|||||||
|
# This module is based on the excellent work by Adam Bartoš who
|
||||||
|
# provided a lot of what went into the implementation here in
|
||||||
|
# the discussion to issue1602 in the Python bug tracker.
|
||||||
|
#
|
||||||
|
# There are some general differences in regards to how this works
|
||||||
|
# compared to the original patches as we do not need to patch
|
||||||
|
# the entire interpreter but just work in our little world of
|
||||||
|
# echo and prompt.
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import typing as t
|
||||||
|
from ctypes import byref
|
||||||
|
from ctypes import c_char
|
||||||
|
from ctypes import c_char_p
|
||||||
|
from ctypes import c_int
|
||||||
|
from ctypes import c_ssize_t
|
||||||
|
from ctypes import c_ulong
|
||||||
|
from ctypes import c_void_p
|
||||||
|
from ctypes import POINTER
|
||||||
|
from ctypes import py_object
|
||||||
|
from ctypes import Structure
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from ctypes.wintypes import HANDLE
|
||||||
|
from ctypes.wintypes import LPCWSTR
|
||||||
|
from ctypes.wintypes import LPWSTR
|
||||||
|
|
||||||
|
from ._compat import _NonClosingTextIOWrapper
|
||||||
|
|
||||||
|
assert sys.platform == "win32"
|
||||||
|
import msvcrt # noqa: E402
|
||||||
|
from ctypes import windll # noqa: E402
|
||||||
|
from ctypes import WINFUNCTYPE # noqa: E402
|
||||||
|
|
||||||
|
c_ssize_p = POINTER(c_ssize_t)
|
||||||
|
|
||||||
|
kernel32 = windll.kernel32
|
||||||
|
GetStdHandle = kernel32.GetStdHandle
|
||||||
|
ReadConsoleW = kernel32.ReadConsoleW
|
||||||
|
WriteConsoleW = kernel32.WriteConsoleW
|
||||||
|
GetConsoleMode = kernel32.GetConsoleMode
|
||||||
|
GetLastError = kernel32.GetLastError
|
||||||
|
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
||||||
|
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||||
|
("CommandLineToArgvW", windll.shell32)
|
||||||
|
)
|
||||||
|
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
|
||||||
|
|
||||||
|
STDIN_HANDLE = GetStdHandle(-10)
|
||||||
|
STDOUT_HANDLE = GetStdHandle(-11)
|
||||||
|
STDERR_HANDLE = GetStdHandle(-12)
|
||||||
|
|
||||||
|
PyBUF_SIMPLE = 0
|
||||||
|
PyBUF_WRITABLE = 1
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0
|
||||||
|
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||||
|
ERROR_OPERATION_ABORTED = 995
|
||||||
|
|
||||||
|
STDIN_FILENO = 0
|
||||||
|
STDOUT_FILENO = 1
|
||||||
|
STDERR_FILENO = 2
|
||||||
|
|
||||||
|
EOF = b"\x1a"
|
||||||
|
MAX_BYTES_WRITTEN = 32767
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ctypes import pythonapi
|
||||||
|
except ImportError:
|
||||||
|
# On PyPy we cannot get buffers so our ability to operate here is
|
||||||
|
# severely limited.
|
||||||
|
get_buffer = None
|
||||||
|
else:
|
||||||
|
|
||||||
|
class Py_buffer(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("buf", c_void_p),
|
||||||
|
("obj", py_object),
|
||||||
|
("len", c_ssize_t),
|
||||||
|
("itemsize", c_ssize_t),
|
||||||
|
("readonly", c_int),
|
||||||
|
("ndim", c_int),
|
||||||
|
("format", c_char_p),
|
||||||
|
("shape", c_ssize_p),
|
||||||
|
("strides", c_ssize_p),
|
||||||
|
("suboffsets", c_ssize_p),
|
||||||
|
("internal", c_void_p),
|
||||||
|
]
|
||||||
|
|
||||||
|
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
||||||
|
PyBuffer_Release = pythonapi.PyBuffer_Release
|
||||||
|
|
||||||
|
def get_buffer(obj, writable=False):
|
||||||
|
buf = Py_buffer()
|
||||||
|
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||||
|
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||||
|
|
||||||
|
try:
|
||||||
|
buffer_type = c_char * buf.len
|
||||||
|
return buffer_type.from_address(buf.buf)
|
||||||
|
finally:
|
||||||
|
PyBuffer_Release(byref(buf))
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleRawIOBase(io.RawIOBase):
|
||||||
|
def __init__(self, handle):
|
||||||
|
self.handle = handle
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
super().isatty()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
||||||
|
def readable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def readinto(self, b):
|
||||||
|
bytes_to_be_read = len(b)
|
||||||
|
if not bytes_to_be_read:
|
||||||
|
return 0
|
||||||
|
elif bytes_to_be_read % 2:
|
||||||
|
raise ValueError(
|
||||||
|
"cannot read odd number of bytes from UTF-16-LE encoded console"
|
||||||
|
)
|
||||||
|
|
||||||
|
buffer = get_buffer(b, writable=True)
|
||||||
|
code_units_to_be_read = bytes_to_be_read // 2
|
||||||
|
code_units_read = c_ulong()
|
||||||
|
|
||||||
|
rv = ReadConsoleW(
|
||||||
|
HANDLE(self.handle),
|
||||||
|
buffer,
|
||||||
|
code_units_to_be_read,
|
||||||
|
byref(code_units_read),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if GetLastError() == ERROR_OPERATION_ABORTED:
|
||||||
|
# wait for KeyboardInterrupt
|
||||||
|
time.sleep(0.1)
|
||||||
|
if not rv:
|
||||||
|
raise OSError(f"Windows error: {GetLastError()}")
|
||||||
|
|
||||||
|
if buffer[0] == EOF:
|
||||||
|
return 0
|
||||||
|
return 2 * code_units_read.value
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||||
|
def writable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_error_message(errno):
|
||||||
|
if errno == ERROR_SUCCESS:
|
||||||
|
return "ERROR_SUCCESS"
|
||||||
|
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||||
|
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||||
|
return f"Windows error {errno}"
|
||||||
|
|
||||||
|
def write(self, b):
|
||||||
|
bytes_to_be_written = len(b)
|
||||||
|
buf = get_buffer(b)
|
||||||
|
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
|
||||||
|
code_units_written = c_ulong()
|
||||||
|
|
||||||
|
WriteConsoleW(
|
||||||
|
HANDLE(self.handle),
|
||||||
|
buf,
|
||||||
|
code_units_to_be_written,
|
||||||
|
byref(code_units_written),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
bytes_written = 2 * code_units_written.value
|
||||||
|
|
||||||
|
if bytes_written == 0 and bytes_to_be_written > 0:
|
||||||
|
raise OSError(self._get_error_message(GetLastError()))
|
||||||
|
return bytes_written
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleStream:
|
||||||
|
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
|
||||||
|
self._text_stream = text_stream
|
||||||
|
self.buffer = byte_stream
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.buffer.name
|
||||||
|
|
||||||
|
def write(self, x: t.AnyStr) -> int:
|
||||||
|
if isinstance(x, str):
|
||||||
|
return self._text_stream.write(x)
|
||||||
|
try:
|
||||||
|
self.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self.buffer.write(x)
|
||||||
|
|
||||||
|
def writelines(self, lines: t.Iterable[t.AnyStr]) -> None:
|
||||||
|
for line in lines:
|
||||||
|
self.write(line)
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self._text_stream, name)
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
return self.buffer.isatty()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
|
_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
|
||||||
|
0: _get_text_stdin,
|
||||||
|
1: _get_text_stdout,
|
||||||
|
2: _get_text_stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_console(f: t.TextIO) -> bool:
|
||||||
|
if not hasattr(f, "fileno"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
fileno = f.fileno()
|
||||||
|
except (OSError, io.UnsupportedOperation):
|
||||||
|
return False
|
||||||
|
|
||||||
|
handle = msvcrt.get_osfhandle(fileno)
|
||||||
|
return bool(GetConsoleMode(handle, byref(DWORD())))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_windows_console_stream(
|
||||||
|
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> t.Optional[t.TextIO]:
|
||||||
|
if (
|
||||||
|
get_buffer is not None
|
||||||
|
and encoding in {"utf-16-le", None}
|
||||||
|
and errors in {"strict", None}
|
||||||
|
and _is_console(f)
|
||||||
|
):
|
||||||
|
func = _stream_factories.get(f.fileno())
|
||||||
|
if func is not None:
|
||||||
|
b = getattr(f, "buffer", None)
|
||||||
|
|
||||||
|
if b is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return func(b)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,497 @@
|
|||||||
|
import inspect
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
from functools import update_wrapper
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from .core import Argument
|
||||||
|
from .core import Command
|
||||||
|
from .core import Context
|
||||||
|
from .core import Group
|
||||||
|
from .core import Option
|
||||||
|
from .core import Parameter
|
||||||
|
from .globals import get_current_context
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command])
|
||||||
|
|
||||||
|
|
||||||
|
def pass_context(f: F) -> F:
|
||||||
|
"""Marks a callback as wanting to receive the current context
|
||||||
|
object as first argument.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
|
return f(get_current_context(), *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
|
||||||
|
def pass_obj(f: F) -> F:
|
||||||
|
"""Similar to :func:`pass_context`, but only pass the object on the
|
||||||
|
context onwards (:attr:`Context.obj`). This is useful if that object
|
||||||
|
represents the state of a nested system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
|
return f(get_current_context().obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
|
||||||
|
def make_pass_decorator(
|
||||||
|
object_type: t.Type, ensure: bool = False
|
||||||
|
) -> "t.Callable[[F], F]":
|
||||||
|
"""Given an object type this creates a decorator that will work
|
||||||
|
similar to :func:`pass_obj` but instead of passing the object of the
|
||||||
|
current context, it will find the innermost context of type
|
||||||
|
:func:`object_type`.
|
||||||
|
|
||||||
|
This generates a decorator that works roughly like this::
|
||||||
|
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
@pass_context
|
||||||
|
def new_func(ctx, *args, **kwargs):
|
||||||
|
obj = ctx.find_object(object_type)
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
return update_wrapper(new_func, f)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
:param object_type: the type of the object to pass.
|
||||||
|
:param ensure: if set to `True`, a new object will be created and
|
||||||
|
remembered on the context if it's not there yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: F) -> F:
|
||||||
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
|
ctx = get_current_context()
|
||||||
|
|
||||||
|
if ensure:
|
||||||
|
obj = ctx.ensure_object(object_type)
|
||||||
|
else:
|
||||||
|
obj = ctx.find_object(object_type)
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Managed to invoke callback without a context"
|
||||||
|
f" object of type {object_type.__name__!r}"
|
||||||
|
" existing."
|
||||||
|
)
|
||||||
|
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def pass_meta_key(
|
||||||
|
key: str, *, doc_description: t.Optional[str] = None
|
||||||
|
) -> "t.Callable[[F], F]":
|
||||||
|
"""Create a decorator that passes a key from
|
||||||
|
:attr:`click.Context.meta` as the first argument to the decorated
|
||||||
|
function.
|
||||||
|
|
||||||
|
:param key: Key in ``Context.meta`` to pass.
|
||||||
|
:param doc_description: Description of the object being passed,
|
||||||
|
inserted into the decorator's docstring. Defaults to "the 'key'
|
||||||
|
key from Context.meta".
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: F) -> F:
|
||||||
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
|
ctx = get_current_context()
|
||||||
|
obj = ctx.meta[key]
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
if doc_description is None:
|
||||||
|
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
|
||||||
|
|
||||||
|
decorator.__doc__ = (
|
||||||
|
f"Decorator that passes {doc_description} as the first argument"
|
||||||
|
" to the decorated function."
|
||||||
|
)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
CmdType = t.TypeVar("CmdType", bound=Command)
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def command(
|
||||||
|
__func: t.Callable[..., t.Any],
|
||||||
|
) -> Command:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def command(
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> t.Callable[..., Command]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def command(
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
cls: t.Type[CmdType] = ...,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> t.Callable[..., CmdType]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def command(
|
||||||
|
name: t.Union[str, t.Callable[..., t.Any], None] = None,
|
||||||
|
cls: t.Optional[t.Type[Command]] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> t.Union[Command, t.Callable[..., Command]]:
|
||||||
|
r"""Creates a new :class:`Command` and uses the decorated function as
|
||||||
|
callback. This will also automatically attach all decorated
|
||||||
|
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
||||||
|
|
||||||
|
The name of the command defaults to the name of the function with
|
||||||
|
underscores replaced by dashes. If you want to change that, you can
|
||||||
|
pass the intended name as the first argument.
|
||||||
|
|
||||||
|
All keyword arguments are forwarded to the underlying command class.
|
||||||
|
For the ``params`` argument, any decorated params are appended to
|
||||||
|
the end of the list.
|
||||||
|
|
||||||
|
Once decorated the function turns into a :class:`Command` instance
|
||||||
|
that can be invoked as a command line utility or be attached to a
|
||||||
|
command :class:`Group`.
|
||||||
|
|
||||||
|
:param name: the name of the command. This defaults to the function
|
||||||
|
name with underscores replaced by dashes.
|
||||||
|
:param cls: the command class to instantiate. This defaults to
|
||||||
|
:class:`Command`.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.1
|
||||||
|
This decorator can be applied without parentheses.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.1
|
||||||
|
The ``params`` argument can be used. Decorated params are
|
||||||
|
appended to the end of the list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
func: t.Optional[t.Callable[..., t.Any]] = None
|
||||||
|
|
||||||
|
if callable(name):
|
||||||
|
func = name
|
||||||
|
name = None
|
||||||
|
assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class."
|
||||||
|
assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."
|
||||||
|
|
||||||
|
if cls is None:
|
||||||
|
cls = Command
|
||||||
|
|
||||||
|
def decorator(f: t.Callable[..., t.Any]) -> Command:
|
||||||
|
if isinstance(f, Command):
|
||||||
|
raise TypeError("Attempted to convert a callback into a command twice.")
|
||||||
|
|
||||||
|
attr_params = attrs.pop("params", None)
|
||||||
|
params = attr_params if attr_params is not None else []
|
||||||
|
|
||||||
|
try:
|
||||||
|
decorator_params = f.__click_params__ # type: ignore
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
del f.__click_params__ # type: ignore
|
||||||
|
params.extend(reversed(decorator_params))
|
||||||
|
|
||||||
|
if attrs.get("help") is None:
|
||||||
|
attrs["help"] = f.__doc__
|
||||||
|
|
||||||
|
cmd = cls( # type: ignore[misc]
|
||||||
|
name=name or f.__name__.lower().replace("_", "-"), # type: ignore[arg-type]
|
||||||
|
callback=f,
|
||||||
|
params=params,
|
||||||
|
**attrs,
|
||||||
|
)
|
||||||
|
cmd.__doc__ = f.__doc__
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
if func is not None:
|
||||||
|
return decorator(func)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def group(
|
||||||
|
__func: t.Callable[..., t.Any],
|
||||||
|
) -> Group:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def group(
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> t.Callable[[F], Group]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def group(
|
||||||
|
name: t.Union[str, t.Callable[..., t.Any], None] = None, **attrs: t.Any
|
||||||
|
) -> t.Union[Group, t.Callable[[F], Group]]:
|
||||||
|
"""Creates a new :class:`Group` with a function as callback. This
|
||||||
|
works otherwise the same as :func:`command` just that the `cls`
|
||||||
|
parameter is set to :class:`Group`.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.1
|
||||||
|
This decorator can be applied without parentheses.
|
||||||
|
"""
|
||||||
|
if attrs.get("cls") is None:
|
||||||
|
attrs["cls"] = Group
|
||||||
|
|
||||||
|
if callable(name):
|
||||||
|
grp: t.Callable[[F], Group] = t.cast(Group, command(**attrs))
|
||||||
|
return grp(name)
|
||||||
|
|
||||||
|
return t.cast(Group, command(name, **attrs))
|
||||||
|
|
||||||
|
|
||||||
|
def _param_memo(f: FC, param: Parameter) -> None:
|
||||||
|
if isinstance(f, Command):
|
||||||
|
f.params.append(param)
|
||||||
|
else:
|
||||||
|
if not hasattr(f, "__click_params__"):
|
||||||
|
f.__click_params__ = [] # type: ignore
|
||||||
|
|
||||||
|
f.__click_params__.append(param) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Attaches an argument to the command. All positional arguments are
|
||||||
|
passed as parameter declarations to :class:`Argument`; all keyword
|
||||||
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
This is equivalent to creating an :class:`Argument` instance manually
|
||||||
|
and attaching it to the :attr:`Command.params` list.
|
||||||
|
|
||||||
|
:param cls: the argument class to instantiate. This defaults to
|
||||||
|
:class:`Argument`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: FC) -> FC:
|
||||||
|
ArgumentClass = attrs.pop("cls", None) or Argument
|
||||||
|
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Attaches an option to the command. All positional arguments are
|
||||||
|
passed as parameter declarations to :class:`Option`; all keyword
|
||||||
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
This is equivalent to creating an :class:`Option` instance manually
|
||||||
|
and attaching it to the :attr:`Command.params` list.
|
||||||
|
|
||||||
|
:param cls: the option class to instantiate. This defaults to
|
||||||
|
:class:`Option`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: FC) -> FC:
|
||||||
|
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
||||||
|
option_attrs = attrs.copy()
|
||||||
|
OptionClass = option_attrs.pop("cls", None) or Option
|
||||||
|
_param_memo(f, OptionClass(param_decls, **option_attrs))
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--yes`` option which shows a prompt before continuing if
|
||||||
|
not passed. If the prompt is declined, the program will exit.
|
||||||
|
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--yes"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
|
if not value:
|
||||||
|
ctx.abort()
|
||||||
|
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--yes",)
|
||||||
|
|
||||||
|
kwargs.setdefault("is_flag", True)
|
||||||
|
kwargs.setdefault("callback", callback)
|
||||||
|
kwargs.setdefault("expose_value", False)
|
||||||
|
kwargs.setdefault("prompt", "Do you want to continue?")
|
||||||
|
kwargs.setdefault("help", "Confirm the action without prompting.")
|
||||||
|
return option(*param_decls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--password`` option which prompts for a password, hiding
|
||||||
|
input and asking to enter the value again for confirmation.
|
||||||
|
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--password"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
"""
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--password",)
|
||||||
|
|
||||||
|
kwargs.setdefault("prompt", True)
|
||||||
|
kwargs.setdefault("confirmation_prompt", True)
|
||||||
|
kwargs.setdefault("hide_input", True)
|
||||||
|
return option(*param_decls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def version_option(
|
||||||
|
version: t.Optional[str] = None,
|
||||||
|
*param_decls: str,
|
||||||
|
package_name: t.Optional[str] = None,
|
||||||
|
prog_name: t.Optional[str] = None,
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--version`` option which immediately prints the version
|
||||||
|
number and exits the program.
|
||||||
|
|
||||||
|
If ``version`` is not provided, Click will try to detect it using
|
||||||
|
:func:`importlib.metadata.version` to get the version for the
|
||||||
|
``package_name``. On Python < 3.8, the ``importlib_metadata``
|
||||||
|
backport must be installed.
|
||||||
|
|
||||||
|
If ``package_name`` is not provided, Click will try to detect it by
|
||||||
|
inspecting the stack frames. This will be used to detect the
|
||||||
|
version, so it must match the name of the installed package.
|
||||||
|
|
||||||
|
:param version: The version number to show. If not provided, Click
|
||||||
|
will try to detect it.
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--version"``.
|
||||||
|
:param package_name: The package name to detect the version from. If
|
||||||
|
not provided, Click will try to detect it.
|
||||||
|
:param prog_name: The name of the CLI to show in the message. If not
|
||||||
|
provided, it will be detected from the command.
|
||||||
|
:param message: The message to show. The values ``%(prog)s``,
|
||||||
|
``%(package)s``, and ``%(version)s`` are available. Defaults to
|
||||||
|
``"%(prog)s, version %(version)s"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
:raise RuntimeError: ``version`` could not be detected.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Add the ``package_name`` parameter, and the ``%(package)s``
|
||||||
|
value for messages.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Use :mod:`importlib.metadata` instead of ``pkg_resources``. The
|
||||||
|
version is detected based on the package name, not the entry
|
||||||
|
point name. The Python package name must match the installed
|
||||||
|
package name, or be passed with ``package_name=``.
|
||||||
|
"""
|
||||||
|
if message is None:
|
||||||
|
message = _("%(prog)s, version %(version)s")
|
||||||
|
|
||||||
|
if version is None and package_name is None:
|
||||||
|
frame = inspect.currentframe()
|
||||||
|
f_back = frame.f_back if frame is not None else None
|
||||||
|
f_globals = f_back.f_globals if f_back is not None else None
|
||||||
|
# break reference cycle
|
||||||
|
# https://docs.python.org/3/library/inspect.html#the-interpreter-stack
|
||||||
|
del frame
|
||||||
|
|
||||||
|
if f_globals is not None:
|
||||||
|
package_name = f_globals.get("__name__")
|
||||||
|
|
||||||
|
if package_name == "__main__":
|
||||||
|
package_name = f_globals.get("__package__")
|
||||||
|
|
||||||
|
if package_name:
|
||||||
|
package_name = package_name.partition(".")[0]
|
||||||
|
|
||||||
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
nonlocal prog_name
|
||||||
|
nonlocal version
|
||||||
|
|
||||||
|
if prog_name is None:
|
||||||
|
prog_name = ctx.find_root().info_name
|
||||||
|
|
||||||
|
if version is None and package_name is not None:
|
||||||
|
metadata: t.Optional[types.ModuleType]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib import metadata # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
# Python < 3.8
|
||||||
|
import importlib_metadata as metadata # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = metadata.version(package_name) # type: ignore
|
||||||
|
except metadata.PackageNotFoundError: # type: ignore
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{package_name!r} is not installed. Try passing"
|
||||||
|
" 'package_name' instead."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
if version is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not determine the version for {package_name!r} automatically."
|
||||||
|
)
|
||||||
|
|
||||||
|
echo(
|
||||||
|
t.cast(str, message)
|
||||||
|
% {"prog": prog_name, "package": package_name, "version": version},
|
||||||
|
color=ctx.color,
|
||||||
|
)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--version",)
|
||||||
|
|
||||||
|
kwargs.setdefault("is_flag", True)
|
||||||
|
kwargs.setdefault("expose_value", False)
|
||||||
|
kwargs.setdefault("is_eager", True)
|
||||||
|
kwargs.setdefault("help", _("Show the version and exit."))
|
||||||
|
kwargs["callback"] = callback
|
||||||
|
return option(*param_decls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--help`` option which immediately prints the help page
|
||||||
|
and exits the program.
|
||||||
|
|
||||||
|
This is usually unnecessary, as the ``--help`` option is added to
|
||||||
|
each command automatically unless ``add_help_option=False`` is
|
||||||
|
passed.
|
||||||
|
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--help"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--help",)
|
||||||
|
|
||||||
|
kwargs.setdefault("is_flag", True)
|
||||||
|
kwargs.setdefault("expose_value", False)
|
||||||
|
kwargs.setdefault("is_eager", True)
|
||||||
|
kwargs.setdefault("help", _("Show this message and exit."))
|
||||||
|
kwargs["callback"] = callback
|
||||||
|
return option(*param_decls, **kwargs)
|
@ -0,0 +1,287 @@
|
|||||||
|
import os
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
|
|
||||||
|
from ._compat import get_text_stderr
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .core import Context
|
||||||
|
from .core import Parameter
|
||||||
|
|
||||||
|
|
||||||
|
def _join_param_hints(
|
||||||
|
param_hint: t.Optional[t.Union[t.Sequence[str], str]]
|
||||||
|
) -> t.Optional[str]:
|
||||||
|
if param_hint is not None and not isinstance(param_hint, str):
|
||||||
|
return " / ".join(repr(x) for x in param_hint)
|
||||||
|
|
||||||
|
return param_hint
|
||||||
|
|
||||||
|
|
||||||
|
class ClickException(Exception):
|
||||||
|
"""An exception that Click can handle and show to the user."""
|
||||||
|
|
||||||
|
#: The exit code for this exception.
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def format_message(self) -> str:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def show(self, file: t.Optional[t.IO] = None) -> None:
|
||||||
|
if file is None:
|
||||||
|
file = get_text_stderr()
|
||||||
|
|
||||||
|
echo(_("Error: {message}").format(message=self.format_message()), file=file)
|
||||||
|
|
||||||
|
|
||||||
|
class UsageError(ClickException):
|
||||||
|
"""An internal exception that signals a usage error. This typically
|
||||||
|
aborts any further handling.
|
||||||
|
|
||||||
|
:param message: the error message to display.
|
||||||
|
:param ctx: optionally the context that caused this error. Click will
|
||||||
|
fill in the context automatically in some situations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.ctx = ctx
|
||||||
|
self.cmd = self.ctx.command if self.ctx else None
|
||||||
|
|
||||||
|
def show(self, file: t.Optional[t.IO] = None) -> None:
|
||||||
|
if file is None:
|
||||||
|
file = get_text_stderr()
|
||||||
|
color = None
|
||||||
|
hint = ""
|
||||||
|
if (
|
||||||
|
self.ctx is not None
|
||||||
|
and self.ctx.command.get_help_option(self.ctx) is not None
|
||||||
|
):
|
||||||
|
hint = _("Try '{command} {option}' for help.").format(
|
||||||
|
command=self.ctx.command_path, option=self.ctx.help_option_names[0]
|
||||||
|
)
|
||||||
|
hint = f"{hint}\n"
|
||||||
|
if self.ctx is not None:
|
||||||
|
color = self.ctx.color
|
||||||
|
echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
|
||||||
|
echo(
|
||||||
|
_("Error: {message}").format(message=self.format_message()),
|
||||||
|
file=file,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BadParameter(UsageError):
|
||||||
|
"""An exception that formats out a standardized error message for a
|
||||||
|
bad parameter. This is useful when thrown from a callback or type as
|
||||||
|
Click will attach contextual information to it (for instance, which
|
||||||
|
parameter it is).
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param param: the parameter object that caused this error. This can
|
||||||
|
be left out, and Click will attach this info itself
|
||||||
|
if possible.
|
||||||
|
:param param_hint: a string that shows up as parameter name. This
|
||||||
|
can be used as alternative to `param` in cases
|
||||||
|
where custom validation should happen. If it is
|
||||||
|
a string it's used as such, if it's a list then
|
||||||
|
each item is quoted and separated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
param_hint: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message, ctx)
|
||||||
|
self.param = param
|
||||||
|
self.param_hint = param_hint
|
||||||
|
|
||||||
|
def format_message(self) -> str:
|
||||||
|
if self.param_hint is not None:
|
||||||
|
param_hint = self.param_hint
|
||||||
|
elif self.param is not None:
|
||||||
|
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
||||||
|
else:
|
||||||
|
return _("Invalid value: {message}").format(message=self.message)
|
||||||
|
|
||||||
|
return _("Invalid value for {param_hint}: {message}").format(
|
||||||
|
param_hint=_join_param_hints(param_hint), message=self.message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParameter(BadParameter):
|
||||||
|
"""Raised if click required an option or argument but it was not
|
||||||
|
provided when invoking the script.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:param param_type: a string that indicates the type of the parameter.
|
||||||
|
The default is to inherit the parameter type from
|
||||||
|
the given `param`. Valid values are ``'parameter'``,
|
||||||
|
``'option'`` or ``'argument'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
param_hint: t.Optional[str] = None,
|
||||||
|
param_type: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message or "", ctx, param, param_hint)
|
||||||
|
self.param_type = param_type
|
||||||
|
|
||||||
|
def format_message(self) -> str:
|
||||||
|
if self.param_hint is not None:
|
||||||
|
param_hint: t.Optional[str] = self.param_hint
|
||||||
|
elif self.param is not None:
|
||||||
|
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
||||||
|
else:
|
||||||
|
param_hint = None
|
||||||
|
|
||||||
|
param_hint = _join_param_hints(param_hint)
|
||||||
|
param_hint = f" {param_hint}" if param_hint else ""
|
||||||
|
|
||||||
|
param_type = self.param_type
|
||||||
|
if param_type is None and self.param is not None:
|
||||||
|
param_type = self.param.param_type_name
|
||||||
|
|
||||||
|
msg = self.message
|
||||||
|
if self.param is not None:
|
||||||
|
msg_extra = self.param.type.get_missing_message(self.param)
|
||||||
|
if msg_extra:
|
||||||
|
if msg:
|
||||||
|
msg += f". {msg_extra}"
|
||||||
|
else:
|
||||||
|
msg = msg_extra
|
||||||
|
|
||||||
|
msg = f" {msg}" if msg else ""
|
||||||
|
|
||||||
|
# Translate param_type for known types.
|
||||||
|
if param_type == "argument":
|
||||||
|
missing = _("Missing argument")
|
||||||
|
elif param_type == "option":
|
||||||
|
missing = _("Missing option")
|
||||||
|
elif param_type == "parameter":
|
||||||
|
missing = _("Missing parameter")
|
||||||
|
else:
|
||||||
|
missing = _("Missing {param_type}").format(param_type=param_type)
|
||||||
|
|
||||||
|
return f"{missing}{param_hint}.{msg}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if not self.message:
|
||||||
|
param_name = self.param.name if self.param else None
|
||||||
|
return _("Missing parameter: {param_name}").format(param_name=param_name)
|
||||||
|
else:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchOption(UsageError):
|
||||||
|
"""Raised if click attempted to handle an option that does not
|
||||||
|
exist.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
option_name: str,
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
possibilities: t.Optional[t.Sequence[str]] = None,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
) -> None:
|
||||||
|
if message is None:
|
||||||
|
message = _("No such option: {name}").format(name=option_name)
|
||||||
|
|
||||||
|
super().__init__(message, ctx)
|
||||||
|
self.option_name = option_name
|
||||||
|
self.possibilities = possibilities
|
||||||
|
|
||||||
|
def format_message(self) -> str:
|
||||||
|
if not self.possibilities:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
possibility_str = ", ".join(sorted(self.possibilities))
|
||||||
|
suggest = ngettext(
|
||||||
|
"Did you mean {possibility}?",
|
||||||
|
"(Possible options: {possibilities})",
|
||||||
|
len(self.possibilities),
|
||||||
|
).format(possibility=possibility_str, possibilities=possibility_str)
|
||||||
|
return f"{self.message} {suggest}"
|
||||||
|
|
||||||
|
|
||||||
|
class BadOptionUsage(UsageError):
|
||||||
|
"""Raised if an option is generally supplied but the use of the option
|
||||||
|
was incorrect. This is for instance raised if the number of arguments
|
||||||
|
for an option is not correct.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:param option_name: the name of the option being used incorrectly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, option_name: str, message: str, ctx: t.Optional["Context"] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message, ctx)
|
||||||
|
self.option_name = option_name
|
||||||
|
|
||||||
|
|
||||||
|
class BadArgumentUsage(UsageError):
|
||||||
|
"""Raised if an argument is generally supplied but the use of the argument
|
||||||
|
was incorrect. This is for instance raised if the number of values
|
||||||
|
for an argument is not correct.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FileError(ClickException):
|
||||||
|
"""Raised if a file cannot be opened."""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, hint: t.Optional[str] = None) -> None:
|
||||||
|
if hint is None:
|
||||||
|
hint = _("unknown error")
|
||||||
|
|
||||||
|
super().__init__(hint)
|
||||||
|
self.ui_filename = os.fsdecode(filename)
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
def format_message(self) -> str:
|
||||||
|
return _("Could not open file {filename!r}: {message}").format(
|
||||||
|
filename=self.ui_filename, message=self.message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Abort(RuntimeError):
|
||||||
|
"""An internal signalling exception that signals Click to abort."""
|
||||||
|
|
||||||
|
|
||||||
|
class Exit(RuntimeError):
|
||||||
|
"""An exception that indicates that the application should exit with some
|
||||||
|
status code.
|
||||||
|
|
||||||
|
:param code: the status code to exit with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("exit_code",)
|
||||||
|
|
||||||
|
def __init__(self, code: int = 0) -> None:
|
||||||
|
self.exit_code = code
|
@ -0,0 +1,301 @@
|
|||||||
|
import typing as t
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from ._compat import term_len
|
||||||
|
from .parser import split_opt
|
||||||
|
|
||||||
|
# Can force a width. This is used by the test system
|
||||||
|
FORCED_WIDTH: t.Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]:
|
||||||
|
widths: t.Dict[int, int] = {}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
for idx, col in enumerate(row):
|
||||||
|
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
||||||
|
|
||||||
|
return tuple(y for x, y in sorted(widths.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def iter_rows(
|
||||||
|
rows: t.Iterable[t.Tuple[str, str]], col_count: int
|
||||||
|
) -> t.Iterator[t.Tuple[str, ...]]:
|
||||||
|
for row in rows:
|
||||||
|
yield row + ("",) * (col_count - len(row))
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text(
|
||||||
|
text: str,
|
||||||
|
width: int = 78,
|
||||||
|
initial_indent: str = "",
|
||||||
|
subsequent_indent: str = "",
|
||||||
|
preserve_paragraphs: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""A helper function that intelligently wraps text. By default, it
|
||||||
|
assumes that it operates on a single paragraph of text but if the
|
||||||
|
`preserve_paragraphs` parameter is provided it will intelligently
|
||||||
|
handle paragraphs (defined by two empty lines).
|
||||||
|
|
||||||
|
If paragraphs are handled, a paragraph can be prefixed with an empty
|
||||||
|
line containing the ``\\b`` character (``\\x08``) to indicate that
|
||||||
|
no rewrapping should happen in that block.
|
||||||
|
|
||||||
|
:param text: the text that should be rewrapped.
|
||||||
|
:param width: the maximum width for the text.
|
||||||
|
:param initial_indent: the initial indent that should be placed on the
|
||||||
|
first line as a string.
|
||||||
|
:param subsequent_indent: the indent string that should be placed on
|
||||||
|
each consecutive line.
|
||||||
|
:param preserve_paragraphs: if this flag is set then the wrapping will
|
||||||
|
intelligently handle paragraphs.
|
||||||
|
"""
|
||||||
|
from ._textwrap import TextWrapper
|
||||||
|
|
||||||
|
text = text.expandtabs()
|
||||||
|
wrapper = TextWrapper(
|
||||||
|
width,
|
||||||
|
initial_indent=initial_indent,
|
||||||
|
subsequent_indent=subsequent_indent,
|
||||||
|
replace_whitespace=False,
|
||||||
|
)
|
||||||
|
if not preserve_paragraphs:
|
||||||
|
return wrapper.fill(text)
|
||||||
|
|
||||||
|
p: t.List[t.Tuple[int, bool, str]] = []
|
||||||
|
buf: t.List[str] = []
|
||||||
|
indent = None
|
||||||
|
|
||||||
|
def _flush_par() -> None:
|
||||||
|
if not buf:
|
||||||
|
return
|
||||||
|
if buf[0].strip() == "\b":
|
||||||
|
p.append((indent or 0, True, "\n".join(buf[1:])))
|
||||||
|
else:
|
||||||
|
p.append((indent or 0, False, " ".join(buf)))
|
||||||
|
del buf[:]
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
if not line:
|
||||||
|
_flush_par()
|
||||||
|
indent = None
|
||||||
|
else:
|
||||||
|
if indent is None:
|
||||||
|
orig_len = term_len(line)
|
||||||
|
line = line.lstrip()
|
||||||
|
indent = orig_len - term_len(line)
|
||||||
|
buf.append(line)
|
||||||
|
_flush_par()
|
||||||
|
|
||||||
|
rv = []
|
||||||
|
for indent, raw, text in p:
|
||||||
|
with wrapper.extra_indent(" " * indent):
|
||||||
|
if raw:
|
||||||
|
rv.append(wrapper.indent_only(text))
|
||||||
|
else:
|
||||||
|
rv.append(wrapper.fill(text))
|
||||||
|
|
||||||
|
return "\n\n".join(rv)
|
||||||
|
|
||||||
|
|
||||||
|
class HelpFormatter:
|
||||||
|
"""This class helps with formatting text-based help pages. It's
|
||||||
|
usually just needed for very special internal cases, but it's also
|
||||||
|
exposed so that developers can write their own fancy outputs.
|
||||||
|
|
||||||
|
At present, it always writes into memory.
|
||||||
|
|
||||||
|
:param indent_increment: the additional increment for each level.
|
||||||
|
:param width: the width for the text. This defaults to the terminal
|
||||||
|
width clamped to a maximum of 78.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
indent_increment: int = 2,
|
||||||
|
width: t.Optional[int] = None,
|
||||||
|
max_width: t.Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
self.indent_increment = indent_increment
|
||||||
|
if max_width is None:
|
||||||
|
max_width = 80
|
||||||
|
if width is None:
|
||||||
|
width = FORCED_WIDTH
|
||||||
|
if width is None:
|
||||||
|
width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
|
||||||
|
self.width = width
|
||||||
|
self.current_indent = 0
|
||||||
|
self.buffer: t.List[str] = []
|
||||||
|
|
||||||
|
def write(self, string: str) -> None:
|
||||||
|
"""Writes a unicode string into the internal buffer."""
|
||||||
|
self.buffer.append(string)
|
||||||
|
|
||||||
|
def indent(self) -> None:
|
||||||
|
"""Increases the indentation."""
|
||||||
|
self.current_indent += self.indent_increment
|
||||||
|
|
||||||
|
def dedent(self) -> None:
|
||||||
|
"""Decreases the indentation."""
|
||||||
|
self.current_indent -= self.indent_increment
|
||||||
|
|
||||||
|
def write_usage(
|
||||||
|
self, prog: str, args: str = "", prefix: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Writes a usage line into the buffer.
|
||||||
|
|
||||||
|
:param prog: the program name.
|
||||||
|
:param args: whitespace separated list of arguments.
|
||||||
|
:param prefix: The prefix for the first line. Defaults to
|
||||||
|
``"Usage: "``.
|
||||||
|
"""
|
||||||
|
if prefix is None:
|
||||||
|
prefix = f"{_('Usage:')} "
|
||||||
|
|
||||||
|
usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
|
||||||
|
text_width = self.width - self.current_indent
|
||||||
|
|
||||||
|
if text_width >= (term_len(usage_prefix) + 20):
|
||||||
|
# The arguments will fit to the right of the prefix.
|
||||||
|
indent = " " * term_len(usage_prefix)
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
args,
|
||||||
|
text_width,
|
||||||
|
initial_indent=usage_prefix,
|
||||||
|
subsequent_indent=indent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# The prefix is too long, put the arguments on the next line.
|
||||||
|
self.write(usage_prefix)
|
||||||
|
self.write("\n")
|
||||||
|
indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
args, text_width, initial_indent=indent, subsequent_indent=indent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_heading(self, heading: str) -> None:
|
||||||
|
"""Writes a heading into the buffer."""
|
||||||
|
self.write(f"{'':>{self.current_indent}}{heading}:\n")
|
||||||
|
|
||||||
|
def write_paragraph(self) -> None:
|
||||||
|
"""Writes a paragraph into the buffer."""
|
||||||
|
if self.buffer:
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_text(self, text: str) -> None:
|
||||||
|
"""Writes re-indented text into the buffer. This rewraps and
|
||||||
|
preserves paragraphs.
|
||||||
|
"""
|
||||||
|
indent = " " * self.current_indent
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
text,
|
||||||
|
self.width,
|
||||||
|
initial_indent=indent,
|
||||||
|
subsequent_indent=indent,
|
||||||
|
preserve_paragraphs=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_dl(
|
||||||
|
self,
|
||||||
|
rows: t.Sequence[t.Tuple[str, str]],
|
||||||
|
col_max: int = 30,
|
||||||
|
col_spacing: int = 2,
|
||||||
|
) -> None:
|
||||||
|
"""Writes a definition list into the buffer. This is how options
|
||||||
|
and commands are usually formatted.
|
||||||
|
|
||||||
|
:param rows: a list of two item tuples for the terms and values.
|
||||||
|
:param col_max: the maximum width of the first column.
|
||||||
|
:param col_spacing: the number of spaces between the first and
|
||||||
|
second column.
|
||||||
|
"""
|
||||||
|
rows = list(rows)
|
||||||
|
widths = measure_table(rows)
|
||||||
|
if len(widths) != 2:
|
||||||
|
raise TypeError("Expected two columns for definition list")
|
||||||
|
|
||||||
|
first_col = min(widths[0], col_max) + col_spacing
|
||||||
|
|
||||||
|
for first, second in iter_rows(rows, len(widths)):
|
||||||
|
self.write(f"{'':>{self.current_indent}}{first}")
|
||||||
|
if not second:
|
||||||
|
self.write("\n")
|
||||||
|
continue
|
||||||
|
if term_len(first) <= first_col - col_spacing:
|
||||||
|
self.write(" " * (first_col - term_len(first)))
|
||||||
|
else:
|
||||||
|
self.write("\n")
|
||||||
|
self.write(" " * (first_col + self.current_indent))
|
||||||
|
|
||||||
|
text_width = max(self.width - first_col - 2, 10)
|
||||||
|
wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
|
||||||
|
lines = wrapped_text.splitlines()
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
self.write(f"{lines[0]}\n")
|
||||||
|
|
||||||
|
for line in lines[1:]:
|
||||||
|
self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
|
||||||
|
else:
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def section(self, name: str) -> t.Iterator[None]:
|
||||||
|
"""Helpful context manager that writes a paragraph, a heading,
|
||||||
|
and the indents.
|
||||||
|
|
||||||
|
:param name: the section name that is written as heading.
|
||||||
|
"""
|
||||||
|
self.write_paragraph()
|
||||||
|
self.write_heading(name)
|
||||||
|
self.indent()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.dedent()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def indentation(self) -> t.Iterator[None]:
|
||||||
|
"""A context manager that increases the indentation."""
|
||||||
|
self.indent()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.dedent()
|
||||||
|
|
||||||
|
def getvalue(self) -> str:
|
||||||
|
"""Returns the buffer contents."""
|
||||||
|
return "".join(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
|
def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]:
|
||||||
|
"""Given a list of option strings this joins them in the most appropriate
|
||||||
|
way and returns them in the form ``(formatted_string,
|
||||||
|
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
||||||
|
indicates if any of the option prefixes was a slash.
|
||||||
|
"""
|
||||||
|
rv = []
|
||||||
|
any_prefix_is_slash = False
|
||||||
|
|
||||||
|
for opt in options:
|
||||||
|
prefix = split_opt(opt)[0]
|
||||||
|
|
||||||
|
if prefix == "/":
|
||||||
|
any_prefix_is_slash = True
|
||||||
|
|
||||||
|
rv.append((len(prefix), opt))
|
||||||
|
|
||||||
|
rv.sort(key=lambda x: x[0])
|
||||||
|
return ", ".join(x[1] for x in rv), any_prefix_is_slash
|
@ -0,0 +1,68 @@
|
|||||||
|
import typing as t
|
||||||
|
from threading import local
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .core import Context
|
||||||
|
|
||||||
|
_local = local()
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def get_current_context(silent: "te.Literal[False]" = False) -> "Context":
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@t.overload
|
||||||
|
def get_current_context(silent: bool = ...) -> t.Optional["Context"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_context(silent: bool = False) -> t.Optional["Context"]:
|
||||||
|
"""Returns the current click context. This can be used as a way to
|
||||||
|
access the current context object from anywhere. This is a more implicit
|
||||||
|
alternative to the :func:`pass_context` decorator. This function is
|
||||||
|
primarily useful for helpers such as :func:`echo` which might be
|
||||||
|
interested in changing its behavior based on the current context.
|
||||||
|
|
||||||
|
To push the current context, :meth:`Context.scope` can be used.
|
||||||
|
|
||||||
|
.. versionadded:: 5.0
|
||||||
|
|
||||||
|
:param silent: if set to `True` the return value is `None` if no context
|
||||||
|
is available. The default behavior is to raise a
|
||||||
|
:exc:`RuntimeError`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return t.cast("Context", _local.stack[-1])
|
||||||
|
except (AttributeError, IndexError) as e:
|
||||||
|
if not silent:
|
||||||
|
raise RuntimeError("There is no active click context.") from e
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def push_context(ctx: "Context") -> None:
|
||||||
|
"""Pushes a new context to the current stack."""
|
||||||
|
_local.__dict__.setdefault("stack", []).append(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def pop_context() -> None:
|
||||||
|
"""Removes the top level from the stack."""
|
||||||
|
_local.stack.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]:
|
||||||
|
"""Internal helper to get the default value of the color flag. If a
|
||||||
|
value is passed it's returned unchanged, otherwise it's looked up from
|
||||||
|
the current context.
|
||||||
|
"""
|
||||||
|
if color is not None:
|
||||||
|
return color
|
||||||
|
|
||||||
|
ctx = get_current_context(silent=True)
|
||||||
|
|
||||||
|
if ctx is not None:
|
||||||
|
return ctx.color
|
||||||
|
|
||||||
|
return None
|
@ -0,0 +1,529 @@
|
|||||||
|
"""
|
||||||
|
This module started out as largely a copy paste from the stdlib's
|
||||||
|
optparse module with the features removed that we do not need from
|
||||||
|
optparse because we implement them in Click on a higher level (for
|
||||||
|
instance type handling, help formatting and a lot more).
|
||||||
|
|
||||||
|
The plan is to remove more and more from here over time.
|
||||||
|
|
||||||
|
The reason this is a different module and not optparse from the stdlib
|
||||||
|
is that there are differences in 2.x and 3.x about the error messages
|
||||||
|
generated and optparse in the stdlib uses gettext for no good reason
|
||||||
|
and might cause us issues.
|
||||||
|
|
||||||
|
Click uses parts of optparse written by Gregory P. Ward and maintained
|
||||||
|
by the Python Software Foundation. This is limited to code in parser.py.
|
||||||
|
|
||||||
|
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
||||||
|
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
||||||
|
"""
|
||||||
|
# This code uses parts of optparse written by Gregory P. Ward and
|
||||||
|
# maintained by the Python Software Foundation.
|
||||||
|
# Copyright 2001-2006 Gregory P. Ward
|
||||||
|
# Copyright 2002-2006 Python Software Foundation
|
||||||
|
import typing as t
|
||||||
|
from collections import deque
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
|
|
||||||
|
from .exceptions import BadArgumentUsage
|
||||||
|
from .exceptions import BadOptionUsage
|
||||||
|
from .exceptions import NoSuchOption
|
||||||
|
from .exceptions import UsageError
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .core import Argument as CoreArgument
|
||||||
|
from .core import Context
|
||||||
|
from .core import Option as CoreOption
|
||||||
|
from .core import Parameter as CoreParameter
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
# Sentinel value that indicates an option was passed as a flag without a
|
||||||
|
# value but is not a flag option. Option.consume_value uses this to
|
||||||
|
# prompt or use the flag_value.
|
||||||
|
_flag_needs_value = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_args(
|
||||||
|
args: t.Sequence[str], nargs_spec: t.Sequence[int]
|
||||||
|
) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]:
|
||||||
|
"""Given an iterable of arguments and an iterable of nargs specifications,
|
||||||
|
it returns a tuple with all the unpacked arguments at the first index
|
||||||
|
and all remaining arguments as the second.
|
||||||
|
|
||||||
|
The nargs specification is the number of arguments that should be consumed
|
||||||
|
or `-1` to indicate that this position should eat up all the remainders.
|
||||||
|
|
||||||
|
Missing items are filled with `None`.
|
||||||
|
"""
|
||||||
|
args = deque(args)
|
||||||
|
nargs_spec = deque(nargs_spec)
|
||||||
|
rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []
|
||||||
|
spos: t.Optional[int] = None
|
||||||
|
|
||||||
|
def _fetch(c: "te.Deque[V]") -> t.Optional[V]:
|
||||||
|
try:
|
||||||
|
if spos is None:
|
||||||
|
return c.popleft()
|
||||||
|
else:
|
||||||
|
return c.pop()
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
while nargs_spec:
|
||||||
|
nargs = _fetch(nargs_spec)
|
||||||
|
|
||||||
|
if nargs is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nargs == 1:
|
||||||
|
rv.append(_fetch(args))
|
||||||
|
elif nargs > 1:
|
||||||
|
x = [_fetch(args) for _ in range(nargs)]
|
||||||
|
|
||||||
|
# If we're reversed, we're pulling in the arguments in reverse,
|
||||||
|
# so we need to turn them around.
|
||||||
|
if spos is not None:
|
||||||
|
x.reverse()
|
||||||
|
|
||||||
|
rv.append(tuple(x))
|
||||||
|
elif nargs < 0:
|
||||||
|
if spos is not None:
|
||||||
|
raise TypeError("Cannot have two nargs < 0")
|
||||||
|
|
||||||
|
spos = len(rv)
|
||||||
|
rv.append(None)
|
||||||
|
|
||||||
|
# spos is the position of the wildcard (star). If it's not `None`,
|
||||||
|
# we fill it with the remainder.
|
||||||
|
if spos is not None:
|
||||||
|
rv[spos] = tuple(args)
|
||||||
|
args = []
|
||||||
|
rv[spos + 1 :] = reversed(rv[spos + 1 :])
|
||||||
|
|
||||||
|
return tuple(rv), list(args)
|
||||||
|
|
||||||
|
|
||||||
|
def split_opt(opt: str) -> t.Tuple[str, str]:
|
||||||
|
first = opt[:1]
|
||||||
|
if first.isalnum():
|
||||||
|
return "", opt
|
||||||
|
if opt[1:2] == first:
|
||||||
|
return opt[:2], opt[2:]
|
||||||
|
return first, opt[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str:
|
||||||
|
if ctx is None or ctx.token_normalize_func is None:
|
||||||
|
return opt
|
||||||
|
prefix, opt = split_opt(opt)
|
||||||
|
return f"{prefix}{ctx.token_normalize_func(opt)}"
|
||||||
|
|
||||||
|
|
||||||
|
def split_arg_string(string: str) -> t.List[str]:
|
||||||
|
"""Split an argument string as with :func:`shlex.split`, but don't
|
||||||
|
fail if the string is incomplete. Ignores a missing closing quote or
|
||||||
|
incomplete escape sequence and uses the partial token as-is.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
split_arg_string("example 'my file")
|
||||||
|
["example", "my file"]
|
||||||
|
|
||||||
|
split_arg_string("example my\\")
|
||||||
|
["example", "my"]
|
||||||
|
|
||||||
|
:param string: String to split.
|
||||||
|
"""
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
lex = shlex.shlex(string, posix=True)
|
||||||
|
lex.whitespace_split = True
|
||||||
|
lex.commenters = ""
|
||||||
|
out = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for token in lex:
|
||||||
|
out.append(token)
|
||||||
|
except ValueError:
|
||||||
|
# Raised when end-of-string is reached in an invalid state. Use
|
||||||
|
# the partial token as-is. The quote or escape character is in
|
||||||
|
# lex.state, not lex.token.
|
||||||
|
out.append(lex.token)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class Option:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj: "CoreOption",
|
||||||
|
opts: t.Sequence[str],
|
||||||
|
dest: t.Optional[str],
|
||||||
|
action: t.Optional[str] = None,
|
||||||
|
nargs: int = 1,
|
||||||
|
const: t.Optional[t.Any] = None,
|
||||||
|
):
|
||||||
|
self._short_opts = []
|
||||||
|
self._long_opts = []
|
||||||
|
self.prefixes = set()
|
||||||
|
|
||||||
|
for opt in opts:
|
||||||
|
prefix, value = split_opt(opt)
|
||||||
|
if not prefix:
|
||||||
|
raise ValueError(f"Invalid start character for option ({opt})")
|
||||||
|
self.prefixes.add(prefix[0])
|
||||||
|
if len(prefix) == 1 and len(value) == 1:
|
||||||
|
self._short_opts.append(opt)
|
||||||
|
else:
|
||||||
|
self._long_opts.append(opt)
|
||||||
|
self.prefixes.add(prefix)
|
||||||
|
|
||||||
|
if action is None:
|
||||||
|
action = "store"
|
||||||
|
|
||||||
|
self.dest = dest
|
||||||
|
self.action = action
|
||||||
|
self.nargs = nargs
|
||||||
|
self.const = const
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
@property
|
||||||
|
def takes_value(self) -> bool:
|
||||||
|
return self.action in ("store", "append")
|
||||||
|
|
||||||
|
def process(self, value: str, state: "ParsingState") -> None:
|
||||||
|
if self.action == "store":
|
||||||
|
state.opts[self.dest] = value # type: ignore
|
||||||
|
elif self.action == "store_const":
|
||||||
|
state.opts[self.dest] = self.const # type: ignore
|
||||||
|
elif self.action == "append":
|
||||||
|
state.opts.setdefault(self.dest, []).append(value) # type: ignore
|
||||||
|
elif self.action == "append_const":
|
||||||
|
state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
|
||||||
|
elif self.action == "count":
|
||||||
|
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown action '{self.action}'")
|
||||||
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Argument:
|
||||||
|
def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):
|
||||||
|
self.dest = dest
|
||||||
|
self.nargs = nargs
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def process(
|
||||||
|
self,
|
||||||
|
value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]],
|
||||||
|
state: "ParsingState",
|
||||||
|
) -> None:
|
||||||
|
if self.nargs > 1:
|
||||||
|
assert value is not None
|
||||||
|
holes = sum(1 for x in value if x is None)
|
||||||
|
if holes == len(value):
|
||||||
|
value = None
|
||||||
|
elif holes != 0:
|
||||||
|
raise BadArgumentUsage(
|
||||||
|
_("Argument {name!r} takes {nargs} values.").format(
|
||||||
|
name=self.dest, nargs=self.nargs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.nargs == -1 and self.obj.envvar is not None and value == ():
|
||||||
|
# Replace empty tuple with None so that a value from the
|
||||||
|
# environment may be tried.
|
||||||
|
value = None
|
||||||
|
|
||||||
|
state.opts[self.dest] = value # type: ignore
|
||||||
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ParsingState:
|
||||||
|
def __init__(self, rargs: t.List[str]) -> None:
|
||||||
|
self.opts: t.Dict[str, t.Any] = {}
|
||||||
|
self.largs: t.List[str] = []
|
||||||
|
self.rargs = rargs
|
||||||
|
self.order: t.List["CoreParameter"] = []
|
||||||
|
|
||||||
|
|
||||||
|
class OptionParser:
|
||||||
|
"""The option parser is an internal class that is ultimately used to
|
||||||
|
parse options and arguments. It's modelled after optparse and brings
|
||||||
|
a similar but vastly simplified API. It should generally not be used
|
||||||
|
directly as the high level Click classes wrap it for you.
|
||||||
|
|
||||||
|
It's not nearly as extensible as optparse or argparse as it does not
|
||||||
|
implement features that are implemented on a higher level (such as
|
||||||
|
types or defaults).
|
||||||
|
|
||||||
|
:param ctx: optionally the :class:`~click.Context` where this parser
|
||||||
|
should go with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ctx: t.Optional["Context"] = None) -> None:
|
||||||
|
#: The :class:`~click.Context` for this parser. This might be
|
||||||
|
#: `None` for some advanced use cases.
|
||||||
|
self.ctx = ctx
|
||||||
|
#: This controls how the parser deals with interspersed arguments.
|
||||||
|
#: If this is set to `False`, the parser will stop on the first
|
||||||
|
#: non-option. Click uses this to implement nested subcommands
|
||||||
|
#: safely.
|
||||||
|
self.allow_interspersed_args = True
|
||||||
|
#: This tells the parser how to deal with unknown options. By
|
||||||
|
#: default it will error out (which is sensible), but there is a
|
||||||
|
#: second mode where it will ignore it and continue processing
|
||||||
|
#: after shifting all the unknown options into the resulting args.
|
||||||
|
self.ignore_unknown_options = False
|
||||||
|
|
||||||
|
if ctx is not None:
|
||||||
|
self.allow_interspersed_args = ctx.allow_interspersed_args
|
||||||
|
self.ignore_unknown_options = ctx.ignore_unknown_options
|
||||||
|
|
||||||
|
self._short_opt: t.Dict[str, Option] = {}
|
||||||
|
self._long_opt: t.Dict[str, Option] = {}
|
||||||
|
self._opt_prefixes = {"-", "--"}
|
||||||
|
self._args: t.List[Argument] = []
|
||||||
|
|
||||||
|
def add_option(
|
||||||
|
self,
|
||||||
|
obj: "CoreOption",
|
||||||
|
opts: t.Sequence[str],
|
||||||
|
dest: t.Optional[str],
|
||||||
|
action: t.Optional[str] = None,
|
||||||
|
nargs: int = 1,
|
||||||
|
const: t.Optional[t.Any] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Adds a new option named `dest` to the parser. The destination
|
||||||
|
is not inferred (unlike with optparse) and needs to be explicitly
|
||||||
|
provided. Action can be any of ``store``, ``store_const``,
|
||||||
|
``append``, ``append_const`` or ``count``.
|
||||||
|
|
||||||
|
The `obj` can be used to identify the option in the order list
|
||||||
|
that is returned from the parser.
|
||||||
|
"""
|
||||||
|
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
||||||
|
option = Option(obj, opts, dest, action=action, nargs=nargs, const=const)
|
||||||
|
self._opt_prefixes.update(option.prefixes)
|
||||||
|
for opt in option._short_opts:
|
||||||
|
self._short_opt[opt] = option
|
||||||
|
for opt in option._long_opts:
|
||||||
|
self._long_opt[opt] = option
|
||||||
|
|
||||||
|
def add_argument(
|
||||||
|
self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1
|
||||||
|
) -> None:
|
||||||
|
"""Adds a positional argument named `dest` to the parser.
|
||||||
|
|
||||||
|
The `obj` can be used to identify the option in the order list
|
||||||
|
that is returned from the parser.
|
||||||
|
"""
|
||||||
|
self._args.append(Argument(obj, dest=dest, nargs=nargs))
|
||||||
|
|
||||||
|
def parse_args(
|
||||||
|
self, args: t.List[str]
|
||||||
|
) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]:
|
||||||
|
"""Parses positional arguments and returns ``(values, args, order)``
|
||||||
|
for the parsed options and arguments as well as the leftover
|
||||||
|
arguments if there are any. The order is a list of objects as they
|
||||||
|
appear on the command line. If arguments appear multiple times they
|
||||||
|
will be memorized multiple times as well.
|
||||||
|
"""
|
||||||
|
state = ParsingState(args)
|
||||||
|
try:
|
||||||
|
self._process_args_for_options(state)
|
||||||
|
self._process_args_for_args(state)
|
||||||
|
except UsageError:
|
||||||
|
if self.ctx is None or not self.ctx.resilient_parsing:
|
||||||
|
raise
|
||||||
|
return state.opts, state.largs, state.order
|
||||||
|
|
||||||
|
def _process_args_for_args(self, state: ParsingState) -> None:
|
||||||
|
pargs, args = _unpack_args(
|
||||||
|
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, arg in enumerate(self._args):
|
||||||
|
arg.process(pargs[idx], state)
|
||||||
|
|
||||||
|
state.largs = args
|
||||||
|
state.rargs = []
|
||||||
|
|
||||||
|
def _process_args_for_options(self, state: ParsingState) -> None:
|
||||||
|
while state.rargs:
|
||||||
|
arg = state.rargs.pop(0)
|
||||||
|
arglen = len(arg)
|
||||||
|
# Double dashes always handled explicitly regardless of what
|
||||||
|
# prefixes are valid.
|
||||||
|
if arg == "--":
|
||||||
|
return
|
||||||
|
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
||||||
|
self._process_opts(arg, state)
|
||||||
|
elif self.allow_interspersed_args:
|
||||||
|
state.largs.append(arg)
|
||||||
|
else:
|
||||||
|
state.rargs.insert(0, arg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Say this is the original argument list:
|
||||||
|
# [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
|
||||||
|
# ^
|
||||||
|
# (we are about to process arg(i)).
|
||||||
|
#
|
||||||
|
# Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
|
||||||
|
# [arg0, ..., arg(i-1)] (any options and their arguments will have
|
||||||
|
# been removed from largs).
|
||||||
|
#
|
||||||
|
# The while loop will usually consume 1 or more arguments per pass.
|
||||||
|
# If it consumes 1 (eg. arg is an option that takes no arguments),
|
||||||
|
# then after _process_arg() is done the situation is:
|
||||||
|
#
|
||||||
|
# largs = subset of [arg0, ..., arg(i)]
|
||||||
|
# rargs = [arg(i+1), ..., arg(N-1)]
|
||||||
|
#
|
||||||
|
# If allow_interspersed_args is false, largs will always be
|
||||||
|
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
||||||
|
# not a very interesting subset!
|
||||||
|
|
||||||
|
def _match_long_opt(
|
||||||
|
self, opt: str, explicit_value: t.Optional[str], state: ParsingState
|
||||||
|
) -> None:
|
||||||
|
if opt not in self._long_opt:
|
||||||
|
from difflib import get_close_matches
|
||||||
|
|
||||||
|
possibilities = get_close_matches(opt, self._long_opt)
|
||||||
|
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
||||||
|
|
||||||
|
option = self._long_opt[opt]
|
||||||
|
if option.takes_value:
|
||||||
|
# At this point it's safe to modify rargs by injecting the
|
||||||
|
# explicit value, because no exception is raised in this
|
||||||
|
# branch. This means that the inserted value will be fully
|
||||||
|
# consumed.
|
||||||
|
if explicit_value is not None:
|
||||||
|
state.rargs.insert(0, explicit_value)
|
||||||
|
|
||||||
|
value = self._get_value_from_state(opt, option, state)
|
||||||
|
|
||||||
|
elif explicit_value is not None:
|
||||||
|
raise BadOptionUsage(
|
||||||
|
opt, _("Option {name!r} does not take a value.").format(name=opt)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
option.process(value, state)
|
||||||
|
|
||||||
|
def _match_short_opt(self, arg: str, state: ParsingState) -> None:
|
||||||
|
stop = False
|
||||||
|
i = 1
|
||||||
|
prefix = arg[0]
|
||||||
|
unknown_options = []
|
||||||
|
|
||||||
|
for ch in arg[1:]:
|
||||||
|
opt = normalize_opt(f"{prefix}{ch}", self.ctx)
|
||||||
|
option = self._short_opt.get(opt)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not option:
|
||||||
|
if self.ignore_unknown_options:
|
||||||
|
unknown_options.append(ch)
|
||||||
|
continue
|
||||||
|
raise NoSuchOption(opt, ctx=self.ctx)
|
||||||
|
if option.takes_value:
|
||||||
|
# Any characters left in arg? Pretend they're the
|
||||||
|
# next arg, and stop consuming characters of arg.
|
||||||
|
if i < len(arg):
|
||||||
|
state.rargs.insert(0, arg[i:])
|
||||||
|
stop = True
|
||||||
|
|
||||||
|
value = self._get_value_from_state(opt, option, state)
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
option.process(value, state)
|
||||||
|
|
||||||
|
if stop:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we got any unknown options we re-combinate the string of the
|
||||||
|
# remaining options and re-attach the prefix, then report that
|
||||||
|
# to the state as new larg. This way there is basic combinatorics
|
||||||
|
# that can be achieved while still ignoring unknown arguments.
|
||||||
|
if self.ignore_unknown_options and unknown_options:
|
||||||
|
state.largs.append(f"{prefix}{''.join(unknown_options)}")
|
||||||
|
|
||||||
|
def _get_value_from_state(
|
||||||
|
self, option_name: str, option: Option, state: ParsingState
|
||||||
|
) -> t.Any:
|
||||||
|
nargs = option.nargs
|
||||||
|
|
||||||
|
if len(state.rargs) < nargs:
|
||||||
|
if option.obj._flag_needs_value:
|
||||||
|
# Option allows omitting the value.
|
||||||
|
value = _flag_needs_value
|
||||||
|
else:
|
||||||
|
raise BadOptionUsage(
|
||||||
|
option_name,
|
||||||
|
ngettext(
|
||||||
|
"Option {name!r} requires an argument.",
|
||||||
|
"Option {name!r} requires {nargs} arguments.",
|
||||||
|
nargs,
|
||||||
|
).format(name=option_name, nargs=nargs),
|
||||||
|
)
|
||||||
|
elif nargs == 1:
|
||||||
|
next_rarg = state.rargs[0]
|
||||||
|
|
||||||
|
if (
|
||||||
|
option.obj._flag_needs_value
|
||||||
|
and isinstance(next_rarg, str)
|
||||||
|
and next_rarg[:1] in self._opt_prefixes
|
||||||
|
and len(next_rarg) > 1
|
||||||
|
):
|
||||||
|
# The next arg looks like the start of an option, don't
|
||||||
|
# use it as the value if omitting the value is allowed.
|
||||||
|
value = _flag_needs_value
|
||||||
|
else:
|
||||||
|
value = state.rargs.pop(0)
|
||||||
|
else:
|
||||||
|
value = tuple(state.rargs[:nargs])
|
||||||
|
del state.rargs[:nargs]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _process_opts(self, arg: str, state: ParsingState) -> None:
|
||||||
|
explicit_value = None
|
||||||
|
# Long option handling happens in two parts. The first part is
|
||||||
|
# supporting explicitly attached values. In any case, we will try
|
||||||
|
# to long match the option first.
|
||||||
|
if "=" in arg:
|
||||||
|
long_opt, explicit_value = arg.split("=", 1)
|
||||||
|
else:
|
||||||
|
long_opt = arg
|
||||||
|
norm_long_opt = normalize_opt(long_opt, self.ctx)
|
||||||
|
|
||||||
|
# At this point we will match the (assumed) long option through
|
||||||
|
# the long option matching code. Note that this allows options
|
||||||
|
# like "-foo" to be matched as long options.
|
||||||
|
try:
|
||||||
|
self._match_long_opt(norm_long_opt, explicit_value, state)
|
||||||
|
except NoSuchOption:
|
||||||
|
# At this point the long option matching failed, and we need
|
||||||
|
# to try with short options. However there is a special rule
|
||||||
|
# which says, that if we have a two character options prefix
|
||||||
|
# (applies to "--foo" for instance), we do not dispatch to the
|
||||||
|
# short option code and will instead raise the no option
|
||||||
|
# error.
|
||||||
|
if arg[:2] not in self._opt_prefixes:
|
||||||
|
self._match_short_opt(arg, state)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.ignore_unknown_options:
|
||||||
|
raise
|
||||||
|
|
||||||
|
state.largs.append(arg)
|
@ -0,0 +1,580 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from .core import Argument
|
||||||
|
from .core import BaseCommand
|
||||||
|
from .core import Context
|
||||||
|
from .core import MultiCommand
|
||||||
|
from .core import Option
|
||||||
|
from .core import Parameter
|
||||||
|
from .core import ParameterSource
|
||||||
|
from .parser import split_arg_string
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
cli: BaseCommand,
|
||||||
|
ctx_args: t.Dict[str, t.Any],
|
||||||
|
prog_name: str,
|
||||||
|
complete_var: str,
|
||||||
|
instruction: str,
|
||||||
|
) -> int:
|
||||||
|
"""Perform shell completion for the given CLI program.
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param ctx_args: Extra arguments to pass to
|
||||||
|
``cli.make_context``.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param complete_var: Name of the environment variable that holds
|
||||||
|
the completion instruction.
|
||||||
|
:param instruction: Value of ``complete_var`` with the completion
|
||||||
|
instruction and shell, in the form ``instruction_shell``.
|
||||||
|
:return: Status code to exit with.
|
||||||
|
"""
|
||||||
|
shell, _, instruction = instruction.partition("_")
|
||||||
|
comp_cls = get_completion_class(shell)
|
||||||
|
|
||||||
|
if comp_cls is None:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
comp = comp_cls(cli, ctx_args, prog_name, complete_var)
|
||||||
|
|
||||||
|
if instruction == "source":
|
||||||
|
echo(comp.source())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if instruction == "complete":
|
||||||
|
echo(comp.complete())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionItem:
|
||||||
|
"""Represents a completion value and metadata about the value. The
|
||||||
|
default metadata is ``type`` to indicate special shell handling,
|
||||||
|
and ``help`` if a shell supports showing a help string next to the
|
||||||
|
value.
|
||||||
|
|
||||||
|
Arbitrary parameters can be passed when creating the object, and
|
||||||
|
accessed using ``item.attr``. If an attribute wasn't passed,
|
||||||
|
accessing it returns ``None``.
|
||||||
|
|
||||||
|
:param value: The completion suggestion.
|
||||||
|
:param type: Tells the shell script to provide special completion
|
||||||
|
support for the type. Click uses ``"dir"`` and ``"file"``.
|
||||||
|
:param help: String shown next to the value if supported.
|
||||||
|
:param kwargs: Arbitrary metadata. The built-in implementations
|
||||||
|
don't use this, but custom type completions paired with custom
|
||||||
|
shell support could use it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("value", "type", "help", "_info")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: t.Any,
|
||||||
|
type: str = "plain",
|
||||||
|
help: t.Optional[str] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
self.value = value
|
||||||
|
self.type = type
|
||||||
|
self.help = help
|
||||||
|
self._info = kwargs
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return self._info.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
# Only Bash >= 4.4 has the nosort option.
|
||||||
|
_SOURCE_BASH = """\
|
||||||
|
%(complete_func)s() {
|
||||||
|
local IFS=$'\\n'
|
||||||
|
local response
|
||||||
|
|
||||||
|
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
|
||||||
|
%(complete_var)s=bash_complete $1)
|
||||||
|
|
||||||
|
for completion in $response; do
|
||||||
|
IFS=',' read type value <<< "$completion"
|
||||||
|
|
||||||
|
if [[ $type == 'dir' ]]; then
|
||||||
|
COMPREPLY=()
|
||||||
|
compopt -o dirnames
|
||||||
|
elif [[ $type == 'file' ]]; then
|
||||||
|
COMPREPLY=()
|
||||||
|
compopt -o default
|
||||||
|
elif [[ $type == 'plain' ]]; then
|
||||||
|
COMPREPLY+=($value)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)s_setup() {
|
||||||
|
complete -o nosort -F %(complete_func)s %(prog_name)s
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)s_setup;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SOURCE_ZSH = """\
|
||||||
|
#compdef %(prog_name)s
|
||||||
|
|
||||||
|
%(complete_func)s() {
|
||||||
|
local -a completions
|
||||||
|
local -a completions_with_descriptions
|
||||||
|
local -a response
|
||||||
|
(( ! $+commands[%(prog_name)s] )) && return 1
|
||||||
|
|
||||||
|
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
|
||||||
|
%(complete_var)s=zsh_complete %(prog_name)s)}")
|
||||||
|
|
||||||
|
for type key descr in ${response}; do
|
||||||
|
if [[ "$type" == "plain" ]]; then
|
||||||
|
if [[ "$descr" == "_" ]]; then
|
||||||
|
completions+=("$key")
|
||||||
|
else
|
||||||
|
completions_with_descriptions+=("$key":"$descr")
|
||||||
|
fi
|
||||||
|
elif [[ "$type" == "dir" ]]; then
|
||||||
|
_path_files -/
|
||||||
|
elif [[ "$type" == "file" ]]; then
|
||||||
|
_path_files -f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$completions_with_descriptions" ]; then
|
||||||
|
_describe -V unsorted completions_with_descriptions -U
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$completions" ]; then
|
||||||
|
compadd -U -V unsorted -a completions
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef %(complete_func)s %(prog_name)s;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SOURCE_FISH = """\
|
||||||
|
function %(complete_func)s;
|
||||||
|
set -l response;
|
||||||
|
|
||||||
|
for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
|
||||||
|
COMP_CWORD=(commandline -t) %(prog_name)s);
|
||||||
|
set response $response $value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
for completion in $response;
|
||||||
|
set -l metadata (string split "," $completion);
|
||||||
|
|
||||||
|
if test $metadata[1] = "dir";
|
||||||
|
__fish_complete_directories $metadata[2];
|
||||||
|
else if test $metadata[1] = "file";
|
||||||
|
__fish_complete_path $metadata[2];
|
||||||
|
else if test $metadata[1] = "plain";
|
||||||
|
echo $metadata[2];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
complete --no-files --command %(prog_name)s --arguments \
|
||||||
|
"(%(complete_func)s)";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ShellComplete:
|
||||||
|
"""Base class for providing shell completion support. A subclass for
|
||||||
|
a given shell will override attributes and methods to implement the
|
||||||
|
completion instructions (``source`` and ``complete``).
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param complete_var: Name of the environment variable that holds
|
||||||
|
the completion instruction.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: t.ClassVar[str]
|
||||||
|
"""Name to register the shell as with :func:`add_completion_class`.
|
||||||
|
This is used in completion instructions (``{name}_source`` and
|
||||||
|
``{name}_complete``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_template: t.ClassVar[str]
|
||||||
|
"""Completion script template formatted by :meth:`source`. This must
|
||||||
|
be provided by subclasses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cli: BaseCommand,
|
||||||
|
ctx_args: t.Dict[str, t.Any],
|
||||||
|
prog_name: str,
|
||||||
|
complete_var: str,
|
||||||
|
) -> None:
|
||||||
|
self.cli = cli
|
||||||
|
self.ctx_args = ctx_args
|
||||||
|
self.prog_name = prog_name
|
||||||
|
self.complete_var = complete_var
|
||||||
|
|
||||||
|
@property
|
||||||
|
def func_name(self) -> str:
|
||||||
|
"""The name of the shell function defined by the completion
|
||||||
|
script.
|
||||||
|
"""
|
||||||
|
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII)
|
||||||
|
return f"_{safe_name}_completion"
|
||||||
|
|
||||||
|
def source_vars(self) -> t.Dict[str, t.Any]:
|
||||||
|
"""Vars for formatting :attr:`source_template`.
|
||||||
|
|
||||||
|
By default this provides ``complete_func``, ``complete_var``,
|
||||||
|
and ``prog_name``.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"complete_func": self.func_name,
|
||||||
|
"complete_var": self.complete_var,
|
||||||
|
"prog_name": self.prog_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def source(self) -> str:
|
||||||
|
"""Produce the shell script that defines the completion
|
||||||
|
function. By default this ``%``-style formats
|
||||||
|
:attr:`source_template` with the dict returned by
|
||||||
|
:meth:`source_vars`.
|
||||||
|
"""
|
||||||
|
return self.source_template % self.source_vars()
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
"""Use the env vars defined by the shell script to return a
|
||||||
|
tuple of ``args, incomplete``. This must be implemented by
|
||||||
|
subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, args: t.List[str], incomplete: str
|
||||||
|
) -> t.List[CompletionItem]:
|
||||||
|
"""Determine the context and last complete command or parameter
|
||||||
|
from the complete args. Call that object's ``shell_complete``
|
||||||
|
method to get the completions for the incomplete value.
|
||||||
|
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
"""
|
||||||
|
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
|
||||||
|
obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
|
||||||
|
return obj.shell_complete(ctx, incomplete)
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
"""Format a completion item into the form recognized by the
|
||||||
|
shell script. This must be implemented by subclasses.
|
||||||
|
|
||||||
|
:param item: Completion item to format.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def complete(self) -> str:
|
||||||
|
"""Produce the completion data to send back to the shell.
|
||||||
|
|
||||||
|
By default this calls :meth:`get_completion_args`, gets the
|
||||||
|
completions, then calls :meth:`format_completion` for each
|
||||||
|
completion.
|
||||||
|
"""
|
||||||
|
args, incomplete = self.get_completion_args()
|
||||||
|
completions = self.get_completions(args, incomplete)
|
||||||
|
out = [self.format_completion(item) for item in completions]
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
class BashComplete(ShellComplete):
|
||||||
|
"""Shell completion for Bash."""
|
||||||
|
|
||||||
|
name = "bash"
|
||||||
|
source_template = _SOURCE_BASH
|
||||||
|
|
||||||
|
def _check_version(self) -> None:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
output = subprocess.run(
|
||||||
|
["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
|
||||||
|
|
||||||
|
if match is not None:
|
||||||
|
major, minor = match.groups()
|
||||||
|
|
||||||
|
if major < "4" or major == "4" and minor < "4":
|
||||||
|
raise RuntimeError(
|
||||||
|
_(
|
||||||
|
"Shell completion is not supported for Bash"
|
||||||
|
" versions older than 4.4."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
_("Couldn't detect Bash version, shell completion is not supported.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def source(self) -> str:
|
||||||
|
self._check_version()
|
||||||
|
return super().source()
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
args = cwords[1:cword]
|
||||||
|
|
||||||
|
try:
|
||||||
|
incomplete = cwords[cword]
|
||||||
|
except IndexError:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
return f"{item.type},{item.value}"
|
||||||
|
|
||||||
|
|
||||||
|
class ZshComplete(ShellComplete):
|
||||||
|
"""Shell completion for Zsh."""
|
||||||
|
|
||||||
|
name = "zsh"
|
||||||
|
source_template = _SOURCE_ZSH
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
args = cwords[1:cword]
|
||||||
|
|
||||||
|
try:
|
||||||
|
incomplete = cwords[cword]
|
||||||
|
except IndexError:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
|
||||||
|
|
||||||
|
|
||||||
|
class FishComplete(ShellComplete):
|
||||||
|
"""Shell completion for Fish."""
|
||||||
|
|
||||||
|
name = "fish"
|
||||||
|
source_template = _SOURCE_FISH
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
incomplete = os.environ["COMP_CWORD"]
|
||||||
|
args = cwords[1:]
|
||||||
|
|
||||||
|
# Fish stores the partial word in both COMP_WORDS and
|
||||||
|
# COMP_CWORD, remove it from complete args.
|
||||||
|
if incomplete and args and args[-1] == incomplete:
|
||||||
|
args.pop()
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
if item.help:
|
||||||
|
return f"{item.type},{item.value}\t{item.help}"
|
||||||
|
|
||||||
|
return f"{item.type},{item.value}"
|
||||||
|
|
||||||
|
|
||||||
|
_available_shells: t.Dict[str, t.Type[ShellComplete]] = {
|
||||||
|
"bash": BashComplete,
|
||||||
|
"fish": FishComplete,
|
||||||
|
"zsh": ZshComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_completion_class(
|
||||||
|
cls: t.Type[ShellComplete], name: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Register a :class:`ShellComplete` subclass under the given name.
|
||||||
|
The name will be provided by the completion instruction environment
|
||||||
|
variable during completion.
|
||||||
|
|
||||||
|
:param cls: The completion class that will handle completion for the
|
||||||
|
shell.
|
||||||
|
:param name: Name to register the class under. Defaults to the
|
||||||
|
class's ``name`` attribute.
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
name = cls.name
|
||||||
|
|
||||||
|
_available_shells[name] = cls
|
||||||
|
|
||||||
|
|
||||||
|
def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]:
|
||||||
|
"""Look up a registered :class:`ShellComplete` subclass by the name
|
||||||
|
provided by the completion instruction environment variable. If the
|
||||||
|
name isn't registered, returns ``None``.
|
||||||
|
|
||||||
|
:param shell: Name the class is registered under.
|
||||||
|
"""
|
||||||
|
return _available_shells.get(shell)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
|
||||||
|
"""Determine if the given parameter is an argument that can still
|
||||||
|
accept values.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for the command represented by the
|
||||||
|
parsed complete args.
|
||||||
|
:param param: Argument object being checked.
|
||||||
|
"""
|
||||||
|
if not isinstance(param, Argument):
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert param.name is not None
|
||||||
|
value = ctx.params[param.name]
|
||||||
|
return (
|
||||||
|
param.nargs == -1
|
||||||
|
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
|
||||||
|
or (
|
||||||
|
param.nargs > 1
|
||||||
|
and isinstance(value, (tuple, list))
|
||||||
|
and len(value) < param.nargs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_of_option(ctx: Context, value: str) -> bool:
|
||||||
|
"""Check if the value looks like the start of an option."""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
c = value[0]
|
||||||
|
return c in ctx._opt_prefixes
|
||||||
|
|
||||||
|
|
||||||
|
def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool:
|
||||||
|
"""Determine if the given parameter is an option that needs a value.
|
||||||
|
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param param: Option object being checked.
|
||||||
|
"""
|
||||||
|
if not isinstance(param, Option):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if param.is_flag or param.count:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_option = None
|
||||||
|
|
||||||
|
for index, arg in enumerate(reversed(args)):
|
||||||
|
if index + 1 > param.nargs:
|
||||||
|
break
|
||||||
|
|
||||||
|
if _start_of_option(ctx, arg):
|
||||||
|
last_option = arg
|
||||||
|
|
||||||
|
return last_option is not None and last_option in param.opts
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_context(
|
||||||
|
cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str]
|
||||||
|
) -> Context:
|
||||||
|
"""Produce the context hierarchy starting with the command and
|
||||||
|
traversing the complete arguments. This only follows the commands,
|
||||||
|
it doesn't trigger input prompts or callbacks.
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
"""
|
||||||
|
ctx_args["resilient_parsing"] = True
|
||||||
|
ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
|
||||||
|
while args:
|
||||||
|
command = ctx.command
|
||||||
|
|
||||||
|
if isinstance(command, MultiCommand):
|
||||||
|
if not command.chain:
|
||||||
|
name, cmd, args = command.resolve_command(ctx, args)
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
else:
|
||||||
|
while args:
|
||||||
|
name, cmd, args = command.resolve_command(ctx, args)
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
sub_ctx = cmd.make_context(
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
parent=ctx,
|
||||||
|
allow_extra_args=True,
|
||||||
|
allow_interspersed_args=False,
|
||||||
|
resilient_parsing=True,
|
||||||
|
)
|
||||||
|
args = sub_ctx.args
|
||||||
|
|
||||||
|
ctx = sub_ctx
|
||||||
|
args = [*sub_ctx.protected_args, *sub_ctx.args]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_incomplete(
|
||||||
|
ctx: Context, args: t.List[str], incomplete: str
|
||||||
|
) -> t.Tuple[t.Union[BaseCommand, Parameter], str]:
|
||||||
|
"""Find the Click object that will handle the completion of the
|
||||||
|
incomplete value. Return the object and the incomplete value.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for the command represented by
|
||||||
|
the parsed complete args.
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
"""
|
||||||
|
# Different shells treat an "=" between a long option name and
|
||||||
|
# value differently. Might keep the value joined, return the "="
|
||||||
|
# as a separate item, or return the split name and value. Always
|
||||||
|
# split and discard the "=" to make completion easier.
|
||||||
|
if incomplete == "=":
|
||||||
|
incomplete = ""
|
||||||
|
elif "=" in incomplete and _start_of_option(ctx, incomplete):
|
||||||
|
name, _, incomplete = incomplete.partition("=")
|
||||||
|
args.append(name)
|
||||||
|
|
||||||
|
# The "--" marker tells Click to stop treating values as options
|
||||||
|
# even if they start with the option character. If it hasn't been
|
||||||
|
# given and the incomplete arg looks like an option, the current
|
||||||
|
# command will provide option name completions.
|
||||||
|
if "--" not in args and _start_of_option(ctx, incomplete):
|
||||||
|
return ctx.command, incomplete
|
||||||
|
|
||||||
|
params = ctx.command.get_params(ctx)
|
||||||
|
|
||||||
|
# If the last complete arg is an option name with an incomplete
|
||||||
|
# value, the option will provide value completions.
|
||||||
|
for param in params:
|
||||||
|
if _is_incomplete_option(ctx, args, param):
|
||||||
|
return param, incomplete
|
||||||
|
|
||||||
|
# It's not an option name or value. The first argument without a
|
||||||
|
# parsed value will provide value completions.
|
||||||
|
for param in params:
|
||||||
|
if _is_incomplete_argument(ctx, param):
|
||||||
|
return param, incomplete
|
||||||
|
|
||||||
|
# There were no unparsed arguments, the command may be a group that
|
||||||
|
# will provide command name completions.
|
||||||
|
return ctx.command, incomplete
|
@ -0,0 +1,787 @@
|
|||||||
|
import inspect
|
||||||
|
import io
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from ._compat import isatty
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import WIN
|
||||||
|
from .exceptions import Abort
|
||||||
|
from .exceptions import UsageError
|
||||||
|
from .globals import resolve_color_default
|
||||||
|
from .types import Choice
|
||||||
|
from .types import convert_type
|
||||||
|
from .types import ParamType
|
||||||
|
from .utils import echo
|
||||||
|
from .utils import LazyFile
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from ._termui_impl import ProgressBar
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
# The prompt functions to use. The doc tools currently override these
|
||||||
|
# functions to customize how they work.
|
||||||
|
visible_prompt_func: t.Callable[[str], str] = input
|
||||||
|
|
||||||
|
_ansi_colors = {
|
||||||
|
"black": 30,
|
||||||
|
"red": 31,
|
||||||
|
"green": 32,
|
||||||
|
"yellow": 33,
|
||||||
|
"blue": 34,
|
||||||
|
"magenta": 35,
|
||||||
|
"cyan": 36,
|
||||||
|
"white": 37,
|
||||||
|
"reset": 39,
|
||||||
|
"bright_black": 90,
|
||||||
|
"bright_red": 91,
|
||||||
|
"bright_green": 92,
|
||||||
|
"bright_yellow": 93,
|
||||||
|
"bright_blue": 94,
|
||||||
|
"bright_magenta": 95,
|
||||||
|
"bright_cyan": 96,
|
||||||
|
"bright_white": 97,
|
||||||
|
}
|
||||||
|
_ansi_reset_all = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def hidden_prompt_func(prompt: str) -> str:
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
return getpass.getpass(prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prompt(
|
||||||
|
text: str,
|
||||||
|
suffix: str,
|
||||||
|
show_default: bool = False,
|
||||||
|
default: t.Optional[t.Any] = None,
|
||||||
|
show_choices: bool = True,
|
||||||
|
type: t.Optional[ParamType] = None,
|
||||||
|
) -> str:
|
||||||
|
prompt = text
|
||||||
|
if type is not None and show_choices and isinstance(type, Choice):
|
||||||
|
prompt += f" ({', '.join(map(str, type.choices))})"
|
||||||
|
if default is not None and show_default:
|
||||||
|
prompt = f"{prompt} [{_format_default(default)}]"
|
||||||
|
return f"{prompt}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_default(default: t.Any) -> t.Any:
|
||||||
|
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
||||||
|
return default.name # type: ignore
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def prompt(
|
||||||
|
text: str,
|
||||||
|
default: t.Optional[t.Any] = None,
|
||||||
|
hide_input: bool = False,
|
||||||
|
confirmation_prompt: t.Union[bool, str] = False,
|
||||||
|
type: t.Optional[t.Union[ParamType, t.Any]] = None,
|
||||||
|
value_proc: t.Optional[t.Callable[[str], t.Any]] = None,
|
||||||
|
prompt_suffix: str = ": ",
|
||||||
|
show_default: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
show_choices: bool = True,
|
||||||
|
) -> t.Any:
|
||||||
|
"""Prompts a user for input. This is a convenience function that can
|
||||||
|
be used to prompt a user for input later.
|
||||||
|
|
||||||
|
If the user aborts the input by sending an interrupt signal, this
|
||||||
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
|
:param text: the text to show for the prompt.
|
||||||
|
:param default: the default value to use if no input happens. If this
|
||||||
|
is not given it will prompt until it's aborted.
|
||||||
|
:param hide_input: if this is set to true then the input value will
|
||||||
|
be hidden.
|
||||||
|
:param confirmation_prompt: Prompt a second time to confirm the
|
||||||
|
value. Can be set to a string instead of ``True`` to customize
|
||||||
|
the message.
|
||||||
|
:param type: the type to use to check the value against.
|
||||||
|
:param value_proc: if this parameter is provided it's a function that
|
||||||
|
is invoked instead of the type conversion to
|
||||||
|
convert a value.
|
||||||
|
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||||
|
:param show_default: shows or hides the default value in the prompt.
|
||||||
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
:param show_choices: Show or hide choices if the passed type is a Choice.
|
||||||
|
For example if type is a Choice of either day or week,
|
||||||
|
show_choices is true and text is "Group by" then the
|
||||||
|
prompt will be "Group by (day, week): ".
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
``confirmation_prompt`` can be a custom string.
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
|
Added the ``show_choices`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
Added unicode support for cmd.exe on Windows.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def prompt_func(text: str) -> str:
|
||||||
|
f = hidden_prompt_func if hide_input else visible_prompt_func
|
||||||
|
try:
|
||||||
|
# Write the prompt separately so that we get nice
|
||||||
|
# coloring through colorama on Windows
|
||||||
|
echo(text.rstrip(" "), nl=False, err=err)
|
||||||
|
# Echo a space to stdout to work around an issue where
|
||||||
|
# readline causes backspace to clear the whole line.
|
||||||
|
return f(" ")
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
# getpass doesn't print a newline if the user aborts input with ^C.
|
||||||
|
# Allegedly this behavior is inherited from getpass(3).
|
||||||
|
# A doc bug has been filed at https://bugs.python.org/issue24711
|
||||||
|
if hide_input:
|
||||||
|
echo(None, err=err)
|
||||||
|
raise Abort() from None
|
||||||
|
|
||||||
|
if value_proc is None:
|
||||||
|
value_proc = convert_type(type, default)
|
||||||
|
|
||||||
|
prompt = _build_prompt(
|
||||||
|
text, prompt_suffix, show_default, default, show_choices, type
|
||||||
|
)
|
||||||
|
|
||||||
|
if confirmation_prompt:
|
||||||
|
if confirmation_prompt is True:
|
||||||
|
confirmation_prompt = _("Repeat for confirmation")
|
||||||
|
|
||||||
|
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
while True:
|
||||||
|
value = prompt_func(prompt)
|
||||||
|
if value:
|
||||||
|
break
|
||||||
|
elif default is not None:
|
||||||
|
value = default
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
result = value_proc(value)
|
||||||
|
except UsageError as e:
|
||||||
|
if hide_input:
|
||||||
|
echo(_("Error: The value you entered was invalid."), err=err)
|
||||||
|
else:
|
||||||
|
echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306
|
||||||
|
continue
|
||||||
|
if not confirmation_prompt:
|
||||||
|
return result
|
||||||
|
while True:
|
||||||
|
value2 = prompt_func(confirmation_prompt)
|
||||||
|
is_empty = not value and not value2
|
||||||
|
if value2 or is_empty:
|
||||||
|
break
|
||||||
|
if value == value2:
|
||||||
|
return result
|
||||||
|
echo(_("Error: The two entered values do not match."), err=err)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(
|
||||||
|
text: str,
|
||||||
|
default: t.Optional[bool] = False,
|
||||||
|
abort: bool = False,
|
||||||
|
prompt_suffix: str = ": ",
|
||||||
|
show_default: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
"""Prompts for confirmation (yes/no question).
|
||||||
|
|
||||||
|
If the user aborts the input by sending a interrupt signal this
|
||||||
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
|
:param text: the question to ask.
|
||||||
|
:param default: The default value to use when no input is given. If
|
||||||
|
``None``, repeat until input is given.
|
||||||
|
:param abort: if this is set to `True` a negative answer aborts the
|
||||||
|
exception by raising :exc:`Abort`.
|
||||||
|
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||||
|
:param show_default: shows or hides the default value in the prompt.
|
||||||
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Repeat until input is given if ``default`` is ``None``.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the ``err`` parameter.
|
||||||
|
"""
|
||||||
|
prompt = _build_prompt(
|
||||||
|
text,
|
||||||
|
prompt_suffix,
|
||||||
|
show_default,
|
||||||
|
"y/n" if default is None else ("Y/n" if default else "y/N"),
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Write the prompt separately so that we get nice
|
||||||
|
# coloring through colorama on Windows
|
||||||
|
echo(prompt.rstrip(" "), nl=False, err=err)
|
||||||
|
# Echo a space to stdout to work around an issue where
|
||||||
|
# readline causes backspace to clear the whole line.
|
||||||
|
value = visible_prompt_func(" ").lower().strip()
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
raise Abort() from None
|
||||||
|
if value in ("y", "yes"):
|
||||||
|
rv = True
|
||||||
|
elif value in ("n", "no"):
|
||||||
|
rv = False
|
||||||
|
elif default is not None and value == "":
|
||||||
|
rv = default
|
||||||
|
else:
|
||||||
|
echo(_("Error: invalid input"), err=err)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if abort and not rv:
|
||||||
|
raise Abort()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def echo_via_pager(
|
||||||
|
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
) -> None:
|
||||||
|
"""This function takes a text and shows it via an environment specific
|
||||||
|
pager on stdout.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the `color` flag.
|
||||||
|
|
||||||
|
:param text_or_generator: the text to page, or alternatively, a
|
||||||
|
generator emitting the text to page.
|
||||||
|
:param color: controls if the pager supports ANSI colors or not. The
|
||||||
|
default is autodetection.
|
||||||
|
"""
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
|
||||||
|
if inspect.isgeneratorfunction(text_or_generator):
|
||||||
|
i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
|
||||||
|
elif isinstance(text_or_generator, str):
|
||||||
|
i = [text_or_generator]
|
||||||
|
else:
|
||||||
|
i = iter(t.cast(t.Iterable[str], text_or_generator))
|
||||||
|
|
||||||
|
# convert every element of i to a text type if necessary
|
||||||
|
text_generator = (el if isinstance(el, str) else str(el) for el in i)
|
||||||
|
|
||||||
|
from ._termui_impl import pager
|
||||||
|
|
||||||
|
return pager(itertools.chain(text_generator, "\n"), color)
|
||||||
|
|
||||||
|
|
||||||
|
def progressbar(
|
||||||
|
iterable: t.Optional[t.Iterable[V]] = None,
|
||||||
|
length: t.Optional[int] = None,
|
||||||
|
label: t.Optional[str] = None,
|
||||||
|
show_eta: bool = True,
|
||||||
|
show_percent: t.Optional[bool] = None,
|
||||||
|
show_pos: bool = False,
|
||||||
|
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||||
|
fill_char: str = "#",
|
||||||
|
empty_char: str = "-",
|
||||||
|
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
||||||
|
info_sep: str = " ",
|
||||||
|
width: int = 36,
|
||||||
|
file: t.Optional[t.TextIO] = None,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
update_min_steps: int = 1,
|
||||||
|
) -> "ProgressBar[V]":
|
||||||
|
"""This function creates an iterable context manager that can be used
|
||||||
|
to iterate over something while showing a progress bar. It will
|
||||||
|
either iterate over the `iterable` or `length` items (that are counted
|
||||||
|
up). While iteration happens, this function will print a rendered
|
||||||
|
progress bar to the given `file` (defaults to stdout) and will attempt
|
||||||
|
to calculate remaining time and more. By default, this progress bar
|
||||||
|
will not be rendered if the file is not a terminal.
|
||||||
|
|
||||||
|
The context manager creates the progress bar. When the context
|
||||||
|
manager is entered the progress bar is already created. With every
|
||||||
|
iteration over the progress bar, the iterable passed to the bar is
|
||||||
|
advanced and the bar is updated. When the context manager exits,
|
||||||
|
a newline is printed and the progress bar is finalized on screen.
|
||||||
|
|
||||||
|
Note: The progress bar is currently designed for use cases where the
|
||||||
|
total progress can be expected to take at least several seconds.
|
||||||
|
Because of this, the ProgressBar class object won't display
|
||||||
|
progress that is considered too fast, and progress where the time
|
||||||
|
between steps is less than a second.
|
||||||
|
|
||||||
|
No printing must happen or the progress bar will be unintentionally
|
||||||
|
destroyed.
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
with progressbar(items) as bar:
|
||||||
|
for item in bar:
|
||||||
|
do_something_with(item)
|
||||||
|
|
||||||
|
Alternatively, if no iterable is specified, one can manually update the
|
||||||
|
progress bar through the `update()` method instead of directly
|
||||||
|
iterating over the progress bar. The update method accepts the number
|
||||||
|
of steps to increment the bar with::
|
||||||
|
|
||||||
|
with progressbar(length=chunks.total_bytes) as bar:
|
||||||
|
for chunk in chunks:
|
||||||
|
process_chunk(chunk)
|
||||||
|
bar.update(chunks.bytes)
|
||||||
|
|
||||||
|
The ``update()`` method also takes an optional value specifying the
|
||||||
|
``current_item`` at the new position. This is useful when used
|
||||||
|
together with ``item_show_func`` to customize the output for each
|
||||||
|
manual step::
|
||||||
|
|
||||||
|
with click.progressbar(
|
||||||
|
length=total_size,
|
||||||
|
label='Unzipping archive',
|
||||||
|
item_show_func=lambda a: a.filename
|
||||||
|
) as bar:
|
||||||
|
for archive in zip_file:
|
||||||
|
archive.extract()
|
||||||
|
bar.update(archive.size, archive)
|
||||||
|
|
||||||
|
:param iterable: an iterable to iterate over. If not provided the length
|
||||||
|
is required.
|
||||||
|
:param length: the number of items to iterate over. By default the
|
||||||
|
progressbar will attempt to ask the iterator about its
|
||||||
|
length, which might or might not work. If an iterable is
|
||||||
|
also provided this parameter can be used to override the
|
||||||
|
length. If an iterable is not provided the progress bar
|
||||||
|
will iterate over a range of that length.
|
||||||
|
:param label: the label to show next to the progress bar.
|
||||||
|
:param show_eta: enables or disables the estimated time display. This is
|
||||||
|
automatically disabled if the length cannot be
|
||||||
|
determined.
|
||||||
|
:param show_percent: enables or disables the percentage display. The
|
||||||
|
default is `True` if the iterable has a length or
|
||||||
|
`False` if not.
|
||||||
|
:param show_pos: enables or disables the absolute position display. The
|
||||||
|
default is `False`.
|
||||||
|
:param item_show_func: A function called with the current item which
|
||||||
|
can return a string to show next to the progress bar. If the
|
||||||
|
function returns ``None`` nothing is shown. The current item can
|
||||||
|
be ``None``, such as when entering and exiting the bar.
|
||||||
|
:param fill_char: the character to use to show the filled part of the
|
||||||
|
progress bar.
|
||||||
|
:param empty_char: the character to use to show the non-filled part of
|
||||||
|
the progress bar.
|
||||||
|
:param bar_template: the format string to use as template for the bar.
|
||||||
|
The parameters in it are ``label`` for the label,
|
||||||
|
``bar`` for the progress bar and ``info`` for the
|
||||||
|
info section.
|
||||||
|
:param info_sep: the separator between multiple info items (eta etc.)
|
||||||
|
:param width: the width of the progress bar in characters, 0 means full
|
||||||
|
terminal width
|
||||||
|
:param file: The file to write to. If this is not a terminal then
|
||||||
|
only the label is printed.
|
||||||
|
:param color: controls if the terminal supports ANSI colors or not. The
|
||||||
|
default is autodetection. This is only needed if ANSI
|
||||||
|
codes are included anywhere in the progress bar output
|
||||||
|
which is not the case by default.
|
||||||
|
:param update_min_steps: Render only when this many updates have
|
||||||
|
completed. This allows tuning for very fast iterators.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Output is shown even if execution time is less than 0.5 seconds.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``item_show_func`` shows the current item, not the previous one.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Labels are echoed if the output is not a TTY. Reverts a change
|
||||||
|
in 7.0 that removed all output.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
Added the ``update_min_steps`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter. Added the ``update`` method to
|
||||||
|
the object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
from ._termui_impl import ProgressBar
|
||||||
|
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
return ProgressBar(
|
||||||
|
iterable=iterable,
|
||||||
|
length=length,
|
||||||
|
show_eta=show_eta,
|
||||||
|
show_percent=show_percent,
|
||||||
|
show_pos=show_pos,
|
||||||
|
item_show_func=item_show_func,
|
||||||
|
fill_char=fill_char,
|
||||||
|
empty_char=empty_char,
|
||||||
|
bar_template=bar_template,
|
||||||
|
info_sep=info_sep,
|
||||||
|
file=file,
|
||||||
|
label=label,
|
||||||
|
width=width,
|
||||||
|
color=color,
|
||||||
|
update_min_steps=update_min_steps,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear() -> None:
|
||||||
|
"""Clears the terminal screen. This will have the effect of clearing
|
||||||
|
the whole visible space of the terminal and moving the cursor to the
|
||||||
|
top left. This does not do anything if not connected to a terminal.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if not isatty(sys.stdout):
|
||||||
|
return
|
||||||
|
if WIN:
|
||||||
|
os.system("cls")
|
||||||
|
else:
|
||||||
|
sys.stdout.write("\033[2J\033[1;1H")
|
||||||
|
|
||||||
|
|
||||||
|
def _interpret_color(
|
||||||
|
color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0
|
||||||
|
) -> str:
|
||||||
|
if isinstance(color, int):
|
||||||
|
return f"{38 + offset};5;{color:d}"
|
||||||
|
|
||||||
|
if isinstance(color, (tuple, list)):
|
||||||
|
r, g, b = color
|
||||||
|
return f"{38 + offset};2;{r:d};{g:d};{b:d}"
|
||||||
|
|
||||||
|
return str(_ansi_colors[color] + offset)
|
||||||
|
|
||||||
|
|
||||||
|
def style(
|
||||||
|
text: t.Any,
|
||||||
|
fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||||
|
bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||||
|
bold: t.Optional[bool] = None,
|
||||||
|
dim: t.Optional[bool] = None,
|
||||||
|
underline: t.Optional[bool] = None,
|
||||||
|
overline: t.Optional[bool] = None,
|
||||||
|
italic: t.Optional[bool] = None,
|
||||||
|
blink: t.Optional[bool] = None,
|
||||||
|
reverse: t.Optional[bool] = None,
|
||||||
|
strikethrough: t.Optional[bool] = None,
|
||||||
|
reset: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Styles a text with ANSI styles and returns the new string. By
|
||||||
|
default the styling is self contained which means that at the end
|
||||||
|
of the string a reset code is issued. This can be prevented by
|
||||||
|
passing ``reset=False``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
click.echo(click.style('Hello World!', fg='green'))
|
||||||
|
click.echo(click.style('ATTENTION!', blink=True))
|
||||||
|
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
||||||
|
click.echo(click.style('More colors', fg=(255, 12, 128), bg=117))
|
||||||
|
|
||||||
|
Supported color names:
|
||||||
|
|
||||||
|
* ``black`` (might be a gray)
|
||||||
|
* ``red``
|
||||||
|
* ``green``
|
||||||
|
* ``yellow`` (might be an orange)
|
||||||
|
* ``blue``
|
||||||
|
* ``magenta``
|
||||||
|
* ``cyan``
|
||||||
|
* ``white`` (might be light gray)
|
||||||
|
* ``bright_black``
|
||||||
|
* ``bright_red``
|
||||||
|
* ``bright_green``
|
||||||
|
* ``bright_yellow``
|
||||||
|
* ``bright_blue``
|
||||||
|
* ``bright_magenta``
|
||||||
|
* ``bright_cyan``
|
||||||
|
* ``bright_white``
|
||||||
|
* ``reset`` (reset the color code only)
|
||||||
|
|
||||||
|
If the terminal supports it, color may also be specified as:
|
||||||
|
|
||||||
|
- An integer in the interval [0, 255]. The terminal must support
|
||||||
|
8-bit/256-color mode.
|
||||||
|
- An RGB tuple of three integers in [0, 255]. The terminal must
|
||||||
|
support 24-bit/true-color mode.
|
||||||
|
|
||||||
|
See https://en.wikipedia.org/wiki/ANSI_color and
|
||||||
|
https://gist.github.com/XVilka/8346728 for more information.
|
||||||
|
|
||||||
|
:param text: the string to style with ansi codes.
|
||||||
|
:param fg: if provided this will become the foreground color.
|
||||||
|
:param bg: if provided this will become the background color.
|
||||||
|
:param bold: if provided this will enable or disable bold mode.
|
||||||
|
:param dim: if provided this will enable or disable dim mode. This is
|
||||||
|
badly supported.
|
||||||
|
:param underline: if provided this will enable or disable underline.
|
||||||
|
:param overline: if provided this will enable or disable overline.
|
||||||
|
:param italic: if provided this will enable or disable italic.
|
||||||
|
:param blink: if provided this will enable or disable blinking.
|
||||||
|
:param reverse: if provided this will enable or disable inverse
|
||||||
|
rendering (foreground becomes background and the
|
||||||
|
other way round).
|
||||||
|
:param strikethrough: if provided this will enable or disable
|
||||||
|
striking through text.
|
||||||
|
:param reset: by default a reset-all code is added at the end of the
|
||||||
|
string which means that styles do not carry over. This
|
||||||
|
can be disabled to compose styles.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
A non-string ``message`` is converted to a string.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added support for 256 and RGB color codes.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``strikethrough``, ``italic``, and ``overline``
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
.. versionchanged:: 7.0
|
||||||
|
Added support for bright colors.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
text = str(text)
|
||||||
|
|
||||||
|
bits = []
|
||||||
|
|
||||||
|
if fg:
|
||||||
|
try:
|
||||||
|
bits.append(f"\033[{_interpret_color(fg)}m")
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError(f"Unknown color {fg!r}") from None
|
||||||
|
|
||||||
|
if bg:
|
||||||
|
try:
|
||||||
|
bits.append(f"\033[{_interpret_color(bg, 10)}m")
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError(f"Unknown color {bg!r}") from None
|
||||||
|
|
||||||
|
if bold is not None:
|
||||||
|
bits.append(f"\033[{1 if bold else 22}m")
|
||||||
|
if dim is not None:
|
||||||
|
bits.append(f"\033[{2 if dim else 22}m")
|
||||||
|
if underline is not None:
|
||||||
|
bits.append(f"\033[{4 if underline else 24}m")
|
||||||
|
if overline is not None:
|
||||||
|
bits.append(f"\033[{53 if overline else 55}m")
|
||||||
|
if italic is not None:
|
||||||
|
bits.append(f"\033[{3 if italic else 23}m")
|
||||||
|
if blink is not None:
|
||||||
|
bits.append(f"\033[{5 if blink else 25}m")
|
||||||
|
if reverse is not None:
|
||||||
|
bits.append(f"\033[{7 if reverse else 27}m")
|
||||||
|
if strikethrough is not None:
|
||||||
|
bits.append(f"\033[{9 if strikethrough else 29}m")
|
||||||
|
bits.append(text)
|
||||||
|
if reset:
|
||||||
|
bits.append(_ansi_reset_all)
|
||||||
|
return "".join(bits)
|
||||||
|
|
||||||
|
|
||||||
|
def unstyle(text: str) -> str:
|
||||||
|
"""Removes ANSI styling information from a string. Usually it's not
|
||||||
|
necessary to use this function as Click's echo function will
|
||||||
|
automatically remove styling if necessary.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param text: the text to remove style information from.
|
||||||
|
"""
|
||||||
|
return strip_ansi(text)
|
||||||
|
|
||||||
|
|
||||||
|
def secho(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
file: t.Optional[t.IO[t.AnyStr]] = None,
|
||||||
|
nl: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
**styles: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""This function combines :func:`echo` and :func:`style` into one
|
||||||
|
call. As such the following two calls are the same::
|
||||||
|
|
||||||
|
click.secho('Hello World!', fg='green')
|
||||||
|
click.echo(click.style('Hello World!', fg='green'))
|
||||||
|
|
||||||
|
All keyword arguments are forwarded to the underlying functions
|
||||||
|
depending on which one they go with.
|
||||||
|
|
||||||
|
Non-string types will be converted to :class:`str`. However,
|
||||||
|
:class:`bytes` are passed directly to :meth:`echo` without applying
|
||||||
|
style. If you want to style bytes that represent text, call
|
||||||
|
:meth:`bytes.decode` first.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
A non-string ``message`` is converted to a string. Bytes are
|
||||||
|
passed through without style applied.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if message is not None and not isinstance(message, (bytes, bytearray)):
|
||||||
|
message = style(message, **styles)
|
||||||
|
|
||||||
|
return echo(message, file=file, nl=nl, err=err, color=color)
|
||||||
|
|
||||||
|
|
||||||
|
def edit(
|
||||||
|
text: t.Optional[t.AnyStr] = None,
|
||||||
|
editor: t.Optional[str] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, str]] = None,
|
||||||
|
require_save: bool = True,
|
||||||
|
extension: str = ".txt",
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> t.Optional[t.AnyStr]:
|
||||||
|
r"""Edits the given text in the defined editor. If an editor is given
|
||||||
|
(should be the full path to the executable but the regular operating
|
||||||
|
system search path is used for finding the executable) it overrides
|
||||||
|
the detected editor. Optionally, some environment variables can be
|
||||||
|
used. If the editor is closed without changes, `None` is returned. In
|
||||||
|
case a file is edited directly the return value is always `None` and
|
||||||
|
`require_save` and `extension` are ignored.
|
||||||
|
|
||||||
|
If the editor cannot be opened a :exc:`UsageError` is raised.
|
||||||
|
|
||||||
|
Note for Windows: to simplify cross-platform usage, the newlines are
|
||||||
|
automatically converted from POSIX to Windows and vice versa. As such,
|
||||||
|
the message here will have ``\n`` as newline markers.
|
||||||
|
|
||||||
|
:param text: the text to edit.
|
||||||
|
:param editor: optionally the editor to use. Defaults to automatic
|
||||||
|
detection.
|
||||||
|
:param env: environment variables to forward to the editor.
|
||||||
|
:param require_save: if this is true, then not saving in the editor
|
||||||
|
will make the return value become `None`.
|
||||||
|
:param extension: the extension to tell the editor about. This defaults
|
||||||
|
to `.txt` but changing this might change syntax
|
||||||
|
highlighting.
|
||||||
|
:param filename: if provided it will edit this file instead of the
|
||||||
|
provided text contents. It will not use a temporary
|
||||||
|
file as an indirection in that case.
|
||||||
|
"""
|
||||||
|
from ._termui_impl import Editor
|
||||||
|
|
||||||
|
ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
return ed.edit(text)
|
||||||
|
|
||||||
|
ed.edit_file(filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def launch(url: str, wait: bool = False, locate: bool = False) -> int:
|
||||||
|
"""This function launches the given URL (or filename) in the default
|
||||||
|
viewer application for this file type. If this is an executable, it
|
||||||
|
might launch the executable in a new session. The return value is
|
||||||
|
the exit code of the launched application. Usually, ``0`` indicates
|
||||||
|
success.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
click.launch('https://click.palletsprojects.com/')
|
||||||
|
click.launch('/my/downloaded/file', locate=True)
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param url: URL or filename of the thing to launch.
|
||||||
|
:param wait: Wait for the program to exit before returning. This
|
||||||
|
only works if the launched program blocks. In particular,
|
||||||
|
``xdg-open`` on Linux does not block.
|
||||||
|
:param locate: if this is set to `True` then instead of launching the
|
||||||
|
application associated with the URL it will attempt to
|
||||||
|
launch a file manager with the file located. This
|
||||||
|
might have weird effects if the URL does not point to
|
||||||
|
the filesystem.
|
||||||
|
"""
|
||||||
|
from ._termui_impl import open_url
|
||||||
|
|
||||||
|
return open_url(url, wait=wait, locate=locate)
|
||||||
|
|
||||||
|
|
||||||
|
# If this is provided, getchar() calls into this instead. This is used
|
||||||
|
# for unittesting purposes.
|
||||||
|
_getchar: t.Optional[t.Callable[[bool], str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def getchar(echo: bool = False) -> str:
|
||||||
|
"""Fetches a single character from the terminal and returns it. This
|
||||||
|
will always return a unicode character and under certain rare
|
||||||
|
circumstances this might return more than one character. The
|
||||||
|
situations which more than one character is returned is when for
|
||||||
|
whatever reason multiple characters end up in the terminal buffer or
|
||||||
|
standard input was not actually a terminal.
|
||||||
|
|
||||||
|
Note that this will always read from the terminal, even if something
|
||||||
|
is piped into the standard input.
|
||||||
|
|
||||||
|
Note for Windows: in rare cases when typing non-ASCII characters, this
|
||||||
|
function might wait for a second character and then return both at once.
|
||||||
|
This is because certain Unicode characters look like special-key markers.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param echo: if set to `True`, the character read will also show up on
|
||||||
|
the terminal. The default is to not show it.
|
||||||
|
"""
|
||||||
|
global _getchar
|
||||||
|
|
||||||
|
if _getchar is None:
|
||||||
|
from ._termui_impl import getchar as f
|
||||||
|
|
||||||
|
_getchar = f
|
||||||
|
|
||||||
|
return _getchar(echo)
|
||||||
|
|
||||||
|
|
||||||
|
def raw_terminal() -> t.ContextManager[int]:
|
||||||
|
from ._termui_impl import raw_terminal as f
|
||||||
|
|
||||||
|
return f()
|
||||||
|
|
||||||
|
|
||||||
|
def pause(info: t.Optional[str] = None, err: bool = False) -> None:
|
||||||
|
"""This command stops execution and waits for the user to press any
|
||||||
|
key to continue. This is similar to the Windows batch "pause"
|
||||||
|
command. If the program is not run through a terminal, this command
|
||||||
|
will instead do nothing.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
:param info: The message to print before pausing. Defaults to
|
||||||
|
``"Press any key to continue..."``.
|
||||||
|
:param err: if set to message goes to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
"""
|
||||||
|
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
||||||
|
return
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
info = _("Press any key to continue...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if info:
|
||||||
|
echo(info, nl=False, err=err)
|
||||||
|
try:
|
||||||
|
getchar()
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if info:
|
||||||
|
echo(err=err)
|
@ -0,0 +1,479 @@
|
|||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import typing as t
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
from . import formatting
|
||||||
|
from . import termui
|
||||||
|
from . import utils
|
||||||
|
from ._compat import _find_binary_reader
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .core import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class EchoingStdin:
|
||||||
|
def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
|
||||||
|
self._input = input
|
||||||
|
self._output = output
|
||||||
|
self._paused = False
|
||||||
|
|
||||||
|
def __getattr__(self, x: str) -> t.Any:
|
||||||
|
return getattr(self._input, x)
|
||||||
|
|
||||||
|
def _echo(self, rv: bytes) -> bytes:
|
||||||
|
if not self._paused:
|
||||||
|
self._output.write(rv)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def read(self, n: int = -1) -> bytes:
|
||||||
|
return self._echo(self._input.read(n))
|
||||||
|
|
||||||
|
def read1(self, n: int = -1) -> bytes:
|
||||||
|
return self._echo(self._input.read1(n)) # type: ignore
|
||||||
|
|
||||||
|
def readline(self, n: int = -1) -> bytes:
|
||||||
|
return self._echo(self._input.readline(n))
|
||||||
|
|
||||||
|
def readlines(self) -> t.List[bytes]:
|
||||||
|
return [self._echo(x) for x in self._input.readlines()]
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[bytes]:
|
||||||
|
return iter(self._echo(x) for x in self._input)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return repr(self._input)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]:
|
||||||
|
if stream is None:
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
stream._paused = True
|
||||||
|
yield
|
||||||
|
stream._paused = False
|
||||||
|
|
||||||
|
|
||||||
|
class _NamedTextIOWrapper(io.TextIOWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(buffer, **kwargs)
|
||||||
|
self._name = name
|
||||||
|
self._mode = mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self) -> str:
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
|
||||||
|
def make_input_stream(
|
||||||
|
input: t.Optional[t.Union[str, bytes, t.IO]], charset: str
|
||||||
|
) -> t.BinaryIO:
|
||||||
|
# Is already an input stream.
|
||||||
|
if hasattr(input, "read"):
|
||||||
|
rv = _find_binary_reader(t.cast(t.IO, input))
|
||||||
|
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
|
||||||
|
raise TypeError("Could not find binary reader for input stream.")
|
||||||
|
|
||||||
|
if input is None:
|
||||||
|
input = b""
|
||||||
|
elif isinstance(input, str):
|
||||||
|
input = input.encode(charset)
|
||||||
|
|
||||||
|
return io.BytesIO(t.cast(bytes, input))
|
||||||
|
|
||||||
|
|
||||||
|
class Result:
|
||||||
|
"""Holds the captured result of an invoked CLI script."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
runner: "CliRunner",
|
||||||
|
stdout_bytes: bytes,
|
||||||
|
stderr_bytes: t.Optional[bytes],
|
||||||
|
return_value: t.Any,
|
||||||
|
exit_code: int,
|
||||||
|
exception: t.Optional[BaseException],
|
||||||
|
exc_info: t.Optional[
|
||||||
|
t.Tuple[t.Type[BaseException], BaseException, TracebackType]
|
||||||
|
] = None,
|
||||||
|
):
|
||||||
|
#: The runner that created the result
|
||||||
|
self.runner = runner
|
||||||
|
#: The standard output as bytes.
|
||||||
|
self.stdout_bytes = stdout_bytes
|
||||||
|
#: The standard error as bytes, or None if not available
|
||||||
|
self.stderr_bytes = stderr_bytes
|
||||||
|
#: The value returned from the invoked command.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
self.return_value = return_value
|
||||||
|
#: The exit code as integer.
|
||||||
|
self.exit_code = exit_code
|
||||||
|
#: The exception that happened if one did.
|
||||||
|
self.exception = exception
|
||||||
|
#: The traceback
|
||||||
|
self.exc_info = exc_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output(self) -> str:
|
||||||
|
"""The (standard) output as unicode string."""
|
||||||
|
return self.stdout
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stdout(self) -> str:
|
||||||
|
"""The standard output as unicode string."""
|
||||||
|
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
||||||
|
"\r\n", "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stderr(self) -> str:
|
||||||
|
"""The standard error as unicode string."""
|
||||||
|
if self.stderr_bytes is None:
|
||||||
|
raise ValueError("stderr not separately captured")
|
||||||
|
return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
|
||||||
|
"\r\n", "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
exc_str = repr(self.exception) if self.exception else "okay"
|
||||||
|
return f"<{type(self).__name__} {exc_str}>"
|
||||||
|
|
||||||
|
|
||||||
|
class CliRunner:
|
||||||
|
"""The CLI runner provides functionality to invoke a Click command line
|
||||||
|
script for unittesting purposes in a isolated environment. This only
|
||||||
|
works in single-threaded systems without any concurrency as it changes the
|
||||||
|
global interpreter state.
|
||||||
|
|
||||||
|
:param charset: the character set for the input and output data.
|
||||||
|
:param env: a dictionary with environment variables for overriding.
|
||||||
|
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
||||||
|
to stdout. This is useful for showing examples in
|
||||||
|
some circumstances. Note that regular prompts
|
||||||
|
will automatically echo the input.
|
||||||
|
:param mix_stderr: if this is set to `False`, then stdout and stderr are
|
||||||
|
preserved as independent streams. This is useful for
|
||||||
|
Unix-philosophy apps that have predictable stdout and
|
||||||
|
noisy stderr, such that each may be measured
|
||||||
|
independently
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
charset: str = "utf-8",
|
||||||
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
|
echo_stdin: bool = False,
|
||||||
|
mix_stderr: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.charset = charset
|
||||||
|
self.env = env or {}
|
||||||
|
self.echo_stdin = echo_stdin
|
||||||
|
self.mix_stderr = mix_stderr
|
||||||
|
|
||||||
|
def get_default_prog_name(self, cli: "BaseCommand") -> str:
|
||||||
|
"""Given a command object it will return the default program name
|
||||||
|
for it. The default is the `name` attribute or ``"root"`` if not
|
||||||
|
set.
|
||||||
|
"""
|
||||||
|
return cli.name or "root"
|
||||||
|
|
||||||
|
def make_env(
|
||||||
|
self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None
|
||||||
|
) -> t.Mapping[str, t.Optional[str]]:
|
||||||
|
"""Returns the environment overrides for invoking a script."""
|
||||||
|
rv = dict(self.env)
|
||||||
|
if overrides:
|
||||||
|
rv.update(overrides)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def isolation(
|
||||||
|
self,
|
||||||
|
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
|
color: bool = False,
|
||||||
|
) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]:
|
||||||
|
"""A context manager that sets up the isolation for invoking of a
|
||||||
|
command line tool. This sets up stdin with the given input data
|
||||||
|
and `os.environ` with the overrides from the given dictionary.
|
||||||
|
This also rebinds some internals in Click to be mocked (like the
|
||||||
|
prompt functionality).
|
||||||
|
|
||||||
|
This is automatically done in the :meth:`invoke` method.
|
||||||
|
|
||||||
|
:param input: the input stream to put into sys.stdin.
|
||||||
|
:param env: the environment overrides as dictionary.
|
||||||
|
:param color: whether the output should contain color codes. The
|
||||||
|
application can still override this explicitly.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``stderr`` is opened with ``errors="backslashreplace"``
|
||||||
|
instead of the default ``"strict"``.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter.
|
||||||
|
"""
|
||||||
|
bytes_input = make_input_stream(input, self.charset)
|
||||||
|
echo_input = None
|
||||||
|
|
||||||
|
old_stdin = sys.stdin
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
old_stderr = sys.stderr
|
||||||
|
old_forced_width = formatting.FORCED_WIDTH
|
||||||
|
formatting.FORCED_WIDTH = 80
|
||||||
|
|
||||||
|
env = self.make_env(env)
|
||||||
|
|
||||||
|
bytes_output = io.BytesIO()
|
||||||
|
|
||||||
|
if self.echo_stdin:
|
||||||
|
bytes_input = echo_input = t.cast(
|
||||||
|
t.BinaryIO, EchoingStdin(bytes_input, bytes_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.stdin = text_input = _NamedTextIOWrapper(
|
||||||
|
bytes_input, encoding=self.charset, name="<stdin>", mode="r"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.echo_stdin:
|
||||||
|
# Force unbuffered reads, otherwise TextIOWrapper reads a
|
||||||
|
# large chunk which is echoed early.
|
||||||
|
text_input._CHUNK_SIZE = 1 # type: ignore
|
||||||
|
|
||||||
|
sys.stdout = _NamedTextIOWrapper(
|
||||||
|
bytes_output, encoding=self.charset, name="<stdout>", mode="w"
|
||||||
|
)
|
||||||
|
|
||||||
|
bytes_error = None
|
||||||
|
if self.mix_stderr:
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
else:
|
||||||
|
bytes_error = io.BytesIO()
|
||||||
|
sys.stderr = _NamedTextIOWrapper(
|
||||||
|
bytes_error,
|
||||||
|
encoding=self.charset,
|
||||||
|
name="<stderr>",
|
||||||
|
mode="w",
|
||||||
|
errors="backslashreplace",
|
||||||
|
)
|
||||||
|
|
||||||
|
@_pause_echo(echo_input) # type: ignore
|
||||||
|
def visible_input(prompt: t.Optional[str] = None) -> str:
|
||||||
|
sys.stdout.write(prompt or "")
|
||||||
|
val = text_input.readline().rstrip("\r\n")
|
||||||
|
sys.stdout.write(f"{val}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return val
|
||||||
|
|
||||||
|
@_pause_echo(echo_input) # type: ignore
|
||||||
|
def hidden_input(prompt: t.Optional[str] = None) -> str:
|
||||||
|
sys.stdout.write(f"{prompt or ''}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return text_input.readline().rstrip("\r\n")
|
||||||
|
|
||||||
|
@_pause_echo(echo_input) # type: ignore
|
||||||
|
def _getchar(echo: bool) -> str:
|
||||||
|
char = sys.stdin.read(1)
|
||||||
|
|
||||||
|
if echo:
|
||||||
|
sys.stdout.write(char)
|
||||||
|
|
||||||
|
sys.stdout.flush()
|
||||||
|
return char
|
||||||
|
|
||||||
|
default_color = color
|
||||||
|
|
||||||
|
def should_strip_ansi(
|
||||||
|
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
|
||||||
|
) -> bool:
|
||||||
|
if color is None:
|
||||||
|
return not default_color
|
||||||
|
return not color
|
||||||
|
|
||||||
|
old_visible_prompt_func = termui.visible_prompt_func
|
||||||
|
old_hidden_prompt_func = termui.hidden_prompt_func
|
||||||
|
old__getchar_func = termui._getchar
|
||||||
|
old_should_strip_ansi = utils.should_strip_ansi # type: ignore
|
||||||
|
termui.visible_prompt_func = visible_input
|
||||||
|
termui.hidden_prompt_func = hidden_input
|
||||||
|
termui._getchar = _getchar
|
||||||
|
utils.should_strip_ansi = should_strip_ansi # type: ignore
|
||||||
|
|
||||||
|
old_env = {}
|
||||||
|
try:
|
||||||
|
for key, value in env.items():
|
||||||
|
old_env[key] = os.environ.get(key)
|
||||||
|
if value is None:
|
||||||
|
try:
|
||||||
|
del os.environ[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.environ[key] = value
|
||||||
|
yield (bytes_output, bytes_error)
|
||||||
|
finally:
|
||||||
|
for key, value in old_env.items():
|
||||||
|
if value is None:
|
||||||
|
try:
|
||||||
|
del os.environ[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.environ[key] = value
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
sys.stderr = old_stderr
|
||||||
|
sys.stdin = old_stdin
|
||||||
|
termui.visible_prompt_func = old_visible_prompt_func
|
||||||
|
termui.hidden_prompt_func = old_hidden_prompt_func
|
||||||
|
termui._getchar = old__getchar_func
|
||||||
|
utils.should_strip_ansi = old_should_strip_ansi # type: ignore
|
||||||
|
formatting.FORCED_WIDTH = old_forced_width
|
||||||
|
|
||||||
|
def invoke(
|
||||||
|
self,
|
||||||
|
cli: "BaseCommand",
|
||||||
|
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
|
||||||
|
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
|
catch_exceptions: bool = True,
|
||||||
|
color: bool = False,
|
||||||
|
**extra: t.Any,
|
||||||
|
) -> Result:
|
||||||
|
"""Invokes a command in an isolated environment. The arguments are
|
||||||
|
forwarded directly to the command line script, the `extra` keyword
|
||||||
|
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
||||||
|
the command.
|
||||||
|
|
||||||
|
This returns a :class:`Result` object.
|
||||||
|
|
||||||
|
:param cli: the command to invoke
|
||||||
|
:param args: the arguments to invoke. It may be given as an iterable
|
||||||
|
or a string. When given as string it will be interpreted
|
||||||
|
as a Unix shell command. More details at
|
||||||
|
:func:`shlex.split`.
|
||||||
|
:param input: the input data for `sys.stdin`.
|
||||||
|
:param env: the environment overrides.
|
||||||
|
:param catch_exceptions: Whether to catch any other exceptions than
|
||||||
|
``SystemExit``.
|
||||||
|
:param extra: the keyword arguments to pass to :meth:`main`.
|
||||||
|
:param color: whether the output should contain color codes. The
|
||||||
|
application can still override this explicitly.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
The result object has the ``return_value`` attribute with
|
||||||
|
the value returned from the invoked command.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the ``catch_exceptions`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The result object has the ``exc_info`` attribute with the
|
||||||
|
traceback if available.
|
||||||
|
"""
|
||||||
|
exc_info = None
|
||||||
|
with self.isolation(input=input, env=env, color=color) as outstreams:
|
||||||
|
return_value = None
|
||||||
|
exception: t.Optional[BaseException] = None
|
||||||
|
exit_code = 0
|
||||||
|
|
||||||
|
if isinstance(args, str):
|
||||||
|
args = shlex.split(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prog_name = extra.pop("prog_name")
|
||||||
|
except KeyError:
|
||||||
|
prog_name = self.get_default_prog_name(cli)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
|
||||||
|
except SystemExit as e:
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code)
|
||||||
|
|
||||||
|
if e_code is None:
|
||||||
|
e_code = 0
|
||||||
|
|
||||||
|
if e_code != 0:
|
||||||
|
exception = e
|
||||||
|
|
||||||
|
if not isinstance(e_code, int):
|
||||||
|
sys.stdout.write(str(e_code))
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
e_code = 1
|
||||||
|
|
||||||
|
exit_code = e_code
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not catch_exceptions:
|
||||||
|
raise
|
||||||
|
exception = e
|
||||||
|
exit_code = 1
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
finally:
|
||||||
|
sys.stdout.flush()
|
||||||
|
stdout = outstreams[0].getvalue()
|
||||||
|
if self.mix_stderr:
|
||||||
|
stderr = None
|
||||||
|
else:
|
||||||
|
stderr = outstreams[1].getvalue() # type: ignore
|
||||||
|
|
||||||
|
return Result(
|
||||||
|
runner=self,
|
||||||
|
stdout_bytes=stdout,
|
||||||
|
stderr_bytes=stderr,
|
||||||
|
return_value=return_value,
|
||||||
|
exit_code=exit_code,
|
||||||
|
exception=exception,
|
||||||
|
exc_info=exc_info, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def isolated_filesystem(
|
||||||
|
self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None
|
||||||
|
) -> t.Iterator[str]:
|
||||||
|
"""A context manager that creates a temporary directory and
|
||||||
|
changes the current working directory to it. This isolates tests
|
||||||
|
that affect the contents of the CWD to prevent them from
|
||||||
|
interfering with each other.
|
||||||
|
|
||||||
|
:param temp_dir: Create the temporary directory under this
|
||||||
|
directory. If given, the created directory is not removed
|
||||||
|
when exiting.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``temp_dir`` parameter.
|
||||||
|
"""
|
||||||
|
cwd = os.getcwd()
|
||||||
|
dt = tempfile.mkdtemp(dir=temp_dir) # type: ignore[type-var]
|
||||||
|
os.chdir(dt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield t.cast(str, dt)
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
|
if temp_dir is None:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(dt)
|
||||||
|
except OSError: # noqa: B014
|
||||||
|
pass
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,580 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from functools import update_wrapper
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
from ._compat import _default_text_stderr
|
||||||
|
from ._compat import _default_text_stdout
|
||||||
|
from ._compat import _find_binary_writer
|
||||||
|
from ._compat import auto_wrap_for_ansi
|
||||||
|
from ._compat import binary_streams
|
||||||
|
from ._compat import get_filesystem_encoding
|
||||||
|
from ._compat import open_stream
|
||||||
|
from ._compat import should_strip_ansi
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import text_streams
|
||||||
|
from ._compat import WIN
|
||||||
|
from .globals import resolve_color_default
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
|
||||||
|
def _posixify(name: str) -> str:
|
||||||
|
return "-".join(name.split()).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def safecall(func: F) -> F:
|
||||||
|
"""Wraps a function so that it swallows exceptions."""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs): # type: ignore
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, wrapper), func)
|
||||||
|
|
||||||
|
|
||||||
|
def make_str(value: t.Any) -> str:
|
||||||
|
"""Converts a value into a valid string."""
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
try:
|
||||||
|
return value.decode(get_filesystem_encoding())
|
||||||
|
except UnicodeError:
|
||||||
|
return value.decode("utf-8", "replace")
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def make_default_short_help(help: str, max_length: int = 45) -> str:
|
||||||
|
"""Returns a condensed version of help string."""
|
||||||
|
# Consider only the first paragraph.
|
||||||
|
paragraph_end = help.find("\n\n")
|
||||||
|
|
||||||
|
if paragraph_end != -1:
|
||||||
|
help = help[:paragraph_end]
|
||||||
|
|
||||||
|
# Collapse newlines, tabs, and spaces.
|
||||||
|
words = help.split()
|
||||||
|
|
||||||
|
if not words:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# The first paragraph started with a "no rewrap" marker, ignore it.
|
||||||
|
if words[0] == "\b":
|
||||||
|
words = words[1:]
|
||||||
|
|
||||||
|
total_length = 0
|
||||||
|
last_index = len(words) - 1
|
||||||
|
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
total_length += len(word) + (i > 0)
|
||||||
|
|
||||||
|
if total_length > max_length: # too long, truncate
|
||||||
|
break
|
||||||
|
|
||||||
|
if word[-1] == ".": # sentence end, truncate without "..."
|
||||||
|
return " ".join(words[: i + 1])
|
||||||
|
|
||||||
|
if total_length == max_length and i != last_index:
|
||||||
|
break # not at sentence end, truncate with "..."
|
||||||
|
else:
|
||||||
|
return " ".join(words) # no truncation needed
|
||||||
|
|
||||||
|
# Account for the length of the suffix.
|
||||||
|
total_length += len("...")
|
||||||
|
|
||||||
|
# remove words until the length is short enough
|
||||||
|
while i > 0:
|
||||||
|
total_length -= len(words[i]) + (i > 0)
|
||||||
|
|
||||||
|
if total_length <= max_length:
|
||||||
|
break
|
||||||
|
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
return " ".join(words[:i]) + "..."
|
||||||
|
|
||||||
|
|
||||||
|
class LazyFile:
|
||||||
|
"""A lazy file works like a regular file but it does not fully open
|
||||||
|
the file but it does perform some basic checks early to see if the
|
||||||
|
filename parameter does make sense. This is useful for safely opening
|
||||||
|
files for writing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filename: str,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
atomic: bool = False,
|
||||||
|
):
|
||||||
|
self.name = filename
|
||||||
|
self.mode = mode
|
||||||
|
self.encoding = encoding
|
||||||
|
self.errors = errors
|
||||||
|
self.atomic = atomic
|
||||||
|
self._f: t.Optional[t.IO]
|
||||||
|
|
||||||
|
if filename == "-":
|
||||||
|
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
||||||
|
else:
|
||||||
|
if "r" in mode:
|
||||||
|
# Open and close the file in case we're opening it for
|
||||||
|
# reading so that we can catch at least some errors in
|
||||||
|
# some cases early.
|
||||||
|
open(filename, mode).close()
|
||||||
|
self._f = None
|
||||||
|
self.should_close = True
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self.open(), name)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
if self._f is not None:
|
||||||
|
return repr(self._f)
|
||||||
|
return f"<unopened file '{self.name}' {self.mode}>"
|
||||||
|
|
||||||
|
def open(self) -> t.IO:
|
||||||
|
"""Opens the file if it's not yet open. This call might fail with
|
||||||
|
a :exc:`FileError`. Not handling this error will produce an error
|
||||||
|
that Click shows.
|
||||||
|
"""
|
||||||
|
if self._f is not None:
|
||||||
|
return self._f
|
||||||
|
try:
|
||||||
|
rv, self.should_close = open_stream(
|
||||||
|
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
|
)
|
||||||
|
except OSError as e: # noqa: E402
|
||||||
|
from .exceptions import FileError
|
||||||
|
|
||||||
|
raise FileError(self.name, hint=e.strerror) from e
|
||||||
|
self._f = rv
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Closes the underlying file, no matter what."""
|
||||||
|
if self._f is not None:
|
||||||
|
self._f.close()
|
||||||
|
|
||||||
|
def close_intelligently(self) -> None:
|
||||||
|
"""This function only closes the file if it was opened by the lazy
|
||||||
|
file wrapper. For instance this will never close stdin.
|
||||||
|
"""
|
||||||
|
if self.should_close:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __enter__(self) -> "LazyFile":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
|
self.close_intelligently()
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||||
|
self.open()
|
||||||
|
return iter(self._f) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class KeepOpenFile:
|
||||||
|
def __init__(self, file: t.IO) -> None:
|
||||||
|
self._file = file
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return getattr(self._file, name)
|
||||||
|
|
||||||
|
def __enter__(self) -> "KeepOpenFile":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return repr(self._file)
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||||
|
return iter(self._file)
|
||||||
|
|
||||||
|
|
||||||
|
def echo(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
file: t.Optional[t.IO[t.Any]] = None,
|
||||||
|
nl: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Print a message and newline to stdout or a file. This should be
|
||||||
|
used instead of :func:`print` because it provides better support
|
||||||
|
for different data, files, and environments.
|
||||||
|
|
||||||
|
Compared to :func:`print`, this does the following:
|
||||||
|
|
||||||
|
- Ensures that the output encoding is not misconfigured on Linux.
|
||||||
|
- Supports Unicode in the Windows console.
|
||||||
|
- Supports writing to binary outputs, and supports writing bytes
|
||||||
|
to text outputs.
|
||||||
|
- Supports colors and styles on Windows.
|
||||||
|
- Removes ANSI color and style codes if the output does not look
|
||||||
|
like an interactive terminal.
|
||||||
|
- Always flushes the output.
|
||||||
|
|
||||||
|
:param message: The string or bytes to output. Other objects are
|
||||||
|
converted to strings.
|
||||||
|
:param file: The file to write to. Defaults to ``stdout``.
|
||||||
|
:param err: Write to ``stderr`` instead of ``stdout``.
|
||||||
|
:param nl: Print a newline after the message. Enabled by default.
|
||||||
|
:param color: Force showing or hiding colors and other styles. By
|
||||||
|
default Click will remove color if the output does not look like
|
||||||
|
an interactive terminal.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.0
|
||||||
|
Support Unicode output on the Windows console. Click does not
|
||||||
|
modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
|
||||||
|
will still not support Unicode.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
Added the ``err`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Support colors on Windows if colorama is installed.
|
||||||
|
"""
|
||||||
|
if file is None:
|
||||||
|
if err:
|
||||||
|
file = _default_text_stderr()
|
||||||
|
else:
|
||||||
|
file = _default_text_stdout()
|
||||||
|
|
||||||
|
# Convert non bytes/text into the native string type.
|
||||||
|
if message is not None and not isinstance(message, (str, bytes, bytearray)):
|
||||||
|
out: t.Optional[t.Union[str, bytes]] = str(message)
|
||||||
|
else:
|
||||||
|
out = message
|
||||||
|
|
||||||
|
if nl:
|
||||||
|
out = out or ""
|
||||||
|
if isinstance(out, str):
|
||||||
|
out += "\n"
|
||||||
|
else:
|
||||||
|
out += b"\n"
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
file.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there is a message and the value looks like bytes, we manually
|
||||||
|
# need to find the binary stream and write the message in there.
|
||||||
|
# This is done separately so that most stream types will work as you
|
||||||
|
# would expect. Eg: you can write to StringIO for other cases.
|
||||||
|
if isinstance(out, (bytes, bytearray)):
|
||||||
|
binary_file = _find_binary_writer(file)
|
||||||
|
|
||||||
|
if binary_file is not None:
|
||||||
|
file.flush()
|
||||||
|
binary_file.write(out)
|
||||||
|
binary_file.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ANSI style code support. For no message or bytes, nothing happens.
|
||||||
|
# When outputting to a file instead of a terminal, strip codes.
|
||||||
|
else:
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
|
||||||
|
if should_strip_ansi(file, color):
|
||||||
|
out = strip_ansi(out)
|
||||||
|
elif WIN:
|
||||||
|
if auto_wrap_for_ansi is not None:
|
||||||
|
file = auto_wrap_for_ansi(file) # type: ignore
|
||||||
|
elif not color:
|
||||||
|
out = strip_ansi(out)
|
||||||
|
|
||||||
|
file.write(out) # type: ignore
|
||||||
|
file.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO:
|
||||||
|
"""Returns a system stream for byte processing.
|
||||||
|
|
||||||
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
|
``'stdout'`` and ``'stderr'``
|
||||||
|
"""
|
||||||
|
opener = binary_streams.get(name)
|
||||||
|
if opener is None:
|
||||||
|
raise TypeError(f"Unknown standard stream '{name}'")
|
||||||
|
return opener()
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stream(
|
||||||
|
name: "te.Literal['stdin', 'stdout', 'stderr']",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
) -> t.TextIO:
|
||||||
|
"""Returns a system stream for text processing. This usually returns
|
||||||
|
a wrapped stream around a binary stream returned from
|
||||||
|
:func:`get_binary_stream` but it also can take shortcuts for already
|
||||||
|
correctly configured streams.
|
||||||
|
|
||||||
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
|
``'stdout'`` and ``'stderr'``
|
||||||
|
:param encoding: overrides the detected default encoding.
|
||||||
|
:param errors: overrides the default error mode.
|
||||||
|
"""
|
||||||
|
opener = text_streams.get(name)
|
||||||
|
if opener is None:
|
||||||
|
raise TypeError(f"Unknown standard stream '{name}'")
|
||||||
|
return opener(encoding, errors)
|
||||||
|
|
||||||
|
|
||||||
|
def open_file(
|
||||||
|
filename: str,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
lazy: bool = False,
|
||||||
|
atomic: bool = False,
|
||||||
|
) -> t.IO:
|
||||||
|
"""Open a file, with extra behavior to handle ``'-'`` to indicate
|
||||||
|
a standard stream, lazy open on write, and atomic write. Similar to
|
||||||
|
the behavior of the :class:`~click.File` param type.
|
||||||
|
|
||||||
|
If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is
|
||||||
|
wrapped so that using it in a context manager will not close it.
|
||||||
|
This makes it possible to use the function without accidentally
|
||||||
|
closing a standard stream:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with open_file(filename) as f:
|
||||||
|
...
|
||||||
|
|
||||||
|
:param filename: The name of the file to open, or ``'-'`` for
|
||||||
|
``stdin``/``stdout``.
|
||||||
|
:param mode: The mode in which to open the file.
|
||||||
|
:param encoding: The encoding to decode or encode a file opened in
|
||||||
|
text mode.
|
||||||
|
:param errors: The error handling mode.
|
||||||
|
:param lazy: Wait to open the file until it is accessed. For read
|
||||||
|
mode, the file is temporarily opened to raise access errors
|
||||||
|
early, then closed until it is read again.
|
||||||
|
:param atomic: Write to a temporary file and replace the given file
|
||||||
|
on close.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
if lazy:
|
||||||
|
return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
|
||||||
|
|
||||||
|
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
||||||
|
|
||||||
|
if not should_close:
|
||||||
|
f = t.cast(t.IO, KeepOpenFile(f))
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def format_filename(
|
||||||
|
filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
|
||||||
|
) -> str:
|
||||||
|
"""Formats a filename for user display. The main purpose of this
|
||||||
|
function is to ensure that the filename can be displayed at all. This
|
||||||
|
will decode the filename to unicode if necessary in a way that it will
|
||||||
|
not fail. Optionally, it can shorten the filename to not include the
|
||||||
|
full path to the filename.
|
||||||
|
|
||||||
|
:param filename: formats a filename for UI display. This will also convert
|
||||||
|
the filename into unicode without failing.
|
||||||
|
:param shorten: this optionally shortens the filename to strip of the
|
||||||
|
path that leads up to it.
|
||||||
|
"""
|
||||||
|
if shorten:
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
|
||||||
|
return os.fsdecode(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
|
||||||
|
r"""Returns the config folder for the application. The default behavior
|
||||||
|
is to return whatever is most appropriate for the operating system.
|
||||||
|
|
||||||
|
To give you an idea, for an app called ``"Foo Bar"``, something like
|
||||||
|
the following folders could be returned:
|
||||||
|
|
||||||
|
Mac OS X:
|
||||||
|
``~/Library/Application Support/Foo Bar``
|
||||||
|
Mac OS X (POSIX):
|
||||||
|
``~/.foo-bar``
|
||||||
|
Unix:
|
||||||
|
``~/.config/foo-bar``
|
||||||
|
Unix (POSIX):
|
||||||
|
``~/.foo-bar``
|
||||||
|
Windows (roaming):
|
||||||
|
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
||||||
|
Windows (not roaming):
|
||||||
|
``C:\Users\<user>\AppData\Local\Foo Bar``
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param app_name: the application name. This should be properly capitalized
|
||||||
|
and can contain whitespace.
|
||||||
|
:param roaming: controls if the folder should be roaming or not on Windows.
|
||||||
|
Has no affect otherwise.
|
||||||
|
:param force_posix: if this is set to `True` then on any POSIX system the
|
||||||
|
folder will be stored in the home folder with a leading
|
||||||
|
dot instead of the XDG config home or darwin's
|
||||||
|
application support folder.
|
||||||
|
"""
|
||||||
|
if WIN:
|
||||||
|
key = "APPDATA" if roaming else "LOCALAPPDATA"
|
||||||
|
folder = os.environ.get(key)
|
||||||
|
if folder is None:
|
||||||
|
folder = os.path.expanduser("~")
|
||||||
|
return os.path.join(folder, app_name)
|
||||||
|
if force_posix:
|
||||||
|
return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return os.path.join(
|
||||||
|
os.path.expanduser("~/Library/Application Support"), app_name
|
||||||
|
)
|
||||||
|
return os.path.join(
|
||||||
|
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
||||||
|
_posixify(app_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PacifyFlushWrapper:
|
||||||
|
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
||||||
|
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
||||||
|
of the Python interpreter. Notably ``.flush()`` is always called on
|
||||||
|
``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
|
||||||
|
other cleanup code, and the case where the underlying file is not a broken
|
||||||
|
pipe, all calls and attributes are proxied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, wrapped: t.IO) -> None:
|
||||||
|
self.wrapped = wrapped
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
try:
|
||||||
|
self.wrapped.flush()
|
||||||
|
except OSError as e:
|
||||||
|
import errno
|
||||||
|
|
||||||
|
if e.errno != errno.EPIPE:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __getattr__(self, attr: str) -> t.Any:
|
||||||
|
return getattr(self.wrapped, attr)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_program_name(
|
||||||
|
path: t.Optional[str] = None, _main: t.Optional[ModuleType] = None
|
||||||
|
) -> str:
|
||||||
|
"""Determine the command used to run the program, for use in help
|
||||||
|
text. If a file or entry point was executed, the file name is
|
||||||
|
returned. If ``python -m`` was used to execute a module or package,
|
||||||
|
``python -m name`` is returned.
|
||||||
|
|
||||||
|
This doesn't try to be too precise, the goal is to give a concise
|
||||||
|
name for help text. Files are only shown as their name without the
|
||||||
|
path. ``python`` is only shown for modules, and the full path to
|
||||||
|
``sys.executable`` is not shown.
|
||||||
|
|
||||||
|
:param path: The Python file being executed. Python puts this in
|
||||||
|
``sys.argv[0]``, which is used by default.
|
||||||
|
:param _main: The ``__main__`` module. This should only be passed
|
||||||
|
during internal testing.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
Based on command args detection in the Werkzeug reloader.
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
if _main is None:
|
||||||
|
_main = sys.modules["__main__"]
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
path = sys.argv[0]
|
||||||
|
|
||||||
|
# The value of __package__ indicates how Python was called. It may
|
||||||
|
# not exist if a setuptools script is installed as an egg. It may be
|
||||||
|
# set incorrectly for entry points created with pip on Windows.
|
||||||
|
if getattr(_main, "__package__", None) is None or (
|
||||||
|
os.name == "nt"
|
||||||
|
and _main.__package__ == ""
|
||||||
|
and not os.path.exists(path)
|
||||||
|
and os.path.exists(f"{path}.exe")
|
||||||
|
):
|
||||||
|
# Executed a file, like "python app.py".
|
||||||
|
return os.path.basename(path)
|
||||||
|
|
||||||
|
# Executed a module, like "python -m example".
|
||||||
|
# Rewritten by Python from "-m script" to "/path/to/script.py".
|
||||||
|
# Need to look at main module to determine how it was executed.
|
||||||
|
py_module = t.cast(str, _main.__package__)
|
||||||
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
|
||||||
|
# A submodule like "example.cli".
|
||||||
|
if name != "__main__":
|
||||||
|
py_module = f"{py_module}.{name}"
|
||||||
|
|
||||||
|
return f"python -m {py_module.lstrip('.')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_args(
|
||||||
|
args: t.Iterable[str],
|
||||||
|
*,
|
||||||
|
user: bool = True,
|
||||||
|
env: bool = True,
|
||||||
|
glob_recursive: bool = True,
|
||||||
|
) -> t.List[str]:
|
||||||
|
"""Simulate Unix shell expansion with Python functions.
|
||||||
|
|
||||||
|
See :func:`glob.glob`, :func:`os.path.expanduser`, and
|
||||||
|
:func:`os.path.expandvars`.
|
||||||
|
|
||||||
|
This is intended for use on Windows, where the shell does not do any
|
||||||
|
expansion. It may not exactly match what a Unix shell would do.
|
||||||
|
|
||||||
|
:param args: List of command line arguments to expand.
|
||||||
|
:param user: Expand user home directory.
|
||||||
|
:param env: Expand environment variables.
|
||||||
|
:param glob_recursive: ``**`` matches directories recursively.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.1
|
||||||
|
Invalid glob patterns are treated as empty expansions rather
|
||||||
|
than raising an error.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if user:
|
||||||
|
arg = os.path.expanduser(arg)
|
||||||
|
|
||||||
|
if env:
|
||||||
|
arg = os.path.expandvars(arg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
matches = glob(arg, recursive=glob_recursive)
|
||||||
|
except re.error:
|
||||||
|
matches = []
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
out.append(arg)
|
||||||
|
else:
|
||||||
|
out.extend(matches)
|
||||||
|
|
||||||
|
return out
|
@ -0,0 +1,5 @@
|
|||||||
|
"""Run the EasyInstall command"""
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
main()
|
@ -0,0 +1,71 @@
|
|||||||
|
from markupsafe import escape
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from . import json as json
|
||||||
|
from .app import Flask as Flask
|
||||||
|
from .app import Request as Request
|
||||||
|
from .app import Response as Response
|
||||||
|
from .blueprints import Blueprint as Blueprint
|
||||||
|
from .config import Config as Config
|
||||||
|
from .ctx import after_this_request as after_this_request
|
||||||
|
from .ctx import copy_current_request_context as copy_current_request_context
|
||||||
|
from .ctx import has_app_context as has_app_context
|
||||||
|
from .ctx import has_request_context as has_request_context
|
||||||
|
from .globals import current_app as current_app
|
||||||
|
from .globals import g as g
|
||||||
|
from .globals import request as request
|
||||||
|
from .globals import session as session
|
||||||
|
from .helpers import abort as abort
|
||||||
|
from .helpers import flash as flash
|
||||||
|
from .helpers import get_flashed_messages as get_flashed_messages
|
||||||
|
from .helpers import get_template_attribute as get_template_attribute
|
||||||
|
from .helpers import make_response as make_response
|
||||||
|
from .helpers import redirect as redirect
|
||||||
|
from .helpers import send_file as send_file
|
||||||
|
from .helpers import send_from_directory as send_from_directory
|
||||||
|
from .helpers import stream_with_context as stream_with_context
|
||||||
|
from .helpers import url_for as url_for
|
||||||
|
from .json import jsonify as jsonify
|
||||||
|
from .signals import appcontext_popped as appcontext_popped
|
||||||
|
from .signals import appcontext_pushed as appcontext_pushed
|
||||||
|
from .signals import appcontext_tearing_down as appcontext_tearing_down
|
||||||
|
from .signals import before_render_template as before_render_template
|
||||||
|
from .signals import got_request_exception as got_request_exception
|
||||||
|
from .signals import message_flashed as message_flashed
|
||||||
|
from .signals import request_finished as request_finished
|
||||||
|
from .signals import request_started as request_started
|
||||||
|
from .signals import request_tearing_down as request_tearing_down
|
||||||
|
from .signals import signals_available as signals_available
|
||||||
|
from .signals import template_rendered as template_rendered
|
||||||
|
from .templating import render_template as render_template
|
||||||
|
from .templating import render_template_string as render_template_string
|
||||||
|
from .templating import stream_template as stream_template
|
||||||
|
from .templating import stream_template_string as stream_template_string
|
||||||
|
|
||||||
|
__version__ = "2.2.2"
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "_app_ctx_stack":
|
||||||
|
import warnings
|
||||||
|
from .globals import __app_ctx_stack
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return __app_ctx_stack
|
||||||
|
|
||||||
|
if name == "_request_ctx_stack":
|
||||||
|
import warnings
|
||||||
|
from .globals import __request_ctx_stack
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return __request_ctx_stack
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
@ -0,0 +1,3 @@
|
|||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,706 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import typing as t
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
|
from .scaffold import _endpoint_from_view_func
|
||||||
|
from .scaffold import _sentinel
|
||||||
|
from .scaffold import Scaffold
|
||||||
|
from .scaffold import setupmethod
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .app import Flask
|
||||||
|
|
||||||
|
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
||||||
|
T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
|
||||||
|
T_before_first_request = t.TypeVar(
|
||||||
|
"T_before_first_request", bound=ft.BeforeFirstRequestCallable
|
||||||
|
)
|
||||||
|
T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
|
||||||
|
T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
|
||||||
|
T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
|
||||||
|
T_template_context_processor = t.TypeVar(
|
||||||
|
"T_template_context_processor", bound=ft.TemplateContextProcessorCallable
|
||||||
|
)
|
||||||
|
T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
|
||||||
|
T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
|
||||||
|
T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
|
||||||
|
T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
|
||||||
|
T_url_value_preprocessor = t.TypeVar(
|
||||||
|
"T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BlueprintSetupState:
|
||||||
|
"""Temporary holder object for registering a blueprint with the
|
||||||
|
application. An instance of this class is created by the
|
||||||
|
:meth:`~flask.Blueprint.make_setup_state` method and later passed
|
||||||
|
to all register callback functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
blueprint: "Blueprint",
|
||||||
|
app: "Flask",
|
||||||
|
options: t.Any,
|
||||||
|
first_registration: bool,
|
||||||
|
) -> None:
|
||||||
|
#: a reference to the current application
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
#: a reference to the blueprint that created this setup state.
|
||||||
|
self.blueprint = blueprint
|
||||||
|
|
||||||
|
#: a dictionary with all options that were passed to the
|
||||||
|
#: :meth:`~flask.Flask.register_blueprint` method.
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
#: as blueprints can be registered multiple times with the
|
||||||
|
#: application and not everything wants to be registered
|
||||||
|
#: multiple times on it, this attribute can be used to figure
|
||||||
|
#: out if the blueprint was registered in the past already.
|
||||||
|
self.first_registration = first_registration
|
||||||
|
|
||||||
|
subdomain = self.options.get("subdomain")
|
||||||
|
if subdomain is None:
|
||||||
|
subdomain = self.blueprint.subdomain
|
||||||
|
|
||||||
|
#: The subdomain that the blueprint should be active for, ``None``
|
||||||
|
#: otherwise.
|
||||||
|
self.subdomain = subdomain
|
||||||
|
|
||||||
|
url_prefix = self.options.get("url_prefix")
|
||||||
|
if url_prefix is None:
|
||||||
|
url_prefix = self.blueprint.url_prefix
|
||||||
|
#: The prefix that should be used for all URLs defined on the
|
||||||
|
#: blueprint.
|
||||||
|
self.url_prefix = url_prefix
|
||||||
|
|
||||||
|
self.name = self.options.get("name", blueprint.name)
|
||||||
|
self.name_prefix = self.options.get("name_prefix", "")
|
||||||
|
|
||||||
|
#: A dictionary with URL defaults that is added to each and every
|
||||||
|
#: URL that was defined with the blueprint.
|
||||||
|
self.url_defaults = dict(self.blueprint.url_values_defaults)
|
||||||
|
self.url_defaults.update(self.options.get("url_defaults", ()))
|
||||||
|
|
||||||
|
def add_url_rule(
|
||||||
|
self,
|
||||||
|
rule: str,
|
||||||
|
endpoint: t.Optional[str] = None,
|
||||||
|
view_func: t.Optional[t.Callable] = None,
|
||||||
|
**options: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""A helper method to register a rule (and optionally a view function)
|
||||||
|
to the application. The endpoint is automatically prefixed with the
|
||||||
|
blueprint's name.
|
||||||
|
"""
|
||||||
|
if self.url_prefix is not None:
|
||||||
|
if rule:
|
||||||
|
rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
|
||||||
|
else:
|
||||||
|
rule = self.url_prefix
|
||||||
|
options.setdefault("subdomain", self.subdomain)
|
||||||
|
if endpoint is None:
|
||||||
|
endpoint = _endpoint_from_view_func(view_func) # type: ignore
|
||||||
|
defaults = self.url_defaults
|
||||||
|
if "defaults" in options:
|
||||||
|
defaults = dict(defaults, **options.pop("defaults"))
|
||||||
|
|
||||||
|
self.app.add_url_rule(
|
||||||
|
rule,
|
||||||
|
f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
|
||||||
|
view_func,
|
||||||
|
defaults=defaults,
|
||||||
|
**options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Blueprint(Scaffold):
|
||||||
|
"""Represents a blueprint, a collection of routes and other
|
||||||
|
app-related functions that can be registered on a real application
|
||||||
|
later.
|
||||||
|
|
||||||
|
A blueprint is an object that allows defining application functions
|
||||||
|
without requiring an application object ahead of time. It uses the
|
||||||
|
same decorators as :class:`~flask.Flask`, but defers the need for an
|
||||||
|
application by recording them for later registration.
|
||||||
|
|
||||||
|
Decorating a function with a blueprint creates a deferred function
|
||||||
|
that is called with :class:`~flask.blueprints.BlueprintSetupState`
|
||||||
|
when the blueprint is registered on an application.
|
||||||
|
|
||||||
|
See :doc:`/blueprints` for more information.
|
||||||
|
|
||||||
|
:param name: The name of the blueprint. Will be prepended to each
|
||||||
|
endpoint name.
|
||||||
|
:param import_name: The name of the blueprint package, usually
|
||||||
|
``__name__``. This helps locate the ``root_path`` for the
|
||||||
|
blueprint.
|
||||||
|
:param static_folder: A folder with static files that should be
|
||||||
|
served by the blueprint's static route. The path is relative to
|
||||||
|
the blueprint's root path. Blueprint static files are disabled
|
||||||
|
by default.
|
||||||
|
:param static_url_path: The url to serve static files from.
|
||||||
|
Defaults to ``static_folder``. If the blueprint does not have
|
||||||
|
a ``url_prefix``, the app's static route will take precedence,
|
||||||
|
and the blueprint's static files won't be accessible.
|
||||||
|
:param template_folder: A folder with templates that should be added
|
||||||
|
to the app's template search path. The path is relative to the
|
||||||
|
blueprint's root path. Blueprint templates are disabled by
|
||||||
|
default. Blueprint templates have a lower precedence than those
|
||||||
|
in the app's templates folder.
|
||||||
|
:param url_prefix: A path to prepend to all of the blueprint's URLs,
|
||||||
|
to make them distinct from the rest of the app's routes.
|
||||||
|
:param subdomain: A subdomain that blueprint routes will match on by
|
||||||
|
default.
|
||||||
|
:param url_defaults: A dict of default values that blueprint routes
|
||||||
|
will receive by default.
|
||||||
|
:param root_path: By default, the blueprint will automatically set
|
||||||
|
this based on ``import_name``. In certain situations this
|
||||||
|
automatic detection can fail, so the path can be specified
|
||||||
|
manually instead.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1.0
|
||||||
|
Blueprints have a ``cli`` group to register nested CLI commands.
|
||||||
|
The ``cli_group`` parameter controls the name of the group under
|
||||||
|
the ``flask`` command.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
"""
|
||||||
|
|
||||||
|
_got_registered_once = False
|
||||||
|
|
||||||
|
_json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None
|
||||||
|
_json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None
|
||||||
|
|
||||||
|
@property # type: ignore[override]
|
||||||
|
def json_encoder( # type: ignore[override]
|
||||||
|
self,
|
||||||
|
) -> t.Union[t.Type[json.JSONEncoder], None]:
|
||||||
|
"""Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Customize
|
||||||
|
:attr:`json_provider_class` instead.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json_provider_class' or 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return self._json_encoder
|
||||||
|
|
||||||
|
@json_encoder.setter
|
||||||
|
def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json_provider_class' or 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self._json_encoder = value
|
||||||
|
|
||||||
|
@property # type: ignore[override]
|
||||||
|
def json_decoder( # type: ignore[override]
|
||||||
|
self,
|
||||||
|
) -> t.Union[t.Type[json.JSONDecoder], None]:
|
||||||
|
"""Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Customize
|
||||||
|
:attr:`json_provider_class` instead.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json_provider_class' or 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return self._json_decoder
|
||||||
|
|
||||||
|
@json_decoder.setter
|
||||||
|
def json_decoder(self, value: t.Union[t.Type[json.JSONDecoder], None]) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json_provider_class' or 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self._json_decoder = value
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
import_name: str,
|
||||||
|
static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
|
||||||
|
static_url_path: t.Optional[str] = None,
|
||||||
|
template_folder: t.Optional[str] = None,
|
||||||
|
url_prefix: t.Optional[str] = None,
|
||||||
|
subdomain: t.Optional[str] = None,
|
||||||
|
url_defaults: t.Optional[dict] = None,
|
||||||
|
root_path: t.Optional[str] = None,
|
||||||
|
cli_group: t.Optional[str] = _sentinel, # type: ignore
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
import_name=import_name,
|
||||||
|
static_folder=static_folder,
|
||||||
|
static_url_path=static_url_path,
|
||||||
|
template_folder=template_folder,
|
||||||
|
root_path=root_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if "." in name:
|
||||||
|
raise ValueError("'name' may not contain a dot '.' character.")
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.url_prefix = url_prefix
|
||||||
|
self.subdomain = subdomain
|
||||||
|
self.deferred_functions: t.List[DeferredSetupFunction] = []
|
||||||
|
|
||||||
|
if url_defaults is None:
|
||||||
|
url_defaults = {}
|
||||||
|
|
||||||
|
self.url_values_defaults = url_defaults
|
||||||
|
self.cli_group = cli_group
|
||||||
|
self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
|
||||||
|
|
||||||
|
def _check_setup_finished(self, f_name: str) -> None:
|
||||||
|
if self._got_registered_once:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
f"The setup method '{f_name}' can no longer be called on"
|
||||||
|
f" the blueprint '{self.name}'. It has already been"
|
||||||
|
" registered at least once, any changes will not be"
|
||||||
|
" applied consistently.\n"
|
||||||
|
"Make sure all imports, decorators, functions, etc."
|
||||||
|
" needed to set up the blueprint are done before"
|
||||||
|
" registering it.\n"
|
||||||
|
"This warning will become an exception in Flask 2.3.",
|
||||||
|
UserWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def record(self, func: t.Callable) -> None:
|
||||||
|
"""Registers a function that is called when the blueprint is
|
||||||
|
registered on the application. This function is called with the
|
||||||
|
state as argument as returned by the :meth:`make_setup_state`
|
||||||
|
method.
|
||||||
|
"""
|
||||||
|
self.deferred_functions.append(func)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def record_once(self, func: t.Callable) -> None:
|
||||||
|
"""Works like :meth:`record` but wraps the function in another
|
||||||
|
function that will ensure the function is only called once. If the
|
||||||
|
blueprint is registered a second time on the application, the
|
||||||
|
function passed is not called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(state: BlueprintSetupState) -> None:
|
||||||
|
if state.first_registration:
|
||||||
|
func(state)
|
||||||
|
|
||||||
|
self.record(update_wrapper(wrapper, func))
|
||||||
|
|
||||||
|
def make_setup_state(
|
||||||
|
self, app: "Flask", options: dict, first_registration: bool = False
|
||||||
|
) -> BlueprintSetupState:
|
||||||
|
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
|
||||||
|
object that is later passed to the register callback functions.
|
||||||
|
Subclasses can override this to return a subclass of the setup state.
|
||||||
|
"""
|
||||||
|
return BlueprintSetupState(self, app, options, first_registration)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
|
||||||
|
"""Register a :class:`~flask.Blueprint` on this blueprint. Keyword
|
||||||
|
arguments passed to this method will override the defaults set
|
||||||
|
on the blueprint.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.1
|
||||||
|
The ``name`` option can be used to change the (pre-dotted)
|
||||||
|
name the blueprint is registered with. This allows the same
|
||||||
|
blueprint to be registered multiple times with unique names
|
||||||
|
for ``url_for``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if blueprint is self:
|
||||||
|
raise ValueError("Cannot register a blueprint on itself")
|
||||||
|
self._blueprints.append((blueprint, options))
|
||||||
|
|
||||||
|
def register(self, app: "Flask", options: dict) -> None:
|
||||||
|
"""Called by :meth:`Flask.register_blueprint` to register all
|
||||||
|
views and callbacks registered on the blueprint with the
|
||||||
|
application. Creates a :class:`.BlueprintSetupState` and calls
|
||||||
|
each :meth:`record` callback with it.
|
||||||
|
|
||||||
|
:param app: The application this blueprint is being registered
|
||||||
|
with.
|
||||||
|
:param options: Keyword arguments forwarded from
|
||||||
|
:meth:`~Flask.register_blueprint`.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.1
|
||||||
|
Nested blueprints are registered with their dotted name.
|
||||||
|
This allows different blueprints with the same name to be
|
||||||
|
nested at different locations.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.1
|
||||||
|
The ``name`` option can be used to change the (pre-dotted)
|
||||||
|
name the blueprint is registered with. This allows the same
|
||||||
|
blueprint to be registered multiple times with unique names
|
||||||
|
for ``url_for``.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.1
|
||||||
|
Registering the same blueprint with the same name multiple
|
||||||
|
times is deprecated and will become an error in Flask 2.1.
|
||||||
|
"""
|
||||||
|
name_prefix = options.get("name_prefix", "")
|
||||||
|
self_name = options.get("name", self.name)
|
||||||
|
name = f"{name_prefix}.{self_name}".lstrip(".")
|
||||||
|
|
||||||
|
if name in app.blueprints:
|
||||||
|
bp_desc = "this" if app.blueprints[name] is self else "a different"
|
||||||
|
existing_at = f" '{name}'" if self_name != name else ""
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"The name '{self_name}' is already registered for"
|
||||||
|
f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
|
||||||
|
f" provide a unique name."
|
||||||
|
)
|
||||||
|
|
||||||
|
first_bp_registration = not any(bp is self for bp in app.blueprints.values())
|
||||||
|
first_name_registration = name not in app.blueprints
|
||||||
|
|
||||||
|
app.blueprints[name] = self
|
||||||
|
self._got_registered_once = True
|
||||||
|
state = self.make_setup_state(app, options, first_bp_registration)
|
||||||
|
|
||||||
|
if self.has_static_folder:
|
||||||
|
state.add_url_rule(
|
||||||
|
f"{self.static_url_path}/<path:filename>",
|
||||||
|
view_func=self.send_static_file,
|
||||||
|
endpoint="static",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge blueprint data into parent.
|
||||||
|
if first_bp_registration or first_name_registration:
|
||||||
|
|
||||||
|
def extend(bp_dict, parent_dict):
|
||||||
|
for key, values in bp_dict.items():
|
||||||
|
key = name if key is None else f"{name}.{key}"
|
||||||
|
parent_dict[key].extend(values)
|
||||||
|
|
||||||
|
for key, value in self.error_handler_spec.items():
|
||||||
|
key = name if key is None else f"{name}.{key}"
|
||||||
|
value = defaultdict(
|
||||||
|
dict,
|
||||||
|
{
|
||||||
|
code: {
|
||||||
|
exc_class: func for exc_class, func in code_values.items()
|
||||||
|
}
|
||||||
|
for code, code_values in value.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
app.error_handler_spec[key] = value
|
||||||
|
|
||||||
|
for endpoint, func in self.view_functions.items():
|
||||||
|
app.view_functions[endpoint] = func
|
||||||
|
|
||||||
|
extend(self.before_request_funcs, app.before_request_funcs)
|
||||||
|
extend(self.after_request_funcs, app.after_request_funcs)
|
||||||
|
extend(
|
||||||
|
self.teardown_request_funcs,
|
||||||
|
app.teardown_request_funcs,
|
||||||
|
)
|
||||||
|
extend(self.url_default_functions, app.url_default_functions)
|
||||||
|
extend(self.url_value_preprocessors, app.url_value_preprocessors)
|
||||||
|
extend(self.template_context_processors, app.template_context_processors)
|
||||||
|
|
||||||
|
for deferred in self.deferred_functions:
|
||||||
|
deferred(state)
|
||||||
|
|
||||||
|
cli_resolved_group = options.get("cli_group", self.cli_group)
|
||||||
|
|
||||||
|
if self.cli.commands:
|
||||||
|
if cli_resolved_group is None:
|
||||||
|
app.cli.commands.update(self.cli.commands)
|
||||||
|
elif cli_resolved_group is _sentinel:
|
||||||
|
self.cli.name = name
|
||||||
|
app.cli.add_command(self.cli)
|
||||||
|
else:
|
||||||
|
self.cli.name = cli_resolved_group
|
||||||
|
app.cli.add_command(self.cli)
|
||||||
|
|
||||||
|
for blueprint, bp_options in self._blueprints:
|
||||||
|
bp_options = bp_options.copy()
|
||||||
|
bp_url_prefix = bp_options.get("url_prefix")
|
||||||
|
|
||||||
|
if bp_url_prefix is None:
|
||||||
|
bp_url_prefix = blueprint.url_prefix
|
||||||
|
|
||||||
|
if state.url_prefix is not None and bp_url_prefix is not None:
|
||||||
|
bp_options["url_prefix"] = (
|
||||||
|
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
|
||||||
|
)
|
||||||
|
elif bp_url_prefix is not None:
|
||||||
|
bp_options["url_prefix"] = bp_url_prefix
|
||||||
|
elif state.url_prefix is not None:
|
||||||
|
bp_options["url_prefix"] = state.url_prefix
|
||||||
|
|
||||||
|
bp_options["name_prefix"] = name
|
||||||
|
blueprint.register(app, bp_options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def add_url_rule(
|
||||||
|
self,
|
||||||
|
rule: str,
|
||||||
|
endpoint: t.Optional[str] = None,
|
||||||
|
view_func: t.Optional[ft.RouteCallable] = None,
|
||||||
|
provide_automatic_options: t.Optional[bool] = None,
|
||||||
|
**options: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
|
||||||
|
the :func:`url_for` function is prefixed with the name of the blueprint.
|
||||||
|
"""
|
||||||
|
if endpoint and "." in endpoint:
|
||||||
|
raise ValueError("'endpoint' may not contain a dot '.' character.")
|
||||||
|
|
||||||
|
if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
|
||||||
|
raise ValueError("'view_func' name may not contain a dot '.' character.")
|
||||||
|
|
||||||
|
self.record(
|
||||||
|
lambda s: s.add_url_rule(
|
||||||
|
rule,
|
||||||
|
endpoint,
|
||||||
|
view_func,
|
||||||
|
provide_automatic_options=provide_automatic_options,
|
||||||
|
**options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_template_filter(
|
||||||
|
self, name: t.Optional[str] = None
|
||||||
|
) -> t.Callable[[T_template_filter], T_template_filter]:
|
||||||
|
"""Register a custom template filter, available application wide. Like
|
||||||
|
:meth:`Flask.template_filter` but for a blueprint.
|
||||||
|
|
||||||
|
:param name: the optional name of the filter, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_template_filter) -> T_template_filter:
|
||||||
|
self.add_app_template_filter(f, name=name)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def add_app_template_filter(
|
||||||
|
self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Register a custom template filter, available application wide. Like
|
||||||
|
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly
|
||||||
|
like the :meth:`app_template_filter` decorator.
|
||||||
|
|
||||||
|
:param name: the optional name of the filter, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def register_template(state: BlueprintSetupState) -> None:
|
||||||
|
state.app.jinja_env.filters[name or f.__name__] = f
|
||||||
|
|
||||||
|
self.record_once(register_template)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_template_test(
|
||||||
|
self, name: t.Optional[str] = None
|
||||||
|
) -> t.Callable[[T_template_test], T_template_test]:
|
||||||
|
"""Register a custom template test, available application wide. Like
|
||||||
|
:meth:`Flask.template_test` but for a blueprint.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
:param name: the optional name of the test, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_template_test) -> T_template_test:
|
||||||
|
self.add_app_template_test(f, name=name)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def add_app_template_test(
|
||||||
|
self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Register a custom template test, available application wide. Like
|
||||||
|
:meth:`Flask.add_template_test` but for a blueprint. Works exactly
|
||||||
|
like the :meth:`app_template_test` decorator.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
:param name: the optional name of the test, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def register_template(state: BlueprintSetupState) -> None:
|
||||||
|
state.app.jinja_env.tests[name or f.__name__] = f
|
||||||
|
|
||||||
|
self.record_once(register_template)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_template_global(
|
||||||
|
self, name: t.Optional[str] = None
|
||||||
|
) -> t.Callable[[T_template_global], T_template_global]:
|
||||||
|
"""Register a custom template global, available application wide. Like
|
||||||
|
:meth:`Flask.template_global` but for a blueprint.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
:param name: the optional name of the global, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_template_global) -> T_template_global:
|
||||||
|
self.add_app_template_global(f, name=name)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def add_app_template_global(
|
||||||
|
self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Register a custom template global, available application wide. Like
|
||||||
|
:meth:`Flask.add_template_global` but for a blueprint. Works exactly
|
||||||
|
like the :meth:`app_template_global` decorator.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
:param name: the optional name of the global, otherwise the
|
||||||
|
function name will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def register_template(state: BlueprintSetupState) -> None:
|
||||||
|
state.app.jinja_env.globals[name or f.__name__] = f
|
||||||
|
|
||||||
|
self.record_once(register_template)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def before_app_request(self, f: T_before_request) -> T_before_request:
|
||||||
|
"""Like :meth:`Flask.before_request`. Such a function is executed
|
||||||
|
before each request, even if outside of a blueprint.
|
||||||
|
"""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def before_app_first_request(
|
||||||
|
self, f: T_before_first_request
|
||||||
|
) -> T_before_first_request:
|
||||||
|
"""Like :meth:`Flask.before_first_request`. Such a function is
|
||||||
|
executed before the first request to the application.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Run setup code when creating
|
||||||
|
the application instead.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'before_app_first_request' is deprecated and will be"
|
||||||
|
" removed in Flask 2.3. Use 'record_once' instead to run"
|
||||||
|
" setup code when registering the blueprint.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def after_app_request(self, f: T_after_request) -> T_after_request:
|
||||||
|
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
||||||
|
is executed after each request, even if outside of the blueprint.
|
||||||
|
"""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def teardown_app_request(self, f: T_teardown) -> T_teardown:
|
||||||
|
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
||||||
|
function is executed when tearing down each request, even if outside of
|
||||||
|
the blueprint.
|
||||||
|
"""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_context_processor(
|
||||||
|
self, f: T_template_context_processor
|
||||||
|
) -> T_template_context_processor:
|
||||||
|
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
||||||
|
function is executed each request, even if outside of the blueprint.
|
||||||
|
"""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_errorhandler(
|
||||||
|
self, code: t.Union[t.Type[Exception], int]
|
||||||
|
) -> t.Callable[[T_error_handler], T_error_handler]:
|
||||||
|
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
||||||
|
handler is used for all requests, even if outside of the blueprint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_error_handler) -> T_error_handler:
|
||||||
|
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_url_value_preprocessor(
|
||||||
|
self, f: T_url_value_preprocessor
|
||||||
|
) -> T_url_value_preprocessor:
|
||||||
|
"""Same as :meth:`url_value_preprocessor` but application wide."""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
|
||||||
|
"""Same as :meth:`url_defaults` but application wide."""
|
||||||
|
self.record_once(
|
||||||
|
lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
|
||||||
|
)
|
||||||
|
return f
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,337 @@
|
|||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from werkzeug.utils import import_string
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigAttribute:
|
||||||
|
"""Makes an attribute forward to the config"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None:
|
||||||
|
self.__name__ = name
|
||||||
|
self.get_converter = get_converter
|
||||||
|
|
||||||
|
def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any:
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
rv = obj.config[self.__name__]
|
||||||
|
if self.get_converter is not None:
|
||||||
|
rv = self.get_converter(rv)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __set__(self, obj: t.Any, value: t.Any) -> None:
|
||||||
|
obj.config[self.__name__] = value
|
||||||
|
|
||||||
|
|
||||||
|
class Config(dict):
|
||||||
|
"""Works exactly like a dict but provides ways to fill it from files
|
||||||
|
or special dictionaries. There are two common patterns to populate the
|
||||||
|
config.
|
||||||
|
|
||||||
|
Either you can fill the config from a config file::
|
||||||
|
|
||||||
|
app.config.from_pyfile('yourconfig.cfg')
|
||||||
|
|
||||||
|
Or alternatively you can define the configuration options in the
|
||||||
|
module that calls :meth:`from_object` or provide an import path to
|
||||||
|
a module that should be loaded. It is also possible to tell it to
|
||||||
|
use the same module and with that provide the configuration values
|
||||||
|
just before the call::
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
SECRET_KEY = 'development key'
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
|
||||||
|
In both cases (loading from any Python file or loading from modules),
|
||||||
|
only uppercase keys are added to the config. This makes it possible to use
|
||||||
|
lowercase values in the config file for temporary values that are not added
|
||||||
|
to the config or to define the config keys in the same file that implements
|
||||||
|
the application.
|
||||||
|
|
||||||
|
Probably the most interesting way to load configurations is from an
|
||||||
|
environment variable pointing to a file::
|
||||||
|
|
||||||
|
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
|
||||||
|
|
||||||
|
In this case before launching the application you have to set this
|
||||||
|
environment variable to the file you want to use. On Linux and OS X
|
||||||
|
use the export statement::
|
||||||
|
|
||||||
|
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
|
||||||
|
|
||||||
|
On windows use `set` instead.
|
||||||
|
|
||||||
|
:param root_path: path to which files are read relative from. When the
|
||||||
|
config object is created by the application, this is
|
||||||
|
the application's :attr:`~flask.Flask.root_path`.
|
||||||
|
:param defaults: an optional dictionary of default values
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None:
|
||||||
|
super().__init__(defaults or {})
|
||||||
|
self.root_path = root_path
|
||||||
|
|
||||||
|
def from_envvar(self, variable_name: str, silent: bool = False) -> bool:
|
||||||
|
"""Loads a configuration from an environment variable pointing to
|
||||||
|
a configuration file. This is basically just a shortcut with nicer
|
||||||
|
error messages for this line of code::
|
||||||
|
|
||||||
|
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
|
||||||
|
|
||||||
|
:param variable_name: name of the environment variable
|
||||||
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
|
files.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
"""
|
||||||
|
rv = os.environ.get(variable_name)
|
||||||
|
if not rv:
|
||||||
|
if silent:
|
||||||
|
return False
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The environment variable {variable_name!r} is not set"
|
||||||
|
" and as such configuration could not be loaded. Set"
|
||||||
|
" this variable and make it point to a configuration"
|
||||||
|
" file"
|
||||||
|
)
|
||||||
|
return self.from_pyfile(rv, silent=silent)
|
||||||
|
|
||||||
|
def from_prefixed_env(
|
||||||
|
self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads
|
||||||
|
) -> bool:
|
||||||
|
"""Load any environment variables that start with ``FLASK_``,
|
||||||
|
dropping the prefix from the env key for the config key. Values
|
||||||
|
are passed through a loading function to attempt to convert them
|
||||||
|
to more specific types than strings.
|
||||||
|
|
||||||
|
Keys are loaded in :func:`sorted` order.
|
||||||
|
|
||||||
|
The default loading function attempts to parse values as any
|
||||||
|
valid JSON type, including dicts and lists.
|
||||||
|
|
||||||
|
Specific items in nested dicts can be set by separating the
|
||||||
|
keys with double underscores (``__``). If an intermediate key
|
||||||
|
doesn't exist, it will be initialized to an empty dict.
|
||||||
|
|
||||||
|
:param prefix: Load env vars that start with this prefix,
|
||||||
|
separated with an underscore (``_``).
|
||||||
|
:param loads: Pass each string value to this function and use
|
||||||
|
the returned value as the config value. If any error is
|
||||||
|
raised it is ignored and the value remains a string. The
|
||||||
|
default is :func:`json.loads`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
|
prefix = f"{prefix}_"
|
||||||
|
len_prefix = len(prefix)
|
||||||
|
|
||||||
|
for key in sorted(os.environ):
|
||||||
|
if not key.startswith(prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = os.environ[key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = loads(value)
|
||||||
|
except Exception:
|
||||||
|
# Keep the value as a string if loading failed.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Change to key.removeprefix(prefix) on Python >= 3.9.
|
||||||
|
key = key[len_prefix:]
|
||||||
|
|
||||||
|
if "__" not in key:
|
||||||
|
# A non-nested key, set directly.
|
||||||
|
self[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Traverse nested dictionaries with keys separated by "__".
|
||||||
|
current = self
|
||||||
|
*parts, tail = key.split("__")
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# If an intermediate dict does not exist, create it.
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
|
||||||
|
current = current[part]
|
||||||
|
|
||||||
|
current[tail] = value
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def from_pyfile(self, filename: str, silent: bool = False) -> bool:
|
||||||
|
"""Updates the values in the config from a Python file. This function
|
||||||
|
behaves as if the file was imported as module with the
|
||||||
|
:meth:`from_object` function.
|
||||||
|
|
||||||
|
:param filename: the filename of the config. This can either be an
|
||||||
|
absolute filename or a filename relative to the
|
||||||
|
root path.
|
||||||
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
|
files.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
`silent` parameter.
|
||||||
|
"""
|
||||||
|
filename = os.path.join(self.root_path, filename)
|
||||||
|
d = types.ModuleType("config")
|
||||||
|
d.__file__ = filename
|
||||||
|
try:
|
||||||
|
with open(filename, mode="rb") as config_file:
|
||||||
|
exec(compile(config_file.read(), filename, "exec"), d.__dict__)
|
||||||
|
except OSError as e:
|
||||||
|
if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
|
||||||
|
return False
|
||||||
|
e.strerror = f"Unable to load configuration file ({e.strerror})"
|
||||||
|
raise
|
||||||
|
self.from_object(d)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def from_object(self, obj: t.Union[object, str]) -> None:
|
||||||
|
"""Updates the values from the given object. An object can be of one
|
||||||
|
of the following two types:
|
||||||
|
|
||||||
|
- a string: in this case the object with that name will be imported
|
||||||
|
- an actual object reference: that object is used directly
|
||||||
|
|
||||||
|
Objects are usually either modules or classes. :meth:`from_object`
|
||||||
|
loads only the uppercase attributes of the module/class. A ``dict``
|
||||||
|
object will not work with :meth:`from_object` because the keys of a
|
||||||
|
``dict`` are not attributes of the ``dict`` class.
|
||||||
|
|
||||||
|
Example of module-based configuration::
|
||||||
|
|
||||||
|
app.config.from_object('yourapplication.default_config')
|
||||||
|
from yourapplication import default_config
|
||||||
|
app.config.from_object(default_config)
|
||||||
|
|
||||||
|
Nothing is done to the object before loading. If the object is a
|
||||||
|
class and has ``@property`` attributes, it needs to be
|
||||||
|
instantiated before being passed to this method.
|
||||||
|
|
||||||
|
You should not use this function to load the actual configuration but
|
||||||
|
rather configuration defaults. The actual config should be loaded
|
||||||
|
with :meth:`from_pyfile` and ideally from a location not within the
|
||||||
|
package because the package might be installed system wide.
|
||||||
|
|
||||||
|
See :ref:`config-dev-prod` for an example of class-based configuration
|
||||||
|
using :meth:`from_object`.
|
||||||
|
|
||||||
|
:param obj: an import name or object
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
obj = import_string(obj)
|
||||||
|
for key in dir(obj):
|
||||||
|
if key.isupper():
|
||||||
|
self[key] = getattr(obj, key)
|
||||||
|
|
||||||
|
def from_file(
|
||||||
|
self,
|
||||||
|
filename: str,
|
||||||
|
load: t.Callable[[t.IO[t.Any]], t.Mapping],
|
||||||
|
silent: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
"""Update the values in the config from a file that is loaded
|
||||||
|
using the ``load`` parameter. The loaded data is passed to the
|
||||||
|
:meth:`from_mapping` method.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import json
|
||||||
|
app.config.from_file("config.json", load=json.load)
|
||||||
|
|
||||||
|
import toml
|
||||||
|
app.config.from_file("config.toml", load=toml.load)
|
||||||
|
|
||||||
|
:param filename: The path to the data file. This can be an
|
||||||
|
absolute path or relative to the config root path.
|
||||||
|
:param load: A callable that takes a file handle and returns a
|
||||||
|
mapping of loaded data from the file.
|
||||||
|
:type load: ``Callable[[Reader], Mapping]`` where ``Reader``
|
||||||
|
implements a ``read`` method.
|
||||||
|
:param silent: Ignore the file if it doesn't exist.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
filename = os.path.join(self.root_path, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename) as f:
|
||||||
|
obj = load(f)
|
||||||
|
except OSError as e:
|
||||||
|
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||||
|
return False
|
||||||
|
|
||||||
|
e.strerror = f"Unable to load configuration file ({e.strerror})"
|
||||||
|
raise
|
||||||
|
|
||||||
|
return self.from_mapping(obj)
|
||||||
|
|
||||||
|
def from_mapping(
|
||||||
|
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
|
||||||
|
) -> bool:
|
||||||
|
"""Updates the config like :meth:`update` ignoring items with non-upper
|
||||||
|
keys.
|
||||||
|
:return: Always returns ``True``.
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
mappings: t.Dict[str, t.Any] = {}
|
||||||
|
if mapping is not None:
|
||||||
|
mappings.update(mapping)
|
||||||
|
mappings.update(kwargs)
|
||||||
|
for key, value in mappings.items():
|
||||||
|
if key.isupper():
|
||||||
|
self[key] = value
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_namespace(
|
||||||
|
self, namespace: str, lowercase: bool = True, trim_namespace: bool = True
|
||||||
|
) -> t.Dict[str, t.Any]:
|
||||||
|
"""Returns a dictionary containing a subset of configuration options
|
||||||
|
that match the specified namespace/prefix. Example usage::
|
||||||
|
|
||||||
|
app.config['IMAGE_STORE_TYPE'] = 'fs'
|
||||||
|
app.config['IMAGE_STORE_PATH'] = '/var/app/images'
|
||||||
|
app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
|
||||||
|
image_store_config = app.config.get_namespace('IMAGE_STORE_')
|
||||||
|
|
||||||
|
The resulting dictionary `image_store_config` would look like::
|
||||||
|
|
||||||
|
{
|
||||||
|
'type': 'fs',
|
||||||
|
'path': '/var/app/images',
|
||||||
|
'base_url': 'http://img.website.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
This is often useful when configuration options map directly to
|
||||||
|
keyword arguments in functions or class constructors.
|
||||||
|
|
||||||
|
:param namespace: a configuration namespace
|
||||||
|
:param lowercase: a flag indicating if the keys of the resulting
|
||||||
|
dictionary should be lowercase
|
||||||
|
:param trim_namespace: a flag indicating if the keys of the resulting
|
||||||
|
dictionary should not include the namespace
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
rv = {}
|
||||||
|
for k, v in self.items():
|
||||||
|
if not k.startswith(namespace):
|
||||||
|
continue
|
||||||
|
if trim_namespace:
|
||||||
|
key = k[len(namespace) :]
|
||||||
|
else:
|
||||||
|
key = k
|
||||||
|
if lowercase:
|
||||||
|
key = key.lower()
|
||||||
|
rv[key] = v
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{type(self).__name__} {dict.__repr__(self)}>"
|
@ -0,0 +1,438 @@
|
|||||||
|
import contextvars
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from functools import update_wrapper
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
|
from .globals import _cv_app
|
||||||
|
from .globals import _cv_request
|
||||||
|
from .signals import appcontext_popped
|
||||||
|
from .signals import appcontext_pushed
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .app import Flask
|
||||||
|
from .sessions import SessionMixin
|
||||||
|
from .wrappers import Request
|
||||||
|
|
||||||
|
|
||||||
|
# a singleton sentinel value for parameter defaults
|
||||||
|
_sentinel = object()
|
||||||
|
|
||||||
|
|
||||||
|
class _AppCtxGlobals:
|
||||||
|
"""A plain object. Used as a namespace for storing data during an
|
||||||
|
application context.
|
||||||
|
|
||||||
|
Creating an app context automatically creates this object, which is
|
||||||
|
made available as the :data:`g` proxy.
|
||||||
|
|
||||||
|
.. describe:: 'key' in g
|
||||||
|
|
||||||
|
Check whether an attribute is present.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
.. describe:: iter(g)
|
||||||
|
|
||||||
|
Return an iterator over the attribute names.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define attr methods to let mypy know this is a namespace object
|
||||||
|
# that has arbitrary attributes.
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
try:
|
||||||
|
return self.__dict__[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name) from None
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: t.Any) -> None:
|
||||||
|
self.__dict__[name] = value
|
||||||
|
|
||||||
|
def __delattr__(self, name: str) -> None:
|
||||||
|
try:
|
||||||
|
del self.__dict__[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name) from None
|
||||||
|
|
||||||
|
def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any:
|
||||||
|
"""Get an attribute by name, or a default value. Like
|
||||||
|
:meth:`dict.get`.
|
||||||
|
|
||||||
|
:param name: Name of attribute to get.
|
||||||
|
:param default: Value to return if the attribute is not present.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
"""
|
||||||
|
return self.__dict__.get(name, default)
|
||||||
|
|
||||||
|
def pop(self, name: str, default: t.Any = _sentinel) -> t.Any:
|
||||||
|
"""Get and remove an attribute by name. Like :meth:`dict.pop`.
|
||||||
|
|
||||||
|
:param name: Name of attribute to pop.
|
||||||
|
:param default: Value to return if the attribute is not present,
|
||||||
|
instead of raising a ``KeyError``.
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
if default is _sentinel:
|
||||||
|
return self.__dict__.pop(name)
|
||||||
|
else:
|
||||||
|
return self.__dict__.pop(name, default)
|
||||||
|
|
||||||
|
def setdefault(self, name: str, default: t.Any = None) -> t.Any:
|
||||||
|
"""Get the value of an attribute if it is present, otherwise
|
||||||
|
set and return a default value. Like :meth:`dict.setdefault`.
|
||||||
|
|
||||||
|
:param name: Name of attribute to get.
|
||||||
|
:param default: Value to set and return if the attribute is not
|
||||||
|
present.
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
return self.__dict__.setdefault(name, default)
|
||||||
|
|
||||||
|
def __contains__(self, item: str) -> bool:
|
||||||
|
return item in self.__dict__
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[str]:
|
||||||
|
return iter(self.__dict__)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
ctx = _cv_app.get(None)
|
||||||
|
if ctx is not None:
|
||||||
|
return f"<flask.g of '{ctx.app.name}'>"
|
||||||
|
return object.__repr__(self)
|
||||||
|
|
||||||
|
|
||||||
|
def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
|
||||||
|
"""Executes a function after this request. This is useful to modify
|
||||||
|
response objects. The function is passed the response object and has
|
||||||
|
to return the same or a new one.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
@after_this_request
|
||||||
|
def add_header(response):
|
||||||
|
response.headers['X-Foo'] = 'Parachute'
|
||||||
|
return response
|
||||||
|
return 'Hello World!'
|
||||||
|
|
||||||
|
This is more useful if a function other than the view function wants to
|
||||||
|
modify a response. For instance think of a decorator that wants to add
|
||||||
|
some headers without converting the return value into a response object.
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
"""
|
||||||
|
ctx = _cv_request.get(None)
|
||||||
|
|
||||||
|
if ctx is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"'after_this_request' can only be used when a request"
|
||||||
|
" context is active, such as in a view function."
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx._after_request_functions.append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def copy_current_request_context(f: t.Callable) -> t.Callable:
|
||||||
|
"""A helper function that decorates a function to retain the current
|
||||||
|
request context. This is useful when working with greenlets. The moment
|
||||||
|
the function is decorated a copy of the request context is created and
|
||||||
|
then pushed when the function is called. The current session is also
|
||||||
|
included in the copied request context.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
from flask import copy_current_request_context
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
@copy_current_request_context
|
||||||
|
def do_some_work():
|
||||||
|
# do some work here, it can access flask.request or
|
||||||
|
# flask.session like you would otherwise in the view function.
|
||||||
|
...
|
||||||
|
gevent.spawn(do_some_work)
|
||||||
|
return 'Regular response'
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
"""
|
||||||
|
ctx = _cv_request.get(None)
|
||||||
|
|
||||||
|
if ctx is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"'copy_current_request_context' can only be used when a"
|
||||||
|
" request context is active, such as in a view function."
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = ctx.copy()
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
with ctx:
|
||||||
|
return ctx.app.ensure_sync(f)(*args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(wrapper, f)
|
||||||
|
|
||||||
|
|
||||||
|
def has_request_context() -> bool:
|
||||||
|
"""If you have code that wants to test if a request context is there or
|
||||||
|
not this function can be used. For instance, you may want to take advantage
|
||||||
|
of request information if the request object is available, but fail
|
||||||
|
silently if it is unavailable.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
|
||||||
|
def __init__(self, username, remote_addr=None):
|
||||||
|
self.username = username
|
||||||
|
if remote_addr is None and has_request_context():
|
||||||
|
remote_addr = request.remote_addr
|
||||||
|
self.remote_addr = remote_addr
|
||||||
|
|
||||||
|
Alternatively you can also just test any of the context bound objects
|
||||||
|
(such as :class:`request` or :class:`g`) for truthness::
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
|
||||||
|
def __init__(self, username, remote_addr=None):
|
||||||
|
self.username = username
|
||||||
|
if remote_addr is None and request:
|
||||||
|
remote_addr = request.remote_addr
|
||||||
|
self.remote_addr = remote_addr
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
"""
|
||||||
|
return _cv_request.get(None) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def has_app_context() -> bool:
|
||||||
|
"""Works like :func:`has_request_context` but for the application
|
||||||
|
context. You can also just do a boolean check on the
|
||||||
|
:data:`current_app` object instead.
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
"""
|
||||||
|
return _cv_app.get(None) is not None
|
||||||
|
|
||||||
|
|
||||||
|
class AppContext:
|
||||||
|
"""The app context contains application-specific information. An app
|
||||||
|
context is created and pushed at the beginning of each request if
|
||||||
|
one is not already active. An app context is also pushed when
|
||||||
|
running CLI commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: "Flask") -> None:
|
||||||
|
self.app = app
|
||||||
|
self.url_adapter = app.create_url_adapter(None)
|
||||||
|
self.g: _AppCtxGlobals = app.app_ctx_globals_class()
|
||||||
|
self._cv_tokens: t.List[contextvars.Token] = []
|
||||||
|
|
||||||
|
def push(self) -> None:
|
||||||
|
"""Binds the app context to the current context."""
|
||||||
|
self._cv_tokens.append(_cv_app.set(self))
|
||||||
|
appcontext_pushed.send(self.app)
|
||||||
|
|
||||||
|
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
|
||||||
|
"""Pops the app context."""
|
||||||
|
try:
|
||||||
|
if len(self._cv_tokens) == 1:
|
||||||
|
if exc is _sentinel:
|
||||||
|
exc = sys.exc_info()[1]
|
||||||
|
self.app.do_teardown_appcontext(exc)
|
||||||
|
finally:
|
||||||
|
ctx = _cv_app.get()
|
||||||
|
_cv_app.reset(self._cv_tokens.pop())
|
||||||
|
|
||||||
|
if ctx is not self:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Popped wrong app context. ({ctx!r} instead of {self!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
appcontext_popped.send(self.app)
|
||||||
|
|
||||||
|
def __enter__(self) -> "AppContext":
|
||||||
|
self.push()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: t.Optional[type],
|
||||||
|
exc_value: t.Optional[BaseException],
|
||||||
|
tb: t.Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
self.pop(exc_value)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext:
|
||||||
|
"""The request context contains per-request information. The Flask
|
||||||
|
app creates and pushes it at the beginning of the request, then pops
|
||||||
|
it at the end of the request. It will create the URL adapter and
|
||||||
|
request object for the WSGI environment provided.
|
||||||
|
|
||||||
|
Do not attempt to use this class directly, instead use
|
||||||
|
:meth:`~flask.Flask.test_request_context` and
|
||||||
|
:meth:`~flask.Flask.request_context` to create this object.
|
||||||
|
|
||||||
|
When the request context is popped, it will evaluate all the
|
||||||
|
functions registered on the application for teardown execution
|
||||||
|
(:meth:`~flask.Flask.teardown_request`).
|
||||||
|
|
||||||
|
The request context is automatically popped at the end of the
|
||||||
|
request. When using the interactive debugger, the context will be
|
||||||
|
restored so ``request`` is still accessible. Similarly, the test
|
||||||
|
client can preserve the context after the request ends. However,
|
||||||
|
teardown functions may already have closed some resources such as
|
||||||
|
database connections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: "Flask",
|
||||||
|
environ: dict,
|
||||||
|
request: t.Optional["Request"] = None,
|
||||||
|
session: t.Optional["SessionMixin"] = None,
|
||||||
|
) -> None:
|
||||||
|
self.app = app
|
||||||
|
if request is None:
|
||||||
|
request = app.request_class(environ)
|
||||||
|
request.json_module = app.json # type: ignore[misc]
|
||||||
|
self.request: Request = request
|
||||||
|
self.url_adapter = None
|
||||||
|
try:
|
||||||
|
self.url_adapter = app.create_url_adapter(self.request)
|
||||||
|
except HTTPException as e:
|
||||||
|
self.request.routing_exception = e
|
||||||
|
self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None
|
||||||
|
self.session: t.Optional["SessionMixin"] = session
|
||||||
|
# Functions that should be executed after the request on the response
|
||||||
|
# object. These will be called before the regular "after_request"
|
||||||
|
# functions.
|
||||||
|
self._after_request_functions: t.List[ft.AfterRequestCallable] = []
|
||||||
|
|
||||||
|
self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = []
|
||||||
|
|
||||||
|
def copy(self) -> "RequestContext":
|
||||||
|
"""Creates a copy of this request context with the same request object.
|
||||||
|
This can be used to move a request context to a different greenlet.
|
||||||
|
Because the actual request object is the same this cannot be used to
|
||||||
|
move a request context to a different thread unless access to the
|
||||||
|
request object is locked.
|
||||||
|
|
||||||
|
.. versionadded:: 0.10
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1
|
||||||
|
The current session object is used instead of reloading the original
|
||||||
|
data. This prevents `flask.session` pointing to an out-of-date object.
|
||||||
|
"""
|
||||||
|
return self.__class__(
|
||||||
|
self.app,
|
||||||
|
environ=self.request.environ,
|
||||||
|
request=self.request,
|
||||||
|
session=self.session,
|
||||||
|
)
|
||||||
|
|
||||||
|
def match_request(self) -> None:
|
||||||
|
"""Can be overridden by a subclass to hook into the matching
|
||||||
|
of the request.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||||
|
self.request.url_rule, self.request.view_args = result # type: ignore
|
||||||
|
except HTTPException as e:
|
||||||
|
self.request.routing_exception = e
|
||||||
|
|
||||||
|
def push(self) -> None:
|
||||||
|
# Before we push the request context we have to ensure that there
|
||||||
|
# is an application context.
|
||||||
|
app_ctx = _cv_app.get(None)
|
||||||
|
|
||||||
|
if app_ctx is None or app_ctx.app is not self.app:
|
||||||
|
app_ctx = self.app.app_context()
|
||||||
|
app_ctx.push()
|
||||||
|
else:
|
||||||
|
app_ctx = None
|
||||||
|
|
||||||
|
self._cv_tokens.append((_cv_request.set(self), app_ctx))
|
||||||
|
|
||||||
|
# Open the session at the moment that the request context is available.
|
||||||
|
# This allows a custom open_session method to use the request context.
|
||||||
|
# Only open a new session if this is the first time the request was
|
||||||
|
# pushed, otherwise stream_with_context loses the session.
|
||||||
|
if self.session is None:
|
||||||
|
session_interface = self.app.session_interface
|
||||||
|
self.session = session_interface.open_session(self.app, self.request)
|
||||||
|
|
||||||
|
if self.session is None:
|
||||||
|
self.session = session_interface.make_null_session(self.app)
|
||||||
|
|
||||||
|
# Match the request URL after loading the session, so that the
|
||||||
|
# session is available in custom URL converters.
|
||||||
|
if self.url_adapter is not None:
|
||||||
|
self.match_request()
|
||||||
|
|
||||||
|
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
|
||||||
|
"""Pops the request context and unbinds it by doing that. This will
|
||||||
|
also trigger the execution of functions registered by the
|
||||||
|
:meth:`~flask.Flask.teardown_request` decorator.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9
|
||||||
|
Added the `exc` argument.
|
||||||
|
"""
|
||||||
|
clear_request = len(self._cv_tokens) == 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
if clear_request:
|
||||||
|
if exc is _sentinel:
|
||||||
|
exc = sys.exc_info()[1]
|
||||||
|
self.app.do_teardown_request(exc)
|
||||||
|
|
||||||
|
request_close = getattr(self.request, "close", None)
|
||||||
|
if request_close is not None:
|
||||||
|
request_close()
|
||||||
|
finally:
|
||||||
|
ctx = _cv_request.get()
|
||||||
|
token, app_ctx = self._cv_tokens.pop()
|
||||||
|
_cv_request.reset(token)
|
||||||
|
|
||||||
|
# get rid of circular dependencies at the end of the request
|
||||||
|
# so that we don't require the GC to be active.
|
||||||
|
if clear_request:
|
||||||
|
ctx.request.environ["werkzeug.request"] = None
|
||||||
|
|
||||||
|
if app_ctx is not None:
|
||||||
|
app_ctx.pop(exc)
|
||||||
|
|
||||||
|
if ctx is not self:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Popped wrong request context. ({ctx!r} instead of {self!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self) -> "RequestContext":
|
||||||
|
self.push()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: t.Optional[type],
|
||||||
|
exc_value: t.Optional[BaseException],
|
||||||
|
tb: t.Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
self.pop(exc_value)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<{type(self).__name__} {self.request.url!r}"
|
||||||
|
f" [{self.request.method}] of {self.app.name}>"
|
||||||
|
)
|
@ -0,0 +1,158 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .app import Flask
|
||||||
|
from .blueprints import Blueprint
|
||||||
|
from .globals import request_ctx
|
||||||
|
|
||||||
|
|
||||||
|
class UnexpectedUnicodeError(AssertionError, UnicodeError):
|
||||||
|
"""Raised in places where we want some better error reporting for
|
||||||
|
unexpected unicode or binary data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DebugFilesKeyError(KeyError, AssertionError):
|
||||||
|
"""Raised from request.files during debugging. The idea is that it can
|
||||||
|
provide a better error message than just a generic KeyError/BadRequest.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, key):
|
||||||
|
form_matches = request.form.getlist(key)
|
||||||
|
buf = [
|
||||||
|
f"You tried to access the file {key!r} in the request.files"
|
||||||
|
" dictionary but it does not exist. The mimetype for the"
|
||||||
|
f" request is {request.mimetype!r} instead of"
|
||||||
|
" 'multipart/form-data' which means that no file contents"
|
||||||
|
" were transmitted. To fix this error you should provide"
|
||||||
|
' enctype="multipart/form-data" in your form.'
|
||||||
|
]
|
||||||
|
if form_matches:
|
||||||
|
names = ", ".join(repr(x) for x in form_matches)
|
||||||
|
buf.append(
|
||||||
|
"\n\nThe browser instead transmitted some file names. "
|
||||||
|
f"This was submitted: {names}"
|
||||||
|
)
|
||||||
|
self.msg = "".join(buf)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
|
class FormDataRoutingRedirect(AssertionError):
|
||||||
|
"""This exception is raised in debug mode if a routing redirect
|
||||||
|
would cause the browser to drop the method or body. This happens
|
||||||
|
when method is not GET, HEAD or OPTIONS and the status code is not
|
||||||
|
307 or 308.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
exc = request.routing_exception
|
||||||
|
buf = [
|
||||||
|
f"A request was sent to '{request.url}', but routing issued"
|
||||||
|
f" a redirect to the canonical URL '{exc.new_url}'."
|
||||||
|
]
|
||||||
|
|
||||||
|
if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
|
||||||
|
buf.append(
|
||||||
|
" The URL was defined with a trailing slash. Flask"
|
||||||
|
" will redirect to the URL with a trailing slash if it"
|
||||||
|
" was accessed without one."
|
||||||
|
)
|
||||||
|
|
||||||
|
buf.append(
|
||||||
|
" Send requests to the canonical URL, or use 307 or 308 for"
|
||||||
|
" routing redirects. Otherwise, browsers will drop form"
|
||||||
|
" data.\n\n"
|
||||||
|
"This exception is only raised in debug mode."
|
||||||
|
)
|
||||||
|
super().__init__("".join(buf))
|
||||||
|
|
||||||
|
|
||||||
|
def attach_enctype_error_multidict(request):
|
||||||
|
"""Patch ``request.files.__getitem__`` to raise a descriptive error
|
||||||
|
about ``enctype=multipart/form-data``.
|
||||||
|
|
||||||
|
:param request: The request to patch.
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
oldcls = request.files.__class__
|
||||||
|
|
||||||
|
class newcls(oldcls):
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
return super().__getitem__(key)
|
||||||
|
except KeyError as e:
|
||||||
|
if key not in request.form:
|
||||||
|
raise
|
||||||
|
|
||||||
|
raise DebugFilesKeyError(request, key).with_traceback(
|
||||||
|
e.__traceback__
|
||||||
|
) from None
|
||||||
|
|
||||||
|
newcls.__name__ = oldcls.__name__
|
||||||
|
newcls.__module__ = oldcls.__module__
|
||||||
|
request.files.__class__ = newcls
|
||||||
|
|
||||||
|
|
||||||
|
def _dump_loader_info(loader) -> t.Generator:
|
||||||
|
yield f"class: {type(loader).__module__}.{type(loader).__name__}"
|
||||||
|
for key, value in sorted(loader.__dict__.items()):
|
||||||
|
if key.startswith("_"):
|
||||||
|
continue
|
||||||
|
if isinstance(value, (tuple, list)):
|
||||||
|
if not all(isinstance(x, str) for x in value):
|
||||||
|
continue
|
||||||
|
yield f"{key}:"
|
||||||
|
for item in value:
|
||||||
|
yield f" - {item}"
|
||||||
|
continue
|
||||||
|
elif not isinstance(value, (str, int, float, bool)):
|
||||||
|
continue
|
||||||
|
yield f"{key}: {value!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def explain_template_loading_attempts(app: Flask, template, attempts) -> None:
|
||||||
|
"""This should help developers understand what failed"""
|
||||||
|
info = [f"Locating template {template!r}:"]
|
||||||
|
total_found = 0
|
||||||
|
blueprint = None
|
||||||
|
if request_ctx and request_ctx.request.blueprint is not None:
|
||||||
|
blueprint = request_ctx.request.blueprint
|
||||||
|
|
||||||
|
for idx, (loader, srcobj, triple) in enumerate(attempts):
|
||||||
|
if isinstance(srcobj, Flask):
|
||||||
|
src_info = f"application {srcobj.import_name!r}"
|
||||||
|
elif isinstance(srcobj, Blueprint):
|
||||||
|
src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"
|
||||||
|
else:
|
||||||
|
src_info = repr(srcobj)
|
||||||
|
|
||||||
|
info.append(f"{idx + 1:5}: trying loader of {src_info}")
|
||||||
|
|
||||||
|
for line in _dump_loader_info(loader):
|
||||||
|
info.append(f" {line}")
|
||||||
|
|
||||||
|
if triple is None:
|
||||||
|
detail = "no match"
|
||||||
|
else:
|
||||||
|
detail = f"found ({triple[1] or '<string>'!r})"
|
||||||
|
total_found += 1
|
||||||
|
info.append(f" -> {detail}")
|
||||||
|
|
||||||
|
seems_fishy = False
|
||||||
|
if total_found == 0:
|
||||||
|
info.append("Error: the template could not be found.")
|
||||||
|
seems_fishy = True
|
||||||
|
elif total_found > 1:
|
||||||
|
info.append("Warning: multiple loaders returned a match for the template.")
|
||||||
|
seems_fishy = True
|
||||||
|
|
||||||
|
if blueprint is not None and seems_fishy:
|
||||||
|
info.append(
|
||||||
|
" The template was looked up from an endpoint that belongs"
|
||||||
|
f" to the blueprint {blueprint!r}."
|
||||||
|
)
|
||||||
|
info.append(" Maybe you did not place a template in the right folder?")
|
||||||
|
info.append(" See https://flask.palletsprojects.com/blueprints/#templates")
|
||||||
|
|
||||||
|
app.logger.info("\n".join(info))
|
@ -0,0 +1,107 @@
|
|||||||
|
import typing as t
|
||||||
|
from contextvars import ContextVar
|
||||||
|
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .app import Flask
|
||||||
|
from .ctx import _AppCtxGlobals
|
||||||
|
from .ctx import AppContext
|
||||||
|
from .ctx import RequestContext
|
||||||
|
from .sessions import SessionMixin
|
||||||
|
from .wrappers import Request
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeStack:
|
||||||
|
def __init__(self, name: str, cv: ContextVar[t.Any]) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.cv = cv
|
||||||
|
|
||||||
|
def _warn(self):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
f"'_{self.name}_ctx_stack' is deprecated and will be"
|
||||||
|
" removed in Flask 2.3. Use 'g' to store data, or"
|
||||||
|
f" '{self.name}_ctx' to access the current context.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def push(self, obj: t.Any) -> None:
|
||||||
|
self._warn()
|
||||||
|
self.cv.set(obj)
|
||||||
|
|
||||||
|
def pop(self) -> t.Any:
|
||||||
|
self._warn()
|
||||||
|
ctx = self.cv.get(None)
|
||||||
|
self.cv.set(None)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top(self) -> t.Optional[t.Any]:
|
||||||
|
self._warn()
|
||||||
|
return self.cv.get(None)
|
||||||
|
|
||||||
|
|
||||||
|
_no_app_msg = """\
|
||||||
|
Working outside of application context.
|
||||||
|
|
||||||
|
This typically means that you attempted to use functionality that needed
|
||||||
|
the current application. To solve this, set up an application context
|
||||||
|
with app.app_context(). See the documentation for more information.\
|
||||||
|
"""
|
||||||
|
_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx")
|
||||||
|
__app_ctx_stack = _FakeStack("app", _cv_app)
|
||||||
|
app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_app, unbound_message=_no_app_msg
|
||||||
|
)
|
||||||
|
current_app: "Flask" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_app, "app", unbound_message=_no_app_msg
|
||||||
|
)
|
||||||
|
g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_app, "g", unbound_message=_no_app_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
_no_req_msg = """\
|
||||||
|
Working outside of request context.
|
||||||
|
|
||||||
|
This typically means that you attempted to use functionality that needed
|
||||||
|
an active HTTP request. Consult the documentation on testing for
|
||||||
|
information about how to avoid this problem.\
|
||||||
|
"""
|
||||||
|
_cv_request: ContextVar["RequestContext"] = ContextVar("flask.request_ctx")
|
||||||
|
__request_ctx_stack = _FakeStack("request", _cv_request)
|
||||||
|
request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_request, unbound_message=_no_req_msg
|
||||||
|
)
|
||||||
|
request: "Request" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_request, "request", unbound_message=_no_req_msg
|
||||||
|
)
|
||||||
|
session: "SessionMixin" = LocalProxy( # type: ignore[assignment]
|
||||||
|
_cv_request, "session", unbound_message=_no_req_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> t.Any:
|
||||||
|
if name == "_app_ctx_stack":
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return __app_ctx_stack
|
||||||
|
|
||||||
|
if name == "_request_ctx_stack":
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return __request_ctx_stack
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
@ -0,0 +1,705 @@
|
|||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import lru_cache
|
||||||
|
from functools import update_wrapper
|
||||||
|
from threading import RLock
|
||||||
|
|
||||||
|
import werkzeug.utils
|
||||||
|
from werkzeug.exceptions import abort as _wz_abort
|
||||||
|
from werkzeug.utils import redirect as _wz_redirect
|
||||||
|
|
||||||
|
from .globals import _cv_request
|
||||||
|
from .globals import current_app
|
||||||
|
from .globals import request
|
||||||
|
from .globals import request_ctx
|
||||||
|
from .globals import session
|
||||||
|
from .signals import message_flashed
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
|
from .wrappers import Response
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
|
||||||
|
def get_env() -> str:
|
||||||
|
"""Get the environment the app is running in, indicated by the
|
||||||
|
:envvar:`FLASK_ENV` environment variable. The default is
|
||||||
|
``'production'``.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'FLASK_ENV' and 'get_env' are deprecated and will be removed"
|
||||||
|
" in Flask 2.3. Use 'FLASK_DEBUG' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return os.environ.get("FLASK_ENV") or "production"
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_flag() -> bool:
|
||||||
|
"""Get whether debug mode should be enabled for the app, indicated by the
|
||||||
|
:envvar:`FLASK_DEBUG` environment variable. The default is ``False``.
|
||||||
|
"""
|
||||||
|
val = os.environ.get("FLASK_DEBUG")
|
||||||
|
|
||||||
|
if not val:
|
||||||
|
env = os.environ.get("FLASK_ENV")
|
||||||
|
|
||||||
|
if env is not None:
|
||||||
|
print(
|
||||||
|
"'FLASK_ENV' is deprecated and will not be used in"
|
||||||
|
" Flask 2.3. Use 'FLASK_DEBUG' instead.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return env == "development"
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return val.lower() not in {"0", "false", "no"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_load_dotenv(default: bool = True) -> bool:
|
||||||
|
"""Get whether the user has disabled loading default dotenv files by
|
||||||
|
setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load
|
||||||
|
the files.
|
||||||
|
|
||||||
|
:param default: What to return if the env var isn't set.
|
||||||
|
"""
|
||||||
|
val = os.environ.get("FLASK_SKIP_DOTENV")
|
||||||
|
|
||||||
|
if not val:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return val.lower() in ("0", "false", "no")
|
||||||
|
|
||||||
|
|
||||||
|
def stream_with_context(
|
||||||
|
generator_or_function: t.Union[
|
||||||
|
t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]]
|
||||||
|
]
|
||||||
|
) -> t.Iterator[t.AnyStr]:
|
||||||
|
"""Request contexts disappear when the response is started on the server.
|
||||||
|
This is done for efficiency reasons and to make it less likely to encounter
|
||||||
|
memory leaks with badly written WSGI middlewares. The downside is that if
|
||||||
|
you are using streamed responses, the generator cannot access request bound
|
||||||
|
information any more.
|
||||||
|
|
||||||
|
This function however can help you keep the context around for longer::
|
||||||
|
|
||||||
|
from flask import stream_with_context, request, Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def streamed_response():
|
||||||
|
@stream_with_context
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return Response(generate())
|
||||||
|
|
||||||
|
Alternatively it can also be used around a specific generator::
|
||||||
|
|
||||||
|
from flask import stream_with_context, request, Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def streamed_response():
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return Response(stream_with_context(generate()))
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
gen = iter(generator_or_function) # type: ignore
|
||||||
|
except TypeError:
|
||||||
|
|
||||||
|
def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
gen = generator_or_function(*args, **kwargs) # type: ignore
|
||||||
|
return stream_with_context(gen)
|
||||||
|
|
||||||
|
return update_wrapper(decorator, generator_or_function) # type: ignore
|
||||||
|
|
||||||
|
def generator() -> t.Generator:
|
||||||
|
ctx = _cv_request.get(None)
|
||||||
|
if ctx is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"'stream_with_context' can only be used when a request"
|
||||||
|
" context is active, such as in a view function."
|
||||||
|
)
|
||||||
|
with ctx:
|
||||||
|
# Dummy sentinel. Has to be inside the context block or we're
|
||||||
|
# not actually keeping the context around.
|
||||||
|
yield None
|
||||||
|
|
||||||
|
# The try/finally is here so that if someone passes a WSGI level
|
||||||
|
# iterator in we're still running the cleanup logic. Generators
|
||||||
|
# don't need that because they are closed on their destruction
|
||||||
|
# automatically.
|
||||||
|
try:
|
||||||
|
yield from gen
|
||||||
|
finally:
|
||||||
|
if hasattr(gen, "close"):
|
||||||
|
gen.close() # type: ignore
|
||||||
|
|
||||||
|
# The trick is to start the generator. Then the code execution runs until
|
||||||
|
# the first dummy None is yielded at which point the context was already
|
||||||
|
# pushed. This item is discarded. Then when the iteration continues the
|
||||||
|
# real generator is executed.
|
||||||
|
wrapped_g = generator()
|
||||||
|
next(wrapped_g)
|
||||||
|
return wrapped_g
|
||||||
|
|
||||||
|
|
||||||
|
def make_response(*args: t.Any) -> "Response":
|
||||||
|
"""Sometimes it is necessary to set additional headers in a view. Because
|
||||||
|
views do not have to return response objects but can return a value that
|
||||||
|
is converted into a response object by Flask itself, it becomes tricky to
|
||||||
|
add headers to it. This function can be called instead of using a return
|
||||||
|
and you will get a response object which you can use to attach headers.
|
||||||
|
|
||||||
|
If view looked like this and you want to add a new header::
|
||||||
|
|
||||||
|
def index():
|
||||||
|
return render_template('index.html', foo=42)
|
||||||
|
|
||||||
|
You can now do something like this::
|
||||||
|
|
||||||
|
def index():
|
||||||
|
response = make_response(render_template('index.html', foo=42))
|
||||||
|
response.headers['X-Parachutes'] = 'parachutes are cool'
|
||||||
|
return response
|
||||||
|
|
||||||
|
This function accepts the very same arguments you can return from a
|
||||||
|
view function. This for example creates a response with a 404 error
|
||||||
|
code::
|
||||||
|
|
||||||
|
response = make_response(render_template('not_found.html'), 404)
|
||||||
|
|
||||||
|
The other use case of this function is to force the return value of a
|
||||||
|
view function into a response which is helpful with view
|
||||||
|
decorators::
|
||||||
|
|
||||||
|
response = make_response(view_function())
|
||||||
|
response.headers['X-Parachutes'] = 'parachutes are cool'
|
||||||
|
|
||||||
|
Internally this function does the following things:
|
||||||
|
|
||||||
|
- if no arguments are passed, it creates a new response argument
|
||||||
|
- if one argument is passed, :meth:`flask.Flask.make_response`
|
||||||
|
is invoked with it.
|
||||||
|
- if more than one argument is passed, the arguments are passed
|
||||||
|
to the :meth:`flask.Flask.make_response` function as tuple.
|
||||||
|
|
||||||
|
.. versionadded:: 0.6
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
return current_app.response_class()
|
||||||
|
if len(args) == 1:
|
||||||
|
args = args[0]
|
||||||
|
return current_app.make_response(args) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def url_for(
|
||||||
|
endpoint: str,
|
||||||
|
*,
|
||||||
|
_anchor: t.Optional[str] = None,
|
||||||
|
_method: t.Optional[str] = None,
|
||||||
|
_scheme: t.Optional[str] = None,
|
||||||
|
_external: t.Optional[bool] = None,
|
||||||
|
**values: t.Any,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a URL to the given endpoint with the given values.
|
||||||
|
|
||||||
|
This requires an active request or application context, and calls
|
||||||
|
:meth:`current_app.url_for() <flask.Flask.url_for>`. See that method
|
||||||
|
for full documentation.
|
||||||
|
|
||||||
|
:param endpoint: The endpoint name associated with the URL to
|
||||||
|
generate. If this starts with a ``.``, the current blueprint
|
||||||
|
name (if any) will be used.
|
||||||
|
:param _anchor: If given, append this as ``#anchor`` to the URL.
|
||||||
|
:param _method: If given, generate the URL associated with this
|
||||||
|
method for the endpoint.
|
||||||
|
:param _scheme: If given, the URL will have this scheme if it is
|
||||||
|
external.
|
||||||
|
:param _external: If given, prefer the URL to be internal (False) or
|
||||||
|
require it to be external (True). External URLs include the
|
||||||
|
scheme and domain. When not in an active request, URLs are
|
||||||
|
external by default.
|
||||||
|
:param values: Values to use for the variable parts of the URL rule.
|
||||||
|
Unknown keys are appended as query string arguments, like
|
||||||
|
``?a=b&c=d``.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.url_for``, allowing an app to override the
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.10
|
||||||
|
The ``_scheme`` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9
|
||||||
|
The ``_anchor`` and ``_method`` parameters were added.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9
|
||||||
|
Calls ``app.handle_url_build_error`` on build errors.
|
||||||
|
"""
|
||||||
|
return current_app.url_for(
|
||||||
|
endpoint,
|
||||||
|
_anchor=_anchor,
|
||||||
|
_method=_method,
|
||||||
|
_scheme=_scheme,
|
||||||
|
_external=_external,
|
||||||
|
**values,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect(
|
||||||
|
location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None
|
||||||
|
) -> "BaseResponse":
|
||||||
|
"""Create a redirect response object.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
|
:meth:`~flask.Flask.redirect` method, otherwise it will use
|
||||||
|
:func:`werkzeug.utils.redirect`.
|
||||||
|
|
||||||
|
:param location: The URL to redirect to.
|
||||||
|
:param code: The status code for the redirect.
|
||||||
|
:param Response: The response class to use. Not used when
|
||||||
|
``current_app`` is active, which uses ``app.response_class``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
Calls ``current_app.redirect`` if available instead of always
|
||||||
|
using Werkzeug's default ``redirect``.
|
||||||
|
"""
|
||||||
|
if current_app:
|
||||||
|
return current_app.redirect(location, code=code)
|
||||||
|
|
||||||
|
return _wz_redirect(location, code=code, Response=Response)
|
||||||
|
|
||||||
|
|
||||||
|
def abort( # type: ignore[misc]
|
||||||
|
code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> "te.NoReturn":
|
||||||
|
"""Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given
|
||||||
|
status code.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will call its
|
||||||
|
:attr:`~flask.Flask.aborter` object, otherwise it will use
|
||||||
|
:func:`werkzeug.exceptions.abort`.
|
||||||
|
|
||||||
|
:param code: The status code for the exception, which must be
|
||||||
|
registered in ``app.aborter``.
|
||||||
|
:param args: Passed to the exception.
|
||||||
|
:param kwargs: Passed to the exception.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
Calls ``current_app.aborter`` if available instead of always
|
||||||
|
using Werkzeug's default ``abort``.
|
||||||
|
"""
|
||||||
|
if current_app:
|
||||||
|
current_app.aborter(code, *args, **kwargs)
|
||||||
|
|
||||||
|
_wz_abort(code, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_attribute(template_name: str, attribute: str) -> t.Any:
|
||||||
|
"""Loads a macro (or variable) a template exports. This can be used to
|
||||||
|
invoke a macro from within Python code. If you for example have a
|
||||||
|
template named :file:`_cider.html` with the following contents:
|
||||||
|
|
||||||
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
|
{% macro hello(name) %}Hello {{ name }}!{% endmacro %}
|
||||||
|
|
||||||
|
You can access this from Python code like this::
|
||||||
|
|
||||||
|
hello = get_template_attribute('_cider.html', 'hello')
|
||||||
|
return hello('World')
|
||||||
|
|
||||||
|
.. versionadded:: 0.2
|
||||||
|
|
||||||
|
:param template_name: the name of the template
|
||||||
|
:param attribute: the name of the variable of macro to access
|
||||||
|
"""
|
||||||
|
return getattr(current_app.jinja_env.get_template(template_name).module, attribute)
|
||||||
|
|
||||||
|
|
||||||
|
def flash(message: str, category: str = "message") -> None:
|
||||||
|
"""Flashes a message to the next request. In order to remove the
|
||||||
|
flashed message from the session and to display it to the user,
|
||||||
|
the template has to call :func:`get_flashed_messages`.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3
|
||||||
|
`category` parameter added.
|
||||||
|
|
||||||
|
:param message: the message to be flashed.
|
||||||
|
:param category: the category for the message. The following values
|
||||||
|
are recommended: ``'message'`` for any kind of message,
|
||||||
|
``'error'`` for errors, ``'info'`` for information
|
||||||
|
messages and ``'warning'`` for warnings. However any
|
||||||
|
kind of string can be used as category.
|
||||||
|
"""
|
||||||
|
# Original implementation:
|
||||||
|
#
|
||||||
|
# session.setdefault('_flashes', []).append((category, message))
|
||||||
|
#
|
||||||
|
# This assumed that changes made to mutable structures in the session are
|
||||||
|
# always in sync with the session object, which is not true for session
|
||||||
|
# implementations that use external storage for keeping their keys/values.
|
||||||
|
flashes = session.get("_flashes", [])
|
||||||
|
flashes.append((category, message))
|
||||||
|
session["_flashes"] = flashes
|
||||||
|
message_flashed.send(
|
||||||
|
current_app._get_current_object(), # type: ignore
|
||||||
|
message=message,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_flashed_messages(
|
||||||
|
with_categories: bool = False, category_filter: t.Iterable[str] = ()
|
||||||
|
) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]:
|
||||||
|
"""Pulls all flashed messages from the session and returns them.
|
||||||
|
Further calls in the same request to the function will return
|
||||||
|
the same messages. By default just the messages are returned,
|
||||||
|
but when `with_categories` is set to ``True``, the return value will
|
||||||
|
be a list of tuples in the form ``(category, message)`` instead.
|
||||||
|
|
||||||
|
Filter the flashed messages to one or more categories by providing those
|
||||||
|
categories in `category_filter`. This allows rendering categories in
|
||||||
|
separate html blocks. The `with_categories` and `category_filter`
|
||||||
|
arguments are distinct:
|
||||||
|
|
||||||
|
* `with_categories` controls whether categories are returned with message
|
||||||
|
text (``True`` gives a tuple, where ``False`` gives just the message text).
|
||||||
|
* `category_filter` filters the messages down to only those matching the
|
||||||
|
provided categories.
|
||||||
|
|
||||||
|
See :doc:`/patterns/flashing` for examples.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3
|
||||||
|
`with_categories` parameter added.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9
|
||||||
|
`category_filter` parameter added.
|
||||||
|
|
||||||
|
:param with_categories: set to ``True`` to also receive categories.
|
||||||
|
:param category_filter: filter of categories to limit return values. Only
|
||||||
|
categories in the list will be returned.
|
||||||
|
"""
|
||||||
|
flashes = request_ctx.flashes
|
||||||
|
if flashes is None:
|
||||||
|
flashes = session.pop("_flashes") if "_flashes" in session else []
|
||||||
|
request_ctx.flashes = flashes
|
||||||
|
if category_filter:
|
||||||
|
flashes = list(filter(lambda f: f[0] in category_filter, flashes))
|
||||||
|
if not with_categories:
|
||||||
|
return [x[1] for x in flashes]
|
||||||
|
return flashes
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]:
|
||||||
|
if kwargs.get("max_age") is None:
|
||||||
|
kwargs["max_age"] = current_app.get_send_file_max_age
|
||||||
|
|
||||||
|
kwargs.update(
|
||||||
|
environ=request.environ,
|
||||||
|
use_x_sendfile=current_app.config["USE_X_SENDFILE"],
|
||||||
|
response_class=current_app.response_class,
|
||||||
|
_root_path=current_app.root_path, # type: ignore
|
||||||
|
)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def send_file(
|
||||||
|
path_or_file: t.Union[os.PathLike, str, t.BinaryIO],
|
||||||
|
mimetype: t.Optional[str] = None,
|
||||||
|
as_attachment: bool = False,
|
||||||
|
download_name: t.Optional[str] = None,
|
||||||
|
conditional: bool = True,
|
||||||
|
etag: t.Union[bool, str] = True,
|
||||||
|
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
|
||||||
|
max_age: t.Optional[
|
||||||
|
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
|
||||||
|
] = None,
|
||||||
|
) -> "Response":
|
||||||
|
"""Send the contents of a file to the client.
|
||||||
|
|
||||||
|
The first argument can be a file path or a file-like object. Paths
|
||||||
|
are preferred in most cases because Werkzeug can manage the file and
|
||||||
|
get extra information from the path. Passing a file-like object
|
||||||
|
requires that the file is opened in binary mode, and is mostly
|
||||||
|
useful when building a file in memory with :class:`io.BytesIO`.
|
||||||
|
|
||||||
|
Never pass file paths provided by a user. The path is assumed to be
|
||||||
|
trusted, so a user could craft a path to access a file you didn't
|
||||||
|
intend. Use :func:`send_from_directory` to safely serve
|
||||||
|
user-requested paths from within a directory.
|
||||||
|
|
||||||
|
If the WSGI server sets a ``file_wrapper`` in ``environ``, it is
|
||||||
|
used, otherwise Werkzeug's built-in wrapper is used. Alternatively,
|
||||||
|
if the HTTP server supports ``X-Sendfile``, configuring Flask with
|
||||||
|
``USE_X_SENDFILE = True`` will tell the server to send the given
|
||||||
|
path, which is much more efficient than reading it in Python.
|
||||||
|
|
||||||
|
:param path_or_file: The path to the file to send, relative to the
|
||||||
|
current working directory if a relative path is given.
|
||||||
|
Alternatively, a file-like object opened in binary mode. Make
|
||||||
|
sure the file pointer is seeked to the start of the data.
|
||||||
|
:param mimetype: The MIME type to send for the file. If not
|
||||||
|
provided, it will try to detect it from the file name.
|
||||||
|
:param as_attachment: Indicate to a browser that it should offer to
|
||||||
|
save the file instead of displaying it.
|
||||||
|
:param download_name: The default name browsers will use when saving
|
||||||
|
the file. Defaults to the passed file name.
|
||||||
|
:param conditional: Enable conditional and range responses based on
|
||||||
|
request headers. Requires passing a file path and ``environ``.
|
||||||
|
:param etag: Calculate an ETag for the file, which requires passing
|
||||||
|
a file path. Can also be a string to use instead.
|
||||||
|
:param last_modified: The last modified time to send for the file,
|
||||||
|
in seconds. If not provided, it will try to detect it from the
|
||||||
|
file path.
|
||||||
|
:param max_age: How long the client should cache the file, in
|
||||||
|
seconds. If set, ``Cache-Control`` will be ``public``, otherwise
|
||||||
|
it will be ``no-cache`` to prefer conditional caching.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``download_name`` replaces the ``attachment_filename``
|
||||||
|
parameter. If ``as_attachment=False``, it is passed with
|
||||||
|
``Content-Disposition: inline`` instead.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``max_age`` replaces the ``cache_timeout`` parameter.
|
||||||
|
``conditional`` is enabled and ``max_age`` is not set by
|
||||||
|
default.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``etag`` replaces the ``add_etags`` parameter. It can be a
|
||||||
|
string to use instead of generating one.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Passing a file-like object that inherits from
|
||||||
|
:class:`~io.TextIOBase` will raise a :exc:`ValueError` rather
|
||||||
|
than sending an empty file.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
Moved the implementation to Werkzeug. This is now a wrapper to
|
||||||
|
pass some Flask-specific arguments.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1
|
||||||
|
``filename`` may be a :class:`~os.PathLike` object.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.1
|
||||||
|
Passing a :class:`~io.BytesIO` object supports range requests.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0.3
|
||||||
|
Filenames are encoded with ASCII instead of Latin-1 for broader
|
||||||
|
compatibility with WSGI servers.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0
|
||||||
|
UTF-8 filenames as specified in :rfc:`2231` are supported.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.12
|
||||||
|
The filename is no longer automatically inferred from file
|
||||||
|
objects. If you want to use automatic MIME and etag support,
|
||||||
|
pass a filename via ``filename_or_fp`` or
|
||||||
|
``attachment_filename``.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.12
|
||||||
|
``attachment_filename`` is preferred over ``filename`` for MIME
|
||||||
|
detection.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.9
|
||||||
|
``cache_timeout`` defaults to
|
||||||
|
:meth:`Flask.get_send_file_max_age`.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.7
|
||||||
|
MIME guessing and etag support for file-like objects was
|
||||||
|
deprecated because it was unreliable. Pass a filename if you are
|
||||||
|
able to, otherwise attach an etag yourself.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.5
|
||||||
|
The ``add_etags``, ``cache_timeout`` and ``conditional``
|
||||||
|
parameters were added. The default behavior is to add etags.
|
||||||
|
|
||||||
|
.. versionadded:: 0.2
|
||||||
|
"""
|
||||||
|
return werkzeug.utils.send_file( # type: ignore[return-value]
|
||||||
|
**_prepare_send_file_kwargs(
|
||||||
|
path_or_file=path_or_file,
|
||||||
|
environ=request.environ,
|
||||||
|
mimetype=mimetype,
|
||||||
|
as_attachment=as_attachment,
|
||||||
|
download_name=download_name,
|
||||||
|
conditional=conditional,
|
||||||
|
etag=etag,
|
||||||
|
last_modified=last_modified,
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_from_directory(
|
||||||
|
directory: t.Union[os.PathLike, str],
|
||||||
|
path: t.Union[os.PathLike, str],
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> "Response":
|
||||||
|
"""Send a file from within a directory using :func:`send_file`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/uploads/<path:name>")
|
||||||
|
def download_file(name):
|
||||||
|
return send_from_directory(
|
||||||
|
app.config['UPLOAD_FOLDER'], name, as_attachment=True
|
||||||
|
)
|
||||||
|
|
||||||
|
This is a secure way to serve files from a folder, such as static
|
||||||
|
files or uploads. Uses :func:`~werkzeug.security.safe_join` to
|
||||||
|
ensure the path coming from the client is not maliciously crafted to
|
||||||
|
point outside the specified directory.
|
||||||
|
|
||||||
|
If the final path does not point to an existing regular file,
|
||||||
|
raises a 404 :exc:`~werkzeug.exceptions.NotFound` error.
|
||||||
|
|
||||||
|
:param directory: The directory that ``path`` must be located under,
|
||||||
|
relative to the current application's root path.
|
||||||
|
:param path: The path to the file to send, relative to
|
||||||
|
``directory``.
|
||||||
|
:param kwargs: Arguments to pass to :func:`send_file`.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``path`` replaces the ``filename`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
Moved the implementation to Werkzeug. This is now a wrapper to
|
||||||
|
pass some Flask-specific arguments.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
"""
|
||||||
|
return werkzeug.utils.send_from_directory( # type: ignore[return-value]
|
||||||
|
directory, path, **_prepare_send_file_kwargs(**kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_path(import_name: str) -> str:
|
||||||
|
"""Find the root path of a package, or the path that contains a
|
||||||
|
module. If it cannot be found, returns the current working
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Not to be confused with the value returned by :func:`find_package`.
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
# Module already imported and has a file attribute. Use that first.
|
||||||
|
mod = sys.modules.get(import_name)
|
||||||
|
|
||||||
|
if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
|
||||||
|
return os.path.dirname(os.path.abspath(mod.__file__))
|
||||||
|
|
||||||
|
# Next attempt: check the loader.
|
||||||
|
loader = pkgutil.get_loader(import_name)
|
||||||
|
|
||||||
|
# Loader does not exist or we're referring to an unloaded main
|
||||||
|
# module or a main module without path (interactive sessions), go
|
||||||
|
# with the current working directory.
|
||||||
|
if loader is None or import_name == "__main__":
|
||||||
|
return os.getcwd()
|
||||||
|
|
||||||
|
if hasattr(loader, "get_filename"):
|
||||||
|
filepath = loader.get_filename(import_name) # type: ignore
|
||||||
|
else:
|
||||||
|
# Fall back to imports.
|
||||||
|
__import__(import_name)
|
||||||
|
mod = sys.modules[import_name]
|
||||||
|
filepath = getattr(mod, "__file__", None)
|
||||||
|
|
||||||
|
# If we don't have a file path it might be because it is a
|
||||||
|
# namespace package. In this case pick the root path from the
|
||||||
|
# first module that is contained in the package.
|
||||||
|
if filepath is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No root path can be found for the provided module"
|
||||||
|
f" {import_name!r}. This can happen because the module"
|
||||||
|
" came from an import hook that does not provide file"
|
||||||
|
" name information or because it's a namespace package."
|
||||||
|
" In this case the root path needs to be explicitly"
|
||||||
|
" provided."
|
||||||
|
)
|
||||||
|
|
||||||
|
# filepath is import_name.py for a module, or __init__.py for a package.
|
||||||
|
return os.path.dirname(os.path.abspath(filepath))
|
||||||
|
|
||||||
|
|
||||||
|
class locked_cached_property(werkzeug.utils.cached_property):
|
||||||
|
"""A :func:`property` that is only evaluated once. Like
|
||||||
|
:class:`werkzeug.utils.cached_property` except access uses a lock
|
||||||
|
for thread safety.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Inherits from Werkzeug's ``cached_property`` (and ``property``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fget: t.Callable[[t.Any], t.Any],
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
doc: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(fget, name=name, doc=doc)
|
||||||
|
self.lock = RLock()
|
||||||
|
|
||||||
|
def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
return super().__get__(obj, type=type)
|
||||||
|
|
||||||
|
def __set__(self, obj: object, value: t.Any) -> None:
|
||||||
|
with self.lock:
|
||||||
|
super().__set__(obj, value)
|
||||||
|
|
||||||
|
def __delete__(self, obj: object) -> None:
|
||||||
|
with self.lock:
|
||||||
|
super().__delete__(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip(value: str) -> bool:
|
||||||
|
"""Determine if the given string is an IP address.
|
||||||
|
|
||||||
|
:param value: value to check
|
||||||
|
:type value: str
|
||||||
|
|
||||||
|
:return: True if string is an IP address
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
for family in (socket.AF_INET, socket.AF_INET6):
|
||||||
|
try:
|
||||||
|
socket.inet_pton(family, value)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def _split_blueprint_path(name: str) -> t.List[str]:
|
||||||
|
out: t.List[str] = [name]
|
||||||
|
|
||||||
|
if "." in name:
|
||||||
|
out.extend(_split_blueprint_path(name.rpartition(".")[0]))
|
||||||
|
|
||||||
|
return out
|
@ -0,0 +1,342 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json as _json
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
|
||||||
|
|
||||||
|
from ..globals import current_app
|
||||||
|
from .provider import _default
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from ..app import Flask
|
||||||
|
from ..wrappers import Response
|
||||||
|
|
||||||
|
|
||||||
|
class JSONEncoder(_json.JSONEncoder):
|
||||||
|
"""The default JSON encoder. Handles extra types compared to the
|
||||||
|
built-in :class:`json.JSONEncoder`.
|
||||||
|
|
||||||
|
- :class:`datetime.datetime` and :class:`datetime.date` are
|
||||||
|
serialized to :rfc:`822` strings. This is the same as the HTTP
|
||||||
|
date format.
|
||||||
|
- :class:`decimal.Decimal` is serialized to a string.
|
||||||
|
- :class:`uuid.UUID` is serialized to a string.
|
||||||
|
- :class:`dataclasses.dataclass` is passed to
|
||||||
|
:func:`dataclasses.asdict`.
|
||||||
|
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
|
||||||
|
method) will call the ``__html__`` method to get a string.
|
||||||
|
|
||||||
|
Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
|
||||||
|
:attr:`flask.Blueprint.json_encoder` to override the default.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Use ``app.json`` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'JSONEncoder' is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Use 'Flask.json' to provide an alternate"
|
||||||
|
" JSON implementation instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def default(self, o: t.Any) -> t.Any:
|
||||||
|
"""Convert ``o`` to a JSON serializable type. See
|
||||||
|
:meth:`json.JSONEncoder.default`. Python does not support
|
||||||
|
overriding how basic types like ``str`` or ``list`` are
|
||||||
|
serialized, they are handled before this method.
|
||||||
|
"""
|
||||||
|
return _default(o)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONDecoder(_json.JSONDecoder):
|
||||||
|
"""The default JSON decoder.
|
||||||
|
|
||||||
|
This does not change any behavior from the built-in
|
||||||
|
:class:`json.JSONDecoder`.
|
||||||
|
|
||||||
|
Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
|
||||||
|
:attr:`flask.Blueprint.json_decoder` to override the default.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Use ``app.json`` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'JSONDecoder' is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Use 'Flask.json' to provide an alternate"
|
||||||
|
" JSON implementation instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize data as JSON.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
|
:meth:`app.json.dumps() <flask.json.provider.JSONProvider.dumps>`
|
||||||
|
method, otherwise it will use :func:`json.dumps`.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param kwargs: Arguments passed to the ``dumps`` implementation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.dumps``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.2
|
||||||
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``encoding`` will be removed in Flask 2.1.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0.3
|
||||||
|
``app`` can be passed directly, rather than requiring an app
|
||||||
|
context for configuration.
|
||||||
|
"""
|
||||||
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.dumps' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
kwargs.setdefault("default", _default)
|
||||||
|
return _json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def dump(
|
||||||
|
obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
"""Serialize data as JSON and write to a file.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
|
:meth:`app.json.dump() <flask.json.provider.JSONProvider.dump>`
|
||||||
|
method, otherwise it will use :func:`json.dump`.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param fp: A file opened for writing text. Should use the UTF-8
|
||||||
|
encoding to be valid JSON.
|
||||||
|
:param kwargs: Arguments passed to the ``dump`` implementation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.dump``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Writing to a binary file, and the ``encoding`` argument, will be
|
||||||
|
removed in Flask 2.1.
|
||||||
|
"""
|
||||||
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.dump' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
app.json.dump(obj, fp, **kwargs)
|
||||||
|
else:
|
||||||
|
kwargs.setdefault("default", _default)
|
||||||
|
_json.dump(obj, fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
|
:meth:`app.json.loads() <flask.json.provider.JSONProvider.loads>`
|
||||||
|
method, otherwise it will use :func:`json.loads`.
|
||||||
|
|
||||||
|
:param s: Text or UTF-8 bytes.
|
||||||
|
:param kwargs: Arguments passed to the ``loads`` implementation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.loads``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``encoding`` will be removed in Flask 2.1. The data must be a
|
||||||
|
string or UTF-8 bytes.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0.3
|
||||||
|
``app`` can be passed directly, rather than requiring an app
|
||||||
|
context for configuration.
|
||||||
|
"""
|
||||||
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.loads' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
return _json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON read from a file.
|
||||||
|
|
||||||
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
|
:meth:`app.json.load() <flask.json.provider.JSONProvider.load>`
|
||||||
|
method, otherwise it will use :func:`json.load`.
|
||||||
|
|
||||||
|
:param fp: A file opened for reading text or UTF-8 bytes.
|
||||||
|
:param kwargs: Arguments passed to the ``load`` implementation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.load``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
The ``app`` parameter will be removed in Flask 2.3.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``encoding`` will be removed in Flask 2.1. The file must be text
|
||||||
|
mode, or binary mode with UTF-8 bytes.
|
||||||
|
"""
|
||||||
|
if app is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'app' parameter is deprecated and will be removed in"
|
||||||
|
" Flask 2.3. Call 'app.json.load' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app = current_app
|
||||||
|
|
||||||
|
if app:
|
||||||
|
return app.json.load(fp, **kwargs)
|
||||||
|
|
||||||
|
return _json.load(fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize an object to a string of JSON with :func:`dumps`, then
|
||||||
|
replace HTML-unsafe characters with Unicode escapes and mark the
|
||||||
|
result safe with :class:`~markupsafe.Markup`.
|
||||||
|
|
||||||
|
This is available in templates as the ``|tojson`` filter.
|
||||||
|
|
||||||
|
The returned string is safe to render in HTML documents and
|
||||||
|
``<script>`` tags. The exception is in HTML attributes that are
|
||||||
|
double quoted; either use single quotes or the ``|forceescape``
|
||||||
|
filter.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. This is built-in to Jinja now.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
|
||||||
|
value is marked safe by wrapping in :class:`~markupsafe.Markup`.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.10
|
||||||
|
Single quotes are escaped, making this safe to use in HTML,
|
||||||
|
``<script>`` tags, and single-quoted attributes without further
|
||||||
|
escaping.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'htmlsafe_dumps' is deprecated and will be removed in Flask"
|
||||||
|
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def htmlsafe_dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
|
||||||
|
"""Serialize an object to JSON written to a file object, replacing
|
||||||
|
HTML-unsafe characters with Unicode escapes. See
|
||||||
|
:func:`htmlsafe_dumps` and :func:`dumps`.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'htmlsafe_dump' is deprecated and will be removed in Flask"
|
||||||
|
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
fp.write(htmlsafe_dumps(obj, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def jsonify(*args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
|
"""Serialize the given arguments as JSON, and return a
|
||||||
|
:class:`~flask.Response` object with the ``application/json``
|
||||||
|
mimetype. A dict or list returned from a view will be converted to a
|
||||||
|
JSON response automatically without needing to call this.
|
||||||
|
|
||||||
|
This requires an active request or application context, and calls
|
||||||
|
:meth:`app.json.response() <flask.json.provider.JSONProvider.response>`.
|
||||||
|
|
||||||
|
In debug mode, the output is formatted with indentation to make it
|
||||||
|
easier to read. This may also be controlled by the provider.
|
||||||
|
|
||||||
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.json.response``, allowing an app to override
|
||||||
|
the behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.2
|
||||||
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.11
|
||||||
|
Added support for serializing top-level arrays. This was a
|
||||||
|
security risk in ancient browsers. See :ref:`security-json`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.2
|
||||||
|
"""
|
||||||
|
return current_app.json.response(*args, **kwargs)
|
@ -0,0 +1,310 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import decimal
|
||||||
|
import json
|
||||||
|
import typing as t
|
||||||
|
import uuid
|
||||||
|
import weakref
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from werkzeug.http import http_date
|
||||||
|
|
||||||
|
from ..globals import request
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from ..app import Flask
|
||||||
|
from ..wrappers import Response
|
||||||
|
|
||||||
|
|
||||||
|
class JSONProvider:
|
||||||
|
"""A standard set of JSON operations for an application. Subclasses
|
||||||
|
of this can be used to customize JSON behavior or use different
|
||||||
|
JSON libraries.
|
||||||
|
|
||||||
|
To implement a provider for a specific library, subclass this base
|
||||||
|
class and implement at least :meth:`dumps` and :meth:`loads`. All
|
||||||
|
other methods have default implementations.
|
||||||
|
|
||||||
|
To use a different provider, either subclass ``Flask`` and set
|
||||||
|
:attr:`~flask.Flask.json_provider_class` to a provider class, or set
|
||||||
|
:attr:`app.json <flask.Flask.json>` to an instance of the class.
|
||||||
|
|
||||||
|
:param app: An application instance. This will be stored as a
|
||||||
|
:class:`weakref.proxy` on the :attr:`_app` attribute.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: Flask) -> None:
|
||||||
|
self._app = weakref.proxy(app)
|
||||||
|
|
||||||
|
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize data as JSON.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
|
||||||
|
"""Serialize data as JSON and write to a file.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param fp: A file opened for writing text. Should use the UTF-8
|
||||||
|
encoding to be valid JSON.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
fp.write(self.dumps(obj, **kwargs))
|
||||||
|
|
||||||
|
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON.
|
||||||
|
|
||||||
|
:param s: Text or UTF-8 bytes.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON read from a file.
|
||||||
|
|
||||||
|
:param fp: A file opened for reading text or UTF-8 bytes.
|
||||||
|
:param kwargs: May be passed to the underlying JSON library.
|
||||||
|
"""
|
||||||
|
return self.loads(fp.read(), **kwargs)
|
||||||
|
|
||||||
|
def _prepare_response_obj(
|
||||||
|
self, args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any]
|
||||||
|
) -> t.Any:
|
||||||
|
if args and kwargs:
|
||||||
|
raise TypeError("app.json.response() takes either args or kwargs, not both")
|
||||||
|
|
||||||
|
if not args and not kwargs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
return args or kwargs
|
||||||
|
|
||||||
|
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
|
"""Serialize the given arguments as JSON, and return a
|
||||||
|
:class:`~flask.Response` object with the ``application/json``
|
||||||
|
mimetype.
|
||||||
|
|
||||||
|
The :func:`~flask.json.jsonify` function calls this method for
|
||||||
|
the current application.
|
||||||
|
|
||||||
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
"""
|
||||||
|
obj = self._prepare_response_obj(args, kwargs)
|
||||||
|
return self._app.response_class(self.dumps(obj), mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def _default(o: t.Any) -> t.Any:
|
||||||
|
if isinstance(o, date):
|
||||||
|
return http_date(o)
|
||||||
|
|
||||||
|
if isinstance(o, (decimal.Decimal, uuid.UUID)):
|
||||||
|
return str(o)
|
||||||
|
|
||||||
|
if dataclasses and dataclasses.is_dataclass(o):
|
||||||
|
return dataclasses.asdict(o)
|
||||||
|
|
||||||
|
if hasattr(o, "__html__"):
|
||||||
|
return str(o.__html__())
|
||||||
|
|
||||||
|
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultJSONProvider(JSONProvider):
|
||||||
|
"""Provide JSON operations using Python's built-in :mod:`json`
|
||||||
|
library. Serializes the following additional data types:
|
||||||
|
|
||||||
|
- :class:`datetime.datetime` and :class:`datetime.date` are
|
||||||
|
serialized to :rfc:`822` strings. This is the same as the HTTP
|
||||||
|
date format.
|
||||||
|
- :class:`uuid.UUID` is serialized to a string.
|
||||||
|
- :class:`dataclasses.dataclass` is passed to
|
||||||
|
:func:`dataclasses.asdict`.
|
||||||
|
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
|
||||||
|
method) will call the ``__html__`` method to get a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default: t.Callable[[t.Any], t.Any] = staticmethod(
|
||||||
|
_default
|
||||||
|
) # type: ignore[assignment]
|
||||||
|
"""Apply this function to any object that :meth:`json.dumps` does
|
||||||
|
not know how to serialize. It should return a valid JSON type or
|
||||||
|
raise a ``TypeError``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ensure_ascii = True
|
||||||
|
"""Replace non-ASCII characters with escape sequences. This may be
|
||||||
|
more compatible with some clients, but can be disabled for better
|
||||||
|
performance and size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sort_keys = True
|
||||||
|
"""Sort the keys in any serialized dicts. This may be useful for
|
||||||
|
some caching situations, but can be disabled for better performance.
|
||||||
|
When enabled, keys must all be strings, they are not converted
|
||||||
|
before sorting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
compact: bool | None = None
|
||||||
|
"""If ``True``, or ``None`` out of debug mode, the :meth:`response`
|
||||||
|
output will not add indentation, newlines, or spaces. If ``False``,
|
||||||
|
or ``None`` in debug mode, it will use a non-compact representation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mimetype = "application/json"
|
||||||
|
"""The mimetype set in :meth:`response`."""
|
||||||
|
|
||||||
|
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
|
||||||
|
"""Serialize data as JSON to a string.
|
||||||
|
|
||||||
|
Keyword arguments are passed to :func:`json.dumps`. Sets some
|
||||||
|
parameter defaults from the :attr:`default`,
|
||||||
|
:attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
|
||||||
|
|
||||||
|
:param obj: The data to serialize.
|
||||||
|
:param kwargs: Passed to :func:`json.dumps`.
|
||||||
|
"""
|
||||||
|
cls = self._app._json_encoder
|
||||||
|
bp = self._app.blueprints.get(request.blueprint) if request else None
|
||||||
|
|
||||||
|
if bp is not None and bp._json_encoder is not None:
|
||||||
|
cls = bp._json_encoder
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setting 'json_encoder' on the app or a blueprint is"
|
||||||
|
" deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
|
if "default" not in cls.__dict__:
|
||||||
|
kwargs.setdefault("default", self.default)
|
||||||
|
else:
|
||||||
|
kwargs.setdefault("default", self.default)
|
||||||
|
|
||||||
|
ensure_ascii = self._app.config["JSON_AS_ASCII"]
|
||||||
|
sort_keys = self._app.config["JSON_SORT_KEYS"]
|
||||||
|
|
||||||
|
if ensure_ascii is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSON_AS_ASCII' config key is deprecated and will"
|
||||||
|
" be removed in Flask 2.3. Set 'app.json.ensure_ascii'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ensure_ascii = self.ensure_ascii
|
||||||
|
|
||||||
|
if sort_keys is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSON_SORT_KEYS' config key is deprecated and will"
|
||||||
|
" be removed in Flask 2.3. Set 'app.json.sort_keys'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sort_keys = self.sort_keys
|
||||||
|
|
||||||
|
kwargs.setdefault("ensure_ascii", ensure_ascii)
|
||||||
|
kwargs.setdefault("sort_keys", sort_keys)
|
||||||
|
return json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Deserialize data as JSON from a string or bytes.
|
||||||
|
|
||||||
|
:param s: Text or UTF-8 bytes.
|
||||||
|
:param kwargs: Passed to :func:`json.loads`.
|
||||||
|
"""
|
||||||
|
cls = self._app._json_decoder
|
||||||
|
bp = self._app.blueprints.get(request.blueprint) if request else None
|
||||||
|
|
||||||
|
if bp is not None and bp._json_decoder is not None:
|
||||||
|
cls = bp._json_decoder
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setting 'json_decoder' on the app or a blueprint is"
|
||||||
|
" deprecated and will be removed in Flask 2.3."
|
||||||
|
" Customize 'app.json' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
|
return json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
|
||||||
|
"""Serialize the given arguments as JSON, and return a
|
||||||
|
:class:`~flask.Response` object with it. The response mimetype
|
||||||
|
will be "application/json" and can be changed with
|
||||||
|
:attr:`mimetype`.
|
||||||
|
|
||||||
|
If :attr:`compact` is ``False`` or debug mode is enabled, the
|
||||||
|
output will be formatted to be easier to read.
|
||||||
|
|
||||||
|
Either positional or keyword arguments can be given, not both.
|
||||||
|
If no arguments are given, ``None`` is serialized.
|
||||||
|
|
||||||
|
:param args: A single value to serialize, or multiple values to
|
||||||
|
treat as a list to serialize.
|
||||||
|
:param kwargs: Treat as a dict to serialize.
|
||||||
|
"""
|
||||||
|
obj = self._prepare_response_obj(args, kwargs)
|
||||||
|
dump_args: t.Dict[str, t.Any] = {}
|
||||||
|
pretty = self._app.config["JSONIFY_PRETTYPRINT_REGULAR"]
|
||||||
|
mimetype = self._app.config["JSONIFY_MIMETYPE"]
|
||||||
|
|
||||||
|
if pretty is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSONIFY_PRETTYPRINT_REGULAR' config key is"
|
||||||
|
" deprecated and will be removed in Flask 2.3. Set"
|
||||||
|
" 'app.json.compact' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
compact: bool | None = not pretty
|
||||||
|
else:
|
||||||
|
compact = self.compact
|
||||||
|
|
||||||
|
if (compact is None and self._app.debug) or compact is False:
|
||||||
|
dump_args.setdefault("indent", 2)
|
||||||
|
else:
|
||||||
|
dump_args.setdefault("separators", (",", ":"))
|
||||||
|
|
||||||
|
if mimetype is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The 'JSONIFY_MIMETYPE' config key is deprecated and"
|
||||||
|
" will be removed in Flask 2.3. Set 'app.json.mimetype'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mimetype = self.mimetype
|
||||||
|
|
||||||
|
return self._app.response_class(
|
||||||
|
f"{self.dumps(obj, **dump_args)}\n", mimetype=mimetype
|
||||||
|
)
|
@ -0,0 +1,312 @@
|
|||||||
|
"""
|
||||||
|
Tagged JSON
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
A compact representation for lossless serialization of non-standard JSON
|
||||||
|
types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this
|
||||||
|
to serialize the session data, but it may be useful in other places. It
|
||||||
|
can be extended to support other types.
|
||||||
|
|
||||||
|
.. autoclass:: TaggedJSONSerializer
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: JSONTag
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Let's see an example that adds support for
|
||||||
|
:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so
|
||||||
|
to handle this we will dump the items as a list of ``[key, value]``
|
||||||
|
pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to
|
||||||
|
identify the type. The session serializer processes dicts first, so
|
||||||
|
insert the new tag at the front of the order since ``OrderedDict`` must
|
||||||
|
be processed before ``dict``.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from flask.json.tag import JSONTag
|
||||||
|
|
||||||
|
class TagOrderedDict(JSONTag):
|
||||||
|
__slots__ = ('serializer',)
|
||||||
|
key = ' od'
|
||||||
|
|
||||||
|
def check(self, value):
|
||||||
|
return isinstance(value, OrderedDict)
|
||||||
|
|
||||||
|
def to_json(self, value):
|
||||||
|
return [[k, self.serializer.tag(v)] for k, v in iteritems(value)]
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return OrderedDict(value)
|
||||||
|
|
||||||
|
app.session_interface.serializer.register(TagOrderedDict, index=0)
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
from base64 import b64decode
|
||||||
|
from base64 import b64encode
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from markupsafe import Markup
|
||||||
|
from werkzeug.http import http_date
|
||||||
|
from werkzeug.http import parse_date
|
||||||
|
|
||||||
|
from ..json import dumps
|
||||||
|
from ..json import loads
|
||||||
|
|
||||||
|
|
||||||
|
class JSONTag:
|
||||||
|
"""Base class for defining type tags for :class:`TaggedJSONSerializer`."""
|
||||||
|
|
||||||
|
__slots__ = ("serializer",)
|
||||||
|
|
||||||
|
#: The tag to mark the serialized object with. If ``None``, this tag is
|
||||||
|
#: only used as an intermediate step during tagging.
|
||||||
|
key: t.Optional[str] = None
|
||||||
|
|
||||||
|
def __init__(self, serializer: "TaggedJSONSerializer") -> None:
|
||||||
|
"""Create a tagger for the given serializer."""
|
||||||
|
self.serializer = serializer
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
"""Check if the given value should be tagged by this tag."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
"""Convert the Python object to an object that is a valid JSON type.
|
||||||
|
The tag will be added later."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
"""Convert the JSON representation back to the correct type. The tag
|
||||||
|
will already be removed."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def tag(self, value: t.Any) -> t.Any:
|
||||||
|
"""Convert the value to a valid JSON type and add the tag structure
|
||||||
|
around it."""
|
||||||
|
return {self.key: self.to_json(value)}
|
||||||
|
|
||||||
|
|
||||||
|
class TagDict(JSONTag):
|
||||||
|
"""Tag for 1-item dicts whose only key matches a registered tag.
|
||||||
|
|
||||||
|
Internally, the dict key is suffixed with `__`, and the suffix is removed
|
||||||
|
when deserializing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
key = " di"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(value, dict)
|
||||||
|
and len(value) == 1
|
||||||
|
and next(iter(value)) in self.serializer.tags
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
key = next(iter(value))
|
||||||
|
return {f"{key}__": self.serializer.tag(value[key])}
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
key = next(iter(value))
|
||||||
|
return {key[:-2]: value[key]}
|
||||||
|
|
||||||
|
|
||||||
|
class PassDict(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, dict)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
# JSON objects may only have string keys, so don't bother tagging the
|
||||||
|
# key here.
|
||||||
|
return {k: self.serializer.tag(v) for k, v in value.items()}
|
||||||
|
|
||||||
|
tag = to_json
|
||||||
|
|
||||||
|
|
||||||
|
class TagTuple(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = " t"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, tuple)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return [self.serializer.tag(item) for item in value]
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
return tuple(value)
|
||||||
|
|
||||||
|
|
||||||
|
class PassList(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, list)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return [self.serializer.tag(item) for item in value]
|
||||||
|
|
||||||
|
tag = to_json
|
||||||
|
|
||||||
|
|
||||||
|
class TagBytes(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = " b"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, bytes)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return b64encode(value).decode("ascii")
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
return b64decode(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagMarkup(JSONTag):
|
||||||
|
"""Serialize anything matching the :class:`~markupsafe.Markup` API by
|
||||||
|
having a ``__html__`` method to the result of that method. Always
|
||||||
|
deserializes to an instance of :class:`~markupsafe.Markup`."""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
key = " m"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return callable(getattr(value, "__html__", None))
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return str(value.__html__())
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
return Markup(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagUUID(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = " u"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, UUID)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return value.hex
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
return UUID(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagDateTime(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = " d"
|
||||||
|
|
||||||
|
def check(self, value: t.Any) -> bool:
|
||||||
|
return isinstance(value, datetime)
|
||||||
|
|
||||||
|
def to_json(self, value: t.Any) -> t.Any:
|
||||||
|
return http_date(value)
|
||||||
|
|
||||||
|
def to_python(self, value: t.Any) -> t.Any:
|
||||||
|
return parse_date(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedJSONSerializer:
|
||||||
|
"""Serializer that uses a tag system to compactly represent objects that
|
||||||
|
are not JSON types. Passed as the intermediate serializer to
|
||||||
|
:class:`itsdangerous.Serializer`.
|
||||||
|
|
||||||
|
The following extra types are supported:
|
||||||
|
|
||||||
|
* :class:`dict`
|
||||||
|
* :class:`tuple`
|
||||||
|
* :class:`bytes`
|
||||||
|
* :class:`~markupsafe.Markup`
|
||||||
|
* :class:`~uuid.UUID`
|
||||||
|
* :class:`~datetime.datetime`
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("tags", "order")
|
||||||
|
|
||||||
|
#: Tag classes to bind when creating the serializer. Other tags can be
|
||||||
|
#: added later using :meth:`~register`.
|
||||||
|
default_tags = [
|
||||||
|
TagDict,
|
||||||
|
PassDict,
|
||||||
|
TagTuple,
|
||||||
|
PassList,
|
||||||
|
TagBytes,
|
||||||
|
TagMarkup,
|
||||||
|
TagUUID,
|
||||||
|
TagDateTime,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.tags: t.Dict[str, JSONTag] = {}
|
||||||
|
self.order: t.List[JSONTag] = []
|
||||||
|
|
||||||
|
for cls in self.default_tags:
|
||||||
|
self.register(cls)
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
tag_class: t.Type[JSONTag],
|
||||||
|
force: bool = False,
|
||||||
|
index: t.Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Register a new tag with this serializer.
|
||||||
|
|
||||||
|
:param tag_class: tag class to register. Will be instantiated with this
|
||||||
|
serializer instance.
|
||||||
|
:param force: overwrite an existing tag. If false (default), a
|
||||||
|
:exc:`KeyError` is raised.
|
||||||
|
:param index: index to insert the new tag in the tag order. Useful when
|
||||||
|
the new tag is a special case of an existing tag. If ``None``
|
||||||
|
(default), the tag is appended to the end of the order.
|
||||||
|
|
||||||
|
:raise KeyError: if the tag key is already registered and ``force`` is
|
||||||
|
not true.
|
||||||
|
"""
|
||||||
|
tag = tag_class(self)
|
||||||
|
key = tag.key
|
||||||
|
|
||||||
|
if key is not None:
|
||||||
|
if not force and key in self.tags:
|
||||||
|
raise KeyError(f"Tag '{key}' is already registered.")
|
||||||
|
|
||||||
|
self.tags[key] = tag
|
||||||
|
|
||||||
|
if index is None:
|
||||||
|
self.order.append(tag)
|
||||||
|
else:
|
||||||
|
self.order.insert(index, tag)
|
||||||
|
|
||||||
|
def tag(self, value: t.Any) -> t.Dict[str, t.Any]:
|
||||||
|
"""Convert a value to a tagged representation if necessary."""
|
||||||
|
for tag in self.order:
|
||||||
|
if tag.check(value):
|
||||||
|
return tag.tag(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def untag(self, value: t.Dict[str, t.Any]) -> t.Any:
|
||||||
|
"""Convert a tagged representation back to the original type."""
|
||||||
|
if len(value) != 1:
|
||||||
|
return value
|
||||||
|
|
||||||
|
key = next(iter(value))
|
||||||
|
|
||||||
|
if key not in self.tags:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return self.tags[key].to_python(value[key])
|
||||||
|
|
||||||
|
def dumps(self, value: t.Any) -> str:
|
||||||
|
"""Tag the value and dump it to a compact JSON string."""
|
||||||
|
return dumps(self.tag(value), separators=(",", ":"))
|
||||||
|
|
||||||
|
def loads(self, value: str) -> t.Any:
|
||||||
|
"""Load data from a JSON string and deserialized any tagged objects."""
|
||||||
|
return loads(value, object_hook=self.untag)
|
@ -0,0 +1,74 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .globals import request
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .app import Flask
|
||||||
|
|
||||||
|
|
||||||
|
@LocalProxy
|
||||||
|
def wsgi_errors_stream() -> t.TextIO:
|
||||||
|
"""Find the most appropriate error stream for the application. If a request
|
||||||
|
is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``.
|
||||||
|
|
||||||
|
If you configure your own :class:`logging.StreamHandler`, you may want to
|
||||||
|
use this for the stream. If you are using file or dict configuration and
|
||||||
|
can't import this directly, you can refer to it as
|
||||||
|
``ext://flask.logging.wsgi_errors_stream``.
|
||||||
|
"""
|
||||||
|
return request.environ["wsgi.errors"] if request else sys.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def has_level_handler(logger: logging.Logger) -> bool:
|
||||||
|
"""Check if there is a handler in the logging chain that will handle the
|
||||||
|
given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`.
|
||||||
|
"""
|
||||||
|
level = logger.getEffectiveLevel()
|
||||||
|
current = logger
|
||||||
|
|
||||||
|
while current:
|
||||||
|
if any(handler.level <= level for handler in current.handlers):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not current.propagate:
|
||||||
|
break
|
||||||
|
|
||||||
|
current = current.parent # type: ignore
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format
|
||||||
|
#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``.
|
||||||
|
default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore
|
||||||
|
default_handler.setFormatter(
|
||||||
|
logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_logger(app: "Flask") -> logging.Logger:
|
||||||
|
"""Get the Flask app's logger and configure it if needed.
|
||||||
|
|
||||||
|
The logger name will be the same as
|
||||||
|
:attr:`app.import_name <flask.Flask.name>`.
|
||||||
|
|
||||||
|
When :attr:`~flask.Flask.debug` is enabled, set the logger level to
|
||||||
|
:data:`logging.DEBUG` if it is not set.
|
||||||
|
|
||||||
|
If there is no handler for the logger's effective level, add a
|
||||||
|
:class:`~logging.StreamHandler` for
|
||||||
|
:func:`~flask.logging.wsgi_errors_stream` with a basic format.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger(app.name)
|
||||||
|
|
||||||
|
if app.debug and not logger.level:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
if not has_level_handler(logger):
|
||||||
|
logger.addHandler(default_handler)
|
||||||
|
|
||||||
|
return logger
|
@ -0,0 +1,898 @@
|
|||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
from jinja2 import FileSystemLoader
|
||||||
|
from werkzeug.exceptions import default_exceptions
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
|
from .cli import AppGroup
|
||||||
|
from .globals import current_app
|
||||||
|
from .helpers import get_root_path
|
||||||
|
from .helpers import locked_cached_property
|
||||||
|
from .helpers import send_from_directory
|
||||||
|
from .templating import _default_template_ctx_processor
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .wrappers import Response
|
||||||
|
|
||||||
|
# a singleton sentinel value for parameter defaults
|
||||||
|
_sentinel = object()
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
|
||||||
|
T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
|
||||||
|
T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
|
||||||
|
T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
|
||||||
|
T_template_context_processor = t.TypeVar(
|
||||||
|
"T_template_context_processor", bound=ft.TemplateContextProcessorCallable
|
||||||
|
)
|
||||||
|
T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
|
||||||
|
T_url_value_preprocessor = t.TypeVar(
|
||||||
|
"T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
|
||||||
|
)
|
||||||
|
T_route = t.TypeVar("T_route", bound=ft.RouteCallable)
|
||||||
|
|
||||||
|
|
||||||
|
def setupmethod(f: F) -> F:
|
||||||
|
f_name = f.__name__
|
||||||
|
|
||||||
|
def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
self._check_setup_finished(f_name)
|
||||||
|
return f(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return t.cast(F, update_wrapper(wrapper_func, f))
|
||||||
|
|
||||||
|
|
||||||
|
class Scaffold:
|
||||||
|
"""Common behavior shared between :class:`~flask.Flask` and
|
||||||
|
:class:`~flask.blueprints.Blueprint`.
|
||||||
|
|
||||||
|
:param import_name: The import name of the module where this object
|
||||||
|
is defined. Usually :attr:`__name__` should be used.
|
||||||
|
:param static_folder: Path to a folder of static files to serve.
|
||||||
|
If this is set, a static route will be added.
|
||||||
|
:param static_url_path: URL prefix for the static route.
|
||||||
|
:param template_folder: Path to a folder containing template files.
|
||||||
|
for rendering. If this is set, a Jinja loader will be added.
|
||||||
|
:param root_path: The path that static, template, and resource files
|
||||||
|
are relative to. Typically not set, it is discovered based on
|
||||||
|
the ``import_name``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
_static_folder: t.Optional[str] = None
|
||||||
|
_static_url_path: t.Optional[str] = None
|
||||||
|
|
||||||
|
#: JSON encoder class used by :func:`flask.json.dumps`. If a
|
||||||
|
#: blueprint sets this, it will be used instead of the app's value.
|
||||||
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None
|
||||||
|
|
||||||
|
#: JSON decoder class used by :func:`flask.json.loads`. If a
|
||||||
|
#: blueprint sets this, it will be used instead of the app's value.
|
||||||
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3.
|
||||||
|
json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
import_name: str,
|
||||||
|
static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
|
||||||
|
static_url_path: t.Optional[str] = None,
|
||||||
|
template_folder: t.Optional[str] = None,
|
||||||
|
root_path: t.Optional[str] = None,
|
||||||
|
):
|
||||||
|
#: The name of the package or module that this object belongs
|
||||||
|
#: to. Do not change this once it is set by the constructor.
|
||||||
|
self.import_name = import_name
|
||||||
|
|
||||||
|
self.static_folder = static_folder # type: ignore
|
||||||
|
self.static_url_path = static_url_path
|
||||||
|
|
||||||
|
#: The path to the templates folder, relative to
|
||||||
|
#: :attr:`root_path`, to add to the template loader. ``None`` if
|
||||||
|
#: templates should not be added.
|
||||||
|
self.template_folder = template_folder
|
||||||
|
|
||||||
|
if root_path is None:
|
||||||
|
root_path = get_root_path(self.import_name)
|
||||||
|
|
||||||
|
#: Absolute path to the package on the filesystem. Used to look
|
||||||
|
#: up resources contained in the package.
|
||||||
|
self.root_path = root_path
|
||||||
|
|
||||||
|
#: The Click command group for registering CLI commands for this
|
||||||
|
#: object. The commands are available from the ``flask`` command
|
||||||
|
#: once the application has been discovered and blueprints have
|
||||||
|
#: been registered.
|
||||||
|
self.cli = AppGroup()
|
||||||
|
|
||||||
|
#: A dictionary mapping endpoint names to view functions.
|
||||||
|
#:
|
||||||
|
#: To register a view function, use the :meth:`route` decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.view_functions: t.Dict[str, t.Callable] = {}
|
||||||
|
|
||||||
|
#: A data structure of registered error handlers, in the format
|
||||||
|
#: ``{scope: {code: {class: handler}}}``. The ``scope`` key is
|
||||||
|
#: the name of a blueprint the handlers are active for, or
|
||||||
|
#: ``None`` for all requests. The ``code`` key is the HTTP
|
||||||
|
#: status code for ``HTTPException``, or ``None`` for
|
||||||
|
#: other exceptions. The innermost dictionary maps exception
|
||||||
|
#: classes to handler functions.
|
||||||
|
#:
|
||||||
|
#: To register an error handler, use the :meth:`errorhandler`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.error_handler_spec: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey,
|
||||||
|
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ft.ErrorHandlerCallable]],
|
||||||
|
] = defaultdict(lambda: defaultdict(dict))
|
||||||
|
|
||||||
|
#: A data structure of functions to call at the beginning of
|
||||||
|
#: each request, in the format ``{scope: [functions]}``. The
|
||||||
|
#: ``scope`` key is the name of a blueprint the functions are
|
||||||
|
#: active for, or ``None`` for all requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the :meth:`before_request`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.before_request_funcs: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey, t.List[ft.BeforeRequestCallable]
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
#: A data structure of functions to call at the end of each
|
||||||
|
#: request, in the format ``{scope: [functions]}``. The
|
||||||
|
#: ``scope`` key is the name of a blueprint the functions are
|
||||||
|
#: active for, or ``None`` for all requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the :meth:`after_request`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.after_request_funcs: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey, t.List[ft.AfterRequestCallable]
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
#: A data structure of functions to call at the end of each
|
||||||
|
#: request even if an exception is raised, in the format
|
||||||
|
#: ``{scope: [functions]}``. The ``scope`` key is the name of a
|
||||||
|
#: blueprint the functions are active for, or ``None`` for all
|
||||||
|
#: requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the :meth:`teardown_request`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.teardown_request_funcs: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey, t.List[ft.TeardownCallable]
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
#: A data structure of functions to call to pass extra context
|
||||||
|
#: values when rendering templates, in the format
|
||||||
|
#: ``{scope: [functions]}``. The ``scope`` key is the name of a
|
||||||
|
#: blueprint the functions are active for, or ``None`` for all
|
||||||
|
#: requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the :meth:`context_processor`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.template_context_processors: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey, t.List[ft.TemplateContextProcessorCallable]
|
||||||
|
] = defaultdict(list, {None: [_default_template_ctx_processor]})
|
||||||
|
|
||||||
|
#: A data structure of functions to call to modify the keyword
|
||||||
|
#: arguments passed to the view function, in the format
|
||||||
|
#: ``{scope: [functions]}``. The ``scope`` key is the name of a
|
||||||
|
#: blueprint the functions are active for, or ``None`` for all
|
||||||
|
#: requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the
|
||||||
|
#: :meth:`url_value_preprocessor` decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.url_value_preprocessors: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey,
|
||||||
|
t.List[ft.URLValuePreprocessorCallable],
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
#: A data structure of functions to call to modify the keyword
|
||||||
|
#: arguments when generating URLs, in the format
|
||||||
|
#: ``{scope: [functions]}``. The ``scope`` key is the name of a
|
||||||
|
#: blueprint the functions are active for, or ``None`` for all
|
||||||
|
#: requests.
|
||||||
|
#:
|
||||||
|
#: To register a function, use the :meth:`url_defaults`
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: This data structure is internal. It should not be modified
|
||||||
|
#: directly and its format may change at any time.
|
||||||
|
self.url_default_functions: t.Dict[
|
||||||
|
ft.AppOrBlueprintKey, t.List[ft.URLDefaultCallable]
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{type(self).__name__} {self.name!r}>"
|
||||||
|
|
||||||
|
def _check_setup_finished(self, f_name: str) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def static_folder(self) -> t.Optional[str]:
|
||||||
|
"""The absolute path to the configured static folder. ``None``
|
||||||
|
if no static folder is set.
|
||||||
|
"""
|
||||||
|
if self._static_folder is not None:
|
||||||
|
return os.path.join(self.root_path, self._static_folder)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@static_folder.setter
|
||||||
|
def static_folder(self, value: t.Optional[t.Union[str, os.PathLike]]) -> None:
|
||||||
|
if value is not None:
|
||||||
|
value = os.fspath(value).rstrip(r"\/")
|
||||||
|
|
||||||
|
self._static_folder = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_static_folder(self) -> bool:
|
||||||
|
"""``True`` if :attr:`static_folder` is set.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
"""
|
||||||
|
return self.static_folder is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def static_url_path(self) -> t.Optional[str]:
|
||||||
|
"""The URL prefix that the static route will be accessible from.
|
||||||
|
|
||||||
|
If it was not configured during init, it is derived from
|
||||||
|
:attr:`static_folder`.
|
||||||
|
"""
|
||||||
|
if self._static_url_path is not None:
|
||||||
|
return self._static_url_path
|
||||||
|
|
||||||
|
if self.static_folder is not None:
|
||||||
|
basename = os.path.basename(self.static_folder)
|
||||||
|
return f"/{basename}".rstrip("/")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@static_url_path.setter
|
||||||
|
def static_url_path(self, value: t.Optional[str]) -> None:
|
||||||
|
if value is not None:
|
||||||
|
value = value.rstrip("/")
|
||||||
|
|
||||||
|
self._static_url_path = value
|
||||||
|
|
||||||
|
def get_send_file_max_age(self, filename: t.Optional[str]) -> t.Optional[int]:
|
||||||
|
"""Used by :func:`send_file` to determine the ``max_age`` cache
|
||||||
|
value for a given file path if it wasn't passed.
|
||||||
|
|
||||||
|
By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
|
||||||
|
the configuration of :data:`~flask.current_app`. This defaults
|
||||||
|
to ``None``, which tells the browser to use conditional requests
|
||||||
|
instead of a timed cache, which is usually preferable.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
The default configuration is ``None`` instead of 12 hours.
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
"""
|
||||||
|
value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(value, timedelta):
|
||||||
|
return int(value.total_seconds())
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def send_static_file(self, filename: str) -> "Response":
|
||||||
|
"""The view function used to serve files from
|
||||||
|
:attr:`static_folder`. A route is automatically registered for
|
||||||
|
this view at :attr:`static_url_path` if :attr:`static_folder` is
|
||||||
|
set.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
"""
|
||||||
|
if not self.has_static_folder:
|
||||||
|
raise RuntimeError("'static_folder' must be set to serve static_files.")
|
||||||
|
|
||||||
|
# send_file only knows to call get_send_file_max_age on the app,
|
||||||
|
# call it here so it works for blueprints too.
|
||||||
|
max_age = self.get_send_file_max_age(filename)
|
||||||
|
return send_from_directory(
|
||||||
|
t.cast(str, self.static_folder), filename, max_age=max_age
|
||||||
|
)
|
||||||
|
|
||||||
|
@locked_cached_property
|
||||||
|
def jinja_loader(self) -> t.Optional[FileSystemLoader]:
|
||||||
|
"""The Jinja loader for this object's templates. By default this
|
||||||
|
is a class :class:`jinja2.loaders.FileSystemLoader` to
|
||||||
|
:attr:`template_folder` if it is set.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
"""
|
||||||
|
if self.template_folder is not None:
|
||||||
|
return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
|
||||||
|
"""Open a resource file relative to :attr:`root_path` for
|
||||||
|
reading.
|
||||||
|
|
||||||
|
For example, if the file ``schema.sql`` is next to the file
|
||||||
|
``app.py`` where the ``Flask`` app is defined, it can be opened
|
||||||
|
with:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with app.open_resource("schema.sql") as f:
|
||||||
|
conn.executescript(f.read())
|
||||||
|
|
||||||
|
:param resource: Path to the resource relative to
|
||||||
|
:attr:`root_path`.
|
||||||
|
:param mode: Open the file in this mode. Only reading is
|
||||||
|
supported, valid values are "r" (or "rt") and "rb".
|
||||||
|
"""
|
||||||
|
if mode not in {"r", "rt", "rb"}:
|
||||||
|
raise ValueError("Resources can only be opened for reading.")
|
||||||
|
|
||||||
|
return open(os.path.join(self.root_path, resource), mode)
|
||||||
|
|
||||||
|
def _method_route(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
rule: str,
|
||||||
|
options: dict,
|
||||||
|
) -> t.Callable[[T_route], T_route]:
|
||||||
|
if "methods" in options:
|
||||||
|
raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
|
||||||
|
|
||||||
|
return self.route(rule, methods=[method], **options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Shortcut for :meth:`route` with ``methods=["GET"]``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self._method_route("GET", rule, options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Shortcut for :meth:`route` with ``methods=["POST"]``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self._method_route("POST", rule, options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Shortcut for :meth:`route` with ``methods=["PUT"]``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self._method_route("PUT", rule, options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Shortcut for :meth:`route` with ``methods=["DELETE"]``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self._method_route("DELETE", rule, options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Shortcut for :meth:`route` with ``methods=["PATCH"]``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
return self._method_route("PATCH", rule, options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
|
||||||
|
"""Decorate a view function to register it with the given URL
|
||||||
|
rule and options. Calls :meth:`add_url_rule`, which has more
|
||||||
|
details about the implementation.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return "Hello, World!"
|
||||||
|
|
||||||
|
See :ref:`url-route-registrations`.
|
||||||
|
|
||||||
|
The endpoint name for the route defaults to the name of the view
|
||||||
|
function if the ``endpoint`` parameter isn't passed.
|
||||||
|
|
||||||
|
The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and
|
||||||
|
``OPTIONS`` are added automatically.
|
||||||
|
|
||||||
|
:param rule: The URL rule string.
|
||||||
|
:param options: Extra options passed to the
|
||||||
|
:class:`~werkzeug.routing.Rule` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_route) -> T_route:
|
||||||
|
endpoint = options.pop("endpoint", None)
|
||||||
|
self.add_url_rule(rule, endpoint, f, **options)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def add_url_rule(
|
||||||
|
self,
|
||||||
|
rule: str,
|
||||||
|
endpoint: t.Optional[str] = None,
|
||||||
|
view_func: t.Optional[ft.RouteCallable] = None,
|
||||||
|
provide_automatic_options: t.Optional[bool] = None,
|
||||||
|
**options: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""Register a rule for routing incoming requests and building
|
||||||
|
URLs. The :meth:`route` decorator is a shortcut to call this
|
||||||
|
with the ``view_func`` argument. These are equivalent:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
...
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def index():
|
||||||
|
...
|
||||||
|
|
||||||
|
app.add_url_rule("/", view_func=index)
|
||||||
|
|
||||||
|
See :ref:`url-route-registrations`.
|
||||||
|
|
||||||
|
The endpoint name for the route defaults to the name of the view
|
||||||
|
function if the ``endpoint`` parameter isn't passed. An error
|
||||||
|
will be raised if a function has already been registered for the
|
||||||
|
endpoint.
|
||||||
|
|
||||||
|
The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is
|
||||||
|
always added automatically, and ``OPTIONS`` is added
|
||||||
|
automatically by default.
|
||||||
|
|
||||||
|
``view_func`` does not necessarily need to be passed, but if the
|
||||||
|
rule should participate in routing an endpoint name must be
|
||||||
|
associated with a view function at some point with the
|
||||||
|
:meth:`endpoint` decorator.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|
||||||
|
@app.endpoint("index")
|
||||||
|
def index():
|
||||||
|
...
|
||||||
|
|
||||||
|
If ``view_func`` has a ``required_methods`` attribute, those
|
||||||
|
methods are added to the passed and automatic methods. If it
|
||||||
|
has a ``provide_automatic_methods`` attribute, it is used as the
|
||||||
|
default if the parameter is not passed.
|
||||||
|
|
||||||
|
:param rule: The URL rule string.
|
||||||
|
:param endpoint: The endpoint name to associate with the rule
|
||||||
|
and view function. Used when routing and building URLs.
|
||||||
|
Defaults to ``view_func.__name__``.
|
||||||
|
:param view_func: The view function to associate with the
|
||||||
|
endpoint name.
|
||||||
|
:param provide_automatic_options: Add the ``OPTIONS`` method and
|
||||||
|
respond to ``OPTIONS`` requests automatically.
|
||||||
|
:param options: Extra options passed to the
|
||||||
|
:class:`~werkzeug.routing.Rule` object.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def endpoint(self, endpoint: str) -> t.Callable[[F], F]:
|
||||||
|
"""Decorate a view function to register it for the given
|
||||||
|
endpoint. Used if a rule is added without a ``view_func`` with
|
||||||
|
:meth:`add_url_rule`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.add_url_rule("/ex", endpoint="example")
|
||||||
|
|
||||||
|
@app.endpoint("example")
|
||||||
|
def example():
|
||||||
|
...
|
||||||
|
|
||||||
|
:param endpoint: The endpoint name to associate with the view
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: F) -> F:
|
||||||
|
self.view_functions[endpoint] = f
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def before_request(self, f: T_before_request) -> T_before_request:
|
||||||
|
"""Register a function to run before each request.
|
||||||
|
|
||||||
|
For example, this can be used to open a database connection, or
|
||||||
|
to load the logged in user from the session.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def load_user():
|
||||||
|
if "user_id" in session:
|
||||||
|
g.user = db.session.get(session["user_id"])
|
||||||
|
|
||||||
|
The function will be called without any arguments. If it returns
|
||||||
|
a non-``None`` value, the value is handled as if it was the
|
||||||
|
return value from the view, and further request handling is
|
||||||
|
stopped.
|
||||||
|
"""
|
||||||
|
self.before_request_funcs.setdefault(None, []).append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def after_request(self, f: T_after_request) -> T_after_request:
|
||||||
|
"""Register a function to run after each request to this object.
|
||||||
|
|
||||||
|
The function is called with the response object, and must return
|
||||||
|
a response object. This allows the functions to modify or
|
||||||
|
replace the response before it is sent.
|
||||||
|
|
||||||
|
If a function raises an exception, any remaining
|
||||||
|
``after_request`` functions will not be called. Therefore, this
|
||||||
|
should not be used for actions that must execute, such as to
|
||||||
|
close resources. Use :meth:`teardown_request` for that.
|
||||||
|
"""
|
||||||
|
self.after_request_funcs.setdefault(None, []).append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def teardown_request(self, f: T_teardown) -> T_teardown:
|
||||||
|
"""Register a function to be called when the request context is
|
||||||
|
popped. Typically this happens at the end of each request, but
|
||||||
|
contexts may be pushed manually as well during testing.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with app.test_request_context():
|
||||||
|
...
|
||||||
|
|
||||||
|
When the ``with`` block exits (or ``ctx.pop()`` is called), the
|
||||||
|
teardown functions are called just before the request context is
|
||||||
|
made inactive.
|
||||||
|
|
||||||
|
When a teardown function was called because of an unhandled
|
||||||
|
exception it will be passed an error object. If an
|
||||||
|
:meth:`errorhandler` is registered, it will handle the exception
|
||||||
|
and the teardown will not receive it.
|
||||||
|
|
||||||
|
Teardown functions must avoid raising exceptions. If they
|
||||||
|
execute code that might fail they must surround that code with a
|
||||||
|
``try``/``except`` block and log any errors.
|
||||||
|
|
||||||
|
The return values of teardown functions are ignored.
|
||||||
|
"""
|
||||||
|
self.teardown_request_funcs.setdefault(None, []).append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def context_processor(
|
||||||
|
self,
|
||||||
|
f: T_template_context_processor,
|
||||||
|
) -> T_template_context_processor:
|
||||||
|
"""Registers a template context processor function."""
|
||||||
|
self.template_context_processors[None].append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def url_value_preprocessor(
|
||||||
|
self,
|
||||||
|
f: T_url_value_preprocessor,
|
||||||
|
) -> T_url_value_preprocessor:
|
||||||
|
"""Register a URL value preprocessor function for all view
|
||||||
|
functions in the application. These functions will be called before the
|
||||||
|
:meth:`before_request` functions.
|
||||||
|
|
||||||
|
The function can modify the values captured from the matched url before
|
||||||
|
they are passed to the view. For example, this can be used to pop a
|
||||||
|
common language code value and place it in ``g`` rather than pass it to
|
||||||
|
every view.
|
||||||
|
|
||||||
|
The function is passed the endpoint name and values dict. The return
|
||||||
|
value is ignored.
|
||||||
|
"""
|
||||||
|
self.url_value_preprocessors[None].append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def url_defaults(self, f: T_url_defaults) -> T_url_defaults:
|
||||||
|
"""Callback function for URL defaults for all view functions of the
|
||||||
|
application. It's called with the endpoint and values and should
|
||||||
|
update the values passed in place.
|
||||||
|
"""
|
||||||
|
self.url_default_functions[None].append(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def errorhandler(
|
||||||
|
self, code_or_exception: t.Union[t.Type[Exception], int]
|
||||||
|
) -> t.Callable[[T_error_handler], T_error_handler]:
|
||||||
|
"""Register a function to handle errors by code or exception class.
|
||||||
|
|
||||||
|
A decorator that is used to register a function given an
|
||||||
|
error code. Example::
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(error):
|
||||||
|
return 'This page does not exist', 404
|
||||||
|
|
||||||
|
You can also register handlers for arbitrary exceptions::
|
||||||
|
|
||||||
|
@app.errorhandler(DatabaseError)
|
||||||
|
def special_exception_handler(error):
|
||||||
|
return 'Database connection failed', 500
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
Use :meth:`register_error_handler` instead of modifying
|
||||||
|
:attr:`error_handler_spec` directly, for application wide error
|
||||||
|
handlers.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
One can now additionally also register custom exception types
|
||||||
|
that do not necessarily have to be a subclass of the
|
||||||
|
:class:`~werkzeug.exceptions.HTTPException` class.
|
||||||
|
|
||||||
|
:param code_or_exception: the code as integer for the handler, or
|
||||||
|
an arbitrary exception
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: T_error_handler) -> T_error_handler:
|
||||||
|
self.register_error_handler(code_or_exception, f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
|
def register_error_handler(
|
||||||
|
self,
|
||||||
|
code_or_exception: t.Union[t.Type[Exception], int],
|
||||||
|
f: ft.ErrorHandlerCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Alternative error attach function to the :meth:`errorhandler`
|
||||||
|
decorator that is more straightforward to use for non decorator
|
||||||
|
usage.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
"""
|
||||||
|
exc_class, code = self._get_exc_class_and_code(code_or_exception)
|
||||||
|
self.error_handler_spec[None][code][exc_class] = f
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_exc_class_and_code(
|
||||||
|
exc_class_or_code: t.Union[t.Type[Exception], int]
|
||||||
|
) -> t.Tuple[t.Type[Exception], t.Optional[int]]:
|
||||||
|
"""Get the exception class being handled. For HTTP status codes
|
||||||
|
or ``HTTPException`` subclasses, return both the exception and
|
||||||
|
status code.
|
||||||
|
|
||||||
|
:param exc_class_or_code: Any exception class, or an HTTP status
|
||||||
|
code as an integer.
|
||||||
|
"""
|
||||||
|
exc_class: t.Type[Exception]
|
||||||
|
|
||||||
|
if isinstance(exc_class_or_code, int):
|
||||||
|
try:
|
||||||
|
exc_class = default_exceptions[exc_class_or_code]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
f"'{exc_class_or_code}' is not a recognized HTTP"
|
||||||
|
" error code. Use a subclass of HTTPException with"
|
||||||
|
" that code instead."
|
||||||
|
) from None
|
||||||
|
else:
|
||||||
|
exc_class = exc_class_or_code
|
||||||
|
|
||||||
|
if isinstance(exc_class, Exception):
|
||||||
|
raise TypeError(
|
||||||
|
f"{exc_class!r} is an instance, not a class. Handlers"
|
||||||
|
" can only be registered for Exception classes or HTTP"
|
||||||
|
" error codes."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not issubclass(exc_class, Exception):
|
||||||
|
raise ValueError(
|
||||||
|
f"'{exc_class.__name__}' is not a subclass of Exception."
|
||||||
|
" Handlers can only be registered for Exception classes"
|
||||||
|
" or HTTP error codes."
|
||||||
|
)
|
||||||
|
|
||||||
|
if issubclass(exc_class, HTTPException):
|
||||||
|
return exc_class, exc_class.code
|
||||||
|
else:
|
||||||
|
return exc_class, None
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint_from_view_func(view_func: t.Callable) -> str:
|
||||||
|
"""Internal helper that returns the default endpoint for a given
|
||||||
|
function. This always is the function name.
|
||||||
|
"""
|
||||||
|
assert view_func is not None, "expected view func if endpoint is not provided."
|
||||||
|
return view_func.__name__
|
||||||
|
|
||||||
|
|
||||||
|
def _matching_loader_thinks_module_is_package(loader, mod_name):
|
||||||
|
"""Attempt to figure out if the given name is a package or a module.
|
||||||
|
|
||||||
|
:param: loader: The loader that handled the name.
|
||||||
|
:param mod_name: The name of the package or module.
|
||||||
|
"""
|
||||||
|
# Use loader.is_package if it's available.
|
||||||
|
if hasattr(loader, "is_package"):
|
||||||
|
return loader.is_package(mod_name)
|
||||||
|
|
||||||
|
cls = type(loader)
|
||||||
|
|
||||||
|
# NamespaceLoader doesn't implement is_package, but all names it
|
||||||
|
# loads must be packages.
|
||||||
|
if cls.__module__ == "_frozen_importlib" and cls.__name__ == "NamespaceLoader":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Otherwise we need to fail with an error that explains what went
|
||||||
|
# wrong.
|
||||||
|
raise AttributeError(
|
||||||
|
f"'{cls.__name__}.is_package()' must be implemented for PEP 302"
|
||||||
|
f" import hooks."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool:
|
||||||
|
# Path.is_relative_to doesn't exist until Python 3.9
|
||||||
|
try:
|
||||||
|
path.relative_to(base)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _find_package_path(import_name):
|
||||||
|
"""Find the path that contains the package or module."""
|
||||||
|
root_mod_name, _, _ = import_name.partition(".")
|
||||||
|
|
||||||
|
try:
|
||||||
|
root_spec = importlib.util.find_spec(root_mod_name)
|
||||||
|
|
||||||
|
if root_spec is None:
|
||||||
|
raise ValueError("not found")
|
||||||
|
# ImportError: the machinery told us it does not exist
|
||||||
|
# ValueError:
|
||||||
|
# - the module name was invalid
|
||||||
|
# - the module name is __main__
|
||||||
|
# - *we* raised `ValueError` due to `root_spec` being `None`
|
||||||
|
except (ImportError, ValueError):
|
||||||
|
pass # handled below
|
||||||
|
else:
|
||||||
|
# namespace package
|
||||||
|
if root_spec.origin in {"namespace", None}:
|
||||||
|
package_spec = importlib.util.find_spec(import_name)
|
||||||
|
if package_spec is not None and package_spec.submodule_search_locations:
|
||||||
|
# Pick the path in the namespace that contains the submodule.
|
||||||
|
package_path = pathlib.Path(
|
||||||
|
os.path.commonpath(package_spec.submodule_search_locations)
|
||||||
|
)
|
||||||
|
search_locations = (
|
||||||
|
location
|
||||||
|
for location in root_spec.submodule_search_locations
|
||||||
|
if _path_is_relative_to(package_path, location)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Pick the first path.
|
||||||
|
search_locations = iter(root_spec.submodule_search_locations)
|
||||||
|
return os.path.dirname(next(search_locations))
|
||||||
|
# a package (with __init__.py)
|
||||||
|
elif root_spec.submodule_search_locations:
|
||||||
|
return os.path.dirname(os.path.dirname(root_spec.origin))
|
||||||
|
# just a normal module
|
||||||
|
else:
|
||||||
|
return os.path.dirname(root_spec.origin)
|
||||||
|
|
||||||
|
# we were unable to find the `package_path` using PEP 451 loaders
|
||||||
|
loader = pkgutil.get_loader(root_mod_name)
|
||||||
|
|
||||||
|
if loader is None or root_mod_name == "__main__":
|
||||||
|
# import name is not found, or interactive/main module
|
||||||
|
return os.getcwd()
|
||||||
|
|
||||||
|
if hasattr(loader, "get_filename"):
|
||||||
|
filename = loader.get_filename(root_mod_name)
|
||||||
|
elif hasattr(loader, "archive"):
|
||||||
|
# zipimporter's loader.archive points to the .egg or .zip file.
|
||||||
|
filename = loader.archive
|
||||||
|
else:
|
||||||
|
# At least one loader is missing both get_filename and archive:
|
||||||
|
# Google App Engine's HardenedModulesHook, use __file__.
|
||||||
|
filename = importlib.import_module(root_mod_name).__file__
|
||||||
|
|
||||||
|
package_path = os.path.abspath(os.path.dirname(filename))
|
||||||
|
|
||||||
|
# If the imported name is a package, filename is currently pointing
|
||||||
|
# to the root of the package, need to get the current directory.
|
||||||
|
if _matching_loader_thinks_module_is_package(loader, root_mod_name):
|
||||||
|
package_path = os.path.dirname(package_path)
|
||||||
|
|
||||||
|
return package_path
|
||||||
|
|
||||||
|
|
||||||
|
def find_package(import_name: str):
|
||||||
|
"""Find the prefix that a package is installed under, and the path
|
||||||
|
that it would be imported from.
|
||||||
|
|
||||||
|
The prefix is the directory containing the standard directory
|
||||||
|
hierarchy (lib, bin, etc.). If the package is not installed to the
|
||||||
|
system (:attr:`sys.prefix`) or a virtualenv (``site-packages``),
|
||||||
|
``None`` is returned.
|
||||||
|
|
||||||
|
The path is the entry in :attr:`sys.path` that contains the package
|
||||||
|
for import. If the package is not installed, it's assumed that the
|
||||||
|
package was imported from the current working directory.
|
||||||
|
"""
|
||||||
|
package_path = _find_package_path(import_name)
|
||||||
|
py_prefix = os.path.abspath(sys.prefix)
|
||||||
|
|
||||||
|
# installed to the system
|
||||||
|
if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix):
|
||||||
|
return py_prefix, package_path
|
||||||
|
|
||||||
|
site_parent, site_folder = os.path.split(package_path)
|
||||||
|
|
||||||
|
# installed to a virtualenv
|
||||||
|
if site_folder.lower() == "site-packages":
|
||||||
|
parent, folder = os.path.split(site_parent)
|
||||||
|
|
||||||
|
# Windows (prefix/lib/site-packages)
|
||||||
|
if folder.lower() == "lib":
|
||||||
|
return parent, package_path
|
||||||
|
|
||||||
|
# Unix (prefix/lib/pythonX.Y/site-packages)
|
||||||
|
if os.path.basename(parent).lower() == "lib":
|
||||||
|
return os.path.dirname(parent), package_path
|
||||||
|
|
||||||
|
# something else (prefix/site-packages)
|
||||||
|
return site_parent, package_path
|
||||||
|
|
||||||
|
# not installed
|
||||||
|
return None, package_path
|
@ -0,0 +1,419 @@
|
|||||||
|
import hashlib
|
||||||
|
import typing as t
|
||||||
|
import warnings
|
||||||
|
from collections.abc import MutableMapping
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
from itsdangerous import BadSignature
|
||||||
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
from werkzeug.datastructures import CallbackDict
|
||||||
|
|
||||||
|
from .helpers import is_ip
|
||||||
|
from .json.tag import TaggedJSONSerializer
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
import typing_extensions as te
|
||||||
|
from .app import Flask
|
||||||
|
from .wrappers import Request, Response
|
||||||
|
|
||||||
|
|
||||||
|
class SessionMixin(MutableMapping):
|
||||||
|
"""Expands a basic dictionary with session attributes."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def permanent(self) -> bool:
|
||||||
|
"""This reflects the ``'_permanent'`` key in the dict."""
|
||||||
|
return self.get("_permanent", False)
|
||||||
|
|
||||||
|
@permanent.setter
|
||||||
|
def permanent(self, value: bool) -> None:
|
||||||
|
self["_permanent"] = bool(value)
|
||||||
|
|
||||||
|
#: Some implementations can detect whether a session is newly
|
||||||
|
#: created, but that is not guaranteed. Use with caution. The mixin
|
||||||
|
# default is hard-coded ``False``.
|
||||||
|
new = False
|
||||||
|
|
||||||
|
#: Some implementations can detect changes to the session and set
|
||||||
|
#: this when that happens. The mixin default is hard coded to
|
||||||
|
#: ``True``.
|
||||||
|
modified = True
|
||||||
|
|
||||||
|
#: Some implementations can detect when session data is read or
|
||||||
|
#: written and set this when that happens. The mixin default is hard
|
||||||
|
#: coded to ``True``.
|
||||||
|
accessed = True
|
||||||
|
|
||||||
|
|
||||||
|
class SecureCookieSession(CallbackDict, SessionMixin):
|
||||||
|
"""Base class for sessions based on signed cookies.
|
||||||
|
|
||||||
|
This session backend will set the :attr:`modified` and
|
||||||
|
:attr:`accessed` attributes. It cannot reliably track whether a
|
||||||
|
session is new (vs. empty), so :attr:`new` remains hard coded to
|
||||||
|
``False``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: When data is changed, this is set to ``True``. Only the session
|
||||||
|
#: dictionary itself is tracked; if the session contains mutable
|
||||||
|
#: data (for example a nested dict) then this must be set to
|
||||||
|
#: ``True`` manually when modifying that data. The session cookie
|
||||||
|
#: will only be written to the response if this is ``True``.
|
||||||
|
modified = False
|
||||||
|
|
||||||
|
#: When data is read or written, this is set to ``True``. Used by
|
||||||
|
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
|
||||||
|
#: header, which allows caching proxies to cache different pages for
|
||||||
|
#: different users.
|
||||||
|
accessed = False
|
||||||
|
|
||||||
|
def __init__(self, initial: t.Any = None) -> None:
|
||||||
|
def on_update(self) -> None:
|
||||||
|
self.modified = True
|
||||||
|
self.accessed = True
|
||||||
|
|
||||||
|
super().__init__(initial, on_update)
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> t.Any:
|
||||||
|
self.accessed = True
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
||||||
|
def get(self, key: str, default: t.Any = None) -> t.Any:
|
||||||
|
self.accessed = True
|
||||||
|
return super().get(key, default)
|
||||||
|
|
||||||
|
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
|
||||||
|
self.accessed = True
|
||||||
|
return super().setdefault(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
class NullSession(SecureCookieSession):
|
||||||
|
"""Class used to generate nicer error messages if sessions are not
|
||||||
|
available. Will still allow read-only access to the empty session
|
||||||
|
but fail on setting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _fail(self, *args: t.Any, **kwargs: t.Any) -> "te.NoReturn":
|
||||||
|
raise RuntimeError(
|
||||||
|
"The session is unavailable because no secret "
|
||||||
|
"key was set. Set the secret_key on the "
|
||||||
|
"application to something unique and secret."
|
||||||
|
)
|
||||||
|
|
||||||
|
__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
|
||||||
|
del _fail
|
||||||
|
|
||||||
|
|
||||||
|
class SessionInterface:
|
||||||
|
"""The basic interface you have to implement in order to replace the
|
||||||
|
default session interface which uses werkzeug's securecookie
|
||||||
|
implementation. The only methods you have to implement are
|
||||||
|
:meth:`open_session` and :meth:`save_session`, the others have
|
||||||
|
useful defaults which you don't need to change.
|
||||||
|
|
||||||
|
The session object returned by the :meth:`open_session` method has to
|
||||||
|
provide a dictionary like interface plus the properties and methods
|
||||||
|
from the :class:`SessionMixin`. We recommend just subclassing a dict
|
||||||
|
and adding that mixin::
|
||||||
|
|
||||||
|
class Session(dict, SessionMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
If :meth:`open_session` returns ``None`` Flask will call into
|
||||||
|
:meth:`make_null_session` to create a session that acts as replacement
|
||||||
|
if the session support cannot work because some requirement is not
|
||||||
|
fulfilled. The default :class:`NullSession` class that is created
|
||||||
|
will complain that the secret key was not set.
|
||||||
|
|
||||||
|
To replace the session interface on an application all you have to do
|
||||||
|
is to assign :attr:`flask.Flask.session_interface`::
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.session_interface = MySessionInterface()
|
||||||
|
|
||||||
|
Multiple requests with the same session may be sent and handled
|
||||||
|
concurrently. When implementing a new session interface, consider
|
||||||
|
whether reads or writes to the backing store must be synchronized.
|
||||||
|
There is no guarantee on the order in which the session for each
|
||||||
|
request is opened or saved, it will occur in the order that requests
|
||||||
|
begin and end processing.
|
||||||
|
|
||||||
|
.. versionadded:: 0.8
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: :meth:`make_null_session` will look here for the class that should
|
||||||
|
#: be created when a null session is requested. Likewise the
|
||||||
|
#: :meth:`is_null_session` method will perform a typecheck against
|
||||||
|
#: this type.
|
||||||
|
null_session_class = NullSession
|
||||||
|
|
||||||
|
#: A flag that indicates if the session interface is pickle based.
|
||||||
|
#: This can be used by Flask extensions to make a decision in regards
|
||||||
|
#: to how to deal with the session object.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.10
|
||||||
|
pickle_based = False
|
||||||
|
|
||||||
|
def make_null_session(self, app: "Flask") -> NullSession:
|
||||||
|
"""Creates a null session which acts as a replacement object if the
|
||||||
|
real session support could not be loaded due to a configuration
|
||||||
|
error. This mainly aids the user experience because the job of the
|
||||||
|
null session is to still support lookup without complaining but
|
||||||
|
modifications are answered with a helpful error message of what
|
||||||
|
failed.
|
||||||
|
|
||||||
|
This creates an instance of :attr:`null_session_class` by default.
|
||||||
|
"""
|
||||||
|
return self.null_session_class()
|
||||||
|
|
||||||
|
def is_null_session(self, obj: object) -> bool:
|
||||||
|
"""Checks if a given object is a null session. Null sessions are
|
||||||
|
not asked to be saved.
|
||||||
|
|
||||||
|
This checks if the object is an instance of :attr:`null_session_class`
|
||||||
|
by default.
|
||||||
|
"""
|
||||||
|
return isinstance(obj, self.null_session_class)
|
||||||
|
|
||||||
|
def get_cookie_name(self, app: "Flask") -> str:
|
||||||
|
"""The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
|
||||||
|
return app.config["SESSION_COOKIE_NAME"]
|
||||||
|
|
||||||
|
def get_cookie_domain(self, app: "Flask") -> t.Optional[str]:
|
||||||
|
"""Returns the domain that should be set for the session cookie.
|
||||||
|
|
||||||
|
Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
|
||||||
|
falls back to detecting the domain based on ``SERVER_NAME``.
|
||||||
|
|
||||||
|
Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
|
||||||
|
updated to avoid re-running the logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rv = app.config["SESSION_COOKIE_DOMAIN"]
|
||||||
|
|
||||||
|
# set explicitly, or cached from SERVER_NAME detection
|
||||||
|
# if False, return None
|
||||||
|
if rv is not None:
|
||||||
|
return rv if rv else None
|
||||||
|
|
||||||
|
rv = app.config["SERVER_NAME"]
|
||||||
|
|
||||||
|
# server name not set, cache False to return none next time
|
||||||
|
if not rv:
|
||||||
|
app.config["SESSION_COOKIE_DOMAIN"] = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
# chop off the port which is usually not supported by browsers
|
||||||
|
# remove any leading '.' since we'll add that later
|
||||||
|
rv = rv.rsplit(":", 1)[0].lstrip(".")
|
||||||
|
|
||||||
|
if "." not in rv:
|
||||||
|
# Chrome doesn't allow names without a '.'. This should only
|
||||||
|
# come up with localhost. Hack around this by not setting
|
||||||
|
# the name, and show a warning.
|
||||||
|
warnings.warn(
|
||||||
|
f"{rv!r} is not a valid cookie domain, it must contain"
|
||||||
|
" a '.'. Add an entry to your hosts file, for example"
|
||||||
|
f" '{rv}.localdomain', and use that instead."
|
||||||
|
)
|
||||||
|
app.config["SESSION_COOKIE_DOMAIN"] = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
ip = is_ip(rv)
|
||||||
|
|
||||||
|
if ip:
|
||||||
|
warnings.warn(
|
||||||
|
"The session cookie domain is an IP address. This may not work"
|
||||||
|
" as intended in some browsers. Add an entry to your hosts"
|
||||||
|
' file, for example "localhost.localdomain", and use that'
|
||||||
|
" instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# if this is not an ip and app is mounted at the root, allow subdomain
|
||||||
|
# matching by adding a '.' prefix
|
||||||
|
if self.get_cookie_path(app) == "/" and not ip:
|
||||||
|
rv = f".{rv}"
|
||||||
|
|
||||||
|
app.config["SESSION_COOKIE_DOMAIN"] = rv
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def get_cookie_path(self, app: "Flask") -> str:
|
||||||
|
"""Returns the path for which the cookie should be valid. The
|
||||||
|
default implementation uses the value from the ``SESSION_COOKIE_PATH``
|
||||||
|
config var if it's set, and falls back to ``APPLICATION_ROOT`` or
|
||||||
|
uses ``/`` if it's ``None``.
|
||||||
|
"""
|
||||||
|
return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"]
|
||||||
|
|
||||||
|
def get_cookie_httponly(self, app: "Flask") -> bool:
|
||||||
|
"""Returns True if the session cookie should be httponly. This
|
||||||
|
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
|
||||||
|
config var.
|
||||||
|
"""
|
||||||
|
return app.config["SESSION_COOKIE_HTTPONLY"]
|
||||||
|
|
||||||
|
def get_cookie_secure(self, app: "Flask") -> bool:
|
||||||
|
"""Returns True if the cookie should be secure. This currently
|
||||||
|
just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
|
||||||
|
"""
|
||||||
|
return app.config["SESSION_COOKIE_SECURE"]
|
||||||
|
|
||||||
|
def get_cookie_samesite(self, app: "Flask") -> str:
|
||||||
|
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
|
||||||
|
``SameSite`` attribute. This currently just returns the value of
|
||||||
|
the :data:`SESSION_COOKIE_SAMESITE` setting.
|
||||||
|
"""
|
||||||
|
return app.config["SESSION_COOKIE_SAMESITE"]
|
||||||
|
|
||||||
|
def get_expiration_time(
|
||||||
|
self, app: "Flask", session: SessionMixin
|
||||||
|
) -> t.Optional[datetime]:
|
||||||
|
"""A helper method that returns an expiration date for the session
|
||||||
|
or ``None`` if the session is linked to the browser session. The
|
||||||
|
default implementation returns now + the permanent session
|
||||||
|
lifetime configured on the application.
|
||||||
|
"""
|
||||||
|
if session.permanent:
|
||||||
|
return datetime.now(timezone.utc) + app.permanent_session_lifetime
|
||||||
|
return None
|
||||||
|
|
||||||
|
def should_set_cookie(self, app: "Flask", session: SessionMixin) -> bool:
|
||||||
|
"""Used by session backends to determine if a ``Set-Cookie`` header
|
||||||
|
should be set for this session cookie for this response. If the session
|
||||||
|
has been modified, the cookie is set. If the session is permanent and
|
||||||
|
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
|
||||||
|
always set.
|
||||||
|
|
||||||
|
This check is usually skipped if the session was deleted.
|
||||||
|
|
||||||
|
.. versionadded:: 0.11
|
||||||
|
"""
|
||||||
|
|
||||||
|
return session.modified or (
|
||||||
|
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_session(
|
||||||
|
self, app: "Flask", request: "Request"
|
||||||
|
) -> t.Optional[SessionMixin]:
|
||||||
|
"""This is called at the beginning of each request, after
|
||||||
|
pushing the request context, before matching the URL.
|
||||||
|
|
||||||
|
This must return an object which implements a dictionary-like
|
||||||
|
interface as well as the :class:`SessionMixin` interface.
|
||||||
|
|
||||||
|
This will return ``None`` to indicate that loading failed in
|
||||||
|
some way that is not immediately an error. The request
|
||||||
|
context will fall back to using :meth:`make_null_session`
|
||||||
|
in this case.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def save_session(
|
||||||
|
self, app: "Flask", session: SessionMixin, response: "Response"
|
||||||
|
) -> None:
|
||||||
|
"""This is called at the end of each request, after generating
|
||||||
|
a response, before removing the request context. It is skipped
|
||||||
|
if :meth:`is_null_session` returns ``True``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
session_json_serializer = TaggedJSONSerializer()
|
||||||
|
|
||||||
|
|
||||||
|
class SecureCookieSessionInterface(SessionInterface):
|
||||||
|
"""The default session interface that stores sessions in signed cookies
|
||||||
|
through the :mod:`itsdangerous` module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: the salt that should be applied on top of the secret key for the
|
||||||
|
#: signing of cookie based sessions.
|
||||||
|
salt = "cookie-session"
|
||||||
|
#: the hash function to use for the signature. The default is sha1
|
||||||
|
digest_method = staticmethod(hashlib.sha1)
|
||||||
|
#: the name of the itsdangerous supported key derivation. The default
|
||||||
|
#: is hmac.
|
||||||
|
key_derivation = "hmac"
|
||||||
|
#: A python serializer for the payload. The default is a compact
|
||||||
|
#: JSON derived serializer with support for some extra Python types
|
||||||
|
#: such as datetime objects or tuples.
|
||||||
|
serializer = session_json_serializer
|
||||||
|
session_class = SecureCookieSession
|
||||||
|
|
||||||
|
def get_signing_serializer(
|
||||||
|
self, app: "Flask"
|
||||||
|
) -> t.Optional[URLSafeTimedSerializer]:
|
||||||
|
if not app.secret_key:
|
||||||
|
return None
|
||||||
|
signer_kwargs = dict(
|
||||||
|
key_derivation=self.key_derivation, digest_method=self.digest_method
|
||||||
|
)
|
||||||
|
return URLSafeTimedSerializer(
|
||||||
|
app.secret_key,
|
||||||
|
salt=self.salt,
|
||||||
|
serializer=self.serializer,
|
||||||
|
signer_kwargs=signer_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_session(
|
||||||
|
self, app: "Flask", request: "Request"
|
||||||
|
) -> t.Optional[SecureCookieSession]:
|
||||||
|
s = self.get_signing_serializer(app)
|
||||||
|
if s is None:
|
||||||
|
return None
|
||||||
|
val = request.cookies.get(self.get_cookie_name(app))
|
||||||
|
if not val:
|
||||||
|
return self.session_class()
|
||||||
|
max_age = int(app.permanent_session_lifetime.total_seconds())
|
||||||
|
try:
|
||||||
|
data = s.loads(val, max_age=max_age)
|
||||||
|
return self.session_class(data)
|
||||||
|
except BadSignature:
|
||||||
|
return self.session_class()
|
||||||
|
|
||||||
|
def save_session(
|
||||||
|
self, app: "Flask", session: SessionMixin, response: "Response"
|
||||||
|
) -> None:
|
||||||
|
name = self.get_cookie_name(app)
|
||||||
|
domain = self.get_cookie_domain(app)
|
||||||
|
path = self.get_cookie_path(app)
|
||||||
|
secure = self.get_cookie_secure(app)
|
||||||
|
samesite = self.get_cookie_samesite(app)
|
||||||
|
httponly = self.get_cookie_httponly(app)
|
||||||
|
|
||||||
|
# If the session is modified to be empty, remove the cookie.
|
||||||
|
# If the session is empty, return without setting the cookie.
|
||||||
|
if not session:
|
||||||
|
if session.modified:
|
||||||
|
response.delete_cookie(
|
||||||
|
name,
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=secure,
|
||||||
|
samesite=samesite,
|
||||||
|
httponly=httponly,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add a "Vary: Cookie" header if the session was accessed at all.
|
||||||
|
if session.accessed:
|
||||||
|
response.vary.add("Cookie")
|
||||||
|
|
||||||
|
if not self.should_set_cookie(app, session):
|
||||||
|
return
|
||||||
|
|
||||||
|
expires = self.get_expiration_time(app, session)
|
||||||
|
val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore
|
||||||
|
response.set_cookie(
|
||||||
|
name,
|
||||||
|
val, # type: ignore
|
||||||
|
expires=expires,
|
||||||
|
httponly=httponly,
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=secure,
|
||||||
|
samesite=samesite,
|
||||||
|
)
|
@ -0,0 +1,56 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
try:
|
||||||
|
from blinker import Namespace
|
||||||
|
|
||||||
|
signals_available = True
|
||||||
|
except ImportError:
|
||||||
|
signals_available = False
|
||||||
|
|
||||||
|
class Namespace: # type: ignore
|
||||||
|
def signal(self, name: str, doc: t.Optional[str] = None) -> "_FakeSignal":
|
||||||
|
return _FakeSignal(name, doc)
|
||||||
|
|
||||||
|
class _FakeSignal:
|
||||||
|
"""If blinker is unavailable, create a fake class with the same
|
||||||
|
interface that allows sending of signals but will fail with an
|
||||||
|
error on anything else. Instead of doing anything on send, it
|
||||||
|
will just ignore the arguments and do nothing instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: t.Optional[str] = None) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.__doc__ = doc
|
||||||
|
|
||||||
|
def send(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Signalling support is unavailable because the blinker"
|
||||||
|
" library is not installed."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
connect = connect_via = connected_to = temporarily_connected_to = _fail
|
||||||
|
disconnect = _fail
|
||||||
|
has_receivers_for = receivers_for = _fail
|
||||||
|
del _fail
|
||||||
|
|
||||||
|
|
||||||
|
# The namespace for code signals. If you are not Flask code, do
|
||||||
|
# not put signals in here. Create your own namespace instead.
|
||||||
|
_signals = Namespace()
|
||||||
|
|
||||||
|
|
||||||
|
# Core signals. For usage examples grep the source code or consult
|
||||||
|
# the API documentation in docs/api.rst as well as docs/signals.rst
|
||||||
|
template_rendered = _signals.signal("template-rendered")
|
||||||
|
before_render_template = _signals.signal("before-render-template")
|
||||||
|
request_started = _signals.signal("request-started")
|
||||||
|
request_finished = _signals.signal("request-finished")
|
||||||
|
request_tearing_down = _signals.signal("request-tearing-down")
|
||||||
|
got_request_exception = _signals.signal("got-request-exception")
|
||||||
|
appcontext_tearing_down = _signals.signal("appcontext-tearing-down")
|
||||||
|
appcontext_pushed = _signals.signal("appcontext-pushed")
|
||||||
|
appcontext_popped = _signals.signal("appcontext-popped")
|
||||||
|
message_flashed = _signals.signal("message-flashed")
|
@ -0,0 +1,212 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from jinja2 import BaseLoader
|
||||||
|
from jinja2 import Environment as BaseEnvironment
|
||||||
|
from jinja2 import Template
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
|
|
||||||
|
from .globals import _cv_app
|
||||||
|
from .globals import _cv_request
|
||||||
|
from .globals import current_app
|
||||||
|
from .globals import request
|
||||||
|
from .helpers import stream_with_context
|
||||||
|
from .signals import before_render_template
|
||||||
|
from .signals import template_rendered
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .app import Flask
|
||||||
|
from .scaffold import Scaffold
|
||||||
|
|
||||||
|
|
||||||
|
def _default_template_ctx_processor() -> t.Dict[str, t.Any]:
|
||||||
|
"""Default template context processor. Injects `request`,
|
||||||
|
`session` and `g`.
|
||||||
|
"""
|
||||||
|
appctx = _cv_app.get(None)
|
||||||
|
reqctx = _cv_request.get(None)
|
||||||
|
rv: t.Dict[str, t.Any] = {}
|
||||||
|
if appctx is not None:
|
||||||
|
rv["g"] = appctx.g
|
||||||
|
if reqctx is not None:
|
||||||
|
rv["request"] = reqctx.request
|
||||||
|
rv["session"] = reqctx.session
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(BaseEnvironment):
|
||||||
|
"""Works like a regular Jinja2 environment but has some additional
|
||||||
|
knowledge of how Flask's blueprint works so that it can prepend the
|
||||||
|
name of the blueprint to referenced templates if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: "Flask", **options: t.Any) -> None:
|
||||||
|
if "loader" not in options:
|
||||||
|
options["loader"] = app.create_global_jinja_loader()
|
||||||
|
BaseEnvironment.__init__(self, **options)
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
|
||||||
|
class DispatchingJinjaLoader(BaseLoader):
|
||||||
|
"""A loader that looks for templates in the application and all
|
||||||
|
the blueprint folders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: "Flask") -> None:
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def get_source( # type: ignore
|
||||||
|
self, environment: Environment, template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
|
||||||
|
if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
|
||||||
|
return self._get_source_explained(environment, template)
|
||||||
|
return self._get_source_fast(environment, template)
|
||||||
|
|
||||||
|
def _get_source_explained(
|
||||||
|
self, environment: Environment, template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
|
||||||
|
attempts = []
|
||||||
|
rv: t.Optional[t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]]
|
||||||
|
trv: t.Optional[
|
||||||
|
t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
|
||||||
|
] = None
|
||||||
|
|
||||||
|
for srcobj, loader in self._iter_loaders(template):
|
||||||
|
try:
|
||||||
|
rv = loader.get_source(environment, template)
|
||||||
|
if trv is None:
|
||||||
|
trv = rv
|
||||||
|
except TemplateNotFound:
|
||||||
|
rv = None
|
||||||
|
attempts.append((loader, srcobj, rv))
|
||||||
|
|
||||||
|
from .debughelpers import explain_template_loading_attempts
|
||||||
|
|
||||||
|
explain_template_loading_attempts(self.app, template, attempts)
|
||||||
|
|
||||||
|
if trv is not None:
|
||||||
|
return trv
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def _get_source_fast(
|
||||||
|
self, environment: Environment, template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
|
||||||
|
for _srcobj, loader in self._iter_loaders(template):
|
||||||
|
try:
|
||||||
|
return loader.get_source(environment, template)
|
||||||
|
except TemplateNotFound:
|
||||||
|
continue
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def _iter_loaders(
|
||||||
|
self, template: str
|
||||||
|
) -> t.Generator[t.Tuple["Scaffold", BaseLoader], None, None]:
|
||||||
|
loader = self.app.jinja_loader
|
||||||
|
if loader is not None:
|
||||||
|
yield self.app, loader
|
||||||
|
|
||||||
|
for blueprint in self.app.iter_blueprints():
|
||||||
|
loader = blueprint.jinja_loader
|
||||||
|
if loader is not None:
|
||||||
|
yield blueprint, loader
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
result = set()
|
||||||
|
loader = self.app.jinja_loader
|
||||||
|
if loader is not None:
|
||||||
|
result.update(loader.list_templates())
|
||||||
|
|
||||||
|
for blueprint in self.app.iter_blueprints():
|
||||||
|
loader = blueprint.jinja_loader
|
||||||
|
if loader is not None:
|
||||||
|
for template in loader.list_templates():
|
||||||
|
result.add(template)
|
||||||
|
|
||||||
|
return list(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _render(app: "Flask", template: Template, context: t.Dict[str, t.Any]) -> str:
|
||||||
|
app.update_template_context(context)
|
||||||
|
before_render_template.send(app, template=template, context=context)
|
||||||
|
rv = template.render(context)
|
||||||
|
template_rendered.send(app, template=template, context=context)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def render_template(
|
||||||
|
template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]],
|
||||||
|
**context: t.Any
|
||||||
|
) -> str:
|
||||||
|
"""Render a template by name with the given context.
|
||||||
|
|
||||||
|
:param template_name_or_list: The name of the template to render. If
|
||||||
|
a list is given, the first name to exist will be rendered.
|
||||||
|
:param context: The variables to make available in the template.
|
||||||
|
"""
|
||||||
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||||
|
template = app.jinja_env.get_or_select_template(template_name_or_list)
|
||||||
|
return _render(app, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
def render_template_string(source: str, **context: t.Any) -> str:
|
||||||
|
"""Render a template from the given source string with the given
|
||||||
|
context.
|
||||||
|
|
||||||
|
:param source: The source code of the template to render.
|
||||||
|
:param context: The variables to make available in the template.
|
||||||
|
"""
|
||||||
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||||
|
template = app.jinja_env.from_string(source)
|
||||||
|
return _render(app, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
def _stream(
|
||||||
|
app: "Flask", template: Template, context: t.Dict[str, t.Any]
|
||||||
|
) -> t.Iterator[str]:
|
||||||
|
app.update_template_context(context)
|
||||||
|
before_render_template.send(app, template=template, context=context)
|
||||||
|
|
||||||
|
def generate() -> t.Iterator[str]:
|
||||||
|
yield from template.generate(context)
|
||||||
|
template_rendered.send(app, template=template, context=context)
|
||||||
|
|
||||||
|
rv = generate()
|
||||||
|
|
||||||
|
# If a request context is active, keep it while generating.
|
||||||
|
if request:
|
||||||
|
rv = stream_with_context(rv)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def stream_template(
|
||||||
|
template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]],
|
||||||
|
**context: t.Any
|
||||||
|
) -> t.Iterator[str]:
|
||||||
|
"""Render a template by name with the given context as a stream.
|
||||||
|
This returns an iterator of strings, which can be used as a
|
||||||
|
streaming response from a view.
|
||||||
|
|
||||||
|
:param template_name_or_list: The name of the template to render. If
|
||||||
|
a list is given, the first name to exist will be rendered.
|
||||||
|
:param context: The variables to make available in the template.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||||
|
template = app.jinja_env.get_or_select_template(template_name_or_list)
|
||||||
|
return _stream(app, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
|
||||||
|
"""Render a template from the given source string with the given
|
||||||
|
context as a stream. This returns an iterator of strings, which can
|
||||||
|
be used as a streaming response from a view.
|
||||||
|
|
||||||
|
:param source: The source code of the template to render.
|
||||||
|
:param context: The variables to make available in the template.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||||
|
template = app.jinja_env.from_string(source)
|
||||||
|
return _stream(app, template, context)
|
@ -0,0 +1,286 @@
|
|||||||
|
import typing as t
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from copy import copy
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
import werkzeug.test
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from werkzeug.test import Client
|
||||||
|
from werkzeug.urls import url_parse
|
||||||
|
from werkzeug.wrappers import Request as BaseRequest
|
||||||
|
|
||||||
|
from .cli import ScriptInfo
|
||||||
|
from .globals import _cv_request
|
||||||
|
from .sessions import SessionMixin
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from werkzeug.test import TestResponse
|
||||||
|
|
||||||
|
from .app import Flask
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironBuilder(werkzeug.test.EnvironBuilder):
|
||||||
|
"""An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the
|
||||||
|
application.
|
||||||
|
|
||||||
|
:param app: The Flask application to configure the environment from.
|
||||||
|
:param path: URL path being requested.
|
||||||
|
:param base_url: Base URL where the app is being served, which
|
||||||
|
``path`` is relative to. If not given, built from
|
||||||
|
:data:`PREFERRED_URL_SCHEME`, ``subdomain``,
|
||||||
|
:data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
|
||||||
|
:param subdomain: Subdomain name to append to :data:`SERVER_NAME`.
|
||||||
|
:param url_scheme: Scheme to use instead of
|
||||||
|
:data:`PREFERRED_URL_SCHEME`.
|
||||||
|
:param json: If given, this is serialized as JSON and passed as
|
||||||
|
``data``. Also defaults ``content_type`` to
|
||||||
|
``application/json``.
|
||||||
|
:param args: other positional arguments passed to
|
||||||
|
:class:`~werkzeug.test.EnvironBuilder`.
|
||||||
|
:param kwargs: other keyword arguments passed to
|
||||||
|
:class:`~werkzeug.test.EnvironBuilder`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: "Flask",
|
||||||
|
path: str = "/",
|
||||||
|
base_url: t.Optional[str] = None,
|
||||||
|
subdomain: t.Optional[str] = None,
|
||||||
|
url_scheme: t.Optional[str] = None,
|
||||||
|
*args: t.Any,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
assert not (base_url or subdomain or url_scheme) or (
|
||||||
|
base_url is not None
|
||||||
|
) != bool(
|
||||||
|
subdomain or url_scheme
|
||||||
|
), 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
|
||||||
|
|
||||||
|
if base_url is None:
|
||||||
|
http_host = app.config.get("SERVER_NAME") or "localhost"
|
||||||
|
app_root = app.config["APPLICATION_ROOT"]
|
||||||
|
|
||||||
|
if subdomain:
|
||||||
|
http_host = f"{subdomain}.{http_host}"
|
||||||
|
|
||||||
|
if url_scheme is None:
|
||||||
|
url_scheme = app.config["PREFERRED_URL_SCHEME"]
|
||||||
|
|
||||||
|
url = url_parse(path)
|
||||||
|
base_url = (
|
||||||
|
f"{url.scheme or url_scheme}://{url.netloc or http_host}"
|
||||||
|
f"/{app_root.lstrip('/')}"
|
||||||
|
)
|
||||||
|
path = url.path
|
||||||
|
|
||||||
|
if url.query:
|
||||||
|
sep = b"?" if isinstance(url.query, bytes) else "?"
|
||||||
|
path += sep + url.query
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
super().__init__(path, base_url, *args, **kwargs)
|
||||||
|
|
||||||
|
def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
|
||||||
|
"""Serialize ``obj`` to a JSON-formatted string.
|
||||||
|
|
||||||
|
The serialization will be configured according to the config associated
|
||||||
|
with this EnvironBuilder's ``app``.
|
||||||
|
"""
|
||||||
|
return self.app.json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskClient(Client):
|
||||||
|
"""Works like a regular Werkzeug test client but has knowledge about
|
||||||
|
Flask's contexts to defer the cleanup of the request context until
|
||||||
|
the end of a ``with`` block. For general information about how to
|
||||||
|
use this class refer to :class:`werkzeug.test.Client`.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.12
|
||||||
|
`app.test_client()` includes preset default environment, which can be
|
||||||
|
set after instantiation of the `app.test_client()` object in
|
||||||
|
`client.environ_base`.
|
||||||
|
|
||||||
|
Basic usage is outlined in the :doc:`/testing` chapter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
application: "Flask"
|
||||||
|
|
||||||
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.preserve_context = False
|
||||||
|
self._new_contexts: t.List[t.ContextManager[t.Any]] = []
|
||||||
|
self._context_stack = ExitStack()
|
||||||
|
self.environ_base = {
|
||||||
|
"REMOTE_ADDR": "127.0.0.1",
|
||||||
|
"HTTP_USER_AGENT": f"werkzeug/{werkzeug.__version__}",
|
||||||
|
}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def session_transaction(
|
||||||
|
self, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> t.Generator[SessionMixin, None, None]:
|
||||||
|
"""When used in combination with a ``with`` statement this opens a
|
||||||
|
session transaction. This can be used to modify the session that
|
||||||
|
the test client uses. Once the ``with`` block is left the session is
|
||||||
|
stored back.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
with client.session_transaction() as session:
|
||||||
|
session['value'] = 42
|
||||||
|
|
||||||
|
Internally this is implemented by going through a temporary test
|
||||||
|
request context and since session handling could depend on
|
||||||
|
request variables this function accepts the same arguments as
|
||||||
|
:meth:`~flask.Flask.test_request_context` which are directly
|
||||||
|
passed through.
|
||||||
|
"""
|
||||||
|
if self.cookie_jar is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Session transactions only make sense with cookies enabled."
|
||||||
|
)
|
||||||
|
app = self.application
|
||||||
|
environ_overrides = kwargs.setdefault("environ_overrides", {})
|
||||||
|
self.cookie_jar.inject_wsgi(environ_overrides)
|
||||||
|
outer_reqctx = _cv_request.get(None)
|
||||||
|
with app.test_request_context(*args, **kwargs) as c:
|
||||||
|
session_interface = app.session_interface
|
||||||
|
sess = session_interface.open_session(app, c.request)
|
||||||
|
if sess is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Session backend did not open a session. Check the configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Since we have to open a new request context for the session
|
||||||
|
# handling we want to make sure that we hide out own context
|
||||||
|
# from the caller. By pushing the original request context
|
||||||
|
# (or None) on top of this and popping it we get exactly that
|
||||||
|
# behavior. It's important to not use the push and pop
|
||||||
|
# methods of the actual request context object since that would
|
||||||
|
# mean that cleanup handlers are called
|
||||||
|
token = _cv_request.set(outer_reqctx) # type: ignore[arg-type]
|
||||||
|
try:
|
||||||
|
yield sess
|
||||||
|
finally:
|
||||||
|
_cv_request.reset(token)
|
||||||
|
|
||||||
|
resp = app.response_class()
|
||||||
|
if not session_interface.is_null_session(sess):
|
||||||
|
session_interface.save_session(app, sess, resp)
|
||||||
|
headers = resp.get_wsgi_headers(c.request.environ)
|
||||||
|
self.cookie_jar.extract_wsgi(c.request.environ, headers)
|
||||||
|
|
||||||
|
def _copy_environ(self, other):
|
||||||
|
out = {**self.environ_base, **other}
|
||||||
|
|
||||||
|
if self.preserve_context:
|
||||||
|
out["werkzeug.debug.preserve_context"] = self._new_contexts.append
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _request_from_builder_args(self, args, kwargs):
|
||||||
|
kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {}))
|
||||||
|
builder = EnvironBuilder(self.application, *args, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return builder.get_request()
|
||||||
|
finally:
|
||||||
|
builder.close()
|
||||||
|
|
||||||
|
def open(
|
||||||
|
self,
|
||||||
|
*args: t.Any,
|
||||||
|
buffered: bool = False,
|
||||||
|
follow_redirects: bool = False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> "TestResponse":
|
||||||
|
if args and isinstance(
|
||||||
|
args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest)
|
||||||
|
):
|
||||||
|
if isinstance(args[0], werkzeug.test.EnvironBuilder):
|
||||||
|
builder = copy(args[0])
|
||||||
|
builder.environ_base = self._copy_environ(builder.environ_base or {})
|
||||||
|
request = builder.get_request()
|
||||||
|
elif isinstance(args[0], dict):
|
||||||
|
request = EnvironBuilder.from_environ(
|
||||||
|
args[0], app=self.application, environ_base=self._copy_environ({})
|
||||||
|
).get_request()
|
||||||
|
else:
|
||||||
|
# isinstance(args[0], BaseRequest)
|
||||||
|
request = copy(args[0])
|
||||||
|
request.environ = self._copy_environ(request.environ)
|
||||||
|
else:
|
||||||
|
# request is None
|
||||||
|
request = self._request_from_builder_args(args, kwargs)
|
||||||
|
|
||||||
|
# Pop any previously preserved contexts. This prevents contexts
|
||||||
|
# from being preserved across redirects or multiple requests
|
||||||
|
# within a single block.
|
||||||
|
self._context_stack.close()
|
||||||
|
|
||||||
|
response = super().open(
|
||||||
|
request,
|
||||||
|
buffered=buffered,
|
||||||
|
follow_redirects=follow_redirects,
|
||||||
|
)
|
||||||
|
response.json_module = self.application.json # type: ignore[misc]
|
||||||
|
|
||||||
|
# Re-push contexts that were preserved during the request.
|
||||||
|
while self._new_contexts:
|
||||||
|
cm = self._new_contexts.pop()
|
||||||
|
self._context_stack.enter_context(cm)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __enter__(self) -> "FlaskClient":
|
||||||
|
if self.preserve_context:
|
||||||
|
raise RuntimeError("Cannot nest client invocations")
|
||||||
|
self.preserve_context = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: t.Optional[type],
|
||||||
|
exc_value: t.Optional[BaseException],
|
||||||
|
tb: t.Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
self.preserve_context = False
|
||||||
|
self._context_stack.close()
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskCliRunner(CliRunner):
|
||||||
|
"""A :class:`~click.testing.CliRunner` for testing a Flask app's
|
||||||
|
CLI commands. Typically created using
|
||||||
|
:meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: "Flask", **kwargs: t.Any) -> None:
|
||||||
|
self.app = app
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def invoke( # type: ignore
|
||||||
|
self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any
|
||||||
|
) -> t.Any:
|
||||||
|
"""Invokes a CLI command in an isolated environment. See
|
||||||
|
:meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for
|
||||||
|
full method documentation. See :ref:`testing-cli` for examples.
|
||||||
|
|
||||||
|
If the ``obj`` argument is not given, passes an instance of
|
||||||
|
:class:`~flask.cli.ScriptInfo` that knows how to load the Flask
|
||||||
|
app being tested.
|
||||||
|
|
||||||
|
:param cli: Command object to invoke. Default is the app's
|
||||||
|
:attr:`~flask.app.Flask.cli` group.
|
||||||
|
:param args: List of strings to invoke the command with.
|
||||||
|
|
||||||
|
:return: a :class:`~click.testing.Result` object.
|
||||||
|
"""
|
||||||
|
if cli is None:
|
||||||
|
cli = self.app.cli # type: ignore
|
||||||
|
|
||||||
|
if "obj" not in kwargs:
|
||||||
|
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
|
||||||
|
|
||||||
|
return super().invoke(cli, args, **kwargs)
|
@ -0,0 +1,80 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from _typeshed.wsgi import WSGIApplication # noqa: F401
|
||||||
|
from werkzeug.datastructures import Headers # noqa: F401
|
||||||
|
from werkzeug.wrappers import Response # noqa: F401
|
||||||
|
|
||||||
|
# The possible types that are directly convertible or are a Response object.
|
||||||
|
ResponseValue = t.Union[
|
||||||
|
"Response",
|
||||||
|
str,
|
||||||
|
bytes,
|
||||||
|
t.List[t.Any],
|
||||||
|
# Only dict is actually accepted, but Mapping allows for TypedDict.
|
||||||
|
t.Mapping[str, t.Any],
|
||||||
|
t.Iterator[str],
|
||||||
|
t.Iterator[bytes],
|
||||||
|
]
|
||||||
|
|
||||||
|
# the possible types for an individual HTTP header
|
||||||
|
# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
|
||||||
|
HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]]
|
||||||
|
|
||||||
|
# the possible types for HTTP headers
|
||||||
|
HeadersValue = t.Union[
|
||||||
|
"Headers",
|
||||||
|
t.Mapping[str, HeaderValue],
|
||||||
|
t.Sequence[t.Tuple[str, HeaderValue]],
|
||||||
|
]
|
||||||
|
|
||||||
|
# The possible types returned by a route function.
|
||||||
|
ResponseReturnValue = t.Union[
|
||||||
|
ResponseValue,
|
||||||
|
t.Tuple[ResponseValue, HeadersValue],
|
||||||
|
t.Tuple[ResponseValue, int],
|
||||||
|
t.Tuple[ResponseValue, int, HeadersValue],
|
||||||
|
"WSGIApplication",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Allow any subclass of werkzeug.Response, such as the one from Flask,
|
||||||
|
# as a callback argument. Using werkzeug.Response directly makes a
|
||||||
|
# callback annotated with flask.Response fail type checking.
|
||||||
|
ResponseClass = t.TypeVar("ResponseClass", bound="Response")
|
||||||
|
|
||||||
|
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
|
||||||
|
AfterRequestCallable = t.Union[
|
||||||
|
t.Callable[[ResponseClass], ResponseClass],
|
||||||
|
t.Callable[[ResponseClass], t.Awaitable[ResponseClass]],
|
||||||
|
]
|
||||||
|
BeforeFirstRequestCallable = t.Union[
|
||||||
|
t.Callable[[], None], t.Callable[[], t.Awaitable[None]]
|
||||||
|
]
|
||||||
|
BeforeRequestCallable = t.Union[
|
||||||
|
t.Callable[[], t.Optional[ResponseReturnValue]],
|
||||||
|
t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
|
||||||
|
]
|
||||||
|
ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
|
||||||
|
TeardownCallable = t.Union[
|
||||||
|
t.Callable[[t.Optional[BaseException]], None],
|
||||||
|
t.Callable[[t.Optional[BaseException]], t.Awaitable[None]],
|
||||||
|
]
|
||||||
|
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
|
||||||
|
TemplateFilterCallable = t.Callable[..., t.Any]
|
||||||
|
TemplateGlobalCallable = t.Callable[..., t.Any]
|
||||||
|
TemplateTestCallable = t.Callable[..., bool]
|
||||||
|
URLDefaultCallable = t.Callable[[str, dict], None]
|
||||||
|
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
|
||||||
|
|
||||||
|
# This should take Exception, but that either breaks typing the argument
|
||||||
|
# with a specific exception, or decorating multiple times with different
|
||||||
|
# exceptions (and using a union type on the argument).
|
||||||
|
# https://github.com/pallets/flask/issues/4095
|
||||||
|
# https://github.com/pallets/flask/issues/4295
|
||||||
|
# https://github.com/pallets/flask/issues/4297
|
||||||
|
ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue]
|
||||||
|
|
||||||
|
RouteCallable = t.Union[
|
||||||
|
t.Callable[..., ResponseReturnValue],
|
||||||
|
t.Callable[..., t.Awaitable[ResponseReturnValue]],
|
||||||
|
]
|
@ -0,0 +1,188 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
|
from .globals import current_app
|
||||||
|
from .globals import request
|
||||||
|
|
||||||
|
|
||||||
|
http_method_funcs = frozenset(
|
||||||
|
["get", "post", "head", "options", "delete", "put", "trace", "patch"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class View:
|
||||||
|
"""Subclass this class and override :meth:`dispatch_request` to
|
||||||
|
create a generic class-based view. Call :meth:`as_view` to create a
|
||||||
|
view function that creates an instance of the class with the given
|
||||||
|
arguments and calls its ``dispatch_request`` method with any URL
|
||||||
|
variables.
|
||||||
|
|
||||||
|
See :doc:`views` for a detailed guide.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class Hello(View):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
|
def dispatch_request(self, name):
|
||||||
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
|
app.add_url_rule(
|
||||||
|
"/hello/<name>", view_func=Hello.as_view("hello")
|
||||||
|
)
|
||||||
|
|
||||||
|
Set :attr:`methods` on the class to change what methods the view
|
||||||
|
accepts.
|
||||||
|
|
||||||
|
Set :attr:`decorators` on the class to apply a list of decorators to
|
||||||
|
the generated view function. Decorators applied to the class itself
|
||||||
|
will not be applied to the generated view function!
|
||||||
|
|
||||||
|
Set :attr:`init_every_request` to ``False`` for efficiency, unless
|
||||||
|
you need to store request-global data on ``self``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: The methods this view is registered for. Uses the same default
|
||||||
|
#: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and
|
||||||
|
#: ``add_url_rule`` by default.
|
||||||
|
methods: t.ClassVar[t.Optional[t.Collection[str]]] = None
|
||||||
|
|
||||||
|
#: Control whether the ``OPTIONS`` method is handled automatically.
|
||||||
|
#: Uses the same default (``True``) as ``route`` and
|
||||||
|
#: ``add_url_rule`` by default.
|
||||||
|
provide_automatic_options: t.ClassVar[t.Optional[bool]] = None
|
||||||
|
|
||||||
|
#: A list of decorators to apply, in order, to the generated view
|
||||||
|
#: function. Remember that ``@decorator`` syntax is applied bottom
|
||||||
|
#: to top, so the first decorator in the list would be the bottom
|
||||||
|
#: decorator.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.8
|
||||||
|
decorators: t.ClassVar[t.List[t.Callable]] = []
|
||||||
|
|
||||||
|
#: Create a new instance of this view class for every request by
|
||||||
|
#: default. If a view subclass sets this to ``False``, the same
|
||||||
|
#: instance is used for every request.
|
||||||
|
#:
|
||||||
|
#: A single instance is more efficient, especially if complex setup
|
||||||
|
#: is done during init. However, storing data on ``self`` is no
|
||||||
|
#: longer safe across requests, and :data:`~flask.g` should be used
|
||||||
|
#: instead.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.2
|
||||||
|
init_every_request: t.ClassVar[bool] = True
|
||||||
|
|
||||||
|
def dispatch_request(self) -> ft.ResponseReturnValue:
|
||||||
|
"""The actual view function behavior. Subclasses must override
|
||||||
|
this and return a valid response. Any variables from the URL
|
||||||
|
rule are passed as keyword arguments.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_view(
|
||||||
|
cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
|
||||||
|
) -> ft.RouteCallable:
|
||||||
|
"""Convert the class into a view function that can be registered
|
||||||
|
for a route.
|
||||||
|
|
||||||
|
By default, the generated view will create a new instance of the
|
||||||
|
view class for every request and call its
|
||||||
|
:meth:`dispatch_request` method. If the view class sets
|
||||||
|
:attr:`init_every_request` to ``False``, the same instance will
|
||||||
|
be used for every request.
|
||||||
|
|
||||||
|
The arguments passed to this method are forwarded to the view
|
||||||
|
class ``__init__`` method.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Added the ``init_every_request`` class attribute.
|
||||||
|
"""
|
||||||
|
if cls.init_every_request:
|
||||||
|
|
||||||
|
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
|
self = view.view_class( # type: ignore[attr-defined]
|
||||||
|
*class_args, **class_kwargs
|
||||||
|
)
|
||||||
|
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self = cls(*class_args, **class_kwargs)
|
||||||
|
|
||||||
|
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
|
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
|
||||||
|
|
||||||
|
if cls.decorators:
|
||||||
|
view.__name__ = name
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
for decorator in cls.decorators:
|
||||||
|
view = decorator(view)
|
||||||
|
|
||||||
|
# We attach the view class to the view function for two reasons:
|
||||||
|
# first of all it allows us to easily figure out what class-based
|
||||||
|
# view this thing came from, secondly it's also used for instantiating
|
||||||
|
# the view class so you can actually replace it with something else
|
||||||
|
# for testing purposes and debugging.
|
||||||
|
view.view_class = cls # type: ignore
|
||||||
|
view.__name__ = name
|
||||||
|
view.__doc__ = cls.__doc__
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
view.methods = cls.methods # type: ignore
|
||||||
|
view.provide_automatic_options = cls.provide_automatic_options # type: ignore
|
||||||
|
return view
|
||||||
|
|
||||||
|
|
||||||
|
class MethodView(View):
|
||||||
|
"""Dispatches request methods to the corresponding instance methods.
|
||||||
|
For example, if you implement a ``get`` method, it will be used to
|
||||||
|
handle ``GET`` requests.
|
||||||
|
|
||||||
|
This can be useful for defining a REST API.
|
||||||
|
|
||||||
|
:attr:`methods` is automatically set based on the methods defined on
|
||||||
|
the class.
|
||||||
|
|
||||||
|
See :doc:`views` for a detailed guide.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class CounterAPI(MethodView):
|
||||||
|
def get(self):
|
||||||
|
return str(session.get("counter", 0))
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
session["counter"] = session.get("counter", 0) + 1
|
||||||
|
return redirect(url_for("counter"))
|
||||||
|
|
||||||
|
app.add_url_rule(
|
||||||
|
"/counter", view_func=CounterAPI.as_view("counter")
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs: t.Any) -> None:
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
if "methods" not in cls.__dict__:
|
||||||
|
methods = set()
|
||||||
|
|
||||||
|
for base in cls.__bases__:
|
||||||
|
if getattr(base, "methods", None):
|
||||||
|
methods.update(base.methods) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
for key in http_method_funcs:
|
||||||
|
if hasattr(cls, key):
|
||||||
|
methods.add(key.upper())
|
||||||
|
|
||||||
|
if methods:
|
||||||
|
cls.methods = methods
|
||||||
|
|
||||||
|
def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
|
meth = getattr(self, request.method.lower(), None)
|
||||||
|
|
||||||
|
# If the request method is HEAD and we don't have a handler for it
|
||||||
|
# retry with GET.
|
||||||
|
if meth is None and request.method == "HEAD":
|
||||||
|
meth = getattr(self, "get", None)
|
||||||
|
|
||||||
|
assert meth is not None, f"Unimplemented method {request.method!r}"
|
||||||
|
return current_app.ensure_sync(meth)(**kwargs)
|
@ -0,0 +1,171 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
from werkzeug.wrappers import Request as RequestBase
|
||||||
|
from werkzeug.wrappers import Response as ResponseBase
|
||||||
|
|
||||||
|
from . import json
|
||||||
|
from .globals import current_app
|
||||||
|
from .helpers import _split_blueprint_path
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
|
|
||||||
|
class Request(RequestBase):
|
||||||
|
"""The request object used by default in Flask. Remembers the
|
||||||
|
matched endpoint and view arguments.
|
||||||
|
|
||||||
|
It is what ends up as :class:`~flask.request`. If you want to replace
|
||||||
|
the request object used you can subclass this and set
|
||||||
|
:attr:`~flask.Flask.request_class` to your subclass.
|
||||||
|
|
||||||
|
The request object is a :class:`~werkzeug.wrappers.Request` subclass and
|
||||||
|
provides all of the attributes Werkzeug defines plus a few Flask
|
||||||
|
specific ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
json_module = json
|
||||||
|
|
||||||
|
#: The internal URL rule that matched the request. This can be
|
||||||
|
#: useful to inspect which methods are allowed for the URL from
|
||||||
|
#: a before/after handler (``request.url_rule.methods``) etc.
|
||||||
|
#: Though if the request's method was invalid for the URL rule,
|
||||||
|
#: the valid list is available in ``routing_exception.valid_methods``
|
||||||
|
#: instead (an attribute of the Werkzeug exception
|
||||||
|
#: :exc:`~werkzeug.exceptions.MethodNotAllowed`)
|
||||||
|
#: because the request was never internally bound.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.6
|
||||||
|
url_rule: t.Optional["Rule"] = None
|
||||||
|
|
||||||
|
#: A dict of view arguments that matched the request. If an exception
|
||||||
|
#: happened when matching, this will be ``None``.
|
||||||
|
view_args: t.Optional[t.Dict[str, t.Any]] = None
|
||||||
|
|
||||||
|
#: If matching the URL failed, this is the exception that will be
|
||||||
|
#: raised / was raised as part of the request handling. This is
|
||||||
|
#: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
|
||||||
|
#: something similar.
|
||||||
|
routing_exception: t.Optional[Exception] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_content_length(self) -> t.Optional[int]: # type: ignore
|
||||||
|
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
|
||||||
|
if current_app:
|
||||||
|
return current_app.config["MAX_CONTENT_LENGTH"]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def endpoint(self) -> t.Optional[str]:
|
||||||
|
"""The endpoint that matched the request URL.
|
||||||
|
|
||||||
|
This will be ``None`` if matching failed or has not been
|
||||||
|
performed yet.
|
||||||
|
|
||||||
|
This in combination with :attr:`view_args` can be used to
|
||||||
|
reconstruct the same URL or a modified URL.
|
||||||
|
"""
|
||||||
|
if self.url_rule is not None:
|
||||||
|
return self.url_rule.endpoint
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blueprint(self) -> t.Optional[str]:
|
||||||
|
"""The registered name of the current blueprint.
|
||||||
|
|
||||||
|
This will be ``None`` if the endpoint is not part of a
|
||||||
|
blueprint, or if URL matching failed or has not been performed
|
||||||
|
yet.
|
||||||
|
|
||||||
|
This does not necessarily match the name the blueprint was
|
||||||
|
created with. It may have been nested, or registered with a
|
||||||
|
different name.
|
||||||
|
"""
|
||||||
|
endpoint = self.endpoint
|
||||||
|
|
||||||
|
if endpoint is not None and "." in endpoint:
|
||||||
|
return endpoint.rpartition(".")[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blueprints(self) -> t.List[str]:
|
||||||
|
"""The registered names of the current blueprint upwards through
|
||||||
|
parent blueprints.
|
||||||
|
|
||||||
|
This will be an empty list if there is no current blueprint, or
|
||||||
|
if URL matching failed.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.1
|
||||||
|
"""
|
||||||
|
name = self.blueprint
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return _split_blueprint_path(name)
|
||||||
|
|
||||||
|
def _load_form_data(self) -> None:
|
||||||
|
super()._load_form_data()
|
||||||
|
|
||||||
|
# In debug mode we're replacing the files multidict with an ad-hoc
|
||||||
|
# subclass that raises a different error for key errors.
|
||||||
|
if (
|
||||||
|
current_app
|
||||||
|
and current_app.debug
|
||||||
|
and self.mimetype != "multipart/form-data"
|
||||||
|
and not self.files
|
||||||
|
):
|
||||||
|
from .debughelpers import attach_enctype_error_multidict
|
||||||
|
|
||||||
|
attach_enctype_error_multidict(self)
|
||||||
|
|
||||||
|
def on_json_loading_failed(self, e: t.Optional[ValueError]) -> t.Any:
|
||||||
|
try:
|
||||||
|
return super().on_json_loading_failed(e)
|
||||||
|
except BadRequest as e:
|
||||||
|
if current_app and current_app.debug:
|
||||||
|
raise
|
||||||
|
|
||||||
|
raise BadRequest() from e
|
||||||
|
|
||||||
|
|
||||||
|
class Response(ResponseBase):
|
||||||
|
"""The response object that is used by default in Flask. Works like the
|
||||||
|
response object from Werkzeug but is set to have an HTML mimetype by
|
||||||
|
default. Quite often you don't have to create this object yourself because
|
||||||
|
:meth:`~flask.Flask.make_response` will take care of that for you.
|
||||||
|
|
||||||
|
If you want to replace the response object used you can subclass this and
|
||||||
|
set :attr:`~flask.Flask.response_class` to your subclass.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0
|
||||||
|
JSON support is added to the response, like the request. This is useful
|
||||||
|
when testing to get the test client response data as JSON.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.0
|
||||||
|
|
||||||
|
Added :attr:`max_cookie_size`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_mimetype = "text/html"
|
||||||
|
|
||||||
|
json_module = json
|
||||||
|
|
||||||
|
autocorrect_location_header = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_cookie_size(self) -> int: # type: ignore
|
||||||
|
"""Read-only view of the :data:`MAX_COOKIE_SIZE` config key.
|
||||||
|
|
||||||
|
See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in
|
||||||
|
Werkzeug's docs.
|
||||||
|
"""
|
||||||
|
if current_app:
|
||||||
|
return current_app.config["MAX_COOKIE_SIZE"]
|
||||||
|
|
||||||
|
# return Werkzeug's default when not in an app context
|
||||||
|
return super().max_cookie_size
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
@ -0,0 +1,114 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: importlib-metadata
|
||||||
|
Version: 4.12.0
|
||||||
|
Summary: Read metadata from Python packages
|
||||||
|
Home-page: https://github.com/python/importlib_metadata
|
||||||
|
Author: Jason R. Coombs
|
||||||
|
Author-email: jaraco@jaraco.com
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: zipp (>=0.5)
|
||||||
|
Requires-Dist: typing-extensions (>=3.6.4) ; python_version < "3.8"
|
||||||
|
Provides-Extra: docs
|
||||||
|
Requires-Dist: sphinx ; extra == 'docs'
|
||||||
|
Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs'
|
||||||
|
Requires-Dist: rst.linker (>=1.9) ; extra == 'docs'
|
||||||
|
Provides-Extra: perf
|
||||||
|
Requires-Dist: ipython ; extra == 'perf'
|
||||||
|
Provides-Extra: testing
|
||||||
|
Requires-Dist: pytest (>=6) ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-flake8 ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-cov ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing'
|
||||||
|
Requires-Dist: packaging ; extra == 'testing'
|
||||||
|
Requires-Dist: pyfakefs ; extra == 'testing'
|
||||||
|
Requires-Dist: flufl.flake8 ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-perf (>=0.9.2) ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing'
|
||||||
|
Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing'
|
||||||
|
Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing'
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
|
||||||
|
:target: `PyPI link`_
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
|
||||||
|
:target: `PyPI link`_
|
||||||
|
|
||||||
|
.. _PyPI link: https://pypi.org/project/importlib_metadata
|
||||||
|
|
||||||
|
.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg
|
||||||
|
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
|
||||||
|
:alt: tests
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
|
:target: https://github.com/psf/black
|
||||||
|
:alt: Code style: Black
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
|
||||||
|
:target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/skeleton-2022-informational
|
||||||
|
:target: https://blog.jaraco.com/skeleton
|
||||||
|
|
||||||
|
|
||||||
|
Library to access the metadata for a Python package.
|
||||||
|
|
||||||
|
This package supplies third-party access to the functionality of
|
||||||
|
`importlib.metadata <https://docs.python.org/3/library/importlib.metadata.html>`_
|
||||||
|
including improvements added to subsequent Python versions.
|
||||||
|
|
||||||
|
|
||||||
|
Compatibility
|
||||||
|
=============
|
||||||
|
|
||||||
|
New features are introduced in this third-party library and later merged
|
||||||
|
into CPython. The following table indicates which versions of this library
|
||||||
|
were contributed to different versions in the standard library:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - importlib_metadata
|
||||||
|
- stdlib
|
||||||
|
* - 4.8
|
||||||
|
- 3.11
|
||||||
|
* - 4.4
|
||||||
|
- 3.10
|
||||||
|
* - 1.4
|
||||||
|
- 3.8
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
|
||||||
|
for usage details.
|
||||||
|
|
||||||
|
`Finder authors
|
||||||
|
<https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
|
||||||
|
also add support for custom package installers. See the above documentation
|
||||||
|
for details.
|
||||||
|
|
||||||
|
|
||||||
|
Caveats
|
||||||
|
=======
|
||||||
|
|
||||||
|
This project primarily supports third-party packages installed by PyPA
|
||||||
|
tools (or other conforming packages). It does not support:
|
||||||
|
|
||||||
|
- Packages in the stdlib.
|
||||||
|
- Packages installed without metadata.
|
||||||
|
|
||||||
|
Project details
|
||||||
|
===============
|
||||||
|
|
||||||
|
* Project home: https://github.com/python/importlib_metadata
|
||||||
|
* Report bugs at: https://github.com/python/importlib_metadata/issues
|
||||||
|
* Code hosting: https://github.com/python/importlib_metadata
|
||||||
|
* Documentation: https://importlib_metadata.readthedocs.io/
|
@ -0,0 +1,15 @@
|
|||||||
|
importlib_metadata-4.12.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
importlib_metadata-4.12.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
||||||
|
importlib_metadata-4.12.0.dist-info/METADATA,sha256=yrDKK_OYHbefwKP_XCpmmto2CHtqh7uygKjFc3-kVcM,3958
|
||||||
|
importlib_metadata-4.12.0.dist-info/RECORD,,
|
||||||
|
importlib_metadata-4.12.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
importlib_metadata-4.12.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
||||||
|
importlib_metadata/__init__.py,sha256=bfcTHMKmbYRIYLQ80BNARmTHlPE1zjGqOdzKXQywMWo,31383
|
||||||
|
importlib_metadata/_adapters.py,sha256=B6fCi5-8mLVDFUZj3krI5nAo-mKp1dH_qIavyIyFrJs,1862
|
||||||
|
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
||||||
|
importlib_metadata/_compat.py,sha256=9zOKf0eDgkCMnnaEhU5kQVxHd1P8BIYV7Stso7av5h8,1857
|
||||||
|
importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895
|
||||||
|
importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068
|
||||||
|
importlib_metadata/_meta.py,sha256=_F48Hu_jFxkfKWz5wcYS8vO23qEygbVdF9r-6qh-hjE,1154
|
||||||
|
importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166
|
||||||
|
importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
importlib_metadata
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,68 @@
|
|||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
import email.message
|
||||||
|
|
||||||
|
from ._text import FoldedCase
|
||||||
|
|
||||||
|
|
||||||
|
class Message(email.message.Message):
|
||||||
|
multiple_use_keys = set(
|
||||||
|
map(
|
||||||
|
FoldedCase,
|
||||||
|
[
|
||||||
|
'Classifier',
|
||||||
|
'Obsoletes-Dist',
|
||||||
|
'Platform',
|
||||||
|
'Project-URL',
|
||||||
|
'Provides-Dist',
|
||||||
|
'Provides-Extra',
|
||||||
|
'Requires-Dist',
|
||||||
|
'Requires-External',
|
||||||
|
'Supported-Platform',
|
||||||
|
'Dynamic',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
Keys that may be indicated multiple times per PEP 566.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, orig: email.message.Message):
|
||||||
|
res = super().__new__(cls)
|
||||||
|
vars(res).update(vars(orig))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._headers = self._repair_headers()
|
||||||
|
|
||||||
|
# suppress spurious error from mypy
|
||||||
|
def __iter__(self):
|
||||||
|
return super().__iter__()
|
||||||
|
|
||||||
|
def _repair_headers(self):
|
||||||
|
def redent(value):
|
||||||
|
"Correct for RFC822 indentation"
|
||||||
|
if not value or '\n' not in value:
|
||||||
|
return value
|
||||||
|
return textwrap.dedent(' ' * 8 + value)
|
||||||
|
|
||||||
|
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
|
||||||
|
if self._payload:
|
||||||
|
headers.append(('Description', self.get_payload()))
|
||||||
|
return headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
"""
|
||||||
|
Convert PackageMetadata to a JSON-compatible format
|
||||||
|
per PEP 0566.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def transform(key):
|
||||||
|
value = self.get_all(key) if key in self.multiple_use_keys else self[key]
|
||||||
|
if key == 'Keywords':
|
||||||
|
value = re.split(r'\s+', value)
|
||||||
|
tk = key.lower().replace('-', '_')
|
||||||
|
return tk, value
|
||||||
|
|
||||||
|
return dict(map(transform, map(FoldedCase, self)))
|
@ -0,0 +1,30 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
# from jaraco.collections 3.3
|
||||||
|
class FreezableDefaultDict(collections.defaultdict):
|
||||||
|
"""
|
||||||
|
Often it is desirable to prevent the mutation of
|
||||||
|
a default dict after its initial construction, such
|
||||||
|
as to prevent mutation during iteration.
|
||||||
|
|
||||||
|
>>> dd = FreezableDefaultDict(list)
|
||||||
|
>>> dd[0].append('1')
|
||||||
|
>>> dd.freeze()
|
||||||
|
>>> dd[1]
|
||||||
|
[]
|
||||||
|
>>> len(dd)
|
||||||
|
1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
return getattr(self, '_frozen', super().__missing__)(key)
|
||||||
|
|
||||||
|
def freeze(self):
|
||||||
|
self._frozen = lambda key: self.default_factory()
|
||||||
|
|
||||||
|
|
||||||
|
class Pair(collections.namedtuple('Pair', 'name value')):
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, text):
|
||||||
|
return cls(*map(str.strip, text.split("=", 1)))
|
@ -0,0 +1,72 @@
|
|||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['install', 'NullFinder', 'Protocol']
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Protocol
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
# Python 3.7 compatibility
|
||||||
|
from typing_extensions import Protocol # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def install(cls):
|
||||||
|
"""
|
||||||
|
Class decorator for installation on sys.meta_path.
|
||||||
|
|
||||||
|
Adds the backport DistributionFinder to sys.meta_path and
|
||||||
|
attempts to disable the finder functionality of the stdlib
|
||||||
|
DistributionFinder.
|
||||||
|
"""
|
||||||
|
sys.meta_path.append(cls())
|
||||||
|
disable_stdlib_finder()
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
def disable_stdlib_finder():
|
||||||
|
"""
|
||||||
|
Give the backport primacy for discovering path-based distributions
|
||||||
|
by monkey-patching the stdlib O_O.
|
||||||
|
|
||||||
|
See #91 for more background for rationale on this sketchy
|
||||||
|
behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def matches(finder):
|
||||||
|
return getattr(
|
||||||
|
finder, '__module__', None
|
||||||
|
) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions')
|
||||||
|
|
||||||
|
for finder in filter(matches, sys.meta_path): # pragma: nocover
|
||||||
|
del finder.find_distributions
|
||||||
|
|
||||||
|
|
||||||
|
class NullFinder:
|
||||||
|
"""
|
||||||
|
A "Finder" (aka "MetaClassFinder") that never finds any modules,
|
||||||
|
but may find distributions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_spec(*args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# In Python 2, the import system requires finders
|
||||||
|
# to have a find_module() method, but this usage
|
||||||
|
# is deprecated in Python 3 in favor of find_spec().
|
||||||
|
# For the purposes of this finder (i.e. being present
|
||||||
|
# on sys.meta_path but having no other import
|
||||||
|
# system functionality), the two methods are identical.
|
||||||
|
find_module = find_spec
|
||||||
|
|
||||||
|
|
||||||
|
def pypy_partial(val):
|
||||||
|
"""
|
||||||
|
Adjust for variable stacklevel on partial under PyPy.
|
||||||
|
|
||||||
|
Workaround for #327.
|
||||||
|
"""
|
||||||
|
is_pypy = platform.python_implementation() == 'PyPy'
|
||||||
|
return val + is_pypy
|
@ -0,0 +1,104 @@
|
|||||||
|
import types
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
# from jaraco.functools 3.3
|
||||||
|
def method_cache(method, cache_wrapper=None):
|
||||||
|
"""
|
||||||
|
Wrap lru_cache to support storing the cache data in the object instances.
|
||||||
|
|
||||||
|
Abstracts the common paradigm where the method explicitly saves an
|
||||||
|
underscore-prefixed protected property on first call and returns that
|
||||||
|
subsequently.
|
||||||
|
|
||||||
|
>>> class MyClass:
|
||||||
|
... calls = 0
|
||||||
|
...
|
||||||
|
... @method_cache
|
||||||
|
... def method(self, value):
|
||||||
|
... self.calls += 1
|
||||||
|
... return value
|
||||||
|
|
||||||
|
>>> a = MyClass()
|
||||||
|
>>> a.method(3)
|
||||||
|
3
|
||||||
|
>>> for x in range(75):
|
||||||
|
... res = a.method(x)
|
||||||
|
>>> a.calls
|
||||||
|
75
|
||||||
|
|
||||||
|
Note that the apparent behavior will be exactly like that of lru_cache
|
||||||
|
except that the cache is stored on each instance, so values in one
|
||||||
|
instance will not flush values from another, and when an instance is
|
||||||
|
deleted, so are the cached values for that instance.
|
||||||
|
|
||||||
|
>>> b = MyClass()
|
||||||
|
>>> for x in range(35):
|
||||||
|
... res = b.method(x)
|
||||||
|
>>> b.calls
|
||||||
|
35
|
||||||
|
>>> a.method(0)
|
||||||
|
0
|
||||||
|
>>> a.calls
|
||||||
|
75
|
||||||
|
|
||||||
|
Note that if method had been decorated with ``functools.lru_cache()``,
|
||||||
|
a.calls would have been 76 (due to the cached value of 0 having been
|
||||||
|
flushed by the 'b' instance).
|
||||||
|
|
||||||
|
Clear the cache with ``.cache_clear()``
|
||||||
|
|
||||||
|
>>> a.method.cache_clear()
|
||||||
|
|
||||||
|
Same for a method that hasn't yet been called.
|
||||||
|
|
||||||
|
>>> c = MyClass()
|
||||||
|
>>> c.method.cache_clear()
|
||||||
|
|
||||||
|
Another cache wrapper may be supplied:
|
||||||
|
|
||||||
|
>>> cache = functools.lru_cache(maxsize=2)
|
||||||
|
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
|
||||||
|
>>> a = MyClass()
|
||||||
|
>>> a.method2()
|
||||||
|
3
|
||||||
|
|
||||||
|
Caution - do not subsequently wrap the method with another decorator, such
|
||||||
|
as ``@property``, which changes the semantics of the function.
|
||||||
|
|
||||||
|
See also
|
||||||
|
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
|
||||||
|
for another implementation and additional justification.
|
||||||
|
"""
|
||||||
|
cache_wrapper = cache_wrapper or functools.lru_cache()
|
||||||
|
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
# it's the first call, replace the method with a cached, bound method
|
||||||
|
bound_method = types.MethodType(method, self)
|
||||||
|
cached_method = cache_wrapper(bound_method)
|
||||||
|
setattr(self, method.__name__, cached_method)
|
||||||
|
return cached_method(*args, **kwargs)
|
||||||
|
|
||||||
|
# Support cache clear even before cache has been created.
|
||||||
|
wrapper.cache_clear = lambda: None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# From jaraco.functools 3.3
|
||||||
|
def pass_none(func):
|
||||||
|
"""
|
||||||
|
Wrap func so it's not called if its first param is None
|
||||||
|
|
||||||
|
>>> print_text = pass_none(print)
|
||||||
|
>>> print_text('text')
|
||||||
|
text
|
||||||
|
>>> print_text(None)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(param, *args, **kwargs):
|
||||||
|
if param is not None:
|
||||||
|
return func(param, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
@ -0,0 +1,73 @@
|
|||||||
|
from itertools import filterfalse
|
||||||
|
|
||||||
|
|
||||||
|
def unique_everseen(iterable, key=None):
|
||||||
|
"List unique elements, preserving order. Remember all elements ever seen."
|
||||||
|
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||||
|
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||||
|
seen = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
if key is None:
|
||||||
|
for element in filterfalse(seen.__contains__, iterable):
|
||||||
|
seen_add(element)
|
||||||
|
yield element
|
||||||
|
else:
|
||||||
|
for element in iterable:
|
||||||
|
k = key(element)
|
||||||
|
if k not in seen:
|
||||||
|
seen_add(k)
|
||||||
|
yield element
|
||||||
|
|
||||||
|
|
||||||
|
# copied from more_itertools 8.8
|
||||||
|
def always_iterable(obj, base_type=(str, bytes)):
|
||||||
|
"""If *obj* is iterable, return an iterator over its items::
|
||||||
|
|
||||||
|
>>> obj = (1, 2, 3)
|
||||||
|
>>> list(always_iterable(obj))
|
||||||
|
[1, 2, 3]
|
||||||
|
|
||||||
|
If *obj* is not iterable, return a one-item iterable containing *obj*::
|
||||||
|
|
||||||
|
>>> obj = 1
|
||||||
|
>>> list(always_iterable(obj))
|
||||||
|
[1]
|
||||||
|
|
||||||
|
If *obj* is ``None``, return an empty iterable:
|
||||||
|
|
||||||
|
>>> obj = None
|
||||||
|
>>> list(always_iterable(None))
|
||||||
|
[]
|
||||||
|
|
||||||
|
By default, binary and text strings are not considered iterable::
|
||||||
|
|
||||||
|
>>> obj = 'foo'
|
||||||
|
>>> list(always_iterable(obj))
|
||||||
|
['foo']
|
||||||
|
|
||||||
|
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
|
||||||
|
returns ``True`` won't be considered iterable.
|
||||||
|
|
||||||
|
>>> obj = {'a': 1}
|
||||||
|
>>> list(always_iterable(obj)) # Iterate over the dict's keys
|
||||||
|
['a']
|
||||||
|
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
|
||||||
|
[{'a': 1}]
|
||||||
|
|
||||||
|
Set *base_type* to ``None`` to avoid any special handling and treat objects
|
||||||
|
Python considers iterable as iterable:
|
||||||
|
|
||||||
|
>>> obj = 'foo'
|
||||||
|
>>> list(always_iterable(obj, base_type=None))
|
||||||
|
['f', 'o', 'o']
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
if (base_type is not None) and isinstance(obj, base_type):
|
||||||
|
return iter((obj,))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return iter(obj)
|
||||||
|
except TypeError:
|
||||||
|
return iter((obj,))
|
@ -0,0 +1,48 @@
|
|||||||
|
from ._compat import Protocol
|
||||||
|
from typing import Any, Dict, Iterator, List, TypeVar, Union
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class PackageMetadata(Protocol):
|
||||||
|
def __len__(self) -> int:
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def __contains__(self, item: str) -> bool:
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> str:
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
|
||||||
|
"""
|
||||||
|
Return all values associated with a possibly multi-valued key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self) -> Dict[str, Union[str, List[str]]]:
|
||||||
|
"""
|
||||||
|
A JSON-compatible form of the metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePath(Protocol):
|
||||||
|
"""
|
||||||
|
A minimal subset of pathlib.Path required by PathDistribution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def joinpath(self) -> 'SimplePath':
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def __truediv__(self) -> 'SimplePath':
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def parent(self) -> 'SimplePath':
|
||||||
|
... # pragma: no cover
|
||||||
|
|
||||||
|
def read_text(self) -> str:
|
||||||
|
... # pragma: no cover
|
@ -0,0 +1,99 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from ._functools import method_cache
|
||||||
|
|
||||||
|
|
||||||
|
# from jaraco.text 3.5
|
||||||
|
class FoldedCase(str):
|
||||||
|
"""
|
||||||
|
A case insensitive string class; behaves just like str
|
||||||
|
except compares equal when the only variation is case.
|
||||||
|
|
||||||
|
>>> s = FoldedCase('hello world')
|
||||||
|
|
||||||
|
>>> s == 'Hello World'
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> 'Hello World' == s
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> s != 'Hello World'
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> s.index('O')
|
||||||
|
4
|
||||||
|
|
||||||
|
>>> s.split('O')
|
||||||
|
['hell', ' w', 'rld']
|
||||||
|
|
||||||
|
>>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
|
||||||
|
['alpha', 'Beta', 'GAMMA']
|
||||||
|
|
||||||
|
Sequence membership is straightforward.
|
||||||
|
|
||||||
|
>>> "Hello World" in [s]
|
||||||
|
True
|
||||||
|
>>> s in ["Hello World"]
|
||||||
|
True
|
||||||
|
|
||||||
|
You may test for set inclusion, but candidate and elements
|
||||||
|
must both be folded.
|
||||||
|
|
||||||
|
>>> FoldedCase("Hello World") in {s}
|
||||||
|
True
|
||||||
|
>>> s in {FoldedCase("Hello World")}
|
||||||
|
True
|
||||||
|
|
||||||
|
String inclusion works as long as the FoldedCase object
|
||||||
|
is on the right.
|
||||||
|
|
||||||
|
>>> "hello" in FoldedCase("Hello World")
|
||||||
|
True
|
||||||
|
|
||||||
|
But not if the FoldedCase object is on the left:
|
||||||
|
|
||||||
|
>>> FoldedCase('hello') in 'Hello World'
|
||||||
|
False
|
||||||
|
|
||||||
|
In that case, use in_:
|
||||||
|
|
||||||
|
>>> FoldedCase('hello').in_('Hello World')
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> FoldedCase('hello') > FoldedCase('Hello')
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.lower() < other.lower()
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self.lower() > other.lower()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.lower() == other.lower()
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self.lower() != other.lower()
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.lower())
|
||||||
|
|
||||||
|
def __contains__(self, other):
|
||||||
|
return super().lower().__contains__(other.lower())
|
||||||
|
|
||||||
|
def in_(self, other):
|
||||||
|
"Does self appear in other?"
|
||||||
|
return self in FoldedCase(other)
|
||||||
|
|
||||||
|
# cache lower since it's likely to be called frequently.
|
||||||
|
@method_cache
|
||||||
|
def lower(self):
|
||||||
|
return super().lower()
|
||||||
|
|
||||||
|
def index(self, sub):
|
||||||
|
return self.lower().index(sub.lower())
|
||||||
|
|
||||||
|
def split(self, splitter=' ', maxsplit=0):
|
||||||
|
pattern = re.compile(re.escape(splitter), re.I)
|
||||||
|
return pattern.split(self, maxsplit)
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2011 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder 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.
|
@ -0,0 +1,97 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: itsdangerous
|
||||||
|
Version: 2.1.2
|
||||||
|
Summary: Safely pass data to untrusted environments and back.
|
||||||
|
Home-page: https://palletsprojects.com/p/itsdangerous/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://itsdangerous.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/itsdangerous/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/itsdangerous/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
|
||||||
|
ItsDangerous
|
||||||
|
============
|
||||||
|
|
||||||
|
... so better sign this
|
||||||
|
|
||||||
|
Various helpers to pass data to untrusted environments and to get it
|
||||||
|
back safe and sound. Data is cryptographically signed to ensure that a
|
||||||
|
token has not been tampered with.
|
||||||
|
|
||||||
|
It's possible to customize how data is serialized. Data is compressed as
|
||||||
|
needed. A timestamp can be added and verified automatically while
|
||||||
|
loading a token.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
pip install -U itsdangerous
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
A Simple Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Here's how you could generate a token for transmitting a user's id and
|
||||||
|
name between web requests.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from itsdangerous import URLSafeSerializer
|
||||||
|
auth_s = URLSafeSerializer("secret key", "auth")
|
||||||
|
token = auth_s.dumps({"id": 5, "name": "itsdangerous"})
|
||||||
|
|
||||||
|
print(token)
|
||||||
|
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg
|
||||||
|
|
||||||
|
data = auth_s.loads(token)
|
||||||
|
print(data["name"])
|
||||||
|
# itsdangerous
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports ItsDangerous and other
|
||||||
|
popular packages. In order to grow the community of contributors and
|
||||||
|
users, and allow the maintainers to devote more time to the projects,
|
||||||
|
`please donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://itsdangerous.palletsprojects.com/
|
||||||
|
- Changes: https://itsdangerous.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/ItsDangerous/
|
||||||
|
- Source Code: https://github.com/pallets/itsdangerous/
|
||||||
|
- Issue Tracker: https://github.com/pallets/itsdangerous/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/itsdangerous/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
|||||||
|
itsdangerous-2.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
itsdangerous-2.1.2.dist-info/LICENSE.rst,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475
|
||||||
|
itsdangerous-2.1.2.dist-info/METADATA,sha256=ThrHIJQ_6XlfbDMCAVe_hawT7IXiIxnTBIDrwxxtucQ,2928
|
||||||
|
itsdangerous-2.1.2.dist-info/RECORD,,
|
||||||
|
itsdangerous-2.1.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
itsdangerous-2.1.2.dist-info/top_level.txt,sha256=gKN1OKLk81i7fbWWildJA88EQ9NhnGMSvZqhfz9ICjk,13
|
||||||
|
itsdangerous/__init__.py,sha256=n4mkyjlIVn23pgsgCIw0MJKPdcHIetyeRpe5Fwsn8qg,876
|
||||||
|
itsdangerous/_json.py,sha256=wIhs_7-_XZolmyr-JvKNiy_LgAcfevYR0qhCVdlIhg8,450
|
||||||
|
itsdangerous/encoding.py,sha256=pgh86snHC76dPLNCnPlrjR5SaYL_M8H-gWRiiLNbhCU,1419
|
||||||
|
itsdangerous/exc.py,sha256=VFxmP2lMoSJFqxNMzWonqs35ROII4-fvCBfG0v1Tkbs,3206
|
||||||
|
itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
itsdangerous/serializer.py,sha256=zgZ1-U705jHDpt62x_pmLJdryEKDNAbt5UkJtnkcCSw,11144
|
||||||
|
itsdangerous/signer.py,sha256=QUH0iX0in-OTptMAXKU5zWMwmOCXn1fsDsubXiGdFN4,9367
|
||||||
|
itsdangerous/timed.py,sha256=5CBWLds4Nm8-3bFVC8RxNzFjx6PSwjch8wuZ5cwcHFI,8174
|
||||||
|
itsdangerous/url_safe.py,sha256=5bC4jSKOjWNRkWrFseifWVXUnHnPgwOLROjiOwb-eeo,2402
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
itsdangerous
|
@ -0,0 +1,19 @@
|
|||||||
|
from .encoding import base64_decode as base64_decode
|
||||||
|
from .encoding import base64_encode as base64_encode
|
||||||
|
from .encoding import want_bytes as want_bytes
|
||||||
|
from .exc import BadData as BadData
|
||||||
|
from .exc import BadHeader as BadHeader
|
||||||
|
from .exc import BadPayload as BadPayload
|
||||||
|
from .exc import BadSignature as BadSignature
|
||||||
|
from .exc import BadTimeSignature as BadTimeSignature
|
||||||
|
from .exc import SignatureExpired as SignatureExpired
|
||||||
|
from .serializer import Serializer as Serializer
|
||||||
|
from .signer import HMACAlgorithm as HMACAlgorithm
|
||||||
|
from .signer import NoneAlgorithm as NoneAlgorithm
|
||||||
|
from .signer import Signer as Signer
|
||||||
|
from .timed import TimedSerializer as TimedSerializer
|
||||||
|
from .timed import TimestampSigner as TimestampSigner
|
||||||
|
from .url_safe import URLSafeSerializer as URLSafeSerializer
|
||||||
|
from .url_safe import URLSafeTimedSerializer as URLSafeTimedSerializer
|
||||||
|
|
||||||
|
__version__ = "2.1.2"
|
@ -0,0 +1,16 @@
|
|||||||
|
import json as _json
|
||||||
|
import typing as _t
|
||||||
|
|
||||||
|
|
||||||
|
class _CompactJSON:
|
||||||
|
"""Wrapper around json module that strips whitespace."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(payload: _t.Union[str, bytes]) -> _t.Any:
|
||||||
|
return _json.loads(payload)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dumps(obj: _t.Any, **kwargs: _t.Any) -> str:
|
||||||
|
kwargs.setdefault("ensure_ascii", False)
|
||||||
|
kwargs.setdefault("separators", (",", ":"))
|
||||||
|
return _json.dumps(obj, **kwargs)
|
@ -0,0 +1,54 @@
|
|||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import struct
|
||||||
|
import typing as _t
|
||||||
|
|
||||||
|
from .exc import BadData
|
||||||
|
|
||||||
|
_t_str_bytes = _t.Union[str, bytes]
|
||||||
|
|
||||||
|
|
||||||
|
def want_bytes(
|
||||||
|
s: _t_str_bytes, encoding: str = "utf-8", errors: str = "strict"
|
||||||
|
) -> bytes:
|
||||||
|
if isinstance(s, str):
|
||||||
|
s = s.encode(encoding, errors)
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def base64_encode(string: _t_str_bytes) -> bytes:
|
||||||
|
"""Base64 encode a string of bytes or text. The resulting bytes are
|
||||||
|
safe to use in URLs.
|
||||||
|
"""
|
||||||
|
string = want_bytes(string)
|
||||||
|
return base64.urlsafe_b64encode(string).rstrip(b"=")
|
||||||
|
|
||||||
|
|
||||||
|
def base64_decode(string: _t_str_bytes) -> bytes:
|
||||||
|
"""Base64 decode a URL-safe string of bytes or text. The result is
|
||||||
|
bytes.
|
||||||
|
"""
|
||||||
|
string = want_bytes(string, encoding="ascii", errors="ignore")
|
||||||
|
string += b"=" * (-len(string) % 4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return base64.urlsafe_b64decode(string)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise BadData("Invalid base64-encoded data") from e
|
||||||
|
|
||||||
|
|
||||||
|
# The alphabet used by base64.urlsafe_*
|
||||||
|
_base64_alphabet = f"{string.ascii_letters}{string.digits}-_=".encode("ascii")
|
||||||
|
|
||||||
|
_int64_struct = struct.Struct(">Q")
|
||||||
|
_int_to_bytes = _int64_struct.pack
|
||||||
|
_bytes_to_int = _t.cast("_t.Callable[[bytes], _t.Tuple[int]]", _int64_struct.unpack)
|
||||||
|
|
||||||
|
|
||||||
|
def int_to_bytes(num: int) -> bytes:
|
||||||
|
return _int_to_bytes(num).lstrip(b"\x00")
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_int(bytestr: bytes) -> int:
|
||||||
|
return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0]
|
@ -0,0 +1,107 @@
|
|||||||
|
import typing as _t
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
_t_opt_any = _t.Optional[_t.Any]
|
||||||
|
_t_opt_exc = _t.Optional[Exception]
|
||||||
|
|
||||||
|
|
||||||
|
class BadData(Exception):
|
||||||
|
"""Raised if bad data of any sort was encountered. This is the base
|
||||||
|
for all exceptions that ItsDangerous defines.
|
||||||
|
|
||||||
|
.. versionadded:: 0.15
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class BadSignature(BadData):
|
||||||
|
"""Raised if a signature does not match."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, payload: _t_opt_any = None):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
#: The payload that failed the signature test. In some
|
||||||
|
#: situations you might still want to inspect this, even if
|
||||||
|
#: you know it was tampered with.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.14
|
||||||
|
self.payload: _t_opt_any = payload
|
||||||
|
|
||||||
|
|
||||||
|
class BadTimeSignature(BadSignature):
|
||||||
|
"""Raised if a time-based signature is invalid. This is a subclass
|
||||||
|
of :class:`BadSignature`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
payload: _t_opt_any = None,
|
||||||
|
date_signed: _t.Optional[datetime] = None,
|
||||||
|
):
|
||||||
|
super().__init__(message, payload)
|
||||||
|
|
||||||
|
#: If the signature expired this exposes the date of when the
|
||||||
|
#: signature was created. This can be helpful in order to
|
||||||
|
#: tell the user how long a link has been gone stale.
|
||||||
|
#:
|
||||||
|
#: .. versionchanged:: 2.0
|
||||||
|
#: The datetime value is timezone-aware rather than naive.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 0.14
|
||||||
|
self.date_signed = date_signed
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureExpired(BadTimeSignature):
|
||||||
|
"""Raised if a signature timestamp is older than ``max_age``. This
|
||||||
|
is a subclass of :exc:`BadTimeSignature`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BadHeader(BadSignature):
|
||||||
|
"""Raised if a signed header is invalid in some form. This only
|
||||||
|
happens for serializers that have a header that goes with the
|
||||||
|
signature.
|
||||||
|
|
||||||
|
.. versionadded:: 0.24
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
payload: _t_opt_any = None,
|
||||||
|
header: _t_opt_any = None,
|
||||||
|
original_error: _t_opt_exc = None,
|
||||||
|
):
|
||||||
|
super().__init__(message, payload)
|
||||||
|
|
||||||
|
#: If the header is actually available but just malformed it
|
||||||
|
#: might be stored here.
|
||||||
|
self.header: _t_opt_any = header
|
||||||
|
|
||||||
|
#: If available, the error that indicates why the payload was
|
||||||
|
#: not valid. This might be ``None``.
|
||||||
|
self.original_error: _t_opt_exc = original_error
|
||||||
|
|
||||||
|
|
||||||
|
class BadPayload(BadData):
|
||||||
|
"""Raised if a payload is invalid. This could happen if the payload
|
||||||
|
is loaded despite an invalid signature, or if there is a mismatch
|
||||||
|
between the serializer and deserializer. The original exception
|
||||||
|
that occurred during loading is stored on as :attr:`original_error`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.15
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, original_error: _t_opt_exc = None):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
#: If available, the error that indicates why the payload was
|
||||||
|
#: not valid. This might be ``None``.
|
||||||
|
self.original_error: _t_opt_exc = original_error
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue