parent
4013679f2e
commit
6131195d3d
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
export PYTHONPATH="../Sources"
|
||||||
|
./../Resources/LPy64-3105/bin/python3.10 "OrchestratorSettings.py"
|
@ -0,0 +1,8 @@
|
|||||||
|
#!
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pyclip.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
@ -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,59 @@
|
|||||||
|
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/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
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/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/_identifier.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/async_utils.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/bccache.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/compiler.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/constants.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/debug.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/defaults.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/environment.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/exceptions.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/ext.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/filters.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/idtracking.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/lexer.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/loaders.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/meta.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/nativetypes.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/nodes.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/optimizer.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/parser.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/runtime.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/sandbox.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/tests.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/utils.cpython-310.pyc,,
|
||||||
|
jinja2/__pycache__/visitor.cpython-310.pyc,,
|
||||||
|
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,14 @@
|
|||||||
|
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=6B0vZ-Dd34LpGZN_4Y_GbBSl1fSb7keGXRzuHUFcOBA,152
|
||||||
|
MarkupSafe-2.1.1.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||||
|
markupsafe/__init__.py,sha256=xfaUQkKNRTdYWe6HnnJ2HjguFmS-C_0H6g8-Q9VAfkQ,9284
|
||||||
|
markupsafe/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
markupsafe/__pycache__/_native.cpython-310.pyc,,
|
||||||
|
markupsafe/_native.py,sha256=GR86Qvo_GcgKmKreA1WmYN9ud17OFwkww8E-fiW-57s,1713
|
||||||
|
markupsafe/_speedups.c,sha256=X2XvQVtIdcK4Usz70BvkzoOfjTCmQlDkkjYSn-swE0g,7083
|
||||||
|
markupsafe/_speedups.cpython-310-x86_64-linux-gnu.so,sha256=MK7Cz4YNHiq95odgpldQy_64gBMHWU12vH7ZlN8KikE,44224
|
||||||
|
markupsafe/_speedups.pyi,sha256=vfMCsOgbAXRNLUXkyuyonG8uEWKYU4PDqNuMaDELAYw,229
|
||||||
|
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.0)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp310-cp310-manylinux_2_17_x86_64
|
||||||
|
Tag: cp310-cp310-manylinux2014_x86_64
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
markupsafe
|
@ -0,0 +1,37 @@
|
|||||||
|
"""Jinja is a template engine written in pure Python. It provides a
|
||||||
|
non-XML syntax that supports inline expressions and an optional
|
||||||
|
sandboxed environment.
|
||||||
|
"""
|
||||||
|
from .bccache import BytecodeCache as BytecodeCache
|
||||||
|
from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache
|
||||||
|
from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache
|
||||||
|
from .environment import Environment as Environment
|
||||||
|
from .environment import Template as Template
|
||||||
|
from .exceptions import TemplateAssertionError as TemplateAssertionError
|
||||||
|
from .exceptions import TemplateError as TemplateError
|
||||||
|
from .exceptions import TemplateNotFound as TemplateNotFound
|
||||||
|
from .exceptions import TemplateRuntimeError as TemplateRuntimeError
|
||||||
|
from .exceptions import TemplatesNotFound as TemplatesNotFound
|
||||||
|
from .exceptions import TemplateSyntaxError as TemplateSyntaxError
|
||||||
|
from .exceptions import UndefinedError as UndefinedError
|
||||||
|
from .loaders import BaseLoader as BaseLoader
|
||||||
|
from .loaders import ChoiceLoader as ChoiceLoader
|
||||||
|
from .loaders import DictLoader as DictLoader
|
||||||
|
from .loaders import FileSystemLoader as FileSystemLoader
|
||||||
|
from .loaders import FunctionLoader as FunctionLoader
|
||||||
|
from .loaders import ModuleLoader as ModuleLoader
|
||||||
|
from .loaders import PackageLoader as PackageLoader
|
||||||
|
from .loaders import PrefixLoader as PrefixLoader
|
||||||
|
from .runtime import ChainableUndefined as ChainableUndefined
|
||||||
|
from .runtime import DebugUndefined as DebugUndefined
|
||||||
|
from .runtime import make_logging_undefined as make_logging_undefined
|
||||||
|
from .runtime import StrictUndefined as StrictUndefined
|
||||||
|
from .runtime import Undefined as Undefined
|
||||||
|
from .utils import clear_caches as clear_caches
|
||||||
|
from .utils import is_undefined as is_undefined
|
||||||
|
from .utils import pass_context as pass_context
|
||||||
|
from .utils import pass_environment as pass_environment
|
||||||
|
from .utils import pass_eval_context as pass_eval_context
|
||||||
|
from .utils import select_autoescape as select_autoescape
|
||||||
|
|
||||||
|
__version__ = "3.1.2"
|
@ -0,0 +1,84 @@
|
|||||||
|
import inspect
|
||||||
|
import typing as t
|
||||||
|
from functools import WRAPPER_ASSIGNMENTS
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from .utils import _PassArg
|
||||||
|
from .utils import pass_eval_context
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
|
||||||
|
def async_variant(normal_func): # type: ignore
|
||||||
|
def decorator(async_func): # type: ignore
|
||||||
|
pass_arg = _PassArg.from_obj(normal_func)
|
||||||
|
need_eval_context = pass_arg is None
|
||||||
|
|
||||||
|
if pass_arg is _PassArg.environment:
|
||||||
|
|
||||||
|
def is_async(args: t.Any) -> bool:
|
||||||
|
return t.cast(bool, args[0].is_async)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def is_async(args: t.Any) -> bool:
|
||||||
|
return t.cast(bool, args[0].environment.is_async)
|
||||||
|
|
||||||
|
# Take the doc and annotations from the sync function, but the
|
||||||
|
# name from the async function. Pallets-Sphinx-Themes
|
||||||
|
# build_function_directive expects __wrapped__ to point to the
|
||||||
|
# sync function.
|
||||||
|
async_func_attrs = ("__module__", "__name__", "__qualname__")
|
||||||
|
normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs))
|
||||||
|
|
||||||
|
@wraps(normal_func, assigned=normal_func_attrs)
|
||||||
|
@wraps(async_func, assigned=async_func_attrs, updated=())
|
||||||
|
def wrapper(*args, **kwargs): # type: ignore
|
||||||
|
b = is_async(args)
|
||||||
|
|
||||||
|
if need_eval_context:
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
if b:
|
||||||
|
return async_func(*args, **kwargs)
|
||||||
|
|
||||||
|
return normal_func(*args, **kwargs)
|
||||||
|
|
||||||
|
if need_eval_context:
|
||||||
|
wrapper = pass_eval_context(wrapper)
|
||||||
|
|
||||||
|
wrapper.jinja_async_variant = True
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)}
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
|
||||||
|
# Avoid a costly call to isawaitable
|
||||||
|
if type(value) in _common_primitives:
|
||||||
|
return t.cast("V", value)
|
||||||
|
|
||||||
|
if inspect.isawaitable(value):
|
||||||
|
return await t.cast("t.Awaitable[V]", value)
|
||||||
|
|
||||||
|
return t.cast("V", value)
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_aiter(
|
||||||
|
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||||
|
) -> "t.AsyncIterator[V]":
|
||||||
|
if hasattr(iterable, "__aiter__"):
|
||||||
|
async for item in t.cast("t.AsyncIterable[V]", iterable):
|
||||||
|
yield item
|
||||||
|
else:
|
||||||
|
for item in t.cast("t.Iterable[V]", iterable):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_to_list(
|
||||||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||||
|
) -> t.List["V"]:
|
||||||
|
return [x async for x in auto_aiter(value)]
|
@ -0,0 +1,406 @@
|
|||||||
|
"""The optional bytecode cache system. This is useful if you have very
|
||||||
|
complex template situations and the compilation of all those templates
|
||||||
|
slows down your application too much.
|
||||||
|
|
||||||
|
Situations where this is useful are often forking web applications that
|
||||||
|
are initialized on the first request.
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import fnmatch
|
||||||
|
import marshal
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import typing as t
|
||||||
|
from hashlib import sha1
|
||||||
|
from io import BytesIO
|
||||||
|
from types import CodeType
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
class _MemcachedClient(te.Protocol):
|
||||||
|
def get(self, key: str) -> bytes:
|
||||||
|
...
|
||||||
|
|
||||||
|
def set(self, key: str, value: bytes, timeout: t.Optional[int] = None) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
bc_version = 5
|
||||||
|
# Magic bytes to identify Jinja bytecode cache files. Contains the
|
||||||
|
# Python major and minor version to avoid loading incompatible bytecode
|
||||||
|
# if a project upgrades its Python version.
|
||||||
|
bc_magic = (
|
||||||
|
b"j2"
|
||||||
|
+ pickle.dumps(bc_version, 2)
|
||||||
|
+ pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Bucket:
|
||||||
|
"""Buckets are used to store the bytecode for one template. It's created
|
||||||
|
and initialized by the bytecode cache and passed to the loading functions.
|
||||||
|
|
||||||
|
The buckets get an internal checksum from the cache assigned and use this
|
||||||
|
to automatically reject outdated cache material. Individual bytecode
|
||||||
|
cache subclasses don't have to care about cache invalidation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
|
||||||
|
self.environment = environment
|
||||||
|
self.key = key
|
||||||
|
self.checksum = checksum
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Resets the bucket (unloads the bytecode)."""
|
||||||
|
self.code: t.Optional[CodeType] = None
|
||||||
|
|
||||||
|
def load_bytecode(self, f: t.BinaryIO) -> None:
|
||||||
|
"""Loads bytecode from a file or file like object."""
|
||||||
|
# make sure the magic header is correct
|
||||||
|
magic = f.read(len(bc_magic))
|
||||||
|
if magic != bc_magic:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
# the source code of the file changed, we need to reload
|
||||||
|
checksum = pickle.load(f)
|
||||||
|
if self.checksum != checksum:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
# if marshal_load fails then we need to reload
|
||||||
|
try:
|
||||||
|
self.code = marshal.load(f)
|
||||||
|
except (EOFError, ValueError, TypeError):
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
def write_bytecode(self, f: t.IO[bytes]) -> None:
|
||||||
|
"""Dump the bytecode into the file or file like object passed."""
|
||||||
|
if self.code is None:
|
||||||
|
raise TypeError("can't write empty bucket")
|
||||||
|
f.write(bc_magic)
|
||||||
|
pickle.dump(self.checksum, f, 2)
|
||||||
|
marshal.dump(self.code, f)
|
||||||
|
|
||||||
|
def bytecode_from_string(self, string: bytes) -> None:
|
||||||
|
"""Load bytecode from bytes."""
|
||||||
|
self.load_bytecode(BytesIO(string))
|
||||||
|
|
||||||
|
def bytecode_to_string(self) -> bytes:
|
||||||
|
"""Return the bytecode as bytes."""
|
||||||
|
out = BytesIO()
|
||||||
|
self.write_bytecode(out)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class BytecodeCache:
|
||||||
|
"""To implement your own bytecode cache you have to subclass this class
|
||||||
|
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
|
||||||
|
these methods are passed a :class:`~jinja2.bccache.Bucket`.
|
||||||
|
|
||||||
|
A very basic bytecode cache that saves the bytecode on the file system::
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
class MyCache(BytecodeCache):
|
||||||
|
|
||||||
|
def __init__(self, directory):
|
||||||
|
self.directory = directory
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket):
|
||||||
|
filename = path.join(self.directory, bucket.key)
|
||||||
|
if path.exists(filename):
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
bucket.load_bytecode(f)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket):
|
||||||
|
filename = path.join(self.directory, bucket.key)
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
bucket.write_bytecode(f)
|
||||||
|
|
||||||
|
A more advanced version of a filesystem based bytecode cache is part of
|
||||||
|
Jinja.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
"""Subclasses have to override this method to load bytecode into a
|
||||||
|
bucket. If they are not able to find code in the cache for the
|
||||||
|
bucket, it must not do anything.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
"""Subclasses have to override this method to write the bytecode
|
||||||
|
from a bucket back to the cache. If it unable to do so it must not
|
||||||
|
fail silently but raise an exception.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clears the cache. This method is not used by Jinja but should be
|
||||||
|
implemented to allow applications to clear the bytecode cache used
|
||||||
|
by a particular environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_cache_key(
|
||||||
|
self, name: str, filename: t.Optional[t.Union[str]] = None
|
||||||
|
) -> str:
|
||||||
|
"""Returns the unique hash key for this template name."""
|
||||||
|
hash = sha1(name.encode("utf-8"))
|
||||||
|
|
||||||
|
if filename is not None:
|
||||||
|
hash.update(f"|{filename}".encode())
|
||||||
|
|
||||||
|
return hash.hexdigest()
|
||||||
|
|
||||||
|
def get_source_checksum(self, source: str) -> str:
|
||||||
|
"""Returns a checksum for the source."""
|
||||||
|
return sha1(source.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def get_bucket(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
filename: t.Optional[str],
|
||||||
|
source: str,
|
||||||
|
) -> Bucket:
|
||||||
|
"""Return a cache bucket for the given template. All arguments are
|
||||||
|
mandatory but filename may be `None`.
|
||||||
|
"""
|
||||||
|
key = self.get_cache_key(name, filename)
|
||||||
|
checksum = self.get_source_checksum(source)
|
||||||
|
bucket = Bucket(environment, key, checksum)
|
||||||
|
self.load_bytecode(bucket)
|
||||||
|
return bucket
|
||||||
|
|
||||||
|
def set_bucket(self, bucket: Bucket) -> None:
|
||||||
|
"""Put the bucket into the cache."""
|
||||||
|
self.dump_bytecode(bucket)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemBytecodeCache(BytecodeCache):
|
||||||
|
"""A bytecode cache that stores bytecode on the filesystem. It accepts
|
||||||
|
two arguments: The directory where the cache items are stored and a
|
||||||
|
pattern string that is used to build the filename.
|
||||||
|
|
||||||
|
If no directory is specified a default cache directory is selected. On
|
||||||
|
Windows the user's temp directory is used, on UNIX systems a directory
|
||||||
|
is created for the user in the system temp directory.
|
||||||
|
|
||||||
|
The pattern can be used to have multiple separate caches operate on the
|
||||||
|
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
|
||||||
|
is replaced with the cache key.
|
||||||
|
|
||||||
|
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
|
||||||
|
|
||||||
|
This bytecode cache supports clearing of the cache using the clear method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
|
||||||
|
) -> None:
|
||||||
|
if directory is None:
|
||||||
|
directory = self._get_default_cache_dir()
|
||||||
|
self.directory = directory
|
||||||
|
self.pattern = pattern
|
||||||
|
|
||||||
|
def _get_default_cache_dir(self) -> str:
|
||||||
|
def _unsafe_dir() -> "te.NoReturn":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot determine safe temp directory. You "
|
||||||
|
"need to explicitly provide one."
|
||||||
|
)
|
||||||
|
|
||||||
|
tmpdir = tempfile.gettempdir()
|
||||||
|
|
||||||
|
# On windows the temporary directory is used specific unless
|
||||||
|
# explicitly forced otherwise. We can just use that.
|
||||||
|
if os.name == "nt":
|
||||||
|
return tmpdir
|
||||||
|
if not hasattr(os, "getuid"):
|
||||||
|
_unsafe_dir()
|
||||||
|
|
||||||
|
dirname = f"_jinja2-cache-{os.getuid()}"
|
||||||
|
actual_dir = os.path.join(tmpdir, dirname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(actual_dir, stat.S_IRWXU)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
os.chmod(actual_dir, stat.S_IRWXU)
|
||||||
|
actual_dir_stat = os.lstat(actual_dir)
|
||||||
|
if (
|
||||||
|
actual_dir_stat.st_uid != os.getuid()
|
||||||
|
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||||
|
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||||
|
):
|
||||||
|
_unsafe_dir()
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
|
actual_dir_stat = os.lstat(actual_dir)
|
||||||
|
if (
|
||||||
|
actual_dir_stat.st_uid != os.getuid()
|
||||||
|
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||||
|
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||||
|
):
|
||||||
|
_unsafe_dir()
|
||||||
|
|
||||||
|
return actual_dir
|
||||||
|
|
||||||
|
def _get_cache_filename(self, bucket: Bucket) -> str:
|
||||||
|
return os.path.join(self.directory, self.pattern % (bucket.key,))
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
filename = self._get_cache_filename(bucket)
|
||||||
|
|
||||||
|
# Don't test for existence before opening the file, since the
|
||||||
|
# file could disappear after the test before the open.
|
||||||
|
try:
|
||||||
|
f = open(filename, "rb")
|
||||||
|
except (FileNotFoundError, IsADirectoryError, PermissionError):
|
||||||
|
# PermissionError can occur on Windows when an operation is
|
||||||
|
# in progress, such as calling clear().
|
||||||
|
return
|
||||||
|
|
||||||
|
with f:
|
||||||
|
bucket.load_bytecode(f)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
# Write to a temporary file, then rename to the real name after
|
||||||
|
# writing. This avoids another process reading the file before
|
||||||
|
# it is fully written.
|
||||||
|
name = self._get_cache_filename(bucket)
|
||||||
|
f = tempfile.NamedTemporaryFile(
|
||||||
|
mode="wb",
|
||||||
|
dir=os.path.dirname(name),
|
||||||
|
prefix=os.path.basename(name),
|
||||||
|
suffix=".tmp",
|
||||||
|
delete=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_silent() -> None:
|
||||||
|
try:
|
||||||
|
os.remove(f.name)
|
||||||
|
except OSError:
|
||||||
|
# Another process may have called clear(). On Windows,
|
||||||
|
# another program may be holding the file open.
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
with f:
|
||||||
|
bucket.write_bytecode(f)
|
||||||
|
except BaseException:
|
||||||
|
remove_silent()
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.replace(f.name, name)
|
||||||
|
except OSError:
|
||||||
|
# Another process may have called clear(). On Windows,
|
||||||
|
# another program may be holding the file open.
|
||||||
|
remove_silent()
|
||||||
|
except BaseException:
|
||||||
|
remove_silent()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
# imported lazily here because google app-engine doesn't support
|
||||||
|
# write access on the file system and the function does not exist
|
||||||
|
# normally.
|
||||||
|
from os import remove
|
||||||
|
|
||||||
|
files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
|
||||||
|
for filename in files:
|
||||||
|
try:
|
||||||
|
remove(os.path.join(self.directory, filename))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedBytecodeCache(BytecodeCache):
|
||||||
|
"""This class implements a bytecode cache that uses a memcache cache for
|
||||||
|
storing the information. It does not enforce a specific memcache library
|
||||||
|
(tummy's memcache or cmemcache) but will accept any class that provides
|
||||||
|
the minimal interface required.
|
||||||
|
|
||||||
|
Libraries compatible with this class:
|
||||||
|
|
||||||
|
- `cachelib <https://github.com/pallets/cachelib>`_
|
||||||
|
- `python-memcached <https://pypi.org/project/python-memcached/>`_
|
||||||
|
|
||||||
|
(Unfortunately the django cache interface is not compatible because it
|
||||||
|
does not support storing binary data, only text. You can however pass
|
||||||
|
the underlying cache client to the bytecode cache which is available
|
||||||
|
as `django.core.cache.cache._client`.)
|
||||||
|
|
||||||
|
The minimal interface for the client passed to the constructor is this:
|
||||||
|
|
||||||
|
.. class:: MinimalClientInterface
|
||||||
|
|
||||||
|
.. method:: set(key, value[, timeout])
|
||||||
|
|
||||||
|
Stores the bytecode in the cache. `value` is a string and
|
||||||
|
`timeout` the timeout of the key. If timeout is not provided
|
||||||
|
a default timeout or no timeout should be assumed, if it's
|
||||||
|
provided it's an integer with the number of seconds the cache
|
||||||
|
item should exist.
|
||||||
|
|
||||||
|
.. method:: get(key)
|
||||||
|
|
||||||
|
Returns the value for the cache key. If the item does not
|
||||||
|
exist in the cache the return value must be `None`.
|
||||||
|
|
||||||
|
The other arguments to the constructor are the prefix for all keys that
|
||||||
|
is added before the actual cache key and the timeout for the bytecode in
|
||||||
|
the cache system. We recommend a high (or no) timeout.
|
||||||
|
|
||||||
|
This bytecode cache does not support clearing of used items in the cache.
|
||||||
|
The clear method is a no-operation function.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7
|
||||||
|
Added support for ignoring memcache errors through the
|
||||||
|
`ignore_memcache_errors` parameter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: "_MemcachedClient",
|
||||||
|
prefix: str = "jinja2/bytecode/",
|
||||||
|
timeout: t.Optional[int] = None,
|
||||||
|
ignore_memcache_errors: bool = True,
|
||||||
|
):
|
||||||
|
self.client = client
|
||||||
|
self.prefix = prefix
|
||||||
|
self.timeout = timeout
|
||||||
|
self.ignore_memcache_errors = ignore_memcache_errors
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
try:
|
||||||
|
code = self.client.get(self.prefix + bucket.key)
|
||||||
|
except Exception:
|
||||||
|
if not self.ignore_memcache_errors:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
bucket.bytecode_from_string(code)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
key = self.prefix + bucket.key
|
||||||
|
value = bucket.bytecode_to_string()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.timeout is not None:
|
||||||
|
self.client.set(key, value, self.timeout)
|
||||||
|
else:
|
||||||
|
self.client.set(key, value)
|
||||||
|
except Exception:
|
||||||
|
if not self.ignore_memcache_errors:
|
||||||
|
raise
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
|
|||||||
|
#: list of lorem ipsum words used by the lipsum() helper function
|
||||||
|
LOREM_IPSUM_WORDS = """\
|
||||||
|
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
|
||||||
|
auctor augue bibendum blandit class commodo condimentum congue consectetuer
|
||||||
|
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
|
||||||
|
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
|
||||||
|
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
|
||||||
|
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
|
||||||
|
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
|
||||||
|
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
|
||||||
|
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
|
||||||
|
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
|
||||||
|
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
|
||||||
|
penatibus per pharetra phasellus placerat platea porta porttitor posuere
|
||||||
|
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
|
||||||
|
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
|
||||||
|
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
|
||||||
|
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
|
||||||
|
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
|
||||||
|
viverra volutpat vulputate"""
|
@ -0,0 +1,191 @@
|
|||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from types import CodeType
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .utils import internal_code
|
||||||
|
from .utils import missing
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .runtime import Context
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
|
||||||
|
"""Rewrite the current exception to replace any tracebacks from
|
||||||
|
within compiled template code with tracebacks that look like they
|
||||||
|
came from the template source.
|
||||||
|
|
||||||
|
This must be called within an ``except`` block.
|
||||||
|
|
||||||
|
:param source: For ``TemplateSyntaxError``, the original source if
|
||||||
|
known.
|
||||||
|
:return: The original exception with the rewritten traceback.
|
||||||
|
"""
|
||||||
|
_, exc_value, tb = sys.exc_info()
|
||||||
|
exc_value = t.cast(BaseException, exc_value)
|
||||||
|
tb = t.cast(TracebackType, tb)
|
||||||
|
|
||||||
|
if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:
|
||||||
|
exc_value.translated = True
|
||||||
|
exc_value.source = source
|
||||||
|
# Remove the old traceback, otherwise the frames from the
|
||||||
|
# compiler still show up.
|
||||||
|
exc_value.with_traceback(None)
|
||||||
|
# Outside of runtime, so the frame isn't executing template
|
||||||
|
# code, but it still needs to point at the template.
|
||||||
|
tb = fake_traceback(
|
||||||
|
exc_value, None, exc_value.filename or "<unknown>", exc_value.lineno
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Skip the frame for the render function.
|
||||||
|
tb = tb.tb_next
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
|
||||||
|
# Build the stack of traceback object, replacing any in template
|
||||||
|
# code with the source file and line information.
|
||||||
|
while tb is not None:
|
||||||
|
# Skip frames decorated with @internalcode. These are internal
|
||||||
|
# calls that aren't useful in template debugging output.
|
||||||
|
if tb.tb_frame.f_code in internal_code:
|
||||||
|
tb = tb.tb_next
|
||||||
|
continue
|
||||||
|
|
||||||
|
template = tb.tb_frame.f_globals.get("__jinja_template__")
|
||||||
|
|
||||||
|
if template is not None:
|
||||||
|
lineno = template.get_corresponding_lineno(tb.tb_lineno)
|
||||||
|
fake_tb = fake_traceback(exc_value, tb, template.filename, lineno)
|
||||||
|
stack.append(fake_tb)
|
||||||
|
else:
|
||||||
|
stack.append(tb)
|
||||||
|
|
||||||
|
tb = tb.tb_next
|
||||||
|
|
||||||
|
tb_next = None
|
||||||
|
|
||||||
|
# Assign tb_next in reverse to avoid circular references.
|
||||||
|
for tb in reversed(stack):
|
||||||
|
tb.tb_next = tb_next
|
||||||
|
tb_next = tb
|
||||||
|
|
||||||
|
return exc_value.with_traceback(tb_next)
|
||||||
|
|
||||||
|
|
||||||
|
def fake_traceback( # type: ignore
|
||||||
|
exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
|
||||||
|
) -> TracebackType:
|
||||||
|
"""Produce a new traceback object that looks like it came from the
|
||||||
|
template source instead of the compiled code. The filename, line
|
||||||
|
number, and location name will point to the template, and the local
|
||||||
|
variables will be the current template context.
|
||||||
|
|
||||||
|
:param exc_value: The original exception to be re-raised to create
|
||||||
|
the new traceback.
|
||||||
|
:param tb: The original traceback to get the local variables and
|
||||||
|
code info from.
|
||||||
|
:param filename: The template filename.
|
||||||
|
:param lineno: The line number in the template source.
|
||||||
|
"""
|
||||||
|
if tb is not None:
|
||||||
|
# Replace the real locals with the context that would be
|
||||||
|
# available at that point in the template.
|
||||||
|
locals = get_template_locals(tb.tb_frame.f_locals)
|
||||||
|
locals.pop("__jinja_exception__", None)
|
||||||
|
else:
|
||||||
|
locals = {}
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"__name__": filename,
|
||||||
|
"__file__": filename,
|
||||||
|
"__jinja_exception__": exc_value,
|
||||||
|
}
|
||||||
|
# Raise an exception at the correct line number.
|
||||||
|
code: CodeType = compile(
|
||||||
|
"\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build a new code object that points to the template file and
|
||||||
|
# replaces the location with a block name.
|
||||||
|
location = "template"
|
||||||
|
|
||||||
|
if tb is not None:
|
||||||
|
function = tb.tb_frame.f_code.co_name
|
||||||
|
|
||||||
|
if function == "root":
|
||||||
|
location = "top-level template code"
|
||||||
|
elif function.startswith("block_"):
|
||||||
|
location = f"block {function[6:]!r}"
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
code = code.replace(co_name=location)
|
||||||
|
else:
|
||||||
|
code = CodeType(
|
||||||
|
code.co_argcount,
|
||||||
|
code.co_kwonlyargcount,
|
||||||
|
code.co_nlocals,
|
||||||
|
code.co_stacksize,
|
||||||
|
code.co_flags,
|
||||||
|
code.co_code,
|
||||||
|
code.co_consts,
|
||||||
|
code.co_names,
|
||||||
|
code.co_varnames,
|
||||||
|
code.co_filename,
|
||||||
|
location,
|
||||||
|
code.co_firstlineno,
|
||||||
|
code.co_lnotab,
|
||||||
|
code.co_freevars,
|
||||||
|
code.co_cellvars,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute the new code, which is guaranteed to raise, and return
|
||||||
|
# the new traceback without this frame.
|
||||||
|
try:
|
||||||
|
exec(code, globals, locals)
|
||||||
|
except BaseException:
|
||||||
|
return sys.exc_info()[2].tb_next # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:
|
||||||
|
"""Based on the runtime locals, get the context that would be
|
||||||
|
available at that point in the template.
|
||||||
|
"""
|
||||||
|
# Start with the current template context.
|
||||||
|
ctx: "t.Optional[Context]" = real_locals.get("context")
|
||||||
|
|
||||||
|
if ctx is not None:
|
||||||
|
data: t.Dict[str, t.Any] = ctx.get_all().copy()
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Might be in a derived context that only sets local variables
|
||||||
|
# rather than pushing a context. Local variables follow the scheme
|
||||||
|
# l_depth_name. Find the highest-depth local that has a value for
|
||||||
|
# each name.
|
||||||
|
local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}
|
||||||
|
|
||||||
|
for name, value in real_locals.items():
|
||||||
|
if not name.startswith("l_") or value is missing:
|
||||||
|
# Not a template variable, or no longer relevant.
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, depth_str, name = name.split("_", 2)
|
||||||
|
depth = int(depth_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur_depth = local_overrides.get(name, (-1,))[0]
|
||||||
|
|
||||||
|
if cur_depth < depth:
|
||||||
|
local_overrides[name] = (depth, value)
|
||||||
|
|
||||||
|
# Modify the context with any derived context.
|
||||||
|
for name, (_, value) in local_overrides.items():
|
||||||
|
if value is missing:
|
||||||
|
data.pop(name, None)
|
||||||
|
else:
|
||||||
|
data[name] = value
|
||||||
|
|
||||||
|
return data
|
@ -0,0 +1,48 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401
|
||||||
|
from .tests import TESTS as DEFAULT_TESTS # noqa: F401
|
||||||
|
from .utils import Cycler
|
||||||
|
from .utils import generate_lorem_ipsum
|
||||||
|
from .utils import Joiner
|
||||||
|
from .utils import Namespace
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
# defaults for the parser / lexer
|
||||||
|
BLOCK_START_STRING = "{%"
|
||||||
|
BLOCK_END_STRING = "%}"
|
||||||
|
VARIABLE_START_STRING = "{{"
|
||||||
|
VARIABLE_END_STRING = "}}"
|
||||||
|
COMMENT_START_STRING = "{#"
|
||||||
|
COMMENT_END_STRING = "#}"
|
||||||
|
LINE_STATEMENT_PREFIX: t.Optional[str] = None
|
||||||
|
LINE_COMMENT_PREFIX: t.Optional[str] = None
|
||||||
|
TRIM_BLOCKS = False
|
||||||
|
LSTRIP_BLOCKS = False
|
||||||
|
NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n"
|
||||||
|
KEEP_TRAILING_NEWLINE = False
|
||||||
|
|
||||||
|
# default filters, tests and namespace
|
||||||
|
|
||||||
|
DEFAULT_NAMESPACE = {
|
||||||
|
"range": range,
|
||||||
|
"dict": dict,
|
||||||
|
"lipsum": generate_lorem_ipsum,
|
||||||
|
"cycler": Cycler,
|
||||||
|
"joiner": Joiner,
|
||||||
|
"namespace": Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
# default policies
|
||||||
|
DEFAULT_POLICIES: t.Dict[str, t.Any] = {
|
||||||
|
"compiler.ascii_str": True,
|
||||||
|
"urlize.rel": "noopener",
|
||||||
|
"urlize.target": None,
|
||||||
|
"urlize.extra_schemes": None,
|
||||||
|
"truncate.leeway": 5,
|
||||||
|
"json.dumps_function": None,
|
||||||
|
"json.dumps_kwargs": {"sort_keys": True},
|
||||||
|
"ext.i18n.trimmed": False,
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,166 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateError(Exception):
|
||||||
|
"""Baseclass for all template errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: t.Optional[str] = None) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self) -> t.Optional[str]:
|
||||||
|
return self.args[0] if self.args else None
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateNotFound(IOError, LookupError, TemplateError):
|
||||||
|
"""Raised if a template does not exist.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.11
|
||||||
|
If the given name is :class:`Undefined` and no message was
|
||||||
|
provided, an :exc:`UndefinedError` is raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Silence the Python warning about message being deprecated since
|
||||||
|
# it's not valid here.
|
||||||
|
message: t.Optional[str] = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: t.Optional[t.Union[str, "Undefined"]],
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
IOError.__init__(self, name)
|
||||||
|
|
||||||
|
if message is None:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
if isinstance(name, Undefined):
|
||||||
|
name._fail_with_undefined_error()
|
||||||
|
|
||||||
|
message = name
|
||||||
|
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.templates = [name]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatesNotFound(TemplateNotFound):
|
||||||
|
"""Like :class:`TemplateNotFound` but raised if multiple templates
|
||||||
|
are selected. This is a subclass of :class:`TemplateNotFound`
|
||||||
|
exception, so just catching the base exception will catch both.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.11
|
||||||
|
If a name in the list of names is :class:`Undefined`, a message
|
||||||
|
about it being undefined is shown rather than the empty string.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
names: t.Sequence[t.Union[str, "Undefined"]] = (),
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
if message is None:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
if isinstance(name, Undefined):
|
||||||
|
parts.append(name._undefined_message)
|
||||||
|
else:
|
||||||
|
parts.append(name)
|
||||||
|
|
||||||
|
parts_str = ", ".join(map(str, parts))
|
||||||
|
message = f"none of the templates given were found: {parts_str}"
|
||||||
|
|
||||||
|
super().__init__(names[-1] if names else None, message)
|
||||||
|
self.templates = list(names)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateSyntaxError(TemplateError):
|
||||||
|
"""Raised to tell the user that there is a problem with the template."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
lineno: int,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.lineno = lineno
|
||||||
|
self.name = name
|
||||||
|
self.filename = filename
|
||||||
|
self.source: t.Optional[str] = None
|
||||||
|
|
||||||
|
# this is set to True if the debug.translate_syntax_error
|
||||||
|
# function translated the syntax error into a new traceback
|
||||||
|
self.translated = False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
# for translated errors we only return the message
|
||||||
|
if self.translated:
|
||||||
|
return t.cast(str, self.message)
|
||||||
|
|
||||||
|
# otherwise attach some stuff
|
||||||
|
location = f"line {self.lineno}"
|
||||||
|
name = self.filename or self.name
|
||||||
|
if name:
|
||||||
|
location = f'File "{name}", {location}'
|
||||||
|
lines = [t.cast(str, self.message), " " + location]
|
||||||
|
|
||||||
|
# if the source is set, add the line to the output
|
||||||
|
if self.source is not None:
|
||||||
|
try:
|
||||||
|
line = self.source.splitlines()[self.lineno - 1]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
lines.append(" " + line.strip())
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def __reduce__(self): # type: ignore
|
||||||
|
# https://bugs.python.org/issue1692335 Exceptions that take
|
||||||
|
# multiple required arguments have problems with pickling.
|
||||||
|
# Without this, raises TypeError: __init__() missing 1 required
|
||||||
|
# positional argument: 'lineno'
|
||||||
|
return self.__class__, (self.message, self.lineno, self.name, self.filename)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateAssertionError(TemplateSyntaxError):
|
||||||
|
"""Like a template syntax error, but covers cases where something in the
|
||||||
|
template caused an error at compile time that wasn't necessarily caused
|
||||||
|
by a syntax error. However it's a direct subclass of
|
||||||
|
:exc:`TemplateSyntaxError` and has the same attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateRuntimeError(TemplateError):
|
||||||
|
"""A generic runtime error in the template engine. Under some situations
|
||||||
|
Jinja may raise this exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UndefinedError(TemplateRuntimeError):
|
||||||
|
"""Raised if a template tries to operate on :class:`Undefined`."""
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityError(TemplateRuntimeError):
|
||||||
|
"""Raised if a template tries to do something insecure if the
|
||||||
|
sandbox is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FilterArgumentError(TemplateRuntimeError):
|
||||||
|
"""This error is raised if a filter was called with inappropriate
|
||||||
|
arguments
|
||||||
|
"""
|
@ -0,0 +1,859 @@
|
|||||||
|
"""Extension API for adding custom tags and behavior."""
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from . import defaults
|
||||||
|
from . import nodes
|
||||||
|
from .environment import Environment
|
||||||
|
from .exceptions import TemplateAssertionError
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .runtime import concat # type: ignore
|
||||||
|
from .runtime import Context
|
||||||
|
from .runtime import Undefined
|
||||||
|
from .utils import import_string
|
||||||
|
from .utils import pass_context
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .lexer import Token
|
||||||
|
from .lexer import TokenStream
|
||||||
|
from .parser import Parser
|
||||||
|
|
||||||
|
class _TranslationsBasic(te.Protocol):
|
||||||
|
def gettext(self, message: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
def ngettext(self, singular: str, plural: str, n: int) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _TranslationsContext(_TranslationsBasic):
|
||||||
|
def pgettext(self, context: str, message: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
_SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
|
||||||
|
|
||||||
|
|
||||||
|
# I18N functions available in Jinja templates. If the I18N library
|
||||||
|
# provides ugettext, it will be assigned to gettext.
|
||||||
|
GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
|
||||||
|
"_",
|
||||||
|
"gettext",
|
||||||
|
"ngettext",
|
||||||
|
"pgettext",
|
||||||
|
"npgettext",
|
||||||
|
)
|
||||||
|
_ws_re = re.compile(r"\s*\n\s*")
|
||||||
|
|
||||||
|
|
||||||
|
class Extension:
|
||||||
|
"""Extensions can be used to add extra functionality to the Jinja template
|
||||||
|
system at the parser level. Custom extensions are bound to an environment
|
||||||
|
but may not store environment specific data on `self`. The reason for
|
||||||
|
this is that an extension can be bound to another environment (for
|
||||||
|
overlays) by creating a copy and reassigning the `environment` attribute.
|
||||||
|
|
||||||
|
As extensions are created by the environment they cannot accept any
|
||||||
|
arguments for configuration. One may want to work around that by using
|
||||||
|
a factory function, but that is not possible as extensions are identified
|
||||||
|
by their import name. The correct way to configure the extension is
|
||||||
|
storing the configuration values on the environment. Because this way the
|
||||||
|
environment ends up acting as central configuration storage the
|
||||||
|
attributes may clash which is why extensions have to ensure that the names
|
||||||
|
they choose for configuration are not too generic. ``prefix`` for example
|
||||||
|
is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
|
||||||
|
name as includes the name of the extension (fragment cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
identifier: t.ClassVar[str]
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
cls.identifier = f"{cls.__module__}.{cls.__name__}"
|
||||||
|
|
||||||
|
#: if this extension parses this is the list of tags it's listening to.
|
||||||
|
tags: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: the priority of that extension. This is especially useful for
|
||||||
|
#: extensions that preprocess values. A lower value means higher
|
||||||
|
#: priority.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.4
|
||||||
|
priority = 100
|
||||||
|
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
def bind(self, environment: Environment) -> "Extension":
|
||||||
|
"""Create a copy of this extension bound to another environment."""
|
||||||
|
rv = object.__new__(self.__class__)
|
||||||
|
rv.__dict__.update(self.__dict__)
|
||||||
|
rv.environment = environment
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def preprocess(
|
||||||
|
self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""This method is called before the actual lexing and can be used to
|
||||||
|
preprocess the source. The `filename` is optional. The return value
|
||||||
|
must be the preprocessed source.
|
||||||
|
"""
|
||||||
|
return source
|
||||||
|
|
||||||
|
def filter_stream(
|
||||||
|
self, stream: "TokenStream"
|
||||||
|
) -> t.Union["TokenStream", t.Iterable["Token"]]:
|
||||||
|
"""It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
|
||||||
|
to filter tokens returned. This method has to return an iterable of
|
||||||
|
:class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a
|
||||||
|
:class:`~jinja2.lexer.TokenStream`.
|
||||||
|
"""
|
||||||
|
return stream
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||||
|
"""If any of the :attr:`tags` matched this method is called with the
|
||||||
|
parser as first argument. The token the parser stream is pointing at
|
||||||
|
is the name token that matched. This method has to return one or a
|
||||||
|
list of multiple nodes.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def attr(
|
||||||
|
self, name: str, lineno: t.Optional[int] = None
|
||||||
|
) -> nodes.ExtensionAttribute:
|
||||||
|
"""Return an attribute node for the current extension. This is useful
|
||||||
|
to pass constants on extensions to generated template code.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
self.attr('_my_attribute', lineno=lineno)
|
||||||
|
"""
|
||||||
|
return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
|
||||||
|
|
||||||
|
def call_method(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
args: t.Optional[t.List[nodes.Expr]] = None,
|
||||||
|
kwargs: t.Optional[t.List[nodes.Keyword]] = None,
|
||||||
|
dyn_args: t.Optional[nodes.Expr] = None,
|
||||||
|
dyn_kwargs: t.Optional[nodes.Expr] = None,
|
||||||
|
lineno: t.Optional[int] = None,
|
||||||
|
) -> nodes.Call:
|
||||||
|
"""Call a method of the extension. This is a shortcut for
|
||||||
|
:meth:`attr` + :class:`jinja2.nodes.Call`.
|
||||||
|
"""
|
||||||
|
if args is None:
|
||||||
|
args = []
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = []
|
||||||
|
return nodes.Call(
|
||||||
|
self.attr(name, lineno=lineno),
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
dyn_args,
|
||||||
|
dyn_kwargs,
|
||||||
|
lineno=lineno,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_context
|
||||||
|
def _gettext_alias(
|
||||||
|
__context: Context, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> t.Union[t.Any, Undefined]:
|
||||||
|
return __context.call(__context.resolve("gettext"), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
|
||||||
|
rv = __context.call(func, __string)
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
# Always treat as a format string, even if there are no
|
||||||
|
# variables. This makes translation strings more consistent
|
||||||
|
# and predictable. This requires escaping
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return gettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def ngettext(
|
||||||
|
__context: Context,
|
||||||
|
__singular: str,
|
||||||
|
__plural: str,
|
||||||
|
__num: int,
|
||||||
|
**variables: t.Any,
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("num", __num)
|
||||||
|
rv = __context.call(func, __singular, __plural, __num)
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return ngettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def pgettext(
|
||||||
|
__context: Context, __string_ctx: str, __string: str, **variables: t.Any
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("context", __string_ctx)
|
||||||
|
rv = __context.call(func, __string_ctx, __string)
|
||||||
|
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return pgettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_npgettext(
|
||||||
|
func: t.Callable[[str, str, str, int], str]
|
||||||
|
) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def npgettext(
|
||||||
|
__context: Context,
|
||||||
|
__string_ctx: str,
|
||||||
|
__singular: str,
|
||||||
|
__plural: str,
|
||||||
|
__num: int,
|
||||||
|
**variables: t.Any,
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("context", __string_ctx)
|
||||||
|
variables.setdefault("num", __num)
|
||||||
|
rv = __context.call(func, __string_ctx, __singular, __plural, __num)
|
||||||
|
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return npgettext
|
||||||
|
|
||||||
|
|
||||||
|
class InternationalizationExtension(Extension):
|
||||||
|
"""This extension adds gettext support to Jinja."""
|
||||||
|
|
||||||
|
tags = {"trans"}
|
||||||
|
|
||||||
|
# TODO: the i18n extension is currently reevaluating values in a few
|
||||||
|
# situations. Take this example:
|
||||||
|
# {% trans count=something() %}{{ count }} foo{% pluralize
|
||||||
|
# %}{{ count }} fooss{% endtrans %}
|
||||||
|
# something is called twice here. One time for the gettext value and
|
||||||
|
# the other time for the n-parameter of the ngettext function.
|
||||||
|
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
super().__init__(environment)
|
||||||
|
environment.globals["_"] = _gettext_alias
|
||||||
|
environment.extend(
|
||||||
|
install_gettext_translations=self._install,
|
||||||
|
install_null_translations=self._install_null,
|
||||||
|
install_gettext_callables=self._install_callables,
|
||||||
|
uninstall_gettext_translations=self._uninstall,
|
||||||
|
extract_translations=self._extract,
|
||||||
|
newstyle_gettext=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install(
|
||||||
|
self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
|
||||||
|
) -> None:
|
||||||
|
# ugettext and ungettext are preferred in case the I18N library
|
||||||
|
# is providing compatibility with older Python versions.
|
||||||
|
gettext = getattr(translations, "ugettext", None)
|
||||||
|
if gettext is None:
|
||||||
|
gettext = translations.gettext
|
||||||
|
ngettext = getattr(translations, "ungettext", None)
|
||||||
|
if ngettext is None:
|
||||||
|
ngettext = translations.ngettext
|
||||||
|
|
||||||
|
pgettext = getattr(translations, "pgettext", None)
|
||||||
|
npgettext = getattr(translations, "npgettext", None)
|
||||||
|
self._install_callables(
|
||||||
|
gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
|
||||||
|
import gettext
|
||||||
|
|
||||||
|
translations = gettext.NullTranslations()
|
||||||
|
|
||||||
|
if hasattr(translations, "pgettext"):
|
||||||
|
# Python < 3.8
|
||||||
|
pgettext = translations.pgettext # type: ignore
|
||||||
|
else:
|
||||||
|
|
||||||
|
def pgettext(c: str, s: str) -> str:
|
||||||
|
return s
|
||||||
|
|
||||||
|
if hasattr(translations, "npgettext"):
|
||||||
|
npgettext = translations.npgettext # type: ignore
|
||||||
|
else:
|
||||||
|
|
||||||
|
def npgettext(c: str, s: str, p: str, n: int) -> str:
|
||||||
|
return s if n == 1 else p
|
||||||
|
|
||||||
|
self._install_callables(
|
||||||
|
gettext=translations.gettext,
|
||||||
|
ngettext=translations.ngettext,
|
||||||
|
newstyle=newstyle,
|
||||||
|
pgettext=pgettext,
|
||||||
|
npgettext=npgettext,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install_callables(
|
||||||
|
self,
|
||||||
|
gettext: t.Callable[[str], str],
|
||||||
|
ngettext: t.Callable[[str, str, int], str],
|
||||||
|
newstyle: t.Optional[bool] = None,
|
||||||
|
pgettext: t.Optional[t.Callable[[str, str], str]] = None,
|
||||||
|
npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
|
||||||
|
) -> None:
|
||||||
|
if newstyle is not None:
|
||||||
|
self.environment.newstyle_gettext = newstyle # type: ignore
|
||||||
|
if self.environment.newstyle_gettext: # type: ignore
|
||||||
|
gettext = _make_new_gettext(gettext)
|
||||||
|
ngettext = _make_new_ngettext(ngettext)
|
||||||
|
|
||||||
|
if pgettext is not None:
|
||||||
|
pgettext = _make_new_pgettext(pgettext)
|
||||||
|
|
||||||
|
if npgettext is not None:
|
||||||
|
npgettext = _make_new_npgettext(npgettext)
|
||||||
|
|
||||||
|
self.environment.globals.update(
|
||||||
|
gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
|
||||||
|
)
|
||||||
|
|
||||||
|
def _uninstall(self, translations: "_SupportedTranslations") -> None:
|
||||||
|
for key in ("gettext", "ngettext", "pgettext", "npgettext"):
|
||||||
|
self.environment.globals.pop(key, None)
|
||||||
|
|
||||||
|
def _extract(
|
||||||
|
self,
|
||||||
|
source: t.Union[str, nodes.Template],
|
||||||
|
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||||
|
]:
|
||||||
|
if isinstance(source, str):
|
||||||
|
source = self.environment.parse(source)
|
||||||
|
return extract_from_ast(source, gettext_functions)
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||||
|
"""Parse a translatable tag."""
|
||||||
|
lineno = next(parser.stream).lineno
|
||||||
|
|
||||||
|
context = None
|
||||||
|
context_token = parser.stream.next_if("string")
|
||||||
|
|
||||||
|
if context_token is not None:
|
||||||
|
context = context_token.value
|
||||||
|
|
||||||
|
# find all the variables referenced. Additionally a variable can be
|
||||||
|
# defined in the body of the trans block too, but this is checked at
|
||||||
|
# a later state.
|
||||||
|
plural_expr: t.Optional[nodes.Expr] = None
|
||||||
|
plural_expr_assignment: t.Optional[nodes.Assign] = None
|
||||||
|
num_called_num = False
|
||||||
|
variables: t.Dict[str, nodes.Expr] = {}
|
||||||
|
trimmed = None
|
||||||
|
while parser.stream.current.type != "block_end":
|
||||||
|
if variables:
|
||||||
|
parser.stream.expect("comma")
|
||||||
|
|
||||||
|
# skip colon for python compatibility
|
||||||
|
if parser.stream.skip_if("colon"):
|
||||||
|
break
|
||||||
|
|
||||||
|
token = parser.stream.expect("name")
|
||||||
|
if token.value in variables:
|
||||||
|
parser.fail(
|
||||||
|
f"translatable variable {token.value!r} defined twice.",
|
||||||
|
token.lineno,
|
||||||
|
exc=TemplateAssertionError,
|
||||||
|
)
|
||||||
|
|
||||||
|
# expressions
|
||||||
|
if parser.stream.current.type == "assign":
|
||||||
|
next(parser.stream)
|
||||||
|
variables[token.value] = var = parser.parse_expression()
|
||||||
|
elif trimmed is None and token.value in ("trimmed", "notrimmed"):
|
||||||
|
trimmed = token.value == "trimmed"
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
variables[token.value] = var = nodes.Name(token.value, "load")
|
||||||
|
|
||||||
|
if plural_expr is None:
|
||||||
|
if isinstance(var, nodes.Call):
|
||||||
|
plural_expr = nodes.Name("_trans", "load")
|
||||||
|
variables[token.value] = plural_expr
|
||||||
|
plural_expr_assignment = nodes.Assign(
|
||||||
|
nodes.Name("_trans", "store"), var
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
plural_expr = var
|
||||||
|
num_called_num = token.value == "num"
|
||||||
|
|
||||||
|
parser.stream.expect("block_end")
|
||||||
|
|
||||||
|
plural = None
|
||||||
|
have_plural = False
|
||||||
|
referenced = set()
|
||||||
|
|
||||||
|
# now parse until endtrans or pluralize
|
||||||
|
singular_names, singular = self._parse_block(parser, True)
|
||||||
|
if singular_names:
|
||||||
|
referenced.update(singular_names)
|
||||||
|
if plural_expr is None:
|
||||||
|
plural_expr = nodes.Name(singular_names[0], "load")
|
||||||
|
num_called_num = singular_names[0] == "num"
|
||||||
|
|
||||||
|
# if we have a pluralize block, we parse that too
|
||||||
|
if parser.stream.current.test("name:pluralize"):
|
||||||
|
have_plural = True
|
||||||
|
next(parser.stream)
|
||||||
|
if parser.stream.current.type != "block_end":
|
||||||
|
token = parser.stream.expect("name")
|
||||||
|
if token.value not in variables:
|
||||||
|
parser.fail(
|
||||||
|
f"unknown variable {token.value!r} for pluralization",
|
||||||
|
token.lineno,
|
||||||
|
exc=TemplateAssertionError,
|
||||||
|
)
|
||||||
|
plural_expr = variables[token.value]
|
||||||
|
num_called_num = token.value == "num"
|
||||||
|
parser.stream.expect("block_end")
|
||||||
|
plural_names, plural = self._parse_block(parser, False)
|
||||||
|
next(parser.stream)
|
||||||
|
referenced.update(plural_names)
|
||||||
|
else:
|
||||||
|
next(parser.stream)
|
||||||
|
|
||||||
|
# register free names as simple name expressions
|
||||||
|
for name in referenced:
|
||||||
|
if name not in variables:
|
||||||
|
variables[name] = nodes.Name(name, "load")
|
||||||
|
|
||||||
|
if not have_plural:
|
||||||
|
plural_expr = None
|
||||||
|
elif plural_expr is None:
|
||||||
|
parser.fail("pluralize without variables", lineno)
|
||||||
|
|
||||||
|
if trimmed is None:
|
||||||
|
trimmed = self.environment.policies["ext.i18n.trimmed"]
|
||||||
|
if trimmed:
|
||||||
|
singular = self._trim_whitespace(singular)
|
||||||
|
if plural:
|
||||||
|
plural = self._trim_whitespace(plural)
|
||||||
|
|
||||||
|
node = self._make_node(
|
||||||
|
singular,
|
||||||
|
plural,
|
||||||
|
context,
|
||||||
|
variables,
|
||||||
|
plural_expr,
|
||||||
|
bool(referenced),
|
||||||
|
num_called_num and have_plural,
|
||||||
|
)
|
||||||
|
node.set_lineno(lineno)
|
||||||
|
if plural_expr_assignment is not None:
|
||||||
|
return [plural_expr_assignment, node]
|
||||||
|
else:
|
||||||
|
return node
|
||||||
|
|
||||||
|
def _trim_whitespace(self, string: str, _ws_re: t.Pattern[str] = _ws_re) -> str:
|
||||||
|
return _ws_re.sub(" ", string.strip())
|
||||||
|
|
||||||
|
def _parse_block(
|
||||||
|
self, parser: "Parser", allow_pluralize: bool
|
||||||
|
) -> t.Tuple[t.List[str], str]:
|
||||||
|
"""Parse until the next block tag with a given name."""
|
||||||
|
referenced = []
|
||||||
|
buf = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if parser.stream.current.type == "data":
|
||||||
|
buf.append(parser.stream.current.value.replace("%", "%%"))
|
||||||
|
next(parser.stream)
|
||||||
|
elif parser.stream.current.type == "variable_begin":
|
||||||
|
next(parser.stream)
|
||||||
|
name = parser.stream.expect("name").value
|
||||||
|
referenced.append(name)
|
||||||
|
buf.append(f"%({name})s")
|
||||||
|
parser.stream.expect("variable_end")
|
||||||
|
elif parser.stream.current.type == "block_begin":
|
||||||
|
next(parser.stream)
|
||||||
|
if parser.stream.current.test("name:endtrans"):
|
||||||
|
break
|
||||||
|
elif parser.stream.current.test("name:pluralize"):
|
||||||
|
if allow_pluralize:
|
||||||
|
break
|
||||||
|
parser.fail(
|
||||||
|
"a translatable section can have only one pluralize section"
|
||||||
|
)
|
||||||
|
parser.fail(
|
||||||
|
"control structures in translatable sections are not allowed"
|
||||||
|
)
|
||||||
|
elif parser.stream.eos:
|
||||||
|
parser.fail("unclosed translation block")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("internal parser error")
|
||||||
|
|
||||||
|
return referenced, concat(buf)
|
||||||
|
|
||||||
|
def _make_node(
|
||||||
|
self,
|
||||||
|
singular: str,
|
||||||
|
plural: t.Optional[str],
|
||||||
|
context: t.Optional[str],
|
||||||
|
variables: t.Dict[str, nodes.Expr],
|
||||||
|
plural_expr: t.Optional[nodes.Expr],
|
||||||
|
vars_referenced: bool,
|
||||||
|
num_called_num: bool,
|
||||||
|
) -> nodes.Output:
|
||||||
|
"""Generates a useful node from the data provided."""
|
||||||
|
newstyle = self.environment.newstyle_gettext # type: ignore
|
||||||
|
node: nodes.Expr
|
||||||
|
|
||||||
|
# no variables referenced? no need to escape for old style
|
||||||
|
# gettext invocations only if there are vars.
|
||||||
|
if not vars_referenced and not newstyle:
|
||||||
|
singular = singular.replace("%%", "%")
|
||||||
|
if plural:
|
||||||
|
plural = plural.replace("%%", "%")
|
||||||
|
|
||||||
|
func_name = "gettext"
|
||||||
|
func_args: t.List[nodes.Expr] = [nodes.Const(singular)]
|
||||||
|
|
||||||
|
if context is not None:
|
||||||
|
func_args.insert(0, nodes.Const(context))
|
||||||
|
func_name = f"p{func_name}"
|
||||||
|
|
||||||
|
if plural_expr is not None:
|
||||||
|
func_name = f"n{func_name}"
|
||||||
|
func_args.extend((nodes.Const(plural), plural_expr))
|
||||||
|
|
||||||
|
node = nodes.Call(nodes.Name(func_name, "load"), func_args, [], None, None)
|
||||||
|
|
||||||
|
# in case newstyle gettext is used, the method is powerful
|
||||||
|
# enough to handle the variable expansion and autoescape
|
||||||
|
# handling itself
|
||||||
|
if newstyle:
|
||||||
|
for key, value in variables.items():
|
||||||
|
# the function adds that later anyways in case num was
|
||||||
|
# called num, so just skip it.
|
||||||
|
if num_called_num and key == "num":
|
||||||
|
continue
|
||||||
|
node.kwargs.append(nodes.Keyword(key, value))
|
||||||
|
|
||||||
|
# otherwise do that here
|
||||||
|
else:
|
||||||
|
# mark the return value as safe if we are in an
|
||||||
|
# environment with autoescaping turned on
|
||||||
|
node = nodes.MarkSafeIfAutoescape(node)
|
||||||
|
if variables:
|
||||||
|
node = nodes.Mod(
|
||||||
|
node,
|
||||||
|
nodes.Dict(
|
||||||
|
[
|
||||||
|
nodes.Pair(nodes.Const(key), value)
|
||||||
|
for key, value in variables.items()
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nodes.Output([node])
|
||||||
|
|
||||||
|
|
||||||
|
class ExprStmtExtension(Extension):
|
||||||
|
"""Adds a `do` tag to Jinja that works like the print statement just
|
||||||
|
that it doesn't print the return value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = {"do"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> nodes.ExprStmt:
|
||||||
|
node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
|
||||||
|
node.node = parser.parse_tuple()
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class LoopControlExtension(Extension):
|
||||||
|
"""Adds break and continue to the template engine."""
|
||||||
|
|
||||||
|
tags = {"break", "continue"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
|
||||||
|
token = next(parser.stream)
|
||||||
|
if token.value == "break":
|
||||||
|
return nodes.Break(lineno=token.lineno)
|
||||||
|
return nodes.Continue(lineno=token.lineno)
|
||||||
|
|
||||||
|
|
||||||
|
class DebugExtension(Extension):
|
||||||
|
"""A ``{% debug %}`` tag that dumps the available variables,
|
||||||
|
filters, and tests.
|
||||||
|
|
||||||
|
.. code-block:: html+jinja
|
||||||
|
|
||||||
|
<pre>{% debug %}</pre>
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
{'context': {'cycler': <class 'jinja2.utils.Cycler'>,
|
||||||
|
...,
|
||||||
|
'namespace': <class 'jinja2.utils.Namespace'>},
|
||||||
|
'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd',
|
||||||
|
..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'],
|
||||||
|
'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined',
|
||||||
|
..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']}
|
||||||
|
|
||||||
|
.. versionadded:: 2.11.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = {"debug"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> nodes.Output:
|
||||||
|
lineno = parser.stream.expect("name:debug").lineno
|
||||||
|
context = nodes.ContextReference()
|
||||||
|
result = self.call_method("_render", [context], lineno=lineno)
|
||||||
|
return nodes.Output([result], lineno=lineno)
|
||||||
|
|
||||||
|
def _render(self, context: Context) -> str:
|
||||||
|
result = {
|
||||||
|
"context": context.get_all(),
|
||||||
|
"filters": sorted(self.environment.filters.keys()),
|
||||||
|
"tests": sorted(self.environment.tests.keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the depth since the intent is to show the top few names.
|
||||||
|
return pprint.pformat(result, depth=3, compact=True)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_from_ast(
|
||||||
|
ast: nodes.Template,
|
||||||
|
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||||
|
babel_style: bool = True,
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||||
|
]:
|
||||||
|
"""Extract localizable strings from the given template node. Per
|
||||||
|
default this function returns matches in babel style that means non string
|
||||||
|
parameters as well as keyword arguments are returned as `None`. This
|
||||||
|
allows Babel to figure out what you really meant if you are using
|
||||||
|
gettext functions that allow keyword arguments for placeholder expansion.
|
||||||
|
If you don't want that behavior set the `babel_style` parameter to `False`
|
||||||
|
which causes only strings to be returned and parameters are always stored
|
||||||
|
in tuples. As a consequence invalid gettext calls (calls without a single
|
||||||
|
string parameter or string parameters after non-string parameters) are
|
||||||
|
skipped.
|
||||||
|
|
||||||
|
This example explains the behavior:
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
|
||||||
|
>>> list(extract_from_ast(node))
|
||||||
|
[(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
|
||||||
|
>>> list(extract_from_ast(node, babel_style=False))
|
||||||
|
[(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
|
||||||
|
|
||||||
|
For every string found this function yields a ``(lineno, function,
|
||||||
|
message)`` tuple, where:
|
||||||
|
|
||||||
|
* ``lineno`` is the number of the line on which the string was found,
|
||||||
|
* ``function`` is the name of the ``gettext`` function used (if the
|
||||||
|
string was extracted from embedded Python code), and
|
||||||
|
* ``message`` is the string, or a tuple of strings for functions
|
||||||
|
with multiple string arguments.
|
||||||
|
|
||||||
|
This extraction function operates on the AST and is because of that unable
|
||||||
|
to extract any comments. For comment support you have to use the babel
|
||||||
|
extraction interface or extract comments yourself.
|
||||||
|
"""
|
||||||
|
out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
|
||||||
|
|
||||||
|
for node in ast.find_all(nodes.Call):
|
||||||
|
if (
|
||||||
|
not isinstance(node.node, nodes.Name)
|
||||||
|
or node.node.name not in gettext_functions
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
strings: t.List[t.Optional[str]] = []
|
||||||
|
|
||||||
|
for arg in node.args:
|
||||||
|
if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
|
||||||
|
strings.append(arg.value)
|
||||||
|
else:
|
||||||
|
strings.append(None)
|
||||||
|
|
||||||
|
for _ in node.kwargs:
|
||||||
|
strings.append(None)
|
||||||
|
if node.dyn_args is not None:
|
||||||
|
strings.append(None)
|
||||||
|
if node.dyn_kwargs is not None:
|
||||||
|
strings.append(None)
|
||||||
|
|
||||||
|
if not babel_style:
|
||||||
|
out = tuple(x for x in strings if x is not None)
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(strings) == 1:
|
||||||
|
out = strings[0]
|
||||||
|
else:
|
||||||
|
out = tuple(strings)
|
||||||
|
|
||||||
|
yield node.lineno, node.node.name, out
|
||||||
|
|
||||||
|
|
||||||
|
class _CommentFinder:
|
||||||
|
"""Helper class to find comments in a token stream. Can only
|
||||||
|
find comments for gettext calls forwards. Once the comment
|
||||||
|
from line 4 is found, a comment for line 1 will not return a
|
||||||
|
usable value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
|
||||||
|
) -> None:
|
||||||
|
self.tokens = tokens
|
||||||
|
self.comment_tags = comment_tags
|
||||||
|
self.offset = 0
|
||||||
|
self.last_lineno = 0
|
||||||
|
|
||||||
|
def find_backwards(self, offset: int) -> t.List[str]:
|
||||||
|
try:
|
||||||
|
for _, token_type, token_value in reversed(
|
||||||
|
self.tokens[self.offset : offset]
|
||||||
|
):
|
||||||
|
if token_type in ("comment", "linecomment"):
|
||||||
|
try:
|
||||||
|
prefix, comment = token_value.split(None, 1)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if prefix in self.comment_tags:
|
||||||
|
return [comment.rstrip()]
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
self.offset = offset
|
||||||
|
|
||||||
|
def find_comments(self, lineno: int) -> t.List[str]:
|
||||||
|
if not self.comment_tags or self.last_lineno > lineno:
|
||||||
|
return []
|
||||||
|
for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
|
||||||
|
if token_lineno > lineno:
|
||||||
|
return self.find_backwards(self.offset + idx)
|
||||||
|
return self.find_backwards(len(self.tokens))
|
||||||
|
|
||||||
|
|
||||||
|
def babel_extract(
|
||||||
|
fileobj: t.BinaryIO,
|
||||||
|
keywords: t.Sequence[str],
|
||||||
|
comment_tags: t.Sequence[str],
|
||||||
|
options: t.Dict[str, t.Any],
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[
|
||||||
|
int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
|
||||||
|
]
|
||||||
|
]:
|
||||||
|
"""Babel extraction method for Jinja templates.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
Basic support for translation comments was added. If `comment_tags`
|
||||||
|
is now set to a list of keywords for extraction, the extractor will
|
||||||
|
try to find the best preceding comment that begins with one of the
|
||||||
|
keywords. For best results, make sure to not have more than one
|
||||||
|
gettext call in one line of code and the matching comment in the
|
||||||
|
same line or the line before.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.5.1
|
||||||
|
The `newstyle_gettext` flag can be set to `True` to enable newstyle
|
||||||
|
gettext calls.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.7
|
||||||
|
A `silent` option can now be provided. If set to `False` template
|
||||||
|
syntax errors are propagated instead of being ignored.
|
||||||
|
|
||||||
|
:param fileobj: the file-like object the messages should be extracted from
|
||||||
|
:param keywords: a list of keywords (i.e. function names) that should be
|
||||||
|
recognized as translation functions
|
||||||
|
:param comment_tags: a list of translator tags to search for and include
|
||||||
|
in the results.
|
||||||
|
:param options: a dictionary of additional options (optional)
|
||||||
|
:return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
|
||||||
|
(comments will be empty currently)
|
||||||
|
"""
|
||||||
|
extensions: t.Dict[t.Type[Extension], None] = {}
|
||||||
|
|
||||||
|
for extension_name in options.get("extensions", "").split(","):
|
||||||
|
extension_name = extension_name.strip()
|
||||||
|
|
||||||
|
if not extension_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
extensions[import_string(extension_name)] = None
|
||||||
|
|
||||||
|
if InternationalizationExtension not in extensions:
|
||||||
|
extensions[InternationalizationExtension] = None
|
||||||
|
|
||||||
|
def getbool(options: t.Mapping[str, str], key: str, default: bool = False) -> bool:
|
||||||
|
return options.get(key, str(default)).lower() in {"1", "on", "yes", "true"}
|
||||||
|
|
||||||
|
silent = getbool(options, "silent", True)
|
||||||
|
environment = Environment(
|
||||||
|
options.get("block_start_string", defaults.BLOCK_START_STRING),
|
||||||
|
options.get("block_end_string", defaults.BLOCK_END_STRING),
|
||||||
|
options.get("variable_start_string", defaults.VARIABLE_START_STRING),
|
||||||
|
options.get("variable_end_string", defaults.VARIABLE_END_STRING),
|
||||||
|
options.get("comment_start_string", defaults.COMMENT_START_STRING),
|
||||||
|
options.get("comment_end_string", defaults.COMMENT_END_STRING),
|
||||||
|
options.get("line_statement_prefix") or defaults.LINE_STATEMENT_PREFIX,
|
||||||
|
options.get("line_comment_prefix") or defaults.LINE_COMMENT_PREFIX,
|
||||||
|
getbool(options, "trim_blocks", defaults.TRIM_BLOCKS),
|
||||||
|
getbool(options, "lstrip_blocks", defaults.LSTRIP_BLOCKS),
|
||||||
|
defaults.NEWLINE_SEQUENCE,
|
||||||
|
getbool(options, "keep_trailing_newline", defaults.KEEP_TRAILING_NEWLINE),
|
||||||
|
tuple(extensions),
|
||||||
|
cache_size=0,
|
||||||
|
auto_reload=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if getbool(options, "trimmed"):
|
||||||
|
environment.policies["ext.i18n.trimmed"] = True
|
||||||
|
if getbool(options, "newstyle_gettext"):
|
||||||
|
environment.newstyle_gettext = True # type: ignore
|
||||||
|
|
||||||
|
source = fileobj.read().decode(options.get("encoding", "utf-8"))
|
||||||
|
try:
|
||||||
|
node = environment.parse(source)
|
||||||
|
tokens = list(environment.lex(environment.preprocess(source)))
|
||||||
|
except TemplateSyntaxError:
|
||||||
|
if not silent:
|
||||||
|
raise
|
||||||
|
# skip templates with syntax errors
|
||||||
|
return
|
||||||
|
|
||||||
|
finder = _CommentFinder(tokens, comment_tags)
|
||||||
|
for lineno, func, message in extract_from_ast(node, keywords):
|
||||||
|
yield lineno, func, message, finder.find_comments(lineno)
|
||||||
|
|
||||||
|
|
||||||
|
#: nicer import names
|
||||||
|
i18n = InternationalizationExtension
|
||||||
|
do = ExprStmtExtension
|
||||||
|
loopcontrols = LoopControlExtension
|
||||||
|
debug = DebugExtension
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,318 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .visitor import NodeVisitor
|
||||||
|
|
||||||
|
VAR_LOAD_PARAMETER = "param"
|
||||||
|
VAR_LOAD_RESOLVE = "resolve"
|
||||||
|
VAR_LOAD_ALIAS = "alias"
|
||||||
|
VAR_LOAD_UNDEFINED = "undefined"
|
||||||
|
|
||||||
|
|
||||||
|
def find_symbols(
|
||||||
|
nodes: t.Iterable[nodes.Node], parent_symbols: t.Optional["Symbols"] = None
|
||||||
|
) -> "Symbols":
|
||||||
|
sym = Symbols(parent=parent_symbols)
|
||||||
|
visitor = FrameSymbolVisitor(sym)
|
||||||
|
for node in nodes:
|
||||||
|
visitor.visit(node)
|
||||||
|
return sym
|
||||||
|
|
||||||
|
|
||||||
|
def symbols_for_node(
|
||||||
|
node: nodes.Node, parent_symbols: t.Optional["Symbols"] = None
|
||||||
|
) -> "Symbols":
|
||||||
|
sym = Symbols(parent=parent_symbols)
|
||||||
|
sym.analyze_node(node)
|
||||||
|
return sym
|
||||||
|
|
||||||
|
|
||||||
|
class Symbols:
|
||||||
|
def __init__(
|
||||||
|
self, parent: t.Optional["Symbols"] = None, level: t.Optional[int] = None
|
||||||
|
) -> None:
|
||||||
|
if level is None:
|
||||||
|
if parent is None:
|
||||||
|
level = 0
|
||||||
|
else:
|
||||||
|
level = parent.level + 1
|
||||||
|
|
||||||
|
self.level: int = level
|
||||||
|
self.parent = parent
|
||||||
|
self.refs: t.Dict[str, str] = {}
|
||||||
|
self.loads: t.Dict[str, t.Any] = {}
|
||||||
|
self.stores: t.Set[str] = set()
|
||||||
|
|
||||||
|
def analyze_node(self, node: nodes.Node, **kwargs: t.Any) -> None:
|
||||||
|
visitor = RootVisitor(self)
|
||||||
|
visitor.visit(node, **kwargs)
|
||||||
|
|
||||||
|
def _define_ref(
|
||||||
|
self, name: str, load: t.Optional[t.Tuple[str, t.Optional[str]]] = None
|
||||||
|
) -> str:
|
||||||
|
ident = f"l_{self.level}_{name}"
|
||||||
|
self.refs[name] = ident
|
||||||
|
if load is not None:
|
||||||
|
self.loads[ident] = load
|
||||||
|
return ident
|
||||||
|
|
||||||
|
def find_load(self, target: str) -> t.Optional[t.Any]:
|
||||||
|
if target in self.loads:
|
||||||
|
return self.loads[target]
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
return self.parent.find_load(target)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_ref(self, name: str) -> t.Optional[str]:
|
||||||
|
if name in self.refs:
|
||||||
|
return self.refs[name]
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
return self.parent.find_ref(name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ref(self, name: str) -> str:
|
||||||
|
rv = self.find_ref(name)
|
||||||
|
if rv is None:
|
||||||
|
raise AssertionError(
|
||||||
|
"Tried to resolve a name to a reference that was"
|
||||||
|
f" unknown to the frame ({name!r})"
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def copy(self) -> "Symbols":
|
||||||
|
rv = object.__new__(self.__class__)
|
||||||
|
rv.__dict__.update(self.__dict__)
|
||||||
|
rv.refs = self.refs.copy()
|
||||||
|
rv.loads = self.loads.copy()
|
||||||
|
rv.stores = self.stores.copy()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def store(self, name: str) -> None:
|
||||||
|
self.stores.add(name)
|
||||||
|
|
||||||
|
# If we have not see the name referenced yet, we need to figure
|
||||||
|
# out what to set it to.
|
||||||
|
if name not in self.refs:
|
||||||
|
# If there is a parent scope we check if the name has a
|
||||||
|
# reference there. If it does it means we might have to alias
|
||||||
|
# to a variable there.
|
||||||
|
if self.parent is not None:
|
||||||
|
outer_ref = self.parent.find_ref(name)
|
||||||
|
if outer_ref is not None:
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_ALIAS, outer_ref))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise we can just set it to undefined.
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_UNDEFINED, None))
|
||||||
|
|
||||||
|
def declare_parameter(self, name: str) -> str:
|
||||||
|
self.stores.add(name)
|
||||||
|
return self._define_ref(name, load=(VAR_LOAD_PARAMETER, None))
|
||||||
|
|
||||||
|
def load(self, name: str) -> None:
|
||||||
|
if self.find_ref(name) is None:
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_RESOLVE, name))
|
||||||
|
|
||||||
|
def branch_update(self, branch_symbols: t.Sequence["Symbols"]) -> None:
|
||||||
|
stores: t.Dict[str, int] = {}
|
||||||
|
for branch in branch_symbols:
|
||||||
|
for target in branch.stores:
|
||||||
|
if target in self.stores:
|
||||||
|
continue
|
||||||
|
stores[target] = stores.get(target, 0) + 1
|
||||||
|
|
||||||
|
for sym in branch_symbols:
|
||||||
|
self.refs.update(sym.refs)
|
||||||
|
self.loads.update(sym.loads)
|
||||||
|
self.stores.update(sym.stores)
|
||||||
|
|
||||||
|
for name, branch_count in stores.items():
|
||||||
|
if branch_count == len(branch_symbols):
|
||||||
|
continue
|
||||||
|
|
||||||
|
target = self.find_ref(name) # type: ignore
|
||||||
|
assert target is not None, "should not happen"
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
outer_target = self.parent.find_ref(name)
|
||||||
|
if outer_target is not None:
|
||||||
|
self.loads[target] = (VAR_LOAD_ALIAS, outer_target)
|
||||||
|
continue
|
||||||
|
self.loads[target] = (VAR_LOAD_RESOLVE, name)
|
||||||
|
|
||||||
|
def dump_stores(self) -> t.Dict[str, str]:
|
||||||
|
rv: t.Dict[str, str] = {}
|
||||||
|
node: t.Optional["Symbols"] = self
|
||||||
|
|
||||||
|
while node is not None:
|
||||||
|
for name in sorted(node.stores):
|
||||||
|
if name not in rv:
|
||||||
|
rv[name] = self.find_ref(name) # type: ignore
|
||||||
|
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def dump_param_targets(self) -> t.Set[str]:
|
||||||
|
rv = set()
|
||||||
|
node: t.Optional["Symbols"] = self
|
||||||
|
|
||||||
|
while node is not None:
|
||||||
|
for target, (instr, _) in self.loads.items():
|
||||||
|
if instr == VAR_LOAD_PARAMETER:
|
||||||
|
rv.add(target)
|
||||||
|
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class RootVisitor(NodeVisitor):
|
||||||
|
def __init__(self, symbols: "Symbols") -> None:
|
||||||
|
self.sym_visitor = FrameSymbolVisitor(symbols)
|
||||||
|
|
||||||
|
def _simple_visit(self, node: nodes.Node, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.iter_child_nodes():
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
visit_Template = _simple_visit
|
||||||
|
visit_Block = _simple_visit
|
||||||
|
visit_Macro = _simple_visit
|
||||||
|
visit_FilterBlock = _simple_visit
|
||||||
|
visit_Scope = _simple_visit
|
||||||
|
visit_If = _simple_visit
|
||||||
|
visit_ScopedEvalContextModifier = _simple_visit
|
||||||
|
|
||||||
|
def visit_AssignBlock(self, node: nodes.AssignBlock, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_CallBlock(self, node: nodes.CallBlock, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.iter_child_nodes(exclude=("call",)):
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_OverlayScope(self, node: nodes.OverlayScope, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_For(
|
||||||
|
self, node: nodes.For, for_branch: str = "body", **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
if for_branch == "body":
|
||||||
|
self.sym_visitor.visit(node.target, store_as_param=True)
|
||||||
|
branch = node.body
|
||||||
|
elif for_branch == "else":
|
||||||
|
branch = node.else_
|
||||||
|
elif for_branch == "test":
|
||||||
|
self.sym_visitor.visit(node.target, store_as_param=True)
|
||||||
|
if node.test is not None:
|
||||||
|
self.sym_visitor.visit(node.test)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unknown for branch")
|
||||||
|
|
||||||
|
if branch:
|
||||||
|
for item in branch:
|
||||||
|
self.sym_visitor.visit(item)
|
||||||
|
|
||||||
|
def visit_With(self, node: nodes.With, **kwargs: t.Any) -> None:
|
||||||
|
for target in node.targets:
|
||||||
|
self.sym_visitor.visit(target)
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def generic_visit(self, node: nodes.Node, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
|
raise NotImplementedError(f"Cannot find symbols for {type(node).__name__!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class FrameSymbolVisitor(NodeVisitor):
|
||||||
|
"""A visitor for `Frame.inspect`."""
|
||||||
|
|
||||||
|
def __init__(self, symbols: "Symbols") -> None:
|
||||||
|
self.symbols = symbols
|
||||||
|
|
||||||
|
def visit_Name(
|
||||||
|
self, node: nodes.Name, store_as_param: bool = False, **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
"""All assignments to names go through this function."""
|
||||||
|
if store_as_param or node.ctx == "param":
|
||||||
|
self.symbols.declare_parameter(node.name)
|
||||||
|
elif node.ctx == "store":
|
||||||
|
self.symbols.store(node.name)
|
||||||
|
elif node.ctx == "load":
|
||||||
|
self.symbols.load(node.name)
|
||||||
|
|
||||||
|
def visit_NSRef(self, node: nodes.NSRef, **kwargs: t.Any) -> None:
|
||||||
|
self.symbols.load(node.name)
|
||||||
|
|
||||||
|
def visit_If(self, node: nodes.If, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.test, **kwargs)
|
||||||
|
original_symbols = self.symbols
|
||||||
|
|
||||||
|
def inner_visit(nodes: t.Iterable[nodes.Node]) -> "Symbols":
|
||||||
|
self.symbols = rv = original_symbols.copy()
|
||||||
|
|
||||||
|
for subnode in nodes:
|
||||||
|
self.visit(subnode, **kwargs)
|
||||||
|
|
||||||
|
self.symbols = original_symbols
|
||||||
|
return rv
|
||||||
|
|
||||||
|
body_symbols = inner_visit(node.body)
|
||||||
|
elif_symbols = inner_visit(node.elif_)
|
||||||
|
else_symbols = inner_visit(node.else_ or ())
|
||||||
|
self.symbols.branch_update([body_symbols, elif_symbols, else_symbols])
|
||||||
|
|
||||||
|
def visit_Macro(self, node: nodes.Macro, **kwargs: t.Any) -> None:
|
||||||
|
self.symbols.store(node.name)
|
||||||
|
|
||||||
|
def visit_Import(self, node: nodes.Import, **kwargs: t.Any) -> None:
|
||||||
|
self.generic_visit(node, **kwargs)
|
||||||
|
self.symbols.store(node.target)
|
||||||
|
|
||||||
|
def visit_FromImport(self, node: nodes.FromImport, **kwargs: t.Any) -> None:
|
||||||
|
self.generic_visit(node, **kwargs)
|
||||||
|
|
||||||
|
for name in node.names:
|
||||||
|
if isinstance(name, tuple):
|
||||||
|
self.symbols.store(name[1])
|
||||||
|
else:
|
||||||
|
self.symbols.store(name)
|
||||||
|
|
||||||
|
def visit_Assign(self, node: nodes.Assign, **kwargs: t.Any) -> None:
|
||||||
|
"""Visit assignments in the correct order."""
|
||||||
|
self.visit(node.node, **kwargs)
|
||||||
|
self.visit(node.target, **kwargs)
|
||||||
|
|
||||||
|
def visit_For(self, node: nodes.For, **kwargs: t.Any) -> None:
|
||||||
|
"""Visiting stops at for blocks. However the block sequence
|
||||||
|
is visited as part of the outer scope.
|
||||||
|
"""
|
||||||
|
self.visit(node.iter, **kwargs)
|
||||||
|
|
||||||
|
def visit_CallBlock(self, node: nodes.CallBlock, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.call, **kwargs)
|
||||||
|
|
||||||
|
def visit_FilterBlock(self, node: nodes.FilterBlock, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.filter, **kwargs)
|
||||||
|
|
||||||
|
def visit_With(self, node: nodes.With, **kwargs: t.Any) -> None:
|
||||||
|
for target in node.values:
|
||||||
|
self.visit(target)
|
||||||
|
|
||||||
|
def visit_AssignBlock(self, node: nodes.AssignBlock, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at block assigns."""
|
||||||
|
self.visit(node.target, **kwargs)
|
||||||
|
|
||||||
|
def visit_Scope(self, node: nodes.Scope, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at scopes."""
|
||||||
|
|
||||||
|
def visit_Block(self, node: nodes.Block, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at blocks."""
|
||||||
|
|
||||||
|
def visit_OverlayScope(self, node: nodes.OverlayScope, **kwargs: t.Any) -> None:
|
||||||
|
"""Do not visit into overlay scopes."""
|
@ -0,0 +1,866 @@
|
|||||||
|
"""Implements a Jinja / Python combination lexer. The ``Lexer`` class
|
||||||
|
is used to do some preprocessing. It filters out invalid operators like
|
||||||
|
the bitshift operators we don't allow in templates. It separates
|
||||||
|
template code and python code in expressions.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
from ast import literal_eval
|
||||||
|
from collections import deque
|
||||||
|
from sys import intern
|
||||||
|
|
||||||
|
from ._identifier import pattern as name_re
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .utils import LRUCache
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
# cache for the lexers. Exists in order to be able to have multiple
|
||||||
|
# environments with the same lexer
|
||||||
|
_lexer_cache: t.MutableMapping[t.Tuple, "Lexer"] = LRUCache(50) # type: ignore
|
||||||
|
|
||||||
|
# static regular expressions
|
||||||
|
whitespace_re = re.compile(r"\s+")
|
||||||
|
newline_re = re.compile(r"(\r\n|\r|\n)")
|
||||||
|
string_re = re.compile(
|
||||||
|
r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S
|
||||||
|
)
|
||||||
|
integer_re = re.compile(
|
||||||
|
r"""
|
||||||
|
(
|
||||||
|
0b(_?[0-1])+ # binary
|
||||||
|
|
|
||||||
|
0o(_?[0-7])+ # octal
|
||||||
|
|
|
||||||
|
0x(_?[\da-f])+ # hex
|
||||||
|
|
|
||||||
|
[1-9](_?\d)* # decimal
|
||||||
|
|
|
||||||
|
0(_?0)* # decimal zero
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
float_re = re.compile(
|
||||||
|
r"""
|
||||||
|
(?<!\.) # doesn't start with a .
|
||||||
|
(\d+_)*\d+ # digits, possibly _ separated
|
||||||
|
(
|
||||||
|
(\.(\d+_)*\d+)? # optional fractional part
|
||||||
|
e[+\-]?(\d+_)*\d+ # exponent part
|
||||||
|
|
|
||||||
|
\.(\d+_)*\d+ # required fractional part
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# internal the tokens and keep references to them
|
||||||
|
TOKEN_ADD = intern("add")
|
||||||
|
TOKEN_ASSIGN = intern("assign")
|
||||||
|
TOKEN_COLON = intern("colon")
|
||||||
|
TOKEN_COMMA = intern("comma")
|
||||||
|
TOKEN_DIV = intern("div")
|
||||||
|
TOKEN_DOT = intern("dot")
|
||||||
|
TOKEN_EQ = intern("eq")
|
||||||
|
TOKEN_FLOORDIV = intern("floordiv")
|
||||||
|
TOKEN_GT = intern("gt")
|
||||||
|
TOKEN_GTEQ = intern("gteq")
|
||||||
|
TOKEN_LBRACE = intern("lbrace")
|
||||||
|
TOKEN_LBRACKET = intern("lbracket")
|
||||||
|
TOKEN_LPAREN = intern("lparen")
|
||||||
|
TOKEN_LT = intern("lt")
|
||||||
|
TOKEN_LTEQ = intern("lteq")
|
||||||
|
TOKEN_MOD = intern("mod")
|
||||||
|
TOKEN_MUL = intern("mul")
|
||||||
|
TOKEN_NE = intern("ne")
|
||||||
|
TOKEN_PIPE = intern("pipe")
|
||||||
|
TOKEN_POW = intern("pow")
|
||||||
|
TOKEN_RBRACE = intern("rbrace")
|
||||||
|
TOKEN_RBRACKET = intern("rbracket")
|
||||||
|
TOKEN_RPAREN = intern("rparen")
|
||||||
|
TOKEN_SEMICOLON = intern("semicolon")
|
||||||
|
TOKEN_SUB = intern("sub")
|
||||||
|
TOKEN_TILDE = intern("tilde")
|
||||||
|
TOKEN_WHITESPACE = intern("whitespace")
|
||||||
|
TOKEN_FLOAT = intern("float")
|
||||||
|
TOKEN_INTEGER = intern("integer")
|
||||||
|
TOKEN_NAME = intern("name")
|
||||||
|
TOKEN_STRING = intern("string")
|
||||||
|
TOKEN_OPERATOR = intern("operator")
|
||||||
|
TOKEN_BLOCK_BEGIN = intern("block_begin")
|
||||||
|
TOKEN_BLOCK_END = intern("block_end")
|
||||||
|
TOKEN_VARIABLE_BEGIN = intern("variable_begin")
|
||||||
|
TOKEN_VARIABLE_END = intern("variable_end")
|
||||||
|
TOKEN_RAW_BEGIN = intern("raw_begin")
|
||||||
|
TOKEN_RAW_END = intern("raw_end")
|
||||||
|
TOKEN_COMMENT_BEGIN = intern("comment_begin")
|
||||||
|
TOKEN_COMMENT_END = intern("comment_end")
|
||||||
|
TOKEN_COMMENT = intern("comment")
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN = intern("linestatement_begin")
|
||||||
|
TOKEN_LINESTATEMENT_END = intern("linestatement_end")
|
||||||
|
TOKEN_LINECOMMENT_BEGIN = intern("linecomment_begin")
|
||||||
|
TOKEN_LINECOMMENT_END = intern("linecomment_end")
|
||||||
|
TOKEN_LINECOMMENT = intern("linecomment")
|
||||||
|
TOKEN_DATA = intern("data")
|
||||||
|
TOKEN_INITIAL = intern("initial")
|
||||||
|
TOKEN_EOF = intern("eof")
|
||||||
|
|
||||||
|
# bind operators to token types
|
||||||
|
operators = {
|
||||||
|
"+": TOKEN_ADD,
|
||||||
|
"-": TOKEN_SUB,
|
||||||
|
"/": TOKEN_DIV,
|
||||||
|
"//": TOKEN_FLOORDIV,
|
||||||
|
"*": TOKEN_MUL,
|
||||||
|
"%": TOKEN_MOD,
|
||||||
|
"**": TOKEN_POW,
|
||||||
|
"~": TOKEN_TILDE,
|
||||||
|
"[": TOKEN_LBRACKET,
|
||||||
|
"]": TOKEN_RBRACKET,
|
||||||
|
"(": TOKEN_LPAREN,
|
||||||
|
")": TOKEN_RPAREN,
|
||||||
|
"{": TOKEN_LBRACE,
|
||||||
|
"}": TOKEN_RBRACE,
|
||||||
|
"==": TOKEN_EQ,
|
||||||
|
"!=": TOKEN_NE,
|
||||||
|
">": TOKEN_GT,
|
||||||
|
">=": TOKEN_GTEQ,
|
||||||
|
"<": TOKEN_LT,
|
||||||
|
"<=": TOKEN_LTEQ,
|
||||||
|
"=": TOKEN_ASSIGN,
|
||||||
|
".": TOKEN_DOT,
|
||||||
|
":": TOKEN_COLON,
|
||||||
|
"|": TOKEN_PIPE,
|
||||||
|
",": TOKEN_COMMA,
|
||||||
|
";": TOKEN_SEMICOLON,
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_operators = {v: k for k, v in operators.items()}
|
||||||
|
assert len(operators) == len(reverse_operators), "operators dropped"
|
||||||
|
operator_re = re.compile(
|
||||||
|
f"({'|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))})"
|
||||||
|
)
|
||||||
|
|
||||||
|
ignored_tokens = frozenset(
|
||||||
|
[
|
||||||
|
TOKEN_COMMENT_BEGIN,
|
||||||
|
TOKEN_COMMENT,
|
||||||
|
TOKEN_COMMENT_END,
|
||||||
|
TOKEN_WHITESPACE,
|
||||||
|
TOKEN_LINECOMMENT_BEGIN,
|
||||||
|
TOKEN_LINECOMMENT_END,
|
||||||
|
TOKEN_LINECOMMENT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ignore_if_empty = frozenset(
|
||||||
|
[TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_token_type(token_type: str) -> str:
|
||||||
|
if token_type in reverse_operators:
|
||||||
|
return reverse_operators[token_type]
|
||||||
|
|
||||||
|
return {
|
||||||
|
TOKEN_COMMENT_BEGIN: "begin of comment",
|
||||||
|
TOKEN_COMMENT_END: "end of comment",
|
||||||
|
TOKEN_COMMENT: "comment",
|
||||||
|
TOKEN_LINECOMMENT: "comment",
|
||||||
|
TOKEN_BLOCK_BEGIN: "begin of statement block",
|
||||||
|
TOKEN_BLOCK_END: "end of statement block",
|
||||||
|
TOKEN_VARIABLE_BEGIN: "begin of print statement",
|
||||||
|
TOKEN_VARIABLE_END: "end of print statement",
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN: "begin of line statement",
|
||||||
|
TOKEN_LINESTATEMENT_END: "end of line statement",
|
||||||
|
TOKEN_DATA: "template data / text",
|
||||||
|
TOKEN_EOF: "end of template",
|
||||||
|
}.get(token_type, token_type)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_token(token: "Token") -> str:
|
||||||
|
"""Returns a description of the token."""
|
||||||
|
if token.type == TOKEN_NAME:
|
||||||
|
return token.value
|
||||||
|
|
||||||
|
return _describe_token_type(token.type)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_token_expr(expr: str) -> str:
|
||||||
|
"""Like `describe_token` but for token expressions."""
|
||||||
|
if ":" in expr:
|
||||||
|
type, value = expr.split(":", 1)
|
||||||
|
|
||||||
|
if type == TOKEN_NAME:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
type = expr
|
||||||
|
|
||||||
|
return _describe_token_type(type)
|
||||||
|
|
||||||
|
|
||||||
|
def count_newlines(value: str) -> int:
|
||||||
|
"""Count the number of newline characters in the string. This is
|
||||||
|
useful for extensions that filter a stream.
|
||||||
|
"""
|
||||||
|
return len(newline_re.findall(value))
|
||||||
|
|
||||||
|
|
||||||
|
def compile_rules(environment: "Environment") -> t.List[t.Tuple[str, str]]:
|
||||||
|
"""Compiles all the rules from the environment into a list of rules."""
|
||||||
|
e = re.escape
|
||||||
|
rules = [
|
||||||
|
(
|
||||||
|
len(environment.comment_start_string),
|
||||||
|
TOKEN_COMMENT_BEGIN,
|
||||||
|
e(environment.comment_start_string),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
len(environment.block_start_string),
|
||||||
|
TOKEN_BLOCK_BEGIN,
|
||||||
|
e(environment.block_start_string),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
len(environment.variable_start_string),
|
||||||
|
TOKEN_VARIABLE_BEGIN,
|
||||||
|
e(environment.variable_start_string),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if environment.line_statement_prefix is not None:
|
||||||
|
rules.append(
|
||||||
|
(
|
||||||
|
len(environment.line_statement_prefix),
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN,
|
||||||
|
r"^[ \t\v]*" + e(environment.line_statement_prefix),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if environment.line_comment_prefix is not None:
|
||||||
|
rules.append(
|
||||||
|
(
|
||||||
|
len(environment.line_comment_prefix),
|
||||||
|
TOKEN_LINECOMMENT_BEGIN,
|
||||||
|
r"(?:^|(?<=\S))[^\S\r\n]*" + e(environment.line_comment_prefix),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [x[1:] for x in sorted(rules, reverse=True)]
|
||||||
|
|
||||||
|
|
||||||
|
class Failure:
|
||||||
|
"""Class that raises a `TemplateSyntaxError` if called.
|
||||||
|
Used by the `Lexer` to specify known errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, message: str, cls: t.Type[TemplateSyntaxError] = TemplateSyntaxError
|
||||||
|
) -> None:
|
||||||
|
self.message = message
|
||||||
|
self.error_class = cls
|
||||||
|
|
||||||
|
def __call__(self, lineno: int, filename: str) -> "te.NoReturn":
|
||||||
|
raise self.error_class(self.message, lineno, filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(t.NamedTuple):
|
||||||
|
lineno: int
|
||||||
|
type: str
|
||||||
|
value: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return describe_token(self)
|
||||||
|
|
||||||
|
def test(self, expr: str) -> bool:
|
||||||
|
"""Test a token against a token expression. This can either be a
|
||||||
|
token type or ``'token_type:token_value'``. This can only test
|
||||||
|
against string values and types.
|
||||||
|
"""
|
||||||
|
# here we do a regular string equality check as test_any is usually
|
||||||
|
# passed an iterable of not interned strings.
|
||||||
|
if self.type == expr:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ":" in expr:
|
||||||
|
return expr.split(":", 1) == [self.type, self.value]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_any(self, *iterable: str) -> bool:
|
||||||
|
"""Test against multiple token expressions."""
|
||||||
|
return any(self.test(expr) for expr in iterable)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStreamIterator:
|
||||||
|
"""The iterator for tokenstreams. Iterate over the stream
|
||||||
|
until the eof token is reached.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream: "TokenStream") -> None:
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def __iter__(self) -> "TokenStreamIterator":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> Token:
|
||||||
|
token = self.stream.current
|
||||||
|
|
||||||
|
if token.type is TOKEN_EOF:
|
||||||
|
self.stream.close()
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
next(self.stream)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStream:
|
||||||
|
"""A token stream is an iterable that yields :class:`Token`\\s. The
|
||||||
|
parser however does not iterate over it but calls :meth:`next` to go
|
||||||
|
one token ahead. The current active token is stored as :attr:`current`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
generator: t.Iterable[Token],
|
||||||
|
name: t.Optional[str],
|
||||||
|
filename: t.Optional[str],
|
||||||
|
):
|
||||||
|
self._iter = iter(generator)
|
||||||
|
self._pushed: "te.Deque[Token]" = deque()
|
||||||
|
self.name = name
|
||||||
|
self.filename = filename
|
||||||
|
self.closed = False
|
||||||
|
self.current = Token(1, TOKEN_INITIAL, "")
|
||||||
|
next(self)
|
||||||
|
|
||||||
|
def __iter__(self) -> TokenStreamIterator:
|
||||||
|
return TokenStreamIterator(self)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(self._pushed) or self.current.type is not TOKEN_EOF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eos(self) -> bool:
|
||||||
|
"""Are we at the end of the stream?"""
|
||||||
|
return not self
|
||||||
|
|
||||||
|
def push(self, token: Token) -> None:
|
||||||
|
"""Push a token back to the stream."""
|
||||||
|
self._pushed.append(token)
|
||||||
|
|
||||||
|
def look(self) -> Token:
|
||||||
|
"""Look at the next token."""
|
||||||
|
old_token = next(self)
|
||||||
|
result = self.current
|
||||||
|
self.push(result)
|
||||||
|
self.current = old_token
|
||||||
|
return result
|
||||||
|
|
||||||
|
def skip(self, n: int = 1) -> None:
|
||||||
|
"""Got n tokens ahead."""
|
||||||
|
for _ in range(n):
|
||||||
|
next(self)
|
||||||
|
|
||||||
|
def next_if(self, expr: str) -> t.Optional[Token]:
|
||||||
|
"""Perform the token test and return the token if it matched.
|
||||||
|
Otherwise the return value is `None`.
|
||||||
|
"""
|
||||||
|
if self.current.test(expr):
|
||||||
|
return next(self)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def skip_if(self, expr: str) -> bool:
|
||||||
|
"""Like :meth:`next_if` but only returns `True` or `False`."""
|
||||||
|
return self.next_if(expr) is not None
|
||||||
|
|
||||||
|
def __next__(self) -> Token:
|
||||||
|
"""Go one token ahead and return the old one.
|
||||||
|
|
||||||
|
Use the built-in :func:`next` instead of calling this directly.
|
||||||
|
"""
|
||||||
|
rv = self.current
|
||||||
|
|
||||||
|
if self._pushed:
|
||||||
|
self.current = self._pushed.popleft()
|
||||||
|
elif self.current.type is not TOKEN_EOF:
|
||||||
|
try:
|
||||||
|
self.current = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the stream."""
|
||||||
|
self.current = Token(self.current.lineno, TOKEN_EOF, "")
|
||||||
|
self._iter = iter(())
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
def expect(self, expr: str) -> Token:
|
||||||
|
"""Expect a given token type and return it. This accepts the same
|
||||||
|
argument as :meth:`jinja2.lexer.Token.test`.
|
||||||
|
"""
|
||||||
|
if not self.current.test(expr):
|
||||||
|
expr = describe_token_expr(expr)
|
||||||
|
|
||||||
|
if self.current.type is TOKEN_EOF:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected end of template, expected {expr!r}.",
|
||||||
|
self.current.lineno,
|
||||||
|
self.name,
|
||||||
|
self.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"expected token {expr!r}, got {describe_token(self.current)!r}",
|
||||||
|
self.current.lineno,
|
||||||
|
self.name,
|
||||||
|
self.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
return next(self)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lexer(environment: "Environment") -> "Lexer":
|
||||||
|
"""Return a lexer which is probably cached."""
|
||||||
|
key = (
|
||||||
|
environment.block_start_string,
|
||||||
|
environment.block_end_string,
|
||||||
|
environment.variable_start_string,
|
||||||
|
environment.variable_end_string,
|
||||||
|
environment.comment_start_string,
|
||||||
|
environment.comment_end_string,
|
||||||
|
environment.line_statement_prefix,
|
||||||
|
environment.line_comment_prefix,
|
||||||
|
environment.trim_blocks,
|
||||||
|
environment.lstrip_blocks,
|
||||||
|
environment.newline_sequence,
|
||||||
|
environment.keep_trailing_newline,
|
||||||
|
)
|
||||||
|
lexer = _lexer_cache.get(key)
|
||||||
|
|
||||||
|
if lexer is None:
|
||||||
|
_lexer_cache[key] = lexer = Lexer(environment)
|
||||||
|
|
||||||
|
return lexer
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalLStrip(tuple):
|
||||||
|
"""A special tuple for marking a point in the state that can have
|
||||||
|
lstrip applied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
# Even though it looks like a no-op, creating instances fails
|
||||||
|
# without this.
|
||||||
|
def __new__(cls, *members, **kwargs): # type: ignore
|
||||||
|
return super().__new__(cls, members)
|
||||||
|
|
||||||
|
|
||||||
|
class _Rule(t.NamedTuple):
|
||||||
|
pattern: t.Pattern[str]
|
||||||
|
tokens: t.Union[str, t.Tuple[str, ...], t.Tuple[Failure]]
|
||||||
|
command: t.Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Lexer:
|
||||||
|
"""Class that implements a lexer for a given environment. Automatically
|
||||||
|
created by the environment class, usually you don't have to do that.
|
||||||
|
|
||||||
|
Note that the lexer is not automatically bound to an environment.
|
||||||
|
Multiple environments can share the same lexer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment") -> None:
|
||||||
|
# shortcuts
|
||||||
|
e = re.escape
|
||||||
|
|
||||||
|
def c(x: str) -> t.Pattern[str]:
|
||||||
|
return re.compile(x, re.M | re.S)
|
||||||
|
|
||||||
|
# lexing rules for tags
|
||||||
|
tag_rules: t.List[_Rule] = [
|
||||||
|
_Rule(whitespace_re, TOKEN_WHITESPACE, None),
|
||||||
|
_Rule(float_re, TOKEN_FLOAT, None),
|
||||||
|
_Rule(integer_re, TOKEN_INTEGER, None),
|
||||||
|
_Rule(name_re, TOKEN_NAME, None),
|
||||||
|
_Rule(string_re, TOKEN_STRING, None),
|
||||||
|
_Rule(operator_re, TOKEN_OPERATOR, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
# assemble the root lexing rule. because "|" is ungreedy
|
||||||
|
# we have to sort by length so that the lexer continues working
|
||||||
|
# as expected when we have parsing rules like <% for block and
|
||||||
|
# <%= for variables. (if someone wants asp like syntax)
|
||||||
|
# variables are just part of the rules if variable processing
|
||||||
|
# is required.
|
||||||
|
root_tag_rules = compile_rules(environment)
|
||||||
|
|
||||||
|
block_start_re = e(environment.block_start_string)
|
||||||
|
block_end_re = e(environment.block_end_string)
|
||||||
|
comment_end_re = e(environment.comment_end_string)
|
||||||
|
variable_end_re = e(environment.variable_end_string)
|
||||||
|
|
||||||
|
# block suffix if trimming is enabled
|
||||||
|
block_suffix_re = "\\n?" if environment.trim_blocks else ""
|
||||||
|
|
||||||
|
self.lstrip_blocks = environment.lstrip_blocks
|
||||||
|
|
||||||
|
self.newline_sequence = environment.newline_sequence
|
||||||
|
self.keep_trailing_newline = environment.keep_trailing_newline
|
||||||
|
|
||||||
|
root_raw_re = (
|
||||||
|
rf"(?P<raw_begin>{block_start_re}(\-|\+|)\s*raw\s*"
|
||||||
|
rf"(?:\-{block_end_re}\s*|{block_end_re}))"
|
||||||
|
)
|
||||||
|
root_parts_re = "|".join(
|
||||||
|
[root_raw_re] + [rf"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules]
|
||||||
|
)
|
||||||
|
|
||||||
|
# global lexing rules
|
||||||
|
self.rules: t.Dict[str, t.List[_Rule]] = {
|
||||||
|
"root": [
|
||||||
|
# directives
|
||||||
|
_Rule(
|
||||||
|
c(rf"(.*?)(?:{root_parts_re})"),
|
||||||
|
OptionalLStrip(TOKEN_DATA, "#bygroup"), # type: ignore
|
||||||
|
"#bygroup",
|
||||||
|
),
|
||||||
|
# data
|
||||||
|
_Rule(c(".+"), TOKEN_DATA, None),
|
||||||
|
],
|
||||||
|
# comments
|
||||||
|
TOKEN_COMMENT_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
rf"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*"
|
||||||
|
rf"|{comment_end_re}{block_suffix_re}))"
|
||||||
|
),
|
||||||
|
(TOKEN_COMMENT, TOKEN_COMMENT_END),
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
_Rule(c(r"(.)"), (Failure("Missing end of comment tag"),), None),
|
||||||
|
],
|
||||||
|
# blocks
|
||||||
|
TOKEN_BLOCK_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
rf"(?:\+{block_end_re}|\-{block_end_re}\s*"
|
||||||
|
rf"|{block_end_re}{block_suffix_re})"
|
||||||
|
),
|
||||||
|
TOKEN_BLOCK_END,
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# variables
|
||||||
|
TOKEN_VARIABLE_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(rf"\-{variable_end_re}\s*|{variable_end_re}"),
|
||||||
|
TOKEN_VARIABLE_END,
|
||||||
|
"#pop",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# raw block
|
||||||
|
TOKEN_RAW_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
rf"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*"
|
||||||
|
rf"(?:\+{block_end_re}|\-{block_end_re}\s*"
|
||||||
|
rf"|{block_end_re}{block_suffix_re}))"
|
||||||
|
),
|
||||||
|
OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END), # type: ignore
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
_Rule(c(r"(.)"), (Failure("Missing end of raw directive"),), None),
|
||||||
|
],
|
||||||
|
# line statements
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN: [
|
||||||
|
_Rule(c(r"\s*(\n|$)"), TOKEN_LINESTATEMENT_END, "#pop")
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# line comments
|
||||||
|
TOKEN_LINECOMMENT_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(r"(.*?)()(?=\n|$)"),
|
||||||
|
(TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END),
|
||||||
|
"#pop",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _normalize_newlines(self, value: str) -> str:
|
||||||
|
"""Replace all newlines with the configured sequence in strings
|
||||||
|
and template data.
|
||||||
|
"""
|
||||||
|
return newline_re.sub(self.newline_sequence, value)
|
||||||
|
|
||||||
|
def tokenize(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
state: t.Optional[str] = None,
|
||||||
|
) -> TokenStream:
|
||||||
|
"""Calls tokeniter + tokenize and wraps it in a token stream."""
|
||||||
|
stream = self.tokeniter(source, name, filename, state)
|
||||||
|
return TokenStream(self.wrap(stream, name, filename), name, filename)
|
||||||
|
|
||||||
|
def wrap(
|
||||||
|
self,
|
||||||
|
stream: t.Iterable[t.Tuple[int, str, str]],
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> t.Iterator[Token]:
|
||||||
|
"""This is called with the stream as returned by `tokenize` and wraps
|
||||||
|
every token in a :class:`Token` and converts the value.
|
||||||
|
"""
|
||||||
|
for lineno, token, value_str in stream:
|
||||||
|
if token in ignored_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value: t.Any = value_str
|
||||||
|
|
||||||
|
if token == TOKEN_LINESTATEMENT_BEGIN:
|
||||||
|
token = TOKEN_BLOCK_BEGIN
|
||||||
|
elif token == TOKEN_LINESTATEMENT_END:
|
||||||
|
token = TOKEN_BLOCK_END
|
||||||
|
# we are not interested in those tokens in the parser
|
||||||
|
elif token in (TOKEN_RAW_BEGIN, TOKEN_RAW_END):
|
||||||
|
continue
|
||||||
|
elif token == TOKEN_DATA:
|
||||||
|
value = self._normalize_newlines(value_str)
|
||||||
|
elif token == "keyword":
|
||||||
|
token = value_str
|
||||||
|
elif token == TOKEN_NAME:
|
||||||
|
value = value_str
|
||||||
|
|
||||||
|
if not value.isidentifier():
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
"Invalid character in identifier", lineno, name, filename
|
||||||
|
)
|
||||||
|
elif token == TOKEN_STRING:
|
||||||
|
# try to unescape string
|
||||||
|
try:
|
||||||
|
value = (
|
||||||
|
self._normalize_newlines(value_str[1:-1])
|
||||||
|
.encode("ascii", "backslashreplace")
|
||||||
|
.decode("unicode-escape")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = str(e).split(":")[-1].strip()
|
||||||
|
raise TemplateSyntaxError(msg, lineno, name, filename) from e
|
||||||
|
elif token == TOKEN_INTEGER:
|
||||||
|
value = int(value_str.replace("_", ""), 0)
|
||||||
|
elif token == TOKEN_FLOAT:
|
||||||
|
# remove all "_" first to support more Python versions
|
||||||
|
value = literal_eval(value_str.replace("_", ""))
|
||||||
|
elif token == TOKEN_OPERATOR:
|
||||||
|
token = operators[value_str]
|
||||||
|
|
||||||
|
yield Token(lineno, token, value)
|
||||||
|
|
||||||
|
def tokeniter(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
name: t.Optional[str],
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
state: t.Optional[str] = None,
|
||||||
|
) -> t.Iterator[t.Tuple[int, str, str]]:
|
||||||
|
"""This method tokenizes the text and returns the tokens in a
|
||||||
|
generator. Use this method if you just want to tokenize a template.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Only ``\\n``, ``\\r\\n`` and ``\\r`` are treated as line
|
||||||
|
breaks.
|
||||||
|
"""
|
||||||
|
lines = newline_re.split(source)[::2]
|
||||||
|
|
||||||
|
if not self.keep_trailing_newline and lines[-1] == "":
|
||||||
|
del lines[-1]
|
||||||
|
|
||||||
|
source = "\n".join(lines)
|
||||||
|
pos = 0
|
||||||
|
lineno = 1
|
||||||
|
stack = ["root"]
|
||||||
|
|
||||||
|
if state is not None and state != "root":
|
||||||
|
assert state in ("variable", "block"), "invalid state"
|
||||||
|
stack.append(state + "_begin")
|
||||||
|
|
||||||
|
statetokens = self.rules[stack[-1]]
|
||||||
|
source_length = len(source)
|
||||||
|
balancing_stack: t.List[str] = []
|
||||||
|
newlines_stripped = 0
|
||||||
|
line_starting = True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# tokenizer loop
|
||||||
|
for regex, tokens, new_state in statetokens:
|
||||||
|
m = regex.match(source, pos)
|
||||||
|
|
||||||
|
# if no match we try again with the next rule
|
||||||
|
if m is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# we only match blocks and variables if braces / parentheses
|
||||||
|
# are balanced. continue parsing with the lower rule which
|
||||||
|
# is the operator rule. do this only if the end tags look
|
||||||
|
# like operators
|
||||||
|
if balancing_stack and tokens in (
|
||||||
|
TOKEN_VARIABLE_END,
|
||||||
|
TOKEN_BLOCK_END,
|
||||||
|
TOKEN_LINESTATEMENT_END,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# tuples support more options
|
||||||
|
if isinstance(tokens, tuple):
|
||||||
|
groups: t.Sequence[str] = m.groups()
|
||||||
|
|
||||||
|
if isinstance(tokens, OptionalLStrip):
|
||||||
|
# Rule supports lstrip. Match will look like
|
||||||
|
# text, block type, whitespace control, type, control, ...
|
||||||
|
text = groups[0]
|
||||||
|
# Skipping the text and first type, every other group is the
|
||||||
|
# whitespace control for each type. One of the groups will be
|
||||||
|
# -, +, or empty string instead of None.
|
||||||
|
strip_sign = next(g for g in groups[2::2] if g is not None)
|
||||||
|
|
||||||
|
if strip_sign == "-":
|
||||||
|
# Strip all whitespace between the text and the tag.
|
||||||
|
stripped = text.rstrip()
|
||||||
|
newlines_stripped = text[len(stripped) :].count("\n")
|
||||||
|
groups = [stripped, *groups[1:]]
|
||||||
|
elif (
|
||||||
|
# Not marked for preserving whitespace.
|
||||||
|
strip_sign != "+"
|
||||||
|
# lstrip is enabled.
|
||||||
|
and self.lstrip_blocks
|
||||||
|
# Not a variable expression.
|
||||||
|
and not m.groupdict().get(TOKEN_VARIABLE_BEGIN)
|
||||||
|
):
|
||||||
|
# The start of text between the last newline and the tag.
|
||||||
|
l_pos = text.rfind("\n") + 1
|
||||||
|
|
||||||
|
if l_pos > 0 or line_starting:
|
||||||
|
# If there's only whitespace between the newline and the
|
||||||
|
# tag, strip it.
|
||||||
|
if whitespace_re.fullmatch(text, l_pos):
|
||||||
|
groups = [text[:l_pos], *groups[1:]]
|
||||||
|
|
||||||
|
for idx, token in enumerate(tokens):
|
||||||
|
# failure group
|
||||||
|
if token.__class__ is Failure:
|
||||||
|
raise token(lineno, filename)
|
||||||
|
# bygroup is a bit more complex, in that case we
|
||||||
|
# yield for the current token the first named
|
||||||
|
# group that matched
|
||||||
|
elif token == "#bygroup":
|
||||||
|
for key, value in m.groupdict().items():
|
||||||
|
if value is not None:
|
||||||
|
yield lineno, key, value
|
||||||
|
lineno += value.count("\n")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} wanted to resolve the token dynamically"
|
||||||
|
" but no group matched"
|
||||||
|
)
|
||||||
|
# normal group
|
||||||
|
else:
|
||||||
|
data = groups[idx]
|
||||||
|
|
||||||
|
if data or token not in ignore_if_empty:
|
||||||
|
yield lineno, token, data
|
||||||
|
|
||||||
|
lineno += data.count("\n") + newlines_stripped
|
||||||
|
newlines_stripped = 0
|
||||||
|
|
||||||
|
# strings as token just are yielded as it.
|
||||||
|
else:
|
||||||
|
data = m.group()
|
||||||
|
|
||||||
|
# update brace/parentheses balance
|
||||||
|
if tokens == TOKEN_OPERATOR:
|
||||||
|
if data == "{":
|
||||||
|
balancing_stack.append("}")
|
||||||
|
elif data == "(":
|
||||||
|
balancing_stack.append(")")
|
||||||
|
elif data == "[":
|
||||||
|
balancing_stack.append("]")
|
||||||
|
elif data in ("}", ")", "]"):
|
||||||
|
if not balancing_stack:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected '{data}'", lineno, name, filename
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_op = balancing_stack.pop()
|
||||||
|
|
||||||
|
if expected_op != data:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected '{data}', expected '{expected_op}'",
|
||||||
|
lineno,
|
||||||
|
name,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
# yield items
|
||||||
|
if data or tokens not in ignore_if_empty:
|
||||||
|
yield lineno, tokens, data
|
||||||
|
|
||||||
|
lineno += data.count("\n")
|
||||||
|
|
||||||
|
line_starting = m.group()[-1:] == "\n"
|
||||||
|
# fetch new position into new variable so that we can check
|
||||||
|
# if there is a internal parsing error which would result
|
||||||
|
# in an infinite loop
|
||||||
|
pos2 = m.end()
|
||||||
|
|
||||||
|
# handle state changes
|
||||||
|
if new_state is not None:
|
||||||
|
# remove the uppermost state
|
||||||
|
if new_state == "#pop":
|
||||||
|
stack.pop()
|
||||||
|
# resolve the new state by group checking
|
||||||
|
elif new_state == "#bygroup":
|
||||||
|
for key, value in m.groupdict().items():
|
||||||
|
if value is not None:
|
||||||
|
stack.append(key)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} wanted to resolve the new state dynamically"
|
||||||
|
f" but no group matched"
|
||||||
|
)
|
||||||
|
# direct state name given
|
||||||
|
else:
|
||||||
|
stack.append(new_state)
|
||||||
|
|
||||||
|
statetokens = self.rules[stack[-1]]
|
||||||
|
# we are still at the same position and no stack change.
|
||||||
|
# this means a loop without break condition, avoid that and
|
||||||
|
# raise error
|
||||||
|
elif pos2 == pos:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} yielded empty string without stack change"
|
||||||
|
)
|
||||||
|
|
||||||
|
# publish new function and start again
|
||||||
|
pos = pos2
|
||||||
|
break
|
||||||
|
# if loop terminated without break we haven't found a single match
|
||||||
|
# either we are at the end of the file or we have a problem
|
||||||
|
else:
|
||||||
|
# end of text
|
||||||
|
if pos >= source_length:
|
||||||
|
return
|
||||||
|
|
||||||
|
# something went wrong
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected char {source[pos]!r} at {pos}", lineno, name, filename
|
||||||
|
)
|
@ -0,0 +1,661 @@
|
|||||||
|
"""API and implementations for loading templates from different data
|
||||||
|
sources.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
import weakref
|
||||||
|
import zipimport
|
||||||
|
from collections import abc
|
||||||
|
from hashlib import sha1
|
||||||
|
from importlib import import_module
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
from .exceptions import TemplateNotFound
|
||||||
|
from .utils import internalcode
|
||||||
|
from .utils import open_if_exists
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
from .environment import Template
|
||||||
|
|
||||||
|
|
||||||
|
def split_template_path(template: str) -> t.List[str]:
|
||||||
|
"""Split a path into segments and perform a sanity check. If it detects
|
||||||
|
'..' in the path it will raise a `TemplateNotFound` error.
|
||||||
|
"""
|
||||||
|
pieces = []
|
||||||
|
for piece in template.split("/"):
|
||||||
|
if (
|
||||||
|
os.path.sep in piece
|
||||||
|
or (os.path.altsep and os.path.altsep in piece)
|
||||||
|
or piece == os.path.pardir
|
||||||
|
):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
elif piece and piece != ".":
|
||||||
|
pieces.append(piece)
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLoader:
|
||||||
|
"""Baseclass for all loaders. Subclass this and override `get_source` to
|
||||||
|
implement a custom loading mechanism. The environment provides a
|
||||||
|
`get_template` method that calls the loader's `load` method to get the
|
||||||
|
:class:`Template` object.
|
||||||
|
|
||||||
|
A very basic example for a loader that looks up templates on the file
|
||||||
|
system could look like this::
|
||||||
|
|
||||||
|
from jinja2 import BaseLoader, TemplateNotFound
|
||||||
|
from os.path import join, exists, getmtime
|
||||||
|
|
||||||
|
class MyLoader(BaseLoader):
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def get_source(self, environment, template):
|
||||||
|
path = join(self.path, template)
|
||||||
|
if not exists(path):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
mtime = getmtime(path)
|
||||||
|
with open(path) as f:
|
||||||
|
source = f.read()
|
||||||
|
return source, path, lambda: mtime == getmtime(path)
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: if set to `False` it indicates that the loader cannot provide access
|
||||||
|
#: to the source of templates.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.4
|
||||||
|
has_source_access = True
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
"""Get the template source, filename and reload helper for a template.
|
||||||
|
It's passed the environment and template name and has to return a
|
||||||
|
tuple in the form ``(source, filename, uptodate)`` or raise a
|
||||||
|
`TemplateNotFound` error if it can't locate the template.
|
||||||
|
|
||||||
|
The source part of the returned tuple must be the source of the
|
||||||
|
template as a string. The filename should be the name of the
|
||||||
|
file on the filesystem if it was loaded from there, otherwise
|
||||||
|
``None``. The filename is used by Python for the tracebacks
|
||||||
|
if no loader extension is used.
|
||||||
|
|
||||||
|
The last item in the tuple is the `uptodate` function. If auto
|
||||||
|
reloading is enabled it's always called to check if the template
|
||||||
|
changed. No arguments are passed so the function must store the
|
||||||
|
old state somewhere (for example in a closure). If it returns `False`
|
||||||
|
the template will be reloaded.
|
||||||
|
"""
|
||||||
|
if not self.has_source_access:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{type(self).__name__} cannot provide access to the source"
|
||||||
|
)
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
"""Iterates over all templates. If the loader does not support that
|
||||||
|
it should raise a :exc:`TypeError` which is the default behavior.
|
||||||
|
"""
|
||||||
|
raise TypeError("this loader cannot iterate over all templates")
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
"""Loads a template. This method looks up the template in the cache
|
||||||
|
or loads one by calling :meth:`get_source`. Subclasses should not
|
||||||
|
override this method as loaders working on collections of other
|
||||||
|
loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
|
||||||
|
will not call this method but `get_source` directly.
|
||||||
|
"""
|
||||||
|
code = None
|
||||||
|
if globals is None:
|
||||||
|
globals = {}
|
||||||
|
|
||||||
|
# first we try to get the source for this template together
|
||||||
|
# with the filename and the uptodate function.
|
||||||
|
source, filename, uptodate = self.get_source(environment, name)
|
||||||
|
|
||||||
|
# try to load the code from the bytecode cache if there is a
|
||||||
|
# bytecode cache configured.
|
||||||
|
bcc = environment.bytecode_cache
|
||||||
|
if bcc is not None:
|
||||||
|
bucket = bcc.get_bucket(environment, name, filename, source)
|
||||||
|
code = bucket.code
|
||||||
|
|
||||||
|
# if we don't have code so far (not cached, no longer up to
|
||||||
|
# date) etc. we compile the template
|
||||||
|
if code is None:
|
||||||
|
code = environment.compile(source, name, filename)
|
||||||
|
|
||||||
|
# if the bytecode cache is available and the bucket doesn't
|
||||||
|
# have a code so far, we give the bucket the new code and put
|
||||||
|
# it back to the bytecode cache.
|
||||||
|
if bcc is not None and bucket.code is None:
|
||||||
|
bucket.code = code
|
||||||
|
bcc.set_bucket(bucket)
|
||||||
|
|
||||||
|
return environment.template_class.from_code(
|
||||||
|
environment, code, globals, uptodate
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemLoader(BaseLoader):
|
||||||
|
"""Load templates from a directory in the file system.
|
||||||
|
|
||||||
|
The path can be relative or absolute. Relative paths are relative to
|
||||||
|
the current working directory.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = FileSystemLoader("templates")
|
||||||
|
|
||||||
|
A list of paths can be given. The directories will be searched in
|
||||||
|
order, stopping at the first matching template.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = FileSystemLoader(["/override/templates", "/default/templates"])
|
||||||
|
|
||||||
|
:param searchpath: A path, or list of paths, to the directory that
|
||||||
|
contains the templates.
|
||||||
|
:param encoding: Use this encoding to read the text from template
|
||||||
|
files.
|
||||||
|
:param followlinks: Follow symbolic links in the path.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.8
|
||||||
|
Added the ``followlinks`` parameter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
searchpath: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]],
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
followlinks: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
|
||||||
|
searchpath = [searchpath]
|
||||||
|
|
||||||
|
self.searchpath = [os.fspath(p) for p in searchpath]
|
||||||
|
self.encoding = encoding
|
||||||
|
self.followlinks = followlinks
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, str, t.Callable[[], bool]]:
|
||||||
|
pieces = split_template_path(template)
|
||||||
|
for searchpath in self.searchpath:
|
||||||
|
# Use posixpath even on Windows to avoid "drive:" or UNC
|
||||||
|
# segments breaking out of the search directory.
|
||||||
|
filename = posixpath.join(searchpath, *pieces)
|
||||||
|
f = open_if_exists(filename)
|
||||||
|
if f is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
contents = f.read().decode(self.encoding)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(filename)
|
||||||
|
|
||||||
|
def uptodate() -> bool:
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(filename) == mtime
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Use normpath to convert Windows altsep to sep.
|
||||||
|
return contents, os.path.normpath(filename), uptodate
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
found = set()
|
||||||
|
for searchpath in self.searchpath:
|
||||||
|
walk_dir = os.walk(searchpath, followlinks=self.followlinks)
|
||||||
|
for dirpath, _, filenames in walk_dir:
|
||||||
|
for filename in filenames:
|
||||||
|
template = (
|
||||||
|
os.path.join(dirpath, filename)[len(searchpath) :]
|
||||||
|
.strip(os.path.sep)
|
||||||
|
.replace(os.path.sep, "/")
|
||||||
|
)
|
||||||
|
if template[:2] == "./":
|
||||||
|
template = template[2:]
|
||||||
|
if template not in found:
|
||||||
|
found.add(template)
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageLoader(BaseLoader):
|
||||||
|
"""Load templates from a directory in a Python package.
|
||||||
|
|
||||||
|
:param package_name: Import name of the package that contains the
|
||||||
|
template directory.
|
||||||
|
:param package_path: Directory within the imported package that
|
||||||
|
contains the templates.
|
||||||
|
:param encoding: Encoding of template files.
|
||||||
|
|
||||||
|
The following example looks up templates in the ``pages`` directory
|
||||||
|
within the ``project.ui`` package.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = PackageLoader("project.ui", "pages")
|
||||||
|
|
||||||
|
Only packages installed as directories (standard pip behavior) or
|
||||||
|
zip/egg files (less common) are supported. The Python API for
|
||||||
|
introspecting data in packages is too limited to support other
|
||||||
|
installation methods the way this loader requires.
|
||||||
|
|
||||||
|
There is limited support for :pep:`420` namespace packages. The
|
||||||
|
template directory is assumed to only be in one namespace
|
||||||
|
contributor. Zip files contributing to a namespace are not
|
||||||
|
supported.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
No longer uses ``setuptools`` as a dependency.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Limited PEP 420 namespace package support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
package_name: str,
|
||||||
|
package_path: "str" = "templates",
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
) -> None:
|
||||||
|
package_path = os.path.normpath(package_path).rstrip(os.path.sep)
|
||||||
|
|
||||||
|
# normpath preserves ".", which isn't valid in zip paths.
|
||||||
|
if package_path == os.path.curdir:
|
||||||
|
package_path = ""
|
||||||
|
elif package_path[:2] == os.path.curdir + os.path.sep:
|
||||||
|
package_path = package_path[2:]
|
||||||
|
|
||||||
|
self.package_path = package_path
|
||||||
|
self.package_name = package_name
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
# Make sure the package exists. This also makes namespace
|
||||||
|
# packages work, otherwise get_loader returns None.
|
||||||
|
import_module(package_name)
|
||||||
|
spec = importlib.util.find_spec(package_name)
|
||||||
|
assert spec is not None, "An import spec was not found for the package."
|
||||||
|
loader = spec.loader
|
||||||
|
assert loader is not None, "A loader was not found for the package."
|
||||||
|
self._loader = loader
|
||||||
|
self._archive = None
|
||||||
|
template_root = None
|
||||||
|
|
||||||
|
if isinstance(loader, zipimport.zipimporter):
|
||||||
|
self._archive = loader.archive
|
||||||
|
pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
|
||||||
|
template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep)
|
||||||
|
else:
|
||||||
|
roots: t.List[str] = []
|
||||||
|
|
||||||
|
# One element for regular packages, multiple for namespace
|
||||||
|
# packages, or None for single module file.
|
||||||
|
if spec.submodule_search_locations:
|
||||||
|
roots.extend(spec.submodule_search_locations)
|
||||||
|
# A single module file, use the parent directory instead.
|
||||||
|
elif spec.origin is not None:
|
||||||
|
roots.append(os.path.dirname(spec.origin))
|
||||||
|
|
||||||
|
for root in roots:
|
||||||
|
root = os.path.join(root, package_path)
|
||||||
|
|
||||||
|
if os.path.isdir(root):
|
||||||
|
template_root = root
|
||||||
|
break
|
||||||
|
|
||||||
|
if template_root is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"The {package_name!r} package was not installed in a"
|
||||||
|
" way that PackageLoader understands."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._template_root = template_root
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
|
||||||
|
# Use posixpath even on Windows to avoid "drive:" or UNC
|
||||||
|
# segments breaking out of the search directory. Use normpath to
|
||||||
|
# convert Windows altsep to sep.
|
||||||
|
p = os.path.normpath(
|
||||||
|
posixpath.join(self._template_root, *split_template_path(template))
|
||||||
|
)
|
||||||
|
up_to_date: t.Optional[t.Callable[[], bool]]
|
||||||
|
|
||||||
|
if self._archive is None:
|
||||||
|
# Package is a directory.
|
||||||
|
if not os.path.isfile(p):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
with open(p, "rb") as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(p)
|
||||||
|
|
||||||
|
def up_to_date() -> bool:
|
||||||
|
return os.path.isfile(p) and os.path.getmtime(p) == mtime
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Package is a zip file.
|
||||||
|
try:
|
||||||
|
source = self._loader.get_data(p) # type: ignore
|
||||||
|
except OSError as e:
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
|
||||||
|
# Could use the zip's mtime for all template mtimes, but
|
||||||
|
# would need to safely reload the module if it's out of
|
||||||
|
# date, so just report it as always current.
|
||||||
|
up_to_date = None
|
||||||
|
|
||||||
|
return source.decode(self.encoding), p, up_to_date
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
results: t.List[str] = []
|
||||||
|
|
||||||
|
if self._archive is None:
|
||||||
|
# Package is a directory.
|
||||||
|
offset = len(self._template_root)
|
||||||
|
|
||||||
|
for dirpath, _, filenames in os.walk(self._template_root):
|
||||||
|
dirpath = dirpath[offset:].lstrip(os.path.sep)
|
||||||
|
results.extend(
|
||||||
|
os.path.join(dirpath, name).replace(os.path.sep, "/")
|
||||||
|
for name in filenames
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not hasattr(self._loader, "_files"):
|
||||||
|
raise TypeError(
|
||||||
|
"This zip import does not have the required"
|
||||||
|
" metadata to list templates."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Package is a zip file.
|
||||||
|
prefix = (
|
||||||
|
self._template_root[len(self._archive) :].lstrip(os.path.sep)
|
||||||
|
+ os.path.sep
|
||||||
|
)
|
||||||
|
offset = len(prefix)
|
||||||
|
|
||||||
|
for name in self._loader._files.keys(): # type: ignore
|
||||||
|
# Find names under the templates directory that aren't directories.
|
||||||
|
if name.startswith(prefix) and name[-1] != os.path.sep:
|
||||||
|
results.append(name[offset:].replace(os.path.sep, "/"))
|
||||||
|
|
||||||
|
results.sort()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class DictLoader(BaseLoader):
|
||||||
|
"""Loads a template from a Python dict mapping template names to
|
||||||
|
template source. This loader is useful for unittesting:
|
||||||
|
|
||||||
|
>>> loader = DictLoader({'index.html': 'source here'})
|
||||||
|
|
||||||
|
Because auto reloading is rarely useful this is disabled per default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapping: t.Mapping[str, str]) -> None:
|
||||||
|
self.mapping = mapping
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, None, t.Callable[[], bool]]:
|
||||||
|
if template in self.mapping:
|
||||||
|
source = self.mapping[template]
|
||||||
|
return source, None, lambda: source == self.mapping.get(template)
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
return sorted(self.mapping)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionLoader(BaseLoader):
|
||||||
|
"""A loader that is passed a function which does the loading. The
|
||||||
|
function receives the name of the template and has to return either
|
||||||
|
a string with the template source, a tuple in the form ``(source,
|
||||||
|
filename, uptodatefunc)`` or `None` if the template does not exist.
|
||||||
|
|
||||||
|
>>> def load_template(name):
|
||||||
|
... if name == 'index.html':
|
||||||
|
... return '...'
|
||||||
|
...
|
||||||
|
>>> loader = FunctionLoader(load_template)
|
||||||
|
|
||||||
|
The `uptodatefunc` is a function that is called if autoreload is enabled
|
||||||
|
and has to return `True` if the template is still up to date. For more
|
||||||
|
details have a look at :meth:`BaseLoader.get_source` which has the same
|
||||||
|
return value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
load_func: t.Callable[
|
||||||
|
[str],
|
||||||
|
t.Optional[
|
||||||
|
t.Union[
|
||||||
|
str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
self.load_func = load_func
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
rv = self.load_func(template)
|
||||||
|
|
||||||
|
if rv is None:
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
if isinstance(rv, str):
|
||||||
|
return rv, None, None
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixLoader(BaseLoader):
|
||||||
|
"""A loader that is passed a dict of loaders where each loader is bound
|
||||||
|
to a prefix. The prefix is delimited from the template by a slash per
|
||||||
|
default, which can be changed by setting the `delimiter` argument to
|
||||||
|
something else::
|
||||||
|
|
||||||
|
loader = PrefixLoader({
|
||||||
|
'app1': PackageLoader('mypackage.app1'),
|
||||||
|
'app2': PackageLoader('mypackage.app2')
|
||||||
|
})
|
||||||
|
|
||||||
|
By loading ``'app1/index.html'`` the file from the app1 package is loaded,
|
||||||
|
by loading ``'app2/index.html'`` the file from the second.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/"
|
||||||
|
) -> None:
|
||||||
|
self.mapping = mapping
|
||||||
|
self.delimiter = delimiter
|
||||||
|
|
||||||
|
def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]:
|
||||||
|
try:
|
||||||
|
prefix, name = template.split(self.delimiter, 1)
|
||||||
|
loader = self.mapping[prefix]
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
return loader, name
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
loader, name = self.get_loader(template)
|
||||||
|
try:
|
||||||
|
return loader.get_source(environment, name)
|
||||||
|
except TemplateNotFound as e:
|
||||||
|
# re-raise the exception with the correct filename here.
|
||||||
|
# (the one that includes the prefix)
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
loader, local_name = self.get_loader(name)
|
||||||
|
try:
|
||||||
|
return loader.load(environment, local_name, globals)
|
||||||
|
except TemplateNotFound as e:
|
||||||
|
# re-raise the exception with the correct filename here.
|
||||||
|
# (the one that includes the prefix)
|
||||||
|
raise TemplateNotFound(name) from e
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
result = []
|
||||||
|
for prefix, loader in self.mapping.items():
|
||||||
|
for template in loader.list_templates():
|
||||||
|
result.append(prefix + self.delimiter + template)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceLoader(BaseLoader):
|
||||||
|
"""This loader works like the `PrefixLoader` just that no prefix is
|
||||||
|
specified. If a template could not be found by one loader the next one
|
||||||
|
is tried.
|
||||||
|
|
||||||
|
>>> loader = ChoiceLoader([
|
||||||
|
... FileSystemLoader('/path/to/user/templates'),
|
||||||
|
... FileSystemLoader('/path/to/system/templates')
|
||||||
|
... ])
|
||||||
|
|
||||||
|
This is useful if you want to allow users to override builtin templates
|
||||||
|
from a different location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, loaders: t.Sequence[BaseLoader]) -> None:
|
||||||
|
self.loaders = loaders
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
for loader in self.loaders:
|
||||||
|
try:
|
||||||
|
return loader.get_source(environment, template)
|
||||||
|
except TemplateNotFound:
|
||||||
|
pass
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
for loader in self.loaders:
|
||||||
|
try:
|
||||||
|
return loader.load(environment, name, globals)
|
||||||
|
except TemplateNotFound:
|
||||||
|
pass
|
||||||
|
raise TemplateNotFound(name)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
found = set()
|
||||||
|
for loader in self.loaders:
|
||||||
|
found.update(loader.list_templates())
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
class _TemplateModule(ModuleType):
|
||||||
|
"""Like a normal module but with support for weak references"""
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLoader(BaseLoader):
|
||||||
|
"""This loader loads templates from precompiled templates.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
>>> loader = ChoiceLoader([
|
||||||
|
... ModuleLoader('/path/to/compiled/templates'),
|
||||||
|
... FileSystemLoader('/path/to/templates')
|
||||||
|
... ])
|
||||||
|
|
||||||
|
Templates can be precompiled with :meth:`Environment.compile_templates`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
has_source_access = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]]
|
||||||
|
) -> None:
|
||||||
|
package_name = f"_jinja2_module_templates_{id(self):x}"
|
||||||
|
|
||||||
|
# create a fake module that looks for the templates in the
|
||||||
|
# path given.
|
||||||
|
mod = _TemplateModule(package_name)
|
||||||
|
|
||||||
|
if not isinstance(path, abc.Iterable) or isinstance(path, str):
|
||||||
|
path = [path]
|
||||||
|
|
||||||
|
mod.__path__ = [os.fspath(p) for p in path]
|
||||||
|
|
||||||
|
sys.modules[package_name] = weakref.proxy(
|
||||||
|
mod, lambda x: sys.modules.pop(package_name, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# the only strong reference, the sys.modules entry is weak
|
||||||
|
# so that the garbage collector can remove it once the
|
||||||
|
# loader that created it goes out of business.
|
||||||
|
self.module = mod
|
||||||
|
self.package_name = package_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_template_key(name: str) -> str:
|
||||||
|
return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_module_filename(name: str) -> str:
|
||||||
|
return ModuleLoader.get_template_key(name) + ".py"
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
key = self.get_template_key(name)
|
||||||
|
module = f"{self.package_name}.{key}"
|
||||||
|
mod = getattr(self.module, module, None)
|
||||||
|
|
||||||
|
if mod is None:
|
||||||
|
try:
|
||||||
|
mod = __import__(module, None, None, ["root"])
|
||||||
|
except ImportError as e:
|
||||||
|
raise TemplateNotFound(name) from e
|
||||||
|
|
||||||
|
# remove the entry from sys.modules, we only want the attribute
|
||||||
|
# on the module object we have stored on the loader.
|
||||||
|
sys.modules.pop(module, None)
|
||||||
|
|
||||||
|
if globals is None:
|
||||||
|
globals = {}
|
||||||
|
|
||||||
|
return environment.template_class.from_module_dict(
|
||||||
|
environment, mod.__dict__, globals
|
||||||
|
)
|
@ -0,0 +1,111 @@
|
|||||||
|
"""Functions that expose information about templates that might be
|
||||||
|
interesting for introspection.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .compiler import CodeGenerator
|
||||||
|
from .compiler import Frame
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
class TrackingCodeGenerator(CodeGenerator):
|
||||||
|
"""We abuse the code generator for introspection."""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment") -> None:
|
||||||
|
super().__init__(environment, "<introspection>", "<introspection>")
|
||||||
|
self.undeclared_identifiers: t.Set[str] = set()
|
||||||
|
|
||||||
|
def write(self, x: str) -> None:
|
||||||
|
"""Don't write."""
|
||||||
|
|
||||||
|
def enter_frame(self, frame: Frame) -> None:
|
||||||
|
"""Remember all undeclared identifiers."""
|
||||||
|
super().enter_frame(frame)
|
||||||
|
|
||||||
|
for _, (action, param) in frame.symbols.loads.items():
|
||||||
|
if action == "resolve" and param not in self.environment.globals:
|
||||||
|
self.undeclared_identifiers.add(param)
|
||||||
|
|
||||||
|
|
||||||
|
def find_undeclared_variables(ast: nodes.Template) -> t.Set[str]:
|
||||||
|
"""Returns a set of all variables in the AST that will be looked up from
|
||||||
|
the context at runtime. Because at compile time it's not known which
|
||||||
|
variables will be used depending on the path the execution takes at
|
||||||
|
runtime, all variables are returned.
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment, meta
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}')
|
||||||
|
>>> meta.find_undeclared_variables(ast) == {'bar'}
|
||||||
|
True
|
||||||
|
|
||||||
|
.. admonition:: Implementation
|
||||||
|
|
||||||
|
Internally the code generator is used for finding undeclared variables.
|
||||||
|
This is good to know because the code generator might raise a
|
||||||
|
:exc:`TemplateAssertionError` during compilation and as a matter of
|
||||||
|
fact this function can currently raise that exception as well.
|
||||||
|
"""
|
||||||
|
codegen = TrackingCodeGenerator(ast.environment) # type: ignore
|
||||||
|
codegen.visit(ast)
|
||||||
|
return codegen.undeclared_identifiers
|
||||||
|
|
||||||
|
|
||||||
|
_ref_types = (nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include)
|
||||||
|
_RefType = t.Union[nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include]
|
||||||
|
|
||||||
|
|
||||||
|
def find_referenced_templates(ast: nodes.Template) -> t.Iterator[t.Optional[str]]:
|
||||||
|
"""Finds all the referenced templates from the AST. This will return an
|
||||||
|
iterator over all the hardcoded template extensions, inclusions and
|
||||||
|
imports. If dynamic inheritance or inclusion is used, `None` will be
|
||||||
|
yielded.
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment, meta
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> ast = env.parse('{% extends "layout.html" %}{% include helper %}')
|
||||||
|
>>> list(meta.find_referenced_templates(ast))
|
||||||
|
['layout.html', None]
|
||||||
|
|
||||||
|
This function is useful for dependency tracking. For example if you want
|
||||||
|
to rebuild parts of the website after a layout template has changed.
|
||||||
|
"""
|
||||||
|
template_name: t.Any
|
||||||
|
|
||||||
|
for node in ast.find_all(_ref_types):
|
||||||
|
template: nodes.Expr = node.template # type: ignore
|
||||||
|
|
||||||
|
if not isinstance(template, nodes.Const):
|
||||||
|
# a tuple with some non consts in there
|
||||||
|
if isinstance(template, (nodes.Tuple, nodes.List)):
|
||||||
|
for template_name in template.items:
|
||||||
|
# something const, only yield the strings and ignore
|
||||||
|
# non-string consts that really just make no sense
|
||||||
|
if isinstance(template_name, nodes.Const):
|
||||||
|
if isinstance(template_name.value, str):
|
||||||
|
yield template_name.value
|
||||||
|
# something dynamic in there
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
# something dynamic we don't know about here
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
continue
|
||||||
|
# constant is a basestring, direct template name
|
||||||
|
if isinstance(template.value, str):
|
||||||
|
yield template.value
|
||||||
|
# a tuple or list (latter *should* not happen) made of consts,
|
||||||
|
# yield the consts that are strings. We could warn here for
|
||||||
|
# non string values
|
||||||
|
elif isinstance(node, nodes.Include) and isinstance(
|
||||||
|
template.value, (tuple, list)
|
||||||
|
):
|
||||||
|
for template_name in template.value:
|
||||||
|
if isinstance(template_name, str):
|
||||||
|
yield template_name
|
||||||
|
# something else we don't care about, we could warn here
|
||||||
|
else:
|
||||||
|
yield None
|
@ -0,0 +1,130 @@
|
|||||||
|
import typing as t
|
||||||
|
from ast import literal_eval
|
||||||
|
from ast import parse
|
||||||
|
from itertools import chain
|
||||||
|
from itertools import islice
|
||||||
|
from types import GeneratorType
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .compiler import CodeGenerator
|
||||||
|
from .compiler import Frame
|
||||||
|
from .compiler import has_safe_repr
|
||||||
|
from .environment import Environment
|
||||||
|
from .environment import Template
|
||||||
|
|
||||||
|
|
||||||
|
def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]:
|
||||||
|
"""Return a native Python type from the list of compiled nodes. If
|
||||||
|
the result is a single node, its value is returned. Otherwise, the
|
||||||
|
nodes are concatenated as strings. If the result can be parsed with
|
||||||
|
:func:`ast.literal_eval`, the parsed value is returned. Otherwise,
|
||||||
|
the string is returned.
|
||||||
|
|
||||||
|
:param values: Iterable of outputs to concatenate.
|
||||||
|
"""
|
||||||
|
head = list(islice(values, 2))
|
||||||
|
|
||||||
|
if not head:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(head) == 1:
|
||||||
|
raw = head[0]
|
||||||
|
if not isinstance(raw, str):
|
||||||
|
return raw
|
||||||
|
else:
|
||||||
|
if isinstance(values, GeneratorType):
|
||||||
|
values = chain(head, values)
|
||||||
|
raw = "".join([str(v) for v in values])
|
||||||
|
|
||||||
|
try:
|
||||||
|
return literal_eval(
|
||||||
|
# In Python 3.10+ ast.literal_eval removes leading spaces/tabs
|
||||||
|
# from the given string. For backwards compatibility we need to
|
||||||
|
# parse the string ourselves without removing leading spaces/tabs.
|
||||||
|
parse(raw, mode="eval")
|
||||||
|
)
|
||||||
|
except (ValueError, SyntaxError, MemoryError):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
class NativeCodeGenerator(CodeGenerator):
|
||||||
|
"""A code generator which renders Python types by not adding
|
||||||
|
``str()`` around output nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_finalize(value: t.Any) -> t.Any:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _output_const_repr(self, group: t.Iterable[t.Any]) -> str:
|
||||||
|
return repr("".join([str(v) for v in group]))
|
||||||
|
|
||||||
|
def _output_child_to_const(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> t.Any:
|
||||||
|
const = node.as_const(frame.eval_ctx)
|
||||||
|
|
||||||
|
if not has_safe_repr(const):
|
||||||
|
raise nodes.Impossible()
|
||||||
|
|
||||||
|
if isinstance(node, nodes.TemplateData):
|
||||||
|
return const
|
||||||
|
|
||||||
|
return finalize.const(const) # type: ignore
|
||||||
|
|
||||||
|
def _output_child_pre(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> None:
|
||||||
|
if finalize.src is not None:
|
||||||
|
self.write(finalize.src)
|
||||||
|
|
||||||
|
def _output_child_post(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> None:
|
||||||
|
if finalize.src is not None:
|
||||||
|
self.write(")")
|
||||||
|
|
||||||
|
|
||||||
|
class NativeEnvironment(Environment):
|
||||||
|
"""An environment that renders templates to native Python types."""
|
||||||
|
|
||||||
|
code_generator_class = NativeCodeGenerator
|
||||||
|
concat = staticmethod(native_concat) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class NativeTemplate(Template):
|
||||||
|
environment_class = NativeEnvironment
|
||||||
|
|
||||||
|
def render(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Render the template to produce a native Python type. If the
|
||||||
|
result is a single node, its value is returned. Otherwise, the
|
||||||
|
nodes are concatenated as strings. If the result can be parsed
|
||||||
|
with :func:`ast.literal_eval`, the parsed value is returned.
|
||||||
|
Otherwise, the string is returned.
|
||||||
|
"""
|
||||||
|
ctx = self.new_context(dict(*args, **kwargs))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.environment_class.concat( # type: ignore
|
||||||
|
self.root_render_func(ctx) # type: ignore
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return self.environment.handle_exception()
|
||||||
|
|
||||||
|
async def render_async(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
if not self.environment.is_async:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The environment was not created with async mode enabled."
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = self.new_context(dict(*args, **kwargs))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.environment_class.concat( # type: ignore
|
||||||
|
[n async for n in self.root_render_func(ctx)] # type: ignore
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return self.environment.handle_exception()
|
||||||
|
|
||||||
|
|
||||||
|
NativeEnvironment.template_class = NativeTemplate
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@
|
|||||||
|
"""The optimizer tries to constant fold expressions and modify the AST
|
||||||
|
in place so that it should be faster to evaluate.
|
||||||
|
|
||||||
|
Because the AST does not contain all the scoping information and the
|
||||||
|
compiler has to find that out, we cannot do all the optimizations we
|
||||||
|
want. For example, loop unrolling doesn't work because unrolled loops
|
||||||
|
would have a different scope. The solution would be a second syntax tree
|
||||||
|
that stored the scoping rules.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .visitor import NodeTransformer
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def optimize(node: nodes.Node, environment: "Environment") -> nodes.Node:
|
||||||
|
"""The context hint can be used to perform an static optimization
|
||||||
|
based on the context given."""
|
||||||
|
optimizer = Optimizer(environment)
|
||||||
|
return t.cast(nodes.Node, optimizer.visit(node))
|
||||||
|
|
||||||
|
|
||||||
|
class Optimizer(NodeTransformer):
|
||||||
|
def __init__(self, environment: "t.Optional[Environment]") -> None:
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
def generic_visit(
|
||||||
|
self, node: nodes.Node, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> nodes.Node:
|
||||||
|
node = super().generic_visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
# Do constant folding. Some other nodes besides Expr have
|
||||||
|
# as_const, but folding them causes errors later on.
|
||||||
|
if isinstance(node, nodes.Expr):
|
||||||
|
try:
|
||||||
|
return nodes.Const.from_untrusted(
|
||||||
|
node.as_const(args[0] if args else None),
|
||||||
|
lineno=node.lineno,
|
||||||
|
environment=self.environment,
|
||||||
|
)
|
||||||
|
except nodes.Impossible:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return node
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,428 @@
|
|||||||
|
"""A sandbox layer that ensures unsafe operations cannot be performed.
|
||||||
|
Useful when the template itself comes from an untrusted source.
|
||||||
|
"""
|
||||||
|
import operator
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
from _string import formatter_field_name_split # type: ignore
|
||||||
|
from collections import abc
|
||||||
|
from collections import deque
|
||||||
|
from string import Formatter
|
||||||
|
|
||||||
|
from markupsafe import EscapeFormatter
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from .environment import Environment
|
||||||
|
from .exceptions import SecurityError
|
||||||
|
from .runtime import Context
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
#: maximum number of items a range may produce
|
||||||
|
MAX_RANGE = 100000
|
||||||
|
|
||||||
|
#: Unsafe function attributes.
|
||||||
|
UNSAFE_FUNCTION_ATTRIBUTES: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: Unsafe method attributes. Function attributes are unsafe for methods too.
|
||||||
|
UNSAFE_METHOD_ATTRIBUTES: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: unsafe generator attributes.
|
||||||
|
UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
|
||||||
|
|
||||||
|
#: unsafe attributes on coroutines
|
||||||
|
UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
|
||||||
|
|
||||||
|
#: unsafe attributes on async generators
|
||||||
|
UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
|
||||||
|
|
||||||
|
_mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = (
|
||||||
|
(
|
||||||
|
abc.MutableSet,
|
||||||
|
frozenset(
|
||||||
|
[
|
||||||
|
"add",
|
||||||
|
"clear",
|
||||||
|
"difference_update",
|
||||||
|
"discard",
|
||||||
|
"pop",
|
||||||
|
"remove",
|
||||||
|
"symmetric_difference_update",
|
||||||
|
"update",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
abc.MutableMapping,
|
||||||
|
frozenset(["clear", "pop", "popitem", "setdefault", "update"]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
abc.MutableSequence,
|
||||||
|
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
deque,
|
||||||
|
frozenset(
|
||||||
|
[
|
||||||
|
"append",
|
||||||
|
"appendleft",
|
||||||
|
"clear",
|
||||||
|
"extend",
|
||||||
|
"extendleft",
|
||||||
|
"pop",
|
||||||
|
"popleft",
|
||||||
|
"remove",
|
||||||
|
"rotate",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_format_method(callable: t.Callable) -> t.Optional[str]:
|
||||||
|
if not isinstance(
|
||||||
|
callable, (types.MethodType, types.BuiltinMethodType)
|
||||||
|
) or callable.__name__ not in ("format", "format_map"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
obj = callable.__self__
|
||||||
|
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def safe_range(*args: int) -> range:
|
||||||
|
"""A range that can't generate ranges with a length of more than
|
||||||
|
MAX_RANGE items.
|
||||||
|
"""
|
||||||
|
rng = range(*args)
|
||||||
|
|
||||||
|
if len(rng) > MAX_RANGE:
|
||||||
|
raise OverflowError(
|
||||||
|
"Range too big. The sandbox blocks ranges larger than"
|
||||||
|
f" MAX_RANGE ({MAX_RANGE})."
|
||||||
|
)
|
||||||
|
|
||||||
|
return rng
|
||||||
|
|
||||||
|
|
||||||
|
def unsafe(f: F) -> F:
|
||||||
|
"""Marks a function or method as unsafe.
|
||||||
|
|
||||||
|
.. code-block: python
|
||||||
|
|
||||||
|
@unsafe
|
||||||
|
def delete(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
f.unsafe_callable = True # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def is_internal_attribute(obj: t.Any, attr: str) -> bool:
|
||||||
|
"""Test if the attribute given is an internal python attribute. For
|
||||||
|
example this function returns `True` for the `func_code` attribute of
|
||||||
|
python objects. This is useful if the environment method
|
||||||
|
:meth:`~SandboxedEnvironment.is_safe_attribute` is overridden.
|
||||||
|
|
||||||
|
>>> from jinja2.sandbox import is_internal_attribute
|
||||||
|
>>> is_internal_attribute(str, "mro")
|
||||||
|
True
|
||||||
|
>>> is_internal_attribute(str, "upper")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
if isinstance(obj, types.FunctionType):
|
||||||
|
if attr in UNSAFE_FUNCTION_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, types.MethodType):
|
||||||
|
if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, type):
|
||||||
|
if attr == "mro":
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, types.GeneratorType):
|
||||||
|
if attr in UNSAFE_GENERATOR_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):
|
||||||
|
if attr in UNSAFE_COROUTINE_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif hasattr(types, "AsyncGeneratorType") and isinstance(
|
||||||
|
obj, types.AsyncGeneratorType
|
||||||
|
):
|
||||||
|
if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
return attr.startswith("__")
|
||||||
|
|
||||||
|
|
||||||
|
def modifies_known_mutable(obj: t.Any, attr: str) -> bool:
|
||||||
|
"""This function checks if an attribute on a builtin mutable object
|
||||||
|
(list, dict, set or deque) or the corresponding ABCs would modify it
|
||||||
|
if called.
|
||||||
|
|
||||||
|
>>> modifies_known_mutable({}, "clear")
|
||||||
|
True
|
||||||
|
>>> modifies_known_mutable({}, "keys")
|
||||||
|
False
|
||||||
|
>>> modifies_known_mutable([], "append")
|
||||||
|
True
|
||||||
|
>>> modifies_known_mutable([], "index")
|
||||||
|
False
|
||||||
|
|
||||||
|
If called with an unsupported object, ``False`` is returned.
|
||||||
|
|
||||||
|
>>> modifies_known_mutable("foo", "upper")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
for typespec, unsafe in _mutable_spec:
|
||||||
|
if isinstance(obj, typespec):
|
||||||
|
return attr in unsafe
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedEnvironment(Environment):
|
||||||
|
"""The sandboxed environment. It works like the regular environment but
|
||||||
|
tells the compiler to generate sandboxed code. Additionally subclasses of
|
||||||
|
this environment may override the methods that tell the runtime what
|
||||||
|
attributes or functions are safe to access.
|
||||||
|
|
||||||
|
If the template tries to access insecure code a :exc:`SecurityError` is
|
||||||
|
raised. However also other exceptions may occur during the rendering so
|
||||||
|
the caller has to ensure that all exceptions are caught.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sandboxed = True
|
||||||
|
|
||||||
|
#: default callback table for the binary operators. A copy of this is
|
||||||
|
#: available on each instance of a sandboxed environment as
|
||||||
|
#: :attr:`binop_table`
|
||||||
|
default_binop_table: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||||
|
"+": operator.add,
|
||||||
|
"-": operator.sub,
|
||||||
|
"*": operator.mul,
|
||||||
|
"/": operator.truediv,
|
||||||
|
"//": operator.floordiv,
|
||||||
|
"**": operator.pow,
|
||||||
|
"%": operator.mod,
|
||||||
|
}
|
||||||
|
|
||||||
|
#: default callback table for the unary operators. A copy of this is
|
||||||
|
#: available on each instance of a sandboxed environment as
|
||||||
|
#: :attr:`unop_table`
|
||||||
|
default_unop_table: t.Dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||||
|
"+": operator.pos,
|
||||||
|
"-": operator.neg,
|
||||||
|
}
|
||||||
|
|
||||||
|
#: a set of binary operators that should be intercepted. Each operator
|
||||||
|
#: that is added to this set (empty by default) is delegated to the
|
||||||
|
#: :meth:`call_binop` method that will perform the operator. The default
|
||||||
|
#: operator callback is specified by :attr:`binop_table`.
|
||||||
|
#:
|
||||||
|
#: The following binary operators are interceptable:
|
||||||
|
#: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**``
|
||||||
|
#:
|
||||||
|
#: The default operation form the operator table corresponds to the
|
||||||
|
#: builtin function. Intercepted calls are always slower than the native
|
||||||
|
#: operator call, so make sure only to intercept the ones you are
|
||||||
|
#: interested in.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.6
|
||||||
|
intercepted_binops: t.FrozenSet[str] = frozenset()
|
||||||
|
|
||||||
|
#: a set of unary operators that should be intercepted. Each operator
|
||||||
|
#: that is added to this set (empty by default) is delegated to the
|
||||||
|
#: :meth:`call_unop` method that will perform the operator. The default
|
||||||
|
#: operator callback is specified by :attr:`unop_table`.
|
||||||
|
#:
|
||||||
|
#: The following unary operators are interceptable: ``+``, ``-``
|
||||||
|
#:
|
||||||
|
#: The default operation form the operator table corresponds to the
|
||||||
|
#: builtin function. Intercepted calls are always slower than the native
|
||||||
|
#: operator call, so make sure only to intercept the ones you are
|
||||||
|
#: interested in.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.6
|
||||||
|
intercepted_unops: t.FrozenSet[str] = frozenset()
|
||||||
|
|
||||||
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.globals["range"] = safe_range
|
||||||
|
self.binop_table = self.default_binop_table.copy()
|
||||||
|
self.unop_table = self.default_unop_table.copy()
|
||||||
|
|
||||||
|
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
|
||||||
|
"""The sandboxed environment will call this method to check if the
|
||||||
|
attribute of an object is safe to access. Per default all attributes
|
||||||
|
starting with an underscore are considered private as well as the
|
||||||
|
special attributes of internal python objects as returned by the
|
||||||
|
:func:`is_internal_attribute` function.
|
||||||
|
"""
|
||||||
|
return not (attr.startswith("_") or is_internal_attribute(obj, attr))
|
||||||
|
|
||||||
|
def is_safe_callable(self, obj: t.Any) -> bool:
|
||||||
|
"""Check if an object is safely callable. By default callables
|
||||||
|
are considered safe unless decorated with :func:`unsafe`.
|
||||||
|
|
||||||
|
This also recognizes the Django convention of setting
|
||||||
|
``func.alters_data = True``.
|
||||||
|
"""
|
||||||
|
return not (
|
||||||
|
getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
def call_binop(
|
||||||
|
self, context: Context, operator: str, left: t.Any, right: t.Any
|
||||||
|
) -> t.Any:
|
||||||
|
"""For intercepted binary operator calls (:meth:`intercepted_binops`)
|
||||||
|
this function is executed instead of the builtin operator. This can
|
||||||
|
be used to fine tune the behavior of certain operators.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return self.binop_table[operator](left, right)
|
||||||
|
|
||||||
|
def call_unop(self, context: Context, operator: str, arg: t.Any) -> t.Any:
|
||||||
|
"""For intercepted unary operator calls (:meth:`intercepted_unops`)
|
||||||
|
this function is executed instead of the builtin operator. This can
|
||||||
|
be used to fine tune the behavior of certain operators.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return self.unop_table[operator](arg)
|
||||||
|
|
||||||
|
def getitem(
|
||||||
|
self, obj: t.Any, argument: t.Union[str, t.Any]
|
||||||
|
) -> t.Union[t.Any, Undefined]:
|
||||||
|
"""Subscribe an object from sandboxed code."""
|
||||||
|
try:
|
||||||
|
return obj[argument]
|
||||||
|
except (TypeError, LookupError):
|
||||||
|
if isinstance(argument, str):
|
||||||
|
try:
|
||||||
|
attr = str(argument)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = getattr(obj, attr)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if self.is_safe_attribute(obj, argument, value):
|
||||||
|
return value
|
||||||
|
return self.unsafe_undefined(obj, argument)
|
||||||
|
return self.undefined(obj=obj, name=argument)
|
||||||
|
|
||||||
|
def getattr(self, obj: t.Any, attribute: str) -> t.Union[t.Any, Undefined]:
|
||||||
|
"""Subscribe an object from sandboxed code and prefer the
|
||||||
|
attribute. The attribute passed *must* be a bytestring.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
value = getattr(obj, attribute)
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
return obj[attribute]
|
||||||
|
except (TypeError, LookupError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if self.is_safe_attribute(obj, attribute, value):
|
||||||
|
return value
|
||||||
|
return self.unsafe_undefined(obj, attribute)
|
||||||
|
return self.undefined(obj=obj, name=attribute)
|
||||||
|
|
||||||
|
def unsafe_undefined(self, obj: t.Any, attribute: str) -> Undefined:
|
||||||
|
"""Return an undefined object for unsafe attributes."""
|
||||||
|
return self.undefined(
|
||||||
|
f"access to attribute {attribute!r} of"
|
||||||
|
f" {type(obj).__name__!r} object is unsafe.",
|
||||||
|
name=attribute,
|
||||||
|
obj=obj,
|
||||||
|
exc=SecurityError,
|
||||||
|
)
|
||||||
|
|
||||||
|
def format_string(
|
||||||
|
self,
|
||||||
|
s: str,
|
||||||
|
args: t.Tuple[t.Any, ...],
|
||||||
|
kwargs: t.Dict[str, t.Any],
|
||||||
|
format_func: t.Optional[t.Callable] = None,
|
||||||
|
) -> str:
|
||||||
|
"""If a format call is detected, then this is routed through this
|
||||||
|
method so that our safety sandbox can be used for it.
|
||||||
|
"""
|
||||||
|
formatter: SandboxedFormatter
|
||||||
|
if isinstance(s, Markup):
|
||||||
|
formatter = SandboxedEscapeFormatter(self, escape=s.escape)
|
||||||
|
else:
|
||||||
|
formatter = SandboxedFormatter(self)
|
||||||
|
|
||||||
|
if format_func is not None and format_func.__name__ == "format_map":
|
||||||
|
if len(args) != 1 or kwargs:
|
||||||
|
raise TypeError(
|
||||||
|
"format_map() takes exactly one argument"
|
||||||
|
f" {len(args) + (kwargs is not None)} given"
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = args[0]
|
||||||
|
args = ()
|
||||||
|
|
||||||
|
rv = formatter.vformat(s, args, kwargs)
|
||||||
|
return type(s)(rv)
|
||||||
|
|
||||||
|
def call(
|
||||||
|
__self, # noqa: B902
|
||||||
|
__context: Context,
|
||||||
|
__obj: t.Any,
|
||||||
|
*args: t.Any,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Any:
|
||||||
|
"""Call an object from sandboxed code."""
|
||||||
|
fmt = inspect_format_method(__obj)
|
||||||
|
if fmt is not None:
|
||||||
|
return __self.format_string(fmt, args, kwargs, __obj)
|
||||||
|
|
||||||
|
# the double prefixes are to avoid double keyword argument
|
||||||
|
# errors when proxying the call.
|
||||||
|
if not __self.is_safe_callable(__obj):
|
||||||
|
raise SecurityError(f"{__obj!r} is not safely callable")
|
||||||
|
return __context.call(__obj, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ImmutableSandboxedEnvironment(SandboxedEnvironment):
|
||||||
|
"""Works exactly like the regular `SandboxedEnvironment` but does not
|
||||||
|
permit modifications on the builtin mutable objects `list`, `set`, and
|
||||||
|
`dict` by using the :func:`modifies_known_mutable` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
|
||||||
|
if not super().is_safe_attribute(obj, attr, value):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return not modifies_known_mutable(obj, attr)
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedFormatter(Formatter):
|
||||||
|
def __init__(self, env: Environment, **kwargs: t.Any) -> None:
|
||||||
|
self._env = env
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def get_field(
|
||||||
|
self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any]
|
||||||
|
) -> t.Tuple[t.Any, str]:
|
||||||
|
first, rest = formatter_field_name_split(field_name)
|
||||||
|
obj = self.get_value(first, args, kwargs)
|
||||||
|
for is_attr, i in rest:
|
||||||
|
if is_attr:
|
||||||
|
obj = self._env.getattr(obj, i)
|
||||||
|
else:
|
||||||
|
obj = self._env.getitem(obj, i)
|
||||||
|
return obj, first
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedEscapeFormatter(SandboxedFormatter, EscapeFormatter):
|
||||||
|
pass
|
@ -0,0 +1,255 @@
|
|||||||
|
"""Built-in template tests used with the ``is`` operator."""
|
||||||
|
import operator
|
||||||
|
import typing as t
|
||||||
|
from collections import abc
|
||||||
|
from numbers import Number
|
||||||
|
|
||||||
|
from .runtime import Undefined
|
||||||
|
from .utils import pass_environment
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def test_odd(value: int) -> bool:
|
||||||
|
"""Return true if the variable is odd."""
|
||||||
|
return value % 2 == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_even(value: int) -> bool:
|
||||||
|
"""Return true if the variable is even."""
|
||||||
|
return value % 2 == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_divisibleby(value: int, num: int) -> bool:
|
||||||
|
"""Check if a variable is divisible by a number."""
|
||||||
|
return value % num == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_defined(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is defined:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% if variable is defined %}
|
||||||
|
value of variable: {{ variable }}
|
||||||
|
{% else %}
|
||||||
|
variable is not defined
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
See the :func:`default` filter for a simple way to set undefined
|
||||||
|
variables.
|
||||||
|
"""
|
||||||
|
return not isinstance(value, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
def test_undefined(value: t.Any) -> bool:
|
||||||
|
"""Like :func:`defined` but the other way round."""
|
||||||
|
return isinstance(value, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_environment
|
||||||
|
def test_filter(env: "Environment", value: str) -> bool:
|
||||||
|
"""Check if a filter exists by name. Useful if a filter may be
|
||||||
|
optionally available.
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% if 'markdown' is filter %}
|
||||||
|
{{ value | markdown }}
|
||||||
|
{% else %}
|
||||||
|
{{ value }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
return value in env.filters
|
||||||
|
|
||||||
|
|
||||||
|
@pass_environment
|
||||||
|
def test_test(env: "Environment", value: str) -> bool:
|
||||||
|
"""Check if a test exists by name. Useful if a test may be
|
||||||
|
optionally available.
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% if 'loud' is test %}
|
||||||
|
{% if value is loud %}
|
||||||
|
{{ value|upper }}
|
||||||
|
{% else %}
|
||||||
|
{{ value|lower }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{{ value }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
return value in env.tests
|
||||||
|
|
||||||
|
|
||||||
|
def test_none(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is none."""
|
||||||
|
return value is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_boolean(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a boolean value.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is True or value is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_false(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is False.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_true(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is True.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is True
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: The existing 'number' test matches booleans and floats
|
||||||
|
def test_integer(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is an integer.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return isinstance(value, int) and value is not True and value is not False
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: The existing 'number' test matches booleans and integers
|
||||||
|
def test_float(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a float.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return isinstance(value, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower(value: str) -> bool:
|
||||||
|
"""Return true if the variable is lowercased."""
|
||||||
|
return str(value).islower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_upper(value: str) -> bool:
|
||||||
|
"""Return true if the variable is uppercased."""
|
||||||
|
return str(value).isupper()
|
||||||
|
|
||||||
|
|
||||||
|
def test_string(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a string."""
|
||||||
|
return isinstance(value, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mapping(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a mapping (dict etc.).
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return isinstance(value, abc.Mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def test_number(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is a number."""
|
||||||
|
return isinstance(value, Number)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sequence(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is a sequence. Sequences are variables
|
||||||
|
that are iterable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
len(value)
|
||||||
|
value.__getitem__
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_sameas(value: t.Any, other: t.Any) -> bool:
|
||||||
|
"""Check if an object points to the same memory address than another
|
||||||
|
object:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% if foo.attribute is sameas false %}
|
||||||
|
the foo attribute really is the `False` singleton
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
return value is other
|
||||||
|
|
||||||
|
|
||||||
|
def test_iterable(value: t.Any) -> bool:
|
||||||
|
"""Check if it's possible to iterate over an object."""
|
||||||
|
try:
|
||||||
|
iter(value)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_escaped(value: t.Any) -> bool:
|
||||||
|
"""Check if the value is escaped."""
|
||||||
|
return hasattr(value, "__html__")
|
||||||
|
|
||||||
|
|
||||||
|
def test_in(value: t.Any, seq: t.Container) -> bool:
|
||||||
|
"""Check if value is in seq.
|
||||||
|
|
||||||
|
.. versionadded:: 2.10
|
||||||
|
"""
|
||||||
|
return value in seq
|
||||||
|
|
||||||
|
|
||||||
|
TESTS = {
|
||||||
|
"odd": test_odd,
|
||||||
|
"even": test_even,
|
||||||
|
"divisibleby": test_divisibleby,
|
||||||
|
"defined": test_defined,
|
||||||
|
"undefined": test_undefined,
|
||||||
|
"filter": test_filter,
|
||||||
|
"test": test_test,
|
||||||
|
"none": test_none,
|
||||||
|
"boolean": test_boolean,
|
||||||
|
"false": test_false,
|
||||||
|
"true": test_true,
|
||||||
|
"integer": test_integer,
|
||||||
|
"float": test_float,
|
||||||
|
"lower": test_lower,
|
||||||
|
"upper": test_upper,
|
||||||
|
"string": test_string,
|
||||||
|
"mapping": test_mapping,
|
||||||
|
"number": test_number,
|
||||||
|
"sequence": test_sequence,
|
||||||
|
"iterable": test_iterable,
|
||||||
|
"callable": callable,
|
||||||
|
"sameas": test_sameas,
|
||||||
|
"escaped": test_escaped,
|
||||||
|
"in": test_in,
|
||||||
|
"==": operator.eq,
|
||||||
|
"eq": operator.eq,
|
||||||
|
"equalto": operator.eq,
|
||||||
|
"!=": operator.ne,
|
||||||
|
"ne": operator.ne,
|
||||||
|
">": operator.gt,
|
||||||
|
"gt": operator.gt,
|
||||||
|
"greaterthan": operator.gt,
|
||||||
|
"ge": operator.ge,
|
||||||
|
">=": operator.ge,
|
||||||
|
"<": operator.lt,
|
||||||
|
"lt": operator.lt,
|
||||||
|
"lessthan": operator.lt,
|
||||||
|
"<=": operator.le,
|
||||||
|
"le": operator.le,
|
||||||
|
}
|
@ -0,0 +1,755 @@
|
|||||||
|
import enum
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
from collections import abc
|
||||||
|
from collections import deque
|
||||||
|
from random import choice
|
||||||
|
from random import randrange
|
||||||
|
from threading import Lock
|
||||||
|
from types import CodeType
|
||||||
|
from urllib.parse import quote_from_bytes
|
||||||
|
|
||||||
|
import markupsafe
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
# special singleton representing missing values for the runtime
|
||||||
|
missing: t.Any = type("MissingType", (), {"__repr__": lambda x: "missing"})()
|
||||||
|
|
||||||
|
internal_code: t.MutableSet[CodeType] = set()
|
||||||
|
|
||||||
|
concat = "".join
|
||||||
|
|
||||||
|
|
||||||
|
def pass_context(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.runtime.Context` as the first argument
|
||||||
|
to the decorated function when called while rendering a template.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
If only ``Context.eval_context`` is needed, use
|
||||||
|
:func:`pass_eval_context`. If only ``Context.environment`` is
|
||||||
|
needed, use :func:`pass_environment`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``contextfunction`` and ``contextfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.context # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def pass_eval_context(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.nodes.EvalContext` as the first argument
|
||||||
|
to the decorated function when called while rendering a template.
|
||||||
|
See :ref:`eval-context`.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
If only ``EvalContext.environment`` is needed, use
|
||||||
|
:func:`pass_environment`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``evalcontextfunction`` and ``evalcontextfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.eval_context # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def pass_environment(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.Environment` as the first argument to
|
||||||
|
the decorated function when called while rendering a template.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``environmentfunction`` and ``environmentfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.environment # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
class _PassArg(enum.Enum):
|
||||||
|
context = enum.auto()
|
||||||
|
eval_context = enum.auto()
|
||||||
|
environment = enum.auto()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_obj(cls, obj: F) -> t.Optional["_PassArg"]:
|
||||||
|
if hasattr(obj, "jinja_pass_arg"):
|
||||||
|
return obj.jinja_pass_arg # type: ignore
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def internalcode(f: F) -> F:
|
||||||
|
"""Marks the function as internally used"""
|
||||||
|
internal_code.add(f.__code__)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def is_undefined(obj: t.Any) -> bool:
|
||||||
|
"""Check if the object passed is undefined. This does nothing more than
|
||||||
|
performing an instance check against :class:`Undefined` but looks nicer.
|
||||||
|
This can be used for custom filters or tests that want to react to
|
||||||
|
undefined variables. For example a custom default filter can look like
|
||||||
|
this::
|
||||||
|
|
||||||
|
def default(var, default=''):
|
||||||
|
if is_undefined(var):
|
||||||
|
return default
|
||||||
|
return var
|
||||||
|
"""
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
return isinstance(obj, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
def consume(iterable: t.Iterable[t.Any]) -> None:
|
||||||
|
"""Consumes an iterable without doing anything with it."""
|
||||||
|
for _ in iterable:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def clear_caches() -> None:
|
||||||
|
"""Jinja keeps internal caches for environments and lexers. These are
|
||||||
|
used so that Jinja doesn't have to recreate environments and lexers all
|
||||||
|
the time. Normally you don't have to care about that but if you are
|
||||||
|
measuring memory consumption you may want to clean the caches.
|
||||||
|
"""
|
||||||
|
from .environment import get_spontaneous_environment
|
||||||
|
from .lexer import _lexer_cache
|
||||||
|
|
||||||
|
get_spontaneous_environment.cache_clear()
|
||||||
|
_lexer_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def import_string(import_name: str, silent: bool = False) -> t.Any:
|
||||||
|
"""Imports an object based on a string. This is useful if you want to
|
||||||
|
use import paths as endpoints or something similar. An import path can
|
||||||
|
be specified either in dotted notation (``xml.sax.saxutils.escape``)
|
||||||
|
or with a colon as object delimiter (``xml.sax.saxutils:escape``).
|
||||||
|
|
||||||
|
If the `silent` is True the return value will be `None` if the import
|
||||||
|
fails.
|
||||||
|
|
||||||
|
:return: imported object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if ":" in import_name:
|
||||||
|
module, obj = import_name.split(":", 1)
|
||||||
|
elif "." in import_name:
|
||||||
|
module, _, obj = import_name.rpartition(".")
|
||||||
|
else:
|
||||||
|
return __import__(import_name)
|
||||||
|
return getattr(__import__(module, None, None, [obj]), obj)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
if not silent:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def open_if_exists(filename: str, mode: str = "rb") -> t.Optional[t.IO]:
|
||||||
|
"""Returns a file descriptor for the filename if that file exists,
|
||||||
|
otherwise ``None``.
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return open(filename, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def object_type_repr(obj: t.Any) -> str:
|
||||||
|
"""Returns the name of the object's type. For some recognized
|
||||||
|
singletons the name of the object is returned instead. (For
|
||||||
|
example for `None` and `Ellipsis`).
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
return "None"
|
||||||
|
elif obj is Ellipsis:
|
||||||
|
return "Ellipsis"
|
||||||
|
|
||||||
|
cls = type(obj)
|
||||||
|
|
||||||
|
if cls.__module__ == "builtins":
|
||||||
|
return f"{cls.__name__} object"
|
||||||
|
|
||||||
|
return f"{cls.__module__}.{cls.__name__} object"
|
||||||
|
|
||||||
|
|
||||||
|
def pformat(obj: t.Any) -> str:
|
||||||
|
"""Format an object using :func:`pprint.pformat`."""
|
||||||
|
from pprint import pformat # type: ignore
|
||||||
|
|
||||||
|
return pformat(obj)
|
||||||
|
|
||||||
|
|
||||||
|
_http_re = re.compile(
|
||||||
|
r"""
|
||||||
|
^
|
||||||
|
(
|
||||||
|
(https?://|www\.) # scheme or www
|
||||||
|
(([\w%-]+\.)+)? # subdomain
|
||||||
|
(
|
||||||
|
[a-z]{2,63} # basic tld
|
||||||
|
|
|
||||||
|
xn--[\w%]{2,59} # idna tld
|
||||||
|
)
|
||||||
|
|
|
||||||
|
([\w%-]{2,63}\.)+ # basic domain
|
||||||
|
(com|net|int|edu|gov|org|info|mil) # basic tld
|
||||||
|
|
|
||||||
|
(https?://) # scheme
|
||||||
|
(
|
||||||
|
(([\d]{1,3})(\.[\d]{1,3}){3}) # IPv4
|
||||||
|
|
|
||||||
|
(\[([\da-f]{0,4}:){2}([\da-f]{0,4}:?){1,6}]) # IPv6
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(?::[\d]{1,5})? # port
|
||||||
|
(?:[/?#]\S*)? # path, query, and fragment
|
||||||
|
$
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
_email_re = re.compile(r"^\S+@\w[\w.-]*\.\w+$")
|
||||||
|
|
||||||
|
|
||||||
|
def urlize(
|
||||||
|
text: str,
|
||||||
|
trim_url_limit: t.Optional[int] = None,
|
||||||
|
rel: t.Optional[str] = None,
|
||||||
|
target: t.Optional[str] = None,
|
||||||
|
extra_schemes: t.Optional[t.Iterable[str]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Convert URLs in text into clickable links.
|
||||||
|
|
||||||
|
This may not recognize links in some situations. Usually, a more
|
||||||
|
comprehensive formatter, such as a Markdown library, is a better
|
||||||
|
choice.
|
||||||
|
|
||||||
|
Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email
|
||||||
|
addresses. Links with trailing punctuation (periods, commas, closing
|
||||||
|
parentheses) and leading punctuation (opening parentheses) are
|
||||||
|
recognized excluding the punctuation. Email addresses that include
|
||||||
|
header fields are not recognized (for example,
|
||||||
|
``mailto:address@example.com?cc=copy@example.com``).
|
||||||
|
|
||||||
|
:param text: Original text containing URLs to link.
|
||||||
|
:param trim_url_limit: Shorten displayed URL values to this length.
|
||||||
|
:param target: Add the ``target`` attribute to links.
|
||||||
|
:param rel: Add the ``rel`` attribute to links.
|
||||||
|
:param extra_schemes: Recognize URLs that start with these schemes
|
||||||
|
in addition to the default behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The ``extra_schemes`` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Generate ``https://`` links for URLs without a scheme.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The parsing rules were updated. Recognize email addresses with
|
||||||
|
or without the ``mailto:`` scheme. Validate IP addresses. Ignore
|
||||||
|
parentheses and brackets in more cases.
|
||||||
|
"""
|
||||||
|
if trim_url_limit is not None:
|
||||||
|
|
||||||
|
def trim_url(x: str) -> str:
|
||||||
|
if len(x) > trim_url_limit: # type: ignore
|
||||||
|
return f"{x[:trim_url_limit]}..."
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def trim_url(x: str) -> str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
words = re.split(r"(\s+)", str(markupsafe.escape(text)))
|
||||||
|
rel_attr = f' rel="{markupsafe.escape(rel)}"' if rel else ""
|
||||||
|
target_attr = f' target="{markupsafe.escape(target)}"' if target else ""
|
||||||
|
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
head, middle, tail = "", word, ""
|
||||||
|
match = re.match(r"^([(<]|<)+", middle)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
head = match.group()
|
||||||
|
middle = middle[match.end() :]
|
||||||
|
|
||||||
|
# Unlike lead, which is anchored to the start of the string,
|
||||||
|
# need to check that the string ends with any of the characters
|
||||||
|
# before trying to match all of them, to avoid backtracking.
|
||||||
|
if middle.endswith((")", ">", ".", ",", "\n", ">")):
|
||||||
|
match = re.search(r"([)>.,\n]|>)+$", middle)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
tail = match.group()
|
||||||
|
middle = middle[: match.start()]
|
||||||
|
|
||||||
|
# Prefer balancing parentheses in URLs instead of ignoring a
|
||||||
|
# trailing character.
|
||||||
|
for start_char, end_char in ("(", ")"), ("<", ">"), ("<", ">"):
|
||||||
|
start_count = middle.count(start_char)
|
||||||
|
|
||||||
|
if start_count <= middle.count(end_char):
|
||||||
|
# Balanced, or lighter on the left
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Move as many as possible from the tail to balance
|
||||||
|
for _ in range(min(start_count, tail.count(end_char))):
|
||||||
|
end_index = tail.index(end_char) + len(end_char)
|
||||||
|
# Move anything in the tail before the end char too
|
||||||
|
middle += tail[:end_index]
|
||||||
|
tail = tail[end_index:]
|
||||||
|
|
||||||
|
if _http_re.match(middle):
|
||||||
|
if middle.startswith("https://") or middle.startswith("http://"):
|
||||||
|
middle = (
|
||||||
|
f'<a href="{middle}"{rel_attr}{target_attr}>{trim_url(middle)}</a>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
middle = (
|
||||||
|
f'<a href="https://{middle}"{rel_attr}{target_attr}>'
|
||||||
|
f"{trim_url(middle)}</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif middle.startswith("mailto:") and _email_re.match(middle[7:]):
|
||||||
|
middle = f'<a href="{middle}">{middle[7:]}</a>'
|
||||||
|
|
||||||
|
elif (
|
||||||
|
"@" in middle
|
||||||
|
and not middle.startswith("www.")
|
||||||
|
and ":" not in middle
|
||||||
|
and _email_re.match(middle)
|
||||||
|
):
|
||||||
|
middle = f'<a href="mailto:{middle}">{middle}</a>'
|
||||||
|
|
||||||
|
elif extra_schemes is not None:
|
||||||
|
for scheme in extra_schemes:
|
||||||
|
if middle != scheme and middle.startswith(scheme):
|
||||||
|
middle = f'<a href="{middle}"{rel_attr}{target_attr}>{middle}</a>'
|
||||||
|
|
||||||
|
words[i] = f"{head}{middle}{tail}"
|
||||||
|
|
||||||
|
return "".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_lorem_ipsum(
|
||||||
|
n: int = 5, html: bool = True, min: int = 20, max: int = 100
|
||||||
|
) -> str:
|
||||||
|
"""Generate some lorem ipsum for the template."""
|
||||||
|
from .constants import LOREM_IPSUM_WORDS
|
||||||
|
|
||||||
|
words = LOREM_IPSUM_WORDS.split()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for _ in range(n):
|
||||||
|
next_capitalized = True
|
||||||
|
last_comma = last_fullstop = 0
|
||||||
|
word = None
|
||||||
|
last = None
|
||||||
|
p = []
|
||||||
|
|
||||||
|
# each paragraph contains out of 20 to 100 words.
|
||||||
|
for idx, _ in enumerate(range(randrange(min, max))):
|
||||||
|
while True:
|
||||||
|
word = choice(words)
|
||||||
|
if word != last:
|
||||||
|
last = word
|
||||||
|
break
|
||||||
|
if next_capitalized:
|
||||||
|
word = word.capitalize()
|
||||||
|
next_capitalized = False
|
||||||
|
# add commas
|
||||||
|
if idx - randrange(3, 8) > last_comma:
|
||||||
|
last_comma = idx
|
||||||
|
last_fullstop += 2
|
||||||
|
word += ","
|
||||||
|
# add end of sentences
|
||||||
|
if idx - randrange(10, 20) > last_fullstop:
|
||||||
|
last_comma = last_fullstop = idx
|
||||||
|
word += "."
|
||||||
|
next_capitalized = True
|
||||||
|
p.append(word)
|
||||||
|
|
||||||
|
# ensure that the paragraph ends with a dot.
|
||||||
|
p_str = " ".join(p)
|
||||||
|
|
||||||
|
if p_str.endswith(","):
|
||||||
|
p_str = p_str[:-1] + "."
|
||||||
|
elif not p_str.endswith("."):
|
||||||
|
p_str += "."
|
||||||
|
|
||||||
|
result.append(p_str)
|
||||||
|
|
||||||
|
if not html:
|
||||||
|
return "\n\n".join(result)
|
||||||
|
return markupsafe.Markup(
|
||||||
|
"\n".join(f"<p>{markupsafe.escape(x)}</p>" for x in result)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
|
||||||
|
"""Quote a string for use in a URL using the given charset.
|
||||||
|
|
||||||
|
:param obj: String or bytes to quote. Other types are converted to
|
||||||
|
string then encoded to bytes using the given charset.
|
||||||
|
:param charset: Encode text to bytes using this charset.
|
||||||
|
:param for_qs: Quote "/" and use "+" for spaces.
|
||||||
|
"""
|
||||||
|
if not isinstance(obj, bytes):
|
||||||
|
if not isinstance(obj, str):
|
||||||
|
obj = str(obj)
|
||||||
|
|
||||||
|
obj = obj.encode(charset)
|
||||||
|
|
||||||
|
safe = b"" if for_qs else b"/"
|
||||||
|
rv = quote_from_bytes(obj, safe)
|
||||||
|
|
||||||
|
if for_qs:
|
||||||
|
rv = rv.replace("%20", "+")
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
@abc.MutableMapping.register
|
||||||
|
class LRUCache:
|
||||||
|
"""A simple LRU Cache implementation."""
|
||||||
|
|
||||||
|
# this is fast for small capacities (something below 1000) but doesn't
|
||||||
|
# scale. But as long as it's only used as storage for templates this
|
||||||
|
# won't do any harm.
|
||||||
|
|
||||||
|
def __init__(self, capacity: int) -> None:
|
||||||
|
self.capacity = capacity
|
||||||
|
self._mapping: t.Dict[t.Any, t.Any] = {}
|
||||||
|
self._queue: "te.Deque[t.Any]" = deque()
|
||||||
|
self._postinit()
|
||||||
|
|
||||||
|
def _postinit(self) -> None:
|
||||||
|
# alias all queue methods for faster lookup
|
||||||
|
self._popleft = self._queue.popleft
|
||||||
|
self._pop = self._queue.pop
|
||||||
|
self._remove = self._queue.remove
|
||||||
|
self._wlock = Lock()
|
||||||
|
self._append = self._queue.append
|
||||||
|
|
||||||
|
def __getstate__(self) -> t.Mapping[str, t.Any]:
|
||||||
|
return {
|
||||||
|
"capacity": self.capacity,
|
||||||
|
"_mapping": self._mapping,
|
||||||
|
"_queue": self._queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __setstate__(self, d: t.Mapping[str, t.Any]) -> None:
|
||||||
|
self.__dict__.update(d)
|
||||||
|
self._postinit()
|
||||||
|
|
||||||
|
def __getnewargs__(self) -> t.Tuple:
|
||||||
|
return (self.capacity,)
|
||||||
|
|
||||||
|
def copy(self) -> "LRUCache":
|
||||||
|
"""Return a shallow copy of the instance."""
|
||||||
|
rv = self.__class__(self.capacity)
|
||||||
|
rv._mapping.update(self._mapping)
|
||||||
|
rv._queue.extend(self._queue)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def get(self, key: t.Any, default: t.Any = None) -> t.Any:
|
||||||
|
"""Return an item from the cache dict or `default`"""
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def setdefault(self, key: t.Any, default: t.Any = None) -> t.Any:
|
||||||
|
"""Set `default` if the key is not in the cache otherwise
|
||||||
|
leave unchanged. Return the value of this key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
self[key] = default
|
||||||
|
return default
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear the cache."""
|
||||||
|
with self._wlock:
|
||||||
|
self._mapping.clear()
|
||||||
|
self._queue.clear()
|
||||||
|
|
||||||
|
def __contains__(self, key: t.Any) -> bool:
|
||||||
|
"""Check if a key exists in this cache."""
|
||||||
|
return key in self._mapping
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Return the current size of the cache."""
|
||||||
|
return len(self._mapping)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{type(self).__name__} {self._mapping!r}>"
|
||||||
|
|
||||||
|
def __getitem__(self, key: t.Any) -> t.Any:
|
||||||
|
"""Get an item from the cache. Moves the item up so that it has the
|
||||||
|
highest priority then.
|
||||||
|
|
||||||
|
Raise a `KeyError` if it does not exist.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
rv = self._mapping[key]
|
||||||
|
|
||||||
|
if self._queue[-1] != key:
|
||||||
|
try:
|
||||||
|
self._remove(key)
|
||||||
|
except ValueError:
|
||||||
|
# if something removed the key from the container
|
||||||
|
# when we read, ignore the ValueError that we would
|
||||||
|
# get otherwise.
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._append(key)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __setitem__(self, key: t.Any, value: t.Any) -> None:
|
||||||
|
"""Sets the value for an item. Moves the item up so that it
|
||||||
|
has the highest priority then.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
if key in self._mapping:
|
||||||
|
self._remove(key)
|
||||||
|
elif len(self._mapping) == self.capacity:
|
||||||
|
del self._mapping[self._popleft()]
|
||||||
|
|
||||||
|
self._append(key)
|
||||||
|
self._mapping[key] = value
|
||||||
|
|
||||||
|
def __delitem__(self, key: t.Any) -> None:
|
||||||
|
"""Remove an item from the cache dict.
|
||||||
|
Raise a `KeyError` if it does not exist.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
del self._mapping[key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._remove(key)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def items(self) -> t.Iterable[t.Tuple[t.Any, t.Any]]:
|
||||||
|
"""Return a list of items."""
|
||||||
|
result = [(key, self._mapping[key]) for key in list(self._queue)]
|
||||||
|
result.reverse()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def values(self) -> t.Iterable[t.Any]:
|
||||||
|
"""Return a list of all values."""
|
||||||
|
return [x[1] for x in self.items()]
|
||||||
|
|
||||||
|
def keys(self) -> t.Iterable[t.Any]:
|
||||||
|
"""Return a list of all keys ordered by most recent usage."""
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[t.Any]:
|
||||||
|
return reversed(tuple(self._queue))
|
||||||
|
|
||||||
|
def __reversed__(self) -> t.Iterator[t.Any]:
|
||||||
|
"""Iterate over the keys in the cache dict, oldest items
|
||||||
|
coming first.
|
||||||
|
"""
|
||||||
|
return iter(tuple(self._queue))
|
||||||
|
|
||||||
|
__copy__ = copy
|
||||||
|
|
||||||
|
|
||||||
|
def select_autoescape(
|
||||||
|
enabled_extensions: t.Collection[str] = ("html", "htm", "xml"),
|
||||||
|
disabled_extensions: t.Collection[str] = (),
|
||||||
|
default_for_string: bool = True,
|
||||||
|
default: bool = False,
|
||||||
|
) -> t.Callable[[t.Optional[str]], bool]:
|
||||||
|
"""Intelligently sets the initial value of autoescaping based on the
|
||||||
|
filename of the template. This is the recommended way to configure
|
||||||
|
autoescaping if you do not want to write a custom function yourself.
|
||||||
|
|
||||||
|
If you want to enable it for all templates created from strings or
|
||||||
|
for all templates with `.html` and `.xml` extensions::
|
||||||
|
|
||||||
|
from jinja2 import Environment, select_autoescape
|
||||||
|
env = Environment(autoescape=select_autoescape(
|
||||||
|
enabled_extensions=('html', 'xml'),
|
||||||
|
default_for_string=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
Example configuration to turn it on at all times except if the template
|
||||||
|
ends with `.txt`::
|
||||||
|
|
||||||
|
from jinja2 import Environment, select_autoescape
|
||||||
|
env = Environment(autoescape=select_autoescape(
|
||||||
|
disabled_extensions=('txt',),
|
||||||
|
default_for_string=True,
|
||||||
|
default=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
The `enabled_extensions` is an iterable of all the extensions that
|
||||||
|
autoescaping should be enabled for. Likewise `disabled_extensions` is
|
||||||
|
a list of all templates it should be disabled for. If a template is
|
||||||
|
loaded from a string then the default from `default_for_string` is used.
|
||||||
|
If nothing matches then the initial value of autoescaping is set to the
|
||||||
|
value of `default`.
|
||||||
|
|
||||||
|
For security reasons this function operates case insensitive.
|
||||||
|
|
||||||
|
.. versionadded:: 2.9
|
||||||
|
"""
|
||||||
|
enabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in enabled_extensions)
|
||||||
|
disabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in disabled_extensions)
|
||||||
|
|
||||||
|
def autoescape(template_name: t.Optional[str]) -> bool:
|
||||||
|
if template_name is None:
|
||||||
|
return default_for_string
|
||||||
|
template_name = template_name.lower()
|
||||||
|
if template_name.endswith(enabled_patterns):
|
||||||
|
return True
|
||||||
|
if template_name.endswith(disabled_patterns):
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
return autoescape
|
||||||
|
|
||||||
|
|
||||||
|
def htmlsafe_json_dumps(
|
||||||
|
obj: t.Any, dumps: t.Optional[t.Callable[..., str]] = None, **kwargs: t.Any
|
||||||
|
) -> markupsafe.Markup:
|
||||||
|
"""Serialize an object to a string of JSON with :func:`json.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 following characters are escaped: ``<``, ``>``, ``&``, ``'``.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param obj: The object to serialize to JSON.
|
||||||
|
:param dumps: The ``dumps`` function to use. Defaults to
|
||||||
|
``env.policies["json.dumps_function"]``, which defaults to
|
||||||
|
:func:`json.dumps`.
|
||||||
|
:param kwargs: Extra arguments to pass to ``dumps``. Merged onto
|
||||||
|
``env.policies["json.dumps_kwargs"]``.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The ``dumper`` parameter is renamed to ``dumps``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.9
|
||||||
|
"""
|
||||||
|
if dumps is None:
|
||||||
|
dumps = json.dumps
|
||||||
|
|
||||||
|
return markupsafe.Markup(
|
||||||
|
dumps(obj, **kwargs)
|
||||||
|
.replace("<", "\\u003c")
|
||||||
|
.replace(">", "\\u003e")
|
||||||
|
.replace("&", "\\u0026")
|
||||||
|
.replace("'", "\\u0027")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Cycler:
|
||||||
|
"""Cycle through values by yield them one at a time, then restarting
|
||||||
|
once the end is reached. Available as ``cycler`` in templates.
|
||||||
|
|
||||||
|
Similar to ``loop.cycle``, but can be used outside loops or across
|
||||||
|
multiple loops. For example, render a list of folders and files in a
|
||||||
|
list, alternating giving them "odd" and "even" classes.
|
||||||
|
|
||||||
|
.. code-block:: html+jinja
|
||||||
|
|
||||||
|
{% set row_class = cycler("odd", "even") %}
|
||||||
|
<ul class="browser">
|
||||||
|
{% for folder in folders %}
|
||||||
|
<li class="folder {{ row_class.next() }}">{{ folder }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for file in files %}
|
||||||
|
<li class="file {{ row_class.next() }}">{{ file }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
:param items: Each positional argument will be yielded in the order
|
||||||
|
given for each cycle.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *items: t.Any) -> None:
|
||||||
|
if not items:
|
||||||
|
raise RuntimeError("at least one item has to be provided")
|
||||||
|
self.items = items
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Resets the current item to the first item."""
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> t.Any:
|
||||||
|
"""Return the current item. Equivalent to the item that will be
|
||||||
|
returned next time :meth:`next` is called.
|
||||||
|
"""
|
||||||
|
return self.items[self.pos]
|
||||||
|
|
||||||
|
def next(self) -> t.Any:
|
||||||
|
"""Return the current item, then advance :attr:`current` to the
|
||||||
|
next item.
|
||||||
|
"""
|
||||||
|
rv = self.current
|
||||||
|
self.pos = (self.pos + 1) % len(self.items)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
__next__ = next
|
||||||
|
|
||||||
|
|
||||||
|
class Joiner:
|
||||||
|
"""A joining helper for templates."""
|
||||||
|
|
||||||
|
def __init__(self, sep: str = ", ") -> None:
|
||||||
|
self.sep = sep
|
||||||
|
self.used = False
|
||||||
|
|
||||||
|
def __call__(self) -> str:
|
||||||
|
if not self.used:
|
||||||
|
self.used = True
|
||||||
|
return ""
|
||||||
|
return self.sep
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace:
|
||||||
|
"""A namespace object that can hold arbitrary attributes. It may be
|
||||||
|
initialized from a dictionary or with keyword arguments."""
|
||||||
|
|
||||||
|
def __init__(*args: t.Any, **kwargs: t.Any) -> None: # noqa: B902
|
||||||
|
self, args = args[0], args[1:]
|
||||||
|
self.__attrs = dict(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattribute__(self, name: str) -> t.Any:
|
||||||
|
# __class__ is needed for the awaitable check in async mode
|
||||||
|
if name in {"_Namespace__attrs", "__class__"}:
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
try:
|
||||||
|
return self.__attrs[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name) from None
|
||||||
|
|
||||||
|
def __setitem__(self, name: str, value: t.Any) -> None:
|
||||||
|
self.__attrs[name] = value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Namespace {self.__attrs!r}>"
|
@ -0,0 +1,92 @@
|
|||||||
|
"""API for traversing the AST nodes. Implemented by the compiler and
|
||||||
|
meta introspection.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .nodes import Node
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
class VisitCallable(te.Protocol):
|
||||||
|
def __call__(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class NodeVisitor:
|
||||||
|
"""Walks the abstract syntax tree and call visitor functions for every
|
||||||
|
node found. The visitor functions may return values which will be
|
||||||
|
forwarded by the `visit` method.
|
||||||
|
|
||||||
|
Per default the visitor functions for the nodes are ``'visit_'`` +
|
||||||
|
class name of the node. So a `TryFinally` node visit function would
|
||||||
|
be `visit_TryFinally`. This behavior can be changed by overriding
|
||||||
|
the `get_visitor` function. If no visitor function exists for a node
|
||||||
|
(return value `None`) the `generic_visit` visitor is used instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_visitor(self, node: Node) -> "t.Optional[VisitCallable]":
|
||||||
|
"""Return the visitor function for this node or `None` if no visitor
|
||||||
|
exists for this node. In that case the generic visit function is
|
||||||
|
used instead.
|
||||||
|
"""
|
||||||
|
return getattr(self, f"visit_{type(node).__name__}", None)
|
||||||
|
|
||||||
|
def visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Visit a node."""
|
||||||
|
f = self.get_visitor(node)
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
return f(node, *args, **kwargs)
|
||||||
|
|
||||||
|
return self.generic_visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Called if no explicit visitor function exists for a node."""
|
||||||
|
for child_node in node.iter_child_nodes():
|
||||||
|
self.visit(child_node, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeTransformer(NodeVisitor):
|
||||||
|
"""Walks the abstract syntax tree and allows modifications of nodes.
|
||||||
|
|
||||||
|
The `NodeTransformer` will walk the AST and use the return value of the
|
||||||
|
visitor functions to replace or remove the old node. If the return
|
||||||
|
value of the visitor function is `None` the node will be removed
|
||||||
|
from the previous location otherwise it's replaced with the return
|
||||||
|
value. The return value may be the original node in which case no
|
||||||
|
replacement takes place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> Node:
|
||||||
|
for field, old_value in node.iter_fields():
|
||||||
|
if isinstance(old_value, list):
|
||||||
|
new_values = []
|
||||||
|
for value in old_value:
|
||||||
|
if isinstance(value, Node):
|
||||||
|
value = self.visit(value, *args, **kwargs)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
elif not isinstance(value, Node):
|
||||||
|
new_values.extend(value)
|
||||||
|
continue
|
||||||
|
new_values.append(value)
|
||||||
|
old_value[:] = new_values
|
||||||
|
elif isinstance(old_value, Node):
|
||||||
|
new_node = self.visit(old_value, *args, **kwargs)
|
||||||
|
if new_node is None:
|
||||||
|
delattr(node, field)
|
||||||
|
else:
|
||||||
|
setattr(node, field, new_node)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def visit_list(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.List[Node]:
|
||||||
|
"""As transformers may return lists in some places this method
|
||||||
|
can be used to enforce a list as return value.
|
||||||
|
"""
|
||||||
|
rv = self.visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
if not isinstance(rv, list):
|
||||||
|
return [rv]
|
||||||
|
|
||||||
|
return rv
|
@ -0,0 +1,295 @@
|
|||||||
|
import functools
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
class HasHTML(te.Protocol):
|
||||||
|
def __html__(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "2.1.1"
|
||||||
|
|
||||||
|
_strip_comments_re = re.compile(r"<!--.*?-->")
|
||||||
|
_strip_tags_re = re.compile(r"<.*?>")
|
||||||
|
|
||||||
|
|
||||||
|
def _simple_escaping_wrapper(name: str) -> t.Callable[..., "Markup"]:
|
||||||
|
orig = getattr(str, name)
|
||||||
|
|
||||||
|
@functools.wraps(orig)
|
||||||
|
def wrapped(self: "Markup", *args: t.Any, **kwargs: t.Any) -> "Markup":
|
||||||
|
args = _escape_argspec(list(args), enumerate(args), self.escape) # type: ignore
|
||||||
|
_escape_argspec(kwargs, kwargs.items(), self.escape)
|
||||||
|
return self.__class__(orig(self, *args, **kwargs))
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
class Markup(str):
|
||||||
|
"""A string that is ready to be safely inserted into an HTML or XML
|
||||||
|
document, either because it was escaped or because it was marked
|
||||||
|
safe.
|
||||||
|
|
||||||
|
Passing an object to the constructor converts it to text and wraps
|
||||||
|
it to mark it safe without escaping. To escape the text, use the
|
||||||
|
:meth:`escape` class method instead.
|
||||||
|
|
||||||
|
>>> Markup("Hello, <em>World</em>!")
|
||||||
|
Markup('Hello, <em>World</em>!')
|
||||||
|
>>> Markup(42)
|
||||||
|
Markup('42')
|
||||||
|
>>> Markup.escape("Hello, <em>World</em>!")
|
||||||
|
Markup('Hello <em>World</em>!')
|
||||||
|
|
||||||
|
This implements the ``__html__()`` interface that some frameworks
|
||||||
|
use. Passing an object that implements ``__html__()`` will wrap the
|
||||||
|
output of that method, marking it safe.
|
||||||
|
|
||||||
|
>>> class Foo:
|
||||||
|
... def __html__(self):
|
||||||
|
... return '<a href="/foo">foo</a>'
|
||||||
|
...
|
||||||
|
>>> Markup(Foo())
|
||||||
|
Markup('<a href="/foo">foo</a>')
|
||||||
|
|
||||||
|
This is a subclass of :class:`str`. It has the same methods, but
|
||||||
|
escapes their arguments and returns a ``Markup`` instance.
|
||||||
|
|
||||||
|
>>> Markup("<em>%s</em>") % ("foo & bar",)
|
||||||
|
Markup('<em>foo & bar</em>')
|
||||||
|
>>> Markup("<em>Hello</em> ") + "<foo>"
|
||||||
|
Markup('<em>Hello</em> <foo>')
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
cls, base: t.Any = "", encoding: t.Optional[str] = None, errors: str = "strict"
|
||||||
|
) -> "Markup":
|
||||||
|
if hasattr(base, "__html__"):
|
||||||
|
base = base.__html__()
|
||||||
|
|
||||||
|
if encoding is None:
|
||||||
|
return super().__new__(cls, base)
|
||||||
|
|
||||||
|
return super().__new__(cls, base, encoding, errors)
|
||||||
|
|
||||||
|
def __html__(self) -> "Markup":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __add__(self, other: t.Union[str, "HasHTML"]) -> "Markup":
|
||||||
|
if isinstance(other, str) or hasattr(other, "__html__"):
|
||||||
|
return self.__class__(super().__add__(self.escape(other)))
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __radd__(self, other: t.Union[str, "HasHTML"]) -> "Markup":
|
||||||
|
if isinstance(other, str) or hasattr(other, "__html__"):
|
||||||
|
return self.escape(other).__add__(self)
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __mul__(self, num: "te.SupportsIndex") -> "Markup":
|
||||||
|
if isinstance(num, int):
|
||||||
|
return self.__class__(super().__mul__(num))
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
__rmul__ = __mul__
|
||||||
|
|
||||||
|
def __mod__(self, arg: t.Any) -> "Markup":
|
||||||
|
if isinstance(arg, tuple):
|
||||||
|
# a tuple of arguments, each wrapped
|
||||||
|
arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
|
||||||
|
elif hasattr(type(arg), "__getitem__") and not isinstance(arg, str):
|
||||||
|
# a mapping of arguments, wrapped
|
||||||
|
arg = _MarkupEscapeHelper(arg, self.escape)
|
||||||
|
else:
|
||||||
|
# a single argument, wrapped with the helper and a tuple
|
||||||
|
arg = (_MarkupEscapeHelper(arg, self.escape),)
|
||||||
|
|
||||||
|
return self.__class__(super().__mod__(arg))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({super().__repr__()})"
|
||||||
|
|
||||||
|
def join(self, seq: t.Iterable[t.Union[str, "HasHTML"]]) -> "Markup":
|
||||||
|
return self.__class__(super().join(map(self.escape, seq)))
|
||||||
|
|
||||||
|
join.__doc__ = str.join.__doc__
|
||||||
|
|
||||||
|
def split( # type: ignore
|
||||||
|
self, sep: t.Optional[str] = None, maxsplit: int = -1
|
||||||
|
) -> t.List["Markup"]:
|
||||||
|
return [self.__class__(v) for v in super().split(sep, maxsplit)]
|
||||||
|
|
||||||
|
split.__doc__ = str.split.__doc__
|
||||||
|
|
||||||
|
def rsplit( # type: ignore
|
||||||
|
self, sep: t.Optional[str] = None, maxsplit: int = -1
|
||||||
|
) -> t.List["Markup"]:
|
||||||
|
return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]
|
||||||
|
|
||||||
|
rsplit.__doc__ = str.rsplit.__doc__
|
||||||
|
|
||||||
|
def splitlines(self, keepends: bool = False) -> t.List["Markup"]: # type: ignore
|
||||||
|
return [self.__class__(v) for v in super().splitlines(keepends)]
|
||||||
|
|
||||||
|
splitlines.__doc__ = str.splitlines.__doc__
|
||||||
|
|
||||||
|
def unescape(self) -> str:
|
||||||
|
"""Convert escaped markup back into a text string. This replaces
|
||||||
|
HTML entities with the characters they represent.
|
||||||
|
|
||||||
|
>>> Markup("Main » <em>About</em>").unescape()
|
||||||
|
'Main » <em>About</em>'
|
||||||
|
"""
|
||||||
|
from html import unescape
|
||||||
|
|
||||||
|
return unescape(str(self))
|
||||||
|
|
||||||
|
def striptags(self) -> str:
|
||||||
|
""":meth:`unescape` the markup, remove tags, and normalize
|
||||||
|
whitespace to single spaces.
|
||||||
|
|
||||||
|
>>> Markup("Main »\t<em>About</em>").striptags()
|
||||||
|
'Main » About'
|
||||||
|
"""
|
||||||
|
# Use two regexes to avoid ambiguous matches.
|
||||||
|
value = _strip_comments_re.sub("", self)
|
||||||
|
value = _strip_tags_re.sub("", value)
|
||||||
|
value = " ".join(value.split())
|
||||||
|
return Markup(value).unescape()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def escape(cls, s: t.Any) -> "Markup":
|
||||||
|
"""Escape a string. Calls :func:`escape` and ensures that for
|
||||||
|
subclasses the correct type is returned.
|
||||||
|
"""
|
||||||
|
rv = escape(s)
|
||||||
|
|
||||||
|
if rv.__class__ is not cls:
|
||||||
|
return cls(rv)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
for method in (
|
||||||
|
"__getitem__",
|
||||||
|
"capitalize",
|
||||||
|
"title",
|
||||||
|
"lower",
|
||||||
|
"upper",
|
||||||
|
"replace",
|
||||||
|
"ljust",
|
||||||
|
"rjust",
|
||||||
|
"lstrip",
|
||||||
|
"rstrip",
|
||||||
|
"center",
|
||||||
|
"strip",
|
||||||
|
"translate",
|
||||||
|
"expandtabs",
|
||||||
|
"swapcase",
|
||||||
|
"zfill",
|
||||||
|
):
|
||||||
|
locals()[method] = _simple_escaping_wrapper(method)
|
||||||
|
|
||||||
|
del method
|
||||||
|
|
||||||
|
def partition(self, sep: str) -> t.Tuple["Markup", "Markup", "Markup"]:
|
||||||
|
l, s, r = super().partition(self.escape(sep))
|
||||||
|
cls = self.__class__
|
||||||
|
return cls(l), cls(s), cls(r)
|
||||||
|
|
||||||
|
def rpartition(self, sep: str) -> t.Tuple["Markup", "Markup", "Markup"]:
|
||||||
|
l, s, r = super().rpartition(self.escape(sep))
|
||||||
|
cls = self.__class__
|
||||||
|
return cls(l), cls(s), cls(r)
|
||||||
|
|
||||||
|
def format(self, *args: t.Any, **kwargs: t.Any) -> "Markup":
|
||||||
|
formatter = EscapeFormatter(self.escape)
|
||||||
|
return self.__class__(formatter.vformat(self, args, kwargs))
|
||||||
|
|
||||||
|
def __html_format__(self, format_spec: str) -> "Markup":
|
||||||
|
if format_spec:
|
||||||
|
raise ValueError("Unsupported format specification for Markup.")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class EscapeFormatter(string.Formatter):
|
||||||
|
__slots__ = ("escape",)
|
||||||
|
|
||||||
|
def __init__(self, escape: t.Callable[[t.Any], Markup]) -> None:
|
||||||
|
self.escape = escape
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def format_field(self, value: t.Any, format_spec: str) -> str:
|
||||||
|
if hasattr(value, "__html_format__"):
|
||||||
|
rv = value.__html_format__(format_spec)
|
||||||
|
elif hasattr(value, "__html__"):
|
||||||
|
if format_spec:
|
||||||
|
raise ValueError(
|
||||||
|
f"Format specifier {format_spec} given, but {type(value)} does not"
|
||||||
|
" define __html_format__. A class that defines __html__ must define"
|
||||||
|
" __html_format__ to work with format specifiers."
|
||||||
|
)
|
||||||
|
rv = value.__html__()
|
||||||
|
else:
|
||||||
|
# We need to make sure the format spec is str here as
|
||||||
|
# otherwise the wrong callback methods are invoked.
|
||||||
|
rv = string.Formatter.format_field(self, value, str(format_spec))
|
||||||
|
return str(self.escape(rv))
|
||||||
|
|
||||||
|
|
||||||
|
_ListOrDict = t.TypeVar("_ListOrDict", list, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_argspec(
|
||||||
|
obj: _ListOrDict, iterable: t.Iterable[t.Any], escape: t.Callable[[t.Any], Markup]
|
||||||
|
) -> _ListOrDict:
|
||||||
|
"""Helper for various string-wrapped functions."""
|
||||||
|
for key, value in iterable:
|
||||||
|
if isinstance(value, str) or hasattr(value, "__html__"):
|
||||||
|
obj[key] = escape(value)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class _MarkupEscapeHelper:
|
||||||
|
"""Helper for :meth:`Markup.__mod__`."""
|
||||||
|
|
||||||
|
__slots__ = ("obj", "escape")
|
||||||
|
|
||||||
|
def __init__(self, obj: t.Any, escape: t.Callable[[t.Any], Markup]) -> None:
|
||||||
|
self.obj = obj
|
||||||
|
self.escape = escape
|
||||||
|
|
||||||
|
def __getitem__(self, item: t.Any) -> "_MarkupEscapeHelper":
|
||||||
|
return _MarkupEscapeHelper(self.obj[item], self.escape)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.escape(self.obj))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return str(self.escape(repr(self.obj)))
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return int(self.obj)
|
||||||
|
|
||||||
|
def __float__(self) -> float:
|
||||||
|
return float(self.obj)
|
||||||
|
|
||||||
|
|
||||||
|
# circular import
|
||||||
|
try:
|
||||||
|
from ._speedups import escape as escape
|
||||||
|
from ._speedups import escape_silent as escape_silent
|
||||||
|
from ._speedups import soft_str as soft_str
|
||||||
|
except ImportError:
|
||||||
|
from ._native import escape as escape
|
||||||
|
from ._native import escape_silent as escape_silent # noqa: F401
|
||||||
|
from ._native import soft_str as soft_str # noqa: F401
|
@ -0,0 +1,63 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import Markup
|
||||||
|
|
||||||
|
|
||||||
|
def escape(s: t.Any) -> Markup:
|
||||||
|
"""Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in
|
||||||
|
the string with HTML-safe sequences. Use this if you need to display
|
||||||
|
text that might contain such characters in HTML.
|
||||||
|
|
||||||
|
If the object has an ``__html__`` method, it is called and the
|
||||||
|
return value is assumed to already be safe for HTML.
|
||||||
|
|
||||||
|
:param s: An object to be converted to a string and escaped.
|
||||||
|
:return: A :class:`Markup` string with the escaped text.
|
||||||
|
"""
|
||||||
|
if hasattr(s, "__html__"):
|
||||||
|
return Markup(s.__html__())
|
||||||
|
|
||||||
|
return Markup(
|
||||||
|
str(s)
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace("'", "'")
|
||||||
|
.replace('"', """)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def escape_silent(s: t.Optional[t.Any]) -> Markup:
|
||||||
|
"""Like :func:`escape` but treats ``None`` as the empty string.
|
||||||
|
Useful with optional values, as otherwise you get the string
|
||||||
|
``'None'`` when the value is ``None``.
|
||||||
|
|
||||||
|
>>> escape(None)
|
||||||
|
Markup('None')
|
||||||
|
>>> escape_silent(None)
|
||||||
|
Markup('')
|
||||||
|
"""
|
||||||
|
if s is None:
|
||||||
|
return Markup()
|
||||||
|
|
||||||
|
return escape(s)
|
||||||
|
|
||||||
|
|
||||||
|
def soft_str(s: t.Any) -> str:
|
||||||
|
"""Convert an object to a string if it isn't already. This preserves
|
||||||
|
a :class:`Markup` string rather than converting it back to a basic
|
||||||
|
string, so it will still be marked as safe and won't be escaped
|
||||||
|
again.
|
||||||
|
|
||||||
|
>>> value = escape("<User 1>")
|
||||||
|
>>> value
|
||||||
|
Markup('<User 1>')
|
||||||
|
>>> escape(str(value))
|
||||||
|
Markup('&lt;User 1&gt;')
|
||||||
|
>>> escape(soft_str(value))
|
||||||
|
Markup('<User 1>')
|
||||||
|
"""
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return str(s)
|
||||||
|
|
||||||
|
return s
|
@ -0,0 +1,320 @@
|
|||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
static PyObject* markup;
|
||||||
|
|
||||||
|
static int
|
||||||
|
init_constants(void)
|
||||||
|
{
|
||||||
|
PyObject *module;
|
||||||
|
|
||||||
|
/* import markup type so that we can mark the return value */
|
||||||
|
module = PyImport_ImportModule("markupsafe");
|
||||||
|
if (!module)
|
||||||
|
return 0;
|
||||||
|
markup = PyObject_GetAttrString(module, "Markup");
|
||||||
|
Py_DECREF(module);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define GET_DELTA(inp, inp_end, delta) \
|
||||||
|
while (inp < inp_end) { \
|
||||||
|
switch (*inp++) { \
|
||||||
|
case '"': \
|
||||||
|
case '\'': \
|
||||||
|
case '&': \
|
||||||
|
delta += 4; \
|
||||||
|
break; \
|
||||||
|
case '<': \
|
||||||
|
case '>': \
|
||||||
|
delta += 3; \
|
||||||
|
break; \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DO_ESCAPE(inp, inp_end, outp) \
|
||||||
|
{ \
|
||||||
|
Py_ssize_t ncopy = 0; \
|
||||||
|
while (inp < inp_end) { \
|
||||||
|
switch (*inp) { \
|
||||||
|
case '"': \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
outp += ncopy; ncopy = 0; \
|
||||||
|
*outp++ = '&'; \
|
||||||
|
*outp++ = '#'; \
|
||||||
|
*outp++ = '3'; \
|
||||||
|
*outp++ = '4'; \
|
||||||
|
*outp++ = ';'; \
|
||||||
|
break; \
|
||||||
|
case '\'': \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
outp += ncopy; ncopy = 0; \
|
||||||
|
*outp++ = '&'; \
|
||||||
|
*outp++ = '#'; \
|
||||||
|
*outp++ = '3'; \
|
||||||
|
*outp++ = '9'; \
|
||||||
|
*outp++ = ';'; \
|
||||||
|
break; \
|
||||||
|
case '&': \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
outp += ncopy; ncopy = 0; \
|
||||||
|
*outp++ = '&'; \
|
||||||
|
*outp++ = 'a'; \
|
||||||
|
*outp++ = 'm'; \
|
||||||
|
*outp++ = 'p'; \
|
||||||
|
*outp++ = ';'; \
|
||||||
|
break; \
|
||||||
|
case '<': \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
outp += ncopy; ncopy = 0; \
|
||||||
|
*outp++ = '&'; \
|
||||||
|
*outp++ = 'l'; \
|
||||||
|
*outp++ = 't'; \
|
||||||
|
*outp++ = ';'; \
|
||||||
|
break; \
|
||||||
|
case '>': \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
outp += ncopy; ncopy = 0; \
|
||||||
|
*outp++ = '&'; \
|
||||||
|
*outp++ = 'g'; \
|
||||||
|
*outp++ = 't'; \
|
||||||
|
*outp++ = ';'; \
|
||||||
|
break; \
|
||||||
|
default: \
|
||||||
|
ncopy++; \
|
||||||
|
} \
|
||||||
|
inp++; \
|
||||||
|
} \
|
||||||
|
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape_unicode_kind1(PyUnicodeObject *in)
|
||||||
|
{
|
||||||
|
Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
|
||||||
|
Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
|
||||||
|
Py_UCS1 *outp;
|
||||||
|
PyObject *out;
|
||||||
|
Py_ssize_t delta = 0;
|
||||||
|
|
||||||
|
GET_DELTA(inp, inp_end, delta);
|
||||||
|
if (!delta) {
|
||||||
|
Py_INCREF(in);
|
||||||
|
return (PyObject*)in;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
|
||||||
|
PyUnicode_IS_ASCII(in) ? 127 : 255);
|
||||||
|
if (!out)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
inp = PyUnicode_1BYTE_DATA(in);
|
||||||
|
outp = PyUnicode_1BYTE_DATA(out);
|
||||||
|
DO_ESCAPE(inp, inp_end, outp);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape_unicode_kind2(PyUnicodeObject *in)
|
||||||
|
{
|
||||||
|
Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
|
||||||
|
Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
|
||||||
|
Py_UCS2 *outp;
|
||||||
|
PyObject *out;
|
||||||
|
Py_ssize_t delta = 0;
|
||||||
|
|
||||||
|
GET_DELTA(inp, inp_end, delta);
|
||||||
|
if (!delta) {
|
||||||
|
Py_INCREF(in);
|
||||||
|
return (PyObject*)in;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
|
||||||
|
if (!out)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
inp = PyUnicode_2BYTE_DATA(in);
|
||||||
|
outp = PyUnicode_2BYTE_DATA(out);
|
||||||
|
DO_ESCAPE(inp, inp_end, outp);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape_unicode_kind4(PyUnicodeObject *in)
|
||||||
|
{
|
||||||
|
Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
|
||||||
|
Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
|
||||||
|
Py_UCS4 *outp;
|
||||||
|
PyObject *out;
|
||||||
|
Py_ssize_t delta = 0;
|
||||||
|
|
||||||
|
GET_DELTA(inp, inp_end, delta);
|
||||||
|
if (!delta) {
|
||||||
|
Py_INCREF(in);
|
||||||
|
return (PyObject*)in;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
|
||||||
|
if (!out)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
inp = PyUnicode_4BYTE_DATA(in);
|
||||||
|
outp = PyUnicode_4BYTE_DATA(out);
|
||||||
|
DO_ESCAPE(inp, inp_end, outp);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape_unicode(PyUnicodeObject *in)
|
||||||
|
{
|
||||||
|
if (PyUnicode_READY(in))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
switch (PyUnicode_KIND(in)) {
|
||||||
|
case PyUnicode_1BYTE_KIND:
|
||||||
|
return escape_unicode_kind1(in);
|
||||||
|
case PyUnicode_2BYTE_KIND:
|
||||||
|
return escape_unicode_kind2(in);
|
||||||
|
case PyUnicode_4BYTE_KIND:
|
||||||
|
return escape_unicode_kind4(in);
|
||||||
|
}
|
||||||
|
assert(0); /* shouldn't happen */
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape(PyObject *self, PyObject *text)
|
||||||
|
{
|
||||||
|
static PyObject *id_html;
|
||||||
|
PyObject *s = NULL, *rv = NULL, *html;
|
||||||
|
|
||||||
|
if (id_html == NULL) {
|
||||||
|
id_html = PyUnicode_InternFromString("__html__");
|
||||||
|
if (id_html == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* we don't have to escape integers, bools or floats */
|
||||||
|
if (PyLong_CheckExact(text) ||
|
||||||
|
PyFloat_CheckExact(text) || PyBool_Check(text) ||
|
||||||
|
text == Py_None)
|
||||||
|
return PyObject_CallFunctionObjArgs(markup, text, NULL);
|
||||||
|
|
||||||
|
/* if the object has an __html__ method that performs the escaping */
|
||||||
|
html = PyObject_GetAttr(text ,id_html);
|
||||||
|
if (html) {
|
||||||
|
s = PyObject_CallObject(html, NULL);
|
||||||
|
Py_DECREF(html);
|
||||||
|
if (s == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* Convert to Markup object */
|
||||||
|
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
|
||||||
|
Py_DECREF(s);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* otherwise make the object unicode if it isn't, then escape */
|
||||||
|
PyErr_Clear();
|
||||||
|
if (!PyUnicode_Check(text)) {
|
||||||
|
PyObject *unicode = PyObject_Str(text);
|
||||||
|
if (!unicode)
|
||||||
|
return NULL;
|
||||||
|
s = escape_unicode((PyUnicodeObject*)unicode);
|
||||||
|
Py_DECREF(unicode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
s = escape_unicode((PyUnicodeObject*)text);
|
||||||
|
|
||||||
|
/* convert the unicode string into a markup object. */
|
||||||
|
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
|
||||||
|
Py_DECREF(s);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
escape_silent(PyObject *self, PyObject *text)
|
||||||
|
{
|
||||||
|
if (text != Py_None)
|
||||||
|
return escape(self, text);
|
||||||
|
return PyObject_CallFunctionObjArgs(markup, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
soft_str(PyObject *self, PyObject *s)
|
||||||
|
{
|
||||||
|
if (!PyUnicode_Check(s))
|
||||||
|
return PyObject_Str(s);
|
||||||
|
Py_INCREF(s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyMethodDef module_methods[] = {
|
||||||
|
{
|
||||||
|
"escape",
|
||||||
|
(PyCFunction)escape,
|
||||||
|
METH_O,
|
||||||
|
"Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\"`` in"
|
||||||
|
" the string with HTML-safe sequences. Use this if you need to display"
|
||||||
|
" text that might contain such characters in HTML.\n\n"
|
||||||
|
"If the object has an ``__html__`` method, it is called and the"
|
||||||
|
" return value is assumed to already be safe for HTML.\n\n"
|
||||||
|
":param s: An object to be converted to a string and escaped.\n"
|
||||||
|
":return: A :class:`Markup` string with the escaped text.\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"escape_silent",
|
||||||
|
(PyCFunction)escape_silent,
|
||||||
|
METH_O,
|
||||||
|
"Like :func:`escape` but treats ``None`` as the empty string."
|
||||||
|
" Useful with optional values, as otherwise you get the string"
|
||||||
|
" ``'None'`` when the value is ``None``.\n\n"
|
||||||
|
">>> escape(None)\n"
|
||||||
|
"Markup('None')\n"
|
||||||
|
">>> escape_silent(None)\n"
|
||||||
|
"Markup('')\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"soft_str",
|
||||||
|
(PyCFunction)soft_str,
|
||||||
|
METH_O,
|
||||||
|
"Convert an object to a string if it isn't already. This preserves"
|
||||||
|
" a :class:`Markup` string rather than converting it back to a basic"
|
||||||
|
" string, so it will still be marked as safe and won't be escaped"
|
||||||
|
" again.\n\n"
|
||||||
|
">>> value = escape(\"<User 1>\")\n"
|
||||||
|
">>> value\n"
|
||||||
|
"Markup('<User 1>')\n"
|
||||||
|
">>> escape(str(value))\n"
|
||||||
|
"Markup('&lt;User 1&gt;')\n"
|
||||||
|
">>> escape(soft_str(value))\n"
|
||||||
|
"Markup('<User 1>')\n"
|
||||||
|
},
|
||||||
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct PyModuleDef module_definition = {
|
||||||
|
PyModuleDef_HEAD_INIT,
|
||||||
|
"markupsafe._speedups",
|
||||||
|
NULL,
|
||||||
|
-1,
|
||||||
|
module_methods,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
PyMODINIT_FUNC
|
||||||
|
PyInit__speedups(void)
|
||||||
|
{
|
||||||
|
if (!init_constants())
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
return PyModule_Create(&module_definition);
|
||||||
|
}
|
Binary file not shown.
@ -0,0 +1,9 @@
|
|||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from . import Markup
|
||||||
|
|
||||||
|
def escape(s: Any) -> Markup: ...
|
||||||
|
def escape_silent(s: Optional[Any]) -> Markup: ...
|
||||||
|
def soft_str(s: Any) -> str: ...
|
||||||
|
def soft_unicode(s: Any) -> str: ...
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola'
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the psutil authors 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 OWNER 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,526 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: psutil
|
||||||
|
Version: 5.9.1
|
||||||
|
Summary: Cross-platform lib for process and system monitoring in Python.
|
||||||
|
Home-page: https://github.com/giampaolo/psutil
|
||||||
|
Author: Giampaolo Rodola
|
||||||
|
Author-email: g.rodola@gmail.com
|
||||||
|
License: BSD
|
||||||
|
Keywords: ps,top,kill,free,lsof,netstat,nice,tty,ionice,uptime,taskmgr,process,df,iotop,iostat,ifconfig,taskset,who,pidof,pmap,smem,pstree,monitoring,ulimit,prlimit,smem,performance,metrics,agent,observability
|
||||||
|
Platform: Platform Independent
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Console
|
||||||
|
Classifier: Environment :: Win32 (MS Windows)
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: Information Technology
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 7
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 8
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 8.1
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows Server 2003
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows Server 2008
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows Vista
|
||||||
|
Classifier: Operating System :: Microsoft
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Operating System :: POSIX :: AIX
|
||||||
|
Classifier: Operating System :: POSIX :: BSD :: FreeBSD
|
||||||
|
Classifier: Operating System :: POSIX :: BSD :: NetBSD
|
||||||
|
Classifier: Operating System :: POSIX :: BSD :: OpenBSD
|
||||||
|
Classifier: Operating System :: POSIX :: BSD
|
||||||
|
Classifier: Operating System :: POSIX :: Linux
|
||||||
|
Classifier: Operating System :: POSIX :: SunOS/Solaris
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: Programming Language :: C
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
||||||
|
Classifier: Topic :: System :: Benchmark
|
||||||
|
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
||||||
|
Classifier: Topic :: System :: Hardware
|
||||||
|
Classifier: Topic :: System :: Monitoring
|
||||||
|
Classifier: Topic :: System :: Networking :: Monitoring :: Hardware Watchdog
|
||||||
|
Classifier: Topic :: System :: Networking :: Monitoring
|
||||||
|
Classifier: Topic :: System :: Networking
|
||||||
|
Classifier: Topic :: System :: Operating System
|
||||||
|
Classifier: Topic :: System :: Systems Administration
|
||||||
|
Classifier: Topic :: Utilities
|
||||||
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Provides-Extra: test
|
||||||
|
Requires-Dist: ipaddress ; (python_version < "3.0") and extra == 'test'
|
||||||
|
Requires-Dist: mock ; (python_version < "3.0") and extra == 'test'
|
||||||
|
Requires-Dist: enum34 ; (python_version <= "3.4") and extra == 'test'
|
||||||
|
Requires-Dist: pywin32 ; (sys_platform == "win32") and extra == 'test'
|
||||||
|
Requires-Dist: wmi ; (sys_platform == "win32") and extra == 'test'
|
||||||
|
|
||||||
|
| |downloads| |stars| |forks| |contributors| |coverage|
|
||||||
|
| |version| |py-versions| |packages| |license|
|
||||||
|
| |github-actions| |appveyor| |doc| |twitter| |tidelift|
|
||||||
|
|
||||||
|
.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg
|
||||||
|
:target: https://pepy.tech/project/psutil
|
||||||
|
:alt: Downloads
|
||||||
|
|
||||||
|
.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg
|
||||||
|
:target: https://github.com/giampaolo/psutil/stargazers
|
||||||
|
:alt: Github stars
|
||||||
|
|
||||||
|
.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg
|
||||||
|
:target: https://github.com/giampaolo/psutil/network/members
|
||||||
|
:alt: Github forks
|
||||||
|
|
||||||
|
.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg
|
||||||
|
:target: https://github.com/giampaolo/psutil/graphs/contributors
|
||||||
|
:alt: Contributors
|
||||||
|
|
||||||
|
.. |github-actions| image:: https://img.shields.io/github/workflow/status/giampaolo/psutil/CI?label=Linux%2C%20macOS%2C%20FreeBSD
|
||||||
|
:target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild
|
||||||
|
:alt: Linux, macOS, Windows tests
|
||||||
|
|
||||||
|
.. |appveyor| image:: https://img.shields.io/appveyor/ci/giampaolo/psutil/master.svg?maxAge=3600&label=Windows
|
||||||
|
:target: https://ci.appveyor.com/project/giampaolo/psutil
|
||||||
|
:alt: Windows tests (Appveyor)
|
||||||
|
|
||||||
|
.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master
|
||||||
|
:target: https://coveralls.io/github/giampaolo/psutil?branch=master
|
||||||
|
:alt: Test coverage (coverall.io)
|
||||||
|
|
||||||
|
.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest
|
||||||
|
:target: https://psutil.readthedocs.io/en/latest/
|
||||||
|
:alt: Documentation Status
|
||||||
|
|
||||||
|
.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi
|
||||||
|
:target: https://pypi.org/project/psutil
|
||||||
|
:alt: Latest version
|
||||||
|
|
||||||
|
.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg
|
||||||
|
:alt: Supported Python versions
|
||||||
|
|
||||||
|
.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg
|
||||||
|
:target: https://repology.org/metapackage/python:psutil/versions
|
||||||
|
:alt: Binary packages
|
||||||
|
|
||||||
|
.. |license| image:: https://img.shields.io/pypi/l/psutil.svg
|
||||||
|
:target: https://github.com/giampaolo/psutil/blob/master/LICENSE
|
||||||
|
:alt: License
|
||||||
|
|
||||||
|
.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF
|
||||||
|
:target: https://twitter.com/grodola
|
||||||
|
:alt: Twitter Follow
|
||||||
|
|
||||||
|
.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat
|
||||||
|
:target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme
|
||||||
|
:alt: Tidelift
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Quick links
|
||||||
|
===========
|
||||||
|
|
||||||
|
- `Home page <https://github.com/giampaolo/psutil>`_
|
||||||
|
- `Install <https://github.com/giampaolo/psutil/blob/master/INSTALL.rst>`_
|
||||||
|
- `Documentation <http://psutil.readthedocs.io>`_
|
||||||
|
- `Download <https://pypi.org/project/psutil/#files>`_
|
||||||
|
- `Forum <http://groups.google.com/group/psutil/topics>`_
|
||||||
|
- `StackOverflow <https://stackoverflow.com/questions/tagged/psutil>`_
|
||||||
|
- `Blog <https://gmpy.dev/tags/psutil>`_
|
||||||
|
- `What's new <https://github.com/giampaolo/psutil/blob/master/HISTORY.rst>`_
|
||||||
|
|
||||||
|
|
||||||
|
Summary
|
||||||
|
=======
|
||||||
|
|
||||||
|
psutil (process and system utilities) is a cross-platform library for
|
||||||
|
retrieving information on **running processes** and **system utilization**
|
||||||
|
(CPU, memory, disks, network, sensors) in Python.
|
||||||
|
It is useful mainly for **system monitoring**, **profiling and limiting process
|
||||||
|
resources** and **management of running processes**.
|
||||||
|
It implements many functionalities offered by classic UNIX command line tools
|
||||||
|
such as *ps, top, iotop, lsof, netstat, ifconfig, free* and others.
|
||||||
|
psutil currently supports the following platforms:
|
||||||
|
|
||||||
|
- **Linux**
|
||||||
|
- **Windows**
|
||||||
|
- **macOS**
|
||||||
|
- **FreeBSD, OpenBSD**, **NetBSD**
|
||||||
|
- **Sun Solaris**
|
||||||
|
- **AIX**
|
||||||
|
|
||||||
|
Supported Python versions are **2.7**, **3.4+** and
|
||||||
|
`PyPy <http://pypy.org/>`__.
|
||||||
|
|
||||||
|
Funding
|
||||||
|
=======
|
||||||
|
|
||||||
|
While psutil is free software and will always be, the project would benefit
|
||||||
|
immensely from some funding.
|
||||||
|
Keeping up with bug reports and maintenance has become hardly sustainable for
|
||||||
|
me alone in terms of time.
|
||||||
|
If you're a company that's making significant use of psutil you can consider
|
||||||
|
becoming a sponsor via `GitHub Sponsors <https://github.com/sponsors/giampaolo>`__,
|
||||||
|
`Open Collective <https://opencollective.com/psutil>`__ or
|
||||||
|
`PayPal <https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8>`__
|
||||||
|
and have your logo displayed in here and psutil `doc <https://psutil.readthedocs.io>`__.
|
||||||
|
|
||||||
|
Sponsors
|
||||||
|
========
|
||||||
|
|
||||||
|
.. image:: https://github.com/giampaolo/psutil/raw/master/docs/_static/tidelift-logo.png
|
||||||
|
:width: 200
|
||||||
|
:alt: Alternative text
|
||||||
|
|
||||||
|
`Add your logo <https://github.com/sponsors/giampaolo>`__.
|
||||||
|
|
||||||
|
Example usages
|
||||||
|
==============
|
||||||
|
|
||||||
|
This represents pretty much the whole psutil API.
|
||||||
|
|
||||||
|
CPU
|
||||||
|
---
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> import psutil
|
||||||
|
>>>
|
||||||
|
>>> psutil.cpu_times()
|
||||||
|
scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, nice=0.0)
|
||||||
|
>>>
|
||||||
|
>>> for x in range(3):
|
||||||
|
... psutil.cpu_percent(interval=1)
|
||||||
|
...
|
||||||
|
4.0
|
||||||
|
5.9
|
||||||
|
3.8
|
||||||
|
>>>
|
||||||
|
>>> for x in range(3):
|
||||||
|
... psutil.cpu_percent(interval=1, percpu=True)
|
||||||
|
...
|
||||||
|
[4.0, 6.9, 3.7, 9.2]
|
||||||
|
[7.0, 8.5, 2.4, 2.1]
|
||||||
|
[1.2, 9.0, 9.9, 7.2]
|
||||||
|
>>>
|
||||||
|
>>> for x in range(3):
|
||||||
|
... psutil.cpu_times_percent(interval=1, percpu=False)
|
||||||
|
...
|
||||||
|
scputimes(user=1.5, nice=0.0, system=0.5, idle=96.5, iowait=1.5, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0)
|
||||||
|
scputimes(user=1.0, nice=0.0, system=0.0, idle=99.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0)
|
||||||
|
scputimes(user=2.0, nice=0.0, system=0.0, idle=98.0, iowait=0.0, irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0)
|
||||||
|
>>>
|
||||||
|
>>> psutil.cpu_count()
|
||||||
|
4
|
||||||
|
>>> psutil.cpu_count(logical=False)
|
||||||
|
2
|
||||||
|
>>>
|
||||||
|
>>> psutil.cpu_stats()
|
||||||
|
scpustats(ctx_switches=20455687, interrupts=6598984, soft_interrupts=2134212, syscalls=0)
|
||||||
|
>>>
|
||||||
|
>>> psutil.cpu_freq()
|
||||||
|
scpufreq(current=931.42925, min=800.0, max=3500.0)
|
||||||
|
>>>
|
||||||
|
>>> psutil.getloadavg() # also on Windows (emulated)
|
||||||
|
(3.14, 3.89, 4.67)
|
||||||
|
|
||||||
|
Memory
|
||||||
|
------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> psutil.virtual_memory()
|
||||||
|
svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304)
|
||||||
|
>>> psutil.swap_memory()
|
||||||
|
sswap(total=2097147904, used=296128512, free=1801019392, percent=14.1, sin=304193536, sout=677842944)
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Disks
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> psutil.disk_partitions()
|
||||||
|
[sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid', maxfile=255, maxpath=4096),
|
||||||
|
sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext, opts='rw', maxfile=255, maxpath=4096)]
|
||||||
|
>>>
|
||||||
|
>>> psutil.disk_usage('/')
|
||||||
|
sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5)
|
||||||
|
>>>
|
||||||
|
>>> psutil.disk_io_counters(perdisk=False)
|
||||||
|
sdiskio(read_count=719566, write_count=1082197, read_bytes=18626220032, write_bytes=24081764352, read_time=5023392, write_time=63199568, read_merged_count=619166, write_merged_count=812396, busy_time=4523412)
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Network
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> psutil.net_io_counters(pernic=True)
|
||||||
|
{'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0),
|
||||||
|
'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)}
|
||||||
|
>>>
|
||||||
|
>>> psutil.net_connections(kind='tcp')
|
||||||
|
[sconn(fd=115, family=<AddressFamily.AF_INET: 2>, type=<SocketType.SOCK_STREAM: 1>, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED', pid=1254),
|
||||||
|
sconn(fd=117, family=<AddressFamily.AF_INET: 2>, type=<SocketType.SOCK_STREAM: 1>, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING', pid=2987),
|
||||||
|
...]
|
||||||
|
>>>
|
||||||
|
>>> psutil.net_if_addrs()
|
||||||
|
{'lo': [snicaddr(family=<AddressFamily.AF_INET: 2>, address='127.0.0.1', netmask='255.0.0.0', broadcast='127.0.0.1', ptp=None),
|
||||||
|
snicaddr(family=<AddressFamily.AF_INET6: 10>, address='::1', netmask='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', broadcast=None, ptp=None),
|
||||||
|
snicaddr(family=<AddressFamily.AF_LINK: 17>, address='00:00:00:00:00:00', netmask=None, broadcast='00:00:00:00:00:00', ptp=None)],
|
||||||
|
'wlan0': [snicaddr(family=<AddressFamily.AF_INET: 2>, address='192.168.1.3', netmask='255.255.255.0', broadcast='192.168.1.255', ptp=None),
|
||||||
|
snicaddr(family=<AddressFamily.AF_INET6: 10>, address='fe80::c685:8ff:fe45:641%wlan0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None),
|
||||||
|
snicaddr(family=<AddressFamily.AF_LINK: 17>, address='c4:85:08:45:06:41', netmask=None, broadcast='ff:ff:ff:ff:ff:ff', ptp=None)]}
|
||||||
|
>>>
|
||||||
|
>>> psutil.net_if_stats()
|
||||||
|
{'lo': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_UNKNOWN: 0>, speed=0, mtu=65536),
|
||||||
|
'wlan0': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_FULL: 2>, speed=100, mtu=1500)}
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Sensors
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> import psutil
|
||||||
|
>>> psutil.sensors_temperatures()
|
||||||
|
{'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)],
|
||||||
|
'asus': [shwtemp(label='', current=47.0, high=None, critical=None)],
|
||||||
|
'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0),
|
||||||
|
shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0)]}
|
||||||
|
>>>
|
||||||
|
>>> psutil.sensors_fans()
|
||||||
|
{'asus': [sfan(label='cpu_fan', current=3200)]}
|
||||||
|
>>>
|
||||||
|
>>> psutil.sensors_battery()
|
||||||
|
sbattery(percent=93, secsleft=16628, power_plugged=False)
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Other system info
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> import psutil
|
||||||
|
>>> psutil.users()
|
||||||
|
[suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352),
|
||||||
|
suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)]
|
||||||
|
>>>
|
||||||
|
>>> psutil.boot_time()
|
||||||
|
1365519115.0
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Process management
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> import psutil
|
||||||
|
>>> psutil.pids()
|
||||||
|
[1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215,
|
||||||
|
1216, 1220, 1221, 1243, 1244, 1301, 1601, 2237, 2355, 2637, 2774, 3932,
|
||||||
|
4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, 4306, 4311,
|
||||||
|
4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433,
|
||||||
|
4443, 4445, 4446, 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054,
|
||||||
|
7055, 7071]
|
||||||
|
>>>
|
||||||
|
>>> p = psutil.Process(7055)
|
||||||
|
>>> p
|
||||||
|
psutil.Process(pid=7055, name='python3', status='running', started='09:04:44')
|
||||||
|
>>> p.name()
|
||||||
|
'python3'
|
||||||
|
>>> p.exe()
|
||||||
|
'/usr/bin/python3'
|
||||||
|
>>> p.cwd()
|
||||||
|
'/home/giampaolo'
|
||||||
|
>>> p.cmdline()
|
||||||
|
['/usr/bin/python', 'main.py']
|
||||||
|
>>>
|
||||||
|
>>> p.pid
|
||||||
|
7055
|
||||||
|
>>> p.ppid()
|
||||||
|
7054
|
||||||
|
>>> p.children(recursive=True)
|
||||||
|
[psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'),
|
||||||
|
psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')]
|
||||||
|
>>>
|
||||||
|
>>> p.parent()
|
||||||
|
psutil.Process(pid=4699, name='bash', status='sleeping', started='09:06:44')
|
||||||
|
>>> p.parents()
|
||||||
|
[psutil.Process(pid=4699, name='bash', started='09:06:44'),
|
||||||
|
psutil.Process(pid=4689, name='gnome-terminal-server', status='sleeping', started='0:06:44'),
|
||||||
|
psutil.Process(pid=1, name='systemd', status='sleeping', started='05:56:55')]
|
||||||
|
>>>
|
||||||
|
>>> p.status()
|
||||||
|
'running'
|
||||||
|
>>> p.username()
|
||||||
|
'giampaolo'
|
||||||
|
>>> p.create_time()
|
||||||
|
1267551141.5019531
|
||||||
|
>>> p.terminal()
|
||||||
|
'/dev/pts/0'
|
||||||
|
>>>
|
||||||
|
>>> p.uids()
|
||||||
|
puids(real=1000, effective=1000, saved=1000)
|
||||||
|
>>> p.gids()
|
||||||
|
pgids(real=1000, effective=1000, saved=1000)
|
||||||
|
>>>
|
||||||
|
>>> p.cpu_times()
|
||||||
|
pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0)
|
||||||
|
>>> p.cpu_percent(interval=1.0)
|
||||||
|
12.1
|
||||||
|
>>> p.cpu_affinity()
|
||||||
|
[0, 1, 2, 3]
|
||||||
|
>>> p.cpu_affinity([0, 1]) # set
|
||||||
|
>>> p.cpu_num()
|
||||||
|
1
|
||||||
|
>>>
|
||||||
|
>>> p.memory_info()
|
||||||
|
pmem(rss=10915840, vms=67608576, shared=3313664, text=2310144, lib=0, data=7262208, dirty=0)
|
||||||
|
>>> p.memory_full_info() # "real" USS memory usage (Linux, macOS, Win only)
|
||||||
|
pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0)
|
||||||
|
>>> p.memory_percent()
|
||||||
|
0.7823
|
||||||
|
>>> p.memory_maps()
|
||||||
|
[pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0),
|
||||||
|
pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0),
|
||||||
|
pmmap_grouped(path='[heap]', rss=32768, size=139264, pss=32768, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=32768, referenced=32768, anonymous=32768, swap=0),
|
||||||
|
pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0),
|
||||||
|
...]
|
||||||
|
>>>
|
||||||
|
>>> p.io_counters()
|
||||||
|
pio(read_count=478001, write_count=59371, read_bytes=700416, write_bytes=69632, read_chars=456232, write_chars=517543)
|
||||||
|
>>>
|
||||||
|
>>> p.open_files()
|
||||||
|
[popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768),
|
||||||
|
popenfile(path='/var/log/monit.log', fd=4, position=235542, mode='a', flags=33793)]
|
||||||
|
>>>
|
||||||
|
>>> p.connections(kind='tcp')
|
||||||
|
[pconn(fd=115, family=<AddressFamily.AF_INET: 2>, type=<SocketType.SOCK_STREAM: 1>, laddr=addr(ip='10.0.0.1', port=48776), raddr=addr(ip='93.186.135.91', port=80), status='ESTABLISHED'),
|
||||||
|
pconn(fd=117, family=<AddressFamily.AF_INET: 2>, type=<SocketType.SOCK_STREAM: 1>, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')]
|
||||||
|
>>>
|
||||||
|
>>> p.num_threads()
|
||||||
|
4
|
||||||
|
>>> p.num_fds()
|
||||||
|
8
|
||||||
|
>>> p.threads()
|
||||||
|
[pthread(id=5234, user_time=22.5, system_time=9.2891),
|
||||||
|
pthread(id=5237, user_time=0.0707, system_time=1.1)]
|
||||||
|
>>>
|
||||||
|
>>> p.num_ctx_switches()
|
||||||
|
pctxsw(voluntary=78, involuntary=19)
|
||||||
|
>>>
|
||||||
|
>>> p.nice()
|
||||||
|
0
|
||||||
|
>>> p.nice(10) # set
|
||||||
|
>>>
|
||||||
|
>>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only)
|
||||||
|
>>> p.ionice()
|
||||||
|
pionice(ioclass=<IOPriority.IOPRIO_CLASS_IDLE: 3>, value=0)
|
||||||
|
>>>
|
||||||
|
>>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only)
|
||||||
|
>>> p.rlimit(psutil.RLIMIT_NOFILE)
|
||||||
|
(5, 5)
|
||||||
|
>>>
|
||||||
|
>>> p.environ()
|
||||||
|
{'LC_PAPER': 'it_IT.UTF-8', 'SHELL': '/bin/bash', 'GREP_OPTIONS': '--color=auto',
|
||||||
|
'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg',
|
||||||
|
...}
|
||||||
|
>>>
|
||||||
|
>>> p.as_dict()
|
||||||
|
{'status': 'running', 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...}
|
||||||
|
>>> p.is_running()
|
||||||
|
True
|
||||||
|
>>> p.suspend()
|
||||||
|
>>> p.resume()
|
||||||
|
>>>
|
||||||
|
>>> p.terminate()
|
||||||
|
>>> p.kill()
|
||||||
|
>>> p.wait(timeout=3)
|
||||||
|
<Exitcode.EX_OK: 0>
|
||||||
|
>>>
|
||||||
|
>>> psutil.test()
|
||||||
|
USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND
|
||||||
|
root 1 0.0 0.0 24584 2240 Jun17 00:00 init
|
||||||
|
root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd
|
||||||
|
...
|
||||||
|
giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4
|
||||||
|
giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome
|
||||||
|
root 31763 0.0 0.0 0 0 00:05 00:00 kworker/0:1
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Further process APIs
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> import psutil
|
||||||
|
>>> for proc in psutil.process_iter(['pid', 'name']):
|
||||||
|
... print(proc.info)
|
||||||
|
...
|
||||||
|
{'pid': 1, 'name': 'systemd'}
|
||||||
|
{'pid': 2, 'name': 'kthreadd'}
|
||||||
|
{'pid': 3, 'name': 'ksoftirqd/0'}
|
||||||
|
...
|
||||||
|
>>>
|
||||||
|
>>> psutil.pid_exists(3)
|
||||||
|
True
|
||||||
|
>>>
|
||||||
|
>>> def on_terminate(proc):
|
||||||
|
... print("process {} terminated".format(proc))
|
||||||
|
...
|
||||||
|
>>> # waits for multiple processes to terminate
|
||||||
|
>>> gone, alive = psutil.wait_procs(procs_list, timeout=3, callback=on_terminate)
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Windows services
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> list(psutil.win_service_iter())
|
||||||
|
[<WindowsService(name='AeLookupSvc', display_name='Application Experience') at 38850096>,
|
||||||
|
<WindowsService(name='ALG', display_name='Application Layer Gateway Service') at 38850128>,
|
||||||
|
<WindowsService(name='APNMCP', display_name='Ask Update Service') at 38850160>,
|
||||||
|
<WindowsService(name='AppIDSvc', display_name='Application Identity') at 38850192>,
|
||||||
|
...]
|
||||||
|
>>> s = psutil.win_service_get('alg')
|
||||||
|
>>> s.as_dict()
|
||||||
|
{'binpath': 'C:\\Windows\\System32\\alg.exe',
|
||||||
|
'description': 'Provides support for 3rd party protocol plug-ins for Internet Connection Sharing',
|
||||||
|
'display_name': 'Application Layer Gateway Service',
|
||||||
|
'name': 'alg',
|
||||||
|
'pid': None,
|
||||||
|
'start_type': 'manual',
|
||||||
|
'status': 'stopped',
|
||||||
|
'username': 'NT AUTHORITY\\LocalService'}
|
||||||
|
|
||||||
|
Projects using psutil
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Here's some I find particularly interesting:
|
||||||
|
|
||||||
|
- https://github.com/google/grr
|
||||||
|
- https://github.com/facebook/osquery/
|
||||||
|
- https://github.com/nicolargo/glances
|
||||||
|
- https://github.com/Jahaja/psdash
|
||||||
|
- https://github.com/ajenti/ajenti
|
||||||
|
- https://github.com/home-assistant/home-assistant/
|
||||||
|
|
||||||
|
Portings
|
||||||
|
========
|
||||||
|
|
||||||
|
- Go: https://github.com/shirou/gopsutil
|
||||||
|
- C: https://github.com/hamon-in/cpslib
|
||||||
|
- Rust: https://github.com/rust-psutil/rust-psutil
|
||||||
|
- Nim: https://github.com/johnscillieri/psutil-nim
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
|||||||
|
psutil-5.9.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
psutil-5.9.1.dist-info/LICENSE,sha256=JMEphFAMqgf_3OGe68BjlsXm0kS1c7xsQ49KbvjlbBs,1549
|
||||||
|
psutil-5.9.1.dist-info/METADATA,sha256=HznvJXCoUMTL24QwoTuZU0MFBJToXTFgMKIGvX2kxjs,21348
|
||||||
|
psutil-5.9.1.dist-info/RECORD,,
|
||||||
|
psutil-5.9.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
psutil-5.9.1.dist-info/WHEEL,sha256=M6xTlFCb8v2YV6L6jA5gzcMuyFFFbWG3abJcfsXu3jk,229
|
||||||
|
psutil-5.9.1.dist-info/top_level.txt,sha256=gCNhn57wzksDjSAISmgMJ0aiXzQulk0GJhb2-BAyYgw,7
|
||||||
|
psutil/__init__.py,sha256=9itxv6FFB9ZX18znCM-zV8Wztj6lLm5FbLxWogJRdjg,87349
|
||||||
|
psutil/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_common.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_compat.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_psaix.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_psbsd.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_pslinux.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_psosx.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_psposix.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_pssunos.cpython-310.pyc,,
|
||||||
|
psutil/__pycache__/_pswindows.cpython-310.pyc,,
|
||||||
|
psutil/_common.py,sha256=ZRDfxC5d_bWd5Unot1Z6IFp_rzSGNGWPuKnxgdxxrWQ,28196
|
||||||
|
psutil/_compat.py,sha256=YyWKdWQM5nyBSqDi5-5QQn1R-LdAlKulotAfidSBbI4,15043
|
||||||
|
psutil/_psaix.py,sha256=gREBEu-V3NNC_COJSoabqWwkef4JgvxNsgOYc4u7jIY,18552
|
||||||
|
psutil/_psbsd.py,sha256=BtP-5CJZkutfJxrsA0_cIne2LlZI8ibgYRVXshSr8Xw,31162
|
||||||
|
psutil/_pslinux.py,sha256=6cHy0Fz86SgxP7JX6knR1W2ErrPPyO4Y9sX-A46wGzc,86181
|
||||||
|
psutil/_psosx.py,sha256=uKv_DphRKC5C3UG4rUxCM8HpeicknmM0q12tY0IVFeQ,16142
|
||||||
|
psutil/_psposix.py,sha256=e8lmY7z3zKqo5XoePP7dhy5NRwxGMVEW7BrE4FfLc_M,8046
|
||||||
|
psutil/_pssunos.py,sha256=la5rmL0A2XmC10LL0F0avnwMNttJuiJdFPgYR_Hu-Lw,25489
|
||||||
|
psutil/_psutil_linux.cpython-310-x86_64-linux-gnu.so,sha256=Uh1s6Uj7vINTPq3lBLN2Ovv4mWCAlTO1ZIDtmBAoFRU,107080
|
||||||
|
psutil/_psutil_posix.cpython-310-x86_64-linux-gnu.so,sha256=C0N6iBGDQ1QPCNSOsjMl7x5bCIRQihtRnzwgrVI78fA,64712
|
||||||
|
psutil/_pswindows.py,sha256=Q_q_I-XrgBjQkBPvgyJ18HuFg9j45NrP7MiLUbxexmA,37119
|
||||||
|
psutil/tests/__init__.py,sha256=aYl0_WBXD7KORm_BVkFkt7VrmLjb3QWXpseYQ5K7NCM,57896
|
||||||
|
psutil/tests/__main__.py,sha256=hhM384jjFQtDF9sTj_DXaBQCXCVLwdyjLil4UTXke8Q,293
|
||||||
|
psutil/tests/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/__main__.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/runner.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_aix.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_bsd.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_connections.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_contracts.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_linux.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_memleaks.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_misc.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_osx.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_posix.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_process.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_sunos.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_system.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_testutils.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_unicode.cpython-310.pyc,,
|
||||||
|
psutil/tests/__pycache__/test_windows.cpython-310.pyc,,
|
||||||
|
psutil/tests/runner.py,sha256=ezm1dJbuimOLEYRk_8LrAS1RF-hGT1Kkha_hb8720tY,11204
|
||||||
|
psutil/tests/test_aix.py,sha256=B5zO6M4JF5noyt0Tui_GzQTvBh-MjG7Rk5AFzkOmXLM,4508
|
||||||
|
psutil/tests/test_bsd.py,sha256=akzc3w9g84kIoW38zDkkWysyDM6dt6CNUzlwB2AVwVI,20689
|
||||||
|
psutil/tests/test_connections.py,sha256=QWXNRiMSBdROkaPjKJz0fez_dqbHULGDcXFd-N9iKrM,21362
|
||||||
|
psutil/tests/test_contracts.py,sha256=NOxI_4Wbu-J70nlwhIozg1Ddvy75k_6cy_grT42eF0U,27067
|
||||||
|
psutil/tests/test_linux.py,sha256=aabJALVsFEy36Nt49U5ui8h4R4qzBTwJTGTe4t11r54,92914
|
||||||
|
psutil/tests/test_memleaks.py,sha256=BdS2aVI1wp3AhPY5epY4HeV-DP2VsPaQDN6U-fFCSuA,14862
|
||||||
|
psutil/tests/test_misc.py,sha256=0NHaf7OJTA24AxJFqPaYDn2c0pwY6rhZNU0htRQV1M0,31464
|
||||||
|
psutil/tests/test_osx.py,sha256=BbbEKiYPrBB04YbgOm-DOnW6LLcvBAMevFrGTrd-zKI,7565
|
||||||
|
psutil/tests/test_posix.py,sha256=RG1fEupGZHvUJkC_yo1RJKRgwhQP3m2-sNIWVTGxwqM,15138
|
||||||
|
psutil/tests/test_process.py,sha256=HybGnOqS1YhHcridRskbiiEt5aZ6ctUa29-MDLwrRbQ,61655
|
||||||
|
psutil/tests/test_sunos.py,sha256=-gnzJy9mc6rwovtoXKYJw_h71FaCXgWLp85POlL1RtE,1333
|
||||||
|
psutil/tests/test_system.py,sha256=uYh_LPJ24OAQVrLG6rwtlvSPOHPErcrOEVrVm25-LUc,35556
|
||||||
|
psutil/tests/test_testutils.py,sha256=vZ0UiZNOyQsydXXA38Atz0FS1qE7MlyCjHvetaPpSqM,14427
|
||||||
|
psutil/tests/test_unicode.py,sha256=HgK3AzYGTRnqJKc6ytzuTUiglF5nZSBiVtkPHavoOfg,12441
|
||||||
|
psutil/tests/test_windows.py,sha256=uzEWavNQYsLoBY51jw7uEQpEr-37mOBkzYNb7tbhggw,32815
|
@ -0,0 +1,8 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp310-cp310-manylinux_2_12_x86_64
|
||||||
|
Tag: cp310-cp310-manylinux2010_x86_64
|
||||||
|
Tag: cp310-cp310-manylinux_2_17_x86_64
|
||||||
|
Tag: cp310-cp310-manylinux2014_x86_64
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
psutil
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,898 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Common objects shared by __init__.py and _ps*.py modules."""
|
||||||
|
|
||||||
|
# Note: this module is imported by setup.py so it should not import
|
||||||
|
# psutil or third-party modules.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import warnings
|
||||||
|
from collections import namedtuple
|
||||||
|
from socket import AF_INET
|
||||||
|
from socket import SOCK_DGRAM
|
||||||
|
from socket import SOCK_STREAM
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from socket import AF_INET6
|
||||||
|
except ImportError:
|
||||||
|
AF_INET6 = None
|
||||||
|
try:
|
||||||
|
from socket import AF_UNIX
|
||||||
|
except ImportError:
|
||||||
|
AF_UNIX = None
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 4):
|
||||||
|
import enum
|
||||||
|
else:
|
||||||
|
enum = None
|
||||||
|
|
||||||
|
|
||||||
|
# can't take it from _common.py as this script is imported by setup.py
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG', 0))
|
||||||
|
_DEFAULT = object()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# OS constants
|
||||||
|
'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX',
|
||||||
|
'SUNOS', 'WINDOWS',
|
||||||
|
# connection constants
|
||||||
|
'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED',
|
||||||
|
'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN',
|
||||||
|
'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT',
|
||||||
|
# net constants
|
||||||
|
'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN',
|
||||||
|
# process status constants
|
||||||
|
'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED',
|
||||||
|
'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED',
|
||||||
|
'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL',
|
||||||
|
'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED',
|
||||||
|
# other constants
|
||||||
|
'ENCODING', 'ENCODING_ERRS', 'AF_INET6',
|
||||||
|
# named tuples
|
||||||
|
'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile',
|
||||||
|
'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart',
|
||||||
|
'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser',
|
||||||
|
# utility functions
|
||||||
|
'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize',
|
||||||
|
'parse_environ_block', 'path_exists_strict', 'usage_percent',
|
||||||
|
'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers",
|
||||||
|
'open_text', 'open_binary', 'cat', 'bcat',
|
||||||
|
'bytes2human', 'conn_to_ntuple', 'debug',
|
||||||
|
# shell utils
|
||||||
|
'hilite', 'term_supports_colors', 'print_color',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- OS constants
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
POSIX = os.name == "posix"
|
||||||
|
WINDOWS = os.name == "nt"
|
||||||
|
LINUX = sys.platform.startswith("linux")
|
||||||
|
MACOS = sys.platform.startswith("darwin")
|
||||||
|
OSX = MACOS # deprecated alias
|
||||||
|
FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd"))
|
||||||
|
OPENBSD = sys.platform.startswith("openbsd")
|
||||||
|
NETBSD = sys.platform.startswith("netbsd")
|
||||||
|
BSD = FREEBSD or OPENBSD or NETBSD
|
||||||
|
SUNOS = sys.platform.startswith(("sunos", "solaris"))
|
||||||
|
AIX = sys.platform.startswith("aix")
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- API constants
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# Process.status()
|
||||||
|
STATUS_RUNNING = "running"
|
||||||
|
STATUS_SLEEPING = "sleeping"
|
||||||
|
STATUS_DISK_SLEEP = "disk-sleep"
|
||||||
|
STATUS_STOPPED = "stopped"
|
||||||
|
STATUS_TRACING_STOP = "tracing-stop"
|
||||||
|
STATUS_ZOMBIE = "zombie"
|
||||||
|
STATUS_DEAD = "dead"
|
||||||
|
STATUS_WAKE_KILL = "wake-kill"
|
||||||
|
STATUS_WAKING = "waking"
|
||||||
|
STATUS_IDLE = "idle" # Linux, macOS, FreeBSD
|
||||||
|
STATUS_LOCKED = "locked" # FreeBSD
|
||||||
|
STATUS_WAITING = "waiting" # FreeBSD
|
||||||
|
STATUS_SUSPENDED = "suspended" # NetBSD
|
||||||
|
STATUS_PARKED = "parked" # Linux
|
||||||
|
|
||||||
|
# Process.connections() and psutil.net_connections()
|
||||||
|
CONN_ESTABLISHED = "ESTABLISHED"
|
||||||
|
CONN_SYN_SENT = "SYN_SENT"
|
||||||
|
CONN_SYN_RECV = "SYN_RECV"
|
||||||
|
CONN_FIN_WAIT1 = "FIN_WAIT1"
|
||||||
|
CONN_FIN_WAIT2 = "FIN_WAIT2"
|
||||||
|
CONN_TIME_WAIT = "TIME_WAIT"
|
||||||
|
CONN_CLOSE = "CLOSE"
|
||||||
|
CONN_CLOSE_WAIT = "CLOSE_WAIT"
|
||||||
|
CONN_LAST_ACK = "LAST_ACK"
|
||||||
|
CONN_LISTEN = "LISTEN"
|
||||||
|
CONN_CLOSING = "CLOSING"
|
||||||
|
CONN_NONE = "NONE"
|
||||||
|
|
||||||
|
# net_if_stats()
|
||||||
|
if enum is None:
|
||||||
|
NIC_DUPLEX_FULL = 2
|
||||||
|
NIC_DUPLEX_HALF = 1
|
||||||
|
NIC_DUPLEX_UNKNOWN = 0
|
||||||
|
else:
|
||||||
|
class NicDuplex(enum.IntEnum):
|
||||||
|
NIC_DUPLEX_FULL = 2
|
||||||
|
NIC_DUPLEX_HALF = 1
|
||||||
|
NIC_DUPLEX_UNKNOWN = 0
|
||||||
|
|
||||||
|
globals().update(NicDuplex.__members__)
|
||||||
|
|
||||||
|
# sensors_battery()
|
||||||
|
if enum is None:
|
||||||
|
POWER_TIME_UNKNOWN = -1
|
||||||
|
POWER_TIME_UNLIMITED = -2
|
||||||
|
else:
|
||||||
|
class BatteryTime(enum.IntEnum):
|
||||||
|
POWER_TIME_UNKNOWN = -1
|
||||||
|
POWER_TIME_UNLIMITED = -2
|
||||||
|
|
||||||
|
globals().update(BatteryTime.__members__)
|
||||||
|
|
||||||
|
# --- others
|
||||||
|
|
||||||
|
ENCODING = sys.getfilesystemencoding()
|
||||||
|
if not PY3:
|
||||||
|
ENCODING_ERRS = "replace"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
ENCODING_ERRS = sys.getfilesystemencodeerrors() # py 3.6
|
||||||
|
except AttributeError:
|
||||||
|
ENCODING_ERRS = "surrogateescape" if POSIX else "replace"
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- namedtuples
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# --- for system functions
|
||||||
|
|
||||||
|
# psutil.swap_memory()
|
||||||
|
sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin',
|
||||||
|
'sout'])
|
||||||
|
# psutil.disk_usage()
|
||||||
|
sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent'])
|
||||||
|
# psutil.disk_io_counters()
|
||||||
|
sdiskio = namedtuple('sdiskio', ['read_count', 'write_count',
|
||||||
|
'read_bytes', 'write_bytes',
|
||||||
|
'read_time', 'write_time'])
|
||||||
|
# psutil.disk_partitions()
|
||||||
|
sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts',
|
||||||
|
'maxfile', 'maxpath'])
|
||||||
|
# psutil.net_io_counters()
|
||||||
|
snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv',
|
||||||
|
'packets_sent', 'packets_recv',
|
||||||
|
'errin', 'errout',
|
||||||
|
'dropin', 'dropout'])
|
||||||
|
# psutil.users()
|
||||||
|
suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid'])
|
||||||
|
# psutil.net_connections()
|
||||||
|
sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr',
|
||||||
|
'status', 'pid'])
|
||||||
|
# psutil.net_if_addrs()
|
||||||
|
snicaddr = namedtuple('snicaddr',
|
||||||
|
['family', 'address', 'netmask', 'broadcast', 'ptp'])
|
||||||
|
# psutil.net_if_stats()
|
||||||
|
snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu'])
|
||||||
|
# psutil.cpu_stats()
|
||||||
|
scpustats = namedtuple(
|
||||||
|
'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'])
|
||||||
|
# psutil.cpu_freq()
|
||||||
|
scpufreq = namedtuple('scpufreq', ['current', 'min', 'max'])
|
||||||
|
# psutil.sensors_temperatures()
|
||||||
|
shwtemp = namedtuple(
|
||||||
|
'shwtemp', ['label', 'current', 'high', 'critical'])
|
||||||
|
# psutil.sensors_battery()
|
||||||
|
sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged'])
|
||||||
|
# psutil.sensors_fans()
|
||||||
|
sfan = namedtuple('sfan', ['label', 'current'])
|
||||||
|
|
||||||
|
# --- for Process methods
|
||||||
|
|
||||||
|
# psutil.Process.cpu_times()
|
||||||
|
pcputimes = namedtuple('pcputimes',
|
||||||
|
['user', 'system', 'children_user', 'children_system'])
|
||||||
|
# psutil.Process.open_files()
|
||||||
|
popenfile = namedtuple('popenfile', ['path', 'fd'])
|
||||||
|
# psutil.Process.threads()
|
||||||
|
pthread = namedtuple('pthread', ['id', 'user_time', 'system_time'])
|
||||||
|
# psutil.Process.uids()
|
||||||
|
puids = namedtuple('puids', ['real', 'effective', 'saved'])
|
||||||
|
# psutil.Process.gids()
|
||||||
|
pgids = namedtuple('pgids', ['real', 'effective', 'saved'])
|
||||||
|
# psutil.Process.io_counters()
|
||||||
|
pio = namedtuple('pio', ['read_count', 'write_count',
|
||||||
|
'read_bytes', 'write_bytes'])
|
||||||
|
# psutil.Process.ionice()
|
||||||
|
pionice = namedtuple('pionice', ['ioclass', 'value'])
|
||||||
|
# psutil.Process.ctx_switches()
|
||||||
|
pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary'])
|
||||||
|
# psutil.Process.connections()
|
||||||
|
pconn = namedtuple('pconn', ['fd', 'family', 'type', 'laddr', 'raddr',
|
||||||
|
'status'])
|
||||||
|
|
||||||
|
# psutil.connections() and psutil.Process.connections()
|
||||||
|
addr = namedtuple('addr', ['ip', 'port'])
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Process.connections() 'kind' parameter mapping
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
conn_tmap = {
|
||||||
|
"all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]),
|
||||||
|
"tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]),
|
||||||
|
"tcp4": ([AF_INET], [SOCK_STREAM]),
|
||||||
|
"udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]),
|
||||||
|
"udp4": ([AF_INET], [SOCK_DGRAM]),
|
||||||
|
"inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
|
||||||
|
"inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]),
|
||||||
|
"inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if AF_INET6 is not None:
|
||||||
|
conn_tmap.update({
|
||||||
|
"tcp6": ([AF_INET6], [SOCK_STREAM]),
|
||||||
|
"udp6": ([AF_INET6], [SOCK_DGRAM]),
|
||||||
|
})
|
||||||
|
|
||||||
|
if AF_UNIX is not None:
|
||||||
|
conn_tmap.update({
|
||||||
|
"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- Exceptions
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
"""Base exception class. All other psutil exceptions inherit
|
||||||
|
from this one.
|
||||||
|
"""
|
||||||
|
__module__ = 'psutil'
|
||||||
|
|
||||||
|
def _infodict(self, attrs):
|
||||||
|
info = collections.OrderedDict()
|
||||||
|
for name in attrs:
|
||||||
|
value = getattr(self, name, None)
|
||||||
|
if value:
|
||||||
|
info[name] = value
|
||||||
|
elif name == "pid" and value == 0:
|
||||||
|
info[name] = value
|
||||||
|
return info
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# invoked on `raise Error`
|
||||||
|
info = self._infodict(("pid", "ppid", "name"))
|
||||||
|
if info:
|
||||||
|
details = "(%s)" % ", ".join(
|
||||||
|
["%s=%r" % (k, v) for k, v in info.items()])
|
||||||
|
else:
|
||||||
|
details = None
|
||||||
|
return " ".join([x for x in (getattr(self, "msg", ""), details) if x])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# invoked on `repr(Error)`
|
||||||
|
info = self._infodict(("pid", "ppid", "name", "seconds", "msg"))
|
||||||
|
details = ", ".join(["%s=%r" % (k, v) for k, v in info.items()])
|
||||||
|
return "psutil.%s(%s)" % (self.__class__.__name__, details)
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchProcess(Error):
|
||||||
|
"""Exception raised when a process with a certain PID doesn't
|
||||||
|
or no longer exists.
|
||||||
|
"""
|
||||||
|
__module__ = 'psutil'
|
||||||
|
|
||||||
|
def __init__(self, pid, name=None, msg=None):
|
||||||
|
Error.__init__(self)
|
||||||
|
self.pid = pid
|
||||||
|
self.name = name
|
||||||
|
self.msg = msg or "process no longer exists"
|
||||||
|
|
||||||
|
|
||||||
|
class ZombieProcess(NoSuchProcess):
|
||||||
|
"""Exception raised when querying a zombie process. This is
|
||||||
|
raised on macOS, BSD and Solaris only, and not always: depending
|
||||||
|
on the query the OS may be able to succeed anyway.
|
||||||
|
On Linux all zombie processes are querable (hence this is never
|
||||||
|
raised). Windows doesn't have zombie processes.
|
||||||
|
"""
|
||||||
|
__module__ = 'psutil'
|
||||||
|
|
||||||
|
def __init__(self, pid, name=None, ppid=None, msg=None):
|
||||||
|
NoSuchProcess.__init__(self, pid, name, msg)
|
||||||
|
self.ppid = ppid
|
||||||
|
self.msg = msg or "PID still exists but it's a zombie"
|
||||||
|
|
||||||
|
|
||||||
|
class AccessDenied(Error):
|
||||||
|
"""Exception raised when permission to perform an action is denied."""
|
||||||
|
__module__ = 'psutil'
|
||||||
|
|
||||||
|
def __init__(self, pid=None, name=None, msg=None):
|
||||||
|
Error.__init__(self)
|
||||||
|
self.pid = pid
|
||||||
|
self.name = name
|
||||||
|
self.msg = msg or ""
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutExpired(Error):
|
||||||
|
"""Raised on Process.wait(timeout) if timeout expires and process
|
||||||
|
is still alive.
|
||||||
|
"""
|
||||||
|
__module__ = 'psutil'
|
||||||
|
|
||||||
|
def __init__(self, seconds, pid=None, name=None):
|
||||||
|
Error.__init__(self)
|
||||||
|
self.seconds = seconds
|
||||||
|
self.pid = pid
|
||||||
|
self.name = name
|
||||||
|
self.msg = "timeout after %s seconds" % seconds
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- utils
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def usage_percent(used, total, round_=None):
|
||||||
|
"""Calculate percentage usage of 'used' against 'total'."""
|
||||||
|
try:
|
||||||
|
ret = (float(used) / total) * 100
|
||||||
|
except ZeroDivisionError:
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
if round_ is not None:
|
||||||
|
ret = round(ret, round_)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def memoize(fun):
|
||||||
|
"""A simple memoize decorator for functions supporting (hashable)
|
||||||
|
positional arguments.
|
||||||
|
It also provides a cache_clear() function for clearing the cache:
|
||||||
|
|
||||||
|
>>> @memoize
|
||||||
|
... def foo()
|
||||||
|
... return 1
|
||||||
|
...
|
||||||
|
>>> foo()
|
||||||
|
1
|
||||||
|
>>> foo.cache_clear()
|
||||||
|
>>>
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
key = (args, frozenset(sorted(kwargs.items())))
|
||||||
|
try:
|
||||||
|
return cache[key]
|
||||||
|
except KeyError:
|
||||||
|
ret = cache[key] = fun(*args, **kwargs)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def cache_clear():
|
||||||
|
"""Clear cache."""
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
wrapper.cache_clear = cache_clear
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def memoize_when_activated(fun):
|
||||||
|
"""A memoize decorator which is disabled by default. It can be
|
||||||
|
activated and deactivated on request.
|
||||||
|
For efficiency reasons it can be used only against class methods
|
||||||
|
accepting no arguments.
|
||||||
|
|
||||||
|
>>> class Foo:
|
||||||
|
... @memoize
|
||||||
|
... def foo()
|
||||||
|
... print(1)
|
||||||
|
...
|
||||||
|
>>> f = Foo()
|
||||||
|
>>> # deactivated (default)
|
||||||
|
>>> foo()
|
||||||
|
1
|
||||||
|
>>> foo()
|
||||||
|
1
|
||||||
|
>>>
|
||||||
|
>>> # activated
|
||||||
|
>>> foo.cache_activate(self)
|
||||||
|
>>> foo()
|
||||||
|
1
|
||||||
|
>>> foo()
|
||||||
|
>>> foo()
|
||||||
|
>>>
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self):
|
||||||
|
try:
|
||||||
|
# case 1: we previously entered oneshot() ctx
|
||||||
|
ret = self._cache[fun]
|
||||||
|
except AttributeError:
|
||||||
|
# case 2: we never entered oneshot() ctx
|
||||||
|
return fun(self)
|
||||||
|
except KeyError:
|
||||||
|
# case 3: we entered oneshot() ctx but there's no cache
|
||||||
|
# for this entry yet
|
||||||
|
ret = fun(self)
|
||||||
|
try:
|
||||||
|
self._cache[fun] = ret
|
||||||
|
except AttributeError:
|
||||||
|
# multi-threading race condition, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1948
|
||||||
|
pass
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def cache_activate(proc):
|
||||||
|
"""Activate cache. Expects a Process instance. Cache will be
|
||||||
|
stored as a "_cache" instance attribute."""
|
||||||
|
proc._cache = {}
|
||||||
|
|
||||||
|
def cache_deactivate(proc):
|
||||||
|
"""Deactivate and clear cache."""
|
||||||
|
try:
|
||||||
|
del proc._cache
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
wrapper.cache_activate = cache_activate
|
||||||
|
wrapper.cache_deactivate = cache_deactivate
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def isfile_strict(path):
|
||||||
|
"""Same as os.path.isfile() but does not swallow EACCES / EPERM
|
||||||
|
exceptions, see:
|
||||||
|
http://mail.python.org/pipermail/python-dev/2012-June/120787.html
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
st = os.stat(path)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno in (errno.EPERM, errno.EACCES):
|
||||||
|
raise
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return stat.S_ISREG(st.st_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def path_exists_strict(path):
|
||||||
|
"""Same as os.path.exists() but does not swallow EACCES / EPERM
|
||||||
|
exceptions, see:
|
||||||
|
http://mail.python.org/pipermail/python-dev/2012-June/120787.html
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.stat(path)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno in (errno.EPERM, errno.EACCES):
|
||||||
|
raise
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def supports_ipv6():
|
||||||
|
"""Return True if IPv6 is supported on this platform."""
|
||||||
|
if not socket.has_ipv6 or AF_INET6 is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
sock = socket.socket(AF_INET6, socket.SOCK_STREAM)
|
||||||
|
with contextlib.closing(sock):
|
||||||
|
sock.bind(("::1", 0))
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_environ_block(data):
|
||||||
|
"""Parse a C environ block of environment variables into a dictionary."""
|
||||||
|
# The block is usually raw data from the target process. It might contain
|
||||||
|
# trailing garbage and lines that do not look like assignments.
|
||||||
|
ret = {}
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
# localize global variable to speed up access.
|
||||||
|
WINDOWS_ = WINDOWS
|
||||||
|
while True:
|
||||||
|
next_pos = data.find("\0", pos)
|
||||||
|
# nul byte at the beginning or double nul byte means finish
|
||||||
|
if next_pos <= pos:
|
||||||
|
break
|
||||||
|
# there might not be an equals sign
|
||||||
|
equal_pos = data.find("=", pos, next_pos)
|
||||||
|
if equal_pos > pos:
|
||||||
|
key = data[pos:equal_pos]
|
||||||
|
value = data[equal_pos + 1:next_pos]
|
||||||
|
# Windows expects environment variables to be uppercase only
|
||||||
|
if WINDOWS_:
|
||||||
|
key = key.upper()
|
||||||
|
ret[key] = value
|
||||||
|
pos = next_pos + 1
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def sockfam_to_enum(num):
|
||||||
|
"""Convert a numeric socket family value to an IntEnum member.
|
||||||
|
If it's not a known member, return the numeric value itself.
|
||||||
|
"""
|
||||||
|
if enum is None:
|
||||||
|
return num
|
||||||
|
else: # pragma: no cover
|
||||||
|
try:
|
||||||
|
return socket.AddressFamily(num)
|
||||||
|
except ValueError:
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def socktype_to_enum(num):
|
||||||
|
"""Convert a numeric socket type value to an IntEnum member.
|
||||||
|
If it's not a known member, return the numeric value itself.
|
||||||
|
"""
|
||||||
|
if enum is None:
|
||||||
|
return num
|
||||||
|
else: # pragma: no cover
|
||||||
|
try:
|
||||||
|
return socket.SocketKind(num)
|
||||||
|
except ValueError:
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None):
|
||||||
|
"""Convert a raw connection tuple to a proper ntuple."""
|
||||||
|
if fam in (socket.AF_INET, AF_INET6):
|
||||||
|
if laddr:
|
||||||
|
laddr = addr(*laddr)
|
||||||
|
if raddr:
|
||||||
|
raddr = addr(*raddr)
|
||||||
|
if type_ == socket.SOCK_STREAM and fam in (AF_INET, AF_INET6):
|
||||||
|
status = status_map.get(status, CONN_NONE)
|
||||||
|
else:
|
||||||
|
status = CONN_NONE # ignore whatever C returned to us
|
||||||
|
fam = sockfam_to_enum(fam)
|
||||||
|
type_ = socktype_to_enum(type_)
|
||||||
|
if pid is None:
|
||||||
|
return pconn(fd, fam, type_, laddr, raddr, status)
|
||||||
|
else:
|
||||||
|
return sconn(fd, fam, type_, laddr, raddr, status, pid)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated_method(replacement):
|
||||||
|
"""A decorator which can be used to mark a method as deprecated
|
||||||
|
'replcement' is the method name which will be called instead.
|
||||||
|
"""
|
||||||
|
def outer(fun):
|
||||||
|
msg = "%s() is deprecated and will be removed; use %s() instead" % (
|
||||||
|
fun.__name__, replacement)
|
||||||
|
if fun.__doc__ is None:
|
||||||
|
fun.__doc__ = msg
|
||||||
|
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
|
||||||
|
return getattr(self, replacement)(*args, **kwargs)
|
||||||
|
return inner
|
||||||
|
return outer
|
||||||
|
|
||||||
|
|
||||||
|
class _WrapNumbers:
|
||||||
|
"""Watches numbers so that they don't overflow and wrap
|
||||||
|
(reset to zero).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.cache = {}
|
||||||
|
self.reminders = {}
|
||||||
|
self.reminder_keys = {}
|
||||||
|
|
||||||
|
def _add_dict(self, input_dict, name):
|
||||||
|
assert name not in self.cache
|
||||||
|
assert name not in self.reminders
|
||||||
|
assert name not in self.reminder_keys
|
||||||
|
self.cache[name] = input_dict
|
||||||
|
self.reminders[name] = collections.defaultdict(int)
|
||||||
|
self.reminder_keys[name] = collections.defaultdict(set)
|
||||||
|
|
||||||
|
def _remove_dead_reminders(self, input_dict, name):
|
||||||
|
"""In case the number of keys changed between calls (e.g. a
|
||||||
|
disk disappears) this removes the entry from self.reminders.
|
||||||
|
"""
|
||||||
|
old_dict = self.cache[name]
|
||||||
|
gone_keys = set(old_dict.keys()) - set(input_dict.keys())
|
||||||
|
for gone_key in gone_keys:
|
||||||
|
for remkey in self.reminder_keys[name][gone_key]:
|
||||||
|
del self.reminders[name][remkey]
|
||||||
|
del self.reminder_keys[name][gone_key]
|
||||||
|
|
||||||
|
def run(self, input_dict, name):
|
||||||
|
"""Cache dict and sum numbers which overflow and wrap.
|
||||||
|
Return an updated copy of `input_dict`
|
||||||
|
"""
|
||||||
|
if name not in self.cache:
|
||||||
|
# This was the first call.
|
||||||
|
self._add_dict(input_dict, name)
|
||||||
|
return input_dict
|
||||||
|
|
||||||
|
self._remove_dead_reminders(input_dict, name)
|
||||||
|
|
||||||
|
old_dict = self.cache[name]
|
||||||
|
new_dict = {}
|
||||||
|
for key in input_dict.keys():
|
||||||
|
input_tuple = input_dict[key]
|
||||||
|
try:
|
||||||
|
old_tuple = old_dict[key]
|
||||||
|
except KeyError:
|
||||||
|
# The input dict has a new key (e.g. a new disk or NIC)
|
||||||
|
# which didn't exist in the previous call.
|
||||||
|
new_dict[key] = input_tuple
|
||||||
|
continue
|
||||||
|
|
||||||
|
bits = []
|
||||||
|
for i in range(len(input_tuple)):
|
||||||
|
input_value = input_tuple[i]
|
||||||
|
old_value = old_tuple[i]
|
||||||
|
remkey = (key, i)
|
||||||
|
if input_value < old_value:
|
||||||
|
# it wrapped!
|
||||||
|
self.reminders[name][remkey] += old_value
|
||||||
|
self.reminder_keys[name][key].add(remkey)
|
||||||
|
bits.append(input_value + self.reminders[name][remkey])
|
||||||
|
|
||||||
|
new_dict[key] = tuple(bits)
|
||||||
|
|
||||||
|
self.cache[name] = input_dict
|
||||||
|
return new_dict
|
||||||
|
|
||||||
|
def cache_clear(self, name=None):
|
||||||
|
"""Clear the internal cache, optionally only for function 'name'."""
|
||||||
|
with self.lock:
|
||||||
|
if name is None:
|
||||||
|
self.cache.clear()
|
||||||
|
self.reminders.clear()
|
||||||
|
self.reminder_keys.clear()
|
||||||
|
else:
|
||||||
|
self.cache.pop(name, None)
|
||||||
|
self.reminders.pop(name, None)
|
||||||
|
self.reminder_keys.pop(name, None)
|
||||||
|
|
||||||
|
def cache_info(self):
|
||||||
|
"""Return internal cache dicts as a tuple of 3 elements."""
|
||||||
|
with self.lock:
|
||||||
|
return (self.cache, self.reminders, self.reminder_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_numbers(input_dict, name):
|
||||||
|
"""Given an `input_dict` and a function `name`, adjust the numbers
|
||||||
|
which "wrap" (restart from zero) across different calls by adding
|
||||||
|
"old value" to "new value" and return an updated dict.
|
||||||
|
"""
|
||||||
|
with _wn.lock:
|
||||||
|
return _wn.run(input_dict, name)
|
||||||
|
|
||||||
|
|
||||||
|
_wn = _WrapNumbers()
|
||||||
|
wrap_numbers.cache_clear = _wn.cache_clear
|
||||||
|
wrap_numbers.cache_info = _wn.cache_info
|
||||||
|
|
||||||
|
|
||||||
|
# The read buffer size for open() builtin. This (also) dictates how
|
||||||
|
# much data we read(2) when iterating over file lines as in:
|
||||||
|
# >>> with open(file) as f:
|
||||||
|
# ... for line in f:
|
||||||
|
# ... ...
|
||||||
|
# Default per-line buffer size for binary files is 1K. For text files
|
||||||
|
# is 8K. We use a bigger buffer (32K) in order to have more consistent
|
||||||
|
# results when reading /proc pseudo files on Linux, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/2050
|
||||||
|
# On Python 2 this also speeds up the reading of big files:
|
||||||
|
# (namely /proc/{pid}/smaps and /proc/net/*):
|
||||||
|
# https://github.com/giampaolo/psutil/issues/708
|
||||||
|
FILE_READ_BUFFER_SIZE = 32 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def open_binary(fname):
|
||||||
|
return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
def open_text(fname):
|
||||||
|
"""On Python 3 opens a file in text mode by using fs encoding and
|
||||||
|
a proper en/decoding errors handler.
|
||||||
|
On Python 2 this is just an alias for open(name, 'rt').
|
||||||
|
"""
|
||||||
|
if not PY3:
|
||||||
|
return open(fname, "rt", buffering=FILE_READ_BUFFER_SIZE)
|
||||||
|
|
||||||
|
# See:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/675
|
||||||
|
# https://github.com/giampaolo/psutil/pull/733
|
||||||
|
fobj = open(fname, "rt", buffering=FILE_READ_BUFFER_SIZE,
|
||||||
|
encoding=ENCODING, errors=ENCODING_ERRS)
|
||||||
|
try:
|
||||||
|
# Dictates per-line read(2) buffer size. Defaults is 8k. See:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546
|
||||||
|
fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
fobj.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
return fobj
|
||||||
|
|
||||||
|
|
||||||
|
def cat(fname, fallback=_DEFAULT, _open=open_text):
|
||||||
|
"""Read entire file content and return it as a string. File is
|
||||||
|
opened in text mode. If specified, `fallback` is the value
|
||||||
|
returned in case of error, either if the file does not exist or
|
||||||
|
it can't be read().
|
||||||
|
"""
|
||||||
|
if fallback is _DEFAULT:
|
||||||
|
with _open(fname) as f:
|
||||||
|
return f.read()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with _open(fname) as f:
|
||||||
|
return f.read()
|
||||||
|
except (IOError, OSError):
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def bcat(fname, fallback=_DEFAULT):
|
||||||
|
"""Same as above but opens file in binary mode."""
|
||||||
|
return cat(fname, fallback=fallback, _open=open_binary)
|
||||||
|
|
||||||
|
|
||||||
|
def bytes2human(n, format="%(value).1f%(symbol)s"):
|
||||||
|
"""Used by various scripts. See:
|
||||||
|
http://goo.gl/zeJZl
|
||||||
|
|
||||||
|
>>> bytes2human(10000)
|
||||||
|
'9.8K'
|
||||||
|
>>> bytes2human(100001221)
|
||||||
|
'95.4M'
|
||||||
|
"""
|
||||||
|
symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
|
||||||
|
prefix = {}
|
||||||
|
for i, s in enumerate(symbols[1:]):
|
||||||
|
prefix[s] = 1 << (i + 1) * 10
|
||||||
|
for symbol in reversed(symbols[1:]):
|
||||||
|
if n >= prefix[symbol]:
|
||||||
|
value = float(n) / prefix[symbol]
|
||||||
|
return format % locals()
|
||||||
|
return format % dict(symbol=symbols[0], value=n)
|
||||||
|
|
||||||
|
|
||||||
|
def get_procfs_path():
|
||||||
|
"""Return updated psutil.PROCFS_PATH constant."""
|
||||||
|
return sys.modules['psutil'].PROCFS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
def decode(s):
|
||||||
|
return s.decode(encoding=ENCODING, errors=ENCODING_ERRS)
|
||||||
|
else:
|
||||||
|
def decode(s):
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- shell utils
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def term_supports_colors(file=sys.stdout): # pragma: no cover
|
||||||
|
if os.name == 'nt':
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
assert file.isatty()
|
||||||
|
curses.setupterm()
|
||||||
|
assert curses.tigetnum("colors") > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def hilite(s, color=None, bold=False): # pragma: no cover
|
||||||
|
"""Return an highlighted version of 'string'."""
|
||||||
|
if not term_supports_colors():
|
||||||
|
return s
|
||||||
|
attr = []
|
||||||
|
colors = dict(green='32', red='91', brown='33', yellow='93', blue='34',
|
||||||
|
violet='35', lightblue='36', grey='37', darkgrey='30')
|
||||||
|
colors[None] = '29'
|
||||||
|
try:
|
||||||
|
color = colors[color]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("invalid color %r; choose between %s" % (
|
||||||
|
list(colors.keys())))
|
||||||
|
attr.append(color)
|
||||||
|
if bold:
|
||||||
|
attr.append('1')
|
||||||
|
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s)
|
||||||
|
|
||||||
|
|
||||||
|
def print_color(
|
||||||
|
s, color=None, bold=False, file=sys.stdout): # pragma: no cover
|
||||||
|
"""Print a colorized version of string."""
|
||||||
|
if not term_supports_colors():
|
||||||
|
print(s, file=file) # NOQA
|
||||||
|
elif POSIX:
|
||||||
|
print(hilite(s, color, bold), file=file) # NOQA
|
||||||
|
else:
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
DEFAULT_COLOR = 7
|
||||||
|
GetStdHandle = ctypes.windll.Kernel32.GetStdHandle
|
||||||
|
SetConsoleTextAttribute = \
|
||||||
|
ctypes.windll.Kernel32.SetConsoleTextAttribute
|
||||||
|
|
||||||
|
colors = dict(green=2, red=4, brown=6, yellow=6)
|
||||||
|
colors[None] = DEFAULT_COLOR
|
||||||
|
try:
|
||||||
|
color = colors[color]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("invalid color %r; choose between %r" % (
|
||||||
|
color, list(colors.keys())))
|
||||||
|
if bold and color <= 7:
|
||||||
|
color += 8
|
||||||
|
|
||||||
|
handle_id = -12 if file is sys.stderr else -11
|
||||||
|
GetStdHandle.restype = ctypes.c_ulong
|
||||||
|
handle = GetStdHandle(handle_id)
|
||||||
|
SetConsoleTextAttribute(handle, color)
|
||||||
|
try:
|
||||||
|
print(s, file=file) # NOQA
|
||||||
|
finally:
|
||||||
|
SetConsoleTextAttribute(handle, DEFAULT_COLOR)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
"""If PSUTIL_DEBUG env var is set, print a debug message to stderr."""
|
||||||
|
if PSUTIL_DEBUG:
|
||||||
|
import inspect
|
||||||
|
fname, lineno, func_name, lines, index = inspect.getframeinfo(
|
||||||
|
inspect.currentframe().f_back)
|
||||||
|
if isinstance(msg, Exception):
|
||||||
|
if isinstance(msg, (OSError, IOError, EnvironmentError)):
|
||||||
|
# ...because str(exc) may contain info about the file name
|
||||||
|
msg = "ignoring %s" % msg
|
||||||
|
else:
|
||||||
|
msg = "ignoring %r" % msg
|
||||||
|
print("psutil-debug [%s:%s]> %s" % (fname, lineno, msg), # NOQA
|
||||||
|
file=sys.stderr)
|
@ -0,0 +1,450 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Module which provides compatibility with older Python versions.
|
||||||
|
This is more future-compatible rather than the opposite (prefer latest
|
||||||
|
Python 3 way of doing things).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"PY3",
|
||||||
|
# builtins
|
||||||
|
"long", "range", "super", "unicode", "basestring",
|
||||||
|
# literals
|
||||||
|
"u", "b",
|
||||||
|
# collections module
|
||||||
|
"lru_cache",
|
||||||
|
# shutil module
|
||||||
|
"which", "get_terminal_size",
|
||||||
|
# contextlib module
|
||||||
|
"redirect_stderr",
|
||||||
|
# python 3 exceptions
|
||||||
|
"FileNotFoundError", "PermissionError", "ProcessLookupError",
|
||||||
|
"InterruptedError", "ChildProcessError", "FileExistsError"]
|
||||||
|
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
_SENTINEL = object()
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
long = int
|
||||||
|
xrange = range
|
||||||
|
unicode = str
|
||||||
|
basestring = str
|
||||||
|
range = range
|
||||||
|
|
||||||
|
def u(s):
|
||||||
|
return s
|
||||||
|
|
||||||
|
def b(s):
|
||||||
|
return s.encode("latin-1")
|
||||||
|
else:
|
||||||
|
long = long
|
||||||
|
range = xrange
|
||||||
|
unicode = unicode
|
||||||
|
basestring = basestring
|
||||||
|
|
||||||
|
def u(s):
|
||||||
|
return unicode(s, "unicode_escape")
|
||||||
|
|
||||||
|
def b(s):
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# --- builtins
|
||||||
|
|
||||||
|
|
||||||
|
# Python 3 super().
|
||||||
|
# Taken from "future" package.
|
||||||
|
# Credit: Ryan Kelly
|
||||||
|
if PY3:
|
||||||
|
super = super
|
||||||
|
else:
|
||||||
|
_builtin_super = super
|
||||||
|
|
||||||
|
def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1):
|
||||||
|
"""Like Python 3 builtin super(). If called without any arguments
|
||||||
|
it attempts to infer them at runtime.
|
||||||
|
"""
|
||||||
|
if type_ is _SENTINEL:
|
||||||
|
f = sys._getframe(framedepth)
|
||||||
|
try:
|
||||||
|
# Get the function's first positional argument.
|
||||||
|
type_or_obj = f.f_locals[f.f_code.co_varnames[0]]
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
raise RuntimeError('super() used in a function with no args')
|
||||||
|
try:
|
||||||
|
# Get the MRO so we can crawl it.
|
||||||
|
mro = type_or_obj.__mro__
|
||||||
|
except (AttributeError, RuntimeError):
|
||||||
|
try:
|
||||||
|
mro = type_or_obj.__class__.__mro__
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError('super() used in a non-newstyle class')
|
||||||
|
for type_ in mro:
|
||||||
|
# Find the class that owns the currently-executing method.
|
||||||
|
for meth in type_.__dict__.values():
|
||||||
|
# Drill down through any wrappers to the underlying func.
|
||||||
|
# This handles e.g. classmethod() and staticmethod().
|
||||||
|
try:
|
||||||
|
while not isinstance(meth, types.FunctionType):
|
||||||
|
if isinstance(meth, property):
|
||||||
|
# Calling __get__ on the property will invoke
|
||||||
|
# user code which might throw exceptions or
|
||||||
|
# have side effects
|
||||||
|
meth = meth.fget
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
meth = meth.__func__
|
||||||
|
except AttributeError:
|
||||||
|
meth = meth.__get__(type_or_obj, type_)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
continue
|
||||||
|
if meth.func_code is f.f_code:
|
||||||
|
break # found
|
||||||
|
else:
|
||||||
|
# Not found. Move onto the next class in MRO.
|
||||||
|
continue
|
||||||
|
break # found
|
||||||
|
else:
|
||||||
|
raise RuntimeError('super() called outside a method')
|
||||||
|
|
||||||
|
# Dispatch to builtin super().
|
||||||
|
if type_or_obj is not _SENTINEL:
|
||||||
|
return _builtin_super(type_, type_or_obj)
|
||||||
|
return _builtin_super(type_)
|
||||||
|
|
||||||
|
|
||||||
|
# --- exceptions
|
||||||
|
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
FileNotFoundError = FileNotFoundError # NOQA
|
||||||
|
PermissionError = PermissionError # NOQA
|
||||||
|
ProcessLookupError = ProcessLookupError # NOQA
|
||||||
|
InterruptedError = InterruptedError # NOQA
|
||||||
|
ChildProcessError = ChildProcessError # NOQA
|
||||||
|
FileExistsError = FileExistsError # NOQA
|
||||||
|
else:
|
||||||
|
# https://github.com/PythonCharmers/python-future/blob/exceptions/
|
||||||
|
# src/future/types/exceptions/pep3151.py
|
||||||
|
import platform
|
||||||
|
|
||||||
|
def _instance_checking_exception(base_exception=Exception):
|
||||||
|
def wrapped(instance_checker):
|
||||||
|
class TemporaryClass(base_exception):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if len(args) == 1 and isinstance(args[0], TemporaryClass):
|
||||||
|
unwrap_me = args[0]
|
||||||
|
for attr in dir(unwrap_me):
|
||||||
|
if not attr.startswith('__'):
|
||||||
|
setattr(self, attr, getattr(unwrap_me, attr))
|
||||||
|
else:
|
||||||
|
super(TemporaryClass, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class __metaclass__(type):
|
||||||
|
def __instancecheck__(cls, inst):
|
||||||
|
return instance_checker(inst)
|
||||||
|
|
||||||
|
def __subclasscheck__(cls, classinfo):
|
||||||
|
value = sys.exc_info()[1]
|
||||||
|
return isinstance(value, cls)
|
||||||
|
|
||||||
|
TemporaryClass.__name__ = instance_checker.__name__
|
||||||
|
TemporaryClass.__doc__ = instance_checker.__doc__
|
||||||
|
return TemporaryClass
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def FileNotFoundError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) == errno.ENOENT
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def ProcessLookupError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) == errno.ESRCH
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def PermissionError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) in (
|
||||||
|
errno.EACCES, errno.EPERM)
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def InterruptedError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) == errno.EINTR
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def ChildProcessError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) == errno.ECHILD
|
||||||
|
|
||||||
|
@_instance_checking_exception(EnvironmentError)
|
||||||
|
def FileExistsError(inst):
|
||||||
|
return getattr(inst, 'errno', _SENTINEL) == errno.EEXIST
|
||||||
|
|
||||||
|
if platform.python_implementation() != "CPython":
|
||||||
|
try:
|
||||||
|
raise OSError(errno.EEXIST, "perm")
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"broken or incompatible Python implementation, see: "
|
||||||
|
"https://github.com/giampaolo/psutil/issues/1659")
|
||||||
|
|
||||||
|
|
||||||
|
# --- stdlib additions
|
||||||
|
|
||||||
|
|
||||||
|
# py 3.2 functools.lru_cache
|
||||||
|
# Taken from: http://code.activestate.com/recipes/578078
|
||||||
|
# Credit: Raymond Hettinger
|
||||||
|
try:
|
||||||
|
from functools import lru_cache
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from threading import RLock
|
||||||
|
except ImportError:
|
||||||
|
from dummy_threading import RLock
|
||||||
|
|
||||||
|
_CacheInfo = collections.namedtuple(
|
||||||
|
"CacheInfo", ["hits", "misses", "maxsize", "currsize"])
|
||||||
|
|
||||||
|
class _HashedSeq(list):
|
||||||
|
__slots__ = 'hashvalue'
|
||||||
|
|
||||||
|
def __init__(self, tup, hash=hash):
|
||||||
|
self[:] = tup
|
||||||
|
self.hashvalue = hash(tup)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.hashvalue
|
||||||
|
|
||||||
|
def _make_key(args, kwds, typed,
|
||||||
|
kwd_mark=(object(), ),
|
||||||
|
fasttypes=set((int, str, frozenset, type(None))),
|
||||||
|
sorted=sorted, tuple=tuple, type=type, len=len):
|
||||||
|
key = args
|
||||||
|
if kwds:
|
||||||
|
sorted_items = sorted(kwds.items())
|
||||||
|
key += kwd_mark
|
||||||
|
for item in sorted_items:
|
||||||
|
key += item
|
||||||
|
if typed:
|
||||||
|
key += tuple(type(v) for v in args)
|
||||||
|
if kwds:
|
||||||
|
key += tuple(type(v) for k, v in sorted_items)
|
||||||
|
elif len(key) == 1 and type(key[0]) in fasttypes:
|
||||||
|
return key[0]
|
||||||
|
return _HashedSeq(key)
|
||||||
|
|
||||||
|
def lru_cache(maxsize=100, typed=False):
|
||||||
|
"""Least-recently-used cache decorator, see:
|
||||||
|
http://docs.python.org/3/library/functools.html#functools.lru_cache
|
||||||
|
"""
|
||||||
|
def decorating_function(user_function):
|
||||||
|
cache = dict()
|
||||||
|
stats = [0, 0]
|
||||||
|
HITS, MISSES = 0, 1
|
||||||
|
make_key = _make_key
|
||||||
|
cache_get = cache.get
|
||||||
|
_len = len
|
||||||
|
lock = RLock()
|
||||||
|
root = []
|
||||||
|
root[:] = [root, root, None, None]
|
||||||
|
nonlocal_root = [root]
|
||||||
|
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3
|
||||||
|
if maxsize == 0:
|
||||||
|
def wrapper(*args, **kwds):
|
||||||
|
result = user_function(*args, **kwds)
|
||||||
|
stats[MISSES] += 1
|
||||||
|
return result
|
||||||
|
elif maxsize is None:
|
||||||
|
def wrapper(*args, **kwds):
|
||||||
|
key = make_key(args, kwds, typed)
|
||||||
|
result = cache_get(key, root)
|
||||||
|
if result is not root:
|
||||||
|
stats[HITS] += 1
|
||||||
|
return result
|
||||||
|
result = user_function(*args, **kwds)
|
||||||
|
cache[key] = result
|
||||||
|
stats[MISSES] += 1
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
def wrapper(*args, **kwds):
|
||||||
|
if kwds or typed:
|
||||||
|
key = make_key(args, kwds, typed)
|
||||||
|
else:
|
||||||
|
key = args
|
||||||
|
lock.acquire()
|
||||||
|
try:
|
||||||
|
link = cache_get(key)
|
||||||
|
if link is not None:
|
||||||
|
root, = nonlocal_root
|
||||||
|
link_prev, link_next, key, result = link
|
||||||
|
link_prev[NEXT] = link_next
|
||||||
|
link_next[PREV] = link_prev
|
||||||
|
last = root[PREV]
|
||||||
|
last[NEXT] = root[PREV] = link
|
||||||
|
link[PREV] = last
|
||||||
|
link[NEXT] = root
|
||||||
|
stats[HITS] += 1
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
result = user_function(*args, **kwds)
|
||||||
|
lock.acquire()
|
||||||
|
try:
|
||||||
|
root, = nonlocal_root
|
||||||
|
if key in cache:
|
||||||
|
pass
|
||||||
|
elif _len(cache) >= maxsize:
|
||||||
|
oldroot = root
|
||||||
|
oldroot[KEY] = key
|
||||||
|
oldroot[RESULT] = result
|
||||||
|
root = nonlocal_root[0] = oldroot[NEXT]
|
||||||
|
oldkey = root[KEY]
|
||||||
|
root[KEY] = root[RESULT] = None
|
||||||
|
del cache[oldkey]
|
||||||
|
cache[key] = oldroot
|
||||||
|
else:
|
||||||
|
last = root[PREV]
|
||||||
|
link = [last, root, key, result]
|
||||||
|
last[NEXT] = root[PREV] = cache[key] = link
|
||||||
|
stats[MISSES] += 1
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def cache_info():
|
||||||
|
"""Report cache statistics"""
|
||||||
|
lock.acquire()
|
||||||
|
try:
|
||||||
|
return _CacheInfo(stats[HITS], stats[MISSES], maxsize,
|
||||||
|
len(cache))
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
|
||||||
|
def cache_clear():
|
||||||
|
"""Clear the cache and cache statistics"""
|
||||||
|
lock.acquire()
|
||||||
|
try:
|
||||||
|
cache.clear()
|
||||||
|
root = nonlocal_root[0]
|
||||||
|
root[:] = [root, root, None, None]
|
||||||
|
stats[:] = [0, 0]
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
|
||||||
|
wrapper.__wrapped__ = user_function
|
||||||
|
wrapper.cache_info = cache_info
|
||||||
|
wrapper.cache_clear = cache_clear
|
||||||
|
return functools.update_wrapper(wrapper, user_function)
|
||||||
|
|
||||||
|
return decorating_function
|
||||||
|
|
||||||
|
|
||||||
|
# python 3.3
|
||||||
|
try:
|
||||||
|
from shutil import which
|
||||||
|
except ImportError:
|
||||||
|
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||||
|
"""Given a command, mode, and a PATH string, return the path which
|
||||||
|
conforms to the given mode on the PATH, or None if there is no such
|
||||||
|
file.
|
||||||
|
|
||||||
|
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
|
||||||
|
of os.environ.get("PATH"), or can be overridden with a custom search
|
||||||
|
path.
|
||||||
|
"""
|
||||||
|
def _access_check(fn, mode):
|
||||||
|
return (os.path.exists(fn) and os.access(fn, mode) and
|
||||||
|
not os.path.isdir(fn))
|
||||||
|
|
||||||
|
if os.path.dirname(cmd):
|
||||||
|
if _access_check(cmd, mode):
|
||||||
|
return cmd
|
||||||
|
return None
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
path = os.environ.get("PATH", os.defpath)
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
path = path.split(os.pathsep)
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
if os.curdir not in path:
|
||||||
|
path.insert(0, os.curdir)
|
||||||
|
|
||||||
|
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
|
||||||
|
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
||||||
|
files = [cmd]
|
||||||
|
else:
|
||||||
|
files = [cmd + ext for ext in pathext]
|
||||||
|
else:
|
||||||
|
files = [cmd]
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for dir in path:
|
||||||
|
normdir = os.path.normcase(dir)
|
||||||
|
if normdir not in seen:
|
||||||
|
seen.add(normdir)
|
||||||
|
for thefile in files:
|
||||||
|
name = os.path.join(dir, thefile)
|
||||||
|
if _access_check(name, mode):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# python 3.3
|
||||||
|
try:
|
||||||
|
from shutil import get_terminal_size
|
||||||
|
except ImportError:
|
||||||
|
def get_terminal_size(fallback=(80, 24)):
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
import struct
|
||||||
|
import termios
|
||||||
|
except ImportError:
|
||||||
|
return fallback
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# This should work on Linux.
|
||||||
|
res = struct.unpack(
|
||||||
|
'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234'))
|
||||||
|
return (res[1], res[0])
|
||||||
|
except Exception:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
# python 3.3
|
||||||
|
try:
|
||||||
|
from subprocess import TimeoutExpired as SubprocessTimeoutExpired
|
||||||
|
except ImportError:
|
||||||
|
class SubprocessTimeoutExpired:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# python 3.5
|
||||||
|
try:
|
||||||
|
from contextlib import redirect_stderr
|
||||||
|
except ImportError:
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def redirect_stderr(new_target):
|
||||||
|
original = getattr(sys, "stderr")
|
||||||
|
try:
|
||||||
|
setattr(sys, "stderr", new_target)
|
||||||
|
yield new_target
|
||||||
|
finally:
|
||||||
|
setattr(sys, "stderr", original)
|
@ -0,0 +1,552 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'
|
||||||
|
# Copyright (c) 2017, Arnon Yaari
|
||||||
|
# All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""AIX platform implementation."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from . import _common
|
||||||
|
from . import _psposix
|
||||||
|
from . import _psutil_aix as cext
|
||||||
|
from . import _psutil_posix as cext_posix
|
||||||
|
from ._common import NIC_DUPLEX_FULL
|
||||||
|
from ._common import NIC_DUPLEX_HALF
|
||||||
|
from ._common import NIC_DUPLEX_UNKNOWN
|
||||||
|
from ._common import AccessDenied
|
||||||
|
from ._common import NoSuchProcess
|
||||||
|
from ._common import ZombieProcess
|
||||||
|
from ._common import conn_to_ntuple
|
||||||
|
from ._common import get_procfs_path
|
||||||
|
from ._common import memoize_when_activated
|
||||||
|
from ._common import usage_percent
|
||||||
|
from ._compat import PY3
|
||||||
|
from ._compat import FileNotFoundError
|
||||||
|
from ._compat import PermissionError
|
||||||
|
from ._compat import ProcessLookupError
|
||||||
|
|
||||||
|
|
||||||
|
__extra__all__ = ["PROCFS_PATH"]
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- globals
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
HAS_THREADS = hasattr(cext, "proc_threads")
|
||||||
|
HAS_NET_IO_COUNTERS = hasattr(cext, "net_io_counters")
|
||||||
|
HAS_PROC_IO_COUNTERS = hasattr(cext, "proc_io_counters")
|
||||||
|
|
||||||
|
PAGE_SIZE = cext_posix.getpagesize()
|
||||||
|
AF_LINK = cext_posix.AF_LINK
|
||||||
|
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SACTIVE: _common.STATUS_RUNNING,
|
||||||
|
cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this?
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
}
|
||||||
|
|
||||||
|
TCP_STATUSES = {
|
||||||
|
cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED,
|
||||||
|
cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT,
|
||||||
|
cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV,
|
||||||
|
cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1,
|
||||||
|
cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2,
|
||||||
|
cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT,
|
||||||
|
cext.TCPS_CLOSED: _common.CONN_CLOSE,
|
||||||
|
cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT,
|
||||||
|
cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK,
|
||||||
|
cext.TCPS_LISTEN: _common.CONN_LISTEN,
|
||||||
|
cext.TCPS_CLOSING: _common.CONN_CLOSING,
|
||||||
|
cext.PSUTIL_CONN_NONE: _common.CONN_NONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
proc_info_map = dict(
|
||||||
|
ppid=0,
|
||||||
|
rss=1,
|
||||||
|
vms=2,
|
||||||
|
create_time=3,
|
||||||
|
nice=4,
|
||||||
|
num_threads=5,
|
||||||
|
status=6,
|
||||||
|
ttynr=7)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- named tuples
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# psutil.Process.memory_info()
|
||||||
|
pmem = namedtuple('pmem', ['rss', 'vms'])
|
||||||
|
# psutil.Process.memory_full_info()
|
||||||
|
pfullmem = pmem
|
||||||
|
# psutil.Process.cpu_times()
|
||||||
|
scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait'])
|
||||||
|
# psutil.virtual_memory()
|
||||||
|
svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free'])
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- memory
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def virtual_memory():
|
||||||
|
total, avail, free, pinned, inuse = cext.virtual_mem()
|
||||||
|
percent = usage_percent((total - avail), total, round_=1)
|
||||||
|
return svmem(total, avail, percent, inuse, free)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_memory():
|
||||||
|
"""Swap system memory as a (total, used, free, sin, sout) tuple."""
|
||||||
|
total, free, sin, sout = cext.swap_mem()
|
||||||
|
used = total - free
|
||||||
|
percent = usage_percent(used, total, round_=1)
|
||||||
|
return _common.sswap(total, used, free, percent, sin, sout)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- CPU
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_times():
|
||||||
|
"""Return system-wide CPU times as a named tuple"""
|
||||||
|
ret = cext.per_cpu_times()
|
||||||
|
return scputimes(*[sum(x) for x in zip(*ret)])
|
||||||
|
|
||||||
|
|
||||||
|
def per_cpu_times():
|
||||||
|
"""Return system per-CPU times as a list of named tuples"""
|
||||||
|
ret = cext.per_cpu_times()
|
||||||
|
return [scputimes(*x) for x in ret]
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_logical():
|
||||||
|
"""Return the number of logical CPUs in the system."""
|
||||||
|
try:
|
||||||
|
return os.sysconf("SC_NPROCESSORS_ONLN")
|
||||||
|
except ValueError:
|
||||||
|
# mimic os.cpu_count() behavior
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_cores():
|
||||||
|
cmd = "lsdev -Cc processor"
|
||||||
|
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if PY3:
|
||||||
|
stdout, stderr = [x.decode(sys.stdout.encoding)
|
||||||
|
for x in (stdout, stderr)]
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise RuntimeError("%r command error\n%s" % (cmd, stderr))
|
||||||
|
processors = stdout.strip().splitlines()
|
||||||
|
return len(processors) or None
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_stats():
|
||||||
|
"""Return various CPU stats as a named tuple."""
|
||||||
|
ctx_switches, interrupts, soft_interrupts, syscalls = cext.cpu_stats()
|
||||||
|
return _common.scpustats(
|
||||||
|
ctx_switches, interrupts, soft_interrupts, syscalls)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- disks
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
disk_io_counters = cext.disk_io_counters
|
||||||
|
disk_usage = _psposix.disk_usage
|
||||||
|
|
||||||
|
|
||||||
|
def disk_partitions(all=False):
|
||||||
|
"""Return system disk partitions."""
|
||||||
|
# TODO - the filtering logic should be better checked so that
|
||||||
|
# it tries to reflect 'df' as much as possible
|
||||||
|
retlist = []
|
||||||
|
partitions = cext.disk_partitions()
|
||||||
|
for partition in partitions:
|
||||||
|
device, mountpoint, fstype, opts = partition
|
||||||
|
if device == 'none':
|
||||||
|
device = ''
|
||||||
|
if not all:
|
||||||
|
# Differently from, say, Linux, we don't have a list of
|
||||||
|
# common fs types so the best we can do, AFAIK, is to
|
||||||
|
# filter by filesystem having a total size > 0.
|
||||||
|
if not disk_usage(mountpoint).total:
|
||||||
|
continue
|
||||||
|
maxfile = maxpath = None # set later
|
||||||
|
ntuple = _common.sdiskpart(device, mountpoint, fstype, opts,
|
||||||
|
maxfile, maxpath)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- network
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
net_if_addrs = cext_posix.net_if_addrs
|
||||||
|
|
||||||
|
if HAS_NET_IO_COUNTERS:
|
||||||
|
net_io_counters = cext.net_io_counters
|
||||||
|
|
||||||
|
|
||||||
|
def net_connections(kind, _pid=-1):
|
||||||
|
"""Return socket connections. If pid == -1 return system-wide
|
||||||
|
connections (as opposed to connections opened by one process only).
|
||||||
|
"""
|
||||||
|
cmap = _common.conn_tmap
|
||||||
|
if kind not in cmap:
|
||||||
|
raise ValueError("invalid %r kind argument; choose between %s"
|
||||||
|
% (kind, ', '.join([repr(x) for x in cmap])))
|
||||||
|
families, types = _common.conn_tmap[kind]
|
||||||
|
rawlist = cext.net_connections(_pid)
|
||||||
|
ret = []
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type_, laddr, raddr, status, pid = item
|
||||||
|
if fam not in families:
|
||||||
|
continue
|
||||||
|
if type_ not in types:
|
||||||
|
continue
|
||||||
|
nt = conn_to_ntuple(fd, fam, type_, laddr, raddr, status,
|
||||||
|
TCP_STATUSES, pid=pid if _pid == -1 else None)
|
||||||
|
ret.append(nt)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def net_if_stats():
|
||||||
|
"""Get NIC stats (isup, duplex, speed, mtu)."""
|
||||||
|
duplex_map = {"Full": NIC_DUPLEX_FULL,
|
||||||
|
"Half": NIC_DUPLEX_HALF}
|
||||||
|
names = set([x[0] for x in net_if_addrs()])
|
||||||
|
ret = {}
|
||||||
|
for name in names:
|
||||||
|
isup, mtu = cext.net_if_stats(name)
|
||||||
|
|
||||||
|
# try to get speed and duplex
|
||||||
|
# TODO: rewrite this in C (entstat forks, so use truss -f to follow.
|
||||||
|
# looks like it is using an undocumented ioctl?)
|
||||||
|
duplex = ""
|
||||||
|
speed = 0
|
||||||
|
p = subprocess.Popen(["/usr/bin/entstat", "-d", name],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if PY3:
|
||||||
|
stdout, stderr = [x.decode(sys.stdout.encoding)
|
||||||
|
for x in (stdout, stderr)]
|
||||||
|
if p.returncode == 0:
|
||||||
|
re_result = re.search(
|
||||||
|
r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout)
|
||||||
|
if re_result is not None:
|
||||||
|
speed = int(re_result.group(1))
|
||||||
|
duplex = re_result.group(2)
|
||||||
|
|
||||||
|
duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN)
|
||||||
|
ret[name] = _common.snicstats(isup, duplex, speed, mtu)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- other system functions
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def boot_time():
|
||||||
|
"""The system boot time expressed in seconds since the epoch."""
|
||||||
|
return cext.boot_time()
|
||||||
|
|
||||||
|
|
||||||
|
def users():
|
||||||
|
"""Return currently connected users as a list of namedtuples."""
|
||||||
|
retlist = []
|
||||||
|
rawlist = cext.users()
|
||||||
|
localhost = (':0.0', ':0')
|
||||||
|
for item in rawlist:
|
||||||
|
user, tty, hostname, tstamp, user_process, pid = item
|
||||||
|
# note: the underlying C function includes entries about
|
||||||
|
# system boot, run level and others. We might want
|
||||||
|
# to use them in the future.
|
||||||
|
if not user_process:
|
||||||
|
continue
|
||||||
|
if hostname in localhost:
|
||||||
|
hostname = 'localhost'
|
||||||
|
nt = _common.suser(user, tty, hostname, tstamp, pid)
|
||||||
|
retlist.append(nt)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- processes
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def pids():
|
||||||
|
"""Returns a list of PIDs currently running on the system."""
|
||||||
|
return [int(x) for x in os.listdir(get_procfs_path()) if x.isdigit()]
|
||||||
|
|
||||||
|
|
||||||
|
def pid_exists(pid):
|
||||||
|
"""Check for the existence of a unix pid."""
|
||||||
|
return os.path.exists(os.path.join(get_procfs_path(), str(pid), "psinfo"))
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exceptions(fun):
|
||||||
|
"""Call callable into a try/except clause and translate ENOENT,
|
||||||
|
EACCES and EPERM in NoSuchProcess or AccessDenied exceptions.
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
except (FileNotFoundError, ProcessLookupError):
|
||||||
|
# ENOENT (no such file or directory) gets raised on open().
|
||||||
|
# ESRCH (no such process) can get raised on read() if
|
||||||
|
# process is gone in meantime.
|
||||||
|
if not pid_exists(self.pid):
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
else:
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
except PermissionError:
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Process(object):
|
||||||
|
"""Wrapper class around underlying C implementation."""
|
||||||
|
|
||||||
|
__slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"]
|
||||||
|
|
||||||
|
def __init__(self, pid):
|
||||||
|
self.pid = pid
|
||||||
|
self._name = None
|
||||||
|
self._ppid = None
|
||||||
|
self._procfs_path = get_procfs_path()
|
||||||
|
|
||||||
|
def oneshot_enter(self):
|
||||||
|
self._proc_basic_info.cache_activate(self)
|
||||||
|
self._proc_cred.cache_activate(self)
|
||||||
|
|
||||||
|
def oneshot_exit(self):
|
||||||
|
self._proc_basic_info.cache_deactivate(self)
|
||||||
|
self._proc_cred.cache_deactivate(self)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _proc_basic_info(self):
|
||||||
|
return cext.proc_basic_info(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _proc_cred(self):
|
||||||
|
return cext.proc_cred(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def name(self):
|
||||||
|
if self.pid == 0:
|
||||||
|
return "swapper"
|
||||||
|
# note: max 16 characters
|
||||||
|
return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00")
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def exe(self):
|
||||||
|
# there is no way to get executable path in AIX other than to guess,
|
||||||
|
# and guessing is more complex than what's in the wrapping class
|
||||||
|
cmdline = self.cmdline()
|
||||||
|
if not cmdline:
|
||||||
|
return ''
|
||||||
|
exe = cmdline[0]
|
||||||
|
if os.path.sep in exe:
|
||||||
|
# relative or absolute path
|
||||||
|
if not os.path.isabs(exe):
|
||||||
|
# if cwd has changed, we're out of luck - this may be wrong!
|
||||||
|
exe = os.path.abspath(os.path.join(self.cwd(), exe))
|
||||||
|
if (os.path.isabs(exe) and
|
||||||
|
os.path.isfile(exe) and
|
||||||
|
os.access(exe, os.X_OK)):
|
||||||
|
return exe
|
||||||
|
# not found, move to search in PATH using basename only
|
||||||
|
exe = os.path.basename(exe)
|
||||||
|
# search for exe name PATH
|
||||||
|
for path in os.environ["PATH"].split(":"):
|
||||||
|
possible_exe = os.path.abspath(os.path.join(path, exe))
|
||||||
|
if (os.path.isfile(possible_exe) and
|
||||||
|
os.access(possible_exe, os.X_OK)):
|
||||||
|
return possible_exe
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cmdline(self):
|
||||||
|
return cext.proc_args(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def environ(self):
|
||||||
|
return cext.proc_environ(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def create_time(self):
|
||||||
|
return self._proc_basic_info()[proc_info_map['create_time']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_threads(self):
|
||||||
|
return self._proc_basic_info()[proc_info_map['num_threads']]
|
||||||
|
|
||||||
|
if HAS_THREADS:
|
||||||
|
@wrap_exceptions
|
||||||
|
def threads(self):
|
||||||
|
rawlist = cext.proc_threads(self.pid)
|
||||||
|
retlist = []
|
||||||
|
for thread_id, utime, stime in rawlist:
|
||||||
|
ntuple = _common.pthread(thread_id, utime, stime)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
# The underlying C implementation retrieves all OS threads
|
||||||
|
# and filters them by PID. At this point we can't tell whether
|
||||||
|
# an empty list means there were no connections for process or
|
||||||
|
# process is no longer active so we force NSP in case the PID
|
||||||
|
# is no longer there.
|
||||||
|
if not retlist:
|
||||||
|
# will raise NSP if process is gone
|
||||||
|
os.stat('%s/%s' % (self._procfs_path, self.pid))
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def connections(self, kind='inet'):
|
||||||
|
ret = net_connections(kind, _pid=self.pid)
|
||||||
|
# The underlying C implementation retrieves all OS connections
|
||||||
|
# and filters them by PID. At this point we can't tell whether
|
||||||
|
# an empty list means there were no connections for process or
|
||||||
|
# process is no longer active so we force NSP in case the PID
|
||||||
|
# is no longer there.
|
||||||
|
if not ret:
|
||||||
|
# will raise NSP if process is gone
|
||||||
|
os.stat('%s/%s' % (self._procfs_path, self.pid))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_get(self):
|
||||||
|
return cext_posix.getpriority(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_set(self, value):
|
||||||
|
return cext_posix.setpriority(self.pid, value)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def ppid(self):
|
||||||
|
self._ppid = self._proc_basic_info()[proc_info_map['ppid']]
|
||||||
|
return self._ppid
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def uids(self):
|
||||||
|
real, effective, saved, _, _, _ = self._proc_cred()
|
||||||
|
return _common.puids(real, effective, saved)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def gids(self):
|
||||||
|
_, _, _, real, effective, saved = self._proc_cred()
|
||||||
|
return _common.puids(real, effective, saved)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_times(self):
|
||||||
|
cpu_times = cext.proc_cpu_times(self.pid, self._procfs_path)
|
||||||
|
return _common.pcputimes(*cpu_times)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def terminal(self):
|
||||||
|
ttydev = self._proc_basic_info()[proc_info_map['ttynr']]
|
||||||
|
# convert from 64-bit dev_t to 32-bit dev_t and then map the device
|
||||||
|
ttydev = (((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF))
|
||||||
|
# try to match rdev of /dev/pts/* files ttydev
|
||||||
|
for dev in glob.glob("/dev/**/*"):
|
||||||
|
if os.stat(dev).st_rdev == ttydev:
|
||||||
|
return dev
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cwd(self):
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
try:
|
||||||
|
result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid))
|
||||||
|
return result.rstrip('/')
|
||||||
|
except FileNotFoundError:
|
||||||
|
os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_info(self):
|
||||||
|
ret = self._proc_basic_info()
|
||||||
|
rss = ret[proc_info_map['rss']] * 1024
|
||||||
|
vms = ret[proc_info_map['vms']] * 1024
|
||||||
|
return pmem(rss, vms)
|
||||||
|
|
||||||
|
memory_full_info = memory_info
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def status(self):
|
||||||
|
code = self._proc_basic_info()[proc_info_map['status']]
|
||||||
|
# XXX is '?' legit? (we're not supposed to return it anyway)
|
||||||
|
return PROC_STATUSES.get(code, '?')
|
||||||
|
|
||||||
|
def open_files(self):
|
||||||
|
# TODO rewrite without using procfiles (stat /proc/pid/fd/* and then
|
||||||
|
# find matching name of the inode)
|
||||||
|
p = subprocess.Popen(["/usr/bin/procfiles", "-n", str(self.pid)],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if PY3:
|
||||||
|
stdout, stderr = [x.decode(sys.stdout.encoding)
|
||||||
|
for x in (stdout, stderr)]
|
||||||
|
if "no such process" in stderr.lower():
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout)
|
||||||
|
retlist = []
|
||||||
|
for fd, path in procfiles:
|
||||||
|
path = path.strip()
|
||||||
|
if path.startswith("//"):
|
||||||
|
path = path[1:]
|
||||||
|
if path.lower() == "cannot be retrieved":
|
||||||
|
continue
|
||||||
|
retlist.append(_common.popenfile(path, int(fd)))
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_fds(self):
|
||||||
|
if self.pid == 0: # no /proc/0/fd
|
||||||
|
return 0
|
||||||
|
return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_ctx_switches(self):
|
||||||
|
return _common.pctxsw(
|
||||||
|
*cext.proc_num_ctx_switches(self.pid))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return _psposix.wait_pid(self.pid, timeout, self._name)
|
||||||
|
|
||||||
|
if HAS_PROC_IO_COUNTERS:
|
||||||
|
@wrap_exceptions
|
||||||
|
def io_counters(self):
|
||||||
|
try:
|
||||||
|
rc, wc, rb, wb = cext.proc_io_counters(self.pid)
|
||||||
|
except OSError:
|
||||||
|
# if process is terminated, proc_io_counters returns OSError
|
||||||
|
# instead of NSP
|
||||||
|
if not pid_exists(self.pid):
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
raise
|
||||||
|
return _common.pio(rc, wc, rb, wb)
|
@ -0,0 +1,922 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""FreeBSD, OpenBSD and NetBSD platforms implementation."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from . import _common
|
||||||
|
from . import _psposix
|
||||||
|
from . import _psutil_bsd as cext
|
||||||
|
from . import _psutil_posix as cext_posix
|
||||||
|
from ._common import FREEBSD
|
||||||
|
from ._common import NETBSD
|
||||||
|
from ._common import OPENBSD
|
||||||
|
from ._common import AccessDenied
|
||||||
|
from ._common import NoSuchProcess
|
||||||
|
from ._common import ZombieProcess
|
||||||
|
from ._common import conn_tmap
|
||||||
|
from ._common import conn_to_ntuple
|
||||||
|
from ._common import memoize
|
||||||
|
from ._common import memoize_when_activated
|
||||||
|
from ._common import usage_percent
|
||||||
|
from ._compat import FileNotFoundError
|
||||||
|
from ._compat import PermissionError
|
||||||
|
from ._compat import ProcessLookupError
|
||||||
|
from ._compat import which
|
||||||
|
|
||||||
|
|
||||||
|
__extra__all__ = []
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- globals
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
if FREEBSD:
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SRUN: _common.STATUS_RUNNING,
|
||||||
|
cext.SSLEEP: _common.STATUS_SLEEPING,
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SWAIT: _common.STATUS_WAITING,
|
||||||
|
cext.SLOCK: _common.STATUS_LOCKED,
|
||||||
|
}
|
||||||
|
elif OPENBSD:
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SSLEEP: _common.STATUS_SLEEPING,
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
# According to /usr/include/sys/proc.h SZOMB is unused.
|
||||||
|
# test_zombie_process() shows that SDEAD is the right
|
||||||
|
# equivalent. Also it appears there's no equivalent of
|
||||||
|
# psutil.STATUS_DEAD. SDEAD really means STATUS_ZOMBIE.
|
||||||
|
# cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SDEAD: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
# From http://www.eecs.harvard.edu/~margo/cs161/videos/proc.h.txt
|
||||||
|
# OpenBSD has SRUN and SONPROC: SRUN indicates that a process
|
||||||
|
# is runnable but *not* yet running, i.e. is on a run queue.
|
||||||
|
# SONPROC indicates that the process is actually executing on
|
||||||
|
# a CPU, i.e. it is no longer on a run queue.
|
||||||
|
# As such we'll map SRUN to STATUS_WAKING and SONPROC to
|
||||||
|
# STATUS_RUNNING
|
||||||
|
cext.SRUN: _common.STATUS_WAKING,
|
||||||
|
cext.SONPROC: _common.STATUS_RUNNING,
|
||||||
|
}
|
||||||
|
elif NETBSD:
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SSLEEP: _common.STATUS_SLEEPING,
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SRUN: _common.STATUS_WAKING,
|
||||||
|
cext.SONPROC: _common.STATUS_RUNNING,
|
||||||
|
}
|
||||||
|
|
||||||
|
TCP_STATUSES = {
|
||||||
|
cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED,
|
||||||
|
cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT,
|
||||||
|
cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV,
|
||||||
|
cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1,
|
||||||
|
cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2,
|
||||||
|
cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT,
|
||||||
|
cext.TCPS_CLOSED: _common.CONN_CLOSE,
|
||||||
|
cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT,
|
||||||
|
cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK,
|
||||||
|
cext.TCPS_LISTEN: _common.CONN_LISTEN,
|
||||||
|
cext.TCPS_CLOSING: _common.CONN_CLOSING,
|
||||||
|
cext.PSUTIL_CONN_NONE: _common.CONN_NONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
PAGESIZE = cext_posix.getpagesize()
|
||||||
|
AF_LINK = cext_posix.AF_LINK
|
||||||
|
|
||||||
|
HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times")
|
||||||
|
HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads")
|
||||||
|
HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files')
|
||||||
|
HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds')
|
||||||
|
|
||||||
|
kinfo_proc_map = dict(
|
||||||
|
ppid=0,
|
||||||
|
status=1,
|
||||||
|
real_uid=2,
|
||||||
|
effective_uid=3,
|
||||||
|
saved_uid=4,
|
||||||
|
real_gid=5,
|
||||||
|
effective_gid=6,
|
||||||
|
saved_gid=7,
|
||||||
|
ttynr=8,
|
||||||
|
create_time=9,
|
||||||
|
ctx_switches_vol=10,
|
||||||
|
ctx_switches_unvol=11,
|
||||||
|
read_io_count=12,
|
||||||
|
write_io_count=13,
|
||||||
|
user_time=14,
|
||||||
|
sys_time=15,
|
||||||
|
ch_user_time=16,
|
||||||
|
ch_sys_time=17,
|
||||||
|
rss=18,
|
||||||
|
vms=19,
|
||||||
|
memtext=20,
|
||||||
|
memdata=21,
|
||||||
|
memstack=22,
|
||||||
|
cpunum=23,
|
||||||
|
name=24,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- named tuples
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# psutil.virtual_memory()
|
||||||
|
svmem = namedtuple(
|
||||||
|
'svmem', ['total', 'available', 'percent', 'used', 'free',
|
||||||
|
'active', 'inactive', 'buffers', 'cached', 'shared', 'wired'])
|
||||||
|
# psutil.cpu_times()
|
||||||
|
scputimes = namedtuple(
|
||||||
|
'scputimes', ['user', 'nice', 'system', 'idle', 'irq'])
|
||||||
|
# psutil.Process.memory_info()
|
||||||
|
pmem = namedtuple('pmem', ['rss', 'vms', 'text', 'data', 'stack'])
|
||||||
|
# psutil.Process.memory_full_info()
|
||||||
|
pfullmem = pmem
|
||||||
|
# psutil.Process.cpu_times()
|
||||||
|
pcputimes = namedtuple('pcputimes',
|
||||||
|
['user', 'system', 'children_user', 'children_system'])
|
||||||
|
# psutil.Process.memory_maps(grouped=True)
|
||||||
|
pmmap_grouped = namedtuple(
|
||||||
|
'pmmap_grouped', 'path rss, private, ref_count, shadow_count')
|
||||||
|
# psutil.Process.memory_maps(grouped=False)
|
||||||
|
pmmap_ext = namedtuple(
|
||||||
|
'pmmap_ext', 'addr, perms path rss, private, ref_count, shadow_count')
|
||||||
|
# psutil.disk_io_counters()
|
||||||
|
if FREEBSD:
|
||||||
|
sdiskio = namedtuple('sdiskio', ['read_count', 'write_count',
|
||||||
|
'read_bytes', 'write_bytes',
|
||||||
|
'read_time', 'write_time',
|
||||||
|
'busy_time'])
|
||||||
|
else:
|
||||||
|
sdiskio = namedtuple('sdiskio', ['read_count', 'write_count',
|
||||||
|
'read_bytes', 'write_bytes'])
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- memory
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def virtual_memory():
|
||||||
|
"""System virtual memory as a namedtuple."""
|
||||||
|
mem = cext.virtual_mem()
|
||||||
|
total, free, active, inactive, wired, cached, buffers, shared = mem
|
||||||
|
if NETBSD:
|
||||||
|
# On NetBSD buffers and shared mem is determined via /proc.
|
||||||
|
# The C ext set them to 0.
|
||||||
|
with open('/proc/meminfo', 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(b'Buffers:'):
|
||||||
|
buffers = int(line.split()[1]) * 1024
|
||||||
|
elif line.startswith(b'MemShared:'):
|
||||||
|
shared = int(line.split()[1]) * 1024
|
||||||
|
avail = inactive + cached + free
|
||||||
|
used = active + wired + cached
|
||||||
|
percent = usage_percent((total - avail), total, round_=1)
|
||||||
|
return svmem(total, avail, percent, used, free,
|
||||||
|
active, inactive, buffers, cached, shared, wired)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_memory():
|
||||||
|
"""System swap memory as (total, used, free, sin, sout) namedtuple."""
|
||||||
|
total, used, free, sin, sout = cext.swap_mem()
|
||||||
|
percent = usage_percent(used, total, round_=1)
|
||||||
|
return _common.sswap(total, used, free, percent, sin, sout)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- CPU
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_times():
|
||||||
|
"""Return system per-CPU times as a namedtuple"""
|
||||||
|
user, nice, system, idle, irq = cext.cpu_times()
|
||||||
|
return scputimes(user, nice, system, idle, irq)
|
||||||
|
|
||||||
|
|
||||||
|
if HAS_PER_CPU_TIMES:
|
||||||
|
def per_cpu_times():
|
||||||
|
"""Return system CPU times as a namedtuple"""
|
||||||
|
ret = []
|
||||||
|
for cpu_t in cext.per_cpu_times():
|
||||||
|
user, nice, system, idle, irq = cpu_t
|
||||||
|
item = scputimes(user, nice, system, idle, irq)
|
||||||
|
ret.append(item)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
# XXX
|
||||||
|
# Ok, this is very dirty.
|
||||||
|
# On FreeBSD < 8 we cannot gather per-cpu information, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/226
|
||||||
|
# If num cpus > 1, on first call we return single cpu times to avoid a
|
||||||
|
# crash at psutil import time.
|
||||||
|
# Next calls will fail with NotImplementedError
|
||||||
|
def per_cpu_times():
|
||||||
|
"""Return system CPU times as a namedtuple"""
|
||||||
|
if cpu_count_logical() == 1:
|
||||||
|
return [cpu_times()]
|
||||||
|
if per_cpu_times.__called__:
|
||||||
|
raise NotImplementedError("supported only starting from FreeBSD 8")
|
||||||
|
per_cpu_times.__called__ = True
|
||||||
|
return [cpu_times()]
|
||||||
|
|
||||||
|
per_cpu_times.__called__ = False
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_logical():
|
||||||
|
"""Return the number of logical CPUs in the system."""
|
||||||
|
return cext.cpu_count_logical()
|
||||||
|
|
||||||
|
|
||||||
|
if OPENBSD or NETBSD:
|
||||||
|
def cpu_count_cores():
|
||||||
|
# OpenBSD and NetBSD do not implement this.
|
||||||
|
return 1 if cpu_count_logical() == 1 else None
|
||||||
|
else:
|
||||||
|
def cpu_count_cores():
|
||||||
|
"""Return the number of CPU cores in the system."""
|
||||||
|
# From the C module we'll get an XML string similar to this:
|
||||||
|
# http://manpages.ubuntu.com/manpages/precise/man4/smp.4freebsd.html
|
||||||
|
# We may get None in case "sysctl kern.sched.topology_spec"
|
||||||
|
# is not supported on this BSD version, in which case we'll mimic
|
||||||
|
# os.cpu_count() and return None.
|
||||||
|
ret = None
|
||||||
|
s = cext.cpu_topology()
|
||||||
|
if s is not None:
|
||||||
|
# get rid of padding chars appended at the end of the string
|
||||||
|
index = s.rfind("</groups>")
|
||||||
|
if index != -1:
|
||||||
|
s = s[:index + 9]
|
||||||
|
root = ET.fromstring(s)
|
||||||
|
try:
|
||||||
|
ret = len(root.findall('group/children/group/cpu')) or None
|
||||||
|
finally:
|
||||||
|
# needed otherwise it will memleak
|
||||||
|
root.clear()
|
||||||
|
if not ret:
|
||||||
|
# If logical CPUs == 1 it's obvious we' have only 1 core.
|
||||||
|
if cpu_count_logical() == 1:
|
||||||
|
return 1
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_stats():
|
||||||
|
"""Return various CPU stats as a named tuple."""
|
||||||
|
if FREEBSD:
|
||||||
|
# Note: the C ext is returning some metrics we are not exposing:
|
||||||
|
# traps.
|
||||||
|
ctxsw, intrs, soft_intrs, syscalls, traps = cext.cpu_stats()
|
||||||
|
elif NETBSD:
|
||||||
|
# XXX
|
||||||
|
# Note about intrs: the C extension returns 0. intrs
|
||||||
|
# can be determined via /proc/stat; it has the same value as
|
||||||
|
# soft_intrs thought so the kernel is faking it (?).
|
||||||
|
#
|
||||||
|
# Note about syscalls: the C extension always sets it to 0 (?).
|
||||||
|
#
|
||||||
|
# Note: the C ext is returning some metrics we are not exposing:
|
||||||
|
# traps, faults and forks.
|
||||||
|
ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \
|
||||||
|
cext.cpu_stats()
|
||||||
|
with open('/proc/stat', 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(b'intr'):
|
||||||
|
intrs = int(line.split()[1])
|
||||||
|
elif OPENBSD:
|
||||||
|
# Note: the C ext is returning some metrics we are not exposing:
|
||||||
|
# traps, faults and forks.
|
||||||
|
ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \
|
||||||
|
cext.cpu_stats()
|
||||||
|
return _common.scpustats(ctxsw, intrs, soft_intrs, syscalls)
|
||||||
|
|
||||||
|
|
||||||
|
if FREEBSD:
|
||||||
|
def cpu_freq():
|
||||||
|
"""Return frequency metrics for CPUs. As of Dec 2018 only
|
||||||
|
CPU 0 appears to be supported by FreeBSD and all other cores
|
||||||
|
match the frequency of CPU 0.
|
||||||
|
"""
|
||||||
|
ret = []
|
||||||
|
num_cpus = cpu_count_logical()
|
||||||
|
for cpu in range(num_cpus):
|
||||||
|
try:
|
||||||
|
current, available_freq = cext.cpu_freq(cpu)
|
||||||
|
except NotImplementedError:
|
||||||
|
continue
|
||||||
|
if available_freq:
|
||||||
|
try:
|
||||||
|
min_freq = int(available_freq.split(" ")[-1].split("/")[0])
|
||||||
|
except(IndexError, ValueError):
|
||||||
|
min_freq = None
|
||||||
|
try:
|
||||||
|
max_freq = int(available_freq.split(" ")[0].split("/")[0])
|
||||||
|
except(IndexError, ValueError):
|
||||||
|
max_freq = None
|
||||||
|
ret.append(_common.scpufreq(current, min_freq, max_freq))
|
||||||
|
return ret
|
||||||
|
elif OPENBSD:
|
||||||
|
def cpu_freq():
|
||||||
|
curr = float(cext.cpu_freq())
|
||||||
|
return [_common.scpufreq(curr, 0.0, 0.0)]
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- disks
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def disk_partitions(all=False):
|
||||||
|
"""Return mounted disk partitions as a list of namedtuples.
|
||||||
|
'all' argument is ignored, see:
|
||||||
|
https://github.com/giampaolo/psutil/issues/906
|
||||||
|
"""
|
||||||
|
retlist = []
|
||||||
|
partitions = cext.disk_partitions()
|
||||||
|
for partition in partitions:
|
||||||
|
device, mountpoint, fstype, opts = partition
|
||||||
|
maxfile = maxpath = None # set later
|
||||||
|
ntuple = _common.sdiskpart(device, mountpoint, fstype, opts,
|
||||||
|
maxfile, maxpath)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
disk_usage = _psposix.disk_usage
|
||||||
|
disk_io_counters = cext.disk_io_counters
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- network
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
net_io_counters = cext.net_io_counters
|
||||||
|
net_if_addrs = cext_posix.net_if_addrs
|
||||||
|
|
||||||
|
|
||||||
|
def net_if_stats():
|
||||||
|
"""Get NIC stats (isup, duplex, speed, mtu)."""
|
||||||
|
names = net_io_counters().keys()
|
||||||
|
ret = {}
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
mtu = cext_posix.net_if_mtu(name)
|
||||||
|
isup = cext_posix.net_if_is_running(name)
|
||||||
|
duplex, speed = cext_posix.net_if_duplex_speed(name)
|
||||||
|
except OSError as err:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1279
|
||||||
|
if err.errno != errno.ENODEV:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
if hasattr(_common, 'NicDuplex'):
|
||||||
|
duplex = _common.NicDuplex(duplex)
|
||||||
|
ret[name] = _common.snicstats(isup, duplex, speed, mtu)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def net_connections(kind):
|
||||||
|
"""System-wide network connections."""
|
||||||
|
if OPENBSD:
|
||||||
|
ret = []
|
||||||
|
for pid in pids():
|
||||||
|
try:
|
||||||
|
cons = Process(pid).connections(kind)
|
||||||
|
except (NoSuchProcess, ZombieProcess):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for conn in cons:
|
||||||
|
conn = list(conn)
|
||||||
|
conn.append(pid)
|
||||||
|
ret.append(_common.sconn(*conn))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if kind not in _common.conn_tmap:
|
||||||
|
raise ValueError("invalid %r kind argument; choose between %s"
|
||||||
|
% (kind, ', '.join([repr(x) for x in conn_tmap])))
|
||||||
|
families, types = conn_tmap[kind]
|
||||||
|
ret = set()
|
||||||
|
if NETBSD:
|
||||||
|
rawlist = cext.net_connections(-1)
|
||||||
|
else:
|
||||||
|
rawlist = cext.net_connections()
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type, laddr, raddr, status, pid = item
|
||||||
|
# TODO: apply filter at C level
|
||||||
|
if fam in families and type in types:
|
||||||
|
nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status,
|
||||||
|
TCP_STATUSES, pid)
|
||||||
|
ret.add(nt)
|
||||||
|
return list(ret)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- sensors
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
if FREEBSD:
|
||||||
|
|
||||||
|
def sensors_battery():
|
||||||
|
"""Return battery info."""
|
||||||
|
try:
|
||||||
|
percent, minsleft, power_plugged = cext.sensors_battery()
|
||||||
|
except NotImplementedError:
|
||||||
|
# See: https://github.com/giampaolo/psutil/issues/1074
|
||||||
|
return None
|
||||||
|
power_plugged = power_plugged == 1
|
||||||
|
if power_plugged:
|
||||||
|
secsleft = _common.POWER_TIME_UNLIMITED
|
||||||
|
elif minsleft == -1:
|
||||||
|
secsleft = _common.POWER_TIME_UNKNOWN
|
||||||
|
else:
|
||||||
|
secsleft = minsleft * 60
|
||||||
|
return _common.sbattery(percent, secsleft, power_plugged)
|
||||||
|
|
||||||
|
def sensors_temperatures():
|
||||||
|
"Return CPU cores temperatures if available, else an empty dict."
|
||||||
|
ret = defaultdict(list)
|
||||||
|
num_cpus = cpu_count_logical()
|
||||||
|
for cpu in range(num_cpus):
|
||||||
|
try:
|
||||||
|
current, high = cext.sensors_cpu_temperature(cpu)
|
||||||
|
if high <= 0:
|
||||||
|
high = None
|
||||||
|
name = "Core %s" % cpu
|
||||||
|
ret["coretemp"].append(
|
||||||
|
_common.shwtemp(name, current, high, high))
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- other system functions
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def boot_time():
|
||||||
|
"""The system boot time expressed in seconds since the epoch."""
|
||||||
|
return cext.boot_time()
|
||||||
|
|
||||||
|
|
||||||
|
def users():
|
||||||
|
"""Return currently connected users as a list of namedtuples."""
|
||||||
|
retlist = []
|
||||||
|
rawlist = cext.users()
|
||||||
|
for item in rawlist:
|
||||||
|
user, tty, hostname, tstamp, pid = item
|
||||||
|
if pid == -1:
|
||||||
|
assert OPENBSD
|
||||||
|
pid = None
|
||||||
|
if tty == '~':
|
||||||
|
continue # reboot or shutdown
|
||||||
|
nt = _common.suser(user, tty or None, hostname, tstamp, pid)
|
||||||
|
retlist.append(nt)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- processes
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def _pid_0_exists():
|
||||||
|
try:
|
||||||
|
Process(0).name()
|
||||||
|
except NoSuchProcess:
|
||||||
|
return False
|
||||||
|
except AccessDenied:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def pids():
|
||||||
|
"""Returns a list of PIDs currently running on the system."""
|
||||||
|
ret = cext.pids()
|
||||||
|
if OPENBSD and (0 not in ret) and _pid_0_exists():
|
||||||
|
# On OpenBSD the kernel does not return PID 0 (neither does
|
||||||
|
# ps) but it's actually querable (Process(0) will succeed).
|
||||||
|
ret.insert(0, 0)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
if OPENBSD or NETBSD:
|
||||||
|
def pid_exists(pid):
|
||||||
|
"""Return True if pid exists."""
|
||||||
|
exists = _psposix.pid_exists(pid)
|
||||||
|
if not exists:
|
||||||
|
# We do this because _psposix.pid_exists() lies in case of
|
||||||
|
# zombie processes.
|
||||||
|
return pid in pids()
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
pid_exists = _psposix.pid_exists
|
||||||
|
|
||||||
|
|
||||||
|
def is_zombie(pid):
|
||||||
|
try:
|
||||||
|
st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']]
|
||||||
|
return st == cext.SZOMB
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exceptions(fun):
|
||||||
|
"""Decorator which translates bare OSError exceptions into
|
||||||
|
NoSuchProcess and AccessDenied.
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
except ProcessLookupError:
|
||||||
|
if is_zombie(self.pid):
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
else:
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
except PermissionError:
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
except OSError:
|
||||||
|
if self.pid == 0:
|
||||||
|
if 0 in pids():
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def wrap_exceptions_procfs(inst):
|
||||||
|
"""Same as above, for routines relying on reading /proc fs."""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except (ProcessLookupError, FileNotFoundError):
|
||||||
|
# ENOENT (no such file or directory) gets raised on open().
|
||||||
|
# ESRCH (no such process) can get raised on read() if
|
||||||
|
# process is gone in meantime.
|
||||||
|
if is_zombie(inst.pid):
|
||||||
|
raise ZombieProcess(inst.pid, inst._name, inst._ppid)
|
||||||
|
else:
|
||||||
|
raise NoSuchProcess(inst.pid, inst._name)
|
||||||
|
except PermissionError:
|
||||||
|
raise AccessDenied(inst.pid, inst._name)
|
||||||
|
|
||||||
|
|
||||||
|
class Process(object):
|
||||||
|
"""Wrapper class around underlying C implementation."""
|
||||||
|
|
||||||
|
__slots__ = ["pid", "_name", "_ppid", "_cache"]
|
||||||
|
|
||||||
|
def __init__(self, pid):
|
||||||
|
self.pid = pid
|
||||||
|
self._name = None
|
||||||
|
self._ppid = None
|
||||||
|
|
||||||
|
def _assert_alive(self):
|
||||||
|
"""Raise NSP if the process disappeared on us."""
|
||||||
|
# For those C function who do not raise NSP, possibly returning
|
||||||
|
# incorrect or incomplete result.
|
||||||
|
cext.proc_name(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def oneshot(self):
|
||||||
|
"""Retrieves multiple process info in one shot as a raw tuple."""
|
||||||
|
ret = cext.proc_oneshot_info(self.pid)
|
||||||
|
assert len(ret) == len(kinfo_proc_map)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def oneshot_enter(self):
|
||||||
|
self.oneshot.cache_activate(self)
|
||||||
|
|
||||||
|
def oneshot_exit(self):
|
||||||
|
self.oneshot.cache_deactivate(self)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def name(self):
|
||||||
|
name = self.oneshot()[kinfo_proc_map['name']]
|
||||||
|
return name if name is not None else cext.proc_name(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def exe(self):
|
||||||
|
if FREEBSD:
|
||||||
|
if self.pid == 0:
|
||||||
|
return '' # else NSP
|
||||||
|
return cext.proc_exe(self.pid)
|
||||||
|
elif NETBSD:
|
||||||
|
if self.pid == 0:
|
||||||
|
# /proc/0 dir exists but /proc/0/exe doesn't
|
||||||
|
return ""
|
||||||
|
with wrap_exceptions_procfs(self):
|
||||||
|
return os.readlink("/proc/%s/exe" % self.pid)
|
||||||
|
else:
|
||||||
|
# OpenBSD: exe cannot be determined; references:
|
||||||
|
# https://chromium.googlesource.com/chromium/src/base/+/
|
||||||
|
# master/base_paths_posix.cc
|
||||||
|
# We try our best guess by using which against the first
|
||||||
|
# cmdline arg (may return None).
|
||||||
|
cmdline = self.cmdline()
|
||||||
|
if cmdline:
|
||||||
|
return which(cmdline[0]) or ""
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cmdline(self):
|
||||||
|
if OPENBSD and self.pid == 0:
|
||||||
|
return [] # ...else it crashes
|
||||||
|
elif NETBSD:
|
||||||
|
# XXX - most of the times the underlying sysctl() call on Net
|
||||||
|
# and Open BSD returns a truncated string.
|
||||||
|
# Also /proc/pid/cmdline behaves the same so it looks
|
||||||
|
# like this is a kernel bug.
|
||||||
|
try:
|
||||||
|
return cext.proc_cmdline(self.pid)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EINVAL:
|
||||||
|
if is_zombie(self.pid):
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
elif not pid_exists(self.pid):
|
||||||
|
raise NoSuchProcess(self.pid, self._name, self._ppid)
|
||||||
|
else:
|
||||||
|
# XXX: this happens with unicode tests. It means the C
|
||||||
|
# routine is unable to decode invalid unicode chars.
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return cext.proc_cmdline(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def environ(self):
|
||||||
|
return cext.proc_environ(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def terminal(self):
|
||||||
|
tty_nr = self.oneshot()[kinfo_proc_map['ttynr']]
|
||||||
|
tmap = _psposix.get_terminal_map()
|
||||||
|
try:
|
||||||
|
return tmap[tty_nr]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def ppid(self):
|
||||||
|
self._ppid = self.oneshot()[kinfo_proc_map['ppid']]
|
||||||
|
return self._ppid
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def uids(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return _common.puids(
|
||||||
|
rawtuple[kinfo_proc_map['real_uid']],
|
||||||
|
rawtuple[kinfo_proc_map['effective_uid']],
|
||||||
|
rawtuple[kinfo_proc_map['saved_uid']])
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def gids(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return _common.pgids(
|
||||||
|
rawtuple[kinfo_proc_map['real_gid']],
|
||||||
|
rawtuple[kinfo_proc_map['effective_gid']],
|
||||||
|
rawtuple[kinfo_proc_map['saved_gid']])
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_times(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return _common.pcputimes(
|
||||||
|
rawtuple[kinfo_proc_map['user_time']],
|
||||||
|
rawtuple[kinfo_proc_map['sys_time']],
|
||||||
|
rawtuple[kinfo_proc_map['ch_user_time']],
|
||||||
|
rawtuple[kinfo_proc_map['ch_sys_time']])
|
||||||
|
|
||||||
|
if FREEBSD:
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_num(self):
|
||||||
|
return self.oneshot()[kinfo_proc_map['cpunum']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_info(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return pmem(
|
||||||
|
rawtuple[kinfo_proc_map['rss']],
|
||||||
|
rawtuple[kinfo_proc_map['vms']],
|
||||||
|
rawtuple[kinfo_proc_map['memtext']],
|
||||||
|
rawtuple[kinfo_proc_map['memdata']],
|
||||||
|
rawtuple[kinfo_proc_map['memstack']])
|
||||||
|
|
||||||
|
memory_full_info = memory_info
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def create_time(self):
|
||||||
|
return self.oneshot()[kinfo_proc_map['create_time']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_threads(self):
|
||||||
|
if HAS_PROC_NUM_THREADS:
|
||||||
|
# FreeBSD
|
||||||
|
return cext.proc_num_threads(self.pid)
|
||||||
|
else:
|
||||||
|
return len(self.threads())
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_ctx_switches(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return _common.pctxsw(
|
||||||
|
rawtuple[kinfo_proc_map['ctx_switches_vol']],
|
||||||
|
rawtuple[kinfo_proc_map['ctx_switches_unvol']])
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def threads(self):
|
||||||
|
# Note: on OpenSBD this (/dev/mem) requires root access.
|
||||||
|
rawlist = cext.proc_threads(self.pid)
|
||||||
|
retlist = []
|
||||||
|
for thread_id, utime, stime in rawlist:
|
||||||
|
ntuple = _common.pthread(thread_id, utime, stime)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
if OPENBSD:
|
||||||
|
self._assert_alive()
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def connections(self, kind='inet'):
|
||||||
|
if kind not in conn_tmap:
|
||||||
|
raise ValueError("invalid %r kind argument; choose between %s"
|
||||||
|
% (kind, ', '.join([repr(x) for x in conn_tmap])))
|
||||||
|
|
||||||
|
if NETBSD:
|
||||||
|
families, types = conn_tmap[kind]
|
||||||
|
ret = []
|
||||||
|
rawlist = cext.net_connections(self.pid)
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type, laddr, raddr, status, pid = item
|
||||||
|
assert pid == self.pid
|
||||||
|
if fam in families and type in types:
|
||||||
|
nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status,
|
||||||
|
TCP_STATUSES)
|
||||||
|
ret.append(nt)
|
||||||
|
self._assert_alive()
|
||||||
|
return list(ret)
|
||||||
|
|
||||||
|
families, types = conn_tmap[kind]
|
||||||
|
rawlist = cext.proc_connections(self.pid, families, types)
|
||||||
|
ret = []
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type, laddr, raddr, status = item
|
||||||
|
nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status,
|
||||||
|
TCP_STATUSES)
|
||||||
|
ret.append(nt)
|
||||||
|
|
||||||
|
if OPENBSD:
|
||||||
|
self._assert_alive()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return _psposix.wait_pid(self.pid, timeout, self._name)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_get(self):
|
||||||
|
return cext_posix.getpriority(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_set(self, value):
|
||||||
|
return cext_posix.setpriority(self.pid, value)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def status(self):
|
||||||
|
code = self.oneshot()[kinfo_proc_map['status']]
|
||||||
|
# XXX is '?' legit? (we're not supposed to return it anyway)
|
||||||
|
return PROC_STATUSES.get(code, '?')
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def io_counters(self):
|
||||||
|
rawtuple = self.oneshot()
|
||||||
|
return _common.pio(
|
||||||
|
rawtuple[kinfo_proc_map['read_io_count']],
|
||||||
|
rawtuple[kinfo_proc_map['write_io_count']],
|
||||||
|
-1,
|
||||||
|
-1)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cwd(self):
|
||||||
|
"""Return process current working directory."""
|
||||||
|
# sometimes we get an empty string, in which case we turn
|
||||||
|
# it into None
|
||||||
|
if OPENBSD and self.pid == 0:
|
||||||
|
return None # ...else it would raise EINVAL
|
||||||
|
elif NETBSD or HAS_PROC_OPEN_FILES:
|
||||||
|
# FreeBSD < 8 does not support functions based on
|
||||||
|
# kinfo_getfile() and kinfo_getvmmap()
|
||||||
|
return cext.proc_cwd(self.pid) or None
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"supported only starting from FreeBSD 8" if
|
||||||
|
FREEBSD else "")
|
||||||
|
|
||||||
|
nt_mmap_grouped = namedtuple(
|
||||||
|
'mmap', 'path rss, private, ref_count, shadow_count')
|
||||||
|
nt_mmap_ext = namedtuple(
|
||||||
|
'mmap', 'addr, perms path rss, private, ref_count, shadow_count')
|
||||||
|
|
||||||
|
def _not_implemented(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# FreeBSD < 8 does not support functions based on kinfo_getfile()
|
||||||
|
# and kinfo_getvmmap()
|
||||||
|
if HAS_PROC_OPEN_FILES:
|
||||||
|
@wrap_exceptions
|
||||||
|
def open_files(self):
|
||||||
|
"""Return files opened by process as a list of namedtuples."""
|
||||||
|
rawlist = cext.proc_open_files(self.pid)
|
||||||
|
return [_common.popenfile(path, fd) for path, fd in rawlist]
|
||||||
|
else:
|
||||||
|
open_files = _not_implemented
|
||||||
|
|
||||||
|
# FreeBSD < 8 does not support functions based on kinfo_getfile()
|
||||||
|
# and kinfo_getvmmap()
|
||||||
|
if HAS_PROC_NUM_FDS:
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_fds(self):
|
||||||
|
"""Return the number of file descriptors opened by this process."""
|
||||||
|
ret = cext.proc_num_fds(self.pid)
|
||||||
|
if NETBSD:
|
||||||
|
self._assert_alive()
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
num_fds = _not_implemented
|
||||||
|
|
||||||
|
# --- FreeBSD only APIs
|
||||||
|
|
||||||
|
if FREEBSD:
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_affinity_get(self):
|
||||||
|
return cext.proc_cpu_affinity_get(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_affinity_set(self, cpus):
|
||||||
|
# Pre-emptively check if CPUs are valid because the C
|
||||||
|
# function has a weird behavior in case of invalid CPUs,
|
||||||
|
# see: https://github.com/giampaolo/psutil/issues/586
|
||||||
|
allcpus = tuple(range(len(per_cpu_times())))
|
||||||
|
for cpu in cpus:
|
||||||
|
if cpu not in allcpus:
|
||||||
|
raise ValueError("invalid CPU #%i (choose between %s)"
|
||||||
|
% (cpu, allcpus))
|
||||||
|
try:
|
||||||
|
cext.proc_cpu_affinity_set(self.pid, cpus)
|
||||||
|
except OSError as err:
|
||||||
|
# 'man cpuset_setaffinity' about EDEADLK:
|
||||||
|
# <<the call would leave a thread without a valid CPU to run
|
||||||
|
# on because the set does not overlap with the thread's
|
||||||
|
# anonymous mask>>
|
||||||
|
if err.errno in (errno.EINVAL, errno.EDEADLK):
|
||||||
|
for cpu in cpus:
|
||||||
|
if cpu not in allcpus:
|
||||||
|
raise ValueError(
|
||||||
|
"invalid CPU #%i (choose between %s)" % (
|
||||||
|
cpu, allcpus))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_maps(self):
|
||||||
|
return cext.proc_memory_maps(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def rlimit(self, resource, limits=None):
|
||||||
|
if limits is None:
|
||||||
|
return cext.proc_getrlimit(self.pid, resource)
|
||||||
|
else:
|
||||||
|
if len(limits) != 2:
|
||||||
|
raise ValueError(
|
||||||
|
"second argument must be a (soft, hard) tuple, "
|
||||||
|
"got %s" % repr(limits))
|
||||||
|
soft, hard = limits
|
||||||
|
return cext.proc_setrlimit(self.pid, resource, soft, hard)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,540 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""macOS platform implementation."""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from . import _common
|
||||||
|
from . import _psposix
|
||||||
|
from . import _psutil_osx as cext
|
||||||
|
from . import _psutil_posix as cext_posix
|
||||||
|
from ._common import AccessDenied
|
||||||
|
from ._common import NoSuchProcess
|
||||||
|
from ._common import ZombieProcess
|
||||||
|
from ._common import conn_tmap
|
||||||
|
from ._common import conn_to_ntuple
|
||||||
|
from ._common import isfile_strict
|
||||||
|
from ._common import memoize_when_activated
|
||||||
|
from ._common import parse_environ_block
|
||||||
|
from ._common import usage_percent
|
||||||
|
from ._compat import PermissionError
|
||||||
|
from ._compat import ProcessLookupError
|
||||||
|
|
||||||
|
|
||||||
|
__extra__all__ = []
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- globals
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
PAGESIZE = cext_posix.getpagesize()
|
||||||
|
AF_LINK = cext_posix.AF_LINK
|
||||||
|
|
||||||
|
TCP_STATUSES = {
|
||||||
|
cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED,
|
||||||
|
cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT,
|
||||||
|
cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV,
|
||||||
|
cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1,
|
||||||
|
cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2,
|
||||||
|
cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT,
|
||||||
|
cext.TCPS_CLOSED: _common.CONN_CLOSE,
|
||||||
|
cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT,
|
||||||
|
cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK,
|
||||||
|
cext.TCPS_LISTEN: _common.CONN_LISTEN,
|
||||||
|
cext.TCPS_CLOSING: _common.CONN_CLOSING,
|
||||||
|
cext.PSUTIL_CONN_NONE: _common.CONN_NONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SRUN: _common.STATUS_RUNNING,
|
||||||
|
cext.SSLEEP: _common.STATUS_SLEEPING,
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
}
|
||||||
|
|
||||||
|
kinfo_proc_map = dict(
|
||||||
|
ppid=0,
|
||||||
|
ruid=1,
|
||||||
|
euid=2,
|
||||||
|
suid=3,
|
||||||
|
rgid=4,
|
||||||
|
egid=5,
|
||||||
|
sgid=6,
|
||||||
|
ttynr=7,
|
||||||
|
ctime=8,
|
||||||
|
status=9,
|
||||||
|
name=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
pidtaskinfo_map = dict(
|
||||||
|
cpuutime=0,
|
||||||
|
cpustime=1,
|
||||||
|
rss=2,
|
||||||
|
vms=3,
|
||||||
|
pfaults=4,
|
||||||
|
pageins=5,
|
||||||
|
numthreads=6,
|
||||||
|
volctxsw=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- named tuples
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# psutil.cpu_times()
|
||||||
|
scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle'])
|
||||||
|
# psutil.virtual_memory()
|
||||||
|
svmem = namedtuple(
|
||||||
|
'svmem', ['total', 'available', 'percent', 'used', 'free',
|
||||||
|
'active', 'inactive', 'wired'])
|
||||||
|
# psutil.Process.memory_info()
|
||||||
|
pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins'])
|
||||||
|
# psutil.Process.memory_full_info()
|
||||||
|
pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', ))
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- memory
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def virtual_memory():
|
||||||
|
"""System virtual memory as a namedtuple."""
|
||||||
|
total, active, inactive, wired, free, speculative = cext.virtual_mem()
|
||||||
|
# This is how Zabbix calculate avail and used mem:
|
||||||
|
# https://github.com/zabbix/zabbix/blob/trunk/src/libs/zbxsysinfo/
|
||||||
|
# osx/memory.c
|
||||||
|
# Also see: https://github.com/giampaolo/psutil/issues/1277
|
||||||
|
avail = inactive + free
|
||||||
|
used = active + wired
|
||||||
|
# This is NOT how Zabbix calculates free mem but it matches "free"
|
||||||
|
# cmdline utility.
|
||||||
|
free -= speculative
|
||||||
|
percent = usage_percent((total - avail), total, round_=1)
|
||||||
|
return svmem(total, avail, percent, used, free,
|
||||||
|
active, inactive, wired)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_memory():
|
||||||
|
"""Swap system memory as a (total, used, free, sin, sout) tuple."""
|
||||||
|
total, used, free, sin, sout = cext.swap_mem()
|
||||||
|
percent = usage_percent(used, total, round_=1)
|
||||||
|
return _common.sswap(total, used, free, percent, sin, sout)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- CPU
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_times():
|
||||||
|
"""Return system CPU times as a namedtuple."""
|
||||||
|
user, nice, system, idle = cext.cpu_times()
|
||||||
|
return scputimes(user, nice, system, idle)
|
||||||
|
|
||||||
|
|
||||||
|
def per_cpu_times():
|
||||||
|
"""Return system CPU times as a named tuple"""
|
||||||
|
ret = []
|
||||||
|
for cpu_t in cext.per_cpu_times():
|
||||||
|
user, nice, system, idle = cpu_t
|
||||||
|
item = scputimes(user, nice, system, idle)
|
||||||
|
ret.append(item)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_logical():
|
||||||
|
"""Return the number of logical CPUs in the system."""
|
||||||
|
return cext.cpu_count_logical()
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_cores():
|
||||||
|
"""Return the number of CPU cores in the system."""
|
||||||
|
return cext.cpu_count_cores()
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_stats():
|
||||||
|
ctx_switches, interrupts, soft_interrupts, syscalls, traps = \
|
||||||
|
cext.cpu_stats()
|
||||||
|
return _common.scpustats(
|
||||||
|
ctx_switches, interrupts, soft_interrupts, syscalls)
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_freq():
|
||||||
|
"""Return CPU frequency.
|
||||||
|
On macOS per-cpu frequency is not supported.
|
||||||
|
Also, the returned frequency never changes, see:
|
||||||
|
https://arstechnica.com/civis/viewtopic.php?f=19&t=465002
|
||||||
|
"""
|
||||||
|
curr, min_, max_ = cext.cpu_freq()
|
||||||
|
return [_common.scpufreq(curr, min_, max_)]
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- disks
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
disk_usage = _psposix.disk_usage
|
||||||
|
disk_io_counters = cext.disk_io_counters
|
||||||
|
|
||||||
|
|
||||||
|
def disk_partitions(all=False):
|
||||||
|
"""Return mounted disk partitions as a list of namedtuples."""
|
||||||
|
retlist = []
|
||||||
|
partitions = cext.disk_partitions()
|
||||||
|
for partition in partitions:
|
||||||
|
device, mountpoint, fstype, opts = partition
|
||||||
|
if device == 'none':
|
||||||
|
device = ''
|
||||||
|
if not all:
|
||||||
|
if not os.path.isabs(device) or not os.path.exists(device):
|
||||||
|
continue
|
||||||
|
maxfile = maxpath = None # set later
|
||||||
|
ntuple = _common.sdiskpart(device, mountpoint, fstype, opts,
|
||||||
|
maxfile, maxpath)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- sensors
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def sensors_battery():
|
||||||
|
"""Return battery information."""
|
||||||
|
try:
|
||||||
|
percent, minsleft, power_plugged = cext.sensors_battery()
|
||||||
|
except NotImplementedError:
|
||||||
|
# no power source - return None according to interface
|
||||||
|
return None
|
||||||
|
power_plugged = power_plugged == 1
|
||||||
|
if power_plugged:
|
||||||
|
secsleft = _common.POWER_TIME_UNLIMITED
|
||||||
|
elif minsleft == -1:
|
||||||
|
secsleft = _common.POWER_TIME_UNKNOWN
|
||||||
|
else:
|
||||||
|
secsleft = minsleft * 60
|
||||||
|
return _common.sbattery(percent, secsleft, power_plugged)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- network
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
net_io_counters = cext.net_io_counters
|
||||||
|
net_if_addrs = cext_posix.net_if_addrs
|
||||||
|
|
||||||
|
|
||||||
|
def net_connections(kind='inet'):
|
||||||
|
"""System-wide network connections."""
|
||||||
|
# Note: on macOS this will fail with AccessDenied unless
|
||||||
|
# the process is owned by root.
|
||||||
|
ret = []
|
||||||
|
for pid in pids():
|
||||||
|
try:
|
||||||
|
cons = Process(pid).connections(kind)
|
||||||
|
except NoSuchProcess:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if cons:
|
||||||
|
for c in cons:
|
||||||
|
c = list(c) + [pid]
|
||||||
|
ret.append(_common.sconn(*c))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def net_if_stats():
|
||||||
|
"""Get NIC stats (isup, duplex, speed, mtu)."""
|
||||||
|
names = net_io_counters().keys()
|
||||||
|
ret = {}
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
mtu = cext_posix.net_if_mtu(name)
|
||||||
|
isup = cext_posix.net_if_is_running(name)
|
||||||
|
duplex, speed = cext_posix.net_if_duplex_speed(name)
|
||||||
|
except OSError as err:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1279
|
||||||
|
if err.errno != errno.ENODEV:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
if hasattr(_common, 'NicDuplex'):
|
||||||
|
duplex = _common.NicDuplex(duplex)
|
||||||
|
ret[name] = _common.snicstats(isup, duplex, speed, mtu)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- other system functions
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def boot_time():
|
||||||
|
"""The system boot time expressed in seconds since the epoch."""
|
||||||
|
return cext.boot_time()
|
||||||
|
|
||||||
|
|
||||||
|
def users():
|
||||||
|
"""Return currently connected users as a list of namedtuples."""
|
||||||
|
retlist = []
|
||||||
|
rawlist = cext.users()
|
||||||
|
for item in rawlist:
|
||||||
|
user, tty, hostname, tstamp, pid = item
|
||||||
|
if tty == '~':
|
||||||
|
continue # reboot or shutdown
|
||||||
|
if not tstamp:
|
||||||
|
continue
|
||||||
|
nt = _common.suser(user, tty or None, hostname or None, tstamp, pid)
|
||||||
|
retlist.append(nt)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- processes
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def pids():
|
||||||
|
ls = cext.pids()
|
||||||
|
if 0 not in ls:
|
||||||
|
# On certain macOS versions pids() C doesn't return PID 0 but
|
||||||
|
# "ps" does and the process is querable via sysctl():
|
||||||
|
# https://travis-ci.org/giampaolo/psutil/jobs/309619941
|
||||||
|
try:
|
||||||
|
Process(0).create_time()
|
||||||
|
ls.insert(0, 0)
|
||||||
|
except NoSuchProcess:
|
||||||
|
pass
|
||||||
|
except AccessDenied:
|
||||||
|
ls.insert(0, 0)
|
||||||
|
return ls
|
||||||
|
|
||||||
|
|
||||||
|
pid_exists = _psposix.pid_exists
|
||||||
|
|
||||||
|
|
||||||
|
def is_zombie(pid):
|
||||||
|
try:
|
||||||
|
st = cext.proc_kinfo_oneshot(pid)[kinfo_proc_map['status']]
|
||||||
|
return st == cext.SZOMB
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exceptions(fun):
|
||||||
|
"""Decorator which translates bare OSError exceptions into
|
||||||
|
NoSuchProcess and AccessDenied.
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
except ProcessLookupError:
|
||||||
|
if is_zombie(self.pid):
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
else:
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
except PermissionError:
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
except cext.ZombieProcessError:
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Process(object):
|
||||||
|
"""Wrapper class around underlying C implementation."""
|
||||||
|
|
||||||
|
__slots__ = ["pid", "_name", "_ppid", "_cache"]
|
||||||
|
|
||||||
|
def __init__(self, pid):
|
||||||
|
self.pid = pid
|
||||||
|
self._name = None
|
||||||
|
self._ppid = None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _get_kinfo_proc(self):
|
||||||
|
# Note: should work with all PIDs without permission issues.
|
||||||
|
ret = cext.proc_kinfo_oneshot(self.pid)
|
||||||
|
assert len(ret) == len(kinfo_proc_map)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _get_pidtaskinfo(self):
|
||||||
|
# Note: should work for PIDs owned by user only.
|
||||||
|
ret = cext.proc_pidtaskinfo_oneshot(self.pid)
|
||||||
|
assert len(ret) == len(pidtaskinfo_map)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def oneshot_enter(self):
|
||||||
|
self._get_kinfo_proc.cache_activate(self)
|
||||||
|
self._get_pidtaskinfo.cache_activate(self)
|
||||||
|
|
||||||
|
def oneshot_exit(self):
|
||||||
|
self._get_kinfo_proc.cache_deactivate(self)
|
||||||
|
self._get_pidtaskinfo.cache_deactivate(self)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def name(self):
|
||||||
|
name = self._get_kinfo_proc()[kinfo_proc_map['name']]
|
||||||
|
return name if name is not None else cext.proc_name(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def exe(self):
|
||||||
|
return cext.proc_exe(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cmdline(self):
|
||||||
|
return cext.proc_cmdline(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def environ(self):
|
||||||
|
return parse_environ_block(cext.proc_environ(self.pid))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def ppid(self):
|
||||||
|
self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']]
|
||||||
|
return self._ppid
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cwd(self):
|
||||||
|
return cext.proc_cwd(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def uids(self):
|
||||||
|
rawtuple = self._get_kinfo_proc()
|
||||||
|
return _common.puids(
|
||||||
|
rawtuple[kinfo_proc_map['ruid']],
|
||||||
|
rawtuple[kinfo_proc_map['euid']],
|
||||||
|
rawtuple[kinfo_proc_map['suid']])
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def gids(self):
|
||||||
|
rawtuple = self._get_kinfo_proc()
|
||||||
|
return _common.puids(
|
||||||
|
rawtuple[kinfo_proc_map['rgid']],
|
||||||
|
rawtuple[kinfo_proc_map['egid']],
|
||||||
|
rawtuple[kinfo_proc_map['sgid']])
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def terminal(self):
|
||||||
|
tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']]
|
||||||
|
tmap = _psposix.get_terminal_map()
|
||||||
|
try:
|
||||||
|
return tmap[tty_nr]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_info(self):
|
||||||
|
rawtuple = self._get_pidtaskinfo()
|
||||||
|
return pmem(
|
||||||
|
rawtuple[pidtaskinfo_map['rss']],
|
||||||
|
rawtuple[pidtaskinfo_map['vms']],
|
||||||
|
rawtuple[pidtaskinfo_map['pfaults']],
|
||||||
|
rawtuple[pidtaskinfo_map['pageins']],
|
||||||
|
)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_full_info(self):
|
||||||
|
basic_mem = self.memory_info()
|
||||||
|
uss = cext.proc_memory_uss(self.pid)
|
||||||
|
return pfullmem(*basic_mem + (uss, ))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_times(self):
|
||||||
|
rawtuple = self._get_pidtaskinfo()
|
||||||
|
return _common.pcputimes(
|
||||||
|
rawtuple[pidtaskinfo_map['cpuutime']],
|
||||||
|
rawtuple[pidtaskinfo_map['cpustime']],
|
||||||
|
# children user / system times are not retrievable (set to 0)
|
||||||
|
0.0, 0.0)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def create_time(self):
|
||||||
|
return self._get_kinfo_proc()[kinfo_proc_map['ctime']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_ctx_switches(self):
|
||||||
|
# Unvoluntary value seems not to be available;
|
||||||
|
# getrusage() numbers seems to confirm this theory.
|
||||||
|
# We set it to 0.
|
||||||
|
vol = self._get_pidtaskinfo()[pidtaskinfo_map['volctxsw']]
|
||||||
|
return _common.pctxsw(vol, 0)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_threads(self):
|
||||||
|
return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def open_files(self):
|
||||||
|
if self.pid == 0:
|
||||||
|
return []
|
||||||
|
files = []
|
||||||
|
rawlist = cext.proc_open_files(self.pid)
|
||||||
|
for path, fd in rawlist:
|
||||||
|
if isfile_strict(path):
|
||||||
|
ntuple = _common.popenfile(path, fd)
|
||||||
|
files.append(ntuple)
|
||||||
|
return files
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def connections(self, kind='inet'):
|
||||||
|
if kind not in conn_tmap:
|
||||||
|
raise ValueError("invalid %r kind argument; choose between %s"
|
||||||
|
% (kind, ', '.join([repr(x) for x in conn_tmap])))
|
||||||
|
families, types = conn_tmap[kind]
|
||||||
|
rawlist = cext.proc_connections(self.pid, families, types)
|
||||||
|
ret = []
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type, laddr, raddr, status = item
|
||||||
|
nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status,
|
||||||
|
TCP_STATUSES)
|
||||||
|
ret.append(nt)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_fds(self):
|
||||||
|
if self.pid == 0:
|
||||||
|
return 0
|
||||||
|
return cext.proc_num_fds(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return _psposix.wait_pid(self.pid, timeout, self._name)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_get(self):
|
||||||
|
return cext_posix.getpriority(self.pid)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_set(self, value):
|
||||||
|
return cext_posix.setpriority(self.pid, value)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def status(self):
|
||||||
|
code = self._get_kinfo_proc()[kinfo_proc_map['status']]
|
||||||
|
# XXX is '?' legit? (we're not supposed to return it anyway)
|
||||||
|
return PROC_STATUSES.get(code, '?')
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def threads(self):
|
||||||
|
rawlist = cext.proc_threads(self.pid)
|
||||||
|
retlist = []
|
||||||
|
for thread_id, utime, stime in rawlist:
|
||||||
|
ntuple = _common.pthread(thread_id, utime, stime)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
return retlist
|
@ -0,0 +1,224 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Routines common to all posix systems."""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ._common import TimeoutExpired
|
||||||
|
from ._common import memoize
|
||||||
|
from ._common import sdiskusage
|
||||||
|
from ._common import usage_percent
|
||||||
|
from ._compat import PY3
|
||||||
|
from ._compat import ChildProcessError
|
||||||
|
from ._compat import FileNotFoundError
|
||||||
|
from ._compat import InterruptedError
|
||||||
|
from ._compat import PermissionError
|
||||||
|
from ._compat import ProcessLookupError
|
||||||
|
from ._compat import unicode
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 4):
|
||||||
|
import enum
|
||||||
|
else:
|
||||||
|
enum = None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map']
|
||||||
|
|
||||||
|
|
||||||
|
def pid_exists(pid):
|
||||||
|
"""Check whether pid exists in the current process table."""
|
||||||
|
if pid == 0:
|
||||||
|
# According to "man 2 kill" PID 0 has a special meaning:
|
||||||
|
# it refers to <<every process in the process group of the
|
||||||
|
# calling process>> so we don't want to go any further.
|
||||||
|
# If we get here it means this UNIX platform *does* have
|
||||||
|
# a process with id 0.
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
return False
|
||||||
|
except PermissionError:
|
||||||
|
# EPERM clearly means there's a process to deny access to
|
||||||
|
return True
|
||||||
|
# According to "man 2 kill" possible error values are
|
||||||
|
# (EINVAL, EPERM, ESRCH)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Python 3.5 signals enum (contributed by me ^^):
|
||||||
|
# https://bugs.python.org/issue21076
|
||||||
|
if enum is not None and hasattr(signal, "Signals"):
|
||||||
|
Negsignal = enum.IntEnum(
|
||||||
|
'Negsignal', dict([(x.name, -x.value) for x in signal.Signals]))
|
||||||
|
|
||||||
|
def negsig_to_enum(num):
|
||||||
|
"""Convert a negative signal value to an enum."""
|
||||||
|
try:
|
||||||
|
return Negsignal(num)
|
||||||
|
except ValueError:
|
||||||
|
return num
|
||||||
|
else: # pragma: no cover
|
||||||
|
def negsig_to_enum(num):
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def wait_pid(pid, timeout=None, proc_name=None,
|
||||||
|
_waitpid=os.waitpid,
|
||||||
|
_timer=getattr(time, 'monotonic', time.time),
|
||||||
|
_min=min,
|
||||||
|
_sleep=time.sleep,
|
||||||
|
_pid_exists=pid_exists):
|
||||||
|
"""Wait for a process PID to terminate.
|
||||||
|
|
||||||
|
If the process terminated normally by calling exit(3) or _exit(2),
|
||||||
|
or by returning from main(), the return value is the positive integer
|
||||||
|
passed to *exit().
|
||||||
|
|
||||||
|
If it was terminated by a signal it returns the negated value of the
|
||||||
|
signal which caused the termination (e.g. -SIGTERM).
|
||||||
|
|
||||||
|
If PID is not a children of os.getpid() (current process) just
|
||||||
|
wait until the process disappears and return None.
|
||||||
|
|
||||||
|
If PID does not exist at all return None immediately.
|
||||||
|
|
||||||
|
If *timeout* != None and process is still alive raise TimeoutExpired.
|
||||||
|
timeout=0 is also possible (either return immediately or raise).
|
||||||
|
"""
|
||||||
|
if pid <= 0:
|
||||||
|
raise ValueError("can't wait for PID 0") # see "man waitpid"
|
||||||
|
interval = 0.0001
|
||||||
|
flags = 0
|
||||||
|
if timeout is not None:
|
||||||
|
flags |= os.WNOHANG
|
||||||
|
stop_at = _timer() + timeout
|
||||||
|
|
||||||
|
def sleep(interval):
|
||||||
|
# Sleep for some time and return a new increased interval.
|
||||||
|
if timeout is not None:
|
||||||
|
if _timer() >= stop_at:
|
||||||
|
raise TimeoutExpired(timeout, pid=pid, name=proc_name)
|
||||||
|
_sleep(interval)
|
||||||
|
return _min(interval * 2, 0.04)
|
||||||
|
|
||||||
|
# See: https://linux.die.net/man/2/waitpid
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
retpid, status = os.waitpid(pid, flags)
|
||||||
|
except InterruptedError:
|
||||||
|
interval = sleep(interval)
|
||||||
|
except ChildProcessError:
|
||||||
|
# This has two meanings:
|
||||||
|
# - PID is not a child of os.getpid() in which case
|
||||||
|
# we keep polling until it's gone
|
||||||
|
# - PID never existed in the first place
|
||||||
|
# In both cases we'll eventually return None as we
|
||||||
|
# can't determine its exit status code.
|
||||||
|
while _pid_exists(pid):
|
||||||
|
interval = sleep(interval)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if retpid == 0:
|
||||||
|
# WNOHANG flag was used and PID is still running.
|
||||||
|
interval = sleep(interval)
|
||||||
|
continue
|
||||||
|
elif os.WIFEXITED(status):
|
||||||
|
# Process terminated normally by calling exit(3) or _exit(2),
|
||||||
|
# or by returning from main(). The return value is the
|
||||||
|
# positive integer passed to *exit().
|
||||||
|
return os.WEXITSTATUS(status)
|
||||||
|
elif os.WIFSIGNALED(status):
|
||||||
|
# Process exited due to a signal. Return the negative value
|
||||||
|
# of that signal.
|
||||||
|
return negsig_to_enum(-os.WTERMSIG(status))
|
||||||
|
# elif os.WIFSTOPPED(status):
|
||||||
|
# # Process was stopped via SIGSTOP or is being traced, and
|
||||||
|
# # waitpid() was called with WUNTRACED flag. PID is still
|
||||||
|
# # alive. From now on waitpid() will keep returning (0, 0)
|
||||||
|
# # until the process state doesn't change.
|
||||||
|
# # It may make sense to catch/enable this since stopped PIDs
|
||||||
|
# # ignore SIGTERM.
|
||||||
|
# interval = sleep(interval)
|
||||||
|
# continue
|
||||||
|
# elif os.WIFCONTINUED(status):
|
||||||
|
# # Process was resumed via SIGCONT and waitpid() was called
|
||||||
|
# # with WCONTINUED flag.
|
||||||
|
# interval = sleep(interval)
|
||||||
|
# continue
|
||||||
|
else:
|
||||||
|
# Should never happen.
|
||||||
|
raise ValueError("unknown process exit status %r" % status)
|
||||||
|
|
||||||
|
|
||||||
|
def disk_usage(path):
|
||||||
|
"""Return disk usage associated with path.
|
||||||
|
Note: UNIX usually reserves 5% disk space which is not accessible
|
||||||
|
by user. In this function "total" and "used" values reflect the
|
||||||
|
total and used disk space whereas "free" and "percent" represent
|
||||||
|
the "free" and "used percent" user disk space.
|
||||||
|
"""
|
||||||
|
if PY3:
|
||||||
|
st = os.statvfs(path)
|
||||||
|
else: # pragma: no cover
|
||||||
|
# os.statvfs() does not support unicode on Python 2:
|
||||||
|
# - https://github.com/giampaolo/psutil/issues/416
|
||||||
|
# - http://bugs.python.org/issue18695
|
||||||
|
try:
|
||||||
|
st = os.statvfs(path)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
if isinstance(path, unicode):
|
||||||
|
try:
|
||||||
|
path = path.encode(sys.getfilesystemencoding())
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
pass
|
||||||
|
st = os.statvfs(path)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Total space which is only available to root (unless changed
|
||||||
|
# at system level).
|
||||||
|
total = (st.f_blocks * st.f_frsize)
|
||||||
|
# Remaining free space usable by root.
|
||||||
|
avail_to_root = (st.f_bfree * st.f_frsize)
|
||||||
|
# Remaining free space usable by user.
|
||||||
|
avail_to_user = (st.f_bavail * st.f_frsize)
|
||||||
|
# Total space being used in general.
|
||||||
|
used = (total - avail_to_root)
|
||||||
|
# Total space which is available to user (same as 'total' but
|
||||||
|
# for the user).
|
||||||
|
total_user = used + avail_to_user
|
||||||
|
# User usage percent compared to the total amount of space
|
||||||
|
# the user can use. This number would be higher if compared
|
||||||
|
# to root's because the user has less space (usually -5%).
|
||||||
|
usage_percent_user = usage_percent(used, total_user, round_=1)
|
||||||
|
|
||||||
|
# NB: the percentage is -5% than what shown by df due to
|
||||||
|
# reserved blocks that we are currently not considering:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462
|
||||||
|
return sdiskusage(
|
||||||
|
total=total, used=used, free=avail_to_user, percent=usage_percent_user)
|
||||||
|
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def get_terminal_map():
|
||||||
|
"""Get a map of device-id -> path as a dict.
|
||||||
|
Used by Process.terminal()
|
||||||
|
"""
|
||||||
|
ret = {}
|
||||||
|
ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*')
|
||||||
|
for name in ls:
|
||||||
|
assert name not in ret, name
|
||||||
|
try:
|
||||||
|
ret[os.stat(name).st_rdev] = name
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return ret
|
@ -0,0 +1,727 @@
|
|||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Sun OS Solaris platform implementation."""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
|
from socket import AF_INET
|
||||||
|
|
||||||
|
from . import _common
|
||||||
|
from . import _psposix
|
||||||
|
from . import _psutil_posix as cext_posix
|
||||||
|
from . import _psutil_sunos as cext
|
||||||
|
from ._common import AF_INET6
|
||||||
|
from ._common import AccessDenied
|
||||||
|
from ._common import NoSuchProcess
|
||||||
|
from ._common import ZombieProcess
|
||||||
|
from ._common import debug
|
||||||
|
from ._common import get_procfs_path
|
||||||
|
from ._common import isfile_strict
|
||||||
|
from ._common import memoize_when_activated
|
||||||
|
from ._common import sockfam_to_enum
|
||||||
|
from ._common import socktype_to_enum
|
||||||
|
from ._common import usage_percent
|
||||||
|
from ._compat import PY3
|
||||||
|
from ._compat import FileNotFoundError
|
||||||
|
from ._compat import PermissionError
|
||||||
|
from ._compat import ProcessLookupError
|
||||||
|
from ._compat import b
|
||||||
|
|
||||||
|
|
||||||
|
__extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"]
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- globals
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
PAGE_SIZE = cext_posix.getpagesize()
|
||||||
|
AF_LINK = cext_posix.AF_LINK
|
||||||
|
IS_64_BIT = sys.maxsize > 2**32
|
||||||
|
|
||||||
|
CONN_IDLE = "IDLE"
|
||||||
|
CONN_BOUND = "BOUND"
|
||||||
|
|
||||||
|
PROC_STATUSES = {
|
||||||
|
cext.SSLEEP: _common.STATUS_SLEEPING,
|
||||||
|
cext.SRUN: _common.STATUS_RUNNING,
|
||||||
|
cext.SZOMB: _common.STATUS_ZOMBIE,
|
||||||
|
cext.SSTOP: _common.STATUS_STOPPED,
|
||||||
|
cext.SIDL: _common.STATUS_IDLE,
|
||||||
|
cext.SONPROC: _common.STATUS_RUNNING, # same as run
|
||||||
|
cext.SWAIT: _common.STATUS_WAITING,
|
||||||
|
}
|
||||||
|
|
||||||
|
TCP_STATUSES = {
|
||||||
|
cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED,
|
||||||
|
cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT,
|
||||||
|
cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV,
|
||||||
|
cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1,
|
||||||
|
cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2,
|
||||||
|
cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT,
|
||||||
|
cext.TCPS_CLOSED: _common.CONN_CLOSE,
|
||||||
|
cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT,
|
||||||
|
cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK,
|
||||||
|
cext.TCPS_LISTEN: _common.CONN_LISTEN,
|
||||||
|
cext.TCPS_CLOSING: _common.CONN_CLOSING,
|
||||||
|
cext.PSUTIL_CONN_NONE: _common.CONN_NONE,
|
||||||
|
cext.TCPS_IDLE: CONN_IDLE, # sunos specific
|
||||||
|
cext.TCPS_BOUND: CONN_BOUND, # sunos specific
|
||||||
|
}
|
||||||
|
|
||||||
|
proc_info_map = dict(
|
||||||
|
ppid=0,
|
||||||
|
rss=1,
|
||||||
|
vms=2,
|
||||||
|
create_time=3,
|
||||||
|
nice=4,
|
||||||
|
num_threads=5,
|
||||||
|
status=6,
|
||||||
|
ttynr=7,
|
||||||
|
uid=8,
|
||||||
|
euid=9,
|
||||||
|
gid=10,
|
||||||
|
egid=11)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- named tuples
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# psutil.cpu_times()
|
||||||
|
scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait'])
|
||||||
|
# psutil.cpu_times(percpu=True)
|
||||||
|
pcputimes = namedtuple('pcputimes',
|
||||||
|
['user', 'system', 'children_user', 'children_system'])
|
||||||
|
# psutil.virtual_memory()
|
||||||
|
svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free'])
|
||||||
|
# psutil.Process.memory_info()
|
||||||
|
pmem = namedtuple('pmem', ['rss', 'vms'])
|
||||||
|
pfullmem = pmem
|
||||||
|
# psutil.Process.memory_maps(grouped=True)
|
||||||
|
pmmap_grouped = namedtuple('pmmap_grouped',
|
||||||
|
['path', 'rss', 'anonymous', 'locked'])
|
||||||
|
# psutil.Process.memory_maps(grouped=False)
|
||||||
|
pmmap_ext = namedtuple(
|
||||||
|
'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields))
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- memory
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def virtual_memory():
|
||||||
|
"""Report virtual memory metrics."""
|
||||||
|
# we could have done this with kstat, but IMHO this is good enough
|
||||||
|
total = os.sysconf('SC_PHYS_PAGES') * PAGE_SIZE
|
||||||
|
# note: there's no difference on Solaris
|
||||||
|
free = avail = os.sysconf('SC_AVPHYS_PAGES') * PAGE_SIZE
|
||||||
|
used = total - free
|
||||||
|
percent = usage_percent(used, total, round_=1)
|
||||||
|
return svmem(total, avail, percent, used, free)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_memory():
|
||||||
|
"""Report swap memory metrics."""
|
||||||
|
sin, sout = cext.swap_mem()
|
||||||
|
# XXX
|
||||||
|
# we are supposed to get total/free by doing so:
|
||||||
|
# http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/
|
||||||
|
# usr/src/cmd/swap/swap.c
|
||||||
|
# ...nevertheless I can't manage to obtain the same numbers as 'swap'
|
||||||
|
# cmdline utility, so let's parse its output (sigh!)
|
||||||
|
p = subprocess.Popen(['/usr/bin/env', 'PATH=/usr/sbin:/sbin:%s' %
|
||||||
|
os.environ['PATH'], 'swap', '-l'],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if PY3:
|
||||||
|
stdout = stdout.decode(sys.stdout.encoding)
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise RuntimeError("'swap -l' failed (retcode=%s)" % p.returncode)
|
||||||
|
|
||||||
|
lines = stdout.strip().split('\n')[1:]
|
||||||
|
if not lines:
|
||||||
|
raise RuntimeError('no swap device(s) configured')
|
||||||
|
total = free = 0
|
||||||
|
for line in lines:
|
||||||
|
line = line.split()
|
||||||
|
t, f = line[3:5]
|
||||||
|
total += int(int(t) * 512)
|
||||||
|
free += int(int(f) * 512)
|
||||||
|
used = total - free
|
||||||
|
percent = usage_percent(used, total, round_=1)
|
||||||
|
return _common.sswap(total, used, free, percent,
|
||||||
|
sin * PAGE_SIZE, sout * PAGE_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- CPU
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_times():
|
||||||
|
"""Return system-wide CPU times as a named tuple"""
|
||||||
|
ret = cext.per_cpu_times()
|
||||||
|
return scputimes(*[sum(x) for x in zip(*ret)])
|
||||||
|
|
||||||
|
|
||||||
|
def per_cpu_times():
|
||||||
|
"""Return system per-CPU times as a list of named tuples"""
|
||||||
|
ret = cext.per_cpu_times()
|
||||||
|
return [scputimes(*x) for x in ret]
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_logical():
|
||||||
|
"""Return the number of logical CPUs in the system."""
|
||||||
|
try:
|
||||||
|
return os.sysconf("SC_NPROCESSORS_ONLN")
|
||||||
|
except ValueError:
|
||||||
|
# mimic os.cpu_count() behavior
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_count_cores():
|
||||||
|
"""Return the number of CPU cores in the system."""
|
||||||
|
return cext.cpu_count_cores()
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_stats():
|
||||||
|
"""Return various CPU stats as a named tuple."""
|
||||||
|
ctx_switches, interrupts, syscalls, traps = cext.cpu_stats()
|
||||||
|
soft_interrupts = 0
|
||||||
|
return _common.scpustats(ctx_switches, interrupts, soft_interrupts,
|
||||||
|
syscalls)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- disks
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
disk_io_counters = cext.disk_io_counters
|
||||||
|
disk_usage = _psposix.disk_usage
|
||||||
|
|
||||||
|
|
||||||
|
def disk_partitions(all=False):
|
||||||
|
"""Return system disk partitions."""
|
||||||
|
# TODO - the filtering logic should be better checked so that
|
||||||
|
# it tries to reflect 'df' as much as possible
|
||||||
|
retlist = []
|
||||||
|
partitions = cext.disk_partitions()
|
||||||
|
for partition in partitions:
|
||||||
|
device, mountpoint, fstype, opts = partition
|
||||||
|
if device == 'none':
|
||||||
|
device = ''
|
||||||
|
if not all:
|
||||||
|
# Differently from, say, Linux, we don't have a list of
|
||||||
|
# common fs types so the best we can do, AFAIK, is to
|
||||||
|
# filter by filesystem having a total size > 0.
|
||||||
|
try:
|
||||||
|
if not disk_usage(mountpoint).total:
|
||||||
|
continue
|
||||||
|
except OSError as err:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1674
|
||||||
|
debug("skipping %r: %s" % (mountpoint, err))
|
||||||
|
continue
|
||||||
|
maxfile = maxpath = None # set later
|
||||||
|
ntuple = _common.sdiskpart(device, mountpoint, fstype, opts,
|
||||||
|
maxfile, maxpath)
|
||||||
|
retlist.append(ntuple)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- network
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
net_io_counters = cext.net_io_counters
|
||||||
|
net_if_addrs = cext_posix.net_if_addrs
|
||||||
|
|
||||||
|
|
||||||
|
def net_connections(kind, _pid=-1):
|
||||||
|
"""Return socket connections. If pid == -1 return system-wide
|
||||||
|
connections (as opposed to connections opened by one process only).
|
||||||
|
Only INET sockets are returned (UNIX are not).
|
||||||
|
"""
|
||||||
|
cmap = _common.conn_tmap.copy()
|
||||||
|
if _pid == -1:
|
||||||
|
cmap.pop('unix', 0)
|
||||||
|
if kind not in cmap:
|
||||||
|
raise ValueError("invalid %r kind argument; choose between %s"
|
||||||
|
% (kind, ', '.join([repr(x) for x in cmap])))
|
||||||
|
families, types = _common.conn_tmap[kind]
|
||||||
|
rawlist = cext.net_connections(_pid)
|
||||||
|
ret = set()
|
||||||
|
for item in rawlist:
|
||||||
|
fd, fam, type_, laddr, raddr, status, pid = item
|
||||||
|
if fam not in families:
|
||||||
|
continue
|
||||||
|
if type_ not in types:
|
||||||
|
continue
|
||||||
|
# TODO: refactor and use _common.conn_to_ntuple.
|
||||||
|
if fam in (AF_INET, AF_INET6):
|
||||||
|
if laddr:
|
||||||
|
laddr = _common.addr(*laddr)
|
||||||
|
if raddr:
|
||||||
|
raddr = _common.addr(*raddr)
|
||||||
|
status = TCP_STATUSES[status]
|
||||||
|
fam = sockfam_to_enum(fam)
|
||||||
|
type_ = socktype_to_enum(type_)
|
||||||
|
if _pid == -1:
|
||||||
|
nt = _common.sconn(fd, fam, type_, laddr, raddr, status, pid)
|
||||||
|
else:
|
||||||
|
nt = _common.pconn(fd, fam, type_, laddr, raddr, status)
|
||||||
|
ret.add(nt)
|
||||||
|
return list(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def net_if_stats():
|
||||||
|
"""Get NIC stats (isup, duplex, speed, mtu)."""
|
||||||
|
ret = cext.net_if_stats()
|
||||||
|
for name, items in ret.items():
|
||||||
|
isup, duplex, speed, mtu = items
|
||||||
|
if hasattr(_common, 'NicDuplex'):
|
||||||
|
duplex = _common.NicDuplex(duplex)
|
||||||
|
ret[name] = _common.snicstats(isup, duplex, speed, mtu)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- other system functions
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def boot_time():
|
||||||
|
"""The system boot time expressed in seconds since the epoch."""
|
||||||
|
return cext.boot_time()
|
||||||
|
|
||||||
|
|
||||||
|
def users():
|
||||||
|
"""Return currently connected users as a list of namedtuples."""
|
||||||
|
retlist = []
|
||||||
|
rawlist = cext.users()
|
||||||
|
localhost = (':0.0', ':0')
|
||||||
|
for item in rawlist:
|
||||||
|
user, tty, hostname, tstamp, user_process, pid = item
|
||||||
|
# note: the underlying C function includes entries about
|
||||||
|
# system boot, run level and others. We might want
|
||||||
|
# to use them in the future.
|
||||||
|
if not user_process:
|
||||||
|
continue
|
||||||
|
if hostname in localhost:
|
||||||
|
hostname = 'localhost'
|
||||||
|
nt = _common.suser(user, tty, hostname, tstamp, pid)
|
||||||
|
retlist.append(nt)
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- processes
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def pids():
|
||||||
|
"""Returns a list of PIDs currently running on the system."""
|
||||||
|
return [int(x) for x in os.listdir(b(get_procfs_path())) if x.isdigit()]
|
||||||
|
|
||||||
|
|
||||||
|
def pid_exists(pid):
|
||||||
|
"""Check for the existence of a unix pid."""
|
||||||
|
return _psposix.pid_exists(pid)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exceptions(fun):
|
||||||
|
"""Call callable into a try/except clause and translate ENOENT,
|
||||||
|
EACCES and EPERM in NoSuchProcess or AccessDenied exceptions.
|
||||||
|
"""
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
except (FileNotFoundError, ProcessLookupError):
|
||||||
|
# ENOENT (no such file or directory) gets raised on open().
|
||||||
|
# ESRCH (no such process) can get raised on read() if
|
||||||
|
# process is gone in meantime.
|
||||||
|
if not pid_exists(self.pid):
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
else:
|
||||||
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
||||||
|
except PermissionError:
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
except OSError:
|
||||||
|
if self.pid == 0:
|
||||||
|
if 0 in pids():
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Process(object):
|
||||||
|
"""Wrapper class around underlying C implementation."""
|
||||||
|
|
||||||
|
__slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"]
|
||||||
|
|
||||||
|
def __init__(self, pid):
|
||||||
|
self.pid = pid
|
||||||
|
self._name = None
|
||||||
|
self._ppid = None
|
||||||
|
self._procfs_path = get_procfs_path()
|
||||||
|
|
||||||
|
def _assert_alive(self):
|
||||||
|
"""Raise NSP if the process disappeared on us."""
|
||||||
|
# For those C function who do not raise NSP, possibly returning
|
||||||
|
# incorrect or incomplete result.
|
||||||
|
os.stat('%s/%s' % (self._procfs_path, self.pid))
|
||||||
|
|
||||||
|
def oneshot_enter(self):
|
||||||
|
self._proc_name_and_args.cache_activate(self)
|
||||||
|
self._proc_basic_info.cache_activate(self)
|
||||||
|
self._proc_cred.cache_activate(self)
|
||||||
|
|
||||||
|
def oneshot_exit(self):
|
||||||
|
self._proc_name_and_args.cache_deactivate(self)
|
||||||
|
self._proc_basic_info.cache_deactivate(self)
|
||||||
|
self._proc_cred.cache_deactivate(self)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _proc_name_and_args(self):
|
||||||
|
return cext.proc_name_and_args(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _proc_basic_info(self):
|
||||||
|
if self.pid == 0 and not \
|
||||||
|
os.path.exists('%s/%s/psinfo' % (self._procfs_path, self.pid)):
|
||||||
|
raise AccessDenied(self.pid)
|
||||||
|
ret = cext.proc_basic_info(self.pid, self._procfs_path)
|
||||||
|
assert len(ret) == len(proc_info_map)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
@memoize_when_activated
|
||||||
|
def _proc_cred(self):
|
||||||
|
return cext.proc_cred(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def name(self):
|
||||||
|
# note: max len == 15
|
||||||
|
return self._proc_name_and_args()[0]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def exe(self):
|
||||||
|
try:
|
||||||
|
return os.readlink(
|
||||||
|
"%s/%s/path/a.out" % (self._procfs_path, self.pid))
|
||||||
|
except OSError:
|
||||||
|
pass # continue and guess the exe name from the cmdline
|
||||||
|
# Will be guessed later from cmdline but we want to explicitly
|
||||||
|
# invoke cmdline here in order to get an AccessDenied
|
||||||
|
# exception if the user has not enough privileges.
|
||||||
|
self.cmdline()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cmdline(self):
|
||||||
|
return self._proc_name_and_args()[1].split(' ')
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def environ(self):
|
||||||
|
return cext.proc_environ(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def create_time(self):
|
||||||
|
return self._proc_basic_info()[proc_info_map['create_time']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_threads(self):
|
||||||
|
return self._proc_basic_info()[proc_info_map['num_threads']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_get(self):
|
||||||
|
# Note #1: getpriority(3) doesn't work for realtime processes.
|
||||||
|
# Psinfo is what ps uses, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1194
|
||||||
|
return self._proc_basic_info()[proc_info_map['nice']]
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def nice_set(self, value):
|
||||||
|
if self.pid in (2, 3):
|
||||||
|
# Special case PIDs: internally setpriority(3) return ESRCH
|
||||||
|
# (no such process), no matter what.
|
||||||
|
# The process actually exists though, as it has a name,
|
||||||
|
# creation time, etc.
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
return cext_posix.setpriority(self.pid, value)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def ppid(self):
|
||||||
|
self._ppid = self._proc_basic_info()[proc_info_map['ppid']]
|
||||||
|
return self._ppid
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def uids(self):
|
||||||
|
try:
|
||||||
|
real, effective, saved, _, _, _ = self._proc_cred()
|
||||||
|
except AccessDenied:
|
||||||
|
real = self._proc_basic_info()[proc_info_map['uid']]
|
||||||
|
effective = self._proc_basic_info()[proc_info_map['euid']]
|
||||||
|
saved = None
|
||||||
|
return _common.puids(real, effective, saved)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def gids(self):
|
||||||
|
try:
|
||||||
|
_, _, _, real, effective, saved = self._proc_cred()
|
||||||
|
except AccessDenied:
|
||||||
|
real = self._proc_basic_info()[proc_info_map['gid']]
|
||||||
|
effective = self._proc_basic_info()[proc_info_map['egid']]
|
||||||
|
saved = None
|
||||||
|
return _common.puids(real, effective, saved)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_times(self):
|
||||||
|
try:
|
||||||
|
times = cext.proc_cpu_times(self.pid, self._procfs_path)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EOVERFLOW and not IS_64_BIT:
|
||||||
|
# We may get here if we attempt to query a 64bit process
|
||||||
|
# with a 32bit python.
|
||||||
|
# Error originates from read() and also tools like "cat"
|
||||||
|
# fail in the same way (!).
|
||||||
|
# Since there simply is no way to determine CPU times we
|
||||||
|
# return 0.0 as a fallback. See:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/857
|
||||||
|
times = (0.0, 0.0, 0.0, 0.0)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return _common.pcputimes(*times)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cpu_num(self):
|
||||||
|
return cext.proc_cpu_num(self.pid, self._procfs_path)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def terminal(self):
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
hit_enoent = False
|
||||||
|
tty = wrap_exceptions(
|
||||||
|
self._proc_basic_info()[proc_info_map['ttynr']])
|
||||||
|
if tty != cext.PRNODEV:
|
||||||
|
for x in (0, 1, 2, 255):
|
||||||
|
try:
|
||||||
|
return os.readlink(
|
||||||
|
'%s/%d/path/%d' % (procfs_path, self.pid, x))
|
||||||
|
except FileNotFoundError:
|
||||||
|
hit_enoent = True
|
||||||
|
continue
|
||||||
|
if hit_enoent:
|
||||||
|
self._assert_alive()
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def cwd(self):
|
||||||
|
# /proc/PID/path/cwd may not be resolved by readlink() even if
|
||||||
|
# it exists (ls shows it). If that's the case and the process
|
||||||
|
# is still alive return None (we can return None also on BSD).
|
||||||
|
# Reference: http://goo.gl/55XgO
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
try:
|
||||||
|
return os.readlink("%s/%s/path/cwd" % (procfs_path, self.pid))
|
||||||
|
except FileNotFoundError:
|
||||||
|
os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_info(self):
|
||||||
|
ret = self._proc_basic_info()
|
||||||
|
rss = ret[proc_info_map['rss']] * 1024
|
||||||
|
vms = ret[proc_info_map['vms']] * 1024
|
||||||
|
return pmem(rss, vms)
|
||||||
|
|
||||||
|
memory_full_info = memory_info
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def status(self):
|
||||||
|
code = self._proc_basic_info()[proc_info_map['status']]
|
||||||
|
# XXX is '?' legit? (we're not supposed to return it anyway)
|
||||||
|
return PROC_STATUSES.get(code, '?')
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def threads(self):
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
ret = []
|
||||||
|
tids = os.listdir('%s/%d/lwp' % (procfs_path, self.pid))
|
||||||
|
hit_enoent = False
|
||||||
|
for tid in tids:
|
||||||
|
tid = int(tid)
|
||||||
|
try:
|
||||||
|
utime, stime = cext.query_process_thread(
|
||||||
|
self.pid, tid, procfs_path)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
if err.errno == errno.EOVERFLOW and not IS_64_BIT:
|
||||||
|
# We may get here if we attempt to query a 64bit process
|
||||||
|
# with a 32bit python.
|
||||||
|
# Error originates from read() and also tools like "cat"
|
||||||
|
# fail in the same way (!).
|
||||||
|
# Since there simply is no way to determine CPU times we
|
||||||
|
# return 0.0 as a fallback. See:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/857
|
||||||
|
continue
|
||||||
|
# ENOENT == thread gone in meantime
|
||||||
|
if err.errno == errno.ENOENT:
|
||||||
|
hit_enoent = True
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
nt = _common.pthread(tid, utime, stime)
|
||||||
|
ret.append(nt)
|
||||||
|
if hit_enoent:
|
||||||
|
self._assert_alive()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def open_files(self):
|
||||||
|
retlist = []
|
||||||
|
hit_enoent = False
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
pathdir = '%s/%d/path' % (procfs_path, self.pid)
|
||||||
|
for fd in os.listdir('%s/%d/fd' % (procfs_path, self.pid)):
|
||||||
|
path = os.path.join(pathdir, fd)
|
||||||
|
if os.path.islink(path):
|
||||||
|
try:
|
||||||
|
file = os.readlink(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
hit_enoent = True
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if isfile_strict(file):
|
||||||
|
retlist.append(_common.popenfile(file, int(fd)))
|
||||||
|
if hit_enoent:
|
||||||
|
self._assert_alive()
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
def _get_unix_sockets(self, pid):
|
||||||
|
"""Get UNIX sockets used by process by parsing 'pfiles' output."""
|
||||||
|
# TODO: rewrite this in C (...but the damn netstat source code
|
||||||
|
# does not include this part! Argh!!)
|
||||||
|
cmd = "pfiles %s" % pid
|
||||||
|
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if PY3:
|
||||||
|
stdout, stderr = [x.decode(sys.stdout.encoding)
|
||||||
|
for x in (stdout, stderr)]
|
||||||
|
if p.returncode != 0:
|
||||||
|
if 'permission denied' in stderr.lower():
|
||||||
|
raise AccessDenied(self.pid, self._name)
|
||||||
|
if 'no such process' in stderr.lower():
|
||||||
|
raise NoSuchProcess(self.pid, self._name)
|
||||||
|
raise RuntimeError("%r command error\n%s" % (cmd, stderr))
|
||||||
|
|
||||||
|
lines = stdout.split('\n')[2:]
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
line = line.lstrip()
|
||||||
|
if line.startswith('sockname: AF_UNIX'):
|
||||||
|
path = line.split(' ', 2)[2]
|
||||||
|
type = lines[i - 2].strip()
|
||||||
|
if type == 'SOCK_STREAM':
|
||||||
|
type = socket.SOCK_STREAM
|
||||||
|
elif type == 'SOCK_DGRAM':
|
||||||
|
type = socket.SOCK_DGRAM
|
||||||
|
else:
|
||||||
|
type = -1
|
||||||
|
yield (-1, socket.AF_UNIX, type, path, "", _common.CONN_NONE)
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def connections(self, kind='inet'):
|
||||||
|
ret = net_connections(kind, _pid=self.pid)
|
||||||
|
# The underlying C implementation retrieves all OS connections
|
||||||
|
# and filters them by PID. At this point we can't tell whether
|
||||||
|
# an empty list means there were no connections for process or
|
||||||
|
# process is no longer active so we force NSP in case the PID
|
||||||
|
# is no longer there.
|
||||||
|
if not ret:
|
||||||
|
# will raise NSP if process is gone
|
||||||
|
os.stat('%s/%s' % (self._procfs_path, self.pid))
|
||||||
|
|
||||||
|
# UNIX sockets
|
||||||
|
if kind in ('all', 'unix'):
|
||||||
|
ret.extend([_common.pconn(*conn) for conn in
|
||||||
|
self._get_unix_sockets(self.pid)])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
nt_mmap_grouped = namedtuple('mmap', 'path rss anon locked')
|
||||||
|
nt_mmap_ext = namedtuple('mmap', 'addr perms path rss anon locked')
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def memory_maps(self):
|
||||||
|
def toaddr(start, end):
|
||||||
|
return '%s-%s' % (hex(start)[2:].strip('L'),
|
||||||
|
hex(end)[2:].strip('L'))
|
||||||
|
|
||||||
|
procfs_path = self._procfs_path
|
||||||
|
retlist = []
|
||||||
|
try:
|
||||||
|
rawlist = cext.proc_memory_maps(self.pid, procfs_path)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EOVERFLOW and not IS_64_BIT:
|
||||||
|
# We may get here if we attempt to query a 64bit process
|
||||||
|
# with a 32bit python.
|
||||||
|
# Error originates from read() and also tools like "cat"
|
||||||
|
# fail in the same way (!).
|
||||||
|
# Since there simply is no way to determine CPU times we
|
||||||
|
# return 0.0 as a fallback. See:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/857
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
hit_enoent = False
|
||||||
|
for item in rawlist:
|
||||||
|
addr, addrsize, perm, name, rss, anon, locked = item
|
||||||
|
addr = toaddr(addr, addrsize)
|
||||||
|
if not name.startswith('['):
|
||||||
|
try:
|
||||||
|
name = os.readlink(
|
||||||
|
'%s/%s/path/%s' % (procfs_path, self.pid, name))
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.ENOENT:
|
||||||
|
# sometimes the link may not be resolved by
|
||||||
|
# readlink() even if it exists (ls shows it).
|
||||||
|
# If that's the case we just return the
|
||||||
|
# unresolved link path.
|
||||||
|
# This seems an incosistency with /proc similar
|
||||||
|
# to: http://goo.gl/55XgO
|
||||||
|
name = '%s/%s/path/%s' % (procfs_path, self.pid, name)
|
||||||
|
hit_enoent = True
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
retlist.append((addr, perm, name, rss, anon, locked))
|
||||||
|
if hit_enoent:
|
||||||
|
self._assert_alive()
|
||||||
|
return retlist
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_fds(self):
|
||||||
|
return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def num_ctx_switches(self):
|
||||||
|
return _common.pctxsw(
|
||||||
|
*cext.proc_num_ctx_switches(self.pid, self._procfs_path))
|
||||||
|
|
||||||
|
@wrap_exceptions
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return _psposix.wait_pid(self.pid, timeout, self._name)
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Run unit tests. This is invoked by:
|
||||||
|
$ python -m psutil.tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .runner import main
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,350 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test runner, providing new features on top of unittest module:
|
||||||
|
- colourized output
|
||||||
|
- parallel run (UNIX only)
|
||||||
|
- print failures/tracebacks on CTRL+C
|
||||||
|
- re-run failed tests only (make test-failed)
|
||||||
|
|
||||||
|
Invocation examples:
|
||||||
|
- make test
|
||||||
|
- make test-failed
|
||||||
|
|
||||||
|
Parallel:
|
||||||
|
- make test-parallel
|
||||||
|
- make test-process ARGS=--parallel
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
except ImportError:
|
||||||
|
ctypes = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import concurrencytest # pip install concurrencytest
|
||||||
|
except ImportError:
|
||||||
|
concurrencytest = None
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil._common import hilite
|
||||||
|
from psutil._common import print_color
|
||||||
|
from psutil._common import term_supports_colors
|
||||||
|
from psutil._compat import super
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import import_module_by_path
|
||||||
|
from psutil.tests import print_sysinfo
|
||||||
|
from psutil.tests import reap_children
|
||||||
|
from psutil.tests import safe_rmpath
|
||||||
|
|
||||||
|
|
||||||
|
VERBOSITY = 2
|
||||||
|
FAILED_TESTS_FNAME = '.failed-tests.txt'
|
||||||
|
NWORKERS = psutil.cpu_count() or 1
|
||||||
|
USE_COLORS = not CI_TESTING and term_supports_colors()
|
||||||
|
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase
|
||||||
|
|
||||||
|
|
||||||
|
def cprint(msg, color, bold=False, file=None):
|
||||||
|
if file is None:
|
||||||
|
file = sys.stderr if color == 'red' else sys.stdout
|
||||||
|
if USE_COLORS:
|
||||||
|
print_color(msg, color, bold=bold, file=file)
|
||||||
|
else:
|
||||||
|
print(msg, file=file)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoader:
|
||||||
|
|
||||||
|
testdir = HERE
|
||||||
|
skip_files = ['test_memleaks.py']
|
||||||
|
if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ:
|
||||||
|
skip_files.extend(['test_osx.py', 'test_linux.py', 'test_posix.py'])
|
||||||
|
|
||||||
|
def _get_testmods(self):
|
||||||
|
return [os.path.join(self.testdir, x)
|
||||||
|
for x in os.listdir(self.testdir)
|
||||||
|
if x.startswith('test_') and x.endswith('.py') and
|
||||||
|
x not in self.skip_files]
|
||||||
|
|
||||||
|
def _iter_testmod_classes(self):
|
||||||
|
"""Iterate over all test files in this directory and return
|
||||||
|
all TestCase classes in them.
|
||||||
|
"""
|
||||||
|
for path in self._get_testmods():
|
||||||
|
mod = import_module_by_path(path)
|
||||||
|
for name in dir(mod):
|
||||||
|
obj = getattr(mod, name)
|
||||||
|
if isinstance(obj, type) and \
|
||||||
|
issubclass(obj, unittest.TestCase):
|
||||||
|
yield obj
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
for obj in self._iter_testmod_classes():
|
||||||
|
test = loadTestsFromTestCase(obj)
|
||||||
|
suite.addTest(test)
|
||||||
|
return suite
|
||||||
|
|
||||||
|
def last_failed(self):
|
||||||
|
# ...from previously failed test run
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
if not os.path.isfile(FAILED_TESTS_FNAME):
|
||||||
|
return suite
|
||||||
|
with open(FAILED_TESTS_FNAME, 'rt') as f:
|
||||||
|
names = f.read().split()
|
||||||
|
for n in names:
|
||||||
|
test = unittest.defaultTestLoader.loadTestsFromName(n)
|
||||||
|
suite.addTest(test)
|
||||||
|
return suite
|
||||||
|
|
||||||
|
def from_name(self, name):
|
||||||
|
if name.endswith('.py'):
|
||||||
|
name = os.path.splitext(os.path.basename(name))[0]
|
||||||
|
return unittest.defaultTestLoader.loadTestsFromName(name)
|
||||||
|
|
||||||
|
|
||||||
|
class ColouredResult(unittest.TextTestResult):
|
||||||
|
|
||||||
|
def addSuccess(self, test):
|
||||||
|
unittest.TestResult.addSuccess(self, test)
|
||||||
|
cprint("OK", "green")
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
unittest.TestResult.addError(self, test, err)
|
||||||
|
cprint("ERROR", "red", bold=True)
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
unittest.TestResult.addFailure(self, test, err)
|
||||||
|
cprint("FAIL", "red")
|
||||||
|
|
||||||
|
def addSkip(self, test, reason):
|
||||||
|
unittest.TestResult.addSkip(self, test, reason)
|
||||||
|
cprint("skipped: %s" % reason.strip(), "brown")
|
||||||
|
|
||||||
|
def printErrorList(self, flavour, errors):
|
||||||
|
flavour = hilite(flavour, "red", bold=flavour == 'ERROR')
|
||||||
|
super().printErrorList(flavour, errors)
|
||||||
|
|
||||||
|
|
||||||
|
class ColouredTextRunner(unittest.TextTestRunner):
|
||||||
|
"""
|
||||||
|
A coloured text runner which also prints failed tests on KeyboardInterrupt
|
||||||
|
and save failed tests in a file so that they can be re-run.
|
||||||
|
"""
|
||||||
|
resultclass = ColouredResult if USE_COLORS else unittest.TextTestResult
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.failed_tnames = set()
|
||||||
|
|
||||||
|
def _makeResult(self):
|
||||||
|
# Store result instance so that it can be accessed on
|
||||||
|
# KeyboardInterrupt.
|
||||||
|
self.result = super()._makeResult()
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
def _write_last_failed(self):
|
||||||
|
if self.failed_tnames:
|
||||||
|
with open(FAILED_TESTS_FNAME, 'wt') as f:
|
||||||
|
for tname in self.failed_tnames:
|
||||||
|
f.write(tname + '\n')
|
||||||
|
|
||||||
|
def _save_result(self, result):
|
||||||
|
if not result.wasSuccessful():
|
||||||
|
for t in result.errors + result.failures:
|
||||||
|
tname = t[0].id()
|
||||||
|
self.failed_tnames.add(tname)
|
||||||
|
|
||||||
|
def _run(self, suite):
|
||||||
|
try:
|
||||||
|
result = super().run(suite)
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
result = self.runner.result
|
||||||
|
result.printErrors()
|
||||||
|
raise sys.exit(1)
|
||||||
|
else:
|
||||||
|
self._save_result(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _exit(self, success):
|
||||||
|
if success:
|
||||||
|
cprint("SUCCESS", "green", bold=True)
|
||||||
|
safe_rmpath(FAILED_TESTS_FNAME)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
cprint("FAILED", "red", bold=True)
|
||||||
|
self._write_last_failed()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def run(self, suite):
|
||||||
|
result = self._run(suite)
|
||||||
|
self._exit(result.wasSuccessful())
|
||||||
|
|
||||||
|
|
||||||
|
class ParallelRunner(ColouredTextRunner):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parallelize(suite):
|
||||||
|
def fdopen(fd, mode, *kwds):
|
||||||
|
stream = orig_fdopen(fd, mode)
|
||||||
|
atexit.register(stream.close)
|
||||||
|
return stream
|
||||||
|
|
||||||
|
# Monkey patch concurrencytest lib bug (fdopen() stream not closed).
|
||||||
|
# https://github.com/cgoldberg/concurrencytest/issues/11
|
||||||
|
orig_fdopen = os.fdopen
|
||||||
|
concurrencytest.os.fdopen = fdopen
|
||||||
|
forker = concurrencytest.fork_for_tests(NWORKERS)
|
||||||
|
return concurrencytest.ConcurrentTestSuite(suite, forker)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _split_suite(suite):
|
||||||
|
serial = unittest.TestSuite()
|
||||||
|
parallel = unittest.TestSuite()
|
||||||
|
for test in suite:
|
||||||
|
if test.countTestCases() == 0:
|
||||||
|
continue
|
||||||
|
elif isinstance(test, unittest.TestSuite):
|
||||||
|
test_class = test._tests[0].__class__
|
||||||
|
elif isinstance(test, unittest.TestCase):
|
||||||
|
test_class = test
|
||||||
|
else:
|
||||||
|
raise TypeError("can't recognize type %r" % test)
|
||||||
|
|
||||||
|
if getattr(test_class, '_serialrun', False):
|
||||||
|
serial.addTest(test)
|
||||||
|
else:
|
||||||
|
parallel.addTest(test)
|
||||||
|
return (serial, parallel)
|
||||||
|
|
||||||
|
def run(self, suite):
|
||||||
|
ser_suite, par_suite = self._split_suite(suite)
|
||||||
|
par_suite = self._parallelize(par_suite)
|
||||||
|
|
||||||
|
# run parallel
|
||||||
|
cprint("starting parallel tests using %s workers" % NWORKERS,
|
||||||
|
"green", bold=True)
|
||||||
|
t = time.time()
|
||||||
|
par = self._run(par_suite)
|
||||||
|
par_elapsed = time.time() - t
|
||||||
|
|
||||||
|
# At this point we should have N zombies (the workers), which
|
||||||
|
# will disappear with wait().
|
||||||
|
orphans = psutil.Process().children()
|
||||||
|
gone, alive = psutil.wait_procs(orphans, timeout=1)
|
||||||
|
if alive:
|
||||||
|
cprint("alive processes %s" % alive, "red")
|
||||||
|
reap_children()
|
||||||
|
|
||||||
|
# run serial
|
||||||
|
t = time.time()
|
||||||
|
ser = self._run(ser_suite)
|
||||||
|
ser_elapsed = time.time() - t
|
||||||
|
|
||||||
|
# print
|
||||||
|
if not par.wasSuccessful() and ser_suite.countTestCases() > 0:
|
||||||
|
par.printErrors() # print them again at the bottom
|
||||||
|
par_fails, par_errs, par_skips = map(len, (par.failures,
|
||||||
|
par.errors,
|
||||||
|
par.skipped))
|
||||||
|
ser_fails, ser_errs, ser_skips = map(len, (ser.failures,
|
||||||
|
ser.errors,
|
||||||
|
ser.skipped))
|
||||||
|
print(textwrap.dedent("""
|
||||||
|
+----------+----------+----------+----------+----------+----------+
|
||||||
|
| | total | failures | errors | skipped | time |
|
||||||
|
+----------+----------+----------+----------+----------+----------+
|
||||||
|
| parallel | %3s | %3s | %3s | %3s | %.2fs |
|
||||||
|
+----------+----------+----------+----------+----------+----------+
|
||||||
|
| serial | %3s | %3s | %3s | %3s | %.2fs |
|
||||||
|
+----------+----------+----------+----------+----------+----------+
|
||||||
|
""" % (par.testsRun, par_fails, par_errs, par_skips, par_elapsed,
|
||||||
|
ser.testsRun, ser_fails, ser_errs, ser_skips, ser_elapsed)))
|
||||||
|
print("Ran %s tests in %.3fs using %s workers" % (
|
||||||
|
par.testsRun + ser.testsRun, par_elapsed + ser_elapsed, NWORKERS))
|
||||||
|
ok = par.wasSuccessful() and ser.wasSuccessful()
|
||||||
|
self._exit(ok)
|
||||||
|
|
||||||
|
|
||||||
|
def get_runner(parallel=False):
|
||||||
|
def warn(msg):
|
||||||
|
cprint(msg + " Running serial tests instead.", "red")
|
||||||
|
if parallel:
|
||||||
|
if psutil.WINDOWS:
|
||||||
|
warn("Can't run parallel tests on Windows.")
|
||||||
|
elif concurrencytest is None:
|
||||||
|
warn("concurrencytest module is not installed.")
|
||||||
|
elif NWORKERS == 1:
|
||||||
|
warn("Only 1 CPU available.")
|
||||||
|
else:
|
||||||
|
return ParallelRunner(verbosity=VERBOSITY)
|
||||||
|
return ColouredTextRunner(verbosity=VERBOSITY)
|
||||||
|
|
||||||
|
|
||||||
|
# Used by test_*,py modules.
|
||||||
|
def run_from_name(name):
|
||||||
|
if CI_TESTING:
|
||||||
|
print_sysinfo()
|
||||||
|
suite = TestLoader().from_name(name)
|
||||||
|
runner = get_runner()
|
||||||
|
runner.run(suite)
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
psutil._set_debug(True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
setup()
|
||||||
|
usage = "python3 -m psutil.tests [opts] [test-name]"
|
||||||
|
parser = optparse.OptionParser(usage=usage, description="run unit tests")
|
||||||
|
parser.add_option("--last-failed",
|
||||||
|
action="store_true", default=False,
|
||||||
|
help="only run last failed tests")
|
||||||
|
parser.add_option("--parallel",
|
||||||
|
action="store_true", default=False,
|
||||||
|
help="run tests in parallel")
|
||||||
|
opts, args = parser.parse_args()
|
||||||
|
|
||||||
|
if not opts.last_failed:
|
||||||
|
safe_rmpath(FAILED_TESTS_FNAME)
|
||||||
|
|
||||||
|
# loader
|
||||||
|
loader = TestLoader()
|
||||||
|
if args:
|
||||||
|
if len(args) > 1:
|
||||||
|
parser.print_usage()
|
||||||
|
return sys.exit(1)
|
||||||
|
else:
|
||||||
|
suite = loader.from_name(args[0])
|
||||||
|
elif opts.last_failed:
|
||||||
|
suite = loader.last_failed()
|
||||||
|
else:
|
||||||
|
suite = loader.all()
|
||||||
|
|
||||||
|
if CI_TESTING:
|
||||||
|
print_sysinfo()
|
||||||
|
runner = get_runner(opts.parallel)
|
||||||
|
runner.run(suite)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'
|
||||||
|
# Copyright (c) 2017, Arnon Yaari
|
||||||
|
# All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""AIX specific tests."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import AIX
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import sh
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not AIX, "AIX only")
|
||||||
|
class AIXSpecificTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_virtual_memory(self):
|
||||||
|
out = sh('/usr/bin/svmon -O unit=KB')
|
||||||
|
re_pattern = r"memory\s*"
|
||||||
|
for field in ("size inuse free pin virtual available mmode").split():
|
||||||
|
re_pattern += r"(?P<%s>\S+)\s+" % (field,)
|
||||||
|
matchobj = re.search(re_pattern, out)
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
matchobj, "svmon command returned unexpected output")
|
||||||
|
|
||||||
|
KB = 1024
|
||||||
|
total = int(matchobj.group("size")) * KB
|
||||||
|
available = int(matchobj.group("available")) * KB
|
||||||
|
used = int(matchobj.group("inuse")) * KB
|
||||||
|
free = int(matchobj.group("free")) * KB
|
||||||
|
|
||||||
|
psutil_result = psutil.virtual_memory()
|
||||||
|
|
||||||
|
# TOLERANCE_SYS_MEM from psutil.tests is not enough. For some reason
|
||||||
|
# we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance
|
||||||
|
# when compared to GBs.
|
||||||
|
TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB
|
||||||
|
self.assertEqual(psutil_result.total, total)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.used, used, delta=TOLERANCE_SYS_MEM)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.available, available, delta=TOLERANCE_SYS_MEM)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.free, free, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swap_memory(self):
|
||||||
|
out = sh('/usr/sbin/lsps -a')
|
||||||
|
# From the man page, "The size is given in megabytes" so we assume
|
||||||
|
# we'll always have 'MB' in the result
|
||||||
|
# TODO maybe try to use "swap -l" to check "used" too, but its units
|
||||||
|
# are not guaranteed to be "MB" so parsing may not be consistent
|
||||||
|
matchobj = re.search(r"(?P<space>\S+)\s+"
|
||||||
|
r"(?P<vol>\S+)\s+"
|
||||||
|
r"(?P<vg>\S+)\s+"
|
||||||
|
r"(?P<size>\d+)MB", out)
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
matchobj, "lsps command returned unexpected output")
|
||||||
|
|
||||||
|
total_mb = int(matchobj.group("size"))
|
||||||
|
MB = 1024 ** 2
|
||||||
|
psutil_result = psutil.swap_memory()
|
||||||
|
# we divide our result by MB instead of multiplying the lsps value by
|
||||||
|
# MB because lsps may round down, so we round down too
|
||||||
|
self.assertEqual(int(psutil_result.total / MB), total_mb)
|
||||||
|
|
||||||
|
def test_cpu_stats(self):
|
||||||
|
out = sh('/usr/bin/mpstat -a')
|
||||||
|
|
||||||
|
re_pattern = r"ALL\s*"
|
||||||
|
for field in ("min maj mpcs mpcr dev soft dec ph cs ics bound rq "
|
||||||
|
"push S3pull S3grd S0rd S1rd S2rd S3rd S4rd S5rd "
|
||||||
|
"sysc").split():
|
||||||
|
re_pattern += r"(?P<%s>\S+)\s+" % (field,)
|
||||||
|
matchobj = re.search(re_pattern, out)
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
matchobj, "mpstat command returned unexpected output")
|
||||||
|
|
||||||
|
# numbers are usually in the millions so 1000 is ok for tolerance
|
||||||
|
CPU_STATS_TOLERANCE = 1000
|
||||||
|
psutil_result = psutil.cpu_stats()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.ctx_switches,
|
||||||
|
int(matchobj.group("cs")),
|
||||||
|
delta=CPU_STATS_TOLERANCE)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.syscalls,
|
||||||
|
int(matchobj.group("sysc")),
|
||||||
|
delta=CPU_STATS_TOLERANCE)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.interrupts,
|
||||||
|
int(matchobj.group("dev")),
|
||||||
|
delta=CPU_STATS_TOLERANCE)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil_result.soft_interrupts,
|
||||||
|
int(matchobj.group("soft")),
|
||||||
|
delta=CPU_STATS_TOLERANCE)
|
||||||
|
|
||||||
|
def test_cpu_count_logical(self):
|
||||||
|
out = sh('/usr/bin/mpstat -a')
|
||||||
|
mpstat_lcpu = int(re.search(r"lcpu=(\d+)", out).group(1))
|
||||||
|
psutil_lcpu = psutil.cpu_count(logical=True)
|
||||||
|
self.assertEqual(mpstat_lcpu, psutil_lcpu)
|
||||||
|
|
||||||
|
def test_net_if_addrs_names(self):
|
||||||
|
out = sh('/etc/ifconfig -l')
|
||||||
|
ifconfig_names = set(out.split())
|
||||||
|
psutil_names = set(psutil.net_if_addrs().keys())
|
||||||
|
self.assertSetEqual(ifconfig_names, psutil_names)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,568 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd.
|
||||||
|
|
||||||
|
|
||||||
|
"""Tests specific to all BSD platforms."""
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import BSD
|
||||||
|
from psutil import FREEBSD
|
||||||
|
from psutil import NETBSD
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil.tests import HAS_BATTERY
|
||||||
|
from psutil.tests import TOLERANCE_SYS_MEM
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import sh
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import terminate
|
||||||
|
from psutil.tests import which
|
||||||
|
|
||||||
|
|
||||||
|
if BSD:
|
||||||
|
from psutil._psutil_posix import getpagesize
|
||||||
|
|
||||||
|
PAGESIZE = getpagesize()
|
||||||
|
# muse requires root privileges
|
||||||
|
MUSE_AVAILABLE = True if os.getuid() == 0 and which('muse') else False
|
||||||
|
else:
|
||||||
|
PAGESIZE = None
|
||||||
|
MUSE_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def sysctl(cmdline):
|
||||||
|
"""Expects a sysctl command with an argument and parse the result
|
||||||
|
returning only the value of interest.
|
||||||
|
"""
|
||||||
|
result = sh("sysctl " + cmdline)
|
||||||
|
if FREEBSD:
|
||||||
|
result = result[result.find(": ") + 2:]
|
||||||
|
elif OPENBSD or NETBSD:
|
||||||
|
result = result[result.find("=") + 1:]
|
||||||
|
try:
|
||||||
|
return int(result)
|
||||||
|
except ValueError:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def muse(field):
|
||||||
|
"""Thin wrapper around 'muse' cmdline utility."""
|
||||||
|
out = sh('muse')
|
||||||
|
for line in out.split('\n'):
|
||||||
|
if line.startswith(field):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("line not found")
|
||||||
|
return int(line.split()[1])
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- All BSD*
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not BSD, "BSD only")
|
||||||
|
class BSDTestCase(PsutilTestCase):
|
||||||
|
"""Generic tests common to all BSD variants."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
@unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD")
|
||||||
|
def test_process_create_time(self):
|
||||||
|
output = sh("ps -o lstart -p %s" % self.pid)
|
||||||
|
start_ps = output.replace('STARTED', '').strip()
|
||||||
|
start_psutil = psutil.Process(self.pid).create_time()
|
||||||
|
start_psutil = time.strftime("%a %b %e %H:%M:%S %Y",
|
||||||
|
time.localtime(start_psutil))
|
||||||
|
self.assertEqual(start_ps, start_psutil)
|
||||||
|
|
||||||
|
def test_disks(self):
|
||||||
|
# test psutil.disk_usage() and psutil.disk_partitions()
|
||||||
|
# against "df -a"
|
||||||
|
def df(path):
|
||||||
|
out = sh('df -k "%s"' % path).strip()
|
||||||
|
lines = out.split('\n')
|
||||||
|
lines.pop(0)
|
||||||
|
line = lines.pop(0)
|
||||||
|
dev, total, used, free = line.split()[:4]
|
||||||
|
if dev == 'none':
|
||||||
|
dev = ''
|
||||||
|
total = int(total) * 1024
|
||||||
|
used = int(used) * 1024
|
||||||
|
free = int(free) * 1024
|
||||||
|
return dev, total, used, free
|
||||||
|
|
||||||
|
for part in psutil.disk_partitions(all=False):
|
||||||
|
usage = psutil.disk_usage(part.mountpoint)
|
||||||
|
dev, total, used, free = df(part.mountpoint)
|
||||||
|
self.assertEqual(part.device, dev)
|
||||||
|
self.assertEqual(usage.total, total)
|
||||||
|
# 10 MB tollerance
|
||||||
|
if abs(usage.free - free) > 10 * 1024 * 1024:
|
||||||
|
raise self.fail("psutil=%s, df=%s" % (usage.free, free))
|
||||||
|
if abs(usage.used - used) > 10 * 1024 * 1024:
|
||||||
|
raise self.fail("psutil=%s, df=%s" % (usage.used, used))
|
||||||
|
|
||||||
|
@unittest.skipIf(not which('sysctl'), "sysctl cmd not available")
|
||||||
|
def test_cpu_count_logical(self):
|
||||||
|
syst = sysctl("hw.ncpu")
|
||||||
|
self.assertEqual(psutil.cpu_count(logical=True), syst)
|
||||||
|
|
||||||
|
@unittest.skipIf(not which('sysctl'), "sysctl cmd not available")
|
||||||
|
def test_virtual_memory_total(self):
|
||||||
|
num = sysctl('hw.physmem')
|
||||||
|
self.assertEqual(num, psutil.virtual_memory().total)
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
for name, stats in psutil.net_if_stats().items():
|
||||||
|
try:
|
||||||
|
out = sh("ifconfig %s" % name)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertEqual(stats.isup, 'RUNNING' in out, msg=out)
|
||||||
|
if "mtu" in out:
|
||||||
|
self.assertEqual(stats.mtu,
|
||||||
|
int(re.findall(r'mtu (\d+)', out)[0]))
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- FreeBSD
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not FREEBSD, "FREEBSD only")
|
||||||
|
class FreeBSDPsutilTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_memory_maps(self):
|
||||||
|
out = sh('procstat -v %s' % self.pid)
|
||||||
|
maps = psutil.Process(self.pid).memory_maps(grouped=False)
|
||||||
|
lines = out.split('\n')[1:]
|
||||||
|
while lines:
|
||||||
|
line = lines.pop()
|
||||||
|
fields = line.split()
|
||||||
|
_, start, stop, perms, res = fields[:5]
|
||||||
|
map = maps.pop()
|
||||||
|
self.assertEqual("%s-%s" % (start, stop), map.addr)
|
||||||
|
self.assertEqual(int(res), map.rss)
|
||||||
|
if not map.path.startswith('['):
|
||||||
|
self.assertEqual(fields[10], map.path)
|
||||||
|
|
||||||
|
def test_exe(self):
|
||||||
|
out = sh('procstat -b %s' % self.pid)
|
||||||
|
self.assertEqual(psutil.Process(self.pid).exe(),
|
||||||
|
out.split('\n')[1].split()[-1])
|
||||||
|
|
||||||
|
def test_cmdline(self):
|
||||||
|
out = sh('procstat -c %s' % self.pid)
|
||||||
|
self.assertEqual(' '.join(psutil.Process(self.pid).cmdline()),
|
||||||
|
' '.join(out.split('\n')[1].split()[2:]))
|
||||||
|
|
||||||
|
def test_uids_gids(self):
|
||||||
|
out = sh('procstat -s %s' % self.pid)
|
||||||
|
euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
uids = p.uids()
|
||||||
|
gids = p.gids()
|
||||||
|
self.assertEqual(uids.real, int(ruid))
|
||||||
|
self.assertEqual(uids.effective, int(euid))
|
||||||
|
self.assertEqual(uids.saved, int(suid))
|
||||||
|
self.assertEqual(gids.real, int(rgid))
|
||||||
|
self.assertEqual(gids.effective, int(egid))
|
||||||
|
self.assertEqual(gids.saved, int(sgid))
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_ctx_switches(self):
|
||||||
|
tested = []
|
||||||
|
out = sh('procstat -r %s' % self.pid)
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
for line in out.split('\n'):
|
||||||
|
line = line.lower().strip()
|
||||||
|
if ' voluntary context' in line:
|
||||||
|
pstat_value = int(line.split()[-1])
|
||||||
|
psutil_value = p.num_ctx_switches().voluntary
|
||||||
|
self.assertEqual(pstat_value, psutil_value)
|
||||||
|
tested.append(None)
|
||||||
|
elif ' involuntary context' in line:
|
||||||
|
pstat_value = int(line.split()[-1])
|
||||||
|
psutil_value = p.num_ctx_switches().involuntary
|
||||||
|
self.assertEqual(pstat_value, psutil_value)
|
||||||
|
tested.append(None)
|
||||||
|
if len(tested) != 2:
|
||||||
|
raise RuntimeError("couldn't find lines match in procstat out")
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_cpu_times(self):
|
||||||
|
tested = []
|
||||||
|
out = sh('procstat -r %s' % self.pid)
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
for line in out.split('\n'):
|
||||||
|
line = line.lower().strip()
|
||||||
|
if 'user time' in line:
|
||||||
|
pstat_value = float('0.' + line.split()[-1].split('.')[-1])
|
||||||
|
psutil_value = p.cpu_times().user
|
||||||
|
self.assertEqual(pstat_value, psutil_value)
|
||||||
|
tested.append(None)
|
||||||
|
elif 'system time' in line:
|
||||||
|
pstat_value = float('0.' + line.split()[-1].split('.')[-1])
|
||||||
|
psutil_value = p.cpu_times().system
|
||||||
|
self.assertEqual(pstat_value, psutil_value)
|
||||||
|
tested.append(None)
|
||||||
|
if len(tested) != 2:
|
||||||
|
raise RuntimeError("couldn't find lines match in procstat out")
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not FREEBSD, "FREEBSD only")
|
||||||
|
class FreeBSDSystemTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_swapinfo():
|
||||||
|
# the last line is always the total
|
||||||
|
output = sh("swapinfo -k").splitlines()[-1]
|
||||||
|
parts = re.split(r'\s+', output)
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
raise ValueError("Can't parse swapinfo: %s" % output)
|
||||||
|
|
||||||
|
# the size is in 1k units, so multiply by 1024
|
||||||
|
total, used, free = (int(p) * 1024 for p in parts[1:4])
|
||||||
|
return total, used, free
|
||||||
|
|
||||||
|
def test_cpu_frequency_against_sysctl(self):
|
||||||
|
# Currently only cpu 0 is frequency is supported in FreeBSD
|
||||||
|
# All other cores use the same frequency.
|
||||||
|
sensor = "dev.cpu.0.freq"
|
||||||
|
try:
|
||||||
|
sysctl_result = int(sysctl(sensor))
|
||||||
|
except RuntimeError:
|
||||||
|
self.skipTest("frequencies not supported by kernel")
|
||||||
|
self.assertEqual(psutil.cpu_freq().current, sysctl_result)
|
||||||
|
|
||||||
|
sensor = "dev.cpu.0.freq_levels"
|
||||||
|
sysctl_result = sysctl(sensor)
|
||||||
|
# sysctl returns a string of the format:
|
||||||
|
# <freq_level_1>/<voltage_level_1> <freq_level_2>/<voltage_level_2>...
|
||||||
|
# Ordered highest available to lowest available.
|
||||||
|
max_freq = int(sysctl_result.split()[0].split("/")[0])
|
||||||
|
min_freq = int(sysctl_result.split()[-1].split("/")[0])
|
||||||
|
self.assertEqual(psutil.cpu_freq().max, max_freq)
|
||||||
|
self.assertEqual(psutil.cpu_freq().min, min_freq)
|
||||||
|
|
||||||
|
# --- virtual_memory(); tests against sysctl
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_active(self):
|
||||||
|
syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().active, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_inactive(self):
|
||||||
|
syst = sysctl("vm.stats.vm.v_inactive_count") * PAGESIZE
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().inactive, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_wired(self):
|
||||||
|
syst = sysctl("vm.stats.vm.v_wire_count") * PAGESIZE
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().wired, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_cached(self):
|
||||||
|
syst = sysctl("vm.stats.vm.v_cache_count") * PAGESIZE
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().cached, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_free(self):
|
||||||
|
syst = sysctl("vm.stats.vm.v_free_count") * PAGESIZE
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().free, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_buffers(self):
|
||||||
|
syst = sysctl("vfs.bufspace")
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().buffers, syst,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
# --- virtual_memory(); tests against muse
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
def test_muse_vmem_total(self):
|
||||||
|
num = muse('Total')
|
||||||
|
self.assertEqual(psutil.virtual_memory().total, num)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_active(self):
|
||||||
|
num = muse('Active')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().active, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_inactive(self):
|
||||||
|
num = muse('Inactive')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().inactive, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_wired(self):
|
||||||
|
num = muse('Wired')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().wired, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_cached(self):
|
||||||
|
num = muse('Cache')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().cached, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_free(self):
|
||||||
|
num = muse('Free')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().free, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@unittest.skipIf(not MUSE_AVAILABLE, "muse not installed")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_muse_vmem_buffers(self):
|
||||||
|
num = muse('Buffer')
|
||||||
|
self.assertAlmostEqual(psutil.virtual_memory().buffers, num,
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_cpu_stats_ctx_switches(self):
|
||||||
|
self.assertAlmostEqual(psutil.cpu_stats().ctx_switches,
|
||||||
|
sysctl('vm.stats.sys.v_swtch'), delta=1000)
|
||||||
|
|
||||||
|
def test_cpu_stats_interrupts(self):
|
||||||
|
self.assertAlmostEqual(psutil.cpu_stats().interrupts,
|
||||||
|
sysctl('vm.stats.sys.v_intr'), delta=1000)
|
||||||
|
|
||||||
|
def test_cpu_stats_soft_interrupts(self):
|
||||||
|
self.assertAlmostEqual(psutil.cpu_stats().soft_interrupts,
|
||||||
|
sysctl('vm.stats.sys.v_soft'), delta=1000)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_cpu_stats_syscalls(self):
|
||||||
|
# pretty high tolerance but it looks like it's OK.
|
||||||
|
self.assertAlmostEqual(psutil.cpu_stats().syscalls,
|
||||||
|
sysctl('vm.stats.sys.v_syscall'), delta=200000)
|
||||||
|
|
||||||
|
# def test_cpu_stats_traps(self):
|
||||||
|
# self.assertAlmostEqual(psutil.cpu_stats().traps,
|
||||||
|
# sysctl('vm.stats.sys.v_trap'), delta=1000)
|
||||||
|
|
||||||
|
# --- swap memory
|
||||||
|
|
||||||
|
def test_swapmem_free(self):
|
||||||
|
total, used, free = self.parse_swapinfo()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.swap_memory().free, free, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swapmem_used(self):
|
||||||
|
total, used, free = self.parse_swapinfo()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.swap_memory().used, used, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swapmem_total(self):
|
||||||
|
total, used, free = self.parse_swapinfo()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.swap_memory().total, total, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
# --- others
|
||||||
|
|
||||||
|
def test_boot_time(self):
|
||||||
|
s = sysctl('sysctl kern.boottime')
|
||||||
|
s = s[s.find(" sec = ") + 7:]
|
||||||
|
s = s[:s.find(',')]
|
||||||
|
btime = int(s)
|
||||||
|
self.assertEqual(btime, psutil.boot_time())
|
||||||
|
|
||||||
|
# --- sensors_battery
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_sensors_battery(self):
|
||||||
|
def secs2hours(secs):
|
||||||
|
m, s = divmod(secs, 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
return "%d:%02d" % (h, m)
|
||||||
|
|
||||||
|
out = sh("acpiconf -i 0")
|
||||||
|
fields = dict([(x.split('\t')[0], x.split('\t')[-1])
|
||||||
|
for x in out.split("\n")])
|
||||||
|
metrics = psutil.sensors_battery()
|
||||||
|
percent = int(fields['Remaining capacity:'].replace('%', ''))
|
||||||
|
remaining_time = fields['Remaining time:']
|
||||||
|
self.assertEqual(metrics.percent, percent)
|
||||||
|
if remaining_time == 'unknown':
|
||||||
|
self.assertEqual(metrics.secsleft, psutil.POWER_TIME_UNLIMITED)
|
||||||
|
else:
|
||||||
|
self.assertEqual(secs2hours(metrics.secsleft), remaining_time)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_sensors_battery_against_sysctl(self):
|
||||||
|
self.assertEqual(psutil.sensors_battery().percent,
|
||||||
|
sysctl("hw.acpi.battery.life"))
|
||||||
|
self.assertEqual(psutil.sensors_battery().power_plugged,
|
||||||
|
sysctl("hw.acpi.acline") == 1)
|
||||||
|
secsleft = psutil.sensors_battery().secsleft
|
||||||
|
if secsleft < 0:
|
||||||
|
self.assertEqual(sysctl("hw.acpi.battery.time"), -1)
|
||||||
|
else:
|
||||||
|
self.assertEqual(secsleft, sysctl("hw.acpi.battery.time") * 60)
|
||||||
|
|
||||||
|
@unittest.skipIf(HAS_BATTERY, "has battery")
|
||||||
|
def test_sensors_battery_no_battery(self):
|
||||||
|
# If no battery is present one of these calls is supposed
|
||||||
|
# to fail, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1074
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
sysctl("hw.acpi.battery.life")
|
||||||
|
sysctl("hw.acpi.battery.time")
|
||||||
|
sysctl("hw.acpi.acline")
|
||||||
|
self.assertIsNone(psutil.sensors_battery())
|
||||||
|
|
||||||
|
# --- sensors_temperatures
|
||||||
|
|
||||||
|
def test_sensors_temperatures_against_sysctl(self):
|
||||||
|
num_cpus = psutil.cpu_count(True)
|
||||||
|
for cpu in range(num_cpus):
|
||||||
|
sensor = "dev.cpu.%s.temperature" % cpu
|
||||||
|
# sysctl returns a string in the format 46.0C
|
||||||
|
try:
|
||||||
|
sysctl_result = int(float(sysctl(sensor)[:-1]))
|
||||||
|
except RuntimeError:
|
||||||
|
self.skipTest("temperatures not supported by kernel")
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.sensors_temperatures()["coretemp"][cpu].current,
|
||||||
|
sysctl_result, delta=10)
|
||||||
|
|
||||||
|
sensor = "dev.cpu.%s.coretemp.tjmax" % cpu
|
||||||
|
sysctl_result = int(float(sysctl(sensor)[:-1]))
|
||||||
|
self.assertEqual(
|
||||||
|
psutil.sensors_temperatures()["coretemp"][cpu].high,
|
||||||
|
sysctl_result)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- OpenBSD
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not OPENBSD, "OPENBSD only")
|
||||||
|
class OpenBSDTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_boot_time(self):
|
||||||
|
s = sysctl('kern.boottime')
|
||||||
|
sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y")
|
||||||
|
psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time())
|
||||||
|
self.assertEqual(sys_bt, psutil_bt)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# --- NetBSD
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not NETBSD, "NETBSD only")
|
||||||
|
class NetBSDTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_meminfo(look_for):
|
||||||
|
with open('/proc/meminfo', 'rt') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(look_for):
|
||||||
|
return int(line.split()[1]) * 1024
|
||||||
|
raise ValueError("can't find %s" % look_for)
|
||||||
|
|
||||||
|
def test_vmem_total(self):
|
||||||
|
self.assertEqual(
|
||||||
|
psutil.virtual_memory().total, self.parse_meminfo("MemTotal:"))
|
||||||
|
|
||||||
|
def test_vmem_free(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.virtual_memory().free, self.parse_meminfo("MemFree:"),
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_vmem_buffers(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.virtual_memory().buffers, self.parse_meminfo("Buffers:"),
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_vmem_shared(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.virtual_memory().shared, self.parse_meminfo("MemShared:"),
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swapmem_total(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.swap_memory().total, self.parse_meminfo("SwapTotal:"),
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swapmem_free(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.swap_memory().free, self.parse_meminfo("SwapFree:"),
|
||||||
|
delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
def test_swapmem_used(self):
|
||||||
|
smem = psutil.swap_memory()
|
||||||
|
self.assertEqual(smem.used, smem.total - smem.free)
|
||||||
|
|
||||||
|
def test_cpu_stats_interrupts(self):
|
||||||
|
with open('/proc/stat', 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(b'intr'):
|
||||||
|
interrupts = int(line.split()[1])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("couldn't find line")
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.cpu_stats().interrupts, interrupts, delta=1000)
|
||||||
|
|
||||||
|
def test_cpu_stats_ctx_switches(self):
|
||||||
|
with open('/proc/stat', 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(b'ctxt'):
|
||||||
|
ctx_switches = int(line.split()[1])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("couldn't find line")
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,554 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Tests for net_connections() and Process.connections() APIs."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import textwrap
|
||||||
|
import unittest
|
||||||
|
from contextlib import closing
|
||||||
|
from socket import AF_INET
|
||||||
|
from socket import AF_INET6
|
||||||
|
from socket import SOCK_DGRAM
|
||||||
|
from socket import SOCK_STREAM
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import FREEBSD
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import NETBSD
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._common import supports_ipv6
|
||||||
|
from psutil._compat import PY3
|
||||||
|
from psutil.tests import AF_UNIX
|
||||||
|
from psutil.tests import HAS_CONNECTIONS_UNIX
|
||||||
|
from psutil.tests import SKIP_SYSCONS
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import bind_socket
|
||||||
|
from psutil.tests import bind_unix_socket
|
||||||
|
from psutil.tests import check_connection_ntuple
|
||||||
|
from psutil.tests import create_sockets
|
||||||
|
from psutil.tests import reap_children
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import serialrun
|
||||||
|
from psutil.tests import skip_on_access_denied
|
||||||
|
from psutil.tests import tcp_socketpair
|
||||||
|
from psutil.tests import unix_socketpair
|
||||||
|
from psutil.tests import wait_for_file
|
||||||
|
|
||||||
|
|
||||||
|
thisproc = psutil.Process()
|
||||||
|
SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object())
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
class ConnectionTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if not (NETBSD or FREEBSD):
|
||||||
|
# process opens a UNIX socket to /var/log/run.
|
||||||
|
cons = thisproc.connections(kind='all')
|
||||||
|
assert not cons, cons
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if not (FREEBSD or NETBSD):
|
||||||
|
# Make sure we closed all resources.
|
||||||
|
# NetBSD opens a UNIX socket to /var/log/run.
|
||||||
|
cons = thisproc.connections(kind='all')
|
||||||
|
assert not cons, cons
|
||||||
|
|
||||||
|
def compare_procsys_connections(self, pid, proc_cons, kind='all'):
|
||||||
|
"""Given a process PID and its list of connections compare
|
||||||
|
those against system-wide connections retrieved via
|
||||||
|
psutil.net_connections.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sys_cons = psutil.net_connections(kind=kind)
|
||||||
|
except psutil.AccessDenied:
|
||||||
|
# On MACOS, system-wide connections are retrieved by iterating
|
||||||
|
# over all processes
|
||||||
|
if MACOS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
# Filter for this proc PID and exlucde PIDs from the tuple.
|
||||||
|
sys_cons = [c[:-1] for c in sys_cons if c.pid == pid]
|
||||||
|
sys_cons.sort()
|
||||||
|
proc_cons.sort()
|
||||||
|
self.assertEqual(proc_cons, sys_cons)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasicOperations(ConnectionTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf(SKIP_SYSCONS, "requires root")
|
||||||
|
def test_system(self):
|
||||||
|
with create_sockets():
|
||||||
|
for conn in psutil.net_connections(kind='all'):
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
|
||||||
|
def test_process(self):
|
||||||
|
with create_sockets():
|
||||||
|
for conn in psutil.Process().connections(kind='all'):
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
|
||||||
|
def test_invalid_kind(self):
|
||||||
|
self.assertRaises(ValueError, thisproc.connections, kind='???')
|
||||||
|
self.assertRaises(ValueError, psutil.net_connections, kind='???')
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
class TestUnconnectedSockets(ConnectionTestCase):
|
||||||
|
"""Tests sockets which are open but not connected to anything."""
|
||||||
|
|
||||||
|
def get_conn_from_sock(self, sock):
|
||||||
|
cons = thisproc.connections(kind='all')
|
||||||
|
smap = dict([(c.fd, c) for c in cons])
|
||||||
|
if NETBSD or FREEBSD:
|
||||||
|
# NetBSD opens a UNIX socket to /var/log/run
|
||||||
|
# so there may be more connections.
|
||||||
|
return smap[sock.fileno()]
|
||||||
|
else:
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
if cons[0].fd != -1:
|
||||||
|
self.assertEqual(smap[sock.fileno()].fd, sock.fileno())
|
||||||
|
return cons[0]
|
||||||
|
|
||||||
|
def check_socket(self, sock):
|
||||||
|
"""Given a socket, makes sure it matches the one obtained
|
||||||
|
via psutil. It assumes this process created one connection
|
||||||
|
only (the one supposed to be checked).
|
||||||
|
"""
|
||||||
|
conn = self.get_conn_from_sock(sock)
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
|
||||||
|
# fd, family, type
|
||||||
|
if conn.fd != -1:
|
||||||
|
self.assertEqual(conn.fd, sock.fileno())
|
||||||
|
self.assertEqual(conn.family, sock.family)
|
||||||
|
# see: http://bugs.python.org/issue30204
|
||||||
|
self.assertEqual(
|
||||||
|
conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE))
|
||||||
|
|
||||||
|
# local address
|
||||||
|
laddr = sock.getsockname()
|
||||||
|
if not laddr and PY3 and isinstance(laddr, bytes):
|
||||||
|
# See: http://bugs.python.org/issue30205
|
||||||
|
laddr = laddr.decode()
|
||||||
|
if sock.family == AF_INET6:
|
||||||
|
laddr = laddr[:2]
|
||||||
|
if sock.family == AF_UNIX and OPENBSD:
|
||||||
|
# No addresses are set for UNIX sockets on OpenBSD.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertEqual(conn.laddr, laddr)
|
||||||
|
|
||||||
|
# XXX Solaris can't retrieve system-wide UNIX sockets
|
||||||
|
if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX:
|
||||||
|
cons = thisproc.connections(kind='all')
|
||||||
|
self.compare_procsys_connections(os.getpid(), cons, kind='all')
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def test_tcp_v4(self):
|
||||||
|
addr = ("127.0.0.1", 0)
|
||||||
|
with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_LISTEN)
|
||||||
|
|
||||||
|
@unittest.skipIf(not supports_ipv6(), "IPv6 not supported")
|
||||||
|
def test_tcp_v6(self):
|
||||||
|
addr = ("::1", 0)
|
||||||
|
with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_LISTEN)
|
||||||
|
|
||||||
|
def test_udp_v4(self):
|
||||||
|
addr = ("127.0.0.1", 0)
|
||||||
|
with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_NONE)
|
||||||
|
|
||||||
|
@unittest.skipIf(not supports_ipv6(), "IPv6 not supported")
|
||||||
|
def test_udp_v6(self):
|
||||||
|
addr = ("::1", 0)
|
||||||
|
with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_NONE)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, 'POSIX only')
|
||||||
|
def test_unix_tcp(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_NONE)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, 'POSIX only')
|
||||||
|
def test_unix_udp(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock:
|
||||||
|
conn = self.check_socket(sock)
|
||||||
|
assert not conn.raddr
|
||||||
|
self.assertEqual(conn.status, psutil.CONN_NONE)
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
class TestConnectedSocket(ConnectionTestCase):
|
||||||
|
"""Test socket pairs which are are actually connected to
|
||||||
|
each other.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# On SunOS, even after we close() it, the server socket stays around
|
||||||
|
# in TIME_WAIT state.
|
||||||
|
@unittest.skipIf(SUNOS, "unreliable on SUONS")
|
||||||
|
def test_tcp(self):
|
||||||
|
addr = ("127.0.0.1", 0)
|
||||||
|
assert not thisproc.connections(kind='tcp4')
|
||||||
|
server, client = tcp_socketpair(AF_INET, addr=addr)
|
||||||
|
try:
|
||||||
|
cons = thisproc.connections(kind='tcp4')
|
||||||
|
self.assertEqual(len(cons), 2)
|
||||||
|
self.assertEqual(cons[0].status, psutil.CONN_ESTABLISHED)
|
||||||
|
self.assertEqual(cons[1].status, psutil.CONN_ESTABLISHED)
|
||||||
|
# May not be fast enough to change state so it stays
|
||||||
|
# commenteed.
|
||||||
|
# client.close()
|
||||||
|
# cons = thisproc.connections(kind='all')
|
||||||
|
# self.assertEqual(len(cons), 1)
|
||||||
|
# self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT)
|
||||||
|
finally:
|
||||||
|
server.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, 'POSIX only')
|
||||||
|
def test_unix(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
server, client = unix_socketpair(testfn)
|
||||||
|
try:
|
||||||
|
cons = thisproc.connections(kind='unix')
|
||||||
|
assert not (cons[0].laddr and cons[0].raddr)
|
||||||
|
assert not (cons[1].laddr and cons[1].raddr)
|
||||||
|
if NETBSD or FREEBSD:
|
||||||
|
# On NetBSD creating a UNIX socket will cause
|
||||||
|
# a UNIX connection to /var/run/log.
|
||||||
|
cons = [c for c in cons if c.raddr != '/var/run/log']
|
||||||
|
self.assertEqual(len(cons), 2, msg=cons)
|
||||||
|
if LINUX or FREEBSD or SUNOS:
|
||||||
|
# remote path is never set
|
||||||
|
self.assertEqual(cons[0].raddr, "")
|
||||||
|
self.assertEqual(cons[1].raddr, "")
|
||||||
|
# one local address should though
|
||||||
|
self.assertEqual(testfn, cons[0].laddr or cons[1].laddr)
|
||||||
|
elif OPENBSD:
|
||||||
|
# No addresses whatsoever here.
|
||||||
|
for addr in (cons[0].laddr, cons[0].raddr,
|
||||||
|
cons[1].laddr, cons[1].raddr):
|
||||||
|
self.assertEqual(addr, "")
|
||||||
|
else:
|
||||||
|
# On other systems either the laddr or raddr
|
||||||
|
# of both peers are set.
|
||||||
|
self.assertEqual(cons[0].laddr or cons[1].laddr, testfn)
|
||||||
|
self.assertEqual(cons[0].raddr or cons[1].raddr, testfn)
|
||||||
|
finally:
|
||||||
|
server.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilters(ConnectionTestCase):
|
||||||
|
|
||||||
|
def test_filters(self):
|
||||||
|
def check(kind, families, types):
|
||||||
|
for conn in thisproc.connections(kind=kind):
|
||||||
|
self.assertIn(conn.family, families)
|
||||||
|
self.assertIn(conn.type, types)
|
||||||
|
if not SKIP_SYSCONS:
|
||||||
|
for conn in psutil.net_connections(kind=kind):
|
||||||
|
self.assertIn(conn.family, families)
|
||||||
|
self.assertIn(conn.type, types)
|
||||||
|
|
||||||
|
with create_sockets():
|
||||||
|
check('all',
|
||||||
|
[AF_INET, AF_INET6, AF_UNIX],
|
||||||
|
[SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET])
|
||||||
|
check('inet',
|
||||||
|
[AF_INET, AF_INET6],
|
||||||
|
[SOCK_STREAM, SOCK_DGRAM])
|
||||||
|
check('inet4',
|
||||||
|
[AF_INET],
|
||||||
|
[SOCK_STREAM, SOCK_DGRAM])
|
||||||
|
check('tcp',
|
||||||
|
[AF_INET, AF_INET6],
|
||||||
|
[SOCK_STREAM])
|
||||||
|
check('tcp4',
|
||||||
|
[AF_INET],
|
||||||
|
[SOCK_STREAM])
|
||||||
|
check('tcp6',
|
||||||
|
[AF_INET6],
|
||||||
|
[SOCK_STREAM])
|
||||||
|
check('udp',
|
||||||
|
[AF_INET, AF_INET6],
|
||||||
|
[SOCK_DGRAM])
|
||||||
|
check('udp4',
|
||||||
|
[AF_INET],
|
||||||
|
[SOCK_DGRAM])
|
||||||
|
check('udp6',
|
||||||
|
[AF_INET6],
|
||||||
|
[SOCK_DGRAM])
|
||||||
|
if HAS_CONNECTIONS_UNIX:
|
||||||
|
check('unix',
|
||||||
|
[AF_UNIX],
|
||||||
|
[SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET])
|
||||||
|
|
||||||
|
@skip_on_access_denied(only_if=MACOS)
|
||||||
|
def test_combos(self):
|
||||||
|
reap_children()
|
||||||
|
|
||||||
|
def check_conn(proc, conn, family, type, laddr, raddr, status, kinds):
|
||||||
|
all_kinds = ("all", "inet", "inet4", "inet6", "tcp", "tcp4",
|
||||||
|
"tcp6", "udp", "udp4", "udp6")
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
self.assertEqual(conn.family, family)
|
||||||
|
self.assertEqual(conn.type, type)
|
||||||
|
self.assertEqual(conn.laddr, laddr)
|
||||||
|
self.assertEqual(conn.raddr, raddr)
|
||||||
|
self.assertEqual(conn.status, status)
|
||||||
|
for kind in all_kinds:
|
||||||
|
cons = proc.connections(kind=kind)
|
||||||
|
if kind in kinds:
|
||||||
|
assert cons
|
||||||
|
else:
|
||||||
|
assert not cons, cons
|
||||||
|
# compare against system-wide connections
|
||||||
|
# XXX Solaris can't retrieve system-wide UNIX
|
||||||
|
# sockets.
|
||||||
|
if HAS_CONNECTIONS_UNIX:
|
||||||
|
self.compare_procsys_connections(proc.pid, [conn])
|
||||||
|
|
||||||
|
tcp_template = textwrap.dedent("""
|
||||||
|
import socket, time
|
||||||
|
s = socket.socket({family}, socket.SOCK_STREAM)
|
||||||
|
s.bind(('{addr}', 0))
|
||||||
|
s.listen(5)
|
||||||
|
with open('{testfn}', 'w') as f:
|
||||||
|
f.write(str(s.getsockname()[:2]))
|
||||||
|
time.sleep(60)
|
||||||
|
""")
|
||||||
|
|
||||||
|
udp_template = textwrap.dedent("""
|
||||||
|
import socket, time
|
||||||
|
s = socket.socket({family}, socket.SOCK_DGRAM)
|
||||||
|
s.bind(('{addr}', 0))
|
||||||
|
with open('{testfn}', 'w') as f:
|
||||||
|
f.write(str(s.getsockname()[:2]))
|
||||||
|
time.sleep(60)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# must be relative on Windows
|
||||||
|
testfile = os.path.basename(self.get_testfn(dir=os.getcwd()))
|
||||||
|
tcp4_template = tcp_template.format(
|
||||||
|
family=int(AF_INET), addr="127.0.0.1", testfn=testfile)
|
||||||
|
udp4_template = udp_template.format(
|
||||||
|
family=int(AF_INET), addr="127.0.0.1", testfn=testfile)
|
||||||
|
tcp6_template = tcp_template.format(
|
||||||
|
family=int(AF_INET6), addr="::1", testfn=testfile)
|
||||||
|
udp6_template = udp_template.format(
|
||||||
|
family=int(AF_INET6), addr="::1", testfn=testfile)
|
||||||
|
|
||||||
|
# launch various subprocess instantiating a socket of various
|
||||||
|
# families and types to enrich psutil results
|
||||||
|
tcp4_proc = self.pyrun(tcp4_template)
|
||||||
|
tcp4_addr = eval(wait_for_file(testfile, delete=True))
|
||||||
|
udp4_proc = self.pyrun(udp4_template)
|
||||||
|
udp4_addr = eval(wait_for_file(testfile, delete=True))
|
||||||
|
if supports_ipv6():
|
||||||
|
tcp6_proc = self.pyrun(tcp6_template)
|
||||||
|
tcp6_addr = eval(wait_for_file(testfile, delete=True))
|
||||||
|
udp6_proc = self.pyrun(udp6_template)
|
||||||
|
udp6_addr = eval(wait_for_file(testfile, delete=True))
|
||||||
|
else:
|
||||||
|
tcp6_proc = None
|
||||||
|
udp6_proc = None
|
||||||
|
tcp6_addr = None
|
||||||
|
udp6_addr = None
|
||||||
|
|
||||||
|
for p in thisproc.children():
|
||||||
|
cons = p.connections()
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
for conn in cons:
|
||||||
|
# TCP v4
|
||||||
|
if p.pid == tcp4_proc.pid:
|
||||||
|
check_conn(p, conn, AF_INET, SOCK_STREAM, tcp4_addr, (),
|
||||||
|
psutil.CONN_LISTEN,
|
||||||
|
("all", "inet", "inet4", "tcp", "tcp4"))
|
||||||
|
# UDP v4
|
||||||
|
elif p.pid == udp4_proc.pid:
|
||||||
|
check_conn(p, conn, AF_INET, SOCK_DGRAM, udp4_addr, (),
|
||||||
|
psutil.CONN_NONE,
|
||||||
|
("all", "inet", "inet4", "udp", "udp4"))
|
||||||
|
# TCP v6
|
||||||
|
elif p.pid == getattr(tcp6_proc, "pid", None):
|
||||||
|
check_conn(p, conn, AF_INET6, SOCK_STREAM, tcp6_addr, (),
|
||||||
|
psutil.CONN_LISTEN,
|
||||||
|
("all", "inet", "inet6", "tcp", "tcp6"))
|
||||||
|
# UDP v6
|
||||||
|
elif p.pid == getattr(udp6_proc, "pid", None):
|
||||||
|
check_conn(p, conn, AF_INET6, SOCK_DGRAM, udp6_addr, (),
|
||||||
|
psutil.CONN_NONE,
|
||||||
|
("all", "inet", "inet6", "udp", "udp6"))
|
||||||
|
|
||||||
|
def test_count(self):
|
||||||
|
with create_sockets():
|
||||||
|
# tcp
|
||||||
|
cons = thisproc.connections(kind='tcp')
|
||||||
|
self.assertEqual(len(cons), 2 if supports_ipv6() else 1)
|
||||||
|
for conn in cons:
|
||||||
|
self.assertIn(conn.family, (AF_INET, AF_INET6))
|
||||||
|
self.assertEqual(conn.type, SOCK_STREAM)
|
||||||
|
# tcp4
|
||||||
|
cons = thisproc.connections(kind='tcp4')
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
self.assertEqual(cons[0].family, AF_INET)
|
||||||
|
self.assertEqual(cons[0].type, SOCK_STREAM)
|
||||||
|
# tcp6
|
||||||
|
if supports_ipv6():
|
||||||
|
cons = thisproc.connections(kind='tcp6')
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
self.assertEqual(cons[0].family, AF_INET6)
|
||||||
|
self.assertEqual(cons[0].type, SOCK_STREAM)
|
||||||
|
# udp
|
||||||
|
cons = thisproc.connections(kind='udp')
|
||||||
|
self.assertEqual(len(cons), 2 if supports_ipv6() else 1)
|
||||||
|
for conn in cons:
|
||||||
|
self.assertIn(conn.family, (AF_INET, AF_INET6))
|
||||||
|
self.assertEqual(conn.type, SOCK_DGRAM)
|
||||||
|
# udp4
|
||||||
|
cons = thisproc.connections(kind='udp4')
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
self.assertEqual(cons[0].family, AF_INET)
|
||||||
|
self.assertEqual(cons[0].type, SOCK_DGRAM)
|
||||||
|
# udp6
|
||||||
|
if supports_ipv6():
|
||||||
|
cons = thisproc.connections(kind='udp6')
|
||||||
|
self.assertEqual(len(cons), 1)
|
||||||
|
self.assertEqual(cons[0].family, AF_INET6)
|
||||||
|
self.assertEqual(cons[0].type, SOCK_DGRAM)
|
||||||
|
# inet
|
||||||
|
cons = thisproc.connections(kind='inet')
|
||||||
|
self.assertEqual(len(cons), 4 if supports_ipv6() else 2)
|
||||||
|
for conn in cons:
|
||||||
|
self.assertIn(conn.family, (AF_INET, AF_INET6))
|
||||||
|
self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM))
|
||||||
|
# inet6
|
||||||
|
if supports_ipv6():
|
||||||
|
cons = thisproc.connections(kind='inet6')
|
||||||
|
self.assertEqual(len(cons), 2)
|
||||||
|
for conn in cons:
|
||||||
|
self.assertEqual(conn.family, AF_INET6)
|
||||||
|
self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM))
|
||||||
|
# Skipped on BSD becayse by default the Python process
|
||||||
|
# creates a UNIX socket to '/var/run/log'.
|
||||||
|
if HAS_CONNECTIONS_UNIX and not (FREEBSD or NETBSD):
|
||||||
|
cons = thisproc.connections(kind='unix')
|
||||||
|
self.assertEqual(len(cons), 3)
|
||||||
|
for conn in cons:
|
||||||
|
self.assertEqual(conn.family, AF_UNIX)
|
||||||
|
self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM))
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(SKIP_SYSCONS, "requires root")
|
||||||
|
class TestSystemWideConnections(ConnectionTestCase):
|
||||||
|
"""Tests for net_connections()."""
|
||||||
|
|
||||||
|
def test_it(self):
|
||||||
|
def check(cons, families, types_):
|
||||||
|
for conn in cons:
|
||||||
|
self.assertIn(conn.family, families, msg=conn)
|
||||||
|
if conn.family != AF_UNIX:
|
||||||
|
self.assertIn(conn.type, types_, msg=conn)
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
|
||||||
|
with create_sockets():
|
||||||
|
from psutil._common import conn_tmap
|
||||||
|
for kind, groups in conn_tmap.items():
|
||||||
|
# XXX: SunOS does not retrieve UNIX sockets.
|
||||||
|
if kind == 'unix' and not HAS_CONNECTIONS_UNIX:
|
||||||
|
continue
|
||||||
|
families, types_ = groups
|
||||||
|
cons = psutil.net_connections(kind)
|
||||||
|
self.assertEqual(len(cons), len(set(cons)))
|
||||||
|
check(cons, families, types_)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_multi_sockets_procs(self):
|
||||||
|
# Creates multiple sub processes, each creating different
|
||||||
|
# sockets. For each process check that proc.connections()
|
||||||
|
# and net_connections() return the same results.
|
||||||
|
# This is done mainly to check whether net_connections()'s
|
||||||
|
# pid is properly set, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1013
|
||||||
|
with create_sockets() as socks:
|
||||||
|
expected = len(socks)
|
||||||
|
pids = []
|
||||||
|
times = 10
|
||||||
|
fnames = []
|
||||||
|
for i in range(times):
|
||||||
|
fname = self.get_testfn()
|
||||||
|
fnames.append(fname)
|
||||||
|
src = textwrap.dedent("""\
|
||||||
|
import time, os
|
||||||
|
from psutil.tests import create_sockets
|
||||||
|
with create_sockets():
|
||||||
|
with open(r'%s', 'w') as f:
|
||||||
|
f.write("hello")
|
||||||
|
time.sleep(60)
|
||||||
|
""" % fname)
|
||||||
|
sproc = self.pyrun(src)
|
||||||
|
pids.append(sproc.pid)
|
||||||
|
|
||||||
|
# sync
|
||||||
|
for fname in fnames:
|
||||||
|
wait_for_file(fname)
|
||||||
|
|
||||||
|
syscons = [x for x in psutil.net_connections(kind='all') if x.pid
|
||||||
|
in pids]
|
||||||
|
for pid in pids:
|
||||||
|
self.assertEqual(len([x for x in syscons if x.pid == pid]),
|
||||||
|
expected)
|
||||||
|
p = psutil.Process(pid)
|
||||||
|
self.assertEqual(len(p.connections('all')), expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMisc(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_connection_constants(self):
|
||||||
|
ints = []
|
||||||
|
strs = []
|
||||||
|
for name in dir(psutil):
|
||||||
|
if name.startswith('CONN_'):
|
||||||
|
num = getattr(psutil, name)
|
||||||
|
str_ = str(num)
|
||||||
|
assert str_.isupper(), str_
|
||||||
|
self.assertNotIn(str, strs)
|
||||||
|
self.assertNotIn(num, ints)
|
||||||
|
ints.append(num)
|
||||||
|
strs.append(str_)
|
||||||
|
if SUNOS:
|
||||||
|
psutil.CONN_IDLE
|
||||||
|
psutil.CONN_BOUND
|
||||||
|
if WINDOWS:
|
||||||
|
psutil.CONN_DELETE_TCB
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,735 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Contracts tests. These tests mainly check API sanity in terms of
|
||||||
|
returned types and APIs availability.
|
||||||
|
Some of these are duplicates of tests test_system.py and test_process.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import AIX
|
||||||
|
from psutil import BSD
|
||||||
|
from psutil import FREEBSD
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import NETBSD
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import OSX
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._compat import FileNotFoundError
|
||||||
|
from psutil._compat import long
|
||||||
|
from psutil._compat import range
|
||||||
|
from psutil.tests import APPVEYOR
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import GITHUB_ACTIONS
|
||||||
|
from psutil.tests import HAS_CPU_FREQ
|
||||||
|
from psutil.tests import HAS_NET_IO_COUNTERS
|
||||||
|
from psutil.tests import HAS_SENSORS_FANS
|
||||||
|
from psutil.tests import HAS_SENSORS_TEMPERATURES
|
||||||
|
from psutil.tests import PYPY
|
||||||
|
from psutil.tests import SKIP_SYSCONS
|
||||||
|
from psutil.tests import VALID_PROC_STATUSES
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import check_connection_ntuple
|
||||||
|
from psutil.tests import create_sockets
|
||||||
|
from psutil.tests import enum
|
||||||
|
from psutil.tests import is_namedtuple
|
||||||
|
from psutil.tests import kernel_version
|
||||||
|
from psutil.tests import process_namespace
|
||||||
|
from psutil.tests import serialrun
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- APIs availability
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# Make sure code reflects what doc promises in terms of APIs
|
||||||
|
# availability.
|
||||||
|
|
||||||
|
class TestAvailConstantsAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_PROCFS_PATH(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "PROCFS_PATH"),
|
||||||
|
LINUX or SUNOS or AIX)
|
||||||
|
|
||||||
|
def test_win_priority(self):
|
||||||
|
ae = self.assertEqual
|
||||||
|
ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS)
|
||||||
|
|
||||||
|
def test_linux_ioprio_linux(self):
|
||||||
|
ae = self.assertEqual
|
||||||
|
ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX)
|
||||||
|
|
||||||
|
def test_linux_ioprio_windows(self):
|
||||||
|
ae = self.assertEqual
|
||||||
|
ae(hasattr(psutil, "IOPRIO_HIGH"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_NORMAL"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS)
|
||||||
|
ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS)
|
||||||
|
|
||||||
|
@unittest.skipIf(GITHUB_ACTIONS and LINUX,
|
||||||
|
"unsupported on GITHUB_ACTIONS + LINUX")
|
||||||
|
def test_rlimit(self):
|
||||||
|
ae = self.assertEqual
|
||||||
|
ae(hasattr(psutil, "RLIM_INFINITY"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_AS"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_CORE"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_CPU"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_DATA"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_RSS"), LINUX or FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_STACK"), LINUX or FREEBSD)
|
||||||
|
|
||||||
|
ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX)
|
||||||
|
if POSIX:
|
||||||
|
if kernel_version() >= (2, 6, 8):
|
||||||
|
ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX)
|
||||||
|
if kernel_version() >= (2, 6, 12):
|
||||||
|
ae(hasattr(psutil, "RLIMIT_NICE"), LINUX)
|
||||||
|
if kernel_version() >= (2, 6, 12):
|
||||||
|
ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX)
|
||||||
|
if kernel_version() >= (2, 6, 25):
|
||||||
|
ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX)
|
||||||
|
if kernel_version() >= (2, 6, 8):
|
||||||
|
ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX)
|
||||||
|
|
||||||
|
ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD)
|
||||||
|
ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAvailSystemAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_win_service_iter(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS)
|
||||||
|
|
||||||
|
def test_win_service_get(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS)
|
||||||
|
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "cpu_freq"),
|
||||||
|
LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD)
|
||||||
|
|
||||||
|
def test_sensors_temperatures(self):
|
||||||
|
self.assertEqual(
|
||||||
|
hasattr(psutil, "sensors_temperatures"), LINUX or FREEBSD)
|
||||||
|
|
||||||
|
def test_sensors_fans(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX)
|
||||||
|
|
||||||
|
def test_battery(self):
|
||||||
|
self.assertEqual(hasattr(psutil, "sensors_battery"),
|
||||||
|
LINUX or WINDOWS or FREEBSD or MACOS)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAvailProcessAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_environ(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "environ"),
|
||||||
|
LINUX or MACOS or WINDOWS or AIX or SUNOS or
|
||||||
|
FREEBSD or OPENBSD or NETBSD)
|
||||||
|
|
||||||
|
def test_uids(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "uids"), POSIX)
|
||||||
|
|
||||||
|
def test_gids(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "uids"), POSIX)
|
||||||
|
|
||||||
|
def test_terminal(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX)
|
||||||
|
|
||||||
|
def test_ionice(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS)
|
||||||
|
|
||||||
|
@unittest.skipIf(GITHUB_ACTIONS and LINUX,
|
||||||
|
"unsupported on GITHUB_ACTIONS + LINUX")
|
||||||
|
def test_rlimit(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX or FREEBSD)
|
||||||
|
|
||||||
|
def test_io_counters(self):
|
||||||
|
hasit = hasattr(psutil.Process, "io_counters")
|
||||||
|
self.assertEqual(hasit, False if MACOS or SUNOS else True)
|
||||||
|
|
||||||
|
def test_num_fds(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX)
|
||||||
|
|
||||||
|
def test_num_handles(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS)
|
||||||
|
|
||||||
|
def test_cpu_affinity(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "cpu_affinity"),
|
||||||
|
LINUX or WINDOWS or FREEBSD)
|
||||||
|
|
||||||
|
def test_cpu_num(self):
|
||||||
|
self.assertEqual(hasattr(psutil.Process, "cpu_num"),
|
||||||
|
LINUX or FREEBSD or SUNOS)
|
||||||
|
|
||||||
|
def test_memory_maps(self):
|
||||||
|
hasit = hasattr(psutil.Process, "memory_maps")
|
||||||
|
self.assertEqual(
|
||||||
|
hasit, False if OPENBSD or NETBSD or AIX or MACOS else True)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- API types
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemAPITypes(PsutilTestCase):
|
||||||
|
"""Check the return types of system related APIs.
|
||||||
|
Mainly we want to test we never return unicode on Python 2, see:
|
||||||
|
https://github.com/giampaolo/psutil/issues/1039
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.proc = psutil.Process()
|
||||||
|
|
||||||
|
def assert_ntuple_of_nums(self, nt, type_=float, gezero=True):
|
||||||
|
assert is_namedtuple(nt)
|
||||||
|
for n in nt:
|
||||||
|
self.assertIsInstance(n, type_)
|
||||||
|
if gezero:
|
||||||
|
self.assertGreaterEqual(n, 0)
|
||||||
|
|
||||||
|
def test_cpu_times(self):
|
||||||
|
self.assert_ntuple_of_nums(psutil.cpu_times())
|
||||||
|
for nt in psutil.cpu_times(percpu=True):
|
||||||
|
self.assert_ntuple_of_nums(nt)
|
||||||
|
|
||||||
|
def test_cpu_percent(self):
|
||||||
|
self.assertIsInstance(psutil.cpu_percent(interval=None), float)
|
||||||
|
self.assertIsInstance(psutil.cpu_percent(interval=0.00001), float)
|
||||||
|
|
||||||
|
def test_cpu_times_percent(self):
|
||||||
|
self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=None))
|
||||||
|
self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=0.0001))
|
||||||
|
|
||||||
|
def test_cpu_count(self):
|
||||||
|
self.assertIsInstance(psutil.cpu_count(), int)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_CPU_FREQ, "not supported")
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
if psutil.cpu_freq() is None:
|
||||||
|
raise self.skipTest("cpu_freq() returns None")
|
||||||
|
self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long))
|
||||||
|
|
||||||
|
def test_disk_io_counters(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for k, v in psutil.disk_io_counters(perdisk=True).items():
|
||||||
|
self.assertIsInstance(k, str)
|
||||||
|
self.assert_ntuple_of_nums(v, type_=(int, long))
|
||||||
|
|
||||||
|
def test_disk_partitions(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for disk in psutil.disk_partitions():
|
||||||
|
self.assertIsInstance(disk.device, str)
|
||||||
|
self.assertIsInstance(disk.mountpoint, str)
|
||||||
|
self.assertIsInstance(disk.fstype, str)
|
||||||
|
self.assertIsInstance(disk.opts, str)
|
||||||
|
self.assertIsInstance(disk.maxfile, int)
|
||||||
|
self.assertIsInstance(disk.maxpath, int)
|
||||||
|
|
||||||
|
@unittest.skipIf(SKIP_SYSCONS, "requires root")
|
||||||
|
def test_net_connections(self):
|
||||||
|
with create_sockets():
|
||||||
|
ret = psutil.net_connections('all')
|
||||||
|
self.assertEqual(len(ret), len(set(ret)))
|
||||||
|
for conn in ret:
|
||||||
|
assert is_namedtuple(conn)
|
||||||
|
|
||||||
|
def test_net_if_addrs(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for ifname, addrs in psutil.net_if_addrs().items():
|
||||||
|
self.assertIsInstance(ifname, str)
|
||||||
|
for addr in addrs:
|
||||||
|
if enum is not None and not PYPY:
|
||||||
|
self.assertIsInstance(addr.family, enum.IntEnum)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(addr.family, int)
|
||||||
|
self.assertIsInstance(addr.address, str)
|
||||||
|
self.assertIsInstance(addr.netmask, (str, type(None)))
|
||||||
|
self.assertIsInstance(addr.broadcast, (str, type(None)))
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for ifname, info in psutil.net_if_stats().items():
|
||||||
|
self.assertIsInstance(ifname, str)
|
||||||
|
self.assertIsInstance(info.isup, bool)
|
||||||
|
if enum is not None:
|
||||||
|
self.assertIsInstance(info.duplex, enum.IntEnum)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(info.duplex, int)
|
||||||
|
self.assertIsInstance(info.speed, int)
|
||||||
|
self.assertIsInstance(info.mtu, int)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported')
|
||||||
|
def test_net_io_counters(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for ifname, _ in psutil.net_io_counters(pernic=True).items():
|
||||||
|
self.assertIsInstance(ifname, str)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_FANS, "not supported")
|
||||||
|
def test_sensors_fans(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for name, units in psutil.sensors_fans().items():
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
for unit in units:
|
||||||
|
self.assertIsInstance(unit.label, str)
|
||||||
|
self.assertIsInstance(unit.current, (float, int, type(None)))
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported")
|
||||||
|
def test_sensors_temperatures(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for name, units in psutil.sensors_temperatures().items():
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
for unit in units:
|
||||||
|
self.assertIsInstance(unit.label, str)
|
||||||
|
self.assertIsInstance(unit.current, (float, int, type(None)))
|
||||||
|
self.assertIsInstance(unit.high, (float, int, type(None)))
|
||||||
|
self.assertIsInstance(unit.critical, (float, int, type(None)))
|
||||||
|
|
||||||
|
def test_boot_time(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
self.assertIsInstance(psutil.boot_time(), float)
|
||||||
|
|
||||||
|
def test_users(self):
|
||||||
|
# Duplicate of test_system.py. Keep it anyway.
|
||||||
|
for user in psutil.users():
|
||||||
|
self.assertIsInstance(user.name, str)
|
||||||
|
self.assertIsInstance(user.terminal, (str, type(None)))
|
||||||
|
self.assertIsInstance(user.host, (str, type(None)))
|
||||||
|
self.assertIsInstance(user.pid, (int, type(None)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessWaitType(PsutilTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "not POSIX")
|
||||||
|
def test_negative_signal(self):
|
||||||
|
p = psutil.Process(self.spawn_testproc().pid)
|
||||||
|
p.terminate()
|
||||||
|
code = p.wait()
|
||||||
|
self.assertEqual(code, -signal.SIGTERM)
|
||||||
|
if enum is not None:
|
||||||
|
self.assertIsInstance(code, enum.IntEnum)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(code, int)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Featch all processes test
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def proc_info(pid):
|
||||||
|
tcase = PsutilTestCase()
|
||||||
|
|
||||||
|
def check_exception(exc, proc, name, ppid):
|
||||||
|
tcase.assertEqual(exc.pid, pid)
|
||||||
|
tcase.assertEqual(exc.name, name)
|
||||||
|
if isinstance(exc, psutil.ZombieProcess):
|
||||||
|
if exc.ppid is not None:
|
||||||
|
tcase.assertGreaterEqual(exc.ppid, 0)
|
||||||
|
tcase.assertEqual(exc.ppid, ppid)
|
||||||
|
elif isinstance(exc, psutil.NoSuchProcess):
|
||||||
|
tcase.assertProcessGone(proc)
|
||||||
|
str(exc)
|
||||||
|
|
||||||
|
def do_wait():
|
||||||
|
if pid != 0:
|
||||||
|
try:
|
||||||
|
proc.wait(0)
|
||||||
|
except psutil.Error as exc:
|
||||||
|
check_exception(exc, proc, name, ppid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = psutil.Process(pid)
|
||||||
|
d = proc.as_dict(['ppid', 'name'])
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
name, ppid = d['name'], d['ppid']
|
||||||
|
info = {'pid': proc.pid}
|
||||||
|
ns = process_namespace(proc)
|
||||||
|
# We don't use oneshot() because in order not to fool
|
||||||
|
# check_exception() in case of NSP.
|
||||||
|
for fun, fun_name in ns.iter(ns.getters, clear_cache=False):
|
||||||
|
try:
|
||||||
|
info[fun_name] = fun()
|
||||||
|
except psutil.Error as exc:
|
||||||
|
check_exception(exc, proc, name, ppid)
|
||||||
|
continue
|
||||||
|
do_wait()
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
class TestFetchAllProcesses(PsutilTestCase):
|
||||||
|
"""Test which iterates over all running processes and performs
|
||||||
|
some sanity checks against Process API's returned values.
|
||||||
|
Uses a process pool to get info about all processes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.pool = multiprocessing.Pool()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.pool.terminate()
|
||||||
|
self.pool.join()
|
||||||
|
|
||||||
|
def iter_proc_info(self):
|
||||||
|
# Fixes "can't pickle <function proc_info>: it's not the
|
||||||
|
# same object as test_contracts.proc_info".
|
||||||
|
from psutil.tests.test_contracts import proc_info
|
||||||
|
return self.pool.imap_unordered(proc_info, psutil.pids())
|
||||||
|
|
||||||
|
def test_all(self):
|
||||||
|
failures = []
|
||||||
|
for info in self.iter_proc_info():
|
||||||
|
for name, value in info.items():
|
||||||
|
meth = getattr(self, name)
|
||||||
|
try:
|
||||||
|
meth(value, info)
|
||||||
|
except AssertionError:
|
||||||
|
s = '\n' + '=' * 70 + '\n'
|
||||||
|
s += "FAIL: test_%s pid=%s, ret=%s\n" % (
|
||||||
|
name, info['pid'], repr(value))
|
||||||
|
s += '-' * 70
|
||||||
|
s += "\n%s" % traceback.format_exc()
|
||||||
|
s = "\n".join((" " * 4) + i for i in s.splitlines())
|
||||||
|
s += '\n'
|
||||||
|
failures.append(s)
|
||||||
|
else:
|
||||||
|
if value not in (0, 0.0, [], None, '', {}):
|
||||||
|
assert value, value
|
||||||
|
if failures:
|
||||||
|
raise self.fail(''.join(failures))
|
||||||
|
|
||||||
|
def cmdline(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, list)
|
||||||
|
for part in ret:
|
||||||
|
self.assertIsInstance(part, str)
|
||||||
|
|
||||||
|
def exe(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, (str, type(None)))
|
||||||
|
if not ret:
|
||||||
|
self.assertEqual(ret, '')
|
||||||
|
else:
|
||||||
|
if WINDOWS and not ret.endswith('.exe'):
|
||||||
|
return # May be "Registry", "MemCompression", ...
|
||||||
|
assert os.path.isabs(ret), ret
|
||||||
|
# Note: os.stat() may return False even if the file is there
|
||||||
|
# hence we skip the test, see:
|
||||||
|
# http://stackoverflow.com/questions/3112546/os-path-exists-lies
|
||||||
|
if POSIX and os.path.isfile(ret):
|
||||||
|
if hasattr(os, 'access') and hasattr(os, "X_OK"):
|
||||||
|
# XXX: may fail on MACOS
|
||||||
|
try:
|
||||||
|
assert os.access(ret, os.X_OK)
|
||||||
|
except AssertionError:
|
||||||
|
if os.path.exists(ret) and not CI_TESTING:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def pid(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
|
||||||
|
def ppid(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, (int, long))
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
|
||||||
|
def name(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, str)
|
||||||
|
if APPVEYOR and not ret and info['status'] == 'stopped':
|
||||||
|
return
|
||||||
|
# on AIX, "<exiting>" processes don't have names
|
||||||
|
if not AIX:
|
||||||
|
assert ret
|
||||||
|
|
||||||
|
def create_time(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, float)
|
||||||
|
try:
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
except AssertionError:
|
||||||
|
# XXX
|
||||||
|
if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
# this can't be taken for granted on all platforms
|
||||||
|
# self.assertGreaterEqual(ret, psutil.boot_time())
|
||||||
|
# make sure returned value can be pretty printed
|
||||||
|
# with strftime
|
||||||
|
time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret))
|
||||||
|
|
||||||
|
def uids(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
for uid in ret:
|
||||||
|
self.assertIsInstance(uid, int)
|
||||||
|
self.assertGreaterEqual(uid, 0)
|
||||||
|
|
||||||
|
def gids(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
# note: testing all gids as above seems not to be reliable for
|
||||||
|
# gid == 30 (nodoby); not sure why.
|
||||||
|
for gid in ret:
|
||||||
|
self.assertIsInstance(gid, int)
|
||||||
|
if not MACOS and not NETBSD:
|
||||||
|
self.assertGreaterEqual(gid, 0)
|
||||||
|
|
||||||
|
def username(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, str)
|
||||||
|
assert ret
|
||||||
|
|
||||||
|
def status(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, str)
|
||||||
|
assert ret
|
||||||
|
self.assertNotEqual(ret, '?') # XXX
|
||||||
|
self.assertIn(ret, VALID_PROC_STATUSES)
|
||||||
|
|
||||||
|
def io_counters(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
for field in ret:
|
||||||
|
self.assertIsInstance(field, (int, long))
|
||||||
|
if field != -1:
|
||||||
|
self.assertGreaterEqual(field, 0)
|
||||||
|
|
||||||
|
def ionice(self, ret, info):
|
||||||
|
if LINUX:
|
||||||
|
self.assertIsInstance(ret.ioclass, int)
|
||||||
|
self.assertIsInstance(ret.value, int)
|
||||||
|
self.assertGreaterEqual(ret.ioclass, 0)
|
||||||
|
self.assertGreaterEqual(ret.value, 0)
|
||||||
|
else: # Windows, Cygwin
|
||||||
|
choices = [
|
||||||
|
psutil.IOPRIO_VERYLOW,
|
||||||
|
psutil.IOPRIO_LOW,
|
||||||
|
psutil.IOPRIO_NORMAL,
|
||||||
|
psutil.IOPRIO_HIGH]
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
self.assertIn(ret, choices)
|
||||||
|
|
||||||
|
def num_threads(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
if APPVEYOR and not ret and info['status'] == 'stopped':
|
||||||
|
return
|
||||||
|
self.assertGreaterEqual(ret, 1)
|
||||||
|
|
||||||
|
def threads(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, list)
|
||||||
|
for t in ret:
|
||||||
|
assert is_namedtuple(t)
|
||||||
|
self.assertGreaterEqual(t.id, 0)
|
||||||
|
self.assertGreaterEqual(t.user_time, 0)
|
||||||
|
self.assertGreaterEqual(t.system_time, 0)
|
||||||
|
for field in t:
|
||||||
|
self.assertIsInstance(field, (int, float))
|
||||||
|
|
||||||
|
def cpu_times(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
for n in ret:
|
||||||
|
self.assertIsInstance(n, float)
|
||||||
|
self.assertGreaterEqual(n, 0)
|
||||||
|
# TODO: check ntuple fields
|
||||||
|
|
||||||
|
def cpu_percent(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, float)
|
||||||
|
assert 0.0 <= ret <= 100.0, ret
|
||||||
|
|
||||||
|
def cpu_num(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
if FREEBSD and ret == -1:
|
||||||
|
return
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
if psutil.cpu_count() == 1:
|
||||||
|
self.assertEqual(ret, 0)
|
||||||
|
self.assertIn(ret, list(range(psutil.cpu_count())))
|
||||||
|
|
||||||
|
def memory_info(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
for value in ret:
|
||||||
|
self.assertIsInstance(value, (int, long))
|
||||||
|
self.assertGreaterEqual(value, 0)
|
||||||
|
if WINDOWS:
|
||||||
|
self.assertGreaterEqual(ret.peak_wset, ret.wset)
|
||||||
|
self.assertGreaterEqual(ret.peak_paged_pool, ret.paged_pool)
|
||||||
|
self.assertGreaterEqual(ret.peak_nonpaged_pool, ret.nonpaged_pool)
|
||||||
|
self.assertGreaterEqual(ret.peak_pagefile, ret.pagefile)
|
||||||
|
|
||||||
|
def memory_full_info(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
total = psutil.virtual_memory().total
|
||||||
|
for name in ret._fields:
|
||||||
|
value = getattr(ret, name)
|
||||||
|
self.assertIsInstance(value, (int, long))
|
||||||
|
self.assertGreaterEqual(value, 0, msg=(name, value))
|
||||||
|
if LINUX or OSX and name in ('vms', 'data'):
|
||||||
|
# On Linux there are processes (e.g. 'goa-daemon') whose
|
||||||
|
# VMS is incredibly high for some reason.
|
||||||
|
continue
|
||||||
|
self.assertLessEqual(value, total, msg=(name, value, total))
|
||||||
|
|
||||||
|
if LINUX:
|
||||||
|
self.assertGreaterEqual(ret.pss, ret.uss)
|
||||||
|
|
||||||
|
def open_files(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, list)
|
||||||
|
for f in ret:
|
||||||
|
self.assertIsInstance(f.fd, int)
|
||||||
|
self.assertIsInstance(f.path, str)
|
||||||
|
if WINDOWS:
|
||||||
|
self.assertEqual(f.fd, -1)
|
||||||
|
elif LINUX:
|
||||||
|
self.assertIsInstance(f.position, int)
|
||||||
|
self.assertIsInstance(f.mode, str)
|
||||||
|
self.assertIsInstance(f.flags, int)
|
||||||
|
self.assertGreaterEqual(f.position, 0)
|
||||||
|
self.assertIn(f.mode, ('r', 'w', 'a', 'r+', 'a+'))
|
||||||
|
self.assertGreater(f.flags, 0)
|
||||||
|
elif BSD and not f.path:
|
||||||
|
# XXX see: https://github.com/giampaolo/psutil/issues/595
|
||||||
|
continue
|
||||||
|
assert os.path.isabs(f.path), f
|
||||||
|
try:
|
||||||
|
st = os.stat(f.path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
assert stat.S_ISREG(st.st_mode), f
|
||||||
|
|
||||||
|
def num_fds(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
|
||||||
|
def connections(self, ret, info):
|
||||||
|
with create_sockets():
|
||||||
|
self.assertEqual(len(ret), len(set(ret)))
|
||||||
|
for conn in ret:
|
||||||
|
assert is_namedtuple(conn)
|
||||||
|
check_connection_ntuple(conn)
|
||||||
|
|
||||||
|
def cwd(self, ret, info):
|
||||||
|
if ret: # 'ret' can be None or empty
|
||||||
|
self.assertIsInstance(ret, str)
|
||||||
|
assert os.path.isabs(ret), ret
|
||||||
|
try:
|
||||||
|
st = os.stat(ret)
|
||||||
|
except OSError as err:
|
||||||
|
if WINDOWS and err.errno in \
|
||||||
|
psutil._psplatform.ACCESS_DENIED_SET:
|
||||||
|
pass
|
||||||
|
# directory has been removed in mean time
|
||||||
|
elif err.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
assert stat.S_ISDIR(st.st_mode)
|
||||||
|
|
||||||
|
def memory_percent(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, float)
|
||||||
|
assert 0 <= ret <= 100, ret
|
||||||
|
|
||||||
|
def is_running(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, bool)
|
||||||
|
|
||||||
|
def cpu_affinity(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, list)
|
||||||
|
assert ret != [], ret
|
||||||
|
cpus = list(range(psutil.cpu_count()))
|
||||||
|
for n in ret:
|
||||||
|
self.assertIsInstance(n, int)
|
||||||
|
self.assertIn(n, cpus)
|
||||||
|
|
||||||
|
def terminal(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, (str, type(None)))
|
||||||
|
if ret is not None:
|
||||||
|
assert os.path.isabs(ret), ret
|
||||||
|
assert os.path.exists(ret), ret
|
||||||
|
|
||||||
|
def memory_maps(self, ret, info):
|
||||||
|
for nt in ret:
|
||||||
|
self.assertIsInstance(nt.addr, str)
|
||||||
|
self.assertIsInstance(nt.perms, str)
|
||||||
|
self.assertIsInstance(nt.path, str)
|
||||||
|
for fname in nt._fields:
|
||||||
|
value = getattr(nt, fname)
|
||||||
|
if fname == 'path':
|
||||||
|
if not value.startswith('['):
|
||||||
|
assert os.path.isabs(nt.path), nt.path
|
||||||
|
# commented as on Linux we might get
|
||||||
|
# '/foo/bar (deleted)'
|
||||||
|
# assert os.path.exists(nt.path), nt.path
|
||||||
|
elif fname == 'addr':
|
||||||
|
assert value, repr(value)
|
||||||
|
elif fname == 'perms':
|
||||||
|
if not WINDOWS:
|
||||||
|
assert value, repr(value)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(value, (int, long))
|
||||||
|
self.assertGreaterEqual(value, 0)
|
||||||
|
|
||||||
|
def num_handles(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
self.assertGreaterEqual(ret, 0)
|
||||||
|
|
||||||
|
def nice(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
if POSIX:
|
||||||
|
assert -20 <= ret <= 20, ret
|
||||||
|
else:
|
||||||
|
priorities = [getattr(psutil, x) for x in dir(psutil)
|
||||||
|
if x.endswith('_PRIORITY_CLASS')]
|
||||||
|
self.assertIn(ret, priorities)
|
||||||
|
if sys.version_info > (3, 4):
|
||||||
|
self.assertIsInstance(ret, enum.IntEnum)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(ret, int)
|
||||||
|
|
||||||
|
def num_ctx_switches(self, ret, info):
|
||||||
|
assert is_namedtuple(ret)
|
||||||
|
for value in ret:
|
||||||
|
self.assertIsInstance(value, (int, long))
|
||||||
|
self.assertGreaterEqual(value, 0)
|
||||||
|
|
||||||
|
def rlimit(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, tuple)
|
||||||
|
self.assertEqual(len(ret), 2)
|
||||||
|
self.assertGreaterEqual(ret[0], -1)
|
||||||
|
self.assertGreaterEqual(ret[1], -1)
|
||||||
|
|
||||||
|
def environ(self, ret, info):
|
||||||
|
self.assertIsInstance(ret, dict)
|
||||||
|
for k, v in ret.items():
|
||||||
|
self.assertIsInstance(k, str)
|
||||||
|
self.assertIsInstance(v, str)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,488 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for detecting function memory leaks (typically the ones
|
||||||
|
implemented in C). It does so by calling a function many times and
|
||||||
|
checking whether process memory usage keeps increasing between
|
||||||
|
calls or over time.
|
||||||
|
Note that this may produce false positives (especially on Windows
|
||||||
|
for some reason).
|
||||||
|
PyPy appears to be completely unstable for this framework, probably
|
||||||
|
because of how its JIT handles memory, so tests are skipped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import psutil._common
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._compat import ProcessLookupError
|
||||||
|
from psutil._compat import super
|
||||||
|
from psutil.tests import HAS_CPU_AFFINITY
|
||||||
|
from psutil.tests import HAS_CPU_FREQ
|
||||||
|
from psutil.tests import HAS_ENVIRON
|
||||||
|
from psutil.tests import HAS_IONICE
|
||||||
|
from psutil.tests import HAS_MEMORY_MAPS
|
||||||
|
from psutil.tests import HAS_NET_IO_COUNTERS
|
||||||
|
from psutil.tests import HAS_PROC_CPU_NUM
|
||||||
|
from psutil.tests import HAS_PROC_IO_COUNTERS
|
||||||
|
from psutil.tests import HAS_RLIMIT
|
||||||
|
from psutil.tests import HAS_SENSORS_BATTERY
|
||||||
|
from psutil.tests import HAS_SENSORS_FANS
|
||||||
|
from psutil.tests import HAS_SENSORS_TEMPERATURES
|
||||||
|
from psutil.tests import TestMemoryLeak
|
||||||
|
from psutil.tests import create_sockets
|
||||||
|
from psutil.tests import get_testfn
|
||||||
|
from psutil.tests import process_namespace
|
||||||
|
from psutil.tests import skip_on_access_denied
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import system_namespace
|
||||||
|
from psutil.tests import terminate
|
||||||
|
|
||||||
|
|
||||||
|
cext = psutil._psplatform.cext
|
||||||
|
thisproc = psutil.Process()
|
||||||
|
FEW_TIMES = 5
|
||||||
|
|
||||||
|
|
||||||
|
def fewtimes_if_linux():
|
||||||
|
"""Decorator for those Linux functions which are implemented in pure
|
||||||
|
Python, and which we want to run faster.
|
||||||
|
"""
|
||||||
|
def decorator(fun):
|
||||||
|
@functools.wraps(fun)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if LINUX:
|
||||||
|
before = self.__class__.times
|
||||||
|
try:
|
||||||
|
self.__class__.times = FEW_TIMES
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
self.__class__.times = before
|
||||||
|
else:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Process class
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessObjectLeaks(TestMemoryLeak):
|
||||||
|
"""Test leaks of Process class methods."""
|
||||||
|
|
||||||
|
proc = thisproc
|
||||||
|
|
||||||
|
def test_coverage(self):
|
||||||
|
ns = process_namespace(None)
|
||||||
|
ns.test_class_coverage(self, ns.getters + ns.setters)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_name(self):
|
||||||
|
self.execute(self.proc.name)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cmdline(self):
|
||||||
|
self.execute(self.proc.cmdline)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_exe(self):
|
||||||
|
self.execute(self.proc.exe)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_ppid(self):
|
||||||
|
self.execute(self.proc.ppid)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_uids(self):
|
||||||
|
self.execute(self.proc.uids)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_gids(self):
|
||||||
|
self.execute(self.proc.gids)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_status(self):
|
||||||
|
self.execute(self.proc.status)
|
||||||
|
|
||||||
|
def test_nice(self):
|
||||||
|
self.execute(self.proc.nice)
|
||||||
|
|
||||||
|
def test_nice_set(self):
|
||||||
|
niceness = thisproc.nice()
|
||||||
|
self.execute(lambda: self.proc.nice(niceness))
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_IONICE, "not supported")
|
||||||
|
def test_ionice(self):
|
||||||
|
self.execute(self.proc.ionice)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_IONICE, "not supported")
|
||||||
|
def test_ionice_set(self):
|
||||||
|
if WINDOWS:
|
||||||
|
value = thisproc.ionice()
|
||||||
|
self.execute(lambda: self.proc.ionice(value))
|
||||||
|
else:
|
||||||
|
self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE))
|
||||||
|
fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0)
|
||||||
|
self.execute_w_exc(OSError, fun)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_PROC_IO_COUNTERS, "not supported")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_io_counters(self):
|
||||||
|
self.execute(self.proc.io_counters)
|
||||||
|
|
||||||
|
@unittest.skipIf(POSIX, "worthless on POSIX")
|
||||||
|
def test_username(self):
|
||||||
|
# always open 1 handle on Windows (only once)
|
||||||
|
psutil.Process().username()
|
||||||
|
self.execute(self.proc.username)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_create_time(self):
|
||||||
|
self.execute(self.proc.create_time)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@skip_on_access_denied(only_if=OPENBSD)
|
||||||
|
def test_num_threads(self):
|
||||||
|
self.execute(self.proc.num_threads)
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
def test_num_handles(self):
|
||||||
|
self.execute(self.proc.num_handles)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_num_fds(self):
|
||||||
|
self.execute(self.proc.num_fds)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_num_ctx_switches(self):
|
||||||
|
self.execute(self.proc.num_ctx_switches)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@skip_on_access_denied(only_if=OPENBSD)
|
||||||
|
def test_threads(self):
|
||||||
|
self.execute(self.proc.threads)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cpu_times(self):
|
||||||
|
self.execute(self.proc.cpu_times)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported")
|
||||||
|
def test_cpu_num(self):
|
||||||
|
self.execute(self.proc.cpu_num)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_memory_info(self):
|
||||||
|
self.execute(self.proc.memory_info)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_memory_full_info(self):
|
||||||
|
self.execute(self.proc.memory_full_info)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_terminal(self):
|
||||||
|
self.execute(self.proc.terminal)
|
||||||
|
|
||||||
|
def test_resume(self):
|
||||||
|
times = FEW_TIMES if POSIX else self.times
|
||||||
|
self.execute(self.proc.resume, times=times)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cwd(self):
|
||||||
|
self.execute(self.proc.cwd)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_CPU_AFFINITY, "not supported")
|
||||||
|
def test_cpu_affinity(self):
|
||||||
|
self.execute(self.proc.cpu_affinity)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_CPU_AFFINITY, "not supported")
|
||||||
|
def test_cpu_affinity_set(self):
|
||||||
|
affinity = thisproc.cpu_affinity()
|
||||||
|
self.execute(lambda: self.proc.cpu_affinity(affinity))
|
||||||
|
self.execute_w_exc(
|
||||||
|
ValueError, lambda: self.proc.cpu_affinity([-1]))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_open_files(self):
|
||||||
|
with open(get_testfn(), 'w'):
|
||||||
|
self.execute(self.proc.open_files)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_MEMORY_MAPS, "not supported")
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_memory_maps(self):
|
||||||
|
self.execute(self.proc.memory_maps)
|
||||||
|
|
||||||
|
@unittest.skipIf(not LINUX, "LINUX only")
|
||||||
|
@unittest.skipIf(not HAS_RLIMIT, "not supported")
|
||||||
|
def test_rlimit(self):
|
||||||
|
self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE))
|
||||||
|
|
||||||
|
@unittest.skipIf(not LINUX, "LINUX only")
|
||||||
|
@unittest.skipIf(not HAS_RLIMIT, "not supported")
|
||||||
|
def test_rlimit_set(self):
|
||||||
|
limit = thisproc.rlimit(psutil.RLIMIT_NOFILE)
|
||||||
|
self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit))
|
||||||
|
self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
# Windows implementation is based on a single system-wide
|
||||||
|
# function (tested later).
|
||||||
|
@unittest.skipIf(WINDOWS, "worthless on WINDOWS")
|
||||||
|
def test_connections(self):
|
||||||
|
# TODO: UNIX sockets are temporarily implemented by parsing
|
||||||
|
# 'pfiles' cmd output; we don't want that part of the code to
|
||||||
|
# be executed.
|
||||||
|
with create_sockets():
|
||||||
|
kind = 'inet' if SUNOS else 'all'
|
||||||
|
self.execute(lambda: self.proc.connections(kind))
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_ENVIRON, "not supported")
|
||||||
|
def test_environ(self):
|
||||||
|
self.execute(self.proc.environ)
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
def test_proc_info(self):
|
||||||
|
self.execute(lambda: cext.proc_info(os.getpid()))
|
||||||
|
|
||||||
|
|
||||||
|
class TestTerminatedProcessLeaks(TestProcessObjectLeaks):
|
||||||
|
"""Repeat the tests above looking for leaks occurring when dealing
|
||||||
|
with terminated processes raising NoSuchProcess exception.
|
||||||
|
The C functions are still invoked but will follow different code
|
||||||
|
paths. We'll check those code paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.subp = spawn_testproc()
|
||||||
|
cls.proc = psutil.Process(cls.subp.pid)
|
||||||
|
cls.proc.kill()
|
||||||
|
cls.proc.wait()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
terminate(cls.subp)
|
||||||
|
|
||||||
|
def call(self, fun):
|
||||||
|
try:
|
||||||
|
fun()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if WINDOWS:
|
||||||
|
|
||||||
|
def test_kill(self):
|
||||||
|
self.execute(self.proc.kill)
|
||||||
|
|
||||||
|
def test_terminate(self):
|
||||||
|
self.execute(self.proc.terminate)
|
||||||
|
|
||||||
|
def test_suspend(self):
|
||||||
|
self.execute(self.proc.suspend)
|
||||||
|
|
||||||
|
def test_resume(self):
|
||||||
|
self.execute(self.proc.resume)
|
||||||
|
|
||||||
|
def test_wait(self):
|
||||||
|
self.execute(self.proc.wait)
|
||||||
|
|
||||||
|
def test_proc_info(self):
|
||||||
|
# test dual implementation
|
||||||
|
def call():
|
||||||
|
try:
|
||||||
|
return cext.proc_info(self.proc.pid)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.execute(call)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
class TestProcessDualImplementation(TestMemoryLeak):
|
||||||
|
|
||||||
|
def test_cmdline_peb_true(self):
|
||||||
|
self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True))
|
||||||
|
|
||||||
|
def test_cmdline_peb_false(self):
|
||||||
|
self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False))
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# system APIs
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestModuleFunctionsLeaks(TestMemoryLeak):
|
||||||
|
"""Test leaks of psutil module functions."""
|
||||||
|
|
||||||
|
def test_coverage(self):
|
||||||
|
ns = system_namespace()
|
||||||
|
ns.test_class_coverage(self, ns.all)
|
||||||
|
|
||||||
|
# --- cpu
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cpu_count(self): # logical
|
||||||
|
self.execute(lambda: psutil.cpu_count(logical=True))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cpu_count_cores(self):
|
||||||
|
self.execute(lambda: psutil.cpu_count(logical=False))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cpu_times(self):
|
||||||
|
self.execute(psutil.cpu_times)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_per_cpu_times(self):
|
||||||
|
self.execute(lambda: psutil.cpu_times(percpu=True))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_cpu_stats(self):
|
||||||
|
self.execute(psutil.cpu_stats)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_CPU_FREQ, "not supported")
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
self.execute(psutil.cpu_freq)
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
def test_getloadavg(self):
|
||||||
|
psutil.getloadavg()
|
||||||
|
self.execute(psutil.getloadavg)
|
||||||
|
|
||||||
|
# --- mem
|
||||||
|
|
||||||
|
def test_virtual_memory(self):
|
||||||
|
self.execute(psutil.virtual_memory)
|
||||||
|
|
||||||
|
# TODO: remove this skip when this gets fixed
|
||||||
|
@unittest.skipIf(SUNOS, "worthless on SUNOS (uses a subprocess)")
|
||||||
|
def test_swap_memory(self):
|
||||||
|
self.execute(psutil.swap_memory)
|
||||||
|
|
||||||
|
def test_pid_exists(self):
|
||||||
|
times = FEW_TIMES if POSIX else self.times
|
||||||
|
self.execute(lambda: psutil.pid_exists(os.getpid()), times=times)
|
||||||
|
|
||||||
|
# --- disk
|
||||||
|
|
||||||
|
def test_disk_usage(self):
|
||||||
|
times = FEW_TIMES if POSIX else self.times
|
||||||
|
self.execute(lambda: psutil.disk_usage('.'), times=times)
|
||||||
|
|
||||||
|
def test_disk_partitions(self):
|
||||||
|
self.execute(psutil.disk_partitions)
|
||||||
|
|
||||||
|
@unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'),
|
||||||
|
'/proc/diskstats not available on this Linux version')
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_disk_io_counters(self):
|
||||||
|
self.execute(lambda: psutil.disk_io_counters(nowrap=False))
|
||||||
|
|
||||||
|
# --- proc
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_pids(self):
|
||||||
|
self.execute(psutil.pids)
|
||||||
|
|
||||||
|
# --- net
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported')
|
||||||
|
def test_net_io_counters(self):
|
||||||
|
self.execute(lambda: psutil.net_io_counters(nowrap=False))
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(MACOS and os.getuid() != 0, "need root access")
|
||||||
|
def test_net_connections(self):
|
||||||
|
# always opens and handle on Windows() (once)
|
||||||
|
psutil.net_connections(kind='all')
|
||||||
|
with create_sockets():
|
||||||
|
self.execute(lambda: psutil.net_connections(kind='all'))
|
||||||
|
|
||||||
|
def test_net_if_addrs(self):
|
||||||
|
# Note: verified that on Windows this was a false positive.
|
||||||
|
tolerance = 80 * 1024 if WINDOWS else self.tolerance
|
||||||
|
self.execute(psutil.net_if_addrs, tolerance=tolerance)
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
self.execute(psutil.net_if_stats)
|
||||||
|
|
||||||
|
# --- sensors
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported")
|
||||||
|
def test_sensors_battery(self):
|
||||||
|
self.execute(psutil.sensors_battery)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported")
|
||||||
|
def test_sensors_temperatures(self):
|
||||||
|
self.execute(psutil.sensors_temperatures)
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_FANS, "not supported")
|
||||||
|
def test_sensors_fans(self):
|
||||||
|
self.execute(psutil.sensors_fans)
|
||||||
|
|
||||||
|
# --- others
|
||||||
|
|
||||||
|
@fewtimes_if_linux()
|
||||||
|
def test_boot_time(self):
|
||||||
|
self.execute(psutil.boot_time)
|
||||||
|
|
||||||
|
def test_users(self):
|
||||||
|
self.execute(psutil.users)
|
||||||
|
|
||||||
|
def test_set_debug(self):
|
||||||
|
self.execute(lambda: psutil._set_debug(False))
|
||||||
|
|
||||||
|
if WINDOWS:
|
||||||
|
|
||||||
|
# --- win services
|
||||||
|
|
||||||
|
def test_win_service_iter(self):
|
||||||
|
self.execute(cext.winservice_enumerate)
|
||||||
|
|
||||||
|
def test_win_service_get(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_win_service_get_config(self):
|
||||||
|
name = next(psutil.win_service_iter()).name()
|
||||||
|
self.execute(lambda: cext.winservice_query_config(name))
|
||||||
|
|
||||||
|
def test_win_service_get_status(self):
|
||||||
|
name = next(psutil.win_service_iter()).name()
|
||||||
|
self.execute(lambda: cext.winservice_query_status(name))
|
||||||
|
|
||||||
|
def test_win_service_get_description(self):
|
||||||
|
name = next(psutil.win_service_iter()).name()
|
||||||
|
self.execute(lambda: cext.winservice_query_descr(name))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,852 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Miscellaneous tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import collections
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import socket
|
||||||
|
import stat
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import psutil.tests
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._common import bcat
|
||||||
|
from psutil._common import cat
|
||||||
|
from psutil._common import debug
|
||||||
|
from psutil._common import isfile_strict
|
||||||
|
from psutil._common import memoize
|
||||||
|
from psutil._common import memoize_when_activated
|
||||||
|
from psutil._common import parse_environ_block
|
||||||
|
from psutil._common import supports_ipv6
|
||||||
|
from psutil._common import wrap_numbers
|
||||||
|
from psutil._compat import PY3
|
||||||
|
from psutil._compat import FileNotFoundError
|
||||||
|
from psutil._compat import redirect_stderr
|
||||||
|
from psutil.tests import APPVEYOR
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import HAS_BATTERY
|
||||||
|
from psutil.tests import HAS_MEMORY_MAPS
|
||||||
|
from psutil.tests import HAS_NET_IO_COUNTERS
|
||||||
|
from psutil.tests import HAS_SENSORS_BATTERY
|
||||||
|
from psutil.tests import HAS_SENSORS_FANS
|
||||||
|
from psutil.tests import HAS_SENSORS_TEMPERATURES
|
||||||
|
from psutil.tests import PYTHON_EXE
|
||||||
|
from psutil.tests import ROOT_DIR
|
||||||
|
from psutil.tests import SCRIPTS_DIR
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import import_module_by_path
|
||||||
|
from psutil.tests import mock
|
||||||
|
from psutil.tests import reload_module
|
||||||
|
from psutil.tests import sh
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Test classes' repr(), str(), ...
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpecialMethods(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_process__repr__(self, func=repr):
|
||||||
|
p = psutil.Process(self.spawn_testproc().pid)
|
||||||
|
r = func(p)
|
||||||
|
self.assertIn("psutil.Process", r)
|
||||||
|
self.assertIn("pid=%s" % p.pid, r)
|
||||||
|
self.assertIn("name='%s'" % str(p.name()),
|
||||||
|
r.replace("name=u'", "name='"))
|
||||||
|
self.assertIn("status=", r)
|
||||||
|
self.assertNotIn("exitcode=", r)
|
||||||
|
p.terminate()
|
||||||
|
p.wait()
|
||||||
|
r = func(p)
|
||||||
|
self.assertIn("status='terminated'", r)
|
||||||
|
self.assertIn("exitcode=", r)
|
||||||
|
|
||||||
|
with mock.patch.object(psutil.Process, "name",
|
||||||
|
side_effect=psutil.ZombieProcess(os.getpid())):
|
||||||
|
p = psutil.Process()
|
||||||
|
r = func(p)
|
||||||
|
self.assertIn("pid=%s" % p.pid, r)
|
||||||
|
self.assertIn("status='zombie'", r)
|
||||||
|
self.assertNotIn("name=", r)
|
||||||
|
with mock.patch.object(psutil.Process, "name",
|
||||||
|
side_effect=psutil.NoSuchProcess(os.getpid())):
|
||||||
|
p = psutil.Process()
|
||||||
|
r = func(p)
|
||||||
|
self.assertIn("pid=%s" % p.pid, r)
|
||||||
|
self.assertIn("terminated", r)
|
||||||
|
self.assertNotIn("name=", r)
|
||||||
|
with mock.patch.object(psutil.Process, "name",
|
||||||
|
side_effect=psutil.AccessDenied(os.getpid())):
|
||||||
|
p = psutil.Process()
|
||||||
|
r = func(p)
|
||||||
|
self.assertIn("pid=%s" % p.pid, r)
|
||||||
|
self.assertNotIn("name=", r)
|
||||||
|
|
||||||
|
def test_process__str__(self):
|
||||||
|
self.test_process__repr__(func=str)
|
||||||
|
|
||||||
|
def test_error__repr__(self):
|
||||||
|
self.assertEqual(repr(psutil.Error()), "psutil.Error()")
|
||||||
|
|
||||||
|
def test_error__str__(self):
|
||||||
|
self.assertEqual(str(psutil.Error()), "")
|
||||||
|
|
||||||
|
def test_no_such_process__repr__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.NoSuchProcess(321)),
|
||||||
|
"psutil.NoSuchProcess(pid=321, msg='process no longer exists')")
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.NoSuchProcess(321, name="name", msg="msg")),
|
||||||
|
"psutil.NoSuchProcess(pid=321, name='name', msg='msg')")
|
||||||
|
|
||||||
|
def test_no_such_process__str__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.NoSuchProcess(321)),
|
||||||
|
"process no longer exists (pid=321)")
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.NoSuchProcess(321, name="name", msg="msg")),
|
||||||
|
"msg (pid=321, name='name')")
|
||||||
|
|
||||||
|
def test_zombie_process__repr__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.ZombieProcess(321)),
|
||||||
|
'psutil.ZombieProcess(pid=321, msg="PID still '
|
||||||
|
'exists but it\'s a zombie")')
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")),
|
||||||
|
"psutil.ZombieProcess(pid=321, ppid=320, name='name', msg='foo')")
|
||||||
|
|
||||||
|
def test_zombie_process__str__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.ZombieProcess(321)),
|
||||||
|
"PID still exists but it's a zombie (pid=321)")
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")),
|
||||||
|
"foo (pid=321, ppid=320, name='name')")
|
||||||
|
|
||||||
|
def test_access_denied__repr__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.AccessDenied(321)),
|
||||||
|
"psutil.AccessDenied(pid=321)")
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.AccessDenied(321, name="name", msg="msg")),
|
||||||
|
"psutil.AccessDenied(pid=321, name='name', msg='msg')")
|
||||||
|
|
||||||
|
def test_access_denied__str__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.AccessDenied(321)),
|
||||||
|
"(pid=321)")
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.AccessDenied(321, name="name", msg="msg")),
|
||||||
|
"msg (pid=321, name='name')")
|
||||||
|
|
||||||
|
def test_timeout_expired__repr__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.TimeoutExpired(5)),
|
||||||
|
"psutil.TimeoutExpired(seconds=5, msg='timeout after 5 seconds')")
|
||||||
|
self.assertEqual(
|
||||||
|
repr(psutil.TimeoutExpired(5, pid=321, name="name")),
|
||||||
|
"psutil.TimeoutExpired(pid=321, name='name', seconds=5, "
|
||||||
|
"msg='timeout after 5 seconds')")
|
||||||
|
|
||||||
|
def test_timeout_expired__str__(self):
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.TimeoutExpired(5)),
|
||||||
|
"timeout after 5 seconds")
|
||||||
|
self.assertEqual(
|
||||||
|
str(psutil.TimeoutExpired(5, pid=321, name="name")),
|
||||||
|
"timeout after 5 seconds (pid=321, name='name')")
|
||||||
|
|
||||||
|
def test_process__eq__(self):
|
||||||
|
p1 = psutil.Process()
|
||||||
|
p2 = psutil.Process()
|
||||||
|
self.assertEqual(p1, p2)
|
||||||
|
p2._ident = (0, 0)
|
||||||
|
self.assertNotEqual(p1, p2)
|
||||||
|
self.assertNotEqual(p1, 'foo')
|
||||||
|
|
||||||
|
def test_process__hash__(self):
|
||||||
|
s = set([psutil.Process(), psutil.Process()])
|
||||||
|
self.assertEqual(len(s), 1)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Misc, generic, corner cases
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestMisc(PsutilTestCase):
|
||||||
|
|
||||||
|
def test__all__(self):
|
||||||
|
dir_psutil = dir(psutil)
|
||||||
|
for name in dir_psutil:
|
||||||
|
if name in ('long', 'tests', 'test', 'PermissionError',
|
||||||
|
'ProcessLookupError'):
|
||||||
|
continue
|
||||||
|
if not name.startswith('_'):
|
||||||
|
try:
|
||||||
|
__import__(name)
|
||||||
|
except ImportError:
|
||||||
|
if name not in psutil.__all__:
|
||||||
|
fun = getattr(psutil, name)
|
||||||
|
if fun is None:
|
||||||
|
continue
|
||||||
|
if (fun.__doc__ is not None and
|
||||||
|
'deprecated' not in fun.__doc__.lower()):
|
||||||
|
raise self.fail('%r not in psutil.__all__' % name)
|
||||||
|
|
||||||
|
# Import 'star' will break if __all__ is inconsistent, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/656
|
||||||
|
# Can't do `from psutil import *` as it won't work on python 3
|
||||||
|
# so we simply iterate over __all__.
|
||||||
|
for name in psutil.__all__:
|
||||||
|
self.assertIn(name, dir_psutil)
|
||||||
|
|
||||||
|
def test_version(self):
|
||||||
|
self.assertEqual('.'.join([str(x) for x in psutil.version_info]),
|
||||||
|
psutil.__version__)
|
||||||
|
|
||||||
|
def test_process_as_dict_no_new_names(self):
|
||||||
|
# See https://github.com/giampaolo/psutil/issues/813
|
||||||
|
p = psutil.Process()
|
||||||
|
p.foo = '1'
|
||||||
|
self.assertNotIn('foo', p.as_dict())
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
def check(ret):
|
||||||
|
if json is not None:
|
||||||
|
json.loads(json.dumps(ret))
|
||||||
|
a = pickle.dumps(ret)
|
||||||
|
b = pickle.loads(a)
|
||||||
|
self.assertEqual(ret, b)
|
||||||
|
|
||||||
|
check(psutil.Process().as_dict())
|
||||||
|
check(psutil.virtual_memory())
|
||||||
|
check(psutil.swap_memory())
|
||||||
|
check(psutil.cpu_times())
|
||||||
|
check(psutil.cpu_times_percent(interval=0))
|
||||||
|
check(psutil.net_io_counters())
|
||||||
|
if LINUX and not os.path.exists('/proc/diskstats'):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if not APPVEYOR:
|
||||||
|
check(psutil.disk_io_counters())
|
||||||
|
check(psutil.disk_partitions())
|
||||||
|
check(psutil.disk_usage(os.getcwd()))
|
||||||
|
check(psutil.users())
|
||||||
|
|
||||||
|
# XXX: https://github.com/pypa/setuptools/pull/2896
|
||||||
|
@unittest.skipIf(APPVEYOR, "temporarily disabled due to setuptools bug")
|
||||||
|
def test_setup_script(self):
|
||||||
|
setup_py = os.path.join(ROOT_DIR, 'setup.py')
|
||||||
|
if CI_TESTING and not os.path.exists(setup_py):
|
||||||
|
return self.skipTest("can't find setup.py")
|
||||||
|
module = import_module_by_path(setup_py)
|
||||||
|
self.assertRaises(SystemExit, module.setup)
|
||||||
|
self.assertEqual(module.get_version(), psutil.__version__)
|
||||||
|
|
||||||
|
def test_ad_on_process_creation(self):
|
||||||
|
# We are supposed to be able to instantiate Process also in case
|
||||||
|
# of zombie processes or access denied.
|
||||||
|
with mock.patch.object(psutil.Process, 'create_time',
|
||||||
|
side_effect=psutil.AccessDenied) as meth:
|
||||||
|
psutil.Process()
|
||||||
|
assert meth.called
|
||||||
|
with mock.patch.object(psutil.Process, 'create_time',
|
||||||
|
side_effect=psutil.ZombieProcess(1)) as meth:
|
||||||
|
psutil.Process()
|
||||||
|
assert meth.called
|
||||||
|
with mock.patch.object(psutil.Process, 'create_time',
|
||||||
|
side_effect=ValueError) as meth:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
psutil.Process()
|
||||||
|
assert meth.called
|
||||||
|
|
||||||
|
def test_sanity_version_check(self):
|
||||||
|
# see: https://github.com/giampaolo/psutil/issues/564
|
||||||
|
with mock.patch(
|
||||||
|
"psutil._psplatform.cext.version", return_value="0.0.0"):
|
||||||
|
with self.assertRaises(ImportError) as cm:
|
||||||
|
reload_module(psutil)
|
||||||
|
self.assertIn("version conflict", str(cm.exception).lower())
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- psutil/_common.py utils
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommonModule(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_memoize(self):
|
||||||
|
@memoize
|
||||||
|
def foo(*args, **kwargs):
|
||||||
|
"foo docstring"
|
||||||
|
calls.append(None)
|
||||||
|
return (args, kwargs)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
# no args
|
||||||
|
for x in range(2):
|
||||||
|
ret = foo()
|
||||||
|
expected = ((), {})
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
# with args
|
||||||
|
for x in range(2):
|
||||||
|
ret = foo(1)
|
||||||
|
expected = ((1, ), {})
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
self.assertEqual(len(calls), 2)
|
||||||
|
# with args + kwargs
|
||||||
|
for x in range(2):
|
||||||
|
ret = foo(1, bar=2)
|
||||||
|
expected = ((1, ), {'bar': 2})
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
self.assertEqual(len(calls), 3)
|
||||||
|
# clear cache
|
||||||
|
foo.cache_clear()
|
||||||
|
ret = foo()
|
||||||
|
expected = ((), {})
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
self.assertEqual(len(calls), 4)
|
||||||
|
# docstring
|
||||||
|
self.assertEqual(foo.__doc__, "foo docstring")
|
||||||
|
|
||||||
|
def test_memoize_when_activated(self):
|
||||||
|
class Foo:
|
||||||
|
|
||||||
|
@memoize_when_activated
|
||||||
|
def foo(self):
|
||||||
|
calls.append(None)
|
||||||
|
|
||||||
|
f = Foo()
|
||||||
|
calls = []
|
||||||
|
f.foo()
|
||||||
|
f.foo()
|
||||||
|
self.assertEqual(len(calls), 2)
|
||||||
|
|
||||||
|
# activate
|
||||||
|
calls = []
|
||||||
|
f.foo.cache_activate(f)
|
||||||
|
f.foo()
|
||||||
|
f.foo()
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
|
||||||
|
# deactivate
|
||||||
|
calls = []
|
||||||
|
f.foo.cache_deactivate(f)
|
||||||
|
f.foo()
|
||||||
|
f.foo()
|
||||||
|
self.assertEqual(len(calls), 2)
|
||||||
|
|
||||||
|
def test_parse_environ_block(self):
|
||||||
|
def k(s):
|
||||||
|
return s.upper() if WINDOWS else s
|
||||||
|
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0"),
|
||||||
|
{k("a"): "1"})
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0b=2\0\0"),
|
||||||
|
{k("a"): "1", k("b"): "2"})
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0b=\0\0"),
|
||||||
|
{k("a"): "1", k("b"): ""})
|
||||||
|
# ignore everything after \0\0
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0b=2\0\0c=3\0"),
|
||||||
|
{k("a"): "1", k("b"): "2"})
|
||||||
|
# ignore everything that is not an assignment
|
||||||
|
self.assertEqual(parse_environ_block("xxx\0a=1\0"), {k("a"): "1"})
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0=b=2\0"), {k("a"): "1"})
|
||||||
|
# do not fail if the block is incomplete
|
||||||
|
self.assertEqual(parse_environ_block("a=1\0b=2"), {k("a"): "1"})
|
||||||
|
|
||||||
|
def test_supports_ipv6(self):
|
||||||
|
self.addCleanup(supports_ipv6.cache_clear)
|
||||||
|
if supports_ipv6():
|
||||||
|
with mock.patch('psutil._common.socket') as s:
|
||||||
|
s.has_ipv6 = False
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
assert not supports_ipv6()
|
||||||
|
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
with mock.patch('psutil._common.socket.socket',
|
||||||
|
side_effect=socket.error) as s:
|
||||||
|
assert not supports_ipv6()
|
||||||
|
assert s.called
|
||||||
|
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
with mock.patch('psutil._common.socket.socket',
|
||||||
|
side_effect=socket.gaierror) as s:
|
||||||
|
assert not supports_ipv6()
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
assert s.called
|
||||||
|
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
with mock.patch('psutil._common.socket.socket.bind',
|
||||||
|
side_effect=socket.gaierror) as s:
|
||||||
|
assert not supports_ipv6()
|
||||||
|
supports_ipv6.cache_clear()
|
||||||
|
assert s.called
|
||||||
|
else:
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
sock.bind(("::1", 0))
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
def test_isfile_strict(self):
|
||||||
|
this_file = os.path.abspath(__file__)
|
||||||
|
assert isfile_strict(this_file)
|
||||||
|
assert not isfile_strict(os.path.dirname(this_file))
|
||||||
|
with mock.patch('psutil._common.os.stat',
|
||||||
|
side_effect=OSError(errno.EPERM, "foo")):
|
||||||
|
self.assertRaises(OSError, isfile_strict, this_file)
|
||||||
|
with mock.patch('psutil._common.os.stat',
|
||||||
|
side_effect=OSError(errno.EACCES, "foo")):
|
||||||
|
self.assertRaises(OSError, isfile_strict, this_file)
|
||||||
|
with mock.patch('psutil._common.os.stat',
|
||||||
|
side_effect=OSError(errno.ENOENT, "foo")):
|
||||||
|
assert not isfile_strict(this_file)
|
||||||
|
with mock.patch('psutil._common.stat.S_ISREG', return_value=False):
|
||||||
|
assert not isfile_strict(this_file)
|
||||||
|
|
||||||
|
def test_debug(self):
|
||||||
|
if PY3:
|
||||||
|
from io import StringIO
|
||||||
|
else:
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
with redirect_stderr(StringIO()) as f:
|
||||||
|
debug("hello")
|
||||||
|
msg = f.getvalue()
|
||||||
|
assert msg.startswith("psutil-debug"), msg
|
||||||
|
self.assertIn("hello", msg)
|
||||||
|
self.assertIn(__file__.replace('.pyc', '.py'), msg)
|
||||||
|
|
||||||
|
# supposed to use repr(exc)
|
||||||
|
with redirect_stderr(StringIO()) as f:
|
||||||
|
debug(ValueError("this is an error"))
|
||||||
|
msg = f.getvalue()
|
||||||
|
self.assertIn("ignoring ValueError", msg)
|
||||||
|
self.assertIn("'this is an error'", msg)
|
||||||
|
|
||||||
|
# supposed to use str(exc), because of extra info about file name
|
||||||
|
with redirect_stderr(StringIO()) as f:
|
||||||
|
exc = OSError(2, "no such file")
|
||||||
|
exc.filename = "/foo"
|
||||||
|
debug(exc)
|
||||||
|
msg = f.getvalue()
|
||||||
|
self.assertIn("no such file", msg)
|
||||||
|
self.assertIn("/foo", msg)
|
||||||
|
|
||||||
|
def test_cat_bcat(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with open(testfn, "wt") as f:
|
||||||
|
f.write("foo")
|
||||||
|
self.assertEqual(cat(testfn), "foo")
|
||||||
|
self.assertEqual(bcat(testfn), b"foo")
|
||||||
|
self.assertRaises(FileNotFoundError, cat, testfn + '-invalid')
|
||||||
|
self.assertRaises(FileNotFoundError, bcat, testfn + '-invalid')
|
||||||
|
self.assertEqual(cat(testfn + '-invalid', fallback="bar"), "bar")
|
||||||
|
self.assertEqual(bcat(testfn + '-invalid', fallback="bar"), "bar")
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Tests for wrap_numbers() function.
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
nt = collections.namedtuple('foo', 'a b c')
|
||||||
|
|
||||||
|
|
||||||
|
class TestWrapNumbers(PsutilTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
wrap_numbers.cache_clear()
|
||||||
|
|
||||||
|
tearDown = setUp
|
||||||
|
|
||||||
|
def test_first_call(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
|
||||||
|
def test_input_hasnt_changed(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
|
||||||
|
def test_increase_but_no_wrap(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
input = {'disk1': nt(10, 15, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
input = {'disk1': nt(20, 25, 30)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
input = {'disk1': nt(20, 25, 30)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
|
||||||
|
def test_wrap(self):
|
||||||
|
# let's say 100 is the threshold
|
||||||
|
input = {'disk1': nt(100, 100, 100)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
# first wrap restarts from 10
|
||||||
|
input = {'disk1': nt(100, 100, 10)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(100, 100, 110)})
|
||||||
|
# then it remains the same
|
||||||
|
input = {'disk1': nt(100, 100, 10)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(100, 100, 110)})
|
||||||
|
# then it goes up
|
||||||
|
input = {'disk1': nt(100, 100, 90)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(100, 100, 190)})
|
||||||
|
# then it wraps again
|
||||||
|
input = {'disk1': nt(100, 100, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(100, 100, 210)})
|
||||||
|
# and remains the same
|
||||||
|
input = {'disk1': nt(100, 100, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(100, 100, 210)})
|
||||||
|
# now wrap another num
|
||||||
|
input = {'disk1': nt(50, 100, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(150, 100, 210)})
|
||||||
|
# and again
|
||||||
|
input = {'disk1': nt(40, 100, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(190, 100, 210)})
|
||||||
|
# keep it the same
|
||||||
|
input = {'disk1': nt(40, 100, 20)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(190, 100, 210)})
|
||||||
|
|
||||||
|
def test_changing_keys(self):
|
||||||
|
# Emulate a case where the second call to disk_io()
|
||||||
|
# (or whatever) provides a new disk, then the new disk
|
||||||
|
# disappears on the third call.
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
input = {'disk1': nt(5, 5, 5),
|
||||||
|
'disk2': nt(7, 7, 7)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
input = {'disk1': nt(8, 8, 8)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
|
||||||
|
def test_changing_keys_w_wrap(self):
|
||||||
|
input = {'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 100)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
# disk 2 wraps
|
||||||
|
input = {'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 10)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 110)})
|
||||||
|
# disk 2 disappears
|
||||||
|
input = {'disk1': nt(50, 50, 50)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
|
||||||
|
# then it appears again; the old wrap is supposed to be
|
||||||
|
# gone.
|
||||||
|
input = {'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 100)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
# remains the same
|
||||||
|
input = {'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 100)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'), input)
|
||||||
|
# and then wraps again
|
||||||
|
input = {'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 10)}
|
||||||
|
self.assertEqual(wrap_numbers(input, 'disk_io'),
|
||||||
|
{'disk1': nt(50, 50, 50),
|
||||||
|
'disk2': nt(100, 100, 110)})
|
||||||
|
|
||||||
|
def test_real_data(self):
|
||||||
|
d = {'nvme0n1': (300, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048),
|
||||||
|
'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8),
|
||||||
|
'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28),
|
||||||
|
'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)}
|
||||||
|
self.assertEqual(wrap_numbers(d, 'disk_io'), d)
|
||||||
|
self.assertEqual(wrap_numbers(d, 'disk_io'), d)
|
||||||
|
# decrease this ↓
|
||||||
|
d = {'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048),
|
||||||
|
'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8),
|
||||||
|
'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28),
|
||||||
|
'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)}
|
||||||
|
out = wrap_numbers(d, 'disk_io')
|
||||||
|
self.assertEqual(out['nvme0n1'][0], 400)
|
||||||
|
|
||||||
|
# --- cache tests
|
||||||
|
|
||||||
|
def test_cache_first_call(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
self.assertEqual(cache[1], {'disk_io': {}})
|
||||||
|
self.assertEqual(cache[2], {'disk_io': {}})
|
||||||
|
|
||||||
|
def test_cache_call_twice(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
input = {'disk1': nt(10, 10, 10)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
self.assertEqual(
|
||||||
|
cache[1],
|
||||||
|
{'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}})
|
||||||
|
self.assertEqual(cache[2], {'disk_io': {}})
|
||||||
|
|
||||||
|
def test_cache_wrap(self):
|
||||||
|
# let's say 100 is the threshold
|
||||||
|
input = {'disk1': nt(100, 100, 100)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
|
||||||
|
# first wrap restarts from 10
|
||||||
|
input = {'disk1': nt(100, 100, 10)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
self.assertEqual(
|
||||||
|
cache[1],
|
||||||
|
{'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}})
|
||||||
|
self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}})
|
||||||
|
|
||||||
|
def assert_():
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(
|
||||||
|
cache[1],
|
||||||
|
{'disk_io': {('disk1', 0): 0, ('disk1', 1): 0,
|
||||||
|
('disk1', 2): 100}})
|
||||||
|
self.assertEqual(cache[2],
|
||||||
|
{'disk_io': {'disk1': set([('disk1', 2)])}})
|
||||||
|
|
||||||
|
# then it remains the same
|
||||||
|
input = {'disk1': nt(100, 100, 10)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
assert_()
|
||||||
|
|
||||||
|
# then it goes up
|
||||||
|
input = {'disk1': nt(100, 100, 90)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
assert_()
|
||||||
|
|
||||||
|
# then it wraps again
|
||||||
|
input = {'disk1': nt(100, 100, 20)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
self.assertEqual(
|
||||||
|
cache[1],
|
||||||
|
{'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190}})
|
||||||
|
self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}})
|
||||||
|
|
||||||
|
def test_cache_changing_keys(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
input = {'disk1': nt(5, 5, 5),
|
||||||
|
'disk2': nt(7, 7, 7)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
cache = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(cache[0], {'disk_io': input})
|
||||||
|
self.assertEqual(
|
||||||
|
cache[1],
|
||||||
|
{'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}})
|
||||||
|
self.assertEqual(cache[2], {'disk_io': {}})
|
||||||
|
|
||||||
|
def test_cache_clear(self):
|
||||||
|
input = {'disk1': nt(5, 5, 5)}
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
wrap_numbers(input, 'disk_io')
|
||||||
|
wrap_numbers.cache_clear('disk_io')
|
||||||
|
self.assertEqual(wrap_numbers.cache_info(), ({}, {}, {}))
|
||||||
|
wrap_numbers.cache_clear('disk_io')
|
||||||
|
wrap_numbers.cache_clear('?!?')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported')
|
||||||
|
def test_cache_clear_public_apis(self):
|
||||||
|
if not psutil.disk_io_counters() or not psutil.net_io_counters():
|
||||||
|
return self.skipTest("no disks or NICs available")
|
||||||
|
psutil.disk_io_counters()
|
||||||
|
psutil.net_io_counters()
|
||||||
|
caches = wrap_numbers.cache_info()
|
||||||
|
for cache in caches:
|
||||||
|
self.assertIn('psutil.disk_io_counters', cache)
|
||||||
|
self.assertIn('psutil.net_io_counters', cache)
|
||||||
|
|
||||||
|
psutil.disk_io_counters.cache_clear()
|
||||||
|
caches = wrap_numbers.cache_info()
|
||||||
|
for cache in caches:
|
||||||
|
self.assertIn('psutil.net_io_counters', cache)
|
||||||
|
self.assertNotIn('psutil.disk_io_counters', cache)
|
||||||
|
|
||||||
|
psutil.net_io_counters.cache_clear()
|
||||||
|
caches = wrap_numbers.cache_info()
|
||||||
|
self.assertEqual(caches, ({}, {}, {}))
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Example script tests
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not os.path.exists(SCRIPTS_DIR),
|
||||||
|
"can't locate scripts directory")
|
||||||
|
class TestScripts(PsutilTestCase):
|
||||||
|
"""Tests for scripts in the "scripts" directory."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_stdout(exe, *args, **kwargs):
|
||||||
|
exe = '%s' % os.path.join(SCRIPTS_DIR, exe)
|
||||||
|
cmd = [PYTHON_EXE, exe]
|
||||||
|
for arg in args:
|
||||||
|
cmd.append(arg)
|
||||||
|
try:
|
||||||
|
out = sh(cmd, **kwargs).strip()
|
||||||
|
except RuntimeError as err:
|
||||||
|
if 'AccessDenied' in str(err):
|
||||||
|
return str(err)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
assert out, out
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_syntax(exe, args=None):
|
||||||
|
exe = os.path.join(SCRIPTS_DIR, exe)
|
||||||
|
if PY3:
|
||||||
|
f = open(exe, 'rt', encoding='utf8')
|
||||||
|
else:
|
||||||
|
f = open(exe, 'rt')
|
||||||
|
with f:
|
||||||
|
src = f.read()
|
||||||
|
ast.parse(src)
|
||||||
|
|
||||||
|
def test_coverage(self):
|
||||||
|
# make sure all example scripts have a test method defined
|
||||||
|
meths = dir(self)
|
||||||
|
for name in os.listdir(SCRIPTS_DIR):
|
||||||
|
if name.endswith('.py'):
|
||||||
|
if 'test_' + os.path.splitext(name)[0] not in meths:
|
||||||
|
# self.assert_stdout(name)
|
||||||
|
raise self.fail('no test defined for %r script'
|
||||||
|
% os.path.join(SCRIPTS_DIR, name))
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
def test_executable(self):
|
||||||
|
for root, dirs, files in os.walk(SCRIPTS_DIR):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.py'):
|
||||||
|
path = os.path.join(root, file)
|
||||||
|
if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]:
|
||||||
|
raise self.fail('%r is not executable' % path)
|
||||||
|
|
||||||
|
def test_disk_usage(self):
|
||||||
|
self.assert_stdout('disk_usage.py')
|
||||||
|
|
||||||
|
def test_free(self):
|
||||||
|
self.assert_stdout('free.py')
|
||||||
|
|
||||||
|
def test_meminfo(self):
|
||||||
|
self.assert_stdout('meminfo.py')
|
||||||
|
|
||||||
|
def test_procinfo(self):
|
||||||
|
self.assert_stdout('procinfo.py', str(os.getpid()))
|
||||||
|
|
||||||
|
@unittest.skipIf(CI_TESTING and not psutil.users(), "no users")
|
||||||
|
def test_who(self):
|
||||||
|
self.assert_stdout('who.py')
|
||||||
|
|
||||||
|
def test_ps(self):
|
||||||
|
self.assert_stdout('ps.py')
|
||||||
|
|
||||||
|
def test_pstree(self):
|
||||||
|
self.assert_stdout('pstree.py')
|
||||||
|
|
||||||
|
def test_netstat(self):
|
||||||
|
self.assert_stdout('netstat.py')
|
||||||
|
|
||||||
|
def test_ifconfig(self):
|
||||||
|
self.assert_stdout('ifconfig.py')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_MEMORY_MAPS, "not supported")
|
||||||
|
def test_pmap(self):
|
||||||
|
self.assert_stdout('pmap.py', str(os.getpid()))
|
||||||
|
|
||||||
|
def test_procsmem(self):
|
||||||
|
if 'uss' not in psutil.Process().memory_full_info()._fields:
|
||||||
|
raise self.skipTest("not supported")
|
||||||
|
self.assert_stdout('procsmem.py')
|
||||||
|
|
||||||
|
def test_killall(self):
|
||||||
|
self.assert_syntax('killall.py')
|
||||||
|
|
||||||
|
def test_nettop(self):
|
||||||
|
self.assert_syntax('nettop.py')
|
||||||
|
|
||||||
|
def test_top(self):
|
||||||
|
self.assert_syntax('top.py')
|
||||||
|
|
||||||
|
def test_iotop(self):
|
||||||
|
self.assert_syntax('iotop.py')
|
||||||
|
|
||||||
|
def test_pidof(self):
|
||||||
|
output = self.assert_stdout('pidof.py', psutil.Process().name())
|
||||||
|
self.assertIn(str(os.getpid()), output)
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
def test_winservices(self):
|
||||||
|
self.assert_stdout('winservices.py')
|
||||||
|
|
||||||
|
def test_cpu_distribution(self):
|
||||||
|
self.assert_syntax('cpu_distribution.py')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported")
|
||||||
|
def test_temperatures(self):
|
||||||
|
if not psutil.sensors_temperatures():
|
||||||
|
self.skipTest("no temperatures")
|
||||||
|
self.assert_stdout('temperatures.py')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_FANS, "not supported")
|
||||||
|
def test_fans(self):
|
||||||
|
if not psutil.sensors_fans():
|
||||||
|
self.skipTest("no fans")
|
||||||
|
self.assert_stdout('fans.py')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported")
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_battery(self):
|
||||||
|
self.assert_stdout('battery.py')
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported")
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_sensors(self):
|
||||||
|
self.assert_stdout('sensors.py')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""macOS specific tests."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil.tests import HAS_BATTERY
|
||||||
|
from psutil.tests import TOLERANCE_DISK_USAGE
|
||||||
|
from psutil.tests import TOLERANCE_SYS_MEM
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import sh
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import terminate
|
||||||
|
|
||||||
|
|
||||||
|
if POSIX:
|
||||||
|
from psutil._psutil_posix import getpagesize
|
||||||
|
|
||||||
|
|
||||||
|
def sysctl(cmdline):
|
||||||
|
"""Expects a sysctl command with an argument and parse the result
|
||||||
|
returning only the value of interest.
|
||||||
|
"""
|
||||||
|
out = sh(cmdline)
|
||||||
|
result = out.split()[1]
|
||||||
|
try:
|
||||||
|
return int(result)
|
||||||
|
except ValueError:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def vm_stat(field):
|
||||||
|
"""Wrapper around 'vm_stat' cmdline utility."""
|
||||||
|
out = sh('vm_stat')
|
||||||
|
for line in out.split('\n'):
|
||||||
|
if field in line:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("line not found")
|
||||||
|
return int(re.search(r'\d+', line).group(0)) * getpagesize()
|
||||||
|
|
||||||
|
|
||||||
|
# http://code.activestate.com/recipes/578019/
|
||||||
|
def human2bytes(s):
|
||||||
|
SYMBOLS = {
|
||||||
|
'customary': ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'),
|
||||||
|
}
|
||||||
|
init = s
|
||||||
|
num = ""
|
||||||
|
while s and s[0:1].isdigit() or s[0:1] == '.':
|
||||||
|
num += s[0]
|
||||||
|
s = s[1:]
|
||||||
|
num = float(num)
|
||||||
|
letter = s.strip()
|
||||||
|
for name, sset in SYMBOLS.items():
|
||||||
|
if letter in sset:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if letter == 'k':
|
||||||
|
sset = SYMBOLS['customary']
|
||||||
|
letter = letter.upper()
|
||||||
|
else:
|
||||||
|
raise ValueError("can't interpret %r" % init)
|
||||||
|
prefix = {sset[0]: 1}
|
||||||
|
for i, s in enumerate(sset[1:]):
|
||||||
|
prefix[s] = 1 << (i + 1) * 10
|
||||||
|
return int(num * prefix[letter])
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not MACOS, "MACOS only")
|
||||||
|
class TestProcess(PsutilTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
def test_process_create_time(self):
|
||||||
|
output = sh("ps -o lstart -p %s" % self.pid)
|
||||||
|
start_ps = output.replace('STARTED', '').strip()
|
||||||
|
hhmmss = start_ps.split(' ')[-2]
|
||||||
|
year = start_ps.split(' ')[-1]
|
||||||
|
start_psutil = psutil.Process(self.pid).create_time()
|
||||||
|
self.assertEqual(
|
||||||
|
hhmmss,
|
||||||
|
time.strftime("%H:%M:%S", time.localtime(start_psutil)))
|
||||||
|
self.assertEqual(
|
||||||
|
year,
|
||||||
|
time.strftime("%Y", time.localtime(start_psutil)))
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not MACOS, "MACOS only")
|
||||||
|
class TestSystemAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
# --- disk
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_disks(self):
|
||||||
|
# test psutil.disk_usage() and psutil.disk_partitions()
|
||||||
|
# against "df -a"
|
||||||
|
def df(path):
|
||||||
|
out = sh('df -k "%s"' % path).strip()
|
||||||
|
lines = out.split('\n')
|
||||||
|
lines.pop(0)
|
||||||
|
line = lines.pop(0)
|
||||||
|
dev, total, used, free = line.split()[:4]
|
||||||
|
if dev == 'none':
|
||||||
|
dev = ''
|
||||||
|
total = int(total) * 1024
|
||||||
|
used = int(used) * 1024
|
||||||
|
free = int(free) * 1024
|
||||||
|
return dev, total, used, free
|
||||||
|
|
||||||
|
for part in psutil.disk_partitions(all=False):
|
||||||
|
usage = psutil.disk_usage(part.mountpoint)
|
||||||
|
dev, total, used, free = df(part.mountpoint)
|
||||||
|
self.assertEqual(part.device, dev)
|
||||||
|
self.assertEqual(usage.total, total)
|
||||||
|
self.assertAlmostEqual(usage.free, free,
|
||||||
|
delta=TOLERANCE_DISK_USAGE)
|
||||||
|
self.assertAlmostEqual(usage.used, used,
|
||||||
|
delta=TOLERANCE_DISK_USAGE)
|
||||||
|
|
||||||
|
# --- cpu
|
||||||
|
|
||||||
|
def test_cpu_count_logical(self):
|
||||||
|
num = sysctl("sysctl hw.logicalcpu")
|
||||||
|
self.assertEqual(num, psutil.cpu_count(logical=True))
|
||||||
|
|
||||||
|
def test_cpu_count_cores(self):
|
||||||
|
num = sysctl("sysctl hw.physicalcpu")
|
||||||
|
self.assertEqual(num, psutil.cpu_count(logical=False))
|
||||||
|
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
freq = psutil.cpu_freq()
|
||||||
|
self.assertEqual(
|
||||||
|
freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency"))
|
||||||
|
self.assertEqual(
|
||||||
|
freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min"))
|
||||||
|
self.assertEqual(
|
||||||
|
freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max"))
|
||||||
|
|
||||||
|
# --- virtual mem
|
||||||
|
|
||||||
|
def test_vmem_total(self):
|
||||||
|
sysctl_hwphymem = sysctl('sysctl hw.memsize')
|
||||||
|
self.assertEqual(sysctl_hwphymem, psutil.virtual_memory().total)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_free(self):
|
||||||
|
vmstat_val = vm_stat("free")
|
||||||
|
psutil_val = psutil.virtual_memory().free
|
||||||
|
self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_active(self):
|
||||||
|
vmstat_val = vm_stat("active")
|
||||||
|
psutil_val = psutil.virtual_memory().active
|
||||||
|
self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_inactive(self):
|
||||||
|
vmstat_val = vm_stat("inactive")
|
||||||
|
psutil_val = psutil.virtual_memory().inactive
|
||||||
|
self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vmem_wired(self):
|
||||||
|
vmstat_val = vm_stat("wired")
|
||||||
|
psutil_val = psutil.virtual_memory().wired
|
||||||
|
self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM)
|
||||||
|
|
||||||
|
# --- swap mem
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_swapmem_sin(self):
|
||||||
|
vmstat_val = vm_stat("Pageins")
|
||||||
|
psutil_val = psutil.swap_memory().sin
|
||||||
|
self.assertEqual(psutil_val, vmstat_val)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_swapmem_sout(self):
|
||||||
|
vmstat_val = vm_stat("Pageout")
|
||||||
|
psutil_val = psutil.swap_memory().sout
|
||||||
|
self.assertEqual(psutil_val, vmstat_val)
|
||||||
|
|
||||||
|
# Not very reliable.
|
||||||
|
# def test_swapmem_total(self):
|
||||||
|
# out = sh('sysctl vm.swapusage')
|
||||||
|
# out = out.replace('vm.swapusage: ', '')
|
||||||
|
# total, used, free = re.findall('\d+.\d+\w', out)
|
||||||
|
# psutil_smem = psutil.swap_memory()
|
||||||
|
# self.assertEqual(psutil_smem.total, human2bytes(total))
|
||||||
|
# self.assertEqual(psutil_smem.used, human2bytes(used))
|
||||||
|
# self.assertEqual(psutil_smem.free, human2bytes(free))
|
||||||
|
|
||||||
|
# --- network
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
for name, stats in psutil.net_if_stats().items():
|
||||||
|
try:
|
||||||
|
out = sh("ifconfig %s" % name)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertEqual(stats.isup, 'RUNNING' in out, msg=out)
|
||||||
|
self.assertEqual(stats.mtu,
|
||||||
|
int(re.findall(r'mtu (\d+)', out)[0]))
|
||||||
|
|
||||||
|
# --- sensors_battery
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_sensors_battery(self):
|
||||||
|
out = sh("pmset -g batt")
|
||||||
|
percent = re.search(r"(\d+)%", out).group(1)
|
||||||
|
drawing_from = re.search("Now drawing from '([^']+)'", out).group(1)
|
||||||
|
power_plugged = drawing_from == "AC Power"
|
||||||
|
psutil_result = psutil.sensors_battery()
|
||||||
|
self.assertEqual(psutil_result.power_plugged, power_plugged)
|
||||||
|
self.assertEqual(psutil_result.percent, int(percent))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,426 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""POSIX specific tests."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import AIX
|
||||||
|
from psutil import BSD
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import HAS_NET_IO_COUNTERS
|
||||||
|
from psutil.tests import PYTHON_EXE
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import mock
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import sh
|
||||||
|
from psutil.tests import skip_on_access_denied
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import terminate
|
||||||
|
from psutil.tests import which
|
||||||
|
|
||||||
|
|
||||||
|
if POSIX:
|
||||||
|
import mmap
|
||||||
|
import resource
|
||||||
|
|
||||||
|
from psutil._psutil_posix import getpagesize
|
||||||
|
|
||||||
|
|
||||||
|
def ps(fmt, pid=None):
|
||||||
|
"""
|
||||||
|
Wrapper for calling the ps command with a little bit of cross-platform
|
||||||
|
support for a narrow range of features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cmd = ['ps']
|
||||||
|
|
||||||
|
if LINUX:
|
||||||
|
cmd.append('--no-headers')
|
||||||
|
|
||||||
|
if pid is not None:
|
||||||
|
cmd.extend(['-p', str(pid)])
|
||||||
|
else:
|
||||||
|
if SUNOS or AIX:
|
||||||
|
cmd.append('-A')
|
||||||
|
else:
|
||||||
|
cmd.append('ax')
|
||||||
|
|
||||||
|
if SUNOS:
|
||||||
|
fmt_map = set(('command', 'comm', 'start', 'stime'))
|
||||||
|
fmt = fmt_map.get(fmt, fmt)
|
||||||
|
|
||||||
|
cmd.extend(['-o', fmt])
|
||||||
|
|
||||||
|
output = sh(cmd)
|
||||||
|
|
||||||
|
if LINUX:
|
||||||
|
output = output.splitlines()
|
||||||
|
else:
|
||||||
|
output = output.splitlines()[1:]
|
||||||
|
|
||||||
|
all_output = []
|
||||||
|
for line in output:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
line = int(line)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
all_output.append(line)
|
||||||
|
|
||||||
|
if pid is None:
|
||||||
|
return all_output
|
||||||
|
else:
|
||||||
|
return all_output[0]
|
||||||
|
|
||||||
|
# ps "-o" field names differ wildly between platforms.
|
||||||
|
# "comm" means "only executable name" but is not available on BSD platforms.
|
||||||
|
# "args" means "command with all its arguments", and is also not available
|
||||||
|
# on BSD platforms.
|
||||||
|
# "command" is like "args" on most platforms, but like "comm" on AIX,
|
||||||
|
# and not available on SUNOS.
|
||||||
|
# so for the executable name we can use "comm" on Solaris and split "command"
|
||||||
|
# on other platforms.
|
||||||
|
# to get the cmdline (with args) we have to use "args" on AIX and
|
||||||
|
# Solaris, and can use "command" on all others.
|
||||||
|
|
||||||
|
|
||||||
|
def ps_name(pid):
|
||||||
|
field = "command"
|
||||||
|
if SUNOS:
|
||||||
|
field = "comm"
|
||||||
|
return ps(field, pid).split()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ps_args(pid):
|
||||||
|
field = "command"
|
||||||
|
if AIX or SUNOS:
|
||||||
|
field = "args"
|
||||||
|
return ps(field, pid)
|
||||||
|
|
||||||
|
|
||||||
|
def ps_rss(pid):
|
||||||
|
field = "rss"
|
||||||
|
if AIX:
|
||||||
|
field = "rssize"
|
||||||
|
return ps(field, pid)
|
||||||
|
|
||||||
|
|
||||||
|
def ps_vsz(pid):
|
||||||
|
field = "vsz"
|
||||||
|
if AIX:
|
||||||
|
field = "vsize"
|
||||||
|
return ps(field, pid)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
class TestProcess(PsutilTestCase):
|
||||||
|
"""Compare psutil results against 'ps' command line utility (mainly)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc([PYTHON_EXE, "-E", "-O"],
|
||||||
|
stdin=subprocess.PIPE).pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
def test_ppid(self):
|
||||||
|
ppid_ps = ps('ppid', self.pid)
|
||||||
|
ppid_psutil = psutil.Process(self.pid).ppid()
|
||||||
|
self.assertEqual(ppid_ps, ppid_psutil)
|
||||||
|
|
||||||
|
def test_uid(self):
|
||||||
|
uid_ps = ps('uid', self.pid)
|
||||||
|
uid_psutil = psutil.Process(self.pid).uids().real
|
||||||
|
self.assertEqual(uid_ps, uid_psutil)
|
||||||
|
|
||||||
|
def test_gid(self):
|
||||||
|
gid_ps = ps('rgid', self.pid)
|
||||||
|
gid_psutil = psutil.Process(self.pid).gids().real
|
||||||
|
self.assertEqual(gid_ps, gid_psutil)
|
||||||
|
|
||||||
|
def test_username(self):
|
||||||
|
username_ps = ps('user', self.pid)
|
||||||
|
username_psutil = psutil.Process(self.pid).username()
|
||||||
|
self.assertEqual(username_ps, username_psutil)
|
||||||
|
|
||||||
|
def test_username_no_resolution(self):
|
||||||
|
# Emulate a case where the system can't resolve the uid to
|
||||||
|
# a username in which case psutil is supposed to return
|
||||||
|
# the stringified uid.
|
||||||
|
p = psutil.Process()
|
||||||
|
with mock.patch("psutil.pwd.getpwuid", side_effect=KeyError) as fun:
|
||||||
|
self.assertEqual(p.username(), str(p.uids().real))
|
||||||
|
assert fun.called
|
||||||
|
|
||||||
|
@skip_on_access_denied()
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_rss_memory(self):
|
||||||
|
# give python interpreter some time to properly initialize
|
||||||
|
# so that the results are the same
|
||||||
|
time.sleep(0.1)
|
||||||
|
rss_ps = ps_rss(self.pid)
|
||||||
|
rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024
|
||||||
|
self.assertEqual(rss_ps, rss_psutil)
|
||||||
|
|
||||||
|
@skip_on_access_denied()
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_vsz_memory(self):
|
||||||
|
# give python interpreter some time to properly initialize
|
||||||
|
# so that the results are the same
|
||||||
|
time.sleep(0.1)
|
||||||
|
vsz_ps = ps_vsz(self.pid)
|
||||||
|
vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024
|
||||||
|
self.assertEqual(vsz_ps, vsz_psutil)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
name_ps = ps_name(self.pid)
|
||||||
|
# remove path if there is any, from the command
|
||||||
|
name_ps = os.path.basename(name_ps).lower()
|
||||||
|
name_psutil = psutil.Process(self.pid).name().lower()
|
||||||
|
# ...because of how we calculate PYTHON_EXE; on MACOS this may
|
||||||
|
# be "pythonX.Y".
|
||||||
|
name_ps = re.sub(r"\d.\d", "", name_ps)
|
||||||
|
name_psutil = re.sub(r"\d.\d", "", name_psutil)
|
||||||
|
# ...may also be "python.X"
|
||||||
|
name_ps = re.sub(r"\d", "", name_ps)
|
||||||
|
name_psutil = re.sub(r"\d", "", name_psutil)
|
||||||
|
self.assertEqual(name_ps, name_psutil)
|
||||||
|
|
||||||
|
def test_name_long(self):
|
||||||
|
# On UNIX the kernel truncates the name to the first 15
|
||||||
|
# characters. In such a case psutil tries to determine the
|
||||||
|
# full name from the cmdline.
|
||||||
|
name = "long-program-name"
|
||||||
|
cmdline = ["long-program-name-extended", "foo", "bar"]
|
||||||
|
with mock.patch("psutil._psplatform.Process.name",
|
||||||
|
return_value=name):
|
||||||
|
with mock.patch("psutil._psplatform.Process.cmdline",
|
||||||
|
return_value=cmdline):
|
||||||
|
p = psutil.Process()
|
||||||
|
self.assertEqual(p.name(), "long-program-name-extended")
|
||||||
|
|
||||||
|
def test_name_long_cmdline_ad_exc(self):
|
||||||
|
# Same as above but emulates a case where cmdline() raises
|
||||||
|
# AccessDenied in which case psutil is supposed to return
|
||||||
|
# the truncated name instead of crashing.
|
||||||
|
name = "long-program-name"
|
||||||
|
with mock.patch("psutil._psplatform.Process.name",
|
||||||
|
return_value=name):
|
||||||
|
with mock.patch("psutil._psplatform.Process.cmdline",
|
||||||
|
side_effect=psutil.AccessDenied(0, "")):
|
||||||
|
p = psutil.Process()
|
||||||
|
self.assertEqual(p.name(), "long-program-name")
|
||||||
|
|
||||||
|
def test_name_long_cmdline_nsp_exc(self):
|
||||||
|
# Same as above but emulates a case where cmdline() raises NSP
|
||||||
|
# which is supposed to propagate.
|
||||||
|
name = "long-program-name"
|
||||||
|
with mock.patch("psutil._psplatform.Process.name",
|
||||||
|
return_value=name):
|
||||||
|
with mock.patch("psutil._psplatform.Process.cmdline",
|
||||||
|
side_effect=psutil.NoSuchProcess(0, "")):
|
||||||
|
p = psutil.Process()
|
||||||
|
self.assertRaises(psutil.NoSuchProcess, p.name)
|
||||||
|
|
||||||
|
@unittest.skipIf(MACOS or BSD, 'ps -o start not available')
|
||||||
|
def test_create_time(self):
|
||||||
|
time_ps = ps('start', self.pid)
|
||||||
|
time_psutil = psutil.Process(self.pid).create_time()
|
||||||
|
time_psutil_tstamp = datetime.datetime.fromtimestamp(
|
||||||
|
time_psutil).strftime("%H:%M:%S")
|
||||||
|
# sometimes ps shows the time rounded up instead of down, so we check
|
||||||
|
# for both possible values
|
||||||
|
round_time_psutil = round(time_psutil)
|
||||||
|
round_time_psutil_tstamp = datetime.datetime.fromtimestamp(
|
||||||
|
round_time_psutil).strftime("%H:%M:%S")
|
||||||
|
self.assertIn(time_ps, [time_psutil_tstamp, round_time_psutil_tstamp])
|
||||||
|
|
||||||
|
def test_exe(self):
|
||||||
|
ps_pathname = ps_name(self.pid)
|
||||||
|
psutil_pathname = psutil.Process(self.pid).exe()
|
||||||
|
try:
|
||||||
|
self.assertEqual(ps_pathname, psutil_pathname)
|
||||||
|
except AssertionError:
|
||||||
|
# certain platforms such as BSD are more accurate returning:
|
||||||
|
# "/usr/local/bin/python2.7"
|
||||||
|
# ...instead of:
|
||||||
|
# "/usr/local/bin/python"
|
||||||
|
# We do not want to consider this difference in accuracy
|
||||||
|
# an error.
|
||||||
|
adjusted_ps_pathname = ps_pathname[:len(ps_pathname)]
|
||||||
|
self.assertEqual(ps_pathname, adjusted_ps_pathname)
|
||||||
|
|
||||||
|
def test_cmdline(self):
|
||||||
|
ps_cmdline = ps_args(self.pid)
|
||||||
|
psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline())
|
||||||
|
self.assertEqual(ps_cmdline, psutil_cmdline)
|
||||||
|
|
||||||
|
# On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an
|
||||||
|
# incorrect value (20); the real deal is getpriority(2) which
|
||||||
|
# returns 0; psutil relies on it, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1082
|
||||||
|
# AIX has the same issue
|
||||||
|
@unittest.skipIf(SUNOS, "not reliable on SUNOS")
|
||||||
|
@unittest.skipIf(AIX, "not reliable on AIX")
|
||||||
|
def test_nice(self):
|
||||||
|
ps_nice = ps('nice', self.pid)
|
||||||
|
psutil_nice = psutil.Process().nice()
|
||||||
|
self.assertEqual(ps_nice, psutil_nice)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
class TestSystemAPIs(PsutilTestCase):
|
||||||
|
"""Test some system APIs."""
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_pids(self):
|
||||||
|
# Note: this test might fail if the OS is starting/killing
|
||||||
|
# other processes in the meantime
|
||||||
|
pids_ps = sorted(ps("pid"))
|
||||||
|
pids_psutil = psutil.pids()
|
||||||
|
|
||||||
|
# on MACOS and OPENBSD ps doesn't show pid 0
|
||||||
|
if MACOS or OPENBSD and 0 not in pids_ps:
|
||||||
|
pids_ps.insert(0, 0)
|
||||||
|
|
||||||
|
# There will often be one more process in pids_ps for ps itself
|
||||||
|
if len(pids_ps) - len(pids_psutil) > 1:
|
||||||
|
difference = [x for x in pids_psutil if x not in pids_ps] + \
|
||||||
|
[x for x in pids_ps if x not in pids_psutil]
|
||||||
|
raise self.fail("difference: " + str(difference))
|
||||||
|
|
||||||
|
# for some reason ifconfig -a does not report all interfaces
|
||||||
|
# returned by psutil
|
||||||
|
@unittest.skipIf(SUNOS, "unreliable on SUNOS")
|
||||||
|
@unittest.skipIf(not which('ifconfig'), "no ifconfig cmd")
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, "not supported")
|
||||||
|
def test_nic_names(self):
|
||||||
|
output = sh("ifconfig -a")
|
||||||
|
for nic in psutil.net_io_counters(pernic=True).keys():
|
||||||
|
for line in output.split():
|
||||||
|
if line.startswith(nic):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise self.fail(
|
||||||
|
"couldn't find %s nic in 'ifconfig -a' output\n%s" % (
|
||||||
|
nic, output))
|
||||||
|
|
||||||
|
@unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_users(self):
|
||||||
|
out = sh("who")
|
||||||
|
if not out.strip():
|
||||||
|
raise self.skipTest("no users on this system")
|
||||||
|
lines = out.split('\n')
|
||||||
|
users = [x.split()[0] for x in lines]
|
||||||
|
terminals = [x.split()[1] for x in lines]
|
||||||
|
self.assertEqual(len(users), len(psutil.users()))
|
||||||
|
for u in psutil.users():
|
||||||
|
self.assertIn(u.name, users)
|
||||||
|
self.assertIn(u.terminal, terminals)
|
||||||
|
|
||||||
|
def test_pid_exists_let_raise(self):
|
||||||
|
# According to "man 2 kill" possible error values for kill
|
||||||
|
# are (EINVAL, EPERM, ESRCH). Test that any other errno
|
||||||
|
# results in an exception.
|
||||||
|
with mock.patch("psutil._psposix.os.kill",
|
||||||
|
side_effect=OSError(errno.EBADF, "")) as m:
|
||||||
|
self.assertRaises(OSError, psutil._psposix.pid_exists, os.getpid())
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_os_waitpid_let_raise(self):
|
||||||
|
# os.waitpid() is supposed to catch EINTR and ECHILD only.
|
||||||
|
# Test that any other errno results in an exception.
|
||||||
|
with mock.patch("psutil._psposix.os.waitpid",
|
||||||
|
side_effect=OSError(errno.EBADF, "")) as m:
|
||||||
|
self.assertRaises(OSError, psutil._psposix.wait_pid, os.getpid())
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_os_waitpid_eintr(self):
|
||||||
|
# os.waitpid() is supposed to "retry" on EINTR.
|
||||||
|
with mock.patch("psutil._psposix.os.waitpid",
|
||||||
|
side_effect=OSError(errno.EINTR, "")) as m:
|
||||||
|
self.assertRaises(
|
||||||
|
psutil._psposix.TimeoutExpired,
|
||||||
|
psutil._psposix.wait_pid, os.getpid(), timeout=0.01)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_os_waitpid_bad_ret_status(self):
|
||||||
|
# Simulate os.waitpid() returning a bad status.
|
||||||
|
with mock.patch("psutil._psposix.os.waitpid",
|
||||||
|
return_value=(1, -1)) as m:
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
psutil._psposix.wait_pid, os.getpid())
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
# AIX can return '-' in df output instead of numbers, e.g. for /proc
|
||||||
|
@unittest.skipIf(AIX, "unreliable on AIX")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_disk_usage(self):
|
||||||
|
def df(device):
|
||||||
|
out = sh("df -k %s" % device).strip()
|
||||||
|
line = out.split('\n')[1]
|
||||||
|
fields = line.split()
|
||||||
|
total = int(fields[1]) * 1024
|
||||||
|
used = int(fields[2]) * 1024
|
||||||
|
free = int(fields[3]) * 1024
|
||||||
|
percent = float(fields[4].replace('%', ''))
|
||||||
|
return (total, used, free, percent)
|
||||||
|
|
||||||
|
tolerance = 4 * 1024 * 1024 # 4MB
|
||||||
|
for part in psutil.disk_partitions(all=False):
|
||||||
|
usage = psutil.disk_usage(part.mountpoint)
|
||||||
|
try:
|
||||||
|
total, used, free, percent = df(part.device)
|
||||||
|
except RuntimeError as err:
|
||||||
|
# see:
|
||||||
|
# https://travis-ci.org/giampaolo/psutil/jobs/138338464
|
||||||
|
# https://travis-ci.org/giampaolo/psutil/jobs/138343361
|
||||||
|
err = str(err).lower()
|
||||||
|
if "no such file or directory" in err or \
|
||||||
|
"raw devices not supported" in err or \
|
||||||
|
"permission denied" in err:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.assertAlmostEqual(usage.total, total, delta=tolerance)
|
||||||
|
self.assertAlmostEqual(usage.used, used, delta=tolerance)
|
||||||
|
self.assertAlmostEqual(usage.free, free, delta=tolerance)
|
||||||
|
self.assertAlmostEqual(usage.percent, percent, delta=1)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
class TestMisc(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_getpagesize(self):
|
||||||
|
pagesize = getpagesize()
|
||||||
|
self.assertGreater(pagesize, 0)
|
||||||
|
self.assertEqual(pagesize, resource.getpagesize())
|
||||||
|
self.assertEqual(pagesize, mmap.PAGESIZE)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Sun OS specific tests."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import sh
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not SUNOS, "SUNOS only")
|
||||||
|
class SunOSSpecificTestCase(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_swap_memory(self):
|
||||||
|
out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH'])
|
||||||
|
lines = out.strip().split('\n')[1:]
|
||||||
|
if not lines:
|
||||||
|
raise ValueError('no swap device(s) configured')
|
||||||
|
total = free = 0
|
||||||
|
for line in lines:
|
||||||
|
line = line.split()
|
||||||
|
t, f = line[-2:]
|
||||||
|
total += int(int(t) * 512)
|
||||||
|
free += int(int(f) * 512)
|
||||||
|
used = total - free
|
||||||
|
|
||||||
|
psutil_swap = psutil.swap_memory()
|
||||||
|
self.assertEqual(psutil_swap.total, total)
|
||||||
|
self.assertEqual(psutil_swap.used, used)
|
||||||
|
self.assertEqual(psutil_swap.free, free)
|
||||||
|
|
||||||
|
def test_cpu_count(self):
|
||||||
|
out = sh("/usr/sbin/psrinfo")
|
||||||
|
self.assertEqual(psutil.cpu_count(), len(out.split('\n')))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,884 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Tests for system APIS."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import AIX
|
||||||
|
from psutil import BSD
|
||||||
|
from psutil import FREEBSD
|
||||||
|
from psutil import LINUX
|
||||||
|
from psutil import MACOS
|
||||||
|
from psutil import NETBSD
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import SUNOS
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._compat import FileNotFoundError
|
||||||
|
from psutil._compat import long
|
||||||
|
from psutil.tests import ASCII_FS
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import DEVNULL
|
||||||
|
from psutil.tests import GITHUB_ACTIONS
|
||||||
|
from psutil.tests import GLOBAL_TIMEOUT
|
||||||
|
from psutil.tests import HAS_BATTERY
|
||||||
|
from psutil.tests import HAS_CPU_FREQ
|
||||||
|
from psutil.tests import HAS_GETLOADAVG
|
||||||
|
from psutil.tests import HAS_NET_IO_COUNTERS
|
||||||
|
from psutil.tests import HAS_SENSORS_BATTERY
|
||||||
|
from psutil.tests import HAS_SENSORS_FANS
|
||||||
|
from psutil.tests import HAS_SENSORS_TEMPERATURES
|
||||||
|
from psutil.tests import IS_64BIT
|
||||||
|
from psutil.tests import PYPY
|
||||||
|
from psutil.tests import UNICODE_SUFFIX
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import check_net_address
|
||||||
|
from psutil.tests import enum
|
||||||
|
from psutil.tests import mock
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- System-related API tests
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_process_iter(self):
|
||||||
|
self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()])
|
||||||
|
sproc = self.spawn_testproc()
|
||||||
|
self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()])
|
||||||
|
p = psutil.Process(sproc.pid)
|
||||||
|
p.kill()
|
||||||
|
p.wait()
|
||||||
|
self.assertNotIn(sproc.pid, [x.pid for x in psutil.process_iter()])
|
||||||
|
|
||||||
|
with mock.patch('psutil.Process',
|
||||||
|
side_effect=psutil.NoSuchProcess(os.getpid())):
|
||||||
|
self.assertEqual(list(psutil.process_iter()), [])
|
||||||
|
with mock.patch('psutil.Process',
|
||||||
|
side_effect=psutil.AccessDenied(os.getpid())):
|
||||||
|
with self.assertRaises(psutil.AccessDenied):
|
||||||
|
list(psutil.process_iter())
|
||||||
|
|
||||||
|
def test_prcess_iter_w_attrs(self):
|
||||||
|
for p in psutil.process_iter(attrs=['pid']):
|
||||||
|
self.assertEqual(list(p.info.keys()), ['pid'])
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
list(psutil.process_iter(attrs=['foo']))
|
||||||
|
with mock.patch("psutil._psplatform.Process.cpu_times",
|
||||||
|
side_effect=psutil.AccessDenied(0, "")) as m:
|
||||||
|
for p in psutil.process_iter(attrs=["pid", "cpu_times"]):
|
||||||
|
self.assertIsNone(p.info['cpu_times'])
|
||||||
|
self.assertGreaterEqual(p.info['pid'], 0)
|
||||||
|
assert m.called
|
||||||
|
with mock.patch("psutil._psplatform.Process.cpu_times",
|
||||||
|
side_effect=psutil.AccessDenied(0, "")) as m:
|
||||||
|
flag = object()
|
||||||
|
for p in psutil.process_iter(
|
||||||
|
attrs=["pid", "cpu_times"], ad_value=flag):
|
||||||
|
self.assertIs(p.info['cpu_times'], flag)
|
||||||
|
self.assertGreaterEqual(p.info['pid'], 0)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
@unittest.skipIf(PYPY and WINDOWS,
|
||||||
|
"spawn_testproc() unreliable on PYPY + WINDOWS")
|
||||||
|
def test_wait_procs(self):
|
||||||
|
def callback(p):
|
||||||
|
pids.append(p.pid)
|
||||||
|
|
||||||
|
pids = []
|
||||||
|
sproc1 = self.spawn_testproc()
|
||||||
|
sproc2 = self.spawn_testproc()
|
||||||
|
sproc3 = self.spawn_testproc()
|
||||||
|
procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)]
|
||||||
|
self.assertRaises(ValueError, psutil.wait_procs, procs, timeout=-1)
|
||||||
|
self.assertRaises(TypeError, psutil.wait_procs, procs, callback=1)
|
||||||
|
t = time.time()
|
||||||
|
gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback)
|
||||||
|
|
||||||
|
self.assertLess(time.time() - t, 0.5)
|
||||||
|
self.assertEqual(gone, [])
|
||||||
|
self.assertEqual(len(alive), 3)
|
||||||
|
self.assertEqual(pids, [])
|
||||||
|
for p in alive:
|
||||||
|
self.assertFalse(hasattr(p, 'returncode'))
|
||||||
|
|
||||||
|
@retry_on_failure(30)
|
||||||
|
def test(procs, callback):
|
||||||
|
gone, alive = psutil.wait_procs(procs, timeout=0.03,
|
||||||
|
callback=callback)
|
||||||
|
self.assertEqual(len(gone), 1)
|
||||||
|
self.assertEqual(len(alive), 2)
|
||||||
|
return gone, alive
|
||||||
|
|
||||||
|
sproc3.terminate()
|
||||||
|
gone, alive = test(procs, callback)
|
||||||
|
self.assertIn(sproc3.pid, [x.pid for x in gone])
|
||||||
|
if POSIX:
|
||||||
|
self.assertEqual(gone.pop().returncode, -signal.SIGTERM)
|
||||||
|
else:
|
||||||
|
self.assertEqual(gone.pop().returncode, 1)
|
||||||
|
self.assertEqual(pids, [sproc3.pid])
|
||||||
|
for p in alive:
|
||||||
|
self.assertFalse(hasattr(p, 'returncode'))
|
||||||
|
|
||||||
|
@retry_on_failure(30)
|
||||||
|
def test(procs, callback):
|
||||||
|
gone, alive = psutil.wait_procs(procs, timeout=0.03,
|
||||||
|
callback=callback)
|
||||||
|
self.assertEqual(len(gone), 3)
|
||||||
|
self.assertEqual(len(alive), 0)
|
||||||
|
return gone, alive
|
||||||
|
|
||||||
|
sproc1.terminate()
|
||||||
|
sproc2.terminate()
|
||||||
|
gone, alive = test(procs, callback)
|
||||||
|
self.assertEqual(set(pids), set([sproc1.pid, sproc2.pid, sproc3.pid]))
|
||||||
|
for p in gone:
|
||||||
|
self.assertTrue(hasattr(p, 'returncode'))
|
||||||
|
|
||||||
|
@unittest.skipIf(PYPY and WINDOWS,
|
||||||
|
"spawn_testproc() unreliable on PYPY + WINDOWS")
|
||||||
|
def test_wait_procs_no_timeout(self):
|
||||||
|
sproc1 = self.spawn_testproc()
|
||||||
|
sproc2 = self.spawn_testproc()
|
||||||
|
sproc3 = self.spawn_testproc()
|
||||||
|
procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)]
|
||||||
|
for p in procs:
|
||||||
|
p.terminate()
|
||||||
|
gone, alive = psutil.wait_procs(procs)
|
||||||
|
|
||||||
|
def test_pid_exists(self):
|
||||||
|
sproc = self.spawn_testproc()
|
||||||
|
self.assertTrue(psutil.pid_exists(sproc.pid))
|
||||||
|
p = psutil.Process(sproc.pid)
|
||||||
|
p.kill()
|
||||||
|
p.wait()
|
||||||
|
self.assertFalse(psutil.pid_exists(sproc.pid))
|
||||||
|
self.assertFalse(psutil.pid_exists(-1))
|
||||||
|
self.assertEqual(psutil.pid_exists(0), 0 in psutil.pids())
|
||||||
|
|
||||||
|
def test_pid_exists_2(self):
|
||||||
|
pids = psutil.pids()
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
assert psutil.pid_exists(pid)
|
||||||
|
except AssertionError:
|
||||||
|
# in case the process disappeared in meantime fail only
|
||||||
|
# if it is no longer in psutil.pids()
|
||||||
|
time.sleep(.1)
|
||||||
|
self.assertNotIn(pid, psutil.pids())
|
||||||
|
pids = range(max(pids) + 5000, max(pids) + 6000)
|
||||||
|
for pid in pids:
|
||||||
|
self.assertFalse(psutil.pid_exists(pid), msg=pid)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMiscAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_boot_time(self):
|
||||||
|
bt = psutil.boot_time()
|
||||||
|
self.assertIsInstance(bt, float)
|
||||||
|
self.assertGreater(bt, 0)
|
||||||
|
self.assertLess(bt, time.time())
|
||||||
|
|
||||||
|
@unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI")
|
||||||
|
def test_users(self):
|
||||||
|
users = psutil.users()
|
||||||
|
self.assertNotEqual(users, [])
|
||||||
|
for user in users:
|
||||||
|
assert user.name, user
|
||||||
|
self.assertIsInstance(user.name, str)
|
||||||
|
self.assertIsInstance(user.terminal, (str, type(None)))
|
||||||
|
if user.host is not None:
|
||||||
|
self.assertIsInstance(user.host, (str, type(None)))
|
||||||
|
user.terminal
|
||||||
|
user.host
|
||||||
|
assert user.started > 0.0, user
|
||||||
|
datetime.datetime.fromtimestamp(user.started)
|
||||||
|
if WINDOWS or OPENBSD:
|
||||||
|
self.assertIsNone(user.pid)
|
||||||
|
else:
|
||||||
|
psutil.Process(user.pid)
|
||||||
|
|
||||||
|
def test_test(self):
|
||||||
|
# test for psutil.test() function
|
||||||
|
stdout = sys.stdout
|
||||||
|
sys.stdout = DEVNULL
|
||||||
|
try:
|
||||||
|
psutil.test()
|
||||||
|
finally:
|
||||||
|
sys.stdout = stdout
|
||||||
|
|
||||||
|
def test_os_constants(self):
|
||||||
|
names = ["POSIX", "WINDOWS", "LINUX", "MACOS", "FREEBSD", "OPENBSD",
|
||||||
|
"NETBSD", "BSD", "SUNOS"]
|
||||||
|
for name in names:
|
||||||
|
self.assertIsInstance(getattr(psutil, name), bool, msg=name)
|
||||||
|
|
||||||
|
if os.name == 'posix':
|
||||||
|
assert psutil.POSIX
|
||||||
|
assert not psutil.WINDOWS
|
||||||
|
names.remove("POSIX")
|
||||||
|
if "linux" in sys.platform.lower():
|
||||||
|
assert psutil.LINUX
|
||||||
|
names.remove("LINUX")
|
||||||
|
elif "bsd" in sys.platform.lower():
|
||||||
|
assert psutil.BSD
|
||||||
|
self.assertEqual([psutil.FREEBSD, psutil.OPENBSD,
|
||||||
|
psutil.NETBSD].count(True), 1)
|
||||||
|
names.remove("BSD")
|
||||||
|
names.remove("FREEBSD")
|
||||||
|
names.remove("OPENBSD")
|
||||||
|
names.remove("NETBSD")
|
||||||
|
elif "sunos" in sys.platform.lower() or \
|
||||||
|
"solaris" in sys.platform.lower():
|
||||||
|
assert psutil.SUNOS
|
||||||
|
names.remove("SUNOS")
|
||||||
|
elif "darwin" in sys.platform.lower():
|
||||||
|
assert psutil.MACOS
|
||||||
|
names.remove("MACOS")
|
||||||
|
else:
|
||||||
|
assert psutil.WINDOWS
|
||||||
|
assert not psutil.POSIX
|
||||||
|
names.remove("WINDOWS")
|
||||||
|
|
||||||
|
# assert all other constants are set to False
|
||||||
|
for name in names:
|
||||||
|
self.assertIs(getattr(psutil, name), False, msg=name)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemoryAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_virtual_memory(self):
|
||||||
|
mem = psutil.virtual_memory()
|
||||||
|
assert mem.total > 0, mem
|
||||||
|
assert mem.available > 0, mem
|
||||||
|
assert 0 <= mem.percent <= 100, mem
|
||||||
|
assert mem.used > 0, mem
|
||||||
|
assert mem.free >= 0, mem
|
||||||
|
for name in mem._fields:
|
||||||
|
value = getattr(mem, name)
|
||||||
|
if name != 'percent':
|
||||||
|
self.assertIsInstance(value, (int, long))
|
||||||
|
if name != 'total':
|
||||||
|
if not value >= 0:
|
||||||
|
raise self.fail("%r < 0 (%s)" % (name, value))
|
||||||
|
if value > mem.total:
|
||||||
|
raise self.fail("%r > total (total=%s, %s=%s)"
|
||||||
|
% (name, mem.total, name, value))
|
||||||
|
|
||||||
|
def test_swap_memory(self):
|
||||||
|
mem = psutil.swap_memory()
|
||||||
|
self.assertEqual(
|
||||||
|
mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout'))
|
||||||
|
|
||||||
|
assert mem.total >= 0, mem
|
||||||
|
assert mem.used >= 0, mem
|
||||||
|
if mem.total > 0:
|
||||||
|
# likely a system with no swap partition
|
||||||
|
assert mem.free > 0, mem
|
||||||
|
else:
|
||||||
|
assert mem.free == 0, mem
|
||||||
|
assert 0 <= mem.percent <= 100, mem
|
||||||
|
assert mem.sin >= 0, mem
|
||||||
|
assert mem.sout >= 0, mem
|
||||||
|
|
||||||
|
|
||||||
|
class TestCpuAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_cpu_count_logical(self):
|
||||||
|
logical = psutil.cpu_count()
|
||||||
|
self.assertIsNotNone(logical)
|
||||||
|
self.assertEqual(logical, len(psutil.cpu_times(percpu=True)))
|
||||||
|
self.assertGreaterEqual(logical, 1)
|
||||||
|
#
|
||||||
|
if os.path.exists("/proc/cpuinfo"):
|
||||||
|
with open("/proc/cpuinfo") as fd:
|
||||||
|
cpuinfo_data = fd.read()
|
||||||
|
if "physical id" not in cpuinfo_data:
|
||||||
|
raise unittest.SkipTest("cpuinfo doesn't include physical id")
|
||||||
|
|
||||||
|
def test_cpu_count_cores(self):
|
||||||
|
logical = psutil.cpu_count()
|
||||||
|
cores = psutil.cpu_count(logical=False)
|
||||||
|
if cores is None:
|
||||||
|
raise self.skipTest("cpu_count_cores() is None")
|
||||||
|
if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista
|
||||||
|
self.assertIsNone(cores)
|
||||||
|
else:
|
||||||
|
self.assertGreaterEqual(cores, 1)
|
||||||
|
self.assertGreaterEqual(logical, cores)
|
||||||
|
|
||||||
|
def test_cpu_count_none(self):
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1085
|
||||||
|
for val in (-1, 0, None):
|
||||||
|
with mock.patch('psutil._psplatform.cpu_count_logical',
|
||||||
|
return_value=val) as m:
|
||||||
|
self.assertIsNone(psutil.cpu_count())
|
||||||
|
assert m.called
|
||||||
|
with mock.patch('psutil._psplatform.cpu_count_cores',
|
||||||
|
return_value=val) as m:
|
||||||
|
self.assertIsNone(psutil.cpu_count(logical=False))
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_cpu_times(self):
|
||||||
|
# Check type, value >= 0, str().
|
||||||
|
total = 0
|
||||||
|
times = psutil.cpu_times()
|
||||||
|
sum(times)
|
||||||
|
for cp_time in times:
|
||||||
|
self.assertIsInstance(cp_time, float)
|
||||||
|
self.assertGreaterEqual(cp_time, 0.0)
|
||||||
|
total += cp_time
|
||||||
|
self.assertEqual(total, sum(times))
|
||||||
|
str(times)
|
||||||
|
# CPU times are always supposed to increase over time
|
||||||
|
# or at least remain the same and that's because time
|
||||||
|
# cannot go backwards.
|
||||||
|
# Surprisingly sometimes this might not be the case (at
|
||||||
|
# least on Windows and Linux), see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/392
|
||||||
|
# https://github.com/giampaolo/psutil/issues/645
|
||||||
|
# if not WINDOWS:
|
||||||
|
# last = psutil.cpu_times()
|
||||||
|
# for x in range(100):
|
||||||
|
# new = psutil.cpu_times()
|
||||||
|
# for field in new._fields:
|
||||||
|
# new_t = getattr(new, field)
|
||||||
|
# last_t = getattr(last, field)
|
||||||
|
# self.assertGreaterEqual(new_t, last_t,
|
||||||
|
# msg="%s %s" % (new_t, last_t))
|
||||||
|
# last = new
|
||||||
|
|
||||||
|
def test_cpu_times_time_increases(self):
|
||||||
|
# Make sure time increases between calls.
|
||||||
|
t1 = sum(psutil.cpu_times())
|
||||||
|
stop_at = time.time() + GLOBAL_TIMEOUT
|
||||||
|
while time.time() < stop_at:
|
||||||
|
t2 = sum(psutil.cpu_times())
|
||||||
|
if t2 > t1:
|
||||||
|
return
|
||||||
|
raise self.fail("time remained the same")
|
||||||
|
|
||||||
|
def test_per_cpu_times(self):
|
||||||
|
# Check type, value >= 0, str().
|
||||||
|
for times in psutil.cpu_times(percpu=True):
|
||||||
|
total = 0
|
||||||
|
sum(times)
|
||||||
|
for cp_time in times:
|
||||||
|
self.assertIsInstance(cp_time, float)
|
||||||
|
self.assertGreaterEqual(cp_time, 0.0)
|
||||||
|
total += cp_time
|
||||||
|
self.assertEqual(total, sum(times))
|
||||||
|
str(times)
|
||||||
|
self.assertEqual(len(psutil.cpu_times(percpu=True)[0]),
|
||||||
|
len(psutil.cpu_times(percpu=False)))
|
||||||
|
|
||||||
|
# Note: in theory CPU times are always supposed to increase over
|
||||||
|
# time or remain the same but never go backwards. In practice
|
||||||
|
# sometimes this is not the case.
|
||||||
|
# This issue seemd to be afflict Windows:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/392
|
||||||
|
# ...but it turns out also Linux (rarely) behaves the same.
|
||||||
|
# last = psutil.cpu_times(percpu=True)
|
||||||
|
# for x in range(100):
|
||||||
|
# new = psutil.cpu_times(percpu=True)
|
||||||
|
# for index in range(len(new)):
|
||||||
|
# newcpu = new[index]
|
||||||
|
# lastcpu = last[index]
|
||||||
|
# for field in newcpu._fields:
|
||||||
|
# new_t = getattr(newcpu, field)
|
||||||
|
# last_t = getattr(lastcpu, field)
|
||||||
|
# self.assertGreaterEqual(
|
||||||
|
# new_t, last_t, msg="%s %s" % (lastcpu, newcpu))
|
||||||
|
# last = new
|
||||||
|
|
||||||
|
def test_per_cpu_times_2(self):
|
||||||
|
# Simulate some work load then make sure time have increased
|
||||||
|
# between calls.
|
||||||
|
tot1 = psutil.cpu_times(percpu=True)
|
||||||
|
giveup_at = time.time() + GLOBAL_TIMEOUT
|
||||||
|
while True:
|
||||||
|
if time.time() >= giveup_at:
|
||||||
|
return self.fail("timeout")
|
||||||
|
tot2 = psutil.cpu_times(percpu=True)
|
||||||
|
for t1, t2 in zip(tot1, tot2):
|
||||||
|
t1, t2 = psutil._cpu_busy_time(t1), psutil._cpu_busy_time(t2)
|
||||||
|
difference = t2 - t1
|
||||||
|
if difference >= 0.05:
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_cpu_times_comparison(self):
|
||||||
|
# Make sure the sum of all per cpu times is almost equal to
|
||||||
|
# base "one cpu" times.
|
||||||
|
base = psutil.cpu_times()
|
||||||
|
per_cpu = psutil.cpu_times(percpu=True)
|
||||||
|
summed_values = base._make([sum(num) for num in zip(*per_cpu)])
|
||||||
|
for field in base._fields:
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
getattr(base, field), getattr(summed_values, field), delta=1)
|
||||||
|
|
||||||
|
def _test_cpu_percent(self, percent, last_ret, new_ret):
|
||||||
|
try:
|
||||||
|
self.assertIsInstance(percent, float)
|
||||||
|
self.assertGreaterEqual(percent, 0.0)
|
||||||
|
self.assertIsNot(percent, -0.0)
|
||||||
|
self.assertLessEqual(percent, 100.0 * psutil.cpu_count())
|
||||||
|
except AssertionError as err:
|
||||||
|
raise AssertionError("\n%s\nlast=%s\nnew=%s" % (
|
||||||
|
err, pprint.pformat(last_ret), pprint.pformat(new_ret)))
|
||||||
|
|
||||||
|
def test_cpu_percent(self):
|
||||||
|
last = psutil.cpu_percent(interval=0.001)
|
||||||
|
for x in range(100):
|
||||||
|
new = psutil.cpu_percent(interval=None)
|
||||||
|
self._test_cpu_percent(new, last, new)
|
||||||
|
last = new
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
psutil.cpu_percent(interval=-1)
|
||||||
|
|
||||||
|
def test_per_cpu_percent(self):
|
||||||
|
last = psutil.cpu_percent(interval=0.001, percpu=True)
|
||||||
|
self.assertEqual(len(last), psutil.cpu_count())
|
||||||
|
for x in range(100):
|
||||||
|
new = psutil.cpu_percent(interval=None, percpu=True)
|
||||||
|
for percent in new:
|
||||||
|
self._test_cpu_percent(percent, last, new)
|
||||||
|
last = new
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
psutil.cpu_percent(interval=-1, percpu=True)
|
||||||
|
|
||||||
|
def test_cpu_times_percent(self):
|
||||||
|
last = psutil.cpu_times_percent(interval=0.001)
|
||||||
|
for x in range(100):
|
||||||
|
new = psutil.cpu_times_percent(interval=None)
|
||||||
|
for percent in new:
|
||||||
|
self._test_cpu_percent(percent, last, new)
|
||||||
|
self._test_cpu_percent(sum(new), last, new)
|
||||||
|
last = new
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
psutil.cpu_times_percent(interval=-1)
|
||||||
|
|
||||||
|
def test_per_cpu_times_percent(self):
|
||||||
|
last = psutil.cpu_times_percent(interval=0.001, percpu=True)
|
||||||
|
self.assertEqual(len(last), psutil.cpu_count())
|
||||||
|
for x in range(100):
|
||||||
|
new = psutil.cpu_times_percent(interval=None, percpu=True)
|
||||||
|
for cpu in new:
|
||||||
|
for percent in cpu:
|
||||||
|
self._test_cpu_percent(percent, last, new)
|
||||||
|
self._test_cpu_percent(sum(cpu), last, new)
|
||||||
|
last = new
|
||||||
|
|
||||||
|
def test_per_cpu_times_percent_negative(self):
|
||||||
|
# see: https://github.com/giampaolo/psutil/issues/645
|
||||||
|
psutil.cpu_times_percent(percpu=True)
|
||||||
|
zero_times = [x._make([0 for x in range(len(x._fields))])
|
||||||
|
for x in psutil.cpu_times(percpu=True)]
|
||||||
|
with mock.patch('psutil.cpu_times', return_value=zero_times):
|
||||||
|
for cpu in psutil.cpu_times_percent(percpu=True):
|
||||||
|
for percent in cpu:
|
||||||
|
self._test_cpu_percent(percent, None, None)
|
||||||
|
|
||||||
|
def test_cpu_stats(self):
|
||||||
|
# Tested more extensively in per-platform test modules.
|
||||||
|
infos = psutil.cpu_stats()
|
||||||
|
self.assertEqual(
|
||||||
|
infos._fields,
|
||||||
|
('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'))
|
||||||
|
for name in infos._fields:
|
||||||
|
value = getattr(infos, name)
|
||||||
|
self.assertGreaterEqual(value, 0)
|
||||||
|
# on AIX, ctx_switches is always 0
|
||||||
|
if not AIX and name in ('ctx_switches', 'interrupts'):
|
||||||
|
self.assertGreater(value, 0)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_CPU_FREQ, "not supported")
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
def check_ls(ls):
|
||||||
|
for nt in ls:
|
||||||
|
self.assertEqual(nt._fields, ('current', 'min', 'max'))
|
||||||
|
if nt.max != 0.0:
|
||||||
|
self.assertLessEqual(nt.current, nt.max)
|
||||||
|
for name in nt._fields:
|
||||||
|
value = getattr(nt, name)
|
||||||
|
self.assertIsInstance(value, (int, long, float))
|
||||||
|
self.assertGreaterEqual(value, 0)
|
||||||
|
|
||||||
|
ls = psutil.cpu_freq(percpu=True)
|
||||||
|
if FREEBSD and not ls:
|
||||||
|
raise self.skipTest("returns empty list on FreeBSD")
|
||||||
|
|
||||||
|
assert ls, ls
|
||||||
|
check_ls([psutil.cpu_freq(percpu=False)])
|
||||||
|
|
||||||
|
if LINUX:
|
||||||
|
self.assertEqual(len(ls), psutil.cpu_count())
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_GETLOADAVG, "not supported")
|
||||||
|
def test_getloadavg(self):
|
||||||
|
loadavg = psutil.getloadavg()
|
||||||
|
self.assertEqual(len(loadavg), 3)
|
||||||
|
for load in loadavg:
|
||||||
|
self.assertIsInstance(load, float)
|
||||||
|
self.assertGreaterEqual(load, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiskAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf(PYPY and not IS_64BIT, "unreliable on PYPY32 + 32BIT")
|
||||||
|
def test_disk_usage(self):
|
||||||
|
usage = psutil.disk_usage(os.getcwd())
|
||||||
|
self.assertEqual(usage._fields, ('total', 'used', 'free', 'percent'))
|
||||||
|
|
||||||
|
assert usage.total > 0, usage
|
||||||
|
assert usage.used > 0, usage
|
||||||
|
assert usage.free > 0, usage
|
||||||
|
assert usage.total > usage.used, usage
|
||||||
|
assert usage.total > usage.free, usage
|
||||||
|
assert 0 <= usage.percent <= 100, usage.percent
|
||||||
|
if hasattr(shutil, 'disk_usage'):
|
||||||
|
# py >= 3.3, see: http://bugs.python.org/issue12442
|
||||||
|
shutil_usage = shutil.disk_usage(os.getcwd())
|
||||||
|
tolerance = 5 * 1024 * 1024 # 5MB
|
||||||
|
self.assertEqual(usage.total, shutil_usage.total)
|
||||||
|
self.assertAlmostEqual(usage.free, shutil_usage.free,
|
||||||
|
delta=tolerance)
|
||||||
|
self.assertAlmostEqual(usage.used, shutil_usage.used,
|
||||||
|
delta=tolerance)
|
||||||
|
|
||||||
|
# if path does not exist OSError ENOENT is expected across
|
||||||
|
# all platforms
|
||||||
|
fname = self.get_testfn()
|
||||||
|
with self.assertRaises(FileNotFoundError):
|
||||||
|
psutil.disk_usage(fname)
|
||||||
|
|
||||||
|
@unittest.skipIf(not ASCII_FS, "not an ASCII fs")
|
||||||
|
def test_disk_usage_unicode(self):
|
||||||
|
# See: https://github.com/giampaolo/psutil/issues/416
|
||||||
|
with self.assertRaises(UnicodeEncodeError):
|
||||||
|
psutil.disk_usage(UNICODE_SUFFIX)
|
||||||
|
|
||||||
|
def test_disk_usage_bytes(self):
|
||||||
|
psutil.disk_usage(b'.')
|
||||||
|
|
||||||
|
def test_disk_partitions(self):
|
||||||
|
def check_ntuple(nt):
|
||||||
|
self.assertIsInstance(nt.device, str)
|
||||||
|
self.assertIsInstance(nt.mountpoint, str)
|
||||||
|
self.assertIsInstance(nt.fstype, str)
|
||||||
|
self.assertIsInstance(nt.opts, str)
|
||||||
|
self.assertIsInstance(nt.maxfile, (int, type(None)))
|
||||||
|
self.assertIsInstance(nt.maxpath, (int, type(None)))
|
||||||
|
if nt.maxfile is not None and not GITHUB_ACTIONS:
|
||||||
|
self.assertGreater(nt.maxfile, 0)
|
||||||
|
if nt.maxpath is not None:
|
||||||
|
self.assertGreater(nt.maxpath, 0)
|
||||||
|
|
||||||
|
# all = False
|
||||||
|
ls = psutil.disk_partitions(all=False)
|
||||||
|
self.assertTrue(ls, msg=ls)
|
||||||
|
for disk in ls:
|
||||||
|
check_ntuple(disk)
|
||||||
|
if WINDOWS and 'cdrom' in disk.opts:
|
||||||
|
continue
|
||||||
|
if not POSIX:
|
||||||
|
assert os.path.exists(disk.device), disk
|
||||||
|
else:
|
||||||
|
# we cannot make any assumption about this, see:
|
||||||
|
# http://goo.gl/p9c43
|
||||||
|
disk.device
|
||||||
|
# on modern systems mount points can also be files
|
||||||
|
assert os.path.exists(disk.mountpoint), disk
|
||||||
|
assert disk.fstype, disk
|
||||||
|
|
||||||
|
# all = True
|
||||||
|
ls = psutil.disk_partitions(all=True)
|
||||||
|
self.assertTrue(ls, msg=ls)
|
||||||
|
for disk in psutil.disk_partitions(all=True):
|
||||||
|
check_ntuple(disk)
|
||||||
|
if not WINDOWS and disk.mountpoint:
|
||||||
|
try:
|
||||||
|
os.stat(disk.mountpoint)
|
||||||
|
except OSError as err:
|
||||||
|
if GITHUB_ACTIONS and MACOS and err.errno == errno.EIO:
|
||||||
|
continue
|
||||||
|
# http://mail.python.org/pipermail/python-dev/
|
||||||
|
# 2012-June/120787.html
|
||||||
|
if err.errno not in (errno.EPERM, errno.EACCES):
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
assert os.path.exists(disk.mountpoint), disk
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def find_mount_point(path):
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
while not os.path.ismount(path):
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
return path.lower()
|
||||||
|
|
||||||
|
mount = find_mount_point(__file__)
|
||||||
|
mounts = [x.mountpoint.lower() for x in
|
||||||
|
psutil.disk_partitions(all=True) if x.mountpoint]
|
||||||
|
self.assertIn(mount, mounts)
|
||||||
|
|
||||||
|
@unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'),
|
||||||
|
'/proc/diskstats not available on this linux version')
|
||||||
|
@unittest.skipIf(CI_TESTING and not psutil.disk_io_counters(),
|
||||||
|
"unreliable on CI") # no visible disks
|
||||||
|
def test_disk_io_counters(self):
|
||||||
|
def check_ntuple(nt):
|
||||||
|
self.assertEqual(nt[0], nt.read_count)
|
||||||
|
self.assertEqual(nt[1], nt.write_count)
|
||||||
|
self.assertEqual(nt[2], nt.read_bytes)
|
||||||
|
self.assertEqual(nt[3], nt.write_bytes)
|
||||||
|
if not (OPENBSD or NETBSD):
|
||||||
|
self.assertEqual(nt[4], nt.read_time)
|
||||||
|
self.assertEqual(nt[5], nt.write_time)
|
||||||
|
if LINUX:
|
||||||
|
self.assertEqual(nt[6], nt.read_merged_count)
|
||||||
|
self.assertEqual(nt[7], nt.write_merged_count)
|
||||||
|
self.assertEqual(nt[8], nt.busy_time)
|
||||||
|
elif FREEBSD:
|
||||||
|
self.assertEqual(nt[6], nt.busy_time)
|
||||||
|
for name in nt._fields:
|
||||||
|
assert getattr(nt, name) >= 0, nt
|
||||||
|
|
||||||
|
ret = psutil.disk_io_counters(perdisk=False)
|
||||||
|
assert ret is not None, "no disks on this system?"
|
||||||
|
check_ntuple(ret)
|
||||||
|
ret = psutil.disk_io_counters(perdisk=True)
|
||||||
|
# make sure there are no duplicates
|
||||||
|
self.assertEqual(len(ret), len(set(ret)))
|
||||||
|
for key in ret:
|
||||||
|
assert key, key
|
||||||
|
check_ntuple(ret[key])
|
||||||
|
|
||||||
|
def test_disk_io_counters_no_disks(self):
|
||||||
|
# Emulate a case where no disks are installed, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1062
|
||||||
|
with mock.patch('psutil._psplatform.disk_io_counters',
|
||||||
|
return_value={}) as m:
|
||||||
|
self.assertIsNone(psutil.disk_io_counters(perdisk=False))
|
||||||
|
self.assertEqual(psutil.disk_io_counters(perdisk=True), {})
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported')
|
||||||
|
def test_net_io_counters(self):
|
||||||
|
def check_ntuple(nt):
|
||||||
|
self.assertEqual(nt[0], nt.bytes_sent)
|
||||||
|
self.assertEqual(nt[1], nt.bytes_recv)
|
||||||
|
self.assertEqual(nt[2], nt.packets_sent)
|
||||||
|
self.assertEqual(nt[3], nt.packets_recv)
|
||||||
|
self.assertEqual(nt[4], nt.errin)
|
||||||
|
self.assertEqual(nt[5], nt.errout)
|
||||||
|
self.assertEqual(nt[6], nt.dropin)
|
||||||
|
self.assertEqual(nt[7], nt.dropout)
|
||||||
|
assert nt.bytes_sent >= 0, nt
|
||||||
|
assert nt.bytes_recv >= 0, nt
|
||||||
|
assert nt.packets_sent >= 0, nt
|
||||||
|
assert nt.packets_recv >= 0, nt
|
||||||
|
assert nt.errin >= 0, nt
|
||||||
|
assert nt.errout >= 0, nt
|
||||||
|
assert nt.dropin >= 0, nt
|
||||||
|
assert nt.dropout >= 0, nt
|
||||||
|
|
||||||
|
ret = psutil.net_io_counters(pernic=False)
|
||||||
|
check_ntuple(ret)
|
||||||
|
ret = psutil.net_io_counters(pernic=True)
|
||||||
|
self.assertNotEqual(ret, [])
|
||||||
|
for key in ret:
|
||||||
|
self.assertTrue(key)
|
||||||
|
self.assertIsInstance(key, str)
|
||||||
|
check_ntuple(ret[key])
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported')
|
||||||
|
def test_net_io_counters_no_nics(self):
|
||||||
|
# Emulate a case where no NICs are installed, see:
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1062
|
||||||
|
with mock.patch('psutil._psplatform.net_io_counters',
|
||||||
|
return_value={}) as m:
|
||||||
|
self.assertIsNone(psutil.net_io_counters(pernic=False))
|
||||||
|
self.assertEqual(psutil.net_io_counters(pernic=True), {})
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_net_if_addrs(self):
|
||||||
|
nics = psutil.net_if_addrs()
|
||||||
|
assert nics, nics
|
||||||
|
|
||||||
|
nic_stats = psutil.net_if_stats()
|
||||||
|
|
||||||
|
# Not reliable on all platforms (net_if_addrs() reports more
|
||||||
|
# interfaces).
|
||||||
|
# self.assertEqual(sorted(nics.keys()),
|
||||||
|
# sorted(psutil.net_io_counters(pernic=True).keys()))
|
||||||
|
|
||||||
|
families = set([socket.AF_INET, socket.AF_INET6, psutil.AF_LINK])
|
||||||
|
for nic, addrs in nics.items():
|
||||||
|
self.assertIsInstance(nic, str)
|
||||||
|
self.assertEqual(len(set(addrs)), len(addrs))
|
||||||
|
for addr in addrs:
|
||||||
|
self.assertIsInstance(addr.family, int)
|
||||||
|
self.assertIsInstance(addr.address, str)
|
||||||
|
self.assertIsInstance(addr.netmask, (str, type(None)))
|
||||||
|
self.assertIsInstance(addr.broadcast, (str, type(None)))
|
||||||
|
self.assertIn(addr.family, families)
|
||||||
|
if sys.version_info >= (3, 4) and not PYPY:
|
||||||
|
self.assertIsInstance(addr.family, enum.IntEnum)
|
||||||
|
if nic_stats[nic].isup:
|
||||||
|
# Do not test binding to addresses of interfaces
|
||||||
|
# that are down
|
||||||
|
if addr.family == socket.AF_INET:
|
||||||
|
s = socket.socket(addr.family)
|
||||||
|
with contextlib.closing(s):
|
||||||
|
s.bind((addr.address, 0))
|
||||||
|
elif addr.family == socket.AF_INET6:
|
||||||
|
info = socket.getaddrinfo(
|
||||||
|
addr.address, 0, socket.AF_INET6,
|
||||||
|
socket.SOCK_STREAM, 0, socket.AI_PASSIVE)[0]
|
||||||
|
af, socktype, proto, canonname, sa = info
|
||||||
|
s = socket.socket(af, socktype, proto)
|
||||||
|
with contextlib.closing(s):
|
||||||
|
s.bind(sa)
|
||||||
|
for ip in (addr.address, addr.netmask, addr.broadcast,
|
||||||
|
addr.ptp):
|
||||||
|
if ip is not None:
|
||||||
|
# TODO: skip AF_INET6 for now because I get:
|
||||||
|
# AddressValueError: Only hex digits permitted in
|
||||||
|
# u'c6f3%lxcbr0' in u'fe80::c8e0:fff:fe54:c6f3%lxcbr0'
|
||||||
|
if addr.family != socket.AF_INET6:
|
||||||
|
check_net_address(ip, addr.family)
|
||||||
|
# broadcast and ptp addresses are mutually exclusive
|
||||||
|
if addr.broadcast:
|
||||||
|
self.assertIsNone(addr.ptp)
|
||||||
|
elif addr.ptp:
|
||||||
|
self.assertIsNone(addr.broadcast)
|
||||||
|
|
||||||
|
if BSD or MACOS or SUNOS:
|
||||||
|
if hasattr(socket, "AF_LINK"):
|
||||||
|
self.assertEqual(psutil.AF_LINK, socket.AF_LINK)
|
||||||
|
elif LINUX:
|
||||||
|
self.assertEqual(psutil.AF_LINK, socket.AF_PACKET)
|
||||||
|
elif WINDOWS:
|
||||||
|
self.assertEqual(psutil.AF_LINK, -1)
|
||||||
|
|
||||||
|
def test_net_if_addrs_mac_null_bytes(self):
|
||||||
|
# Simulate that the underlying C function returns an incomplete
|
||||||
|
# MAC address. psutil is supposed to fill it with null bytes.
|
||||||
|
# https://github.com/giampaolo/psutil/issues/786
|
||||||
|
if POSIX:
|
||||||
|
ret = [('em1', psutil.AF_LINK, '06:3d:29', None, None, None)]
|
||||||
|
else:
|
||||||
|
ret = [('em1', -1, '06-3d-29', None, None, None)]
|
||||||
|
with mock.patch('psutil._psplatform.net_if_addrs',
|
||||||
|
return_value=ret) as m:
|
||||||
|
addr = psutil.net_if_addrs()['em1'][0]
|
||||||
|
assert m.called
|
||||||
|
if POSIX:
|
||||||
|
self.assertEqual(addr.address, '06:3d:29:00:00:00')
|
||||||
|
else:
|
||||||
|
self.assertEqual(addr.address, '06-3d-29-00-00-00')
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
nics = psutil.net_if_stats()
|
||||||
|
assert nics, nics
|
||||||
|
all_duplexes = (psutil.NIC_DUPLEX_FULL,
|
||||||
|
psutil.NIC_DUPLEX_HALF,
|
||||||
|
psutil.NIC_DUPLEX_UNKNOWN)
|
||||||
|
for name, stats in nics.items():
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
isup, duplex, speed, mtu = stats
|
||||||
|
self.assertIsInstance(isup, bool)
|
||||||
|
self.assertIn(duplex, all_duplexes)
|
||||||
|
self.assertIn(duplex, all_duplexes)
|
||||||
|
self.assertGreaterEqual(speed, 0)
|
||||||
|
self.assertGreaterEqual(mtu, 0)
|
||||||
|
|
||||||
|
@unittest.skipIf(not (LINUX or BSD or MACOS),
|
||||||
|
"LINUX or BSD or MACOS specific")
|
||||||
|
def test_net_if_stats_enodev(self):
|
||||||
|
# See: https://github.com/giampaolo/psutil/issues/1279
|
||||||
|
with mock.patch('psutil._psutil_posix.net_if_mtu',
|
||||||
|
side_effect=OSError(errno.ENODEV, "")) as m:
|
||||||
|
ret = psutil.net_if_stats()
|
||||||
|
self.assertEqual(ret, {})
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
|
||||||
|
class TestSensorsAPIs(PsutilTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported")
|
||||||
|
def test_sensors_temperatures(self):
|
||||||
|
temps = psutil.sensors_temperatures()
|
||||||
|
for name, entries in temps.items():
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
for entry in entries:
|
||||||
|
self.assertIsInstance(entry.label, str)
|
||||||
|
if entry.current is not None:
|
||||||
|
self.assertGreaterEqual(entry.current, 0)
|
||||||
|
if entry.high is not None:
|
||||||
|
self.assertGreaterEqual(entry.high, 0)
|
||||||
|
if entry.critical is not None:
|
||||||
|
self.assertGreaterEqual(entry.critical, 0)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported")
|
||||||
|
def test_sensors_temperatures_fahreneit(self):
|
||||||
|
d = {'coretemp': [('label', 50.0, 60.0, 70.0)]}
|
||||||
|
with mock.patch("psutil._psplatform.sensors_temperatures",
|
||||||
|
return_value=d) as m:
|
||||||
|
temps = psutil.sensors_temperatures(
|
||||||
|
fahrenheit=True)['coretemp'][0]
|
||||||
|
assert m.called
|
||||||
|
self.assertEqual(temps.current, 122.0)
|
||||||
|
self.assertEqual(temps.high, 140.0)
|
||||||
|
self.assertEqual(temps.critical, 158.0)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported")
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_sensors_battery(self):
|
||||||
|
ret = psutil.sensors_battery()
|
||||||
|
self.assertGreaterEqual(ret.percent, 0)
|
||||||
|
self.assertLessEqual(ret.percent, 100)
|
||||||
|
if ret.secsleft not in (psutil.POWER_TIME_UNKNOWN,
|
||||||
|
psutil.POWER_TIME_UNLIMITED):
|
||||||
|
self.assertGreaterEqual(ret.secsleft, 0)
|
||||||
|
else:
|
||||||
|
if ret.secsleft == psutil.POWER_TIME_UNLIMITED:
|
||||||
|
self.assertTrue(ret.power_plugged)
|
||||||
|
self.assertIsInstance(ret.power_plugged, bool)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_SENSORS_FANS, "not supported")
|
||||||
|
def test_sensors_fans(self):
|
||||||
|
fans = psutil.sensors_fans()
|
||||||
|
for name, entries in fans.items():
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
for entry in entries:
|
||||||
|
self.assertIsInstance(entry.label, str)
|
||||||
|
self.assertIsInstance(entry.current, (int, long))
|
||||||
|
self.assertGreaterEqual(entry.current, 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for testing utils (psutil.tests namespace).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import psutil.tests
|
||||||
|
from psutil import FREEBSD
|
||||||
|
from psutil import NETBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil._common import open_binary
|
||||||
|
from psutil._common import open_text
|
||||||
|
from psutil._common import supports_ipv6
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import HAS_CONNECTIONS_UNIX
|
||||||
|
from psutil.tests import PYTHON_EXE
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import TestMemoryLeak
|
||||||
|
from psutil.tests import bind_socket
|
||||||
|
from psutil.tests import bind_unix_socket
|
||||||
|
from psutil.tests import call_until
|
||||||
|
from psutil.tests import chdir
|
||||||
|
from psutil.tests import create_sockets
|
||||||
|
from psutil.tests import get_free_port
|
||||||
|
from psutil.tests import is_namedtuple
|
||||||
|
from psutil.tests import mock
|
||||||
|
from psutil.tests import process_namespace
|
||||||
|
from psutil.tests import reap_children
|
||||||
|
from psutil.tests import retry
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import safe_mkdir
|
||||||
|
from psutil.tests import safe_rmpath
|
||||||
|
from psutil.tests import serialrun
|
||||||
|
from psutil.tests import system_namespace
|
||||||
|
from psutil.tests import tcp_socketpair
|
||||||
|
from psutil.tests import terminate
|
||||||
|
from psutil.tests import unix_socketpair
|
||||||
|
from psutil.tests import wait_for_file
|
||||||
|
from psutil.tests import wait_for_pid
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# --- Unit tests for test utilities.
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetryDecorator(PsutilTestCase):
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_retry_success(self, sleep):
|
||||||
|
# Fail 3 times out of 5; make sure the decorated fun returns.
|
||||||
|
|
||||||
|
@retry(retries=5, interval=1, logfun=None)
|
||||||
|
def foo():
|
||||||
|
while queue:
|
||||||
|
queue.pop()
|
||||||
|
1 / 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
queue = list(range(3))
|
||||||
|
self.assertEqual(foo(), 1)
|
||||||
|
self.assertEqual(sleep.call_count, 3)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_retry_failure(self, sleep):
|
||||||
|
# Fail 6 times out of 5; th function is supposed to raise exc.
|
||||||
|
@retry(retries=5, interval=1, logfun=None)
|
||||||
|
def foo():
|
||||||
|
while queue:
|
||||||
|
queue.pop()
|
||||||
|
1 / 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
queue = list(range(6))
|
||||||
|
self.assertRaises(ZeroDivisionError, foo)
|
||||||
|
self.assertEqual(sleep.call_count, 5)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_exception_arg(self, sleep):
|
||||||
|
@retry(exception=ValueError, interval=1)
|
||||||
|
def foo():
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
self.assertRaises(TypeError, foo)
|
||||||
|
self.assertEqual(sleep.call_count, 0)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_no_interval_arg(self, sleep):
|
||||||
|
# if interval is not specified sleep is not supposed to be called
|
||||||
|
|
||||||
|
@retry(retries=5, interval=None, logfun=None)
|
||||||
|
def foo():
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
self.assertRaises(ZeroDivisionError, foo)
|
||||||
|
self.assertEqual(sleep.call_count, 0)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_retries_arg(self, sleep):
|
||||||
|
|
||||||
|
@retry(retries=5, interval=1, logfun=None)
|
||||||
|
def foo():
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
self.assertRaises(ZeroDivisionError, foo)
|
||||||
|
self.assertEqual(sleep.call_count, 5)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
def test_retries_and_timeout_args(self, sleep):
|
||||||
|
self.assertRaises(ValueError, retry, retries=5, timeout=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSyncTestUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_wait_for_pid(self):
|
||||||
|
wait_for_pid(os.getpid())
|
||||||
|
nopid = max(psutil.pids()) + 99999
|
||||||
|
with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])):
|
||||||
|
self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid)
|
||||||
|
|
||||||
|
def test_wait_for_file(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with open(testfn, 'w') as f:
|
||||||
|
f.write('foo')
|
||||||
|
wait_for_file(testfn)
|
||||||
|
assert not os.path.exists(testfn)
|
||||||
|
|
||||||
|
def test_wait_for_file_empty(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with open(testfn, 'w'):
|
||||||
|
pass
|
||||||
|
wait_for_file(testfn, empty=True)
|
||||||
|
assert not os.path.exists(testfn)
|
||||||
|
|
||||||
|
def test_wait_for_file_no_file(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])):
|
||||||
|
self.assertRaises(IOError, wait_for_file, testfn)
|
||||||
|
|
||||||
|
def test_wait_for_file_no_delete(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
with open(testfn, 'w') as f:
|
||||||
|
f.write('foo')
|
||||||
|
wait_for_file(testfn, delete=False)
|
||||||
|
assert os.path.exists(testfn)
|
||||||
|
|
||||||
|
def test_call_until(self):
|
||||||
|
ret = call_until(lambda: 1, "ret == 1")
|
||||||
|
self.assertEqual(ret, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFSTestUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_open_text(self):
|
||||||
|
with open_text(__file__) as f:
|
||||||
|
self.assertEqual(f.mode, 'rt')
|
||||||
|
|
||||||
|
def test_open_binary(self):
|
||||||
|
with open_binary(__file__) as f:
|
||||||
|
self.assertEqual(f.mode, 'rb')
|
||||||
|
|
||||||
|
def test_safe_mkdir(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
safe_mkdir(testfn)
|
||||||
|
assert os.path.isdir(testfn)
|
||||||
|
safe_mkdir(testfn)
|
||||||
|
assert os.path.isdir(testfn)
|
||||||
|
|
||||||
|
def test_safe_rmpath(self):
|
||||||
|
# test file is removed
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
open(testfn, 'w').close()
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
assert not os.path.exists(testfn)
|
||||||
|
# test no exception if path does not exist
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
# test dir is removed
|
||||||
|
os.mkdir(testfn)
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
assert not os.path.exists(testfn)
|
||||||
|
# test other exceptions are raised
|
||||||
|
with mock.patch('psutil.tests.os.stat',
|
||||||
|
side_effect=OSError(errno.EINVAL, "")) as m:
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_chdir(self):
|
||||||
|
testfn = self.get_testfn()
|
||||||
|
base = os.getcwd()
|
||||||
|
os.mkdir(testfn)
|
||||||
|
with chdir(testfn):
|
||||||
|
self.assertEqual(os.getcwd(), os.path.join(base, testfn))
|
||||||
|
self.assertEqual(os.getcwd(), base)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_reap_children(self):
|
||||||
|
subp = self.spawn_testproc()
|
||||||
|
p = psutil.Process(subp.pid)
|
||||||
|
assert p.is_running()
|
||||||
|
reap_children()
|
||||||
|
assert not p.is_running()
|
||||||
|
assert not psutil.tests._pids_started
|
||||||
|
assert not psutil.tests._subprocesses_started
|
||||||
|
|
||||||
|
def test_spawn_children_pair(self):
|
||||||
|
child, grandchild = self.spawn_children_pair()
|
||||||
|
self.assertNotEqual(child.pid, grandchild.pid)
|
||||||
|
assert child.is_running()
|
||||||
|
assert grandchild.is_running()
|
||||||
|
children = psutil.Process().children()
|
||||||
|
self.assertEqual(children, [child])
|
||||||
|
children = psutil.Process().children(recursive=True)
|
||||||
|
self.assertEqual(len(children), 2)
|
||||||
|
self.assertIn(child, children)
|
||||||
|
self.assertIn(grandchild, children)
|
||||||
|
self.assertEqual(child.ppid(), os.getpid())
|
||||||
|
self.assertEqual(grandchild.ppid(), child.pid)
|
||||||
|
|
||||||
|
terminate(child)
|
||||||
|
assert not child.is_running()
|
||||||
|
assert grandchild.is_running()
|
||||||
|
|
||||||
|
terminate(grandchild)
|
||||||
|
assert not grandchild.is_running()
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
def test_spawn_zombie(self):
|
||||||
|
parent, zombie = self.spawn_zombie()
|
||||||
|
self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE)
|
||||||
|
|
||||||
|
def test_terminate(self):
|
||||||
|
# by subprocess.Popen
|
||||||
|
p = self.spawn_testproc()
|
||||||
|
terminate(p)
|
||||||
|
self.assertProcessGone(p)
|
||||||
|
terminate(p)
|
||||||
|
# by psutil.Process
|
||||||
|
p = psutil.Process(self.spawn_testproc().pid)
|
||||||
|
terminate(p)
|
||||||
|
self.assertProcessGone(p)
|
||||||
|
terminate(p)
|
||||||
|
# by psutil.Popen
|
||||||
|
cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"]
|
||||||
|
p = psutil.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
terminate(p)
|
||||||
|
self.assertProcessGone(p)
|
||||||
|
terminate(p)
|
||||||
|
# by PID
|
||||||
|
pid = self.spawn_testproc().pid
|
||||||
|
terminate(pid)
|
||||||
|
self.assertProcessGone(p)
|
||||||
|
terminate(pid)
|
||||||
|
# zombie
|
||||||
|
if POSIX:
|
||||||
|
parent, zombie = self.spawn_zombie()
|
||||||
|
terminate(parent)
|
||||||
|
terminate(zombie)
|
||||||
|
self.assertProcessGone(parent)
|
||||||
|
self.assertProcessGone(zombie)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def bind_socket(self):
|
||||||
|
port = get_free_port()
|
||||||
|
with contextlib.closing(bind_socket(addr=('', port))) as s:
|
||||||
|
self.assertEqual(s.getsockname()[1], port)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
def test_bind_unix_socket(self):
|
||||||
|
name = self.get_testfn()
|
||||||
|
sock = bind_unix_socket(name)
|
||||||
|
with contextlib.closing(sock):
|
||||||
|
self.assertEqual(sock.family, socket.AF_UNIX)
|
||||||
|
self.assertEqual(sock.type, socket.SOCK_STREAM)
|
||||||
|
self.assertEqual(sock.getsockname(), name)
|
||||||
|
assert os.path.exists(name)
|
||||||
|
assert stat.S_ISSOCK(os.stat(name).st_mode)
|
||||||
|
# UDP
|
||||||
|
name = self.get_testfn()
|
||||||
|
sock = bind_unix_socket(name, type=socket.SOCK_DGRAM)
|
||||||
|
with contextlib.closing(sock):
|
||||||
|
self.assertEqual(sock.type, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
def tcp_tcp_socketpair(self):
|
||||||
|
addr = ("127.0.0.1", get_free_port())
|
||||||
|
server, client = tcp_socketpair(socket.AF_INET, addr=addr)
|
||||||
|
with contextlib.closing(server):
|
||||||
|
with contextlib.closing(client):
|
||||||
|
# Ensure they are connected and the positions are
|
||||||
|
# correct.
|
||||||
|
self.assertEqual(server.getsockname(), addr)
|
||||||
|
self.assertEqual(client.getpeername(), addr)
|
||||||
|
self.assertNotEqual(client.getsockname(), addr)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@unittest.skipIf(NETBSD or FREEBSD,
|
||||||
|
"/var/run/log UNIX socket opened by default")
|
||||||
|
def test_unix_socketpair(self):
|
||||||
|
p = psutil.Process()
|
||||||
|
num_fds = p.num_fds()
|
||||||
|
assert not p.connections(kind='unix')
|
||||||
|
name = self.get_testfn()
|
||||||
|
server, client = unix_socketpair(name)
|
||||||
|
try:
|
||||||
|
assert os.path.exists(name)
|
||||||
|
assert stat.S_ISSOCK(os.stat(name).st_mode)
|
||||||
|
self.assertEqual(p.num_fds() - num_fds, 2)
|
||||||
|
self.assertEqual(len(p.connections(kind='unix')), 2)
|
||||||
|
self.assertEqual(server.getsockname(), name)
|
||||||
|
self.assertEqual(client.getpeername(), name)
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
def test_create_sockets(self):
|
||||||
|
with create_sockets() as socks:
|
||||||
|
fams = collections.defaultdict(int)
|
||||||
|
types = collections.defaultdict(int)
|
||||||
|
for s in socks:
|
||||||
|
fams[s.family] += 1
|
||||||
|
# work around http://bugs.python.org/issue30204
|
||||||
|
types[s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)] += 1
|
||||||
|
self.assertGreaterEqual(fams[socket.AF_INET], 2)
|
||||||
|
if supports_ipv6():
|
||||||
|
self.assertGreaterEqual(fams[socket.AF_INET6], 2)
|
||||||
|
if POSIX and HAS_CONNECTIONS_UNIX:
|
||||||
|
self.assertGreaterEqual(fams[socket.AF_UNIX], 2)
|
||||||
|
self.assertGreaterEqual(types[socket.SOCK_STREAM], 2)
|
||||||
|
self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2)
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
class TestMemLeakClass(TestMemoryLeak):
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_times(self):
|
||||||
|
def fun():
|
||||||
|
cnt['cnt'] += 1
|
||||||
|
cnt = {'cnt': 0}
|
||||||
|
self.execute(fun, times=10, warmup_times=15)
|
||||||
|
self.assertEqual(cnt['cnt'], 26)
|
||||||
|
|
||||||
|
def test_param_err(self):
|
||||||
|
self.assertRaises(ValueError, self.execute, lambda: 0, times=0)
|
||||||
|
self.assertRaises(ValueError, self.execute, lambda: 0, times=-1)
|
||||||
|
self.assertRaises(ValueError, self.execute, lambda: 0, warmup_times=-1)
|
||||||
|
self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1)
|
||||||
|
self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
@unittest.skipIf(CI_TESTING, "skipped on CI")
|
||||||
|
def test_leak_mem(self):
|
||||||
|
ls = []
|
||||||
|
|
||||||
|
def fun(ls=ls):
|
||||||
|
ls.append("x" * 24 * 1024)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# will consume around 3M in total
|
||||||
|
self.assertRaisesRegex(AssertionError, "extra-mem",
|
||||||
|
self.execute, fun, times=50)
|
||||||
|
finally:
|
||||||
|
del ls
|
||||||
|
|
||||||
|
def test_unclosed_files(self):
|
||||||
|
def fun():
|
||||||
|
f = open(__file__)
|
||||||
|
self.addCleanup(f.close)
|
||||||
|
box.append(f)
|
||||||
|
|
||||||
|
box = []
|
||||||
|
kind = "fd" if POSIX else "handle"
|
||||||
|
self.assertRaisesRegex(AssertionError, "unclosed " + kind,
|
||||||
|
self.execute, fun)
|
||||||
|
|
||||||
|
def test_tolerance(self):
|
||||||
|
def fun():
|
||||||
|
ls.append("x" * 24 * 1024)
|
||||||
|
ls = []
|
||||||
|
times = 100
|
||||||
|
self.execute(fun, times=times, warmup_times=0,
|
||||||
|
tolerance=200 * 1024 * 1024)
|
||||||
|
self.assertEqual(len(ls), times + 1)
|
||||||
|
|
||||||
|
def test_execute_w_exc(self):
|
||||||
|
def fun():
|
||||||
|
1 / 0
|
||||||
|
self.execute_w_exc(ZeroDivisionError, fun)
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
self.execute_w_exc(OSError, fun)
|
||||||
|
|
||||||
|
def fun():
|
||||||
|
pass
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.execute_w_exc(ZeroDivisionError, fun)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTestingUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_process_namespace(self):
|
||||||
|
p = psutil.Process()
|
||||||
|
ns = process_namespace(p)
|
||||||
|
ns.test()
|
||||||
|
fun = [x for x in ns.iter(ns.getters) if x[1] == 'ppid'][0][0]
|
||||||
|
self.assertEqual(fun(), p.ppid())
|
||||||
|
|
||||||
|
def test_system_namespace(self):
|
||||||
|
ns = system_namespace()
|
||||||
|
fun = [x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs'][0][0]
|
||||||
|
self.assertEqual(fun(), psutil.net_if_addrs())
|
||||||
|
|
||||||
|
|
||||||
|
class TestOtherUtils(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_is_namedtuple(self):
|
||||||
|
assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3))
|
||||||
|
assert not is_namedtuple(tuple())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,355 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Notes about unicode handling in psutil
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Starting from version 5.3.0 psutil adds unicode support, see:
|
||||||
|
https://github.com/giampaolo/psutil/issues/1040
|
||||||
|
The notes below apply to *any* API returning a string such as
|
||||||
|
process exe(), cwd() or username():
|
||||||
|
|
||||||
|
* all strings are encoded by using the OS filesystem encoding
|
||||||
|
(sys.getfilesystemencoding()) which varies depending on the platform
|
||||||
|
(e.g. "UTF-8" on macOS, "mbcs" on Win)
|
||||||
|
* no API call is supposed to crash with UnicodeDecodeError
|
||||||
|
* instead, in case of badly encoded data returned by the OS, the
|
||||||
|
following error handlers are used to replace the corrupted characters in
|
||||||
|
the string:
|
||||||
|
* Python 3: sys.getfilesystemencodeerrors() (PY 3.6+) or
|
||||||
|
"surrogatescape" on POSIX and "replace" on Windows
|
||||||
|
* Python 2: "replace"
|
||||||
|
* on Python 2 all APIs return bytes (str type), never unicode
|
||||||
|
* on Python 2, you can go back to unicode by doing:
|
||||||
|
|
||||||
|
>>> unicode(p.exe(), sys.getdefaultencoding(), errors="replace")
|
||||||
|
|
||||||
|
For a detailed explanation of how psutil handles unicode see #1040.
|
||||||
|
|
||||||
|
Tests
|
||||||
|
=====
|
||||||
|
|
||||||
|
List of APIs returning or dealing with a string:
|
||||||
|
('not tested' means they are not tested to deal with non-ASCII strings):
|
||||||
|
|
||||||
|
* Process.cmdline()
|
||||||
|
* Process.connections('unix')
|
||||||
|
* Process.cwd()
|
||||||
|
* Process.environ()
|
||||||
|
* Process.exe()
|
||||||
|
* Process.memory_maps()
|
||||||
|
* Process.name()
|
||||||
|
* Process.open_files()
|
||||||
|
* Process.username() (not tested)
|
||||||
|
|
||||||
|
* disk_io_counters() (not tested)
|
||||||
|
* disk_partitions() (not tested)
|
||||||
|
* disk_usage(str)
|
||||||
|
* net_connections('unix')
|
||||||
|
* net_if_addrs() (not tested)
|
||||||
|
* net_if_stats() (not tested)
|
||||||
|
* net_io_counters() (not tested)
|
||||||
|
* sensors_fans() (not tested)
|
||||||
|
* sensors_temperatures() (not tested)
|
||||||
|
* users() (not tested)
|
||||||
|
|
||||||
|
* WindowsService.binpath() (not tested)
|
||||||
|
* WindowsService.description() (not tested)
|
||||||
|
* WindowsService.display_name() (not tested)
|
||||||
|
* WindowsService.name() (not tested)
|
||||||
|
* WindowsService.status() (not tested)
|
||||||
|
* WindowsService.username() (not tested)
|
||||||
|
|
||||||
|
In here we create a unicode path with a funky non-ASCII name and (where
|
||||||
|
possible) make psutil return it back (e.g. on name(), exe(), open_files(),
|
||||||
|
etc.) and make sure that:
|
||||||
|
|
||||||
|
* psutil never crashes with UnicodeDecodeError
|
||||||
|
* the returned path matches
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
|
import unittest
|
||||||
|
import warnings
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import BSD
|
||||||
|
from psutil import OPENBSD
|
||||||
|
from psutil import POSIX
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._compat import PY3
|
||||||
|
from psutil._compat import u
|
||||||
|
from psutil.tests import APPVEYOR
|
||||||
|
from psutil.tests import ASCII_FS
|
||||||
|
from psutil.tests import CI_TESTING
|
||||||
|
from psutil.tests import HAS_CONNECTIONS_UNIX
|
||||||
|
from psutil.tests import HAS_ENVIRON
|
||||||
|
from psutil.tests import HAS_MEMORY_MAPS
|
||||||
|
from psutil.tests import INVALID_UNICODE_SUFFIX
|
||||||
|
from psutil.tests import PYPY
|
||||||
|
from psutil.tests import TESTFN_PREFIX
|
||||||
|
from psutil.tests import UNICODE_SUFFIX
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import bind_unix_socket
|
||||||
|
from psutil.tests import chdir
|
||||||
|
from psutil.tests import copyload_shared_lib
|
||||||
|
from psutil.tests import create_exe
|
||||||
|
from psutil.tests import get_testfn
|
||||||
|
from psutil.tests import safe_mkdir
|
||||||
|
from psutil.tests import safe_rmpath
|
||||||
|
from psutil.tests import serialrun
|
||||||
|
from psutil.tests import skip_on_access_denied
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import terminate
|
||||||
|
|
||||||
|
|
||||||
|
if APPVEYOR:
|
||||||
|
def safe_rmpath(path): # NOQA
|
||||||
|
# TODO - this is quite random and I'm not sure why it happens,
|
||||||
|
# nor I can reproduce it locally:
|
||||||
|
# https://ci.appveyor.com/project/giampaolo/psutil/build/job/
|
||||||
|
# jiq2cgd6stsbtn60
|
||||||
|
# safe_rmpath() happens after reap_children() so this is weird
|
||||||
|
# Perhaps wait_procs() on Windows is broken? Maybe because
|
||||||
|
# of STILL_ACTIVE?
|
||||||
|
# https://github.com/giampaolo/psutil/blob/
|
||||||
|
# 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/
|
||||||
|
# windows/process_info.c#L146
|
||||||
|
from psutil.tests import safe_rmpath as rm
|
||||||
|
try:
|
||||||
|
return rm(path)
|
||||||
|
except WindowsError:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def try_unicode(suffix):
|
||||||
|
"""Return True if both the fs and the subprocess module can
|
||||||
|
deal with a unicode file name.
|
||||||
|
"""
|
||||||
|
sproc = None
|
||||||
|
testfn = get_testfn(suffix=suffix)
|
||||||
|
try:
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
create_exe(testfn)
|
||||||
|
sproc = spawn_testproc(cmd=[testfn])
|
||||||
|
shutil.copyfile(testfn, testfn + '-2')
|
||||||
|
safe_rmpath(testfn + '-2')
|
||||||
|
except (UnicodeEncodeError, IOError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
if sproc is not None:
|
||||||
|
terminate(sproc)
|
||||||
|
safe_rmpath(testfn)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# FS APIs
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUnicodeTest(PsutilTestCase):
|
||||||
|
funky_suffix = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if self.funky_suffix is not None:
|
||||||
|
if not try_unicode(self.funky_suffix):
|
||||||
|
raise self.skipTest("can't handle unicode str")
|
||||||
|
|
||||||
|
|
||||||
|
@serialrun
|
||||||
|
@unittest.skipIf(ASCII_FS, "ASCII fs")
|
||||||
|
@unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2")
|
||||||
|
class TestFSAPIs(BaseUnicodeTest):
|
||||||
|
"""Test FS APIs with a funky, valid, UTF8 path name."""
|
||||||
|
|
||||||
|
funky_suffix = UNICODE_SUFFIX
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.funky_name = get_testfn(suffix=cls.funky_suffix)
|
||||||
|
create_exe(cls.funky_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
safe_rmpath(cls.funky_name)
|
||||||
|
|
||||||
|
def expect_exact_path_match(self):
|
||||||
|
# Do not expect psutil to correctly handle unicode paths on
|
||||||
|
# Python 2 if os.listdir() is not able either.
|
||||||
|
here = '.' if isinstance(self.funky_name, str) else u('.')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
return self.funky_name in os.listdir(here)
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
def test_proc_exe(self):
|
||||||
|
subp = self.spawn_testproc(cmd=[self.funky_name])
|
||||||
|
p = psutil.Process(subp.pid)
|
||||||
|
exe = p.exe()
|
||||||
|
self.assertIsInstance(exe, str)
|
||||||
|
if self.expect_exact_path_match():
|
||||||
|
self.assertEqual(os.path.normcase(exe),
|
||||||
|
os.path.normcase(self.funky_name))
|
||||||
|
|
||||||
|
def test_proc_name(self):
|
||||||
|
subp = self.spawn_testproc(cmd=[self.funky_name])
|
||||||
|
name = psutil.Process(subp.pid).name()
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
if self.expect_exact_path_match():
|
||||||
|
self.assertEqual(name, os.path.basename(self.funky_name))
|
||||||
|
|
||||||
|
def test_proc_cmdline(self):
|
||||||
|
subp = self.spawn_testproc(cmd=[self.funky_name])
|
||||||
|
p = psutil.Process(subp.pid)
|
||||||
|
cmdline = p.cmdline()
|
||||||
|
for part in cmdline:
|
||||||
|
self.assertIsInstance(part, str)
|
||||||
|
if self.expect_exact_path_match():
|
||||||
|
self.assertEqual(cmdline, [self.funky_name])
|
||||||
|
|
||||||
|
def test_proc_cwd(self):
|
||||||
|
dname = self.funky_name + "2"
|
||||||
|
self.addCleanup(safe_rmpath, dname)
|
||||||
|
safe_mkdir(dname)
|
||||||
|
with chdir(dname):
|
||||||
|
p = psutil.Process()
|
||||||
|
cwd = p.cwd()
|
||||||
|
self.assertIsInstance(p.cwd(), str)
|
||||||
|
if self.expect_exact_path_match():
|
||||||
|
self.assertEqual(cwd, dname)
|
||||||
|
|
||||||
|
@unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS")
|
||||||
|
def test_proc_open_files(self):
|
||||||
|
p = psutil.Process()
|
||||||
|
start = set(p.open_files())
|
||||||
|
with open(self.funky_name, 'rb'):
|
||||||
|
new = set(p.open_files())
|
||||||
|
path = (new - start).pop().path
|
||||||
|
self.assertIsInstance(path, str)
|
||||||
|
if BSD and not path:
|
||||||
|
# XXX - see https://github.com/giampaolo/psutil/issues/595
|
||||||
|
return self.skipTest("open_files on BSD is broken")
|
||||||
|
if self.expect_exact_path_match():
|
||||||
|
self.assertEqual(os.path.normcase(path),
|
||||||
|
os.path.normcase(self.funky_name))
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
def test_proc_connections(self):
|
||||||
|
name = self.get_testfn(suffix=self.funky_suffix)
|
||||||
|
try:
|
||||||
|
sock = bind_unix_socket(name)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
if PY3:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise unittest.SkipTest("not supported")
|
||||||
|
with closing(sock):
|
||||||
|
conn = psutil.Process().connections('unix')[0]
|
||||||
|
self.assertIsInstance(conn.laddr, str)
|
||||||
|
# AF_UNIX addr not set on OpenBSD
|
||||||
|
if not OPENBSD: # XXX
|
||||||
|
self.assertEqual(conn.laddr, name)
|
||||||
|
|
||||||
|
@unittest.skipIf(not POSIX, "POSIX only")
|
||||||
|
@unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets")
|
||||||
|
@skip_on_access_denied()
|
||||||
|
def test_net_connections(self):
|
||||||
|
def find_sock(cons):
|
||||||
|
for conn in cons:
|
||||||
|
if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX):
|
||||||
|
return conn
|
||||||
|
raise ValueError("connection not found")
|
||||||
|
|
||||||
|
name = self.get_testfn(suffix=self.funky_suffix)
|
||||||
|
try:
|
||||||
|
sock = bind_unix_socket(name)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
if PY3:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise unittest.SkipTest("not supported")
|
||||||
|
with closing(sock):
|
||||||
|
cons = psutil.net_connections(kind='unix')
|
||||||
|
# AF_UNIX addr not set on OpenBSD
|
||||||
|
if not OPENBSD:
|
||||||
|
conn = find_sock(cons)
|
||||||
|
self.assertIsInstance(conn.laddr, str)
|
||||||
|
self.assertEqual(conn.laddr, name)
|
||||||
|
|
||||||
|
def test_disk_usage(self):
|
||||||
|
dname = self.funky_name + "2"
|
||||||
|
self.addCleanup(safe_rmpath, dname)
|
||||||
|
safe_mkdir(dname)
|
||||||
|
psutil.disk_usage(dname)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_MEMORY_MAPS, "not supported")
|
||||||
|
@unittest.skipIf(not PY3, "ctypes does not support unicode on PY2")
|
||||||
|
@unittest.skipIf(PYPY, "unstable on PYPY")
|
||||||
|
def test_memory_maps(self):
|
||||||
|
# XXX: on Python 2, using ctypes.CDLL with a unicode path
|
||||||
|
# opens a message box which blocks the test run.
|
||||||
|
with copyload_shared_lib(suffix=self.funky_suffix) as funky_path:
|
||||||
|
def normpath(p):
|
||||||
|
return os.path.realpath(os.path.normcase(p))
|
||||||
|
libpaths = [normpath(x.path)
|
||||||
|
for x in psutil.Process().memory_maps()]
|
||||||
|
# ...just to have a clearer msg in case of failure
|
||||||
|
libpaths = [x for x in libpaths if TESTFN_PREFIX in x]
|
||||||
|
self.assertIn(normpath(funky_path), libpaths)
|
||||||
|
for path in libpaths:
|
||||||
|
self.assertIsInstance(path, str)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(CI_TESTING, "unreliable on CI")
|
||||||
|
class TestFSAPIsWithInvalidPath(TestFSAPIs):
|
||||||
|
"""Test FS APIs with a funky, invalid path name."""
|
||||||
|
funky_suffix = INVALID_UNICODE_SUFFIX
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def expect_exact_path_match(cls):
|
||||||
|
# Invalid unicode names are supposed to work on Python 2.
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Non fs APIs
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestNonFSAPIS(BaseUnicodeTest):
|
||||||
|
"""Unicode tests for non fs-related APIs."""
|
||||||
|
funky_suffix = UNICODE_SUFFIX if PY3 else 'è'
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_ENVIRON, "not supported")
|
||||||
|
@unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS")
|
||||||
|
def test_proc_environ(self):
|
||||||
|
# Note: differently from others, this test does not deal
|
||||||
|
# with fs paths. On Python 2 subprocess module is broken as
|
||||||
|
# it's not able to handle with non-ASCII env vars, so
|
||||||
|
# we use "è", which is part of the extended ASCII table
|
||||||
|
# (unicode point <= 255).
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['FUNNY_ARG'] = self.funky_suffix
|
||||||
|
sproc = self.spawn_testproc(env=env)
|
||||||
|
p = psutil.Process(sproc.pid)
|
||||||
|
env = p.environ()
|
||||||
|
for k, v in env.items():
|
||||||
|
self.assertIsInstance(k, str)
|
||||||
|
self.assertIsInstance(v, str)
|
||||||
|
self.assertEqual(env['FUNNY_ARG'], self.funky_suffix)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1,838 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
"""Windows specific tests."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from psutil import WINDOWS
|
||||||
|
from psutil._compat import FileNotFoundError
|
||||||
|
from psutil._compat import super
|
||||||
|
from psutil.tests import APPVEYOR
|
||||||
|
from psutil.tests import GITHUB_ACTIONS
|
||||||
|
from psutil.tests import HAS_BATTERY
|
||||||
|
from psutil.tests import IS_64BIT
|
||||||
|
from psutil.tests import PY3
|
||||||
|
from psutil.tests import PYPY
|
||||||
|
from psutil.tests import TOLERANCE_DISK_USAGE
|
||||||
|
from psutil.tests import PsutilTestCase
|
||||||
|
from psutil.tests import mock
|
||||||
|
from psutil.tests import retry_on_failure
|
||||||
|
from psutil.tests import sh
|
||||||
|
from psutil.tests import spawn_testproc
|
||||||
|
from psutil.tests import terminate
|
||||||
|
|
||||||
|
|
||||||
|
if WINDOWS and not PYPY:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
import win32api # requires "pip install pywin32"
|
||||||
|
import win32con
|
||||||
|
import win32process
|
||||||
|
import wmi # requires "pip install wmi" / "make setup-dev-env"
|
||||||
|
|
||||||
|
if WINDOWS:
|
||||||
|
from psutil._pswindows import convert_oserror
|
||||||
|
|
||||||
|
|
||||||
|
cext = psutil._psplatform.cext
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
@unittest.skipIf(PYPY, "pywin32 not available on PYPY")
|
||||||
|
# https://github.com/giampaolo/psutil/pull/1762#issuecomment-632892692
|
||||||
|
@unittest.skipIf(GITHUB_ACTIONS and not PY3, "pywin32 broken on GITHUB + PY2")
|
||||||
|
class WindowsTestCase(PsutilTestCase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# System APIs
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCpuAPIs(WindowsTestCase):
|
||||||
|
|
||||||
|
@unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ,
|
||||||
|
'NUMBER_OF_PROCESSORS env var is not available')
|
||||||
|
def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self):
|
||||||
|
# Will likely fail on many-cores systems:
|
||||||
|
# https://stackoverflow.com/questions/31209256
|
||||||
|
num_cpus = int(os.environ['NUMBER_OF_PROCESSORS'])
|
||||||
|
self.assertEqual(num_cpus, psutil.cpu_count())
|
||||||
|
|
||||||
|
def test_cpu_count_vs_GetSystemInfo(self):
|
||||||
|
# Will likely fail on many-cores systems:
|
||||||
|
# https://stackoverflow.com/questions/31209256
|
||||||
|
sys_value = win32api.GetSystemInfo()[5]
|
||||||
|
psutil_value = psutil.cpu_count()
|
||||||
|
self.assertEqual(sys_value, psutil_value)
|
||||||
|
|
||||||
|
def test_cpu_count_logical_vs_wmi(self):
|
||||||
|
w = wmi.WMI()
|
||||||
|
procs = sum(proc.NumberOfLogicalProcessors
|
||||||
|
for proc in w.Win32_Processor())
|
||||||
|
self.assertEqual(psutil.cpu_count(), procs)
|
||||||
|
|
||||||
|
def test_cpu_count_cores_vs_wmi(self):
|
||||||
|
w = wmi.WMI()
|
||||||
|
cores = sum(proc.NumberOfCores for proc in w.Win32_Processor())
|
||||||
|
self.assertEqual(psutil.cpu_count(logical=False), cores)
|
||||||
|
|
||||||
|
def test_cpu_count_vs_cpu_times(self):
|
||||||
|
self.assertEqual(psutil.cpu_count(),
|
||||||
|
len(psutil.cpu_times(percpu=True)))
|
||||||
|
|
||||||
|
def test_cpu_freq(self):
|
||||||
|
w = wmi.WMI()
|
||||||
|
proc = w.Win32_Processor()[0]
|
||||||
|
self.assertEqual(proc.CurrentClockSpeed, psutil.cpu_freq().current)
|
||||||
|
self.assertEqual(proc.MaxClockSpeed, psutil.cpu_freq().max)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemAPIs(WindowsTestCase):
|
||||||
|
|
||||||
|
def test_nic_names(self):
|
||||||
|
out = sh('ipconfig /all')
|
||||||
|
nics = psutil.net_io_counters(pernic=True).keys()
|
||||||
|
for nic in nics:
|
||||||
|
if "pseudo-interface" in nic.replace(' ', '-').lower():
|
||||||
|
continue
|
||||||
|
if nic not in out:
|
||||||
|
raise self.fail(
|
||||||
|
"%r nic wasn't found in 'ipconfig /all' output" % nic)
|
||||||
|
|
||||||
|
def test_total_phymem(self):
|
||||||
|
w = wmi.WMI().Win32_ComputerSystem()[0]
|
||||||
|
self.assertEqual(int(w.TotalPhysicalMemory),
|
||||||
|
psutil.virtual_memory().total)
|
||||||
|
|
||||||
|
# @unittest.skipIf(wmi is None, "wmi module is not installed")
|
||||||
|
# def test__UPTIME(self):
|
||||||
|
# # _UPTIME constant is not public but it is used internally
|
||||||
|
# # as value to return for pid 0 creation time.
|
||||||
|
# # WMI behaves the same.
|
||||||
|
# w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
# p = psutil.Process(0)
|
||||||
|
# wmic_create = str(w.CreationDate.split('.')[0])
|
||||||
|
# psutil_create = time.strftime("%Y%m%d%H%M%S",
|
||||||
|
# time.localtime(p.create_time()))
|
||||||
|
|
||||||
|
# Note: this test is not very reliable
|
||||||
|
@unittest.skipIf(APPVEYOR, "test not relieable on appveyor")
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_pids(self):
|
||||||
|
# Note: this test might fail if the OS is starting/killing
|
||||||
|
# other processes in the meantime
|
||||||
|
w = wmi.WMI().Win32_Process()
|
||||||
|
wmi_pids = set([x.ProcessId for x in w])
|
||||||
|
psutil_pids = set(psutil.pids())
|
||||||
|
self.assertEqual(wmi_pids, psutil_pids)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_disks(self):
|
||||||
|
ps_parts = psutil.disk_partitions(all=True)
|
||||||
|
wmi_parts = wmi.WMI().Win32_LogicalDisk()
|
||||||
|
for ps_part in ps_parts:
|
||||||
|
for wmi_part in wmi_parts:
|
||||||
|
if ps_part.device.replace('\\', '') == wmi_part.DeviceID:
|
||||||
|
if not ps_part.mountpoint:
|
||||||
|
# this is usually a CD-ROM with no disk inserted
|
||||||
|
break
|
||||||
|
if 'cdrom' in ps_part.opts:
|
||||||
|
break
|
||||||
|
if ps_part.mountpoint.startswith('A:'):
|
||||||
|
break # floppy
|
||||||
|
try:
|
||||||
|
usage = psutil.disk_usage(ps_part.mountpoint)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# usually this is the floppy
|
||||||
|
break
|
||||||
|
self.assertEqual(usage.total, int(wmi_part.Size))
|
||||||
|
wmi_free = int(wmi_part.FreeSpace)
|
||||||
|
self.assertEqual(usage.free, wmi_free)
|
||||||
|
# 10 MB tollerance
|
||||||
|
if abs(usage.free - wmi_free) > 10 * 1024 * 1024:
|
||||||
|
raise self.fail("psutil=%s, wmi=%s" % (
|
||||||
|
usage.free, wmi_free))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise self.fail("can't find partition %s" % repr(ps_part))
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_disk_usage(self):
|
||||||
|
for disk in psutil.disk_partitions():
|
||||||
|
if 'cdrom' in disk.opts:
|
||||||
|
continue
|
||||||
|
sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint)
|
||||||
|
psutil_value = psutil.disk_usage(disk.mountpoint)
|
||||||
|
self.assertAlmostEqual(sys_value[0], psutil_value.free,
|
||||||
|
delta=TOLERANCE_DISK_USAGE)
|
||||||
|
self.assertAlmostEqual(sys_value[1], psutil_value.total,
|
||||||
|
delta=TOLERANCE_DISK_USAGE)
|
||||||
|
self.assertEqual(psutil_value.used,
|
||||||
|
psutil_value.total - psutil_value.free)
|
||||||
|
|
||||||
|
def test_disk_partitions(self):
|
||||||
|
sys_value = [
|
||||||
|
x + '\\' for x in win32api.GetLogicalDriveStrings().split("\\\x00")
|
||||||
|
if x and not x.startswith('A:')]
|
||||||
|
psutil_value = [x.mountpoint for x in psutil.disk_partitions(all=True)
|
||||||
|
if not x.mountpoint.startswith('A:')]
|
||||||
|
self.assertEqual(sys_value, psutil_value)
|
||||||
|
|
||||||
|
def test_net_if_stats(self):
|
||||||
|
ps_names = set(cext.net_if_stats())
|
||||||
|
wmi_adapters = wmi.WMI().Win32_NetworkAdapter()
|
||||||
|
wmi_names = set()
|
||||||
|
for wmi_adapter in wmi_adapters:
|
||||||
|
wmi_names.add(wmi_adapter.Name)
|
||||||
|
wmi_names.add(wmi_adapter.NetConnectionID)
|
||||||
|
self.assertTrue(ps_names & wmi_names,
|
||||||
|
"no common entries in %s, %s" % (ps_names, wmi_names))
|
||||||
|
|
||||||
|
def test_boot_time(self):
|
||||||
|
wmi_os = wmi.WMI().Win32_OperatingSystem()
|
||||||
|
wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0]
|
||||||
|
wmi_btime_dt = datetime.datetime.strptime(
|
||||||
|
wmi_btime_str, "%Y%m%d%H%M%S")
|
||||||
|
psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time())
|
||||||
|
diff = abs((wmi_btime_dt - psutil_dt).total_seconds())
|
||||||
|
self.assertLessEqual(diff, 5)
|
||||||
|
|
||||||
|
def test_boot_time_fluctuation(self):
|
||||||
|
# https://github.com/giampaolo/psutil/issues/1007
|
||||||
|
with mock.patch('psutil._pswindows.cext.boot_time', return_value=5):
|
||||||
|
self.assertEqual(psutil.boot_time(), 5)
|
||||||
|
with mock.patch('psutil._pswindows.cext.boot_time', return_value=4):
|
||||||
|
self.assertEqual(psutil.boot_time(), 5)
|
||||||
|
with mock.patch('psutil._pswindows.cext.boot_time', return_value=6):
|
||||||
|
self.assertEqual(psutil.boot_time(), 5)
|
||||||
|
with mock.patch('psutil._pswindows.cext.boot_time', return_value=333):
|
||||||
|
self.assertEqual(psutil.boot_time(), 333)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# sensors_battery()
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestSensorsBattery(WindowsTestCase):
|
||||||
|
|
||||||
|
def test_has_battery(self):
|
||||||
|
if win32api.GetPwrCapabilities()['SystemBatteriesPresent']:
|
||||||
|
self.assertIsNotNone(psutil.sensors_battery())
|
||||||
|
else:
|
||||||
|
self.assertIsNone(psutil.sensors_battery())
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_percent(self):
|
||||||
|
w = wmi.WMI()
|
||||||
|
battery_wmi = w.query('select * from Win32_Battery')[0]
|
||||||
|
battery_psutil = psutil.sensors_battery()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
battery_psutil.percent, battery_wmi.EstimatedChargeRemaining,
|
||||||
|
delta=1)
|
||||||
|
|
||||||
|
@unittest.skipIf(not HAS_BATTERY, "no battery")
|
||||||
|
def test_power_plugged(self):
|
||||||
|
w = wmi.WMI()
|
||||||
|
battery_wmi = w.query('select * from Win32_Battery')[0]
|
||||||
|
battery_psutil = psutil.sensors_battery()
|
||||||
|
# Status codes:
|
||||||
|
# https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx
|
||||||
|
self.assertEqual(battery_psutil.power_plugged,
|
||||||
|
battery_wmi.BatteryStatus == 2)
|
||||||
|
|
||||||
|
def test_emulate_no_battery(self):
|
||||||
|
with mock.patch("psutil._pswindows.cext.sensors_battery",
|
||||||
|
return_value=(0, 128, 0, 0)) as m:
|
||||||
|
self.assertIsNone(psutil.sensors_battery())
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_emulate_power_connected(self):
|
||||||
|
with mock.patch("psutil._pswindows.cext.sensors_battery",
|
||||||
|
return_value=(1, 0, 0, 0)) as m:
|
||||||
|
self.assertEqual(psutil.sensors_battery().secsleft,
|
||||||
|
psutil.POWER_TIME_UNLIMITED)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_emulate_power_charging(self):
|
||||||
|
with mock.patch("psutil._pswindows.cext.sensors_battery",
|
||||||
|
return_value=(0, 8, 0, 0)) as m:
|
||||||
|
self.assertEqual(psutil.sensors_battery().secsleft,
|
||||||
|
psutil.POWER_TIME_UNLIMITED)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
def test_emulate_secs_left_unknown(self):
|
||||||
|
with mock.patch("psutil._pswindows.cext.sensors_battery",
|
||||||
|
return_value=(0, 0, 0, -1)) as m:
|
||||||
|
self.assertEqual(psutil.sensors_battery().secsleft,
|
||||||
|
psutil.POWER_TIME_UNKNOWN)
|
||||||
|
assert m.called
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Process APIs
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcess(WindowsTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
def test_issue_24(self):
|
||||||
|
p = psutil.Process(0)
|
||||||
|
self.assertRaises(psutil.AccessDenied, p.kill)
|
||||||
|
|
||||||
|
def test_special_pid(self):
|
||||||
|
p = psutil.Process(4)
|
||||||
|
self.assertEqual(p.name(), 'System')
|
||||||
|
# use __str__ to access all common Process properties to check
|
||||||
|
# that nothing strange happens
|
||||||
|
str(p)
|
||||||
|
p.username()
|
||||||
|
self.assertTrue(p.create_time() >= 0.0)
|
||||||
|
try:
|
||||||
|
rss, vms = p.memory_info()[:2]
|
||||||
|
except psutil.AccessDenied:
|
||||||
|
# expected on Windows Vista and Windows 7
|
||||||
|
if not platform.uname()[1] in ('vista', 'win-7', 'win7'):
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.assertTrue(rss > 0)
|
||||||
|
|
||||||
|
def test_send_signal(self):
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
self.assertRaises(ValueError, p.send_signal, signal.SIGINT)
|
||||||
|
|
||||||
|
def test_num_handles_increment(self):
|
||||||
|
p = psutil.Process(os.getpid())
|
||||||
|
before = p.num_handles()
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, os.getpid())
|
||||||
|
after = p.num_handles()
|
||||||
|
self.assertEqual(after, before + 1)
|
||||||
|
win32api.CloseHandle(handle)
|
||||||
|
self.assertEqual(p.num_handles(), before)
|
||||||
|
|
||||||
|
def test_ctrl_signals(self):
|
||||||
|
p = psutil.Process(self.spawn_testproc().pid)
|
||||||
|
p.send_signal(signal.CTRL_C_EVENT)
|
||||||
|
p.send_signal(signal.CTRL_BREAK_EVENT)
|
||||||
|
p.kill()
|
||||||
|
p.wait()
|
||||||
|
self.assertRaises(psutil.NoSuchProcess,
|
||||||
|
p.send_signal, signal.CTRL_C_EVENT)
|
||||||
|
self.assertRaises(psutil.NoSuchProcess,
|
||||||
|
p.send_signal, signal.CTRL_BREAK_EVENT)
|
||||||
|
|
||||||
|
def test_username(self):
|
||||||
|
name = win32api.GetUserNameEx(win32con.NameSamCompatible)
|
||||||
|
if name.endswith('$'):
|
||||||
|
# When running as a service account (most likely to be
|
||||||
|
# NetworkService), these user name calculations don't produce the
|
||||||
|
# same result, causing the test to fail.
|
||||||
|
raise unittest.SkipTest('running as service account')
|
||||||
|
self.assertEqual(psutil.Process().username(), name)
|
||||||
|
|
||||||
|
def test_cmdline(self):
|
||||||
|
sys_value = re.sub('[ ]+', ' ', win32api.GetCommandLine()).strip()
|
||||||
|
psutil_value = ' '.join(psutil.Process().cmdline())
|
||||||
|
if sys_value[0] == '"' != psutil_value[0]:
|
||||||
|
# The PyWin32 command line may retain quotes around argv[0] if they
|
||||||
|
# were used unnecessarily, while psutil will omit them. So remove
|
||||||
|
# the first 2 quotes from sys_value if not in psutil_value.
|
||||||
|
# A path to an executable will not contain quotes, so this is safe.
|
||||||
|
sys_value = sys_value.replace('"', '', 2)
|
||||||
|
self.assertEqual(sys_value, psutil_value)
|
||||||
|
|
||||||
|
# XXX - occasional failures
|
||||||
|
|
||||||
|
# def test_cpu_times(self):
|
||||||
|
# handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
# win32con.FALSE, os.getpid())
|
||||||
|
# self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
# sys_value = win32process.GetProcessTimes(handle)
|
||||||
|
# psutil_value = psutil.Process().cpu_times()
|
||||||
|
# self.assertAlmostEqual(
|
||||||
|
# psutil_value.user, sys_value['UserTime'] / 10000000.0,
|
||||||
|
# delta=0.2)
|
||||||
|
# self.assertAlmostEqual(
|
||||||
|
# psutil_value.user, sys_value['KernelTime'] / 10000000.0,
|
||||||
|
# delta=0.2)
|
||||||
|
|
||||||
|
def test_nice(self):
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, os.getpid())
|
||||||
|
self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
sys_value = win32process.GetPriorityClass(handle)
|
||||||
|
psutil_value = psutil.Process().nice()
|
||||||
|
self.assertEqual(psutil_value, sys_value)
|
||||||
|
|
||||||
|
def test_memory_info(self):
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, self.pid)
|
||||||
|
self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
sys_value = win32process.GetProcessMemoryInfo(handle)
|
||||||
|
psutil_value = psutil.Process(self.pid).memory_info()
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['PeakWorkingSetSize'], psutil_value.peak_wset)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['WorkingSetSize'], psutil_value.wset)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['QuotaPagedPoolUsage'], psutil_value.paged_pool)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['QuotaPeakNonPagedPoolUsage'],
|
||||||
|
psutil_value.peak_nonpaged_pool)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['QuotaNonPagedPoolUsage'], psutil_value.nonpaged_pool)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['PagefileUsage'], psutil_value.pagefile)
|
||||||
|
self.assertEqual(
|
||||||
|
sys_value['PeakPagefileUsage'], psutil_value.peak_pagefile)
|
||||||
|
|
||||||
|
self.assertEqual(psutil_value.rss, psutil_value.wset)
|
||||||
|
self.assertEqual(psutil_value.vms, psutil_value.pagefile)
|
||||||
|
|
||||||
|
def test_wait(self):
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, self.pid)
|
||||||
|
self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
p.terminate()
|
||||||
|
psutil_value = p.wait()
|
||||||
|
sys_value = win32process.GetExitCodeProcess(handle)
|
||||||
|
self.assertEqual(psutil_value, sys_value)
|
||||||
|
|
||||||
|
def test_cpu_affinity(self):
|
||||||
|
def from_bitmask(x):
|
||||||
|
return [i for i in range(64) if (1 << i) & x]
|
||||||
|
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, self.pid)
|
||||||
|
self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
sys_value = from_bitmask(
|
||||||
|
win32process.GetProcessAffinityMask(handle)[0])
|
||||||
|
psutil_value = psutil.Process(self.pid).cpu_affinity()
|
||||||
|
self.assertEqual(psutil_value, sys_value)
|
||||||
|
|
||||||
|
def test_io_counters(self):
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
|
||||||
|
win32con.FALSE, os.getpid())
|
||||||
|
self.addCleanup(win32api.CloseHandle, handle)
|
||||||
|
sys_value = win32process.GetProcessIoCounters(handle)
|
||||||
|
psutil_value = psutil.Process().io_counters()
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.read_count, sys_value['ReadOperationCount'])
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.write_count, sys_value['WriteOperationCount'])
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.read_bytes, sys_value['ReadTransferCount'])
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.write_bytes, sys_value['WriteTransferCount'])
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.other_count, sys_value['OtherOperationCount'])
|
||||||
|
self.assertEqual(
|
||||||
|
psutil_value.other_bytes, sys_value['OtherTransferCount'])
|
||||||
|
|
||||||
|
def test_num_handles(self):
|
||||||
|
import ctypes
|
||||||
|
import ctypes.wintypes
|
||||||
|
PROCESS_QUERY_INFORMATION = 0x400
|
||||||
|
handle = ctypes.windll.kernel32.OpenProcess(
|
||||||
|
PROCESS_QUERY_INFORMATION, 0, self.pid)
|
||||||
|
self.addCleanup(ctypes.windll.kernel32.CloseHandle, handle)
|
||||||
|
|
||||||
|
hndcnt = ctypes.wintypes.DWORD()
|
||||||
|
ctypes.windll.kernel32.GetProcessHandleCount(
|
||||||
|
handle, ctypes.byref(hndcnt))
|
||||||
|
sys_value = hndcnt.value
|
||||||
|
psutil_value = psutil.Process(self.pid).num_handles()
|
||||||
|
self.assertEqual(psutil_value, sys_value)
|
||||||
|
|
||||||
|
def test_error_partial_copy(self):
|
||||||
|
# https://github.com/giampaolo/psutil/issues/875
|
||||||
|
exc = WindowsError()
|
||||||
|
exc.winerror = 299
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_cwd", side_effect=exc):
|
||||||
|
with mock.patch("time.sleep") as m:
|
||||||
|
p = psutil.Process()
|
||||||
|
self.assertRaises(psutil.AccessDenied, p.cwd)
|
||||||
|
self.assertGreaterEqual(m.call_count, 5)
|
||||||
|
|
||||||
|
def test_exe(self):
|
||||||
|
# NtQuerySystemInformation succeeds if process is gone. Make sure
|
||||||
|
# it raises NSP for a non existent pid.
|
||||||
|
pid = psutil.pids()[-1] + 99999
|
||||||
|
proc = psutil._psplatform.Process(pid)
|
||||||
|
self.assertRaises(psutil.NoSuchProcess, proc.exe)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessWMI(WindowsTestCase):
|
||||||
|
"""Compare Process API results with WMI."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
self.assertEqual(p.name(), w.Caption)
|
||||||
|
|
||||||
|
# This fail on github because using virtualenv for test environment
|
||||||
|
@unittest.skipIf(GITHUB_ACTIONS, "unreliable path on GITHUB_ACTIONS")
|
||||||
|
def test_exe(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
# Note: wmi reports the exe as a lower case string.
|
||||||
|
# Being Windows paths case-insensitive we ignore that.
|
||||||
|
self.assertEqual(p.exe().lower(), w.ExecutablePath.lower())
|
||||||
|
|
||||||
|
def test_cmdline(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
self.assertEqual(' '.join(p.cmdline()),
|
||||||
|
w.CommandLine.replace('"', ''))
|
||||||
|
|
||||||
|
def test_username(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
domain, _, username = w.GetOwner()
|
||||||
|
username = "%s\\%s" % (domain, username)
|
||||||
|
self.assertEqual(p.username(), username)
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_memory_rss(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
rss = p.memory_info().rss
|
||||||
|
self.assertEqual(rss, int(w.WorkingSetSize))
|
||||||
|
|
||||||
|
@retry_on_failure()
|
||||||
|
def test_memory_vms(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
vms = p.memory_info().vms
|
||||||
|
# http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx
|
||||||
|
# ...claims that PageFileUsage is represented in Kilo
|
||||||
|
# bytes but funnily enough on certain platforms bytes are
|
||||||
|
# returned instead.
|
||||||
|
wmi_usage = int(w.PageFileUsage)
|
||||||
|
if (vms != wmi_usage) and (vms != wmi_usage * 1024):
|
||||||
|
raise self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms))
|
||||||
|
|
||||||
|
def test_create_time(self):
|
||||||
|
w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0]
|
||||||
|
p = psutil.Process(self.pid)
|
||||||
|
wmic_create = str(w.CreationDate.split('.')[0])
|
||||||
|
psutil_create = time.strftime("%Y%m%d%H%M%S",
|
||||||
|
time.localtime(p.create_time()))
|
||||||
|
self.assertEqual(wmic_create, psutil_create)
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
class TestDualProcessImplementation(PsutilTestCase):
|
||||||
|
"""
|
||||||
|
Certain APIs on Windows have 2 internal implementations, one
|
||||||
|
based on documented Windows APIs, another one based
|
||||||
|
NtQuerySystemInformation() which gets called as fallback in
|
||||||
|
case the first fails because of limited permission error.
|
||||||
|
Here we test that the two methods return the exact same value,
|
||||||
|
see:
|
||||||
|
https://github.com/giampaolo/psutil/issues/304
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.pid = spawn_testproc().pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
terminate(cls.pid)
|
||||||
|
|
||||||
|
def test_memory_info(self):
|
||||||
|
mem_1 = psutil.Process(self.pid).memory_info()
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_memory_info",
|
||||||
|
side_effect=OSError(errno.EPERM, "msg")) as fun:
|
||||||
|
mem_2 = psutil.Process(self.pid).memory_info()
|
||||||
|
self.assertEqual(len(mem_1), len(mem_2))
|
||||||
|
for i in range(len(mem_1)):
|
||||||
|
self.assertGreaterEqual(mem_1[i], 0)
|
||||||
|
self.assertGreaterEqual(mem_2[i], 0)
|
||||||
|
self.assertAlmostEqual(mem_1[i], mem_2[i], delta=512)
|
||||||
|
assert fun.called
|
||||||
|
|
||||||
|
def test_create_time(self):
|
||||||
|
ctime = psutil.Process(self.pid).create_time()
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_times",
|
||||||
|
side_effect=OSError(errno.EPERM, "msg")) as fun:
|
||||||
|
self.assertEqual(psutil.Process(self.pid).create_time(), ctime)
|
||||||
|
assert fun.called
|
||||||
|
|
||||||
|
def test_cpu_times(self):
|
||||||
|
cpu_times_1 = psutil.Process(self.pid).cpu_times()
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_times",
|
||||||
|
side_effect=OSError(errno.EPERM, "msg")) as fun:
|
||||||
|
cpu_times_2 = psutil.Process(self.pid).cpu_times()
|
||||||
|
assert fun.called
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
cpu_times_1.user, cpu_times_2.user, delta=0.01)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
cpu_times_1.system, cpu_times_2.system, delta=0.01)
|
||||||
|
|
||||||
|
def test_io_counters(self):
|
||||||
|
io_counters_1 = psutil.Process(self.pid).io_counters()
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_io_counters",
|
||||||
|
side_effect=OSError(errno.EPERM, "msg")) as fun:
|
||||||
|
io_counters_2 = psutil.Process(self.pid).io_counters()
|
||||||
|
for i in range(len(io_counters_1)):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
io_counters_1[i], io_counters_2[i], delta=5)
|
||||||
|
assert fun.called
|
||||||
|
|
||||||
|
def test_num_handles(self):
|
||||||
|
num_handles = psutil.Process(self.pid).num_handles()
|
||||||
|
with mock.patch("psutil._psplatform.cext.proc_num_handles",
|
||||||
|
side_effect=OSError(errno.EPERM, "msg")) as fun:
|
||||||
|
self.assertEqual(psutil.Process(self.pid).num_handles(),
|
||||||
|
num_handles)
|
||||||
|
assert fun.called
|
||||||
|
|
||||||
|
def test_cmdline(self):
|
||||||
|
for pid in psutil.pids():
|
||||||
|
try:
|
||||||
|
a = cext.proc_cmdline(pid, use_peb=True)
|
||||||
|
b = cext.proc_cmdline(pid, use_peb=False)
|
||||||
|
except OSError as err:
|
||||||
|
err = convert_oserror(err)
|
||||||
|
if not isinstance(err, (psutil.AccessDenied,
|
||||||
|
psutil.NoSuchProcess)):
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
class RemoteProcessTestCase(PsutilTestCase):
|
||||||
|
"""Certain functions require calling ReadProcessMemory.
|
||||||
|
This trivially works when called on the current process.
|
||||||
|
Check that this works on other processes, especially when they
|
||||||
|
have a different bitness.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_other_interpreter():
|
||||||
|
# find a python interpreter that is of the opposite bitness from us
|
||||||
|
code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))"
|
||||||
|
|
||||||
|
# XXX: a different and probably more stable approach might be to access
|
||||||
|
# the registry but accessing 64 bit paths from a 32 bit process
|
||||||
|
for filename in glob.glob(r"C:\Python*\python.exe"):
|
||||||
|
proc = subprocess.Popen(args=[filename, "-c", code],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
output, _ = proc.communicate()
|
||||||
|
proc.wait()
|
||||||
|
if output == str(not IS_64BIT):
|
||||||
|
return filename
|
||||||
|
|
||||||
|
test_args = ["-c", "import sys; sys.stdin.read()"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
other_python = self.find_other_interpreter()
|
||||||
|
if other_python is None:
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
"could not find interpreter with opposite bitness")
|
||||||
|
if IS_64BIT:
|
||||||
|
self.python64 = sys.executable
|
||||||
|
self.python32 = other_python
|
||||||
|
else:
|
||||||
|
self.python64 = other_python
|
||||||
|
self.python32 = sys.executable
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["THINK_OF_A_NUMBER"] = str(os.getpid())
|
||||||
|
self.proc32 = self.spawn_testproc(
|
||||||
|
[self.python32] + self.test_args,
|
||||||
|
env=env,
|
||||||
|
stdin=subprocess.PIPE)
|
||||||
|
self.proc64 = self.spawn_testproc(
|
||||||
|
[self.python64] + self.test_args,
|
||||||
|
env=env,
|
||||||
|
stdin=subprocess.PIPE)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
self.proc32.communicate()
|
||||||
|
self.proc64.communicate()
|
||||||
|
|
||||||
|
def test_cmdline_32(self):
|
||||||
|
p = psutil.Process(self.proc32.pid)
|
||||||
|
self.assertEqual(len(p.cmdline()), 3)
|
||||||
|
self.assertEqual(p.cmdline()[1:], self.test_args)
|
||||||
|
|
||||||
|
def test_cmdline_64(self):
|
||||||
|
p = psutil.Process(self.proc64.pid)
|
||||||
|
self.assertEqual(len(p.cmdline()), 3)
|
||||||
|
self.assertEqual(p.cmdline()[1:], self.test_args)
|
||||||
|
|
||||||
|
def test_cwd_32(self):
|
||||||
|
p = psutil.Process(self.proc32.pid)
|
||||||
|
self.assertEqual(p.cwd(), os.getcwd())
|
||||||
|
|
||||||
|
def test_cwd_64(self):
|
||||||
|
p = psutil.Process(self.proc64.pid)
|
||||||
|
self.assertEqual(p.cwd(), os.getcwd())
|
||||||
|
|
||||||
|
def test_environ_32(self):
|
||||||
|
p = psutil.Process(self.proc32.pid)
|
||||||
|
e = p.environ()
|
||||||
|
self.assertIn("THINK_OF_A_NUMBER", e)
|
||||||
|
self.assertEqual(e["THINK_OF_A_NUMBER"], str(os.getpid()))
|
||||||
|
|
||||||
|
def test_environ_64(self):
|
||||||
|
p = psutil.Process(self.proc64.pid)
|
||||||
|
try:
|
||||||
|
p.environ()
|
||||||
|
except psutil.AccessDenied:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Windows services
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(not WINDOWS, "WINDOWS only")
|
||||||
|
class TestServices(PsutilTestCase):
|
||||||
|
|
||||||
|
def test_win_service_iter(self):
|
||||||
|
valid_statuses = set([
|
||||||
|
"running",
|
||||||
|
"paused",
|
||||||
|
"start",
|
||||||
|
"pause",
|
||||||
|
"continue",
|
||||||
|
"stop",
|
||||||
|
"stopped",
|
||||||
|
])
|
||||||
|
valid_start_types = set([
|
||||||
|
"automatic",
|
||||||
|
"manual",
|
||||||
|
"disabled",
|
||||||
|
])
|
||||||
|
valid_statuses = set([
|
||||||
|
"running",
|
||||||
|
"paused",
|
||||||
|
"start_pending",
|
||||||
|
"pause_pending",
|
||||||
|
"continue_pending",
|
||||||
|
"stop_pending",
|
||||||
|
"stopped"
|
||||||
|
])
|
||||||
|
for serv in psutil.win_service_iter():
|
||||||
|
data = serv.as_dict()
|
||||||
|
self.assertIsInstance(data['name'], str)
|
||||||
|
self.assertNotEqual(data['name'].strip(), "")
|
||||||
|
self.assertIsInstance(data['display_name'], str)
|
||||||
|
self.assertIsInstance(data['username'], str)
|
||||||
|
self.assertIn(data['status'], valid_statuses)
|
||||||
|
if data['pid'] is not None:
|
||||||
|
psutil.Process(data['pid'])
|
||||||
|
self.assertIsInstance(data['binpath'], str)
|
||||||
|
self.assertIsInstance(data['username'], str)
|
||||||
|
self.assertIsInstance(data['start_type'], str)
|
||||||
|
self.assertIn(data['start_type'], valid_start_types)
|
||||||
|
self.assertIn(data['status'], valid_statuses)
|
||||||
|
self.assertIsInstance(data['description'], str)
|
||||||
|
pid = serv.pid()
|
||||||
|
if pid is not None:
|
||||||
|
p = psutil.Process(pid)
|
||||||
|
self.assertTrue(p.is_running())
|
||||||
|
# win_service_get
|
||||||
|
s = psutil.win_service_get(serv.name())
|
||||||
|
# test __eq__
|
||||||
|
self.assertEqual(serv, s)
|
||||||
|
|
||||||
|
def test_win_service_get(self):
|
||||||
|
ERROR_SERVICE_DOES_NOT_EXIST = \
|
||||||
|
psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST
|
||||||
|
ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED
|
||||||
|
|
||||||
|
name = next(psutil.win_service_iter()).name()
|
||||||
|
with self.assertRaises(psutil.NoSuchProcess) as cm:
|
||||||
|
psutil.win_service_get(name + '???')
|
||||||
|
self.assertEqual(cm.exception.name, name + '???')
|
||||||
|
|
||||||
|
# test NoSuchProcess
|
||||||
|
service = psutil.win_service_get(name)
|
||||||
|
if PY3:
|
||||||
|
args = (0, "msg", 0, ERROR_SERVICE_DOES_NOT_EXIST)
|
||||||
|
else:
|
||||||
|
args = (ERROR_SERVICE_DOES_NOT_EXIST, "msg")
|
||||||
|
exc = WindowsError(*args)
|
||||||
|
with mock.patch("psutil._psplatform.cext.winservice_query_status",
|
||||||
|
side_effect=exc):
|
||||||
|
self.assertRaises(psutil.NoSuchProcess, service.status)
|
||||||
|
with mock.patch("psutil._psplatform.cext.winservice_query_config",
|
||||||
|
side_effect=exc):
|
||||||
|
self.assertRaises(psutil.NoSuchProcess, service.username)
|
||||||
|
|
||||||
|
# test AccessDenied
|
||||||
|
if PY3:
|
||||||
|
args = (0, "msg", 0, ERROR_ACCESS_DENIED)
|
||||||
|
else:
|
||||||
|
args = (ERROR_ACCESS_DENIED, "msg")
|
||||||
|
exc = WindowsError(*args)
|
||||||
|
with mock.patch("psutil._psplatform.cext.winservice_query_status",
|
||||||
|
side_effect=exc):
|
||||||
|
self.assertRaises(psutil.AccessDenied, service.status)
|
||||||
|
with mock.patch("psutil._psplatform.cext.winservice_query_config",
|
||||||
|
side_effect=exc):
|
||||||
|
self.assertRaises(psutil.AccessDenied, service.username)
|
||||||
|
|
||||||
|
# test __str__ and __repr__
|
||||||
|
self.assertIn(service.name(), str(service))
|
||||||
|
self.assertIn(service.display_name(), str(service))
|
||||||
|
self.assertIn(service.name(), repr(service))
|
||||||
|
self.assertIn(service.display_name(), repr(service))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from psutil.tests.runner import run_from_name
|
||||||
|
run_from_name(__file__)
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,201 @@
|
|||||||
|
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,120 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: pyclip
|
||||||
|
Version: 0.6.0
|
||||||
|
Summary: Cross-platform clipboard utilities supporting both binary and text data.
|
||||||
|
Home-page: https://github.com/spyoungtech/pyclip
|
||||||
|
Author: Spencer Young
|
||||||
|
Author-email: spencer.young@spyoung.com
|
||||||
|
License: Apache
|
||||||
|
Keywords: pyperclip clipboard cross-platform binary bytes files
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: pasteboard (==0.3.3) ; platform_system == "Darwin"
|
||||||
|
Requires-Dist: pywin32 (>=1.0) ; platform_system == "Windows"
|
||||||
|
Provides-Extra: test
|
||||||
|
Requires-Dist: pytest ; extra == 'test'
|
||||||
|
|
||||||
|
# PyClip
|
||||||
|
|
||||||
|
Cross-platform clipboard utilities supporting both binary and text data.
|
||||||
|
|
||||||
|
[![Docs](https://readthedocs.org/projects/pyclip/badge/?version=latest)](https://pyclip.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
![Build](https://img.shields.io/github/checks-status/spyoungtech/pyclip/main?label=build)
|
||||||
|
![Coverage](https://img.shields.io/codecov/c/gh/spyoungtech/pyclip/main)
|
||||||
|
![PyPI Version](https://img.shields.io/pypi/v/pyclip?color=blue)
|
||||||
|
![Python Versions](https://img.shields.io/pypi/pyversions/pyclip)
|
||||||
|
[![Download Stats](https://pepy.tech/badge/pyclip)](https://pepy.tech/project/pyclip)
|
||||||
|
|
||||||
|
|
||||||
|
Some key features include:
|
||||||
|
|
||||||
|
- A cross-platform API (supports MacOS, Windows, Linux)
|
||||||
|
- Can handle arbitrary binary data
|
||||||
|
- On Windows, some additional [clipboard formats](https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats)
|
||||||
|
are supported
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requires python 3.7+
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyclip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
pyclip can be used in Python code
|
||||||
|
```python
|
||||||
|
import pyclip
|
||||||
|
|
||||||
|
pyclip.copy("hello clipboard") # copy data to the clipboard
|
||||||
|
cb_data = pyclip.paste() # retrieve clipboard contents
|
||||||
|
print(cb_data) # b'hello clipboard'
|
||||||
|
cb_text = pyclip.paste(text=True) # paste as text
|
||||||
|
print(cb_text) # 'hello clipboard'
|
||||||
|
|
||||||
|
pyclip.clear() # clears the clipboard contents
|
||||||
|
assert not pyclip.paste()
|
||||||
|
```
|
||||||
|
|
||||||
|
Or a CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# paste clipboard contents to stdout
|
||||||
|
python -m pyclip paste
|
||||||
|
|
||||||
|
# load contents to the clipboard from stdin
|
||||||
|
python -m pyclip copy < myfile.text
|
||||||
|
# same as above, but pipe from another command
|
||||||
|
some-program | python -m pyclip copy
|
||||||
|
```
|
||||||
|
|
||||||
|
Installing via pip also provides the console script `pyclip`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyclip copy < my_file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
This library implements functionality for several platforms and clipboard utilities.
|
||||||
|
|
||||||
|
- [x] MacOS
|
||||||
|
- [x] Windows
|
||||||
|
- [x] Linux on x11 (with `xclip`)
|
||||||
|
- [x] Linux on wayland (with `wl-clipboard`)
|
||||||
|
|
||||||
|
If there is a platform or utility not currently listed, please request it by creating an issue.
|
||||||
|
|
||||||
|
## Platform specific notes/issues
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- On Windows, the `pywin32` package is installed as a requirement.
|
||||||
|
- On Windows, additional clipboard formats are supported, including copying from a file
|
||||||
|
(like if you right-click copy from File Explorer)
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
MacOS has support for multiple backends. By default, the `pasteboard` package is used.
|
||||||
|
|
||||||
|
`pbcopy`/`pbpaste` can also be used as a backend, but does not support arbitrary binary data, which may lead to
|
||||||
|
data being lost on copy/paste. This backend may be removed in a future release.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Linux on X11 requires `xclip` to work. Install with your package manager, e.g. `sudo apt install xclip`
|
||||||
|
Linux on Wayland requires `wl-clipboard` to work. Install with your package manager, e.g. `sudo apt install wl-clipboard`
|
||||||
|
|
||||||
|
# Acknowledgements
|
||||||
|
|
||||||
|
Big thanks to [Howard Mao](https://github.com/zhemao) for donating the PyClip project name on PyPI to
|
||||||
|
this project.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
|||||||
|
../../../bin/pyclip,sha256=AawA9qco0Va3y-adT5ZKv7ib3ut8-HmmAsi7yKZPuhg,193
|
||||||
|
pyclip-0.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
pyclip-0.6.0.dist-info/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
||||||
|
pyclip-0.6.0.dist-info/METADATA,sha256=nIp5chSfr9_HU0YWD9v0k33zyZoQgps0R4zEfn5quRo,3839
|
||||||
|
pyclip-0.6.0.dist-info/RECORD,,
|
||||||
|
pyclip-0.6.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
pyclip-0.6.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
||||||
|
pyclip-0.6.0.dist-info/entry_points.txt,sha256=t8LmF4YObliWB64BAJqmYNMO16yZYLUhd9zk9tD_wNs,43
|
||||||
|
pyclip-0.6.0.dist-info/top_level.txt,sha256=h64senT7YjCx1F9iQSor4kFD70wMTG3RAYbCcq768W8,7
|
||||||
|
pyclip/__init__.py,sha256=a6mKjCyQSBxlWsHeZg4xGHmbn7B-jq1sobXUGW9FwDU,1779
|
||||||
|
pyclip/__main__.py,sha256=IifrnAzdJ9Uns8r7tUBBWYn5PFxJZOrd5rFQ7xYKDsQ,657
|
||||||
|
pyclip/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/__main__.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/base.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/cli.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/macos_clip.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/util.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/wayland_clip.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/win_clip.cpython-310.pyc,,
|
||||||
|
pyclip/__pycache__/xclip_clip.cpython-310.pyc,,
|
||||||
|
pyclip/base.py,sha256=TnYebJEPzq_3j2qLLMSaRY7VFgSKDyQazcC8sl7y79g,1370
|
||||||
|
pyclip/cli.py,sha256=yVPWKlJ6YCOjgvaXH_9Pj2RnchAalb14otd9YkFLo6o,1541
|
||||||
|
pyclip/macos_clip.py,sha256=tuAfcp0Tv_tPv-ST0JH85DjMQi0n0I5BKUXYa0wIO-U,7306
|
||||||
|
pyclip/util.py,sha256=dy1FJxVlnUi_4byNlf3Y1dHrWeKbt4fbAefF9vGNT4U,1398
|
||||||
|
pyclip/wayland_clip.py,sha256=hCXMuz4CihHHNOUOtinooQJAslDrTdX_-OhGnUNsuIA,4210
|
||||||
|
pyclip/win_clip.py,sha256=7nOmFCKgktq33sy9d3N30TePodVPHLN9ZJjq0f5mg0o,8425
|
||||||
|
pyclip/xclip_clip.py,sha256=YjyJpPAVa4cpjQL1GUi08jNORP5asdbgPGTLZg6zgNo,4144
|
@ -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]
|
||||||
|
pyclip = pyclip.cli:main
|
@ -0,0 +1 @@
|
|||||||
|
pyclip
|
@ -0,0 +1,49 @@
|
|||||||
|
# Copyright 2021 Spencer Phillip Young
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
import sys
|
||||||
|
from .util import detect_clipboard
|
||||||
|
from .base import ClipboardSetupException
|
||||||
|
from functools import wraps
|
||||||
|
try:
|
||||||
|
DEFAULT_CLIPBOARD = detect_clipboard()
|
||||||
|
except ClipboardSetupException as e:
|
||||||
|
DEFAULT_CLIPBOARD = None
|
||||||
|
_CLIPBOARD_EXCEPTION_TB = sys.exc_info()[2]
|
||||||
|
|
||||||
|
def wrapif(f):
|
||||||
|
if DEFAULT_CLIPBOARD is not None:
|
||||||
|
wrapped = getattr(DEFAULT_CLIPBOARD, f.__name__)
|
||||||
|
wrapper = wraps(wrapped)
|
||||||
|
return wrapper(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@wrapif
|
||||||
|
def copy(*args, **kwargs):
|
||||||
|
if DEFAULT_CLIPBOARD is None:
|
||||||
|
raise ClipboardSetupException("Could not setup clipboard").with_traceback(_CLIPBOARD_EXCEPTION_TB)
|
||||||
|
return DEFAULT_CLIPBOARD.copy(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@wrapif
|
||||||
|
def paste(*args, **kwargs):
|
||||||
|
if DEFAULT_CLIPBOARD is None:
|
||||||
|
raise ClipboardSetupException("Could not setup clipboard").with_traceback(_CLIPBOARD_EXCEPTION_TB)
|
||||||
|
return DEFAULT_CLIPBOARD.paste(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@wrapif
|
||||||
|
def clear(*args, **kwargs):
|
||||||
|
if DEFAULT_CLIPBOARD is None:
|
||||||
|
raise ClipboardSetupException("Could not setup clipboard").with_traceback(_CLIPBOARD_EXCEPTION_TB)
|
||||||
|
return DEFAULT_CLIPBOARD.clear(*args, **kwargs)
|
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2021 Spencer Phillip Young
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,41 @@
|
|||||||
|
# Copyright 2021 Spencer Phillip Young
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Provides the abstract base class from which all clipboard implementations are derived.
|
||||||
|
"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
class ClipboardException(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
class ClipboardSetupException(ClipboardException):
|
||||||
|
...
|
||||||
|
|
||||||
|
class ClipboardBase(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for Clipboard implementations.
|
||||||
|
"""
|
||||||
|
@abstractmethod
|
||||||
|
def copy(self, data: Union[str, bytes], encoding: str = None):
|
||||||
|
return NotImplemented # pragma: no cover
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def paste(self, encoding=None, text=None, errors=None) -> Union[str, bytes]:
|
||||||
|
return NotImplemented # pragma: no cover
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear(self):
|
||||||
|
return NotImplemented # pragma: no cover
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue