ConfigTools & Encription manager
This commit is contained in:
49
ConfigTools.py
Normal file
49
ConfigTools.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from logging import Manager
|
||||||
|
import os
|
||||||
|
from lib.cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
class CredentialManager:
|
||||||
|
def __init__(self, key_file="secret_key"):
|
||||||
|
self.key_file = key_file
|
||||||
|
self.key = self._load_or_generate_key()
|
||||||
|
self.cipher = Fernet(self.key)
|
||||||
|
|
||||||
|
def _load_or_generate_key(self):
|
||||||
|
if not os.path.exists(self.key_file):
|
||||||
|
key = Fernet.generate_key()
|
||||||
|
with open(self.key_file, "wb") as file:
|
||||||
|
file.write(key)
|
||||||
|
os.chmod(self.key_file, 0o600)
|
||||||
|
return key
|
||||||
|
with open(self.key_file, "rb") as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
def encrypt_password(self, password):
|
||||||
|
return self.cipher.encrypt(password.encode()).decode()
|
||||||
|
|
||||||
|
def dencrypt_password(self, encrypted_password):
|
||||||
|
return self.cipher.decrypt(encrypted_password.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
manager = CredentialManager()
|
||||||
|
|
||||||
|
password = "test1"
|
||||||
|
encrypted = manager.encrypt_password(password)
|
||||||
|
print(f"encrpted: {encrypted}")
|
||||||
|
|
||||||
|
|
||||||
|
decripted = manager.dencrypt_password(encrypted)
|
||||||
|
print(f"Decripted: {decripted}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
22
PKGBUILD
Normal file
22
PKGBUILD
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
pkgname=ServerSync
|
||||||
|
pkgver=1.0.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="A tool to simply manage & Sync files and directories to remotes with configs!"
|
||||||
|
arch=('any')
|
||||||
|
url="https://github.com/youruser/your-tool"
|
||||||
|
license=('MIT')
|
||||||
|
depends=('bash, python3') # Add runtime dependencies here
|
||||||
|
makedepends=('git' 'gcc') # Add build-time dependencies here
|
||||||
|
source=("https://github.com/youruser/$pkgname/archive/v$pkgver.tar.gz")
|
||||||
|
sha256sums=('SKIP') # We will fix this in the next step
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
make # Or your specific build command
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
# This installs the binary to /usr/bin inside the package
|
||||||
|
install -Dm755 your-binary-name "$pkgdir/usr/bin/your-binary-name"
|
||||||
|
}
|
||||||
1
lib/jaraco.classes-3.4.0.dist-info/INSTALLER
Normal file
1
lib/jaraco.classes-3.4.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
17
lib/jaraco.classes-3.4.0.dist-info/LICENSE
Normal file
17
lib/jaraco.classes-3.4.0.dist-info/LICENSE
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
60
lib/jaraco.classes-3.4.0.dist-info/METADATA
Normal file
60
lib/jaraco.classes-3.4.0.dist-info/METADATA
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: jaraco.classes
|
||||||
|
Version: 3.4.0
|
||||||
|
Summary: Utility functions for Python class constructs
|
||||||
|
Home-page: https://github.com/jaraco/jaraco.classes
|
||||||
|
Author: Jason R. Coombs
|
||||||
|
Author-email: jaraco@jaraco.com
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: more-itertools
|
||||||
|
Provides-Extra: docs
|
||||||
|
Requires-Dist: sphinx >=3.5 ; extra == 'docs'
|
||||||
|
Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs'
|
||||||
|
Requires-Dist: rst.linker >=1.9 ; extra == 'docs'
|
||||||
|
Requires-Dist: furo ; extra == 'docs'
|
||||||
|
Requires-Dist: sphinx-lint ; extra == 'docs'
|
||||||
|
Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs'
|
||||||
|
Provides-Extra: testing
|
||||||
|
Requires-Dist: pytest >=6 ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-cov ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-mypy ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing'
|
||||||
|
Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'testing'
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/jaraco.classes.svg
|
||||||
|
:target: https://pypi.org/project/jaraco.classes
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/jaraco.classes.svg
|
||||||
|
|
||||||
|
.. image:: https://github.com/jaraco/jaraco.classes/actions/workflows/main.yml/badge.svg
|
||||||
|
:target: https://github.com/jaraco/jaraco.classes/actions?query=workflow%3A%22tests%22
|
||||||
|
:alt: tests
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
|
||||||
|
:target: https://github.com/astral-sh/ruff
|
||||||
|
:alt: Ruff
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/jaracoclasses/badge/?version=latest
|
||||||
|
:target: https://jaracoclasses.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/skeleton-2024-informational
|
||||||
|
:target: https://blog.jaraco.com/skeleton
|
||||||
|
|
||||||
|
.. image:: https://tidelift.com/badges/package/pypi/jaraco.classes
|
||||||
|
:target: https://tidelift.com/subscription/pkg/pypi-jaraco.classes?utm_source=pypi-jaraco.classes&utm_medium=readme
|
||||||
|
|
||||||
|
For Enterprise
|
||||||
|
==============
|
||||||
|
|
||||||
|
Available as part of the Tidelift Subscription.
|
||||||
|
|
||||||
|
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
|
||||||
|
|
||||||
|
`Learn more <https://tidelift.com/subscription/pkg/pypi-jaraco.classes?utm_source=pypi-jaraco.classes&utm_medium=referral&utm_campaign=github>`_.
|
||||||
15
lib/jaraco.classes-3.4.0.dist-info/RECORD
Normal file
15
lib/jaraco.classes-3.4.0.dist-info/RECORD
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
jaraco.classes-3.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
jaraco.classes-3.4.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
||||||
|
jaraco.classes-3.4.0.dist-info/METADATA,sha256=LmsQIjLt1Frhu4prQJH9QM8yAaa7b9S8l8XozXZaRLg,2623
|
||||||
|
jaraco.classes-3.4.0.dist-info/RECORD,,
|
||||||
|
jaraco.classes-3.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
||||||
|
jaraco.classes-3.4.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
||||||
|
jaraco/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jaraco/classes/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jaraco/classes/__pycache__/ancestry.cpython-314.pyc,,
|
||||||
|
jaraco/classes/__pycache__/meta.cpython-314.pyc,,
|
||||||
|
jaraco/classes/__pycache__/properties.cpython-314.pyc,,
|
||||||
|
jaraco/classes/ancestry.py,sha256=FkU7kyOO-TOMgwR3obcpqB93Ht-f0yxjGnTxcvfBLB0,1787
|
||||||
|
jaraco/classes/meta.py,sha256=uz1zmtse_0n7cs2M2hfz8iIqoe2_2vZI-_JiFvQuDwE,2198
|
||||||
|
jaraco/classes/properties.py,sha256=f-88KCSBeeCliwxfXOwe7Uqk9_elEmi9ZSwOh6_yBq4,6191
|
||||||
|
jaraco/classes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
5
lib/jaraco.classes-3.4.0.dist-info/WHEEL
Normal file
5
lib/jaraco.classes-3.4.0.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.43.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
1
lib/jaraco.classes-3.4.0.dist-info/top_level.txt
Normal file
1
lib/jaraco.classes-3.4.0.dist-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jaraco
|
||||||
0
lib/jaraco/classes/__init__.py
Normal file
0
lib/jaraco/classes/__init__.py
Normal file
BIN
lib/jaraco/classes/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jaraco/classes/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jaraco/classes/__pycache__/ancestry.cpython-314.pyc
Normal file
BIN
lib/jaraco/classes/__pycache__/ancestry.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jaraco/classes/__pycache__/meta.cpython-314.pyc
Normal file
BIN
lib/jaraco/classes/__pycache__/meta.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jaraco/classes/__pycache__/properties.cpython-314.pyc
Normal file
BIN
lib/jaraco/classes/__pycache__/properties.cpython-314.pyc
Normal file
Binary file not shown.
76
lib/jaraco/classes/ancestry.py
Normal file
76
lib/jaraco/classes/ancestry.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Routines for obtaining the class names
|
||||||
|
of an object and its parent classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
from more_itertools import unique_everseen
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def all_bases(c: type[object]) -> list[type[Any]]:
|
||||||
|
"""
|
||||||
|
return a tuple of all base classes the class c has as a parent.
|
||||||
|
>>> object in all_bases(list)
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
return c.mro()[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def all_classes(c: type[object]) -> list[type[Any]]:
|
||||||
|
"""
|
||||||
|
return a tuple of all classes to which c belongs
|
||||||
|
>>> list in all_classes(list)
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
return c.mro()
|
||||||
|
|
||||||
|
|
||||||
|
# borrowed from
|
||||||
|
# http://code.activestate.com/recipes/576949-find-all-subclasses-of-a-given-class/
|
||||||
|
|
||||||
|
|
||||||
|
def iter_subclasses(cls: type[object]) -> Iterator[type[Any]]:
|
||||||
|
"""
|
||||||
|
Generator over all subclasses of a given class, in depth-first order.
|
||||||
|
|
||||||
|
>>> bool in list(iter_subclasses(int))
|
||||||
|
True
|
||||||
|
>>> class A(object): pass
|
||||||
|
>>> class B(A): pass
|
||||||
|
>>> class C(A): pass
|
||||||
|
>>> class D(B,C): pass
|
||||||
|
>>> class E(D): pass
|
||||||
|
>>>
|
||||||
|
>>> for cls in iter_subclasses(A):
|
||||||
|
... print(cls.__name__)
|
||||||
|
B
|
||||||
|
D
|
||||||
|
E
|
||||||
|
C
|
||||||
|
>>> # get ALL classes currently defined
|
||||||
|
>>> res = [cls.__name__ for cls in iter_subclasses(object)]
|
||||||
|
>>> 'type' in res
|
||||||
|
True
|
||||||
|
>>> 'tuple' in res
|
||||||
|
True
|
||||||
|
>>> len(res) > 100
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
return unique_everseen(_iter_all_subclasses(cls))
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_all_subclasses(cls: type[object]) -> Iterator[type[Any]]:
|
||||||
|
try:
|
||||||
|
subs = cls.__subclasses__()
|
||||||
|
except TypeError: # fails only when cls is type
|
||||||
|
subs = cast('type[type]', cls).__subclasses__(cls)
|
||||||
|
for sub in subs:
|
||||||
|
yield sub
|
||||||
|
yield from iter_subclasses(sub)
|
||||||
85
lib/jaraco/classes/meta.py
Normal file
85
lib/jaraco/classes/meta.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
meta.py
|
||||||
|
|
||||||
|
Some useful metaclasses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class LeafClassesMeta(type):
|
||||||
|
"""
|
||||||
|
A metaclass for classes that keeps track of all of them that
|
||||||
|
aren't base classes.
|
||||||
|
|
||||||
|
>>> Parent = LeafClassesMeta('MyParentClass', (), {})
|
||||||
|
>>> Parent in Parent._leaf_classes
|
||||||
|
True
|
||||||
|
>>> Child = LeafClassesMeta('MyChildClass', (Parent,), {})
|
||||||
|
>>> Child in Parent._leaf_classes
|
||||||
|
True
|
||||||
|
>>> Parent in Parent._leaf_classes
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> Other = LeafClassesMeta('OtherClass', (), {})
|
||||||
|
>>> Parent in Other._leaf_classes
|
||||||
|
False
|
||||||
|
>>> len(Other._leaf_classes)
|
||||||
|
1
|
||||||
|
"""
|
||||||
|
|
||||||
|
_leaf_classes: set[type[Any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
bases: tuple[type[object], ...],
|
||||||
|
attrs: dict[str, object],
|
||||||
|
) -> None:
|
||||||
|
if not hasattr(cls, '_leaf_classes'):
|
||||||
|
cls._leaf_classes = set()
|
||||||
|
leaf_classes = getattr(cls, '_leaf_classes')
|
||||||
|
leaf_classes.add(cls)
|
||||||
|
# remove any base classes
|
||||||
|
leaf_classes -= set(bases)
|
||||||
|
|
||||||
|
|
||||||
|
class TagRegistered(type):
|
||||||
|
"""
|
||||||
|
As classes of this metaclass are created, they keep a registry in the
|
||||||
|
base class of all classes by a class attribute, indicated by attr_name.
|
||||||
|
|
||||||
|
>>> FooObject = TagRegistered('FooObject', (), dict(tag='foo'))
|
||||||
|
>>> FooObject._registry['foo'] is FooObject
|
||||||
|
True
|
||||||
|
>>> BarObject = TagRegistered('Barobject', (FooObject,), dict(tag='bar'))
|
||||||
|
>>> FooObject._registry is BarObject._registry
|
||||||
|
True
|
||||||
|
>>> len(FooObject._registry)
|
||||||
|
2
|
||||||
|
|
||||||
|
'...' below should be 'jaraco.classes' but for pytest-dev/pytest#3396
|
||||||
|
>>> FooObject._registry['bar']
|
||||||
|
<class '....meta.Barobject'>
|
||||||
|
"""
|
||||||
|
|
||||||
|
attr_name = 'tag'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
bases: tuple[type[object], ...],
|
||||||
|
namespace: dict[str, object],
|
||||||
|
) -> None:
|
||||||
|
super(TagRegistered, cls).__init__(name, bases, namespace)
|
||||||
|
if not hasattr(cls, '_registry'):
|
||||||
|
cls._registry = {}
|
||||||
|
meta = cls.__class__
|
||||||
|
attr = getattr(cls, meta.attr_name, None)
|
||||||
|
if attr:
|
||||||
|
cls._registry[attr] = cls
|
||||||
241
lib/jaraco/classes/properties.py
Normal file
241
lib/jaraco/classes/properties.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
_U = TypeVar('_U')
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
from typing_extensions import Self, TypeAlias
|
||||||
|
|
||||||
|
# TODO(coherent-oss/granary#4): Migrate to PEP 695 by 2027-10.
|
||||||
|
_GetterCallable: TypeAlias = Callable[..., _T]
|
||||||
|
_GetterClassMethod: TypeAlias = classmethod[Any, [], _T]
|
||||||
|
|
||||||
|
_SetterCallable: TypeAlias = Callable[[type[Any], _T], None]
|
||||||
|
_SetterClassMethod: TypeAlias = classmethod[Any, [_T], None]
|
||||||
|
|
||||||
|
class _ClassPropertyAttribute(Protocol[_T]):
|
||||||
|
def __get__(self, obj: object, objtype: type[Any] | None = None) -> _T: ...
|
||||||
|
|
||||||
|
def __set__(self, obj: object, value: _T) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class NonDataProperty(Generic[_T, _U]):
|
||||||
|
"""Much like the property builtin, but only implements __get__,
|
||||||
|
making it a non-data property, and can be subsequently reset.
|
||||||
|
|
||||||
|
See http://users.rcn.com/python/download/Descriptor.htm for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
>>> class X(object):
|
||||||
|
... @NonDataProperty
|
||||||
|
... def foo(self):
|
||||||
|
... return 3
|
||||||
|
>>> x = X()
|
||||||
|
>>> x.foo
|
||||||
|
3
|
||||||
|
>>> x.foo = 4
|
||||||
|
>>> x.foo
|
||||||
|
4
|
||||||
|
|
||||||
|
'...' below should be 'jaraco.classes' but for pytest-dev/pytest#3396
|
||||||
|
>>> X.foo
|
||||||
|
<....properties.NonDataProperty object at ...>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fget: Callable[[_T], _U]) -> None:
|
||||||
|
assert fget is not None, "fget cannot be none"
|
||||||
|
assert callable(fget), "fget must be callable"
|
||||||
|
self.fget = fget
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
obj: None,
|
||||||
|
objtype: None,
|
||||||
|
) -> Self: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
obj: _T,
|
||||||
|
objtype: type[_T] | None = None,
|
||||||
|
) -> _U: ...
|
||||||
|
|
||||||
|
def __get__(
|
||||||
|
self,
|
||||||
|
obj: _T | None,
|
||||||
|
objtype: type[_T] | None = None,
|
||||||
|
) -> Self | _U:
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
return self.fget(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty(Generic[_T]):
|
||||||
|
"""
|
||||||
|
Like @property but applies at the class level.
|
||||||
|
|
||||||
|
|
||||||
|
>>> class X(metaclass=classproperty.Meta):
|
||||||
|
... val = None
|
||||||
|
... @classproperty
|
||||||
|
... def foo(cls):
|
||||||
|
... return cls.val
|
||||||
|
... @foo.setter
|
||||||
|
... def foo(cls, val):
|
||||||
|
... cls.val = val
|
||||||
|
>>> X.foo
|
||||||
|
>>> X.foo = 3
|
||||||
|
>>> X.foo
|
||||||
|
3
|
||||||
|
>>> x = X()
|
||||||
|
>>> x.foo
|
||||||
|
3
|
||||||
|
>>> X.foo = 4
|
||||||
|
>>> x.foo
|
||||||
|
4
|
||||||
|
|
||||||
|
Setting the property on an instance affects the class.
|
||||||
|
|
||||||
|
>>> x.foo = 5
|
||||||
|
>>> x.foo
|
||||||
|
5
|
||||||
|
>>> X.foo
|
||||||
|
5
|
||||||
|
>>> vars(x)
|
||||||
|
{}
|
||||||
|
>>> X().foo
|
||||||
|
5
|
||||||
|
|
||||||
|
Attempting to set an attribute where no setter was defined
|
||||||
|
results in an AttributeError:
|
||||||
|
|
||||||
|
>>> class GetOnly(metaclass=classproperty.Meta):
|
||||||
|
... @classproperty
|
||||||
|
... def foo(cls):
|
||||||
|
... return 'bar'
|
||||||
|
>>> GetOnly.foo = 3
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: can't set attribute
|
||||||
|
|
||||||
|
It is also possible to wrap a classmethod or staticmethod in
|
||||||
|
a classproperty.
|
||||||
|
|
||||||
|
>>> class Static(metaclass=classproperty.Meta):
|
||||||
|
... @classproperty
|
||||||
|
... @classmethod
|
||||||
|
... def foo(cls):
|
||||||
|
... return 'foo'
|
||||||
|
... @classproperty
|
||||||
|
... @staticmethod
|
||||||
|
... def bar():
|
||||||
|
... return 'bar'
|
||||||
|
>>> Static.foo
|
||||||
|
'foo'
|
||||||
|
>>> Static.bar
|
||||||
|
'bar'
|
||||||
|
|
||||||
|
*Legacy*
|
||||||
|
|
||||||
|
For compatibility, if the metaclass isn't specified, the
|
||||||
|
legacy behavior will be invoked.
|
||||||
|
|
||||||
|
>>> class X:
|
||||||
|
... val = None
|
||||||
|
... @classproperty
|
||||||
|
... def foo(cls):
|
||||||
|
... return cls.val
|
||||||
|
... @foo.setter
|
||||||
|
... def foo(cls, val):
|
||||||
|
... cls.val = val
|
||||||
|
>>> X.foo
|
||||||
|
>>> X.foo = 3
|
||||||
|
>>> X.foo
|
||||||
|
3
|
||||||
|
>>> x = X()
|
||||||
|
>>> x.foo
|
||||||
|
3
|
||||||
|
>>> X.foo = 4
|
||||||
|
>>> x.foo
|
||||||
|
4
|
||||||
|
|
||||||
|
Note, because the metaclass was not specified, setting
|
||||||
|
a value on an instance does not have the intended effect.
|
||||||
|
|
||||||
|
>>> x.foo = 5
|
||||||
|
>>> x.foo
|
||||||
|
5
|
||||||
|
>>> X.foo # should be 5
|
||||||
|
4
|
||||||
|
>>> vars(x) # should be empty
|
||||||
|
{'foo': 5}
|
||||||
|
>>> X().foo # should be 5
|
||||||
|
4
|
||||||
|
"""
|
||||||
|
|
||||||
|
fget: _ClassPropertyAttribute[_GetterClassMethod[_T]]
|
||||||
|
fset: _ClassPropertyAttribute[_SetterClassMethod[_T] | None]
|
||||||
|
|
||||||
|
class Meta(type):
|
||||||
|
def __setattr__(self, key: str, value: object) -> None:
|
||||||
|
obj = self.__dict__.get(key, None)
|
||||||
|
if type(obj) is classproperty:
|
||||||
|
return obj.__set__(self, value)
|
||||||
|
return super().__setattr__(key, value)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fget: _GetterCallable[_T] | _GetterClassMethod[_T],
|
||||||
|
fset: _SetterCallable[_T] | _SetterClassMethod[_T] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.fget = self._ensure_method(fget)
|
||||||
|
self.fset = fset # type: ignore[assignment] # Corrected in the next line.
|
||||||
|
fset and self.setter(fset)
|
||||||
|
|
||||||
|
def __get__(self, instance: object, owner: type[object] | None = None) -> _T:
|
||||||
|
return self.fget.__get__(None, owner)()
|
||||||
|
|
||||||
|
def __set__(self, owner: object, value: _T) -> None:
|
||||||
|
if not self.fset:
|
||||||
|
raise AttributeError("can't set attribute")
|
||||||
|
if type(owner) is not classproperty.Meta:
|
||||||
|
owner = type(owner)
|
||||||
|
return self.fset.__get__(None, cast('type[object]', owner))(value)
|
||||||
|
|
||||||
|
def setter(self, fset: _SetterCallable[_T] | _SetterClassMethod[_T]) -> Self:
|
||||||
|
self.fset = self._ensure_method(fset)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def _ensure_method(
|
||||||
|
cls,
|
||||||
|
fn: _GetterCallable[_T] | _GetterClassMethod[_T],
|
||||||
|
) -> _GetterClassMethod[_T]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def _ensure_method(
|
||||||
|
cls,
|
||||||
|
fn: _SetterCallable[_T] | _SetterClassMethod[_T],
|
||||||
|
) -> _SetterClassMethod[_T]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_method(
|
||||||
|
cls,
|
||||||
|
fn: _GetterCallable[_T]
|
||||||
|
| _GetterClassMethod[_T]
|
||||||
|
| _SetterCallable[_T]
|
||||||
|
| _SetterClassMethod[_T],
|
||||||
|
) -> _GetterClassMethod[_T] | _SetterClassMethod[_T]:
|
||||||
|
"""
|
||||||
|
Ensure fn is a classmethod or staticmethod.
|
||||||
|
"""
|
||||||
|
needs_method = not isinstance(fn, (classmethod, staticmethod))
|
||||||
|
return classmethod(fn) if needs_method else fn # type: ignore[arg-type,return-value]
|
||||||
0
lib/jaraco/classes/py.typed
Normal file
0
lib/jaraco/classes/py.typed
Normal file
367
lib/jaraco/context/__init__.py
Normal file
367
lib/jaraco/context/__init__.py
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
if sys.version_info < (3, 12):
|
||||||
|
from backports import tarfile
|
||||||
|
else:
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]:
|
||||||
|
"""
|
||||||
|
>>> tmp_path = getfixture('tmp_path')
|
||||||
|
>>> with pushd(tmp_path):
|
||||||
|
... assert os.getcwd() == os.fspath(tmp_path)
|
||||||
|
>>> assert os.getcwd() != os.fspath(tmp_path)
|
||||||
|
"""
|
||||||
|
|
||||||
|
orig = os.getcwd()
|
||||||
|
os.chdir(dir)
|
||||||
|
try:
|
||||||
|
yield dir
|
||||||
|
finally:
|
||||||
|
os.chdir(orig)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def tarball(
|
||||||
|
url, target_dir: str | os.PathLike | None = None
|
||||||
|
) -> Iterator[str | os.PathLike]:
|
||||||
|
"""
|
||||||
|
Get a URL to a tarball, download, extract, yield, then clean up.
|
||||||
|
|
||||||
|
Assumes everything in the tarball is prefixed with a common
|
||||||
|
directory. That common path is stripped and the contents
|
||||||
|
are extracted to ``target_dir``, similar to passing
|
||||||
|
``-C {target} --strip-components 1`` to the ``tar`` command.
|
||||||
|
|
||||||
|
Uses the streaming protocol to extract the contents from a
|
||||||
|
stream in a single pass without loading the whole file into
|
||||||
|
memory.
|
||||||
|
|
||||||
|
>>> import urllib.request
|
||||||
|
>>> url = getfixture('tarfile_served')
|
||||||
|
>>> target = getfixture('tmp_path') / 'out'
|
||||||
|
>>> tb = tarball(url, target_dir=target)
|
||||||
|
>>> import pathlib
|
||||||
|
>>> with tb as extracted:
|
||||||
|
... contents = pathlib.Path(extracted, 'contents.txt').read_text(encoding='utf-8')
|
||||||
|
>>> assert not os.path.exists(extracted)
|
||||||
|
|
||||||
|
If the target is not specified, contents are extracted to a
|
||||||
|
directory relative to the current working directory named after
|
||||||
|
the name of the file as extracted from the URL.
|
||||||
|
|
||||||
|
>>> target = getfixture('tmp_path')
|
||||||
|
>>> with pushd(target), tarball(url):
|
||||||
|
... target.joinpath('served').is_dir()
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
if target_dir is None:
|
||||||
|
target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '')
|
||||||
|
os.mkdir(target_dir)
|
||||||
|
try:
|
||||||
|
req = urllib.request.urlopen(url)
|
||||||
|
with tarfile.open(fileobj=req, mode='r|*') as tf:
|
||||||
|
tf.extractall(path=target_dir, filter=_default_filter)
|
||||||
|
yield target_dir
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_tarfile_filters(*filters):
|
||||||
|
def compose_two(f1, f2):
|
||||||
|
return lambda member, path: f1(f2(member, path), path)
|
||||||
|
|
||||||
|
return functools.reduce(compose_two, filters, lambda member, path: member)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_first_component(
|
||||||
|
member: tarfile.TarInfo,
|
||||||
|
path,
|
||||||
|
) -> tarfile.TarInfo:
|
||||||
|
_, member.name = member.name.split('/', 1)
|
||||||
|
return member
|
||||||
|
|
||||||
|
|
||||||
|
_default_filter = _compose_tarfile_filters(tarfile.data_filter, strip_first_component)
|
||||||
|
|
||||||
|
|
||||||
|
def _compose(*cmgrs):
|
||||||
|
"""
|
||||||
|
Compose any number of dependent context managers into a single one.
|
||||||
|
|
||||||
|
The last, innermost context manager may take arbitrary arguments, but
|
||||||
|
each successive context manager should accept the result from the
|
||||||
|
previous as a single parameter.
|
||||||
|
|
||||||
|
Like :func:`jaraco.functools.compose`, behavior works from right to
|
||||||
|
left, so the context manager should be indicated from outermost to
|
||||||
|
innermost.
|
||||||
|
|
||||||
|
Example, to create a context manager to change to a temporary
|
||||||
|
directory:
|
||||||
|
|
||||||
|
>>> temp_dir_as_cwd = _compose(pushd, temp_dir)
|
||||||
|
>>> with temp_dir_as_cwd() as dir:
|
||||||
|
... assert os.path.samefile(os.getcwd(), dir)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose_two(inner, outer):
|
||||||
|
def composed(*args, **kwargs):
|
||||||
|
with inner(*args, **kwargs) as saved, outer(saved) as res:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
return contextlib.contextmanager(composed)
|
||||||
|
|
||||||
|
return functools.reduce(compose_two, reversed(cmgrs))
|
||||||
|
|
||||||
|
|
||||||
|
tarball_cwd = _compose(pushd, tarball)
|
||||||
|
"""
|
||||||
|
A tarball context with the current working directory pointing to the contents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def remove_readonly(func, path, exc_info):
|
||||||
|
"""
|
||||||
|
Add support for removing read-only files on Windows.
|
||||||
|
"""
|
||||||
|
_, exc, _ = exc_info
|
||||||
|
if func in (os.rmdir, os.remove, os.unlink) and exc.errno == errno.EACCES:
|
||||||
|
# change the file to be readable,writable,executable: 0777
|
||||||
|
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||||||
|
# retry
|
||||||
|
func(path)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def robust_remover():
|
||||||
|
return (
|
||||||
|
functools.partial(shutil.rmtree, onerror=remove_readonly)
|
||||||
|
if platform.system() == 'Windows'
|
||||||
|
else shutil.rmtree
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def temp_dir(remover=shutil.rmtree):
|
||||||
|
"""
|
||||||
|
Create a temporary directory context. Pass a custom remover
|
||||||
|
to override the removal behavior.
|
||||||
|
|
||||||
|
>>> import pathlib
|
||||||
|
>>> with temp_dir() as the_dir:
|
||||||
|
... assert os.path.isdir(the_dir)
|
||||||
|
>>> assert not os.path.exists(the_dir)
|
||||||
|
"""
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
yield temp_dir
|
||||||
|
finally:
|
||||||
|
remover(temp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
robust_temp_dir = functools.partial(temp_dir, remover=robust_remover())
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def repo_context(
|
||||||
|
url, branch: str | None = None, quiet: bool = True, dest_ctx=robust_temp_dir
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Check out the repo indicated by url.
|
||||||
|
|
||||||
|
If dest_ctx is supplied, it should be a context manager
|
||||||
|
to yield the target directory for the check out.
|
||||||
|
|
||||||
|
>>> getfixture('ensure_git')
|
||||||
|
>>> getfixture('needs_internet')
|
||||||
|
>>> repo = repo_context('https://github.com/jaraco/jaraco.context')
|
||||||
|
>>> with repo as dest:
|
||||||
|
... listing = os.listdir(dest)
|
||||||
|
>>> 'README.rst' in listing
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
exe = 'git' if 'git' in url else 'hg'
|
||||||
|
with dest_ctx() as repo_dir:
|
||||||
|
cmd = [exe, 'clone', url, repo_dir]
|
||||||
|
cmd.extend(['--branch', branch] * bool(branch))
|
||||||
|
stream = subprocess.DEVNULL if quiet else None
|
||||||
|
subprocess.check_call(cmd, stdout=stream, stderr=stream)
|
||||||
|
yield repo_dir
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionTrap:
|
||||||
|
"""
|
||||||
|
A context manager that will catch certain exceptions and provide an
|
||||||
|
indication they occurred.
|
||||||
|
|
||||||
|
>>> with ExceptionTrap() as trap:
|
||||||
|
... raise Exception()
|
||||||
|
>>> bool(trap)
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> with ExceptionTrap() as trap:
|
||||||
|
... pass
|
||||||
|
>>> bool(trap)
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> with ExceptionTrap(ValueError) as trap:
|
||||||
|
... raise ValueError("1 + 1 is not 3")
|
||||||
|
>>> bool(trap)
|
||||||
|
True
|
||||||
|
>>> trap.value
|
||||||
|
ValueError('1 + 1 is not 3')
|
||||||
|
>>> trap.tb
|
||||||
|
<traceback object at ...>
|
||||||
|
|
||||||
|
>>> with ExceptionTrap(ValueError) as trap:
|
||||||
|
... raise Exception()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
Exception
|
||||||
|
|
||||||
|
>>> bool(trap)
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
|
||||||
|
exc_info = None, None, None
|
||||||
|
|
||||||
|
def __init__(self, exceptions=(Exception,)):
|
||||||
|
self.exceptions = exceptions
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return self.exc_info[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.exc_info[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tb(self):
|
||||||
|
return self.exc_info[2]
|
||||||
|
|
||||||
|
def __exit__(self, *exc_info):
|
||||||
|
type = exc_info[0]
|
||||||
|
matches = type and issubclass(type, self.exceptions)
|
||||||
|
if matches:
|
||||||
|
self.exc_info = exc_info
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self.type)
|
||||||
|
|
||||||
|
def raises(self, func, *, _test=bool):
|
||||||
|
"""
|
||||||
|
Wrap func and replace the result with the truth
|
||||||
|
value of the trap (True if an exception occurred).
|
||||||
|
|
||||||
|
First, give the decorator an alias to support Python 3.8
|
||||||
|
Syntax.
|
||||||
|
|
||||||
|
>>> raises = ExceptionTrap(ValueError).raises
|
||||||
|
|
||||||
|
Now decorate a function that always fails.
|
||||||
|
|
||||||
|
>>> @raises
|
||||||
|
... def fail():
|
||||||
|
... raise ValueError('failed')
|
||||||
|
>>> fail()
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
with ExceptionTrap(self.exceptions) as trap:
|
||||||
|
func(*args, **kwargs)
|
||||||
|
return _test(trap)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def passes(self, func):
|
||||||
|
"""
|
||||||
|
Wrap func and replace the result with the truth
|
||||||
|
value of the trap (True if no exception).
|
||||||
|
|
||||||
|
First, give the decorator an alias to support Python 3.8
|
||||||
|
Syntax.
|
||||||
|
|
||||||
|
>>> passes = ExceptionTrap(ValueError).passes
|
||||||
|
|
||||||
|
Now decorate a function that always fails.
|
||||||
|
|
||||||
|
>>> @passes
|
||||||
|
... def fail():
|
||||||
|
... raise ValueError('failed')
|
||||||
|
|
||||||
|
>>> fail()
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
return self.raises(func, _test=operator.not_)
|
||||||
|
|
||||||
|
|
||||||
|
class suppress(contextlib.suppress, contextlib.ContextDecorator):
|
||||||
|
"""
|
||||||
|
A version of contextlib.suppress with decorator support.
|
||||||
|
|
||||||
|
>>> @suppress(KeyError)
|
||||||
|
... def key_error():
|
||||||
|
... {}['']
|
||||||
|
>>> key_error()
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class on_interrupt(contextlib.ContextDecorator):
|
||||||
|
"""
|
||||||
|
Replace a KeyboardInterrupt with SystemExit(1).
|
||||||
|
|
||||||
|
Useful in conjunction with console entry point functions.
|
||||||
|
|
||||||
|
>>> def do_interrupt():
|
||||||
|
... raise KeyboardInterrupt()
|
||||||
|
>>> on_interrupt('error')(do_interrupt)()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
SystemExit: 1
|
||||||
|
>>> on_interrupt('error', code=255)(do_interrupt)()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
SystemExit: 255
|
||||||
|
>>> on_interrupt('suppress')(do_interrupt)()
|
||||||
|
>>> with __import__('pytest').raises(KeyboardInterrupt):
|
||||||
|
... on_interrupt('ignore')(do_interrupt)()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, action='error', /, code=1):
|
||||||
|
self.action = action
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exctype, excinst, exctb):
|
||||||
|
if exctype is not KeyboardInterrupt or self.action == 'ignore':
|
||||||
|
return
|
||||||
|
elif self.action == 'error':
|
||||||
|
raise SystemExit(self.code) from excinst
|
||||||
|
return self.action == 'suppress'
|
||||||
BIN
lib/jaraco/context/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jaraco/context/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
0
lib/jaraco/context/py.typed
Normal file
0
lib/jaraco/context/py.typed
Normal file
722
lib/jaraco/functools/__init__.py
Normal file
722
lib/jaraco/functools/__init__.py
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections.abc
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import itertools
|
||||||
|
import operator
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
import warnings
|
||||||
|
from typing import Callable, TypeVar
|
||||||
|
|
||||||
|
import more_itertools
|
||||||
|
|
||||||
|
|
||||||
|
def compose(*funcs):
|
||||||
|
"""
|
||||||
|
Compose any number of unary functions into a single unary function.
|
||||||
|
|
||||||
|
Comparable to
|
||||||
|
`function composition <https://en.wikipedia.org/wiki/Function_composition>`_
|
||||||
|
in mathematics:
|
||||||
|
|
||||||
|
``h = g ∘ f`` implies ``h(x) = g(f(x))``.
|
||||||
|
|
||||||
|
In Python, ``h = compose(g, f)``.
|
||||||
|
|
||||||
|
>>> import textwrap
|
||||||
|
>>> expected = str.strip(textwrap.dedent(compose.__doc__))
|
||||||
|
>>> strip_and_dedent = compose(str.strip, textwrap.dedent)
|
||||||
|
>>> strip_and_dedent(compose.__doc__) == expected
|
||||||
|
True
|
||||||
|
|
||||||
|
Compose also allows the innermost function to take arbitrary arguments.
|
||||||
|
|
||||||
|
>>> round_three = lambda x: round(x, ndigits=3)
|
||||||
|
>>> f = compose(round_three, int.__truediv__)
|
||||||
|
>>> [f(3*x, x+1) for x in range(1,10)]
|
||||||
|
[1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose_two(f1, f2):
|
||||||
|
return lambda *args, **kwargs: f1(f2(*args, **kwargs))
|
||||||
|
|
||||||
|
return functools.reduce(compose_two, funcs)
|
||||||
|
|
||||||
|
|
||||||
|
def once(func):
|
||||||
|
"""
|
||||||
|
Decorate func so it's only ever called the first time.
|
||||||
|
|
||||||
|
This decorator can ensure that an expensive or non-idempotent function
|
||||||
|
will not be expensive on subsequent calls and is idempotent.
|
||||||
|
|
||||||
|
>>> add_three = once(lambda a: a+3)
|
||||||
|
>>> add_three(3)
|
||||||
|
6
|
||||||
|
>>> add_three(9)
|
||||||
|
6
|
||||||
|
>>> add_three('12')
|
||||||
|
6
|
||||||
|
|
||||||
|
To reset the stored value, simply clear the property ``saved_result``.
|
||||||
|
|
||||||
|
>>> del add_three.saved_result
|
||||||
|
>>> add_three(9)
|
||||||
|
12
|
||||||
|
>>> add_three(8)
|
||||||
|
12
|
||||||
|
|
||||||
|
Or invoke 'reset()' on it.
|
||||||
|
|
||||||
|
>>> add_three.reset()
|
||||||
|
>>> add_three(-3)
|
||||||
|
0
|
||||||
|
>>> add_three(0)
|
||||||
|
0
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not hasattr(wrapper, 'saved_result'):
|
||||||
|
wrapper.saved_result = func(*args, **kwargs)
|
||||||
|
return wrapper.saved_result
|
||||||
|
|
||||||
|
wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result')
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def method_cache(method, cache_wrapper=functools.lru_cache()):
|
||||||
|
"""
|
||||||
|
Wrap lru_cache to support storing the cache data in the object instances.
|
||||||
|
|
||||||
|
Abstracts the common paradigm where the method explicitly saves an
|
||||||
|
underscore-prefixed protected property on first call and returns that
|
||||||
|
subsequently.
|
||||||
|
|
||||||
|
>>> class MyClass:
|
||||||
|
... calls = 0
|
||||||
|
...
|
||||||
|
... @method_cache
|
||||||
|
... def method(self, value):
|
||||||
|
... self.calls += 1
|
||||||
|
... return value
|
||||||
|
|
||||||
|
>>> a = MyClass()
|
||||||
|
>>> a.method(3)
|
||||||
|
3
|
||||||
|
>>> for x in range(75):
|
||||||
|
... res = a.method(x)
|
||||||
|
>>> a.calls
|
||||||
|
75
|
||||||
|
|
||||||
|
Note that the apparent behavior will be exactly like that of lru_cache
|
||||||
|
except that the cache is stored on each instance, so values in one
|
||||||
|
instance will not flush values from another, and when an instance is
|
||||||
|
deleted, so are the cached values for that instance.
|
||||||
|
|
||||||
|
>>> b = MyClass()
|
||||||
|
>>> for x in range(35):
|
||||||
|
... res = b.method(x)
|
||||||
|
>>> b.calls
|
||||||
|
35
|
||||||
|
>>> a.method(0)
|
||||||
|
0
|
||||||
|
>>> a.calls
|
||||||
|
75
|
||||||
|
|
||||||
|
Note that if method had been decorated with ``functools.lru_cache()``,
|
||||||
|
a.calls would have been 76 (due to the cached value of 0 having been
|
||||||
|
flushed by the 'b' instance).
|
||||||
|
|
||||||
|
Clear the cache with ``.cache_clear()``
|
||||||
|
|
||||||
|
>>> a.method.cache_clear()
|
||||||
|
|
||||||
|
Same for a method that hasn't yet been called.
|
||||||
|
|
||||||
|
>>> c = MyClass()
|
||||||
|
>>> c.method.cache_clear()
|
||||||
|
|
||||||
|
Another cache wrapper may be supplied:
|
||||||
|
|
||||||
|
>>> cache = functools.lru_cache(maxsize=2)
|
||||||
|
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
|
||||||
|
>>> a = MyClass()
|
||||||
|
>>> a.method2()
|
||||||
|
3
|
||||||
|
|
||||||
|
Caution - do not subsequently wrap the method with another decorator, such
|
||||||
|
as ``@property``, which changes the semantics of the function.
|
||||||
|
|
||||||
|
See also
|
||||||
|
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
|
||||||
|
for another implementation and additional justification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
# it's the first call, replace the method with a cached, bound method
|
||||||
|
bound_method = types.MethodType(method, self)
|
||||||
|
cached_method = cache_wrapper(bound_method)
|
||||||
|
setattr(self, method.__name__, cached_method)
|
||||||
|
return cached_method(*args, **kwargs)
|
||||||
|
|
||||||
|
# Support cache clear even before cache has been created.
|
||||||
|
wrapper.cache_clear = lambda: None
|
||||||
|
|
||||||
|
return _special_method_cache(method, cache_wrapper) or wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _special_method_cache(method, cache_wrapper):
|
||||||
|
"""
|
||||||
|
Because Python treats special methods differently, it's not
|
||||||
|
possible to use instance attributes to implement the cached
|
||||||
|
methods.
|
||||||
|
|
||||||
|
Instead, install the wrapper method under a different name
|
||||||
|
and return a simple proxy to that wrapper.
|
||||||
|
|
||||||
|
https://github.com/jaraco/jaraco.functools/issues/5
|
||||||
|
"""
|
||||||
|
name = method.__name__
|
||||||
|
special_names = '__getattr__', '__getitem__'
|
||||||
|
|
||||||
|
if name not in special_names:
|
||||||
|
return None
|
||||||
|
|
||||||
|
wrapper_name = '__cached' + name
|
||||||
|
|
||||||
|
def proxy(self, /, *args, **kwargs):
|
||||||
|
if wrapper_name not in vars(self):
|
||||||
|
bound = types.MethodType(method, self)
|
||||||
|
cache = cache_wrapper(bound)
|
||||||
|
setattr(self, wrapper_name, cache)
|
||||||
|
else:
|
||||||
|
cache = getattr(self, wrapper_name)
|
||||||
|
return cache(*args, **kwargs)
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
|
||||||
|
|
||||||
|
def apply(transform):
|
||||||
|
"""
|
||||||
|
Decorate a function with a transform function that is
|
||||||
|
invoked on results returned from the decorated function.
|
||||||
|
|
||||||
|
>>> @apply(reversed)
|
||||||
|
... def get_numbers(start):
|
||||||
|
... "doc for get_numbers"
|
||||||
|
... return range(start, start+3)
|
||||||
|
>>> list(get_numbers(4))
|
||||||
|
[6, 5, 4]
|
||||||
|
>>> get_numbers.__doc__
|
||||||
|
'doc for get_numbers'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrap(func):
|
||||||
|
return functools.wraps(func)(compose(transform, func))
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def result_invoke(action):
|
||||||
|
r"""
|
||||||
|
Decorate a function with an action function that is
|
||||||
|
invoked on the results returned from the decorated
|
||||||
|
function (for its side effect), then return the original
|
||||||
|
result.
|
||||||
|
|
||||||
|
>>> @result_invoke(print)
|
||||||
|
... def add_two(a, b):
|
||||||
|
... return a + b
|
||||||
|
>>> x = add_two(2, 3)
|
||||||
|
5
|
||||||
|
>>> x
|
||||||
|
5
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrap(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
action(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(f, /, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Call a function for its side effect after initialization.
|
||||||
|
|
||||||
|
The benefit of using the decorator instead of simply invoking a function
|
||||||
|
after defining it is that it makes explicit the author's intent for the
|
||||||
|
function to be called immediately. Whereas if one simply calls the
|
||||||
|
function immediately, it's less obvious if that was intentional or
|
||||||
|
incidental. It also avoids repeating the name - the two actions, defining
|
||||||
|
the function and calling it immediately are modeled separately, but linked
|
||||||
|
by the decorator construct.
|
||||||
|
|
||||||
|
The benefit of having a function construct (opposed to just invoking some
|
||||||
|
behavior inline) is to serve as a scope in which the behavior occurs. It
|
||||||
|
avoids polluting the global namespace with local variables, provides an
|
||||||
|
anchor on which to attach documentation (docstring), keeps the behavior
|
||||||
|
logically separated (instead of conceptually separated or not separated at
|
||||||
|
all), and provides potential to re-use the behavior for testing or other
|
||||||
|
purposes.
|
||||||
|
|
||||||
|
This function is named as a pithy way to communicate, "call this function
|
||||||
|
primarily for its side effect", or "while defining this function, also
|
||||||
|
take it aside and call it". It exists because there's no Python construct
|
||||||
|
for "define and call" (nor should there be, as decorators serve this need
|
||||||
|
just fine). The behavior happens immediately and synchronously.
|
||||||
|
|
||||||
|
>>> @invoke
|
||||||
|
... def func(): print("called")
|
||||||
|
called
|
||||||
|
>>> func()
|
||||||
|
called
|
||||||
|
|
||||||
|
Use functools.partial to pass parameters to the initial call
|
||||||
|
|
||||||
|
>>> @functools.partial(invoke, name='bingo')
|
||||||
|
... def func(name): print('called with', name)
|
||||||
|
called with bingo
|
||||||
|
"""
|
||||||
|
f(*args, **kwargs)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
|
||||||
|
def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]:
|
||||||
|
"""
|
||||||
|
Wrap the function to always return the first parameter.
|
||||||
|
|
||||||
|
>>> passthrough(print)('3')
|
||||||
|
3
|
||||||
|
'3'
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(first: _T, *args, **kwargs) -> _T:
|
||||||
|
func(first, *args, **kwargs)
|
||||||
|
return first
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Throttler:
|
||||||
|
"""Rate-limit a function (or other callable)."""
|
||||||
|
|
||||||
|
def __init__(self, func, max_rate=float('Inf')):
|
||||||
|
if isinstance(func, Throttler):
|
||||||
|
func = func.func
|
||||||
|
self.func = func
|
||||||
|
self.max_rate = max_rate
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.last_called = 0
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
self._wait()
|
||||||
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
|
def _wait(self):
|
||||||
|
"""Ensure at least 1/max_rate seconds from last call."""
|
||||||
|
elapsed = time.time() - self.last_called
|
||||||
|
must_wait = 1 / self.max_rate - elapsed
|
||||||
|
time.sleep(max(0, must_wait))
|
||||||
|
self.last_called = time.time()
|
||||||
|
|
||||||
|
def __get__(self, obj, owner=None):
|
||||||
|
return first_invoke(self._wait, functools.partial(self.func, obj))
|
||||||
|
|
||||||
|
|
||||||
|
def first_invoke(func1, func2):
|
||||||
|
"""
|
||||||
|
Return a function that when invoked will invoke func1 without
|
||||||
|
any parameters (for its side effect) and then invoke func2
|
||||||
|
with whatever parameters were passed, returning its result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func1()
|
||||||
|
return func2(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
method_caller = first_invoke(
|
||||||
|
lambda: warnings.warn(
|
||||||
|
'`jaraco.functools.method_caller` is deprecated, '
|
||||||
|
'use `operator.methodcaller` instead',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
),
|
||||||
|
operator.methodcaller,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def retry_call(func, cleanup=lambda: None, retries=0, trap=()):
|
||||||
|
"""
|
||||||
|
Given a callable func, trap the indicated exceptions
|
||||||
|
for up to 'retries' times, invoking cleanup on the
|
||||||
|
exception. On the final attempt, allow any exceptions
|
||||||
|
to propagate.
|
||||||
|
"""
|
||||||
|
attempts = itertools.count() if retries == float('inf') else range(retries)
|
||||||
|
for _ in attempts:
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except trap:
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
return func()
|
||||||
|
|
||||||
|
|
||||||
|
def retry(*r_args, **r_kwargs):
|
||||||
|
"""
|
||||||
|
Decorator wrapper for retry_call. Accepts arguments to retry_call
|
||||||
|
except func and then returns a decorator for the decorated function.
|
||||||
|
|
||||||
|
Ex:
|
||||||
|
|
||||||
|
>>> @retry(retries=3)
|
||||||
|
... def my_func(a, b):
|
||||||
|
... "this is my funk"
|
||||||
|
... print(a, b)
|
||||||
|
>>> my_func.__doc__
|
||||||
|
'this is my funk'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorate(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*f_args, **f_kwargs):
|
||||||
|
bound = functools.partial(func, *f_args, **f_kwargs)
|
||||||
|
return retry_call(bound, *r_args, **r_kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def print_yielded(func):
|
||||||
|
"""
|
||||||
|
Convert a generator into a function that prints all yielded elements.
|
||||||
|
|
||||||
|
>>> @print_yielded
|
||||||
|
... def x():
|
||||||
|
... yield 3; yield None
|
||||||
|
>>> x()
|
||||||
|
3
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
print_all = functools.partial(map, print)
|
||||||
|
print_results = compose(more_itertools.consume, print_all, func)
|
||||||
|
return functools.wraps(func)(print_results)
|
||||||
|
|
||||||
|
|
||||||
|
def pass_none(func):
|
||||||
|
"""
|
||||||
|
Wrap func so it's not called if its first param is None.
|
||||||
|
|
||||||
|
>>> print_text = pass_none(print)
|
||||||
|
>>> print_text('text')
|
||||||
|
text
|
||||||
|
>>> print_text(None)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(param, /, *args, **kwargs):
|
||||||
|
if param is not None:
|
||||||
|
return func(param, *args, **kwargs)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def none_as(value, replacement=None):
|
||||||
|
"""
|
||||||
|
>>> none_as(None, 'foo')
|
||||||
|
'foo'
|
||||||
|
>>> none_as('bar', 'foo')
|
||||||
|
'bar'
|
||||||
|
"""
|
||||||
|
return replacement if value is None else value
|
||||||
|
|
||||||
|
|
||||||
|
def assign_params(func, namespace):
|
||||||
|
"""
|
||||||
|
Assign parameters from namespace where func solicits.
|
||||||
|
|
||||||
|
>>> def func(x, y=3):
|
||||||
|
... print(x, y)
|
||||||
|
>>> assigned = assign_params(func, dict(x=2, z=4))
|
||||||
|
>>> assigned()
|
||||||
|
2 3
|
||||||
|
|
||||||
|
The usual errors are raised if a function doesn't receive
|
||||||
|
its required parameters:
|
||||||
|
|
||||||
|
>>> assigned = assign_params(func, dict(y=3, z=4))
|
||||||
|
>>> assigned()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
TypeError: func() ...argument...
|
||||||
|
|
||||||
|
It even works on methods:
|
||||||
|
|
||||||
|
>>> class Handler:
|
||||||
|
... def meth(self, arg):
|
||||||
|
... print(arg)
|
||||||
|
>>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))()
|
||||||
|
crystal
|
||||||
|
"""
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
params = sig.parameters.keys()
|
||||||
|
call_ns = {k: namespace[k] for k in params if k in namespace}
|
||||||
|
return functools.partial(func, **call_ns)
|
||||||
|
|
||||||
|
|
||||||
|
def save_method_args(method):
|
||||||
|
"""
|
||||||
|
Wrap a method such that when it is called, the args and kwargs are
|
||||||
|
saved on the method.
|
||||||
|
|
||||||
|
>>> class MyClass:
|
||||||
|
... @save_method_args
|
||||||
|
... def method(self, a, b):
|
||||||
|
... print(a, b)
|
||||||
|
>>> my_ob = MyClass()
|
||||||
|
>>> my_ob.method(1, 2)
|
||||||
|
1 2
|
||||||
|
>>> my_ob._saved_method.args
|
||||||
|
(1, 2)
|
||||||
|
>>> my_ob._saved_method.kwargs
|
||||||
|
{}
|
||||||
|
>>> my_ob.method(a=3, b='foo')
|
||||||
|
3 foo
|
||||||
|
>>> my_ob._saved_method.args
|
||||||
|
()
|
||||||
|
>>> my_ob._saved_method.kwargs == dict(a=3, b='foo')
|
||||||
|
True
|
||||||
|
|
||||||
|
The arguments are stored on the instance, allowing for
|
||||||
|
different instance to save different args.
|
||||||
|
|
||||||
|
>>> your_ob = MyClass()
|
||||||
|
>>> your_ob.method({str('x'): 3}, b=[4])
|
||||||
|
{'x': 3} [4]
|
||||||
|
>>> your_ob._saved_method.args
|
||||||
|
({'x': 3},)
|
||||||
|
>>> my_ob._saved_method.args
|
||||||
|
()
|
||||||
|
"""
|
||||||
|
args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') # noqa: PYI024 # Internal; stubs used for typing
|
||||||
|
|
||||||
|
@functools.wraps(method)
|
||||||
|
def wrapper(self, /, *args, **kwargs):
|
||||||
|
attr_name = '_saved_' + method.__name__
|
||||||
|
attr = args_and_kwargs(args, kwargs)
|
||||||
|
setattr(self, attr_name, attr)
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def except_(*exceptions, replace=None, use=None):
|
||||||
|
"""
|
||||||
|
Replace the indicated exceptions, if raised, with the indicated
|
||||||
|
literal replacement or evaluated expression (if present).
|
||||||
|
|
||||||
|
>>> safe_int = except_(ValueError)(int)
|
||||||
|
>>> safe_int('five')
|
||||||
|
>>> safe_int('5')
|
||||||
|
5
|
||||||
|
|
||||||
|
Specify a literal replacement with ``replace``.
|
||||||
|
|
||||||
|
>>> safe_int_r = except_(ValueError, replace=0)(int)
|
||||||
|
>>> safe_int_r('five')
|
||||||
|
0
|
||||||
|
|
||||||
|
Provide an expression to ``use`` to pass through particular parameters.
|
||||||
|
|
||||||
|
>>> safe_int_pt = except_(ValueError, use='args[0]')(int)
|
||||||
|
>>> safe_int_pt('five')
|
||||||
|
'five'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorate(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except exceptions:
|
||||||
|
try:
|
||||||
|
return eval(use)
|
||||||
|
except TypeError:
|
||||||
|
return replace
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def identity(x):
|
||||||
|
"""
|
||||||
|
Return the argument.
|
||||||
|
|
||||||
|
>>> o = object()
|
||||||
|
>>> identity(o) is o
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def bypass_when(check, *, _op=identity):
|
||||||
|
"""
|
||||||
|
Decorate a function to return its parameter when ``check``.
|
||||||
|
|
||||||
|
>>> bypassed = [] # False
|
||||||
|
|
||||||
|
>>> @bypass_when(bypassed)
|
||||||
|
... def double(x):
|
||||||
|
... return x * 2
|
||||||
|
>>> double(2)
|
||||||
|
4
|
||||||
|
>>> bypassed[:] = [object()] # True
|
||||||
|
>>> double(2)
|
||||||
|
2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorate(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(param, /):
|
||||||
|
return param if _op(check) else func(param)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def bypass_unless(check):
|
||||||
|
"""
|
||||||
|
Decorate a function to return its parameter unless ``check``.
|
||||||
|
|
||||||
|
>>> enabled = [object()] # True
|
||||||
|
|
||||||
|
>>> @bypass_unless(enabled)
|
||||||
|
... def double(x):
|
||||||
|
... return x * 2
|
||||||
|
>>> double(2)
|
||||||
|
4
|
||||||
|
>>> del enabled[:] # False
|
||||||
|
>>> double(2)
|
||||||
|
2
|
||||||
|
"""
|
||||||
|
return bypass_when(check, _op=operator.not_)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.singledispatch
|
||||||
|
def _splat_inner(args, func):
|
||||||
|
"""Splat args to func."""
|
||||||
|
return func(*args)
|
||||||
|
|
||||||
|
|
||||||
|
@_splat_inner.register
|
||||||
|
def _(args: collections.abc.Mapping, func):
|
||||||
|
"""Splat kargs to func as kwargs."""
|
||||||
|
return func(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def splat(func):
|
||||||
|
"""
|
||||||
|
Wrap func to expect its parameters to be passed positionally in a tuple.
|
||||||
|
|
||||||
|
Has a similar effect to that of ``itertools.starmap`` over
|
||||||
|
simple ``map``.
|
||||||
|
|
||||||
|
>>> pairs = [(-1, 1), (0, 2)]
|
||||||
|
>>> more_itertools.consume(itertools.starmap(print, pairs))
|
||||||
|
-1 1
|
||||||
|
0 2
|
||||||
|
>>> more_itertools.consume(map(splat(print), pairs))
|
||||||
|
-1 1
|
||||||
|
0 2
|
||||||
|
|
||||||
|
The approach generalizes to other iterators that don't have a "star"
|
||||||
|
equivalent, such as a "starfilter".
|
||||||
|
|
||||||
|
>>> list(filter(splat(operator.add), pairs))
|
||||||
|
[(0, 2)]
|
||||||
|
|
||||||
|
Splat also accepts a mapping argument.
|
||||||
|
|
||||||
|
>>> def is_nice(msg, code):
|
||||||
|
... return "smile" in msg or code == 0
|
||||||
|
>>> msgs = [
|
||||||
|
... dict(msg='smile!', code=20),
|
||||||
|
... dict(msg='error :(', code=1),
|
||||||
|
... dict(msg='unknown', code=0),
|
||||||
|
... ]
|
||||||
|
>>> for msg in filter(splat(is_nice), msgs):
|
||||||
|
... print(msg)
|
||||||
|
{'msg': 'smile!', 'code': 20}
|
||||||
|
{'msg': 'unknown', 'code': 0}
|
||||||
|
"""
|
||||||
|
return functools.wraps(func)(functools.partial(_splat_inner, func=func))
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
|
||||||
|
def chainable(method: Callable[[_T, ...], None]) -> Callable[[_T, ...], _T]:
|
||||||
|
"""
|
||||||
|
Wrap an instance method to always return self.
|
||||||
|
|
||||||
|
|
||||||
|
>>> class Dingus:
|
||||||
|
... @chainable
|
||||||
|
... def set_attr(self, name, val):
|
||||||
|
... setattr(self, name, val)
|
||||||
|
>>> d = Dingus().set_attr('a', 'eh!')
|
||||||
|
>>> d.a
|
||||||
|
'eh!'
|
||||||
|
>>> d2 = Dingus().set_attr('a', 'eh!').set_attr('b', 'bee!')
|
||||||
|
>>> d2.a + d2.b
|
||||||
|
'eh!bee!'
|
||||||
|
|
||||||
|
Enforces that the return value is null.
|
||||||
|
|
||||||
|
>>> class BorkedDingus:
|
||||||
|
... @chainable
|
||||||
|
... def set_attr(self, name, val):
|
||||||
|
... setattr(self, name, val)
|
||||||
|
... return len(name)
|
||||||
|
>>> BorkedDingus().set_attr('a', 'eh!')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(method)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
assert method(self, *args, **kwargs) is None
|
||||||
|
return self
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def noop(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
A no-operation function that does nothing.
|
||||||
|
|
||||||
|
>>> noop(1, 2, three=3)
|
||||||
|
"""
|
||||||
123
lib/jaraco/functools/__init__.pyi
Normal file
123
lib/jaraco/functools/__init__.pyi
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
from collections.abc import Callable, Hashable, Iterator
|
||||||
|
from functools import partial
|
||||||
|
from operator import methodcaller
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Generic,
|
||||||
|
Protocol,
|
||||||
|
TypeVar,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
|
from typing_extensions import Concatenate, ParamSpec, TypeVarTuple, Unpack
|
||||||
|
|
||||||
|
_P = ParamSpec('_P')
|
||||||
|
_R = TypeVar('_R')
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
_Ts = TypeVarTuple('_Ts')
|
||||||
|
_R1 = TypeVar('_R1')
|
||||||
|
_R2 = TypeVar('_R2')
|
||||||
|
_V = TypeVar('_V')
|
||||||
|
_S = TypeVar('_S')
|
||||||
|
_R_co = TypeVar('_R_co', covariant=True)
|
||||||
|
|
||||||
|
class _OnceCallable(Protocol[_P, _R]):
|
||||||
|
saved_result: _R
|
||||||
|
reset: Callable[[], None]
|
||||||
|
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
||||||
|
|
||||||
|
class _ProxyMethodCacheWrapper(Protocol[_R_co]):
|
||||||
|
cache_clear: Callable[[], None]
|
||||||
|
def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ...
|
||||||
|
|
||||||
|
class _MethodCacheWrapper(Protocol[_R_co]):
|
||||||
|
def cache_clear(self) -> None: ...
|
||||||
|
def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ...
|
||||||
|
|
||||||
|
# `compose()` overloads below will cover most use cases.
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def compose(
|
||||||
|
__func1: Callable[[_R], _T],
|
||||||
|
__func2: Callable[_P, _R],
|
||||||
|
/,
|
||||||
|
) -> Callable[_P, _T]: ...
|
||||||
|
@overload
|
||||||
|
def compose(
|
||||||
|
__func1: Callable[[_R], _T],
|
||||||
|
__func2: Callable[[_R1], _R],
|
||||||
|
__func3: Callable[_P, _R1],
|
||||||
|
/,
|
||||||
|
) -> Callable[_P, _T]: ...
|
||||||
|
@overload
|
||||||
|
def compose(
|
||||||
|
__func1: Callable[[_R], _T],
|
||||||
|
__func2: Callable[[_R2], _R],
|
||||||
|
__func3: Callable[[_R1], _R2],
|
||||||
|
__func4: Callable[_P, _R1],
|
||||||
|
/,
|
||||||
|
) -> Callable[_P, _T]: ...
|
||||||
|
def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ...
|
||||||
|
def method_cache(
|
||||||
|
method: Callable[..., _R],
|
||||||
|
cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ...,
|
||||||
|
) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ...
|
||||||
|
def apply(
|
||||||
|
transform: Callable[[_R], _T],
|
||||||
|
) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ...
|
||||||
|
def result_invoke(
|
||||||
|
action: Callable[[_R], Any],
|
||||||
|
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...
|
||||||
|
def invoke(
|
||||||
|
f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs
|
||||||
|
) -> Callable[_P, _R]: ...
|
||||||
|
|
||||||
|
class Throttler(Generic[_R]):
|
||||||
|
last_called: float
|
||||||
|
func: Callable[..., _R]
|
||||||
|
max_rate: float
|
||||||
|
def __init__(
|
||||||
|
self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ...
|
||||||
|
) -> None: ...
|
||||||
|
def reset(self) -> None: ...
|
||||||
|
def __call__(self, *args: Any, **kwargs: Any) -> _R: ...
|
||||||
|
def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ...
|
||||||
|
|
||||||
|
def first_invoke(
|
||||||
|
func1: Callable[..., Any], func2: Callable[_P, _R]
|
||||||
|
) -> Callable[_P, _R]: ...
|
||||||
|
|
||||||
|
method_caller: Callable[..., methodcaller]
|
||||||
|
|
||||||
|
def retry_call(
|
||||||
|
func: Callable[..., _R],
|
||||||
|
cleanup: Callable[..., None] = ...,
|
||||||
|
retries: float = ...,
|
||||||
|
trap: type[BaseException] | tuple[type[BaseException], ...] = ...,
|
||||||
|
) -> _R: ...
|
||||||
|
def retry(
|
||||||
|
cleanup: Callable[..., None] = ...,
|
||||||
|
retries: float = ...,
|
||||||
|
trap: type[BaseException] | tuple[type[BaseException], ...] = ...,
|
||||||
|
) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ...
|
||||||
|
def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ...
|
||||||
|
def pass_none(
|
||||||
|
func: Callable[Concatenate[_T, _P], _R],
|
||||||
|
) -> Callable[Concatenate[_T, _P], _R]: ...
|
||||||
|
def assign_params(
|
||||||
|
func: Callable[..., _R], namespace: dict[str, Any]
|
||||||
|
) -> partial[_R]: ...
|
||||||
|
def save_method_args(
|
||||||
|
method: Callable[Concatenate[_S, _P], _R],
|
||||||
|
) -> Callable[Concatenate[_S, _P], _R]: ...
|
||||||
|
def except_(
|
||||||
|
*exceptions: type[BaseException], replace: Any = ..., use: Any = ...
|
||||||
|
) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ...
|
||||||
|
def identity(x: _T) -> _T: ...
|
||||||
|
def bypass_when(
|
||||||
|
check: _V, *, _op: Callable[[_V], Any] = ...
|
||||||
|
) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ...
|
||||||
|
def bypass_unless(
|
||||||
|
check: Any,
|
||||||
|
) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ...
|
||||||
|
def splat(func: Callable[[Unpack[_Ts]], _R]) -> Callable[[tuple[Unpack[_Ts]]], _R]: ...
|
||||||
BIN
lib/jaraco/functools/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jaraco/functools/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
0
lib/jaraco/functools/py.typed
Normal file
0
lib/jaraco/functools/py.typed
Normal file
1
lib/jaraco_context-6.1.0.dist-info/INSTALLER
Normal file
1
lib/jaraco_context-6.1.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
82
lib/jaraco_context-6.1.0.dist-info/METADATA
Normal file
82
lib/jaraco_context-6.1.0.dist-info/METADATA
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: jaraco.context
|
||||||
|
Version: 6.1.0
|
||||||
|
Summary: Useful decorators and context managers
|
||||||
|
Author-email: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||||
|
License-Expression: MIT
|
||||||
|
Project-URL: Source, https://github.com/jaraco/jaraco.context
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: backports.tarfile; python_version < "3.12"
|
||||||
|
Provides-Extra: test
|
||||||
|
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
|
||||||
|
Requires-Dist: jaraco.test>=5.6.0; extra == "test"
|
||||||
|
Requires-Dist: portend; extra == "test"
|
||||||
|
Provides-Extra: doc
|
||||||
|
Requires-Dist: sphinx>=3.5; extra == "doc"
|
||||||
|
Requires-Dist: jaraco.packaging>=9.3; extra == "doc"
|
||||||
|
Requires-Dist: rst.linker>=1.9; extra == "doc"
|
||||||
|
Requires-Dist: furo; extra == "doc"
|
||||||
|
Requires-Dist: sphinx-lint; extra == "doc"
|
||||||
|
Requires-Dist: jaraco.tidelift>=1.4; extra == "doc"
|
||||||
|
Provides-Extra: check
|
||||||
|
Requires-Dist: pytest-checkdocs>=2.4; extra == "check"
|
||||||
|
Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check"
|
||||||
|
Provides-Extra: cover
|
||||||
|
Requires-Dist: pytest-cov; extra == "cover"
|
||||||
|
Provides-Extra: enabler
|
||||||
|
Requires-Dist: pytest-enabler>=3.4; extra == "enabler"
|
||||||
|
Provides-Extra: type
|
||||||
|
Requires-Dist: pytest-mypy>=1.0.1; extra == "type"
|
||||||
|
Requires-Dist: mypy<1.19; platform_python_implementation == "PyPy" and extra == "type"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/jaraco.context.svg
|
||||||
|
:target: https://pypi.org/project/jaraco.context
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg
|
||||||
|
|
||||||
|
.. image:: https://github.com/jaraco/jaraco.context/actions/workflows/main.yml/badge.svg
|
||||||
|
:target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22
|
||||||
|
:alt: tests
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
||||||
|
:target: https://github.com/astral-sh/ruff
|
||||||
|
:alt: Ruff
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest
|
||||||
|
:target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/skeleton-2025-informational
|
||||||
|
:target: https://blog.jaraco.com/skeleton
|
||||||
|
|
||||||
|
.. image:: https://tidelift.com/badges/package/pypi/jaraco.context
|
||||||
|
:target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme
|
||||||
|
|
||||||
|
|
||||||
|
Highlights
|
||||||
|
==========
|
||||||
|
|
||||||
|
See the docs linked from the badge above for the full details, but here are some features that may be of interest.
|
||||||
|
|
||||||
|
- ``ExceptionTrap`` provides a general-purpose wrapper for trapping exceptions and then acting on the outcome. Includes ``passes`` and ``raises`` decorators to replace the result of a wrapped function by a boolean indicating the outcome of the exception trap. See `this keyring commit <https://github.com/jaraco/keyring/commit/a85a7cbc6c909f8121660ed1f7b487f99a1c2bf7>`_ for an example of it in production.
|
||||||
|
- ``suppress`` simply enables ``contextlib.suppress`` as a decorator.
|
||||||
|
- ``on_interrupt`` is a decorator used by CLI entry points to affect the handling of a ``KeyboardInterrupt``. Inspired by `Lucretiel/autocommand#18 <https://github.com/Lucretiel/autocommand/issues/18>`_.
|
||||||
|
- ``pushd`` is similar to pytest's ``monkeypatch.chdir`` or path's `default context <https://path.readthedocs.io/en/latest/api.html>`_, changes the current working directory for the duration of the context.
|
||||||
|
- ``tarball`` will download a tarball, extract it, change directory, yield, then clean up after. Convenient when working with web assets.
|
||||||
|
- ``null`` is there for those times when one code branch needs a context and the other doesn't; this null context provides symmetry across those branches.
|
||||||
|
|
||||||
|
|
||||||
|
For Enterprise
|
||||||
|
==============
|
||||||
|
|
||||||
|
Available as part of the Tidelift Subscription.
|
||||||
|
|
||||||
|
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
|
||||||
|
|
||||||
|
`Learn more <https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=referral&utm_campaign=github>`_.
|
||||||
9
lib/jaraco_context-6.1.0.dist-info/RECORD
Normal file
9
lib/jaraco_context-6.1.0.dist-info/RECORD
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
jaraco/context/__init__.py,sha256=br1ydYGo1Xr_Pu1anuEdd-QrjUiz_EY5L_5E4C03L4w,9809
|
||||||
|
jaraco/context/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jaraco/context/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jaraco_context-6.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
jaraco_context-6.1.0.dist-info/METADATA,sha256=BDXr_FIFXFqZdO0gwXG2RUOD6vnbsVCIFLp62XxZ1xI,4270
|
||||||
|
jaraco_context-6.1.0.dist-info/RECORD,,
|
||||||
|
jaraco_context-6.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
||||||
|
jaraco_context-6.1.0.dist-info/licenses/LICENSE,sha256=l1WhhRlmbl8PTK49qtPXASvK5IpgCzEjfXXp_hNOZoM,1076
|
||||||
|
jaraco_context-6.1.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
||||||
5
lib/jaraco_context-6.1.0.dist-info/WHEEL
Normal file
5
lib/jaraco_context-6.1.0.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (80.9.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
18
lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE
Normal file
18
lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
1
lib/jaraco_context-6.1.0.dist-info/top_level.txt
Normal file
1
lib/jaraco_context-6.1.0.dist-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jaraco
|
||||||
1
lib/jaraco_functools-4.4.0.dist-info/INSTALLER
Normal file
1
lib/jaraco_functools-4.4.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
69
lib/jaraco_functools-4.4.0.dist-info/METADATA
Normal file
69
lib/jaraco_functools-4.4.0.dist-info/METADATA
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: jaraco.functools
|
||||||
|
Version: 4.4.0
|
||||||
|
Summary: Functools like those found in stdlib
|
||||||
|
Author-email: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||||
|
License-Expression: MIT
|
||||||
|
Project-URL: Source, https://github.com/jaraco/jaraco.functools
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: more_itertools
|
||||||
|
Provides-Extra: test
|
||||||
|
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
|
||||||
|
Requires-Dist: jaraco.classes; extra == "test"
|
||||||
|
Provides-Extra: doc
|
||||||
|
Requires-Dist: sphinx>=3.5; extra == "doc"
|
||||||
|
Requires-Dist: jaraco.packaging>=9.3; extra == "doc"
|
||||||
|
Requires-Dist: rst.linker>=1.9; extra == "doc"
|
||||||
|
Requires-Dist: furo; extra == "doc"
|
||||||
|
Requires-Dist: sphinx-lint; extra == "doc"
|
||||||
|
Requires-Dist: jaraco.tidelift>=1.4; extra == "doc"
|
||||||
|
Provides-Extra: check
|
||||||
|
Requires-Dist: pytest-checkdocs>=2.4; extra == "check"
|
||||||
|
Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check"
|
||||||
|
Provides-Extra: cover
|
||||||
|
Requires-Dist: pytest-cov; extra == "cover"
|
||||||
|
Provides-Extra: enabler
|
||||||
|
Requires-Dist: pytest-enabler>=3.4; extra == "enabler"
|
||||||
|
Provides-Extra: type
|
||||||
|
Requires-Dist: pytest-mypy>=1.0.1; extra == "type"
|
||||||
|
Requires-Dist: mypy<1.19; platform_python_implementation == "PyPy" and extra == "type"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/jaraco.functools.svg
|
||||||
|
:target: https://pypi.org/project/jaraco.functools
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/jaraco.functools.svg
|
||||||
|
|
||||||
|
.. image:: https://github.com/jaraco/jaraco.functools/actions/workflows/main.yml/badge.svg
|
||||||
|
:target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22
|
||||||
|
:alt: tests
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
||||||
|
:target: https://github.com/astral-sh/ruff
|
||||||
|
:alt: Ruff
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/jaracofunctools/badge/?version=latest
|
||||||
|
:target: https://jaracofunctools.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/skeleton-2025-informational
|
||||||
|
:target: https://blog.jaraco.com/skeleton
|
||||||
|
|
||||||
|
.. image:: https://tidelift.com/badges/package/pypi/jaraco.functools
|
||||||
|
:target: https://tidelift.com/subscription/pkg/pypi-jaraco.functools?utm_source=pypi-jaraco.functools&utm_medium=readme
|
||||||
|
|
||||||
|
Additional functools in the spirit of stdlib's functools.
|
||||||
|
|
||||||
|
For Enterprise
|
||||||
|
==============
|
||||||
|
|
||||||
|
Available as part of the Tidelift Subscription.
|
||||||
|
|
||||||
|
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
|
||||||
|
|
||||||
|
`Learn more <https://tidelift.com/subscription/pkg/pypi-jaraco.functools?utm_source=pypi-jaraco.functools&utm_medium=referral&utm_campaign=github>`_.
|
||||||
10
lib/jaraco_functools-4.4.0.dist-info/RECORD
Normal file
10
lib/jaraco_functools-4.4.0.dist-info/RECORD
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
jaraco/functools/__init__.py,sha256=ZJx9cMs2Nvk2xGUl8OjVGkpjdOaNlSzJrN4dGglgX2g,18599
|
||||||
|
jaraco/functools/__init__.pyi,sha256=K4DcbnYIHE5QlMxqf9-cVp-WhycrhuTao4J7O7TMq4Y,3907
|
||||||
|
jaraco/functools/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jaraco_functools-4.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
jaraco_functools-4.4.0.dist-info/METADATA,sha256=LnnajcNGmSSr46yLIqP-tWkqeb-fR7vIa2U11hhkGEk,2960
|
||||||
|
jaraco_functools-4.4.0.dist-info/RECORD,,
|
||||||
|
jaraco_functools-4.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
||||||
|
jaraco_functools-4.4.0.dist-info/licenses/LICENSE,sha256=WlfLTbheKi3YjCkGKJCK3VfjRRRJ4KmnH9-zh3b9dZ0,1076
|
||||||
|
jaraco_functools-4.4.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
||||||
5
lib/jaraco_functools-4.4.0.dist-info/WHEEL
Normal file
5
lib/jaraco_functools-4.4.0.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (80.9.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
18
lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE
Normal file
18
lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
1
lib/jaraco_functools-4.4.0.dist-info/top_level.txt
Normal file
1
lib/jaraco_functools-4.4.0.dist-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jaraco
|
||||||
1
lib/jeepney-0.9.0.dist-info/INSTALLER
Normal file
1
lib/jeepney-0.9.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
35
lib/jeepney-0.9.0.dist-info/METADATA
Normal file
35
lib/jeepney-0.9.0.dist-info/METADATA
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: jeepney
|
||||||
|
Version: 0.9.0
|
||||||
|
Summary: Low-level, pure Python DBus protocol wrapper.
|
||||||
|
Author-email: Thomas Kluyver <thomas@kluyver.me.uk>
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-Expression: MIT
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Topic :: Desktop Environment
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: pytest ; extra == "test"
|
||||||
|
Requires-Dist: pytest-trio ; extra == "test"
|
||||||
|
Requires-Dist: pytest-asyncio >=0.17 ; extra == "test"
|
||||||
|
Requires-Dist: testpath ; extra == "test"
|
||||||
|
Requires-Dist: trio ; extra == "test"
|
||||||
|
Requires-Dist: async-timeout ; extra == "test" and ( python_version < '3.11')
|
||||||
|
Requires-Dist: trio ; extra == "trio"
|
||||||
|
Project-URL: Documentation, https://jeepney.readthedocs.io/en/latest/
|
||||||
|
Project-URL: Source, https://gitlab.com/takluyver/jeepney
|
||||||
|
Provides-Extra: test
|
||||||
|
Provides-Extra: trio
|
||||||
|
|
||||||
|
Jeepney is a pure Python implementation of D-Bus messaging. It has an `I/O-free
|
||||||
|
<https://sans-io.readthedocs.io/>`__ core, and integration modules for different
|
||||||
|
event loops.
|
||||||
|
|
||||||
|
D-Bus is an inter-process communication system, mainly used in Linux.
|
||||||
|
|
||||||
|
To install Jeepney::
|
||||||
|
|
||||||
|
pip install jeepney
|
||||||
|
|
||||||
|
`Jeepney docs on Readthedocs <https://jeepney.readthedocs.io/en/latest/>`__
|
||||||
|
|
||||||
64
lib/jeepney-0.9.0.dist-info/RECORD
Normal file
64
lib/jeepney-0.9.0.dist-info/RECORD
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
jeepney-0.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
jeepney-0.9.0.dist-info/METADATA,sha256=uObDU-mq7Q7QFEApVWQX_aI7ZPNE5xgzGQcFS6BbjGI,1230
|
||||||
|
jeepney-0.9.0.dist-info/RECORD,,
|
||||||
|
jeepney-0.9.0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
|
||||||
|
jeepney-0.9.0.dist-info/licenses/LICENSE,sha256=GyKwSbUmfW38I6Z79KhNjsBLn9-xpR02DkK0NCyLQVQ,1081
|
||||||
|
jeepney/__init__.py,sha256=ULhIr444tY81PUkEHRWNlTXlqRbFmqMTbWBsktKgmoI,408
|
||||||
|
jeepney/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/auth.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/bindgen.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/bus.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/bus_messages.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/fds.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/low_level.cpython-314.pyc,,
|
||||||
|
jeepney/__pycache__/wrappers.cpython-314.pyc,,
|
||||||
|
jeepney/auth.py,sha256=ZW0HMX6Vfwx28P-jNrzVVgEn1ipjO-KJrNJ2SG90V3U,5409
|
||||||
|
jeepney/bindgen.py,sha256=yPDJFt_WjKoFUp08r-_upsqu0L8Rmv8gNKr-MA4T4bI,6085
|
||||||
|
jeepney/bus.py,sha256=KUiSr3ECzdbe-S9tNKm6kvf3oZi4RYnJWkZUXK7tE2k,1817
|
||||||
|
jeepney/bus_messages.py,sha256=uUCc_1Xllzth4F95aghpDLmlv5Gz0are2FpKg7D_gqc,8239
|
||||||
|
jeepney/fds.py,sha256=ZYzN_c_7rkBT0wU7dYUmQRijpSzCv-DATCYEklpXxUU,5056
|
||||||
|
jeepney/io/__init__.py,sha256=inJI_1U-ATymLcAVYs-LD2aUwgl-tihW8-oVFUxYRgA,33
|
||||||
|
jeepney/io/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jeepney/io/__pycache__/asyncio.cpython-314.pyc,,
|
||||||
|
jeepney/io/__pycache__/blocking.cpython-314.pyc,,
|
||||||
|
jeepney/io/__pycache__/common.cpython-314.pyc,,
|
||||||
|
jeepney/io/__pycache__/threading.cpython-314.pyc,,
|
||||||
|
jeepney/io/__pycache__/trio.cpython-314.pyc,,
|
||||||
|
jeepney/io/asyncio.py,sha256=qfWi_1pWCXSP1LNRafHBuvrxHx4tX96b52KBa4sUFMc,7622
|
||||||
|
jeepney/io/blocking.py,sha256=I_rw90IY_EesBZmkfUqk7UniyVkQAngz7jyQmzju680,11940
|
||||||
|
jeepney/io/common.py,sha256=l8lbFUgQmBxfqSC-hqHYmPUYCVFMKbOGB1k5ZWPKXfs,2696
|
||||||
|
jeepney/io/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jeepney/io/tests/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/conftest.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/test_trio.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/__pycache__/utils.cpython-314.pyc,,
|
||||||
|
jeepney/io/tests/conftest.py,sha256=o7JrYypYE-0jNFUndsQ4Ek5dNYM0ofh1sYcIVeCZMj0,2730
|
||||||
|
jeepney/io/tests/test_asyncio.py,sha256=JJtnX5HiRRZjjuGIDoI8LvzfbaSNg-ljiX95yUvd9xk,2720
|
||||||
|
jeepney/io/tests/test_blocking.py,sha256=ETLnoivenN8Dzp0JB4wPOb9PNbpSuiocuP_IDeNRlI4,2804
|
||||||
|
jeepney/io/tests/test_threading.py,sha256=RALwy-aI64TBoFmBnSU63HLcwRnStLVtnewOtoaBl3o,2699
|
||||||
|
jeepney/io/tests/test_trio.py,sha256=DPY1V_K2qLTyBTrbrxZeLTA5dmca3Ye3e6pz08UxbO8,3892
|
||||||
|
jeepney/io/tests/utils.py,sha256=i7VJYT-axefzS8mWcvv-9DeHEB6LdP9M82H3Hx6fyC4,79
|
||||||
|
jeepney/io/threading.py,sha256=mwGCNlun_baX8Y4eienCGDKdZD4SKdTMvBTkIE0EMKo,9391
|
||||||
|
jeepney/io/trio.py,sha256=IdZIJnQcPjVOBA9KooFn0nTBEz3BuBDkz56qLYhGR1M,15088
|
||||||
|
jeepney/low_level.py,sha256=m4wGY-quPnzylgKlBdBccmkuOXF_hQ1gbtT25qPX2GM,19949
|
||||||
|
jeepney/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
jeepney/tests/__pycache__/__init__.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_auth.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_bus.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_fds.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_low_level.cpython-314.pyc,,
|
||||||
|
jeepney/tests/__pycache__/test_wrappers.cpython-314.pyc,,
|
||||||
|
jeepney/tests/secrets_introspect.xml,sha256=9cfNs1aGLtIAykcQVsycwIwCLmEeorKkFjqJCLAknRQ,4575
|
||||||
|
jeepney/tests/test_auth.py,sha256=Ee79vsedCwveukudAZTwqYTXHWV3PYnXkmMl0MBMZEE,611
|
||||||
|
jeepney/tests/test_bindgen.py,sha256=Ez99zr9TIV3mlZdH-2A_dz4LbvxCqzWDIadhOCbbaoc,1098
|
||||||
|
jeepney/tests/test_bus.py,sha256=ApOxd3AcYQB14G1XsiFGBYtQ4xSKw52y9YvmPz700gc,847
|
||||||
|
jeepney/tests/test_bus_messages.py,sha256=elwS7odY9RDsjg9jL4tN0O7uCxUqSYHsWShWXn_WPOQ,3338
|
||||||
|
jeepney/tests/test_fds.py,sha256=-gyvQpfsXtPaIEeqbwhrNPOcIAN0DsrQ7MXZu4nMvvQ,1821
|
||||||
|
jeepney/tests/test_low_level.py,sha256=2SC-wKKGr0yfEguswfHzCojSTwsYlTVLPyuzQbGS3L4,3000
|
||||||
|
jeepney/tests/test_wrappers.py,sha256=NSY6LblWeU2kToISjpi9YHgrd_Y6PVyFwXqnbY93ygU,2202
|
||||||
|
jeepney/wrappers.py,sha256=5zM_v1jFqEGDSaPh0f06SDxCF6JmWVhyXjfYR6KHum4,9605
|
||||||
4
lib/jeepney-0.9.0.dist-info/WHEEL
Normal file
4
lib/jeepney-0.9.0.dist-info/WHEEL
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: flit 3.11.0
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
21
lib/jeepney-0.9.0.dist-info/licenses/LICENSE
Normal file
21
lib/jeepney-0.9.0.dist-info/licenses/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Thomas Kluyver
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
13
lib/jeepney/__init__.py
Normal file
13
lib/jeepney/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""Low-level, pure Python DBus protocol wrapper.
|
||||||
|
"""
|
||||||
|
from .auth import AuthenticationError, FDNegotiationError
|
||||||
|
from .low_level import (
|
||||||
|
Endianness, Header, HeaderFields, Message, MessageFlag, MessageType,
|
||||||
|
Parser, SizeLimitError,
|
||||||
|
)
|
||||||
|
from .bus import find_session_bus, find_system_bus
|
||||||
|
from .bus_messages import *
|
||||||
|
from .fds import FileDescriptor, NoFDError
|
||||||
|
from .wrappers import *
|
||||||
|
|
||||||
|
__version__ = '0.9.0'
|
||||||
BIN
lib/jeepney/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/auth.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/bindgen.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/bindgen.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/bus.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/bus.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/bus_messages.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/bus_messages.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/fds.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/fds.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/low_level.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/low_level.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/__pycache__/wrappers.cpython-314.pyc
Normal file
BIN
lib/jeepney/__pycache__/wrappers.cpython-314.pyc
Normal file
Binary file not shown.
144
lib/jeepney/auth.py
Normal file
144
lib/jeepney/auth.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
from binascii import hexlify
|
||||||
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def make_auth_external() -> bytes:
|
||||||
|
"""Prepare an AUTH command line with the current effective user ID.
|
||||||
|
|
||||||
|
This is the preferred authentication method for typical D-Bus connections
|
||||||
|
over a Unix domain socket.
|
||||||
|
"""
|
||||||
|
hex_uid = hexlify(str(os.geteuid()).encode('ascii'))
|
||||||
|
return b'AUTH EXTERNAL %b\r\n' % hex_uid
|
||||||
|
|
||||||
|
def make_auth_anonymous() -> bytes:
|
||||||
|
"""Format an AUTH command line for the ANONYMOUS mechanism
|
||||||
|
|
||||||
|
Jeepney's higher-level wrappers don't currently use this mechanism,
|
||||||
|
but third-party code may choose to.
|
||||||
|
|
||||||
|
See <https://tools.ietf.org/html/rfc4505> for details.
|
||||||
|
"""
|
||||||
|
from . import __version__
|
||||||
|
trace = hexlify(('Jeepney %s' % __version__).encode('ascii'))
|
||||||
|
return b'AUTH ANONYMOUS %s\r\n' % trace
|
||||||
|
|
||||||
|
BEGIN = b'BEGIN\r\n'
|
||||||
|
NEGOTIATE_UNIX_FD = b'NEGOTIATE_UNIX_FD\r\n'
|
||||||
|
|
||||||
|
class ClientState(Enum):
|
||||||
|
# States from the D-Bus spec (plus 'Success'). Not all used in Jeepney.
|
||||||
|
WaitingForData = 1
|
||||||
|
WaitingForOk = 2
|
||||||
|
WaitingForReject = 3
|
||||||
|
WaitingForAgreeUnixFD = 4
|
||||||
|
Success = 5
|
||||||
|
|
||||||
|
class AuthenticationError(ValueError):
|
||||||
|
"""Raised when DBus authentication fails"""
|
||||||
|
def __init__(self, data, msg="Authentication failed"):
|
||||||
|
self.msg = msg
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.msg}. Bus sent: {self.data!r}"
|
||||||
|
|
||||||
|
class FDNegotiationError(AuthenticationError):
|
||||||
|
"""Raised when file descriptor support is requested but not available"""
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__(data, msg="File descriptor support not available")
|
||||||
|
|
||||||
|
|
||||||
|
class Authenticator:
|
||||||
|
"""Process data for the SASL authentication conversation
|
||||||
|
|
||||||
|
If enable_fds is True, this includes negotiating support for passing
|
||||||
|
file descriptors. If inc_null_byte is True, sends the '\0' byte
|
||||||
|
at the beginning of the negotiations, which was the past behavior,
|
||||||
|
but which prevents sending the SCM_CREDS ancillary data over the socket,
|
||||||
|
breaking authentication on *BSD; the caller should rather send that
|
||||||
|
null byte and ancillary data and pass inc_null_byte=False to prevent
|
||||||
|
it being done here.
|
||||||
|
"""
|
||||||
|
def __init__(self, enable_fds=False, inc_null_byte=True):
|
||||||
|
self.enable_fds = enable_fds
|
||||||
|
self.buffer = bytearray()
|
||||||
|
if inc_null_byte:
|
||||||
|
self._to_send = b'\0' + make_auth_external()
|
||||||
|
else:
|
||||||
|
self._to_send = make_auth_external()
|
||||||
|
self.state = ClientState.WaitingForOk
|
||||||
|
self.error = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authenticated(self):
|
||||||
|
return self.state is ClientState.Success
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.data_to_send, None)
|
||||||
|
|
||||||
|
def data_to_send(self) -> Optional[bytes]:
|
||||||
|
"""Get a line of data to send to the server
|
||||||
|
|
||||||
|
The data returned should be sent before waiting to receive data.
|
||||||
|
Returns empty bytes if waiting for more data from the server, and None
|
||||||
|
if authentication is finished (success or error).
|
||||||
|
|
||||||
|
Iterating over the Authenticator object will also yield these lines;
|
||||||
|
:meth:`feed` should be called with received data inside the loop.
|
||||||
|
"""
|
||||||
|
if self.authenticated or self.error:
|
||||||
|
return None
|
||||||
|
self._to_send, to_send = b'', self._to_send
|
||||||
|
return to_send
|
||||||
|
|
||||||
|
def process_line(self, line):
|
||||||
|
if self.state is ClientState.WaitingForOk:
|
||||||
|
if line.startswith(b'OK '):
|
||||||
|
if self.enable_fds:
|
||||||
|
return NEGOTIATE_UNIX_FD, ClientState.WaitingForAgreeUnixFD
|
||||||
|
else:
|
||||||
|
return BEGIN, ClientState.Success
|
||||||
|
# We only support EXTERNAL authentication, but if we allow others,
|
||||||
|
# 'REJECTED <mechs>' would tell us to try another one.
|
||||||
|
|
||||||
|
elif self.state is ClientState.WaitingForAgreeUnixFD:
|
||||||
|
if line.startswith(b'AGREE_UNIX_FD'):
|
||||||
|
return BEGIN, ClientState.Success
|
||||||
|
# The protocol allows us to continue if FD passing is rejected,
|
||||||
|
# but Jeepney assumes that if you enable FD support you need it,
|
||||||
|
# so we fail rather
|
||||||
|
self.error = line
|
||||||
|
raise FDNegotiationError(line)
|
||||||
|
|
||||||
|
self.error = line
|
||||||
|
raise AuthenticationError(line)
|
||||||
|
|
||||||
|
def feed(self, data: bytes):
|
||||||
|
"""Process received data
|
||||||
|
|
||||||
|
Raises AuthenticationError if the incoming data is not as expected for
|
||||||
|
successful authentication. The connection should then be abandoned.
|
||||||
|
"""
|
||||||
|
self.buffer += data
|
||||||
|
if b'\r\n' in self.buffer:
|
||||||
|
line, self.buffer = self.buffer.split(b'\r\n', 1)
|
||||||
|
if self.buffer:
|
||||||
|
# We only expect one line before we reply
|
||||||
|
raise AuthenticationError(self.buffer, "Unexpected data received")
|
||||||
|
|
||||||
|
self._to_send, self.state = self.process_line(line)
|
||||||
|
|
||||||
|
# Avoid consuming lots of memory if the server is not sending what we
|
||||||
|
# expect. There doesn't appear to be a specified maximum line length,
|
||||||
|
# but 8192 bytes leaves a sizeable margin over all the examples in the
|
||||||
|
# spec (all < 100 bytes per line).
|
||||||
|
elif len(self.buffer) > 8192:
|
||||||
|
raise AuthenticationError(
|
||||||
|
self.buffer, "Too much data received without line ending"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Old name (behaviour on errors has changed, but should work for standard case)
|
||||||
|
SASLParser = Authenticator
|
||||||
170
lib/jeepney/bindgen.py
Normal file
170
lib/jeepney/bindgen.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"""Generate a wrapper class from DBus introspection data"""
|
||||||
|
import argparse
|
||||||
|
import os.path
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from textwrap import indent
|
||||||
|
|
||||||
|
from jeepney.wrappers import Introspectable
|
||||||
|
from jeepney.io.blocking import open_dbus_connection, Proxy
|
||||||
|
from jeepney import __version__
|
||||||
|
|
||||||
|
class Method:
|
||||||
|
def __init__(self, xml_node):
|
||||||
|
self.name = xml_node.attrib['name']
|
||||||
|
self.in_args = []
|
||||||
|
self.signature = []
|
||||||
|
for arg in xml_node.findall("arg[@direction='in']"):
|
||||||
|
try:
|
||||||
|
name = arg.attrib['name']
|
||||||
|
except KeyError:
|
||||||
|
name = 'arg{}'.format(len(self.in_args))
|
||||||
|
self.in_args.append(name)
|
||||||
|
self.signature.append(arg.attrib['type'])
|
||||||
|
|
||||||
|
def _make_code_noargs(self):
|
||||||
|
return ("def {name}(self):\n"
|
||||||
|
" return new_method_call(self, '{name}')\n").format(
|
||||||
|
name=self.name)
|
||||||
|
|
||||||
|
def make_code(self):
|
||||||
|
if not self.in_args:
|
||||||
|
return self._make_code_noargs()
|
||||||
|
|
||||||
|
args = ', '.join(self.in_args)
|
||||||
|
signature = ''.join(self.signature)
|
||||||
|
tuple = ('({},)' if len(self.in_args) == 1 else '({})').format(args)
|
||||||
|
return ("def {name}(self, {args}):\n"
|
||||||
|
" return new_method_call(self, '{name}', '{signature}',\n"
|
||||||
|
" {tuple})\n").format(
|
||||||
|
name=self.name, args=args, signature=signature, tuple=tuple
|
||||||
|
)
|
||||||
|
|
||||||
|
INTERFACE_CLASS_TEMPLATE = """
|
||||||
|
class {cls_name}(MessageGenerator):
|
||||||
|
interface = {interface!r}
|
||||||
|
|
||||||
|
def __init__(self, object_path{path_default},
|
||||||
|
bus_name{name_default}):
|
||||||
|
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Interface:
|
||||||
|
def __init__(self, xml_node, path, bus_name):
|
||||||
|
self.name = xml_node.attrib['name']
|
||||||
|
self.path = path
|
||||||
|
self.bus_name = bus_name
|
||||||
|
self.methods = [Method(node) for node in xml_node.findall('method')]
|
||||||
|
|
||||||
|
def make_code(self):
|
||||||
|
cls_name = self.name.split('.')[-1]
|
||||||
|
chunks = [INTERFACE_CLASS_TEMPLATE.format(
|
||||||
|
cls_name=cls_name,
|
||||||
|
interface=self.name,
|
||||||
|
path_default='' if self.path is None else f'={self.path!r}',
|
||||||
|
name_default='' if self.bus_name is None else f'={self.bus_name!r}'
|
||||||
|
)]
|
||||||
|
for method in self.methods:
|
||||||
|
chunks.append(indent(method.make_code(), ' ' * 4))
|
||||||
|
return '\n'.join(chunks)
|
||||||
|
|
||||||
|
MODULE_TEMPLATE = '''\
|
||||||
|
"""Auto-generated DBus bindings
|
||||||
|
|
||||||
|
Generated by jeepney version {version}
|
||||||
|
|
||||||
|
Object path: {path}
|
||||||
|
Bus name : {bus_name}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from jeepney.wrappers import MessageGenerator, new_method_call
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Jeepney already includes bindings for these common interfaces
|
||||||
|
IGNORE_INTERFACES = {
|
||||||
|
'org.freedesktop.DBus.Introspectable',
|
||||||
|
'org.freedesktop.DBus.Properties',
|
||||||
|
'org.freedesktop.DBus.Peer',
|
||||||
|
}
|
||||||
|
|
||||||
|
def code_from_xml(xml, path, bus_name, fh):
|
||||||
|
if isinstance(fh, (bytes, str)):
|
||||||
|
with open(fh, 'w') as f:
|
||||||
|
return code_from_xml(xml, path, bus_name, f)
|
||||||
|
|
||||||
|
root = ET.fromstring(xml)
|
||||||
|
fh.write(MODULE_TEMPLATE.format(version=__version__, path=path,
|
||||||
|
bus_name=bus_name))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for interface_node in root.findall('interface'):
|
||||||
|
if interface_node.attrib['name'] in IGNORE_INTERFACES:
|
||||||
|
continue
|
||||||
|
fh.write(Interface(interface_node, path, bus_name).make_code())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return i
|
||||||
|
|
||||||
|
def generate_from_introspection(path, name, output_file, bus='SESSION'):
|
||||||
|
# Many D-Bus services have a main object at a predictable name, e.g.
|
||||||
|
# org.freedesktop.Notifications -> /org/freedesktop/Notifications
|
||||||
|
if not path:
|
||||||
|
path = '/' + name.replace('.', '/')
|
||||||
|
|
||||||
|
conn = open_dbus_connection(bus)
|
||||||
|
introspectable = Proxy(Introspectable(path, name), conn)
|
||||||
|
xml, = introspectable.Introspect()
|
||||||
|
# print(xml)
|
||||||
|
|
||||||
|
n_interfaces = code_from_xml(xml, path, name, output_file)
|
||||||
|
print("Written {} interface wrappers to {}".format(n_interfaces, output_file))
|
||||||
|
|
||||||
|
def generate_from_file(input_file, path, name, output_file):
|
||||||
|
with open(input_file, encoding='utf-8') as f:
|
||||||
|
xml = f.read()
|
||||||
|
|
||||||
|
n_interfaces = code_from_xml(xml, path, name, output_file)
|
||||||
|
print("Written {} interface wrappers to {}".format(n_interfaces, output_file))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="Generate a simple wrapper module to call D-Bus methods.",
|
||||||
|
epilog="If you don't use --file, this will connect to D-Bus and introspect the "
|
||||||
|
"given name and path. --name and --path can also be used with --file, "
|
||||||
|
"to give defaults for the generated class."
|
||||||
|
)
|
||||||
|
ap.add_argument('-n', '--name',
|
||||||
|
help='Bus name to introspect, required unless using file')
|
||||||
|
ap.add_argument('-p', '--path',
|
||||||
|
help='Object path to introspect. If not specified, a path matching '
|
||||||
|
'the name will be used, e.g. /org/freedesktop/Notifications for org.freedesktop.Notifications')
|
||||||
|
ap.add_argument('--bus', default='SESSION',
|
||||||
|
help='Bus to connect to for introspection (SESSION/SYSTEM), default SESSION')
|
||||||
|
ap.add_argument('-f', '--file',
|
||||||
|
help='XML file to use instead of D-Bus introspection')
|
||||||
|
ap.add_argument('-o', '--output',
|
||||||
|
help='Output filename')
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not (args.file or args.name):
|
||||||
|
sys.exit("Either --name or --file is required")
|
||||||
|
|
||||||
|
# If no --output, guess a (hopefully) reasonable name.
|
||||||
|
if args.output:
|
||||||
|
output = args.output
|
||||||
|
elif args.file:
|
||||||
|
output = os.path.splitext(os.path.basename(args.file))[0] + '.py'
|
||||||
|
elif args.path and len(args.path) > 1:
|
||||||
|
output = args.path[1:].replace('/', '_') + '.py'
|
||||||
|
else: # e.g. path is '/'
|
||||||
|
output = args.name.replace('.', '_') + '.py'
|
||||||
|
|
||||||
|
if args.file:
|
||||||
|
generate_from_file(args.file, args.path, args.name, output)
|
||||||
|
else:
|
||||||
|
generate_from_introspection(args.path, args.name, output, args.bus)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
62
lib/jeepney/bus.py
Normal file
62
lib/jeepney/bus.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
_escape_pat = re.compile(r'%([0-9A-Fa-f]{2})')
|
||||||
|
def unescape(v):
|
||||||
|
def repl(match):
|
||||||
|
n = int(match.group(1), base=16)
|
||||||
|
return chr(n)
|
||||||
|
return _escape_pat.sub(repl, v)
|
||||||
|
|
||||||
|
def parse_addresses(s):
|
||||||
|
for addr in s.split(';'):
|
||||||
|
transport, info = addr.split(':', 1)
|
||||||
|
kv = {}
|
||||||
|
for x in info.split(','):
|
||||||
|
k, v = x.split('=', 1)
|
||||||
|
kv[k] = unescape(v)
|
||||||
|
yield (transport, kv)
|
||||||
|
|
||||||
|
SUPPORTED_TRANSPORTS = ('unix',)
|
||||||
|
|
||||||
|
def get_connectable_addresses(addr):
|
||||||
|
unsupported_transports = set()
|
||||||
|
found = False
|
||||||
|
for transport, kv in parse_addresses(addr):
|
||||||
|
if transport not in SUPPORTED_TRANSPORTS:
|
||||||
|
unsupported_transports.add(transport)
|
||||||
|
|
||||||
|
elif transport == 'unix':
|
||||||
|
if 'abstract' in kv:
|
||||||
|
yield '\0' + kv['abstract']
|
||||||
|
found = True
|
||||||
|
elif 'path' in kv:
|
||||||
|
yield kv['path']
|
||||||
|
found = True
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise RuntimeError("DBus transports ({}) not supported. Supported: {}"
|
||||||
|
.format(unsupported_transports, SUPPORTED_TRANSPORTS))
|
||||||
|
|
||||||
|
def find_session_bus():
|
||||||
|
addr = os.environ['DBUS_SESSION_BUS_ADDRESS']
|
||||||
|
return next(get_connectable_addresses(addr))
|
||||||
|
# TODO: fallbacks to X, filesystem
|
||||||
|
|
||||||
|
def find_system_bus():
|
||||||
|
addr = os.environ.get('DBUS_SYSTEM_BUS_ADDRESS', '') \
|
||||||
|
or 'unix:path=/var/run/dbus/system_bus_socket'
|
||||||
|
return next(get_connectable_addresses(addr))
|
||||||
|
|
||||||
|
def get_bus(addr):
|
||||||
|
if addr == 'SESSION':
|
||||||
|
return find_session_bus()
|
||||||
|
elif addr == 'SYSTEM':
|
||||||
|
return find_system_bus()
|
||||||
|
else:
|
||||||
|
return next(get_connectable_addresses(addr))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('System bus at:', find_system_bus())
|
||||||
|
print('Session bus at:', find_session_bus())
|
||||||
238
lib/jeepney/bus_messages.py
Normal file
238
lib/jeepney/bus_messages.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
"""Messages for talking to the DBus daemon itself
|
||||||
|
|
||||||
|
Generated by jeepney.bindgen and modified by hand.
|
||||||
|
"""
|
||||||
|
from .low_level import Message, MessageType, HeaderFields
|
||||||
|
from .wrappers import MessageGenerator, new_method_call
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DBusNameFlags',
|
||||||
|
'DBus',
|
||||||
|
'message_bus',
|
||||||
|
'Monitoring',
|
||||||
|
'Stats',
|
||||||
|
'MatchRule',
|
||||||
|
]
|
||||||
|
|
||||||
|
class DBusNameFlags:
|
||||||
|
allow_replacement = 1
|
||||||
|
replace_existing = 2
|
||||||
|
do_not_queue = 4
|
||||||
|
|
||||||
|
class DBus(MessageGenerator):
|
||||||
|
"""Messages to talk to the message bus
|
||||||
|
"""
|
||||||
|
interface = 'org.freedesktop.DBus'
|
||||||
|
|
||||||
|
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||||
|
bus_name='org.freedesktop.DBus'):
|
||||||
|
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||||
|
|
||||||
|
def Hello(self):
|
||||||
|
return new_method_call(self, 'Hello')
|
||||||
|
|
||||||
|
def RequestName(self, name, flags=0):
|
||||||
|
return new_method_call(self, 'RequestName', 'su', (name, flags))
|
||||||
|
|
||||||
|
def ReleaseName(self, name):
|
||||||
|
return new_method_call(self, 'ReleaseName', 's', (name,))
|
||||||
|
|
||||||
|
def StartServiceByName(self, name):
|
||||||
|
return new_method_call(self, 'StartServiceByName', 'su',
|
||||||
|
(name, 0))
|
||||||
|
|
||||||
|
def UpdateActivationEnvironment(self, env):
|
||||||
|
return new_method_call(self, 'UpdateActivationEnvironment', 'a{ss}',
|
||||||
|
(env,))
|
||||||
|
|
||||||
|
def NameHasOwner(self, name):
|
||||||
|
return new_method_call(self, 'NameHasOwner', 's', (name,))
|
||||||
|
|
||||||
|
def ListNames(self):
|
||||||
|
return new_method_call(self, 'ListNames')
|
||||||
|
|
||||||
|
def ListActivatableNames(self):
|
||||||
|
return new_method_call(self, 'ListActivatableNames')
|
||||||
|
|
||||||
|
def AddMatch(self, rule):
|
||||||
|
"""*rule* can be a str or a :class:`MatchRule` instance"""
|
||||||
|
if isinstance(rule, MatchRule):
|
||||||
|
rule = rule.serialise()
|
||||||
|
return new_method_call(self, 'AddMatch', 's', (rule,))
|
||||||
|
|
||||||
|
def RemoveMatch(self, rule):
|
||||||
|
if isinstance(rule, MatchRule):
|
||||||
|
rule = rule.serialise()
|
||||||
|
return new_method_call(self, 'RemoveMatch', 's', (rule,))
|
||||||
|
|
||||||
|
def GetNameOwner(self, name):
|
||||||
|
return new_method_call(self, 'GetNameOwner', 's', (name,))
|
||||||
|
|
||||||
|
def ListQueuedOwners(self, name):
|
||||||
|
return new_method_call(self, 'ListQueuedOwners', 's', (name,))
|
||||||
|
|
||||||
|
def GetConnectionUnixUser(self, name):
|
||||||
|
return new_method_call(self, 'GetConnectionUnixUser', 's', (name,))
|
||||||
|
|
||||||
|
def GetConnectionUnixProcessID(self, name):
|
||||||
|
return new_method_call(self, 'GetConnectionUnixProcessID', 's', (name,))
|
||||||
|
|
||||||
|
def GetAdtAuditSessionData(self, name):
|
||||||
|
return new_method_call(self, 'GetAdtAuditSessionData', 's', (name,))
|
||||||
|
|
||||||
|
def GetConnectionSELinuxSecurityContext(self, name):
|
||||||
|
return new_method_call(self, 'GetConnectionSELinuxSecurityContext', 's',
|
||||||
|
(name,))
|
||||||
|
|
||||||
|
def ReloadConfig(self):
|
||||||
|
return new_method_call(self, 'ReloadConfig')
|
||||||
|
|
||||||
|
def GetId(self):
|
||||||
|
return new_method_call(self, 'GetId')
|
||||||
|
|
||||||
|
def GetConnectionCredentials(self, name):
|
||||||
|
return new_method_call(self, 'GetConnectionCredentials', 's', (name,))
|
||||||
|
|
||||||
|
message_bus = DBus()
|
||||||
|
|
||||||
|
class Monitoring(MessageGenerator):
|
||||||
|
interface = 'org.freedesktop.DBus.Monitoring'
|
||||||
|
|
||||||
|
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||||
|
bus_name='org.freedesktop.DBus'):
|
||||||
|
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||||
|
|
||||||
|
def BecomeMonitor(self, rules):
|
||||||
|
"""Convert this connection to a monitor connection (advanced)"""
|
||||||
|
return new_method_call(self, 'BecomeMonitor', 'asu', (rules, 0))
|
||||||
|
|
||||||
|
class Stats(MessageGenerator):
|
||||||
|
interface = 'org.freedesktop.DBus.Debug.Stats'
|
||||||
|
|
||||||
|
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||||
|
bus_name='org.freedesktop.DBus'):
|
||||||
|
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||||
|
|
||||||
|
def GetStats(self):
|
||||||
|
return new_method_call(self, 'GetStats')
|
||||||
|
|
||||||
|
def GetConnectionStats(self, arg0):
|
||||||
|
return new_method_call(self, 'GetConnectionStats', 's',
|
||||||
|
(arg0,))
|
||||||
|
|
||||||
|
def GetAllMatchRules(self):
|
||||||
|
return new_method_call(self, 'GetAllMatchRules')
|
||||||
|
|
||||||
|
|
||||||
|
class MatchRule:
|
||||||
|
"""Construct a match rule to subscribe to DBus messages.
|
||||||
|
|
||||||
|
e.g.::
|
||||||
|
|
||||||
|
mr = MatchRule(
|
||||||
|
interface='org.freedesktop.DBus',
|
||||||
|
member='NameOwnerChanged',
|
||||||
|
type='signal'
|
||||||
|
)
|
||||||
|
msg = message_bus.AddMatch(mr)
|
||||||
|
# Send this message to subscribe to the signal
|
||||||
|
"""
|
||||||
|
def __init__(self, *, type=None, sender=None, interface=None, member=None,
|
||||||
|
path=None, path_namespace=None, destination=None,
|
||||||
|
eavesdrop=False):
|
||||||
|
if isinstance(type, str):
|
||||||
|
type = MessageType[type]
|
||||||
|
self.message_type = type
|
||||||
|
fields = {
|
||||||
|
'sender': sender,
|
||||||
|
'interface': interface,
|
||||||
|
'member': member,
|
||||||
|
'path': path,
|
||||||
|
'destination': destination,
|
||||||
|
}
|
||||||
|
self.header_fields = {
|
||||||
|
k: v for (k, v) in fields.items() if (v is not None)
|
||||||
|
}
|
||||||
|
self.path_namespace = path_namespace
|
||||||
|
self.eavesdrop = eavesdrop
|
||||||
|
self.arg_conditions = {}
|
||||||
|
|
||||||
|
def add_arg_condition(self, argno: int, value: str, kind='string'):
|
||||||
|
"""Add a condition for a particular argument
|
||||||
|
|
||||||
|
argno: int, 0-63
|
||||||
|
kind: 'string', 'path', 'namespace'
|
||||||
|
"""
|
||||||
|
if kind not in {'string', 'path', 'namespace'}:
|
||||||
|
raise ValueError("kind={!r}".format(kind))
|
||||||
|
if kind == 'namespace' and argno != 0:
|
||||||
|
raise ValueError("argno must be 0 for kind='namespace'")
|
||||||
|
self.arg_conditions[argno] = (value, kind)
|
||||||
|
|
||||||
|
def serialise(self) -> str:
|
||||||
|
"""Convert to a string to use in an AddMatch call to the message bus"""
|
||||||
|
pairs = list(self.header_fields.items())
|
||||||
|
|
||||||
|
if self.message_type:
|
||||||
|
pairs.append(('type', self.message_type.name))
|
||||||
|
|
||||||
|
if self.path_namespace:
|
||||||
|
pairs.append(('path_namespace', self.path_namespace))
|
||||||
|
|
||||||
|
if self.eavesdrop:
|
||||||
|
pairs.append(('eavesdrop', 'true'))
|
||||||
|
|
||||||
|
for argno, (val, kind) in self.arg_conditions.items():
|
||||||
|
if kind == 'string':
|
||||||
|
kind = ''
|
||||||
|
pairs.append((f'arg{argno}{kind}', val))
|
||||||
|
|
||||||
|
# Quoting rules: single quotes ('') needed if the value contains a comma.
|
||||||
|
# A literal ' can only be represented outside single quotes, by
|
||||||
|
# backslash-escaping it. No escaping inside the quotes.
|
||||||
|
# The simplest way to handle this is to use '' around every value, and
|
||||||
|
# use '\'' (end quote, escaped ', restart quote) for literal ' .
|
||||||
|
return ','.join(
|
||||||
|
"{}='{}'".format(k, v.replace("'", r"'\''")) for (k, v) in pairs
|
||||||
|
)
|
||||||
|
|
||||||
|
def matches(self, msg: Message) -> bool:
|
||||||
|
"""Returns True if msg matches this rule"""
|
||||||
|
h = msg.header
|
||||||
|
if (self.message_type is not None) and h.message_type != self.message_type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for field, expected in self.header_fields.items():
|
||||||
|
if h.fields.get(HeaderFields[field], None) != expected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.path_namespace is not None:
|
||||||
|
path = h.fields.get(HeaderFields.path, '\0')
|
||||||
|
path_ns = self.path_namespace.rstrip('/')
|
||||||
|
if not ((path == path_ns) or path.startswith(path_ns + '/')):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for argno, (expected, kind) in self.arg_conditions.items():
|
||||||
|
if argno >= len(msg.body):
|
||||||
|
return False
|
||||||
|
arg = msg.body[argno]
|
||||||
|
if not isinstance(arg, str):
|
||||||
|
return False
|
||||||
|
if kind == 'string':
|
||||||
|
if arg != expected:
|
||||||
|
return False
|
||||||
|
elif kind == 'path':
|
||||||
|
if not (
|
||||||
|
(arg == expected)
|
||||||
|
or (expected.endswith('/') and arg.startswith(expected))
|
||||||
|
or (arg.endswith('/') and expected.startswith(arg))
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
elif kind == 'namespace':
|
||||||
|
if not (
|
||||||
|
(arg == expected)
|
||||||
|
or arg.startswith(expected + '.')
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
158
lib/jeepney/fds.py
Normal file
158
lib/jeepney/fds.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import array
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
|
||||||
|
class NoFDError(RuntimeError):
|
||||||
|
"""Raised by :class:`FileDescriptor` methods if it was already closed/converted
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileDescriptor:
|
||||||
|
"""A file descriptor received in a D-Bus message
|
||||||
|
|
||||||
|
This wrapper helps ensure that the file descriptor is closed exactly once.
|
||||||
|
If you don't explicitly convert or close the FileDescriptor object, it will
|
||||||
|
close its file descriptor when it goes out of scope, and emit a
|
||||||
|
ResourceWarning.
|
||||||
|
"""
|
||||||
|
__slots__ = ('_fd',)
|
||||||
|
_CLOSED = -1
|
||||||
|
_CONVERTED = -2
|
||||||
|
|
||||||
|
def __init__(self, fd):
|
||||||
|
self._fd = fd
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
detail = self._fd
|
||||||
|
if self._fd == self._CLOSED:
|
||||||
|
detail = 'closed'
|
||||||
|
elif self._fd == self._CONVERTED:
|
||||||
|
detail = 'converted'
|
||||||
|
return f"<FileDescriptor ({detail})>"
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the file descriptor
|
||||||
|
|
||||||
|
This can safely be called multiple times, but will raise RuntimeError
|
||||||
|
if called after converting it with one of the ``to_*`` methods.
|
||||||
|
|
||||||
|
This object can also be used in a ``with`` block, to close it on
|
||||||
|
leaving the block.
|
||||||
|
"""
|
||||||
|
if self._fd == self._CLOSED:
|
||||||
|
pass
|
||||||
|
elif self._fd == self._CONVERTED:
|
||||||
|
raise NoFDError("Can't close FileDescriptor after converting it")
|
||||||
|
else:
|
||||||
|
self._fd, fd = self._CLOSED, self._fd
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._fd >= 0:
|
||||||
|
warn(
|
||||||
|
f'FileDescriptor ({self._fd}) was neither closed nor converted',
|
||||||
|
ResourceWarning, stacklevel=2, source=self
|
||||||
|
)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def _check(self):
|
||||||
|
if self._fd < 0:
|
||||||
|
detail = 'closed' if self._fd == self._CLOSED else 'converted'
|
||||||
|
raise NoFDError(f'FileDescriptor object was already {detail}')
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
"""Get the integer file descriptor
|
||||||
|
|
||||||
|
This does not change the state of the :class:`FileDescriptor` object,
|
||||||
|
unlike the ``to_*`` methods.
|
||||||
|
"""
|
||||||
|
self._check()
|
||||||
|
return self._fd
|
||||||
|
|
||||||
|
def to_raw_fd(self):
|
||||||
|
"""Convert to the low-level integer file descriptor::
|
||||||
|
|
||||||
|
raw_fd = fd.to_raw_fd()
|
||||||
|
os.write(raw_fd, b'xyz')
|
||||||
|
os.close(raw_fd)
|
||||||
|
|
||||||
|
The :class:`FileDescriptor` can't be used after calling this. The caller
|
||||||
|
is responsible for closing the file descriptor.
|
||||||
|
"""
|
||||||
|
self._check()
|
||||||
|
self._fd, fd = self._CONVERTED, self._fd
|
||||||
|
return fd
|
||||||
|
|
||||||
|
def to_file(self, mode, buffering=-1, encoding=None, errors=None, newline=None):
|
||||||
|
"""Convert to a Python file object::
|
||||||
|
|
||||||
|
with fd.to_file('w') as f:
|
||||||
|
f.write('xyz')
|
||||||
|
|
||||||
|
The arguments are the same as for the builtin :func:`open` function.
|
||||||
|
|
||||||
|
The :class:`FileDescriptor` can't be used after calling this. Closing
|
||||||
|
the file object will also close the file descriptor.
|
||||||
|
"""
|
||||||
|
self._check()
|
||||||
|
f = open(
|
||||||
|
self._fd, mode, buffering=buffering,
|
||||||
|
encoding=encoding, errors=errors, newline=newline
|
||||||
|
)
|
||||||
|
self._fd = self._CONVERTED
|
||||||
|
return f
|
||||||
|
|
||||||
|
def to_socket(self):
|
||||||
|
"""Convert to a socket object
|
||||||
|
|
||||||
|
This returns a standard library :func:`socket.socket` object::
|
||||||
|
|
||||||
|
with fd.to_socket() as sock:
|
||||||
|
b = sock.sendall(b'xyz')
|
||||||
|
|
||||||
|
The wrapper object can't be used after calling this. Closing the socket
|
||||||
|
object will also close the file descriptor.
|
||||||
|
"""
|
||||||
|
from socket import socket
|
||||||
|
|
||||||
|
self._check()
|
||||||
|
s = socket(fileno=self._fd)
|
||||||
|
self._fd = self._CONVERTED
|
||||||
|
return s
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_ancdata(cls, ancdata) -> ['FileDescriptor']:
|
||||||
|
"""Make a list of FileDescriptor from received file descriptors
|
||||||
|
|
||||||
|
ancdata is a list of ancillary data tuples as returned by socket.recvmsg()
|
||||||
|
"""
|
||||||
|
fds = array.array("i") # Array of ints
|
||||||
|
for cmsg_level, cmsg_type, data in ancdata:
|
||||||
|
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
||||||
|
# Append data, ignoring any truncated integers at the end.
|
||||||
|
fds.frombytes(data[:len(data) - (len(data) % fds.itemsize)])
|
||||||
|
return [cls(i) for i in fds]
|
||||||
|
|
||||||
|
|
||||||
|
_fds_buf_size_cache = None
|
||||||
|
|
||||||
|
def fds_buf_size():
|
||||||
|
# If there may be file descriptors, we try to read 1 message at a time.
|
||||||
|
# The reference implementation of D-Bus defaults to allowing 16 FDs per
|
||||||
|
# message, and the Linux kernel currently allows 253 FDs per sendmsg()
|
||||||
|
# call. So hopefully allowing 256 FDs per recvmsg() will always suffice.
|
||||||
|
global _fds_buf_size_cache
|
||||||
|
if _fds_buf_size_cache is None:
|
||||||
|
maxfds = 256
|
||||||
|
fd_size = array.array('i').itemsize
|
||||||
|
_fds_buf_size_cache = socket.CMSG_SPACE(maxfds * fd_size)
|
||||||
|
return _fds_buf_size_cache
|
||||||
1
lib/jeepney/io/__init__.py
Normal file
1
lib/jeepney/io/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .common import RouterClosed
|
||||||
BIN
lib/jeepney/io/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/__pycache__/asyncio.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/asyncio.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/__pycache__/blocking.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/blocking.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/__pycache__/common.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/common.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/__pycache__/threading.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/threading.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/__pycache__/trio.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/__pycache__/trio.cpython-314.pyc
Normal file
Binary file not shown.
233
lib/jeepney/io/asyncio.py
Normal file
233
lib/jeepney/io/asyncio.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
from itertools import count
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from jeepney.auth import Authenticator, BEGIN
|
||||||
|
from jeepney.bus import get_bus
|
||||||
|
from jeepney import Message, MessageType, Parser
|
||||||
|
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||||
|
from jeepney.bus_messages import message_bus
|
||||||
|
from .common import (
|
||||||
|
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DBusConnection:
|
||||||
|
"""A plain D-Bus connection with no matching of replies.
|
||||||
|
|
||||||
|
This doesn't run any separate tasks: sending and receiving are done in
|
||||||
|
the task that calls those methods. It's suitable for implementing servers:
|
||||||
|
several worker tasks can receive requests and send replies.
|
||||||
|
For a typical client pattern, see :class:`DBusRouter`.
|
||||||
|
"""
|
||||||
|
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||||
|
self.reader = reader
|
||||||
|
self.writer = writer
|
||||||
|
self.parser = Parser()
|
||||||
|
self.outgoing_serial = count(start=1)
|
||||||
|
self.unique_name = None
|
||||||
|
self.send_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def send(self, message: Message, *, serial=None):
|
||||||
|
"""Serialise and send a :class:`~.Message` object"""
|
||||||
|
async with self.send_lock:
|
||||||
|
if serial is None:
|
||||||
|
serial = next(self.outgoing_serial)
|
||||||
|
self.writer.write(message.serialise(serial))
|
||||||
|
await self.writer.drain()
|
||||||
|
|
||||||
|
async def receive(self) -> Message:
|
||||||
|
"""Return the next available message from the connection"""
|
||||||
|
while True:
|
||||||
|
msg = self.parser.get_next_message()
|
||||||
|
if msg is not None:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
b = await self.reader.read(4096)
|
||||||
|
if not b:
|
||||||
|
raise EOFError
|
||||||
|
self.parser.add_data(b)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close the D-Bus connection"""
|
||||||
|
self.writer.close()
|
||||||
|
await self.writer.wait_closed()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def open_dbus_connection(bus='SESSION'):
|
||||||
|
"""Open a plain D-Bus connection
|
||||||
|
|
||||||
|
:return: :class:`DBusConnection`
|
||||||
|
"""
|
||||||
|
bus_addr = get_bus(bus)
|
||||||
|
reader, writer = await asyncio.open_unix_connection(bus_addr)
|
||||||
|
|
||||||
|
# Authentication flow
|
||||||
|
authr = Authenticator()
|
||||||
|
for req_data in authr:
|
||||||
|
writer.write(req_data)
|
||||||
|
await writer.drain()
|
||||||
|
b = await reader.read(1024)
|
||||||
|
if not b:
|
||||||
|
raise EOFError("Socket closed before authentication")
|
||||||
|
authr.feed(b)
|
||||||
|
|
||||||
|
writer.write(BEGIN)
|
||||||
|
await writer.drain()
|
||||||
|
# Authentication finished
|
||||||
|
|
||||||
|
conn = DBusConnection(reader, writer)
|
||||||
|
|
||||||
|
# Say *Hello* to the message bus - this must be the first message, and the
|
||||||
|
# reply gives us our unique name.
|
||||||
|
async with DBusRouter(conn) as router:
|
||||||
|
reply_body = await asyncio.wait_for(Proxy(message_bus, router).Hello(), 10)
|
||||||
|
conn.unique_name = reply_body[0]
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
class DBusRouter:
|
||||||
|
"""A 'client' D-Bus connection which can wait for a specific reply.
|
||||||
|
|
||||||
|
This runs a background receiver task, and makes it possible to send a
|
||||||
|
request and wait for the relevant reply.
|
||||||
|
"""
|
||||||
|
_nursery_mgr = None
|
||||||
|
_send_cancel_scope = None
|
||||||
|
_rcv_cancel_scope = None
|
||||||
|
|
||||||
|
def __init__(self, conn: DBusConnection):
|
||||||
|
self._conn = conn
|
||||||
|
self._replies = ReplyMatcher()
|
||||||
|
self._filters = MessageFilters()
|
||||||
|
self._rcv_task = asyncio.create_task(self._receiver())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_name(self):
|
||||||
|
return self._conn.unique_name
|
||||||
|
|
||||||
|
async def send(self, message, *, serial=None):
|
||||||
|
"""Send a message, don't wait for a reply"""
|
||||||
|
await self._conn.send(message, serial=serial)
|
||||||
|
|
||||||
|
async def send_and_get_reply(self, message) -> Message:
|
||||||
|
"""Send a method call message and wait for the reply
|
||||||
|
|
||||||
|
Returns the reply message (method return or error message type).
|
||||||
|
"""
|
||||||
|
check_replyable(message)
|
||||||
|
if self._rcv_task.done():
|
||||||
|
raise RouterClosed("This DBusRouter has stopped")
|
||||||
|
|
||||||
|
serial = next(self._conn.outgoing_serial)
|
||||||
|
|
||||||
|
with self._replies.catch(serial, asyncio.Future()) as reply_fut:
|
||||||
|
await self.send(message, serial=serial)
|
||||||
|
return (await reply_fut)
|
||||||
|
|
||||||
|
def filter(self, rule, *, queue: Optional[asyncio.Queue] =None, bufsize=1):
|
||||||
|
"""Create a filter for incoming messages
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
with router.filter(rule) as queue:
|
||||||
|
matching_msg = await queue.get()
|
||||||
|
|
||||||
|
:param MatchRule rule: Catch messages matching this rule
|
||||||
|
:param asyncio.Queue queue: Send matching messages here
|
||||||
|
:param int bufsize: If no queue is passed in, create one with this size
|
||||||
|
"""
|
||||||
|
return FilterHandle(self._filters, rule, queue or asyncio.Queue(bufsize))
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self._rcv_task.done():
|
||||||
|
self._rcv_task.result() # Throw exception if receive task failed
|
||||||
|
else:
|
||||||
|
self._rcv_task.cancel()
|
||||||
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
|
await self._rcv_task
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Code to run in receiver task ------------------------------------
|
||||||
|
|
||||||
|
def _dispatch(self, msg: Message):
|
||||||
|
"""Handle one received message"""
|
||||||
|
if self._replies.dispatch(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
for filter in list(self._filters.matches(msg)):
|
||||||
|
try:
|
||||||
|
filter.queue.put_nowait(msg)
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _receiver(self):
|
||||||
|
"""Receiver loop - runs in a separate task"""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = await self._conn.receive()
|
||||||
|
self._dispatch(msg)
|
||||||
|
finally:
|
||||||
|
# Send errors to any tasks still waiting for a message.
|
||||||
|
self._replies.drop_all()
|
||||||
|
|
||||||
|
class open_dbus_router:
|
||||||
|
"""Open a D-Bus 'router' to send and receive messages
|
||||||
|
|
||||||
|
Use as an async context manager::
|
||||||
|
|
||||||
|
async with open_dbus_router() as router:
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
conn = None
|
||||||
|
req_ctx = None
|
||||||
|
|
||||||
|
def __init__(self, bus='SESSION'):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.conn = await open_dbus_connection(self.bus)
|
||||||
|
self.req_ctx = DBusRouter(self.conn)
|
||||||
|
return await self.req_ctx.__aenter__()
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.req_ctx.__aexit__(exc_type, exc_val, exc_tb)
|
||||||
|
await self.conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(ProxyBase):
|
||||||
|
"""An asyncio proxy for calling D-Bus methods
|
||||||
|
|
||||||
|
You can call methods on the proxy object, such as ``await bus_proxy.Hello()``
|
||||||
|
to make a method call over D-Bus and wait for a reply. It will either
|
||||||
|
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||||
|
The methods available are defined by the message generator you wrap.
|
||||||
|
|
||||||
|
:param msggen: A message generator object.
|
||||||
|
:param ~asyncio.DBusRouter router: Router to send and receive messages.
|
||||||
|
"""
|
||||||
|
def __init__(self, msggen, router):
|
||||||
|
super().__init__(msggen)
|
||||||
|
self._router = router
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Proxy({}, {})'.format(self._msggen, self._router)
|
||||||
|
|
||||||
|
def _method_call(self, make_msg):
|
||||||
|
async def inner(*args, **kwargs):
|
||||||
|
msg = make_msg(*args, **kwargs)
|
||||||
|
assert msg.header.message_type is MessageType.method_call
|
||||||
|
reply = await self._router.send_and_get_reply(msg)
|
||||||
|
return unwrap_msg(reply)
|
||||||
|
|
||||||
|
return inner
|
||||||
337
lib/jeepney/io/blocking.py
Normal file
337
lib/jeepney/io/blocking.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
"""Synchronous IO wrappers around jeepney
|
||||||
|
"""
|
||||||
|
import array
|
||||||
|
from collections import deque
|
||||||
|
from errno import ECONNRESET
|
||||||
|
import functools
|
||||||
|
from itertools import count
|
||||||
|
import os
|
||||||
|
from selectors import DefaultSelector, EVENT_READ
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from jeepney import Parser, Message, MessageType, HeaderFields
|
||||||
|
from jeepney.auth import Authenticator, BEGIN
|
||||||
|
from jeepney.bus import get_bus
|
||||||
|
from jeepney.fds import FileDescriptor, fds_buf_size
|
||||||
|
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||||
|
from jeepney.bus_messages import message_bus
|
||||||
|
from .common import MessageFilters, FilterHandle, check_replyable
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'open_dbus_connection',
|
||||||
|
'DBusConnection',
|
||||||
|
'Proxy',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _Future:
|
||||||
|
def __init__(self):
|
||||||
|
self._result = None
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
return bool(self._result)
|
||||||
|
|
||||||
|
def set_exception(self, exception):
|
||||||
|
self._result = (False, exception)
|
||||||
|
|
||||||
|
def set_result(self, result):
|
||||||
|
self._result = (True, result)
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
success, value = self._result
|
||||||
|
if success:
|
||||||
|
return value
|
||||||
|
raise value
|
||||||
|
|
||||||
|
|
||||||
|
def timeout_to_deadline(timeout):
|
||||||
|
if timeout is not None:
|
||||||
|
return time.monotonic() + timeout
|
||||||
|
return None
|
||||||
|
|
||||||
|
def deadline_to_timeout(deadline):
|
||||||
|
if deadline is not None:
|
||||||
|
return max(deadline - time.monotonic(), 0.)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DBusConnectionBase:
|
||||||
|
"""Connection machinery shared by this module and threading"""
|
||||||
|
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||||
|
self.sock = sock
|
||||||
|
self.enable_fds = enable_fds
|
||||||
|
self.parser = Parser()
|
||||||
|
self.outgoing_serial = count(start=1)
|
||||||
|
self.selector = DefaultSelector()
|
||||||
|
self.select_key = self.selector.register(sock, EVENT_READ)
|
||||||
|
self.unique_name = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _serialise(self, message: Message, serial) -> (bytes, Optional[array.array]):
|
||||||
|
if serial is None:
|
||||||
|
serial = next(self.outgoing_serial)
|
||||||
|
fds = array.array('i') if self.enable_fds else None
|
||||||
|
data = message.serialise(serial=serial, fds=fds)
|
||||||
|
return data, fds
|
||||||
|
|
||||||
|
def _send_with_fds(self, data, fds):
|
||||||
|
bytes_sent = self.sock.sendmsg(
|
||||||
|
[data], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
|
||||||
|
)
|
||||||
|
# If sendmsg succeeds, I think ancillary data has been sent atomically?
|
||||||
|
# So now we just need to send any leftover normal data.
|
||||||
|
if bytes_sent < len(data):
|
||||||
|
self.sock.sendall(data[bytes_sent:])
|
||||||
|
|
||||||
|
def _receive(self, deadline):
|
||||||
|
while True:
|
||||||
|
msg = self.parser.get_next_message()
|
||||||
|
if msg is not None:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
b, fds = self._read_some_data(timeout=deadline_to_timeout(deadline))
|
||||||
|
self.parser.add_data(b, fds=fds)
|
||||||
|
|
||||||
|
def _read_some_data(self, timeout=None):
|
||||||
|
for key, ev in self.selector.select(timeout):
|
||||||
|
if key == self.select_key:
|
||||||
|
if self.enable_fds:
|
||||||
|
return self._read_with_fds()
|
||||||
|
else:
|
||||||
|
return unwrap_read(self.sock.recv(4096)), []
|
||||||
|
|
||||||
|
raise TimeoutError
|
||||||
|
|
||||||
|
def _read_with_fds(self):
|
||||||
|
nbytes = self.parser.bytes_desired()
|
||||||
|
data, ancdata, flags, _ = self.sock.recvmsg(nbytes, fds_buf_size())
|
||||||
|
if flags & getattr(socket, 'MSG_CTRUNC', 0):
|
||||||
|
self.close()
|
||||||
|
raise RuntimeError("Unable to receive all file descriptors")
|
||||||
|
return unwrap_read(data), FileDescriptor.from_ancdata(ancdata)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the connection"""
|
||||||
|
self.selector.close()
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
class DBusConnection(DBusConnectionBase):
|
||||||
|
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||||
|
super().__init__(sock, enable_fds)
|
||||||
|
|
||||||
|
# Message routing machinery
|
||||||
|
self._filters = MessageFilters()
|
||||||
|
|
||||||
|
# Say Hello, get our unique name
|
||||||
|
self.bus_proxy = Proxy(message_bus, self)
|
||||||
|
hello_reply = self.bus_proxy.Hello()
|
||||||
|
self.unique_name = hello_reply[0]
|
||||||
|
|
||||||
|
def send(self, message: Message, serial=None):
|
||||||
|
"""Serialise and send a :class:`~.Message` object"""
|
||||||
|
data, fds = self._serialise(message, serial)
|
||||||
|
if fds:
|
||||||
|
self._send_with_fds(data, fds)
|
||||||
|
else:
|
||||||
|
self.sock.sendall(data)
|
||||||
|
|
||||||
|
send_message = send # Backwards compatibility
|
||||||
|
|
||||||
|
def receive(self, *, timeout=None) -> Message:
|
||||||
|
"""Return the next available message from the connection
|
||||||
|
|
||||||
|
If the data is ready, this will return immediately, even if timeout<=0.
|
||||||
|
Otherwise, it will wait for up to timeout seconds, or indefinitely if
|
||||||
|
timeout is None. If no message comes in time, it raises TimeoutError.
|
||||||
|
"""
|
||||||
|
return self._receive(timeout_to_deadline(timeout))
|
||||||
|
|
||||||
|
def recv_messages(self, *, timeout=None):
|
||||||
|
"""Receive one message and apply filters
|
||||||
|
|
||||||
|
See :meth:`filter`. Returns nothing.
|
||||||
|
"""
|
||||||
|
msg = self.receive(timeout=timeout)
|
||||||
|
for filter in self._filters.matches(msg):
|
||||||
|
filter.queue.append(msg)
|
||||||
|
|
||||||
|
def send_and_get_reply(self, message, *, timeout=None):
|
||||||
|
"""Send a message, wait for the reply and return it
|
||||||
|
|
||||||
|
Filters are applied to other messages received before the reply -
|
||||||
|
see :meth:`add_filter`.
|
||||||
|
"""
|
||||||
|
check_replyable(message)
|
||||||
|
deadline = timeout_to_deadline(timeout)
|
||||||
|
|
||||||
|
serial = next(self.outgoing_serial)
|
||||||
|
self.send_message(message, serial=serial)
|
||||||
|
while True:
|
||||||
|
msg_in = self.receive(timeout=deadline_to_timeout(deadline))
|
||||||
|
reply_to = msg_in.header.fields.get(HeaderFields.reply_serial, -1)
|
||||||
|
if reply_to == serial:
|
||||||
|
return msg_in
|
||||||
|
|
||||||
|
# Not the reply
|
||||||
|
for filter in self._filters.matches(msg_in):
|
||||||
|
filter.queue.append(msg_in)
|
||||||
|
|
||||||
|
def filter(self, rule, *, queue: Optional[deque] =None, bufsize=1):
|
||||||
|
"""Create a filter for incoming messages
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
with conn.filter(rule) as matches:
|
||||||
|
# matches is a deque containing matched messages
|
||||||
|
matching_msg = conn.recv_until_filtered(matches)
|
||||||
|
|
||||||
|
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||||
|
:param collections.deque queue: Matched messages will be added to this
|
||||||
|
:param int bufsize: If no deque is passed in, create one with this size
|
||||||
|
"""
|
||||||
|
if queue is None:
|
||||||
|
queue = deque(maxlen=bufsize)
|
||||||
|
return FilterHandle(self._filters, rule, queue)
|
||||||
|
|
||||||
|
def recv_until_filtered(self, queue, *, timeout=None) -> Message:
|
||||||
|
"""Process incoming messages until one is filtered into queue
|
||||||
|
|
||||||
|
Pops the message from queue and returns it, or raises TimeoutError if
|
||||||
|
the optional timeout expires. Without a timeout, this is equivalent to::
|
||||||
|
|
||||||
|
while len(queue) == 0:
|
||||||
|
conn.recv_messages()
|
||||||
|
return queue.popleft()
|
||||||
|
|
||||||
|
In the other I/O modules, there is no need for this, because messages
|
||||||
|
are placed in queues by a separate task.
|
||||||
|
|
||||||
|
:param collections.deque queue: A deque connected by :meth:`filter`
|
||||||
|
:param float timeout: Maximum time to wait in seconds
|
||||||
|
"""
|
||||||
|
deadline = timeout_to_deadline(timeout)
|
||||||
|
while len(queue) == 0:
|
||||||
|
self.recv_messages(timeout=deadline_to_timeout(deadline))
|
||||||
|
return queue.popleft()
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(ProxyBase):
|
||||||
|
"""A blocking proxy for calling D-Bus methods
|
||||||
|
|
||||||
|
You can call methods on the proxy object, such as ``bus_proxy.Hello()``
|
||||||
|
to make a method call over D-Bus and wait for a reply. It will either
|
||||||
|
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||||
|
The methods available are defined by the message generator you wrap.
|
||||||
|
|
||||||
|
You can set a time limit on a call by passing ``_timeout=`` in the method
|
||||||
|
call, or set a default when creating the proxy. The ``_timeout`` argument
|
||||||
|
is not passed to the message generator.
|
||||||
|
All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it
|
||||||
|
expires before a reply arrives.
|
||||||
|
|
||||||
|
:param msggen: A message generator object
|
||||||
|
:param ~blocking.DBusConnection connection: Connection to send and receive messages
|
||||||
|
:param float timeout: Default seconds to wait for a reply, or None for no limit
|
||||||
|
"""
|
||||||
|
def __init__(self, msggen, connection, *, timeout=None):
|
||||||
|
super().__init__(msggen)
|
||||||
|
self._connection = connection
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
extra = '' if (self._timeout is None) else f', timeout={self._timeout}'
|
||||||
|
return f"Proxy({self._msggen}, {self._connection}{extra})"
|
||||||
|
|
||||||
|
def _method_call(self, make_msg):
|
||||||
|
@functools.wraps(make_msg)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
timeout = kwargs.pop('_timeout', self._timeout)
|
||||||
|
msg = make_msg(*args, **kwargs)
|
||||||
|
assert msg.header.message_type is MessageType.method_call
|
||||||
|
return unwrap_msg(self._connection.send_and_get_reply(
|
||||||
|
msg, timeout=timeout
|
||||||
|
))
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def unwrap_read(b):
|
||||||
|
"""Raise ConnectionResetError from an empty read.
|
||||||
|
|
||||||
|
Sometimes the socket raises an error itself, sometimes it gives no data.
|
||||||
|
I haven't worked out when it behaves each way.
|
||||||
|
"""
|
||||||
|
if not b:
|
||||||
|
raise ConnectionResetError(ECONNRESET, os.strerror(ECONNRESET))
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def prep_socket(addr, enable_fds=False, timeout=2.0) -> socket.socket:
|
||||||
|
"""Create a socket and authenticate ready to send D-Bus messages"""
|
||||||
|
sock = socket.socket(family=socket.AF_UNIX)
|
||||||
|
|
||||||
|
# To impose the overall auth timeout, we'll update the timeout on the socket
|
||||||
|
# before each send/receive. This is ugly, but we can't use the socket for
|
||||||
|
# anything else until this has succeeded, so this should be safe.
|
||||||
|
deadline = timeout_to_deadline(timeout)
|
||||||
|
def with_sock_deadline(meth, *args):
|
||||||
|
sock.settimeout(deadline_to_timeout(deadline))
|
||||||
|
return meth(*args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with_sock_deadline(sock.connect, addr)
|
||||||
|
authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False)
|
||||||
|
if hasattr(socket, 'SCM_CREDS'):
|
||||||
|
# BSD: send credentials message to authenticate (kernel fills in data)
|
||||||
|
sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))])
|
||||||
|
else:
|
||||||
|
# Linux: no ancillary data needed, bus checks with SO_PEERCRED
|
||||||
|
sock.send(b'\0')
|
||||||
|
for req_data in authr:
|
||||||
|
with_sock_deadline(sock.sendall, req_data)
|
||||||
|
authr.feed(unwrap_read(with_sock_deadline(sock.recv, 1024)))
|
||||||
|
with_sock_deadline(sock.sendall, BEGIN)
|
||||||
|
except socket.timeout as e:
|
||||||
|
sock.close()
|
||||||
|
raise TimeoutError(f"Did not authenticate in {timeout} seconds") from e
|
||||||
|
except:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
sock.settimeout(None) # Put the socket back in blocking mode
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def open_dbus_connection(
|
||||||
|
bus='SESSION', enable_fds=False, auth_timeout=1.,
|
||||||
|
) -> DBusConnection:
|
||||||
|
"""Connect to a D-Bus message bus
|
||||||
|
|
||||||
|
Pass ``enable_fds=True`` to allow sending & receiving file descriptors.
|
||||||
|
An error will be raised if the bus does not allow this. For simplicity,
|
||||||
|
it's advisable to leave this disabled unless you need it.
|
||||||
|
|
||||||
|
D-Bus has an authentication step before sending or receiving messages.
|
||||||
|
This takes < 1 ms in normal operation, but there is a timeout so that client
|
||||||
|
code won't get stuck if the server doesn't reply. *auth_timeout* configures
|
||||||
|
this timeout in seconds.
|
||||||
|
"""
|
||||||
|
bus_addr = get_bus(bus)
|
||||||
|
sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout)
|
||||||
|
|
||||||
|
conn = DBusConnection(sock, enable_fds)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
conn = open_dbus_connection()
|
||||||
|
print("Unique name:", conn.unique_name)
|
||||||
88
lib/jeepney/io/common.py
Normal file
88
lib/jeepney/io/common.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
from jeepney import HeaderFields, Message, MessageFlag, MessageType
|
||||||
|
|
||||||
|
class MessageFilters:
|
||||||
|
def __init__(self):
|
||||||
|
self.filters = {}
|
||||||
|
self.filter_ids = count()
|
||||||
|
|
||||||
|
def matches(self, message):
|
||||||
|
for handle in self.filters.values():
|
||||||
|
if handle.rule.matches(message):
|
||||||
|
yield handle
|
||||||
|
|
||||||
|
|
||||||
|
class FilterHandle:
|
||||||
|
def __init__(self, filters: MessageFilters, rule, queue):
|
||||||
|
self._filters = filters
|
||||||
|
self._filter_id = next(filters.filter_ids)
|
||||||
|
self.rule = rule
|
||||||
|
self.queue = queue
|
||||||
|
|
||||||
|
self._filters.filters[self._filter_id] = self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
del self._filters.filters[self._filter_id]
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self.queue
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ReplyMatcher:
|
||||||
|
def __init__(self):
|
||||||
|
self._futures = {}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def catch(self, serial, future):
|
||||||
|
"""Context manager to capture a reply for the given serial number"""
|
||||||
|
self._futures[serial] = future
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield future
|
||||||
|
finally:
|
||||||
|
del self._futures[serial]
|
||||||
|
|
||||||
|
def dispatch(self, msg):
|
||||||
|
"""Dispatch an incoming message which may be a reply
|
||||||
|
|
||||||
|
Returns True if a task was waiting for it, otherwise False.
|
||||||
|
"""
|
||||||
|
rep_serial = msg.header.fields.get(HeaderFields.reply_serial, -1)
|
||||||
|
if rep_serial in self._futures:
|
||||||
|
self._futures[rep_serial].set_result(msg)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def drop_all(self, exc: Exception = None):
|
||||||
|
"""Throw an error in any task still waiting for a reply"""
|
||||||
|
if exc is None:
|
||||||
|
exc = RouterClosed("D-Bus router closed before reply arrived")
|
||||||
|
futures, self._futures = self._futures, {}
|
||||||
|
for fut in futures.values():
|
||||||
|
fut.set_exception(exc)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterClosed(Exception):
|
||||||
|
"""Raised in tasks waiting for a reply when the router is closed
|
||||||
|
|
||||||
|
This will also be raised if the receiver task crashes, so tasks are not
|
||||||
|
stuck waiting for a reply that can never come. The router object will not
|
||||||
|
be usable after this is raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_replyable(msg: Message):
|
||||||
|
"""Raise an error if we wouldn't expect a reply for msg"""
|
||||||
|
if msg.header.message_type != MessageType.method_call:
|
||||||
|
raise TypeError("Only method call messages have replies "
|
||||||
|
f"(not {msg.header.message_type})")
|
||||||
|
if MessageFlag.no_reply_expected & msg.header.flags:
|
||||||
|
raise ValueError("This message has the no_reply_expected flag set")
|
||||||
0
lib/jeepney/io/tests/__init__.py
Normal file
0
lib/jeepney/io/tests/__init__.py
Normal file
BIN
lib/jeepney/io/tests/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/conftest.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/conftest.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/test_trio.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/test_trio.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/io/tests/__pycache__/utils.cpython-314.pyc
Normal file
BIN
lib/jeepney/io/tests/__pycache__/utils.cpython-314.pyc
Normal file
Binary file not shown.
81
lib/jeepney/io/tests/conftest.py
Normal file
81
lib/jeepney/io/tests/conftest.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from tempfile import TemporaryFile
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import (
|
||||||
|
DBusAddress, HeaderFields, message_bus, MessageType, new_error,
|
||||||
|
new_method_return,
|
||||||
|
)
|
||||||
|
from jeepney.io.threading import open_dbus_connection, DBusRouter, Proxy
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def respond_with_fd():
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.respond_with_fd"
|
||||||
|
addr = DBusAddress(bus_name=name, object_path='/')
|
||||||
|
|
||||||
|
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||||
|
with DBusRouter(conn) as router:
|
||||||
|
status, = Proxy(message_bus, router).RequestName(name)
|
||||||
|
assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
|
||||||
|
|
||||||
|
def _reply_once():
|
||||||
|
while True:
|
||||||
|
msg = conn.receive()
|
||||||
|
if msg.header.message_type is MessageType.method_call:
|
||||||
|
if msg.header.fields[HeaderFields.member] == 'GetFD':
|
||||||
|
with TemporaryFile('w+') as tf:
|
||||||
|
tf.write('readme')
|
||||||
|
tf.seek(0)
|
||||||
|
rep = new_method_return(msg, 'h', (tf,))
|
||||||
|
conn.send(rep)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
conn.send(new_error(msg, 'NoMethod'))
|
||||||
|
|
||||||
|
reply_thread = threading.Thread(target=_reply_once, daemon=True)
|
||||||
|
reply_thread.start()
|
||||||
|
yield addr
|
||||||
|
|
||||||
|
reply_thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def read_from_fd():
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.read_from_fd"
|
||||||
|
addr = DBusAddress(bus_name=name, object_path='/')
|
||||||
|
|
||||||
|
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||||
|
with DBusRouter(conn) as router:
|
||||||
|
status, = Proxy(message_bus, router).RequestName(name)
|
||||||
|
assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
|
||||||
|
|
||||||
|
def _reply_once():
|
||||||
|
while True:
|
||||||
|
msg = conn.receive()
|
||||||
|
if msg.header.message_type is MessageType.method_call:
|
||||||
|
if msg.header.fields[HeaderFields.member] == 'ReadFD':
|
||||||
|
with msg.body[0].to_file('rb') as f:
|
||||||
|
f.seek(0)
|
||||||
|
b = f.read()
|
||||||
|
conn.send(new_method_return(msg, 'ay', (b,)))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
conn.send(new_error(msg, 'NoMethod'))
|
||||||
|
|
||||||
|
reply_thread = threading.Thread(target=_reply_once, daemon=True)
|
||||||
|
reply_thread.start()
|
||||||
|
yield addr
|
||||||
|
|
||||||
|
reply_thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def temp_file_and_contents():
|
||||||
|
data = b'abc123'
|
||||||
|
with TemporaryFile('w+b') as tf:
|
||||||
|
tf.write(data)
|
||||||
|
tf.flush()
|
||||||
|
tf.seek(0)
|
||||||
|
yield tf, data
|
||||||
|
|
||||||
95
lib/jeepney/io/tests/test_asyncio.py
Normal file
95
lib/jeepney/io/tests/test_asyncio.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
from asyncio import timeout
|
||||||
|
else:
|
||||||
|
from async_timeout import timeout
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from jeepney import DBusAddress, new_method_call
|
||||||
|
from jeepney.bus_messages import message_bus, MatchRule
|
||||||
|
from jeepney.io.asyncio import (
|
||||||
|
open_dbus_connection, open_dbus_router, Proxy
|
||||||
|
)
|
||||||
|
from .utils import have_session_bus
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.asyncio,
|
||||||
|
pytest.mark.skipif(
|
||||||
|
not have_session_bus, reason="Tests require DBus session bus"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
bus_peer = DBusAddress(
|
||||||
|
bus_name='org.freedesktop.DBus',
|
||||||
|
object_path='/org/freedesktop/DBus',
|
||||||
|
interface='org.freedesktop.DBus.Peer'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture()
|
||||||
|
async def connection():
|
||||||
|
async with (await open_dbus_connection(bus='SESSION')) as conn:
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
async def test_connect(connection):
|
||||||
|
assert connection.unique_name.startswith(':')
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture()
|
||||||
|
async def router():
|
||||||
|
async with open_dbus_router(bus='SESSION') as router:
|
||||||
|
yield router
|
||||||
|
|
||||||
|
async def test_send_and_get_reply(router):
|
||||||
|
ping_call = new_method_call(bus_peer, 'Ping')
|
||||||
|
reply = await asyncio.wait_for(
|
||||||
|
router.send_and_get_reply(ping_call), timeout=5
|
||||||
|
)
|
||||||
|
assert reply.body == ()
|
||||||
|
|
||||||
|
async def test_proxy(router):
|
||||||
|
proxy = Proxy(message_bus, router)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||||
|
res = await proxy.RequestName(name)
|
||||||
|
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||||
|
|
||||||
|
has_owner, = await proxy.NameHasOwner(name)
|
||||||
|
assert has_owner is True
|
||||||
|
|
||||||
|
async def test_filter(router):
|
||||||
|
bus = Proxy(message_bus, router)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.asyncio_test_filter"
|
||||||
|
|
||||||
|
match_rule = MatchRule(
|
||||||
|
type="signal",
|
||||||
|
sender=message_bus.bus_name,
|
||||||
|
interface=message_bus.interface,
|
||||||
|
member="NameOwnerChanged",
|
||||||
|
path=message_bus.object_path,
|
||||||
|
)
|
||||||
|
match_rule.add_arg_condition(0, name)
|
||||||
|
|
||||||
|
# Ask the message bus to subscribe us to this signal
|
||||||
|
await bus.AddMatch(match_rule)
|
||||||
|
|
||||||
|
with router.filter(match_rule) as queue:
|
||||||
|
res, = await bus.RequestName(name)
|
||||||
|
assert res == 1 # 1: got the name
|
||||||
|
|
||||||
|
signal_msg = await asyncio.wait_for(queue.get(), timeout=2.0)
|
||||||
|
assert signal_msg.body == (name, '', router.unique_name)
|
||||||
|
|
||||||
|
async def test_recv_after_connect():
|
||||||
|
# Can't use here:
|
||||||
|
# 1. 'connection' fixture
|
||||||
|
# 2. asyncio.wait_for()
|
||||||
|
# If (1) and/or (2) is used, the error won't be triggered.
|
||||||
|
conn = await open_dbus_connection(bus='SESSION')
|
||||||
|
try:
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout(0):
|
||||||
|
await conn.receive()
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
84
lib/jeepney/io/tests/test_blocking.py
Normal file
84
lib/jeepney/io/tests/test_blocking.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import new_method_call, MessageType, DBusAddress
|
||||||
|
from jeepney.bus_messages import message_bus, MatchRule
|
||||||
|
from jeepney.io.blocking import open_dbus_connection, Proxy
|
||||||
|
from .utils import have_session_bus
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not have_session_bus, reason="Tests require DBus session bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def session_conn():
|
||||||
|
with open_dbus_connection(bus='SESSION') as conn:
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect(session_conn):
|
||||||
|
assert session_conn.unique_name.startswith(':')
|
||||||
|
|
||||||
|
bus_peer = DBusAddress(
|
||||||
|
bus_name='org.freedesktop.DBus',
|
||||||
|
object_path='/org/freedesktop/DBus',
|
||||||
|
interface='org.freedesktop.DBus.Peer'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_send_and_get_reply(session_conn):
|
||||||
|
ping_call = new_method_call(bus_peer, 'Ping')
|
||||||
|
reply = session_conn.send_and_get_reply(ping_call, timeout=5)
|
||||||
|
assert reply.header.message_type == MessageType.method_return
|
||||||
|
assert reply.body == ()
|
||||||
|
|
||||||
|
def test_proxy(session_conn):
|
||||||
|
proxy = Proxy(message_bus, session_conn, timeout=5)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||||
|
res = proxy.RequestName(name)
|
||||||
|
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||||
|
|
||||||
|
has_owner, = proxy.NameHasOwner(name, _timeout=3)
|
||||||
|
assert has_owner is True
|
||||||
|
|
||||||
|
def test_filter(session_conn):
|
||||||
|
bus = Proxy(message_bus, session_conn)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.blocking_test_filter"
|
||||||
|
|
||||||
|
match_rule = MatchRule(
|
||||||
|
type="signal",
|
||||||
|
sender=message_bus.bus_name,
|
||||||
|
interface=message_bus.interface,
|
||||||
|
member="NameOwnerChanged",
|
||||||
|
path=message_bus.object_path,
|
||||||
|
)
|
||||||
|
match_rule.add_arg_condition(0, name)
|
||||||
|
|
||||||
|
# Ask the message bus to subscribe us to this signal
|
||||||
|
bus.AddMatch(match_rule)
|
||||||
|
|
||||||
|
with session_conn.filter(match_rule) as matches:
|
||||||
|
res, = bus.RequestName(name)
|
||||||
|
assert res == 1 # 1: got the name
|
||||||
|
|
||||||
|
signal_msg = session_conn.recv_until_filtered(matches, timeout=2)
|
||||||
|
|
||||||
|
assert signal_msg.body == (name, '', session_conn.unique_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recv_fd(respond_with_fd):
|
||||||
|
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||||
|
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||||
|
reply = conn.send_and_get_reply(getfd_call, timeout=5)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
with reply.body[0].to_file('w+') as f:
|
||||||
|
assert f.read() == 'readme'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||||
|
temp_file, data = temp_file_and_contents
|
||||||
|
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||||
|
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||||
|
reply = conn.send_and_get_reply(readfd_call, timeout=5)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
assert reply.body[0] == data
|
||||||
83
lib/jeepney/io/tests/test_threading.py
Normal file
83
lib/jeepney/io/tests/test_threading.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import new_method_call, MessageType, DBusAddress
|
||||||
|
from jeepney.bus_messages import message_bus, MatchRule
|
||||||
|
from jeepney.io.threading import open_dbus_router, Proxy
|
||||||
|
from .utils import have_session_bus
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not have_session_bus, reason="Tests require DBus session bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def router():
|
||||||
|
with open_dbus_router(bus='SESSION') as conn:
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect(router):
|
||||||
|
assert router.unique_name.startswith(':')
|
||||||
|
|
||||||
|
bus_peer = DBusAddress(
|
||||||
|
bus_name='org.freedesktop.DBus',
|
||||||
|
object_path='/org/freedesktop/DBus',
|
||||||
|
interface='org.freedesktop.DBus.Peer'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_send_and_get_reply(router):
|
||||||
|
ping_call = new_method_call(bus_peer, 'Ping')
|
||||||
|
reply = router.send_and_get_reply(ping_call, timeout=5)
|
||||||
|
assert reply.header.message_type == MessageType.method_return
|
||||||
|
assert reply.body == ()
|
||||||
|
|
||||||
|
def test_proxy(router):
|
||||||
|
proxy = Proxy(message_bus, router, timeout=5)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||||
|
res = proxy.RequestName(name)
|
||||||
|
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||||
|
|
||||||
|
has_owner, = proxy.NameHasOwner(name, _timeout=3)
|
||||||
|
assert has_owner is True
|
||||||
|
|
||||||
|
def test_filter(router):
|
||||||
|
bus = Proxy(message_bus, router)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.threading_test_filter"
|
||||||
|
|
||||||
|
match_rule = MatchRule(
|
||||||
|
type="signal",
|
||||||
|
sender=message_bus.bus_name,
|
||||||
|
interface=message_bus.interface,
|
||||||
|
member="NameOwnerChanged",
|
||||||
|
path=message_bus.object_path,
|
||||||
|
)
|
||||||
|
match_rule.add_arg_condition(0, name)
|
||||||
|
|
||||||
|
# Ask the message bus to subscribe us to this signal
|
||||||
|
bus.AddMatch(match_rule)
|
||||||
|
|
||||||
|
with router.filter(match_rule) as queue:
|
||||||
|
res, = bus.RequestName(name)
|
||||||
|
assert res == 1 # 1: got the name
|
||||||
|
|
||||||
|
signal_msg = queue.get(timeout=2.0)
|
||||||
|
assert signal_msg.body == (name, '', router.unique_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_recv_fd(respond_with_fd):
|
||||||
|
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||||
|
with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||||
|
reply = router.send_and_get_reply(getfd_call, timeout=5)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
with reply.body[0].to_file('w+') as f:
|
||||||
|
assert f.read() == 'readme'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||||
|
temp_file, data = temp_file_and_contents
|
||||||
|
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||||
|
with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||||
|
reply = router.send_and_get_reply(readfd_call, timeout=5)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
assert reply.body[0] == data
|
||||||
114
lib/jeepney/io/tests/test_trio.py
Normal file
114
lib/jeepney/io/tests/test_trio.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import trio
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call
|
||||||
|
from jeepney.bus_messages import message_bus, MatchRule
|
||||||
|
from jeepney.io.trio import (
|
||||||
|
open_dbus_connection, open_dbus_router, Proxy,
|
||||||
|
)
|
||||||
|
from .utils import have_session_bus
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.trio,
|
||||||
|
pytest.mark.skipif(
|
||||||
|
not have_session_bus, reason="Tests require DBus session bus"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Can't use any async fixtures here, because pytest-asyncio tries to handle
|
||||||
|
# all of them: https://github.com/pytest-dev/pytest-asyncio/issues/124
|
||||||
|
|
||||||
|
async def test_connect():
|
||||||
|
conn = await open_dbus_connection(bus='SESSION')
|
||||||
|
async with conn:
|
||||||
|
assert conn.unique_name.startswith(':')
|
||||||
|
|
||||||
|
bus_peer = DBusAddress(
|
||||||
|
bus_name='org.freedesktop.DBus',
|
||||||
|
object_path='/org/freedesktop/DBus',
|
||||||
|
interface='org.freedesktop.DBus.Peer'
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_send_and_get_reply():
|
||||||
|
ping_call = new_method_call(bus_peer, 'Ping')
|
||||||
|
async with open_dbus_router(bus='SESSION') as req:
|
||||||
|
with trio.fail_after(5):
|
||||||
|
reply = await req.send_and_get_reply(ping_call)
|
||||||
|
|
||||||
|
assert reply.header.message_type == MessageType.method_return
|
||||||
|
assert reply.body == ()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_and_get_reply_error():
|
||||||
|
ping_call = new_method_call(bus_peer, 'Snart') # No such method
|
||||||
|
async with open_dbus_router(bus='SESSION') as req:
|
||||||
|
with trio.fail_after(5):
|
||||||
|
reply = await req.send_and_get_reply(ping_call)
|
||||||
|
|
||||||
|
assert reply.header.message_type == MessageType.error
|
||||||
|
|
||||||
|
|
||||||
|
async def test_proxy():
|
||||||
|
async with open_dbus_router(bus='SESSION') as req:
|
||||||
|
proxy = Proxy(message_bus, req)
|
||||||
|
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||||
|
res = await proxy.RequestName(name)
|
||||||
|
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||||
|
|
||||||
|
has_owner, = await proxy.NameHasOwner(name)
|
||||||
|
assert has_owner is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_proxy_error():
|
||||||
|
async with open_dbus_router(bus='SESSION') as req:
|
||||||
|
proxy = Proxy(message_bus, req)
|
||||||
|
with pytest.raises(DBusErrorResponse):
|
||||||
|
await proxy.RequestName(":123") # Invalid name
|
||||||
|
|
||||||
|
|
||||||
|
async def test_filter():
|
||||||
|
name = "io.gitlab.takluyver.jeepney.tests.trio_test_filter"
|
||||||
|
async with open_dbus_router(bus='SESSION') as router:
|
||||||
|
bus = Proxy(message_bus, router)
|
||||||
|
|
||||||
|
match_rule = MatchRule(
|
||||||
|
type="signal",
|
||||||
|
sender=message_bus.bus_name,
|
||||||
|
interface=message_bus.interface,
|
||||||
|
member="NameOwnerChanged",
|
||||||
|
path=message_bus.object_path,
|
||||||
|
)
|
||||||
|
match_rule.add_arg_condition(0, name)
|
||||||
|
|
||||||
|
# Ask the message bus to subscribe us to this signal
|
||||||
|
await bus.AddMatch(match_rule)
|
||||||
|
|
||||||
|
async with router.filter(match_rule) as chan:
|
||||||
|
res, = await bus.RequestName(name)
|
||||||
|
assert res == 1 # 1: got the name
|
||||||
|
|
||||||
|
with trio.fail_after(2.0):
|
||||||
|
signal_msg = await chan.receive()
|
||||||
|
assert signal_msg.body == (name, '', router.unique_name)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_recv_fd(respond_with_fd):
|
||||||
|
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||||
|
with trio.fail_after(5):
|
||||||
|
async with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||||
|
reply = await router.send_and_get_reply(getfd_call)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
with reply.body[0].to_file('w+') as f:
|
||||||
|
assert f.read() == 'readme'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||||
|
temp_file, data = temp_file_and_contents
|
||||||
|
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||||
|
with trio.fail_after(5):
|
||||||
|
async with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||||
|
reply = await router.send_and_get_reply(readfd_call)
|
||||||
|
|
||||||
|
assert reply.header.message_type is MessageType.method_return
|
||||||
|
assert reply.body[0] == data
|
||||||
3
lib/jeepney/io/tests/utils.py
Normal file
3
lib/jeepney/io/tests/utils.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
have_session_bus = bool(os.environ.get('DBUS_SESSION_BUS_ADDRESS'))
|
||||||
273
lib/jeepney/io/threading.py
Normal file
273
lib/jeepney/io/threading.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
"""Synchronous IO wrappers with thread safety
|
||||||
|
"""
|
||||||
|
from concurrent.futures import Future
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
from selectors import EVENT_READ
|
||||||
|
import socket
|
||||||
|
from queue import Queue, Full as QueueFull
|
||||||
|
from threading import Lock, Thread
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from jeepney import Message, MessageType
|
||||||
|
from jeepney.bus import get_bus
|
||||||
|
from jeepney.bus_messages import message_bus
|
||||||
|
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||||
|
from .blocking import (
|
||||||
|
unwrap_read, prep_socket, DBusConnectionBase, timeout_to_deadline,
|
||||||
|
)
|
||||||
|
from .common import (
|
||||||
|
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'open_dbus_connection',
|
||||||
|
'open_dbus_router',
|
||||||
|
'DBusConnection',
|
||||||
|
'DBusRouter',
|
||||||
|
'Proxy',
|
||||||
|
'ReceiveStopped',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiveStopped(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DBusConnection(DBusConnectionBase):
|
||||||
|
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||||
|
super().__init__(sock, enable_fds=enable_fds)
|
||||||
|
self._stop_r, self._stop_w = os.pipe()
|
||||||
|
self.stop_key = self.selector.register(self._stop_r, EVENT_READ)
|
||||||
|
self.send_lock = Lock()
|
||||||
|
self.rcv_lock = Lock()
|
||||||
|
|
||||||
|
def send(self, message: Message, serial=None):
|
||||||
|
"""Serialise and send a :class:`~.Message` object"""
|
||||||
|
data, fds = self._serialise(message, serial)
|
||||||
|
with self.send_lock:
|
||||||
|
if fds:
|
||||||
|
self._send_with_fds(data, fds)
|
||||||
|
else:
|
||||||
|
self.sock.sendall(data)
|
||||||
|
|
||||||
|
def receive(self, *, timeout=None) -> Message:
|
||||||
|
"""Return the next available message from the connection
|
||||||
|
|
||||||
|
If the data is ready, this will return immediately, even if timeout<=0.
|
||||||
|
Otherwise, it will wait for up to timeout seconds, or indefinitely if
|
||||||
|
timeout is None. If no message comes in time, it raises TimeoutError.
|
||||||
|
|
||||||
|
If the connection is closed from another thread, this will raise
|
||||||
|
ReceiveStopped.
|
||||||
|
"""
|
||||||
|
deadline = timeout_to_deadline(timeout)
|
||||||
|
|
||||||
|
if not self.rcv_lock.acquire(timeout=(timeout or -1)):
|
||||||
|
raise TimeoutError(f"Did not get receive lock in {timeout} seconds")
|
||||||
|
try:
|
||||||
|
return self._receive(deadline)
|
||||||
|
finally:
|
||||||
|
self.rcv_lock.release()
|
||||||
|
|
||||||
|
def _read_some_data(self, timeout=None):
|
||||||
|
# Wait for data or a signal on the stop pipe
|
||||||
|
for key, ev in self.selector.select(timeout):
|
||||||
|
if key == self.select_key:
|
||||||
|
if self.enable_fds:
|
||||||
|
return self._read_with_fds()
|
||||||
|
else:
|
||||||
|
return unwrap_read(self.sock.recv(4096)), []
|
||||||
|
elif key == self.stop_key:
|
||||||
|
raise ReceiveStopped("DBus receive stopped from another thread")
|
||||||
|
|
||||||
|
raise TimeoutError
|
||||||
|
|
||||||
|
def interrupt(self):
|
||||||
|
"""Make any threads waiting for a message raise ReceiveStopped"""
|
||||||
|
os.write(self._stop_w, b'a')
|
||||||
|
|
||||||
|
def reset_interrupt(self):
|
||||||
|
"""Allow calls to .receive() again after .interrupt()
|
||||||
|
|
||||||
|
To avoid race conditions, you should typically wait for threads to
|
||||||
|
respond (e.g. by joining them) between interrupting and resetting.
|
||||||
|
"""
|
||||||
|
# Clear any data on the stop pipe
|
||||||
|
while (self.stop_key, EVENT_READ) in self.selector.select(timeout=0):
|
||||||
|
os.read(self._stop_r, 1024)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the connection"""
|
||||||
|
self.interrupt()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
|
||||||
|
def open_dbus_connection(bus='SESSION', enable_fds=False, auth_timeout=1.):
|
||||||
|
"""Open a plain D-Bus connection
|
||||||
|
|
||||||
|
D-Bus has an authentication step before sending or receiving messages.
|
||||||
|
This takes < 1 ms in normal operation, but there is a timeout so that client
|
||||||
|
code won't get stuck if the server doesn't reply. *auth_timeout* configures
|
||||||
|
this timeout in seconds.
|
||||||
|
|
||||||
|
:return: :class:`DBusConnection`
|
||||||
|
"""
|
||||||
|
bus_addr = get_bus(bus)
|
||||||
|
sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout)
|
||||||
|
|
||||||
|
conn = DBusConnection(sock, enable_fds)
|
||||||
|
|
||||||
|
with DBusRouter(conn) as router:
|
||||||
|
reply_body = Proxy(message_bus, router, timeout=10).Hello()
|
||||||
|
conn.unique_name = reply_body[0]
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class DBusRouter:
|
||||||
|
"""A client D-Bus connection which can wait for replies.
|
||||||
|
|
||||||
|
This runs a separate receiver thread and dispatches received messages.
|
||||||
|
|
||||||
|
It's possible to wrap a :class:`DBusConnection` in a router temporarily.
|
||||||
|
Using the connection directly while it is wrapped is not supported,
|
||||||
|
but you can use it again after the router is closed.
|
||||||
|
"""
|
||||||
|
def __init__(self, conn: DBusConnection):
|
||||||
|
self.conn = conn
|
||||||
|
self._replies = ReplyMatcher()
|
||||||
|
self._filters = MessageFilters()
|
||||||
|
self._rcv_thread = Thread(target=self._receiver, daemon=True)
|
||||||
|
self._rcv_thread.start()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_name(self):
|
||||||
|
return self.conn.unique_name
|
||||||
|
|
||||||
|
def send(self, message, *, serial=None):
|
||||||
|
"""Serialise and send a :class:`~.Message` object"""
|
||||||
|
self.conn.send(message, serial=serial)
|
||||||
|
|
||||||
|
def send_and_get_reply(self, msg: Message, *, timeout=None) -> Message:
|
||||||
|
"""Send a method call message, wait for and return a reply"""
|
||||||
|
check_replyable(msg)
|
||||||
|
if not self._rcv_thread.is_alive():
|
||||||
|
raise RouterClosed("This D-Bus router has stopped")
|
||||||
|
|
||||||
|
serial = next(self.conn.outgoing_serial)
|
||||||
|
|
||||||
|
with self._replies.catch(serial, Future()) as reply_fut:
|
||||||
|
self.conn.send(msg, serial=serial)
|
||||||
|
return reply_fut.result(timeout=timeout)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close this router
|
||||||
|
|
||||||
|
This does not close the underlying connection.
|
||||||
|
"""
|
||||||
|
self.conn.interrupt()
|
||||||
|
self._rcv_thread.join(timeout=10)
|
||||||
|
self.conn.reset_interrupt()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def filter(self, rule, *, queue: Optional[Queue] =None, bufsize=1):
|
||||||
|
"""Create a filter for incoming messages
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
with router.filter(rule) as queue:
|
||||||
|
matching_msg = queue.get()
|
||||||
|
|
||||||
|
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||||
|
:param queue.Queue queue: Matched messages will be added to this
|
||||||
|
:param int bufsize: If no queue is passed in, create one with this size
|
||||||
|
"""
|
||||||
|
return FilterHandle(self._filters, rule, queue or Queue(maxsize=bufsize))
|
||||||
|
|
||||||
|
# Code to run in receiver thread ------------------------------------
|
||||||
|
|
||||||
|
def _dispatch(self, msg: Message):
|
||||||
|
if self._replies.dispatch(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
for filter in self._filters.matches(msg):
|
||||||
|
try:
|
||||||
|
filter.queue.put_nowait(msg)
|
||||||
|
except QueueFull:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _receiver(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = self.conn.receive()
|
||||||
|
self._dispatch(msg)
|
||||||
|
except ReceiveStopped:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
# Send errors to any tasks still waiting for a message.
|
||||||
|
self._replies.drop_all()
|
||||||
|
|
||||||
|
class Proxy(ProxyBase):
|
||||||
|
"""A blocking proxy for calling D-Bus methods via a :class:`DBusRouter`.
|
||||||
|
|
||||||
|
You can call methods on the proxy object, such as ``bus_proxy.Hello()``
|
||||||
|
to make a method call over D-Bus and wait for a reply. It will either
|
||||||
|
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||||
|
The methods available are defined by the message generator you wrap.
|
||||||
|
|
||||||
|
You can set a time limit on a call by passing ``_timeout=`` in the method
|
||||||
|
call, or set a default when creating the proxy. The ``_timeout`` argument
|
||||||
|
is not passed to the message generator.
|
||||||
|
All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it
|
||||||
|
expires before a reply arrives.
|
||||||
|
|
||||||
|
:param msggen: A message generator object
|
||||||
|
:param ~threading.DBusRouter router: Router to send and receive messages
|
||||||
|
:param float timeout: Default seconds to wait for a reply, or None for no limit
|
||||||
|
"""
|
||||||
|
def __init__(self, msggen, router, *, timeout=None):
|
||||||
|
super().__init__(msggen)
|
||||||
|
self._router = router
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
extra = '' if (self._timeout is None) else f', timeout={self._timeout}'
|
||||||
|
return f"Proxy({self._msggen}, {self._router}{extra})"
|
||||||
|
|
||||||
|
def _method_call(self, make_msg):
|
||||||
|
@functools.wraps(make_msg)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
timeout = kwargs.pop('_timeout', self._timeout)
|
||||||
|
msg = make_msg(*args, **kwargs)
|
||||||
|
assert msg.header.message_type is MessageType.method_call
|
||||||
|
reply = self._router.send_and_get_reply(msg, timeout=timeout)
|
||||||
|
return unwrap_msg(reply)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open_dbus_router(bus='SESSION', enable_fds=False):
|
||||||
|
"""Open a D-Bus 'router' to send and receive messages.
|
||||||
|
|
||||||
|
Use as a context manager::
|
||||||
|
|
||||||
|
with open_dbus_router() as router:
|
||||||
|
...
|
||||||
|
|
||||||
|
On leaving the ``with`` block, the connection will be closed.
|
||||||
|
|
||||||
|
:param str bus: 'SESSION' or 'SYSTEM' or a supported address.
|
||||||
|
:param bool enable_fds: Whether to enable passing file descriptors.
|
||||||
|
:return: :class:`DBusRouter`
|
||||||
|
"""
|
||||||
|
with open_dbus_connection(bus=bus, enable_fds=enable_fds) as conn:
|
||||||
|
with DBusRouter(conn) as router:
|
||||||
|
yield router
|
||||||
424
lib/jeepney/io/trio.py
Normal file
424
lib/jeepney/io/trio.py
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import array
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from contextlib import asynccontextmanager, contextmanager
|
||||||
|
from itertools import count
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from outcome import Value, Error
|
||||||
|
import trio
|
||||||
|
from trio.abc import Channel
|
||||||
|
|
||||||
|
from jeepney.auth import Authenticator, BEGIN
|
||||||
|
from jeepney.bus import get_bus
|
||||||
|
from jeepney.fds import FileDescriptor, fds_buf_size
|
||||||
|
from jeepney.low_level import Parser, MessageType, Message
|
||||||
|
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||||
|
from jeepney.bus_messages import message_bus
|
||||||
|
from .common import (
|
||||||
|
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'open_dbus_connection',
|
||||||
|
'open_dbus_router',
|
||||||
|
'Proxy',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# The function below is copied from trio, which is under the MIT license:
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
@contextmanager
|
||||||
|
def _translate_socket_errors_to_stream_errors():
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno in {errno.EBADF, errno.ENOTSOCK}:
|
||||||
|
# EBADF on Unix, ENOTSOCK on Windows
|
||||||
|
raise trio.ClosedResourceError("this socket was already closed") from None
|
||||||
|
else:
|
||||||
|
raise trio.BrokenResourceError(
|
||||||
|
"socket connection broken: {}".format(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DBusConnection(Channel):
|
||||||
|
"""A plain D-Bus connection with no matching of replies.
|
||||||
|
|
||||||
|
This doesn't run any separate tasks: sending and receiving are done in
|
||||||
|
the task that calls those methods. It's suitable for implementing servers:
|
||||||
|
several worker tasks can receive requests and send replies.
|
||||||
|
For a typical client pattern, see :class:`DBusRouter`.
|
||||||
|
|
||||||
|
Implements trio's channel interface for Message objects.
|
||||||
|
"""
|
||||||
|
def __init__(self, socket, enable_fds=False):
|
||||||
|
self.socket = socket
|
||||||
|
self.enable_fds = enable_fds
|
||||||
|
self.parser = Parser()
|
||||||
|
self.outgoing_serial = count(start=1)
|
||||||
|
self.unique_name = None
|
||||||
|
self.send_lock = trio.Lock()
|
||||||
|
self.recv_lock = trio.Lock()
|
||||||
|
self._leftover_to_send = None # type: Optional[memoryview]
|
||||||
|
|
||||||
|
async def send(self, message: Message, *, serial=None):
|
||||||
|
"""Serialise and send a :class:`~.Message` object"""
|
||||||
|
async with self.send_lock:
|
||||||
|
if serial is None:
|
||||||
|
serial = next(self.outgoing_serial)
|
||||||
|
fds = array.array('i') if self.enable_fds else None
|
||||||
|
data = message.serialise(serial, fds=fds)
|
||||||
|
await self._send_data(data, fds)
|
||||||
|
|
||||||
|
# _send_data is copied & modified from trio's SocketStream.send_all() .
|
||||||
|
# See above for the MIT license.
|
||||||
|
async def _send_data(self, data: bytes, fds):
|
||||||
|
if self.socket.did_shutdown_SHUT_WR:
|
||||||
|
raise trio.ClosedResourceError("can't send data after sending EOF")
|
||||||
|
|
||||||
|
with _translate_socket_errors_to_stream_errors():
|
||||||
|
if self._leftover_to_send:
|
||||||
|
# A previous message was partly sent - finish sending it now.
|
||||||
|
await self._send_remainder(self._leftover_to_send)
|
||||||
|
|
||||||
|
with memoryview(data) as data:
|
||||||
|
if fds:
|
||||||
|
sent = await self.socket.sendmsg([data], [(
|
||||||
|
trio.socket.SOL_SOCKET, trio.socket.SCM_RIGHTS, fds
|
||||||
|
)])
|
||||||
|
else:
|
||||||
|
sent = await self.socket.send(data)
|
||||||
|
|
||||||
|
await self._send_remainder(data, sent)
|
||||||
|
|
||||||
|
async def _send_remainder(self, data: memoryview, already_sent=0):
|
||||||
|
try:
|
||||||
|
while already_sent < len(data):
|
||||||
|
with data[already_sent:] as remaining:
|
||||||
|
sent = await self.socket.send(remaining)
|
||||||
|
already_sent += sent
|
||||||
|
self._leftover_to_send = None
|
||||||
|
except trio.Cancelled:
|
||||||
|
# Sending cancelled mid-message. Keep track of the remaining data
|
||||||
|
# so it can be sent before the next message, otherwise the next
|
||||||
|
# message won't be recognised.
|
||||||
|
self._leftover_to_send = data[already_sent:]
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def receive(self) -> Message:
|
||||||
|
"""Return the next available message from the connection"""
|
||||||
|
async with self.recv_lock:
|
||||||
|
while True:
|
||||||
|
msg = self.parser.get_next_message()
|
||||||
|
if msg is not None:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
# Once data is read, it must be given to the parser with no
|
||||||
|
# checkpoints (where the task could be cancelled).
|
||||||
|
b, fds = await self._read_data()
|
||||||
|
if not b:
|
||||||
|
raise trio.EndOfChannel("Socket closed at the other end")
|
||||||
|
self.parser.add_data(b, fds)
|
||||||
|
|
||||||
|
async def _read_data(self):
|
||||||
|
if self.enable_fds:
|
||||||
|
nbytes = self.parser.bytes_desired()
|
||||||
|
with _translate_socket_errors_to_stream_errors():
|
||||||
|
data, ancdata, flags, _ = await self.socket.recvmsg(
|
||||||
|
nbytes, fds_buf_size()
|
||||||
|
)
|
||||||
|
if flags & getattr(trio.socket, 'MSG_CTRUNC', 0):
|
||||||
|
self._close()
|
||||||
|
raise RuntimeError("Unable to receive all file descriptors")
|
||||||
|
return data, FileDescriptor.from_ancdata(ancdata)
|
||||||
|
|
||||||
|
else: # not self.enable_fds
|
||||||
|
with _translate_socket_errors_to_stream_errors():
|
||||||
|
data = await self.socket.recv(4096)
|
||||||
|
return data, []
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
self.socket.close()
|
||||||
|
self._leftover_to_send = None
|
||||||
|
|
||||||
|
# Our closing is currently sync, but AsyncResource objects must have aclose
|
||||||
|
async def aclose(self):
|
||||||
|
"""Close the D-Bus connection"""
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def router(self):
|
||||||
|
"""Temporarily wrap this connection as a :class:`DBusRouter`
|
||||||
|
|
||||||
|
To be used like::
|
||||||
|
|
||||||
|
async with conn.router() as req:
|
||||||
|
reply = await req.send_and_get_reply(msg)
|
||||||
|
|
||||||
|
While the router is running, you shouldn't use :meth:`receive`.
|
||||||
|
Once the router is closed, you can use the plain connection again.
|
||||||
|
"""
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
router = DBusRouter(self)
|
||||||
|
await router.start(nursery)
|
||||||
|
try:
|
||||||
|
yield router
|
||||||
|
finally:
|
||||||
|
await router.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
async def open_dbus_connection(bus='SESSION', *, enable_fds=False) -> DBusConnection:
|
||||||
|
"""Open a plain D-Bus connection
|
||||||
|
|
||||||
|
:return: :class:`DBusConnection`
|
||||||
|
"""
|
||||||
|
bus_addr = get_bus(bus)
|
||||||
|
sock : trio.SocketStream = await trio.open_unix_socket(bus_addr)
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False)
|
||||||
|
if hasattr(socket, 'SCM_CREDS'):
|
||||||
|
# BSD: send credentials message to authenticate (kernel fills in data)
|
||||||
|
await sock.socket.sendmsg(
|
||||||
|
[b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Linux: no ancillary data needed, bus checks with SO_PEERCRED
|
||||||
|
await sock.send_all(b'\0')
|
||||||
|
for req_data in authr:
|
||||||
|
await sock.send_all(req_data)
|
||||||
|
authr.feed(await sock.receive_some())
|
||||||
|
await sock.send_all(BEGIN)
|
||||||
|
|
||||||
|
conn = DBusConnection(sock.socket, enable_fds=enable_fds)
|
||||||
|
|
||||||
|
# Say *Hello* to the message bus - this must be the first message, and the
|
||||||
|
# reply gives us our unique name.
|
||||||
|
async with conn.router() as router:
|
||||||
|
reply = await router.send_and_get_reply(message_bus.Hello())
|
||||||
|
conn.unique_name = reply.body[0]
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class TrioFilterHandle(FilterHandle):
|
||||||
|
def __init__(self, filters: MessageFilters, rule, send_chn, recv_chn):
|
||||||
|
super().__init__(filters, rule, recv_chn)
|
||||||
|
self.send_channel = send_chn
|
||||||
|
|
||||||
|
@property
|
||||||
|
def receive_channel(self):
|
||||||
|
return self.queue
|
||||||
|
|
||||||
|
async def aclose(self):
|
||||||
|
self.close()
|
||||||
|
await self.send_channel.aclose()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self.queue
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
class Future:
|
||||||
|
"""A very simple Future for trio based on `trio.Event`."""
|
||||||
|
def __init__(self):
|
||||||
|
self._outcome = None
|
||||||
|
self._event = trio.Event()
|
||||||
|
|
||||||
|
def set_result(self, result):
|
||||||
|
self._outcome = Value(result)
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
def set_exception(self, exc):
|
||||||
|
self._outcome = Error(exc)
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
await self._event.wait()
|
||||||
|
return self._outcome.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
class DBusRouter:
|
||||||
|
"""A client D-Bus connection which can wait for replies.
|
||||||
|
|
||||||
|
This runs a separate receiver task and dispatches received messages.
|
||||||
|
"""
|
||||||
|
_nursery_mgr = None
|
||||||
|
_rcv_cancel_scope = None
|
||||||
|
|
||||||
|
def __init__(self, conn: DBusConnection):
|
||||||
|
self._conn = conn
|
||||||
|
self._replies = ReplyMatcher()
|
||||||
|
self._filters = MessageFilters()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_name(self):
|
||||||
|
return self._conn.unique_name
|
||||||
|
|
||||||
|
async def send(self, message, *, serial=None):
|
||||||
|
"""Send a message, don't wait for a reply
|
||||||
|
"""
|
||||||
|
await self._conn.send(message, serial=serial)
|
||||||
|
|
||||||
|
async def send_and_get_reply(self, message) -> Message:
|
||||||
|
"""Send a method call message and wait for the reply
|
||||||
|
|
||||||
|
Returns the reply message (method return or error message type).
|
||||||
|
"""
|
||||||
|
check_replyable(message)
|
||||||
|
if self._rcv_cancel_scope is None:
|
||||||
|
raise RouterClosed("This DBusRouter has stopped")
|
||||||
|
|
||||||
|
serial = next(self._conn.outgoing_serial)
|
||||||
|
|
||||||
|
with self._replies.catch(serial, Future()) as reply_fut:
|
||||||
|
await self.send(message, serial=serial)
|
||||||
|
return (await reply_fut.get())
|
||||||
|
|
||||||
|
def filter(self, rule, *, channel: Optional[trio.MemorySendChannel]=None, bufsize=1):
|
||||||
|
"""Create a filter for incoming messages
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
async with router.filter(rule) as receive_channel:
|
||||||
|
matching_msg = await receive_channel.receive()
|
||||||
|
|
||||||
|
# OR:
|
||||||
|
send_chan, recv_chan = trio.open_memory_channel(1)
|
||||||
|
async with router.filter(rule, channel=send_chan):
|
||||||
|
matching_msg = await recv_chan.receive()
|
||||||
|
|
||||||
|
If the channel fills up,
|
||||||
|
The sending end of the channel is closed when leaving the ``async with``
|
||||||
|
block, whether or not it was passed in.
|
||||||
|
|
||||||
|
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||||
|
:param trio.MemorySendChannel channel: Send matching messages here
|
||||||
|
:param int bufsize: If no channel is passed in, create one with this size
|
||||||
|
"""
|
||||||
|
if channel is None:
|
||||||
|
channel, recv_channel = trio.open_memory_channel(bufsize)
|
||||||
|
else:
|
||||||
|
recv_channel = None
|
||||||
|
return TrioFilterHandle(self._filters, rule, channel, recv_channel)
|
||||||
|
|
||||||
|
# Task management -------------------------------------------
|
||||||
|
|
||||||
|
async def start(self, nursery: trio.Nursery):
|
||||||
|
if self._rcv_cancel_scope is not None:
|
||||||
|
raise RuntimeError("DBusRouter receiver task is already running")
|
||||||
|
self._rcv_cancel_scope = await nursery.start(self._receiver)
|
||||||
|
|
||||||
|
async def aclose(self):
|
||||||
|
"""Stop the sender & receiver tasks"""
|
||||||
|
# It doesn't matter if we receive a partial message - the connection
|
||||||
|
# should ensure that whatever is received is fed to the parser.
|
||||||
|
if self._rcv_cancel_scope is not None:
|
||||||
|
self._rcv_cancel_scope.cancel()
|
||||||
|
self._rcv_cancel_scope = None
|
||||||
|
|
||||||
|
# Ensure trio checkpoint
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# Code to run in receiver task ------------------------------------
|
||||||
|
|
||||||
|
def _dispatch(self, msg: Message):
|
||||||
|
"""Handle one received message"""
|
||||||
|
if self._replies.dispatch(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
for filter in self._filters.matches(msg):
|
||||||
|
try:
|
||||||
|
filter.send_channel.send_nowait(msg)
|
||||||
|
except trio.WouldBlock:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _receiver(self, task_status=trio.TASK_STATUS_IGNORED):
|
||||||
|
"""Receiver loop - runs in a separate task"""
|
||||||
|
with trio.CancelScope() as cscope:
|
||||||
|
self.is_running = True
|
||||||
|
task_status.started(cscope)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = await self._conn.receive()
|
||||||
|
self._dispatch(msg)
|
||||||
|
finally:
|
||||||
|
self.is_running = False
|
||||||
|
# Send errors to any tasks still waiting for a message.
|
||||||
|
self._replies.drop_all()
|
||||||
|
|
||||||
|
# Closing a memory channel can't block, but it only has an
|
||||||
|
# async close method, so we need to shield it from cancellation.
|
||||||
|
with trio.move_on_after(3) as cleanup_scope:
|
||||||
|
for filter in self._filters.filters.values():
|
||||||
|
cleanup_scope.shield = True
|
||||||
|
await filter.send_channel.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(ProxyBase):
|
||||||
|
"""A trio proxy for calling D-Bus methods
|
||||||
|
|
||||||
|
You can call methods on the proxy object, such as ``await bus_proxy.Hello()``
|
||||||
|
to make a method call over D-Bus and wait for a reply. It will either
|
||||||
|
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||||
|
The methods available are defined by the message generator you wrap.
|
||||||
|
|
||||||
|
:param msggen: A message generator object.
|
||||||
|
:param ~trio.DBusRouter router: Router to send and receive messages.
|
||||||
|
"""
|
||||||
|
def __init__(self, msggen, router):
|
||||||
|
super().__init__(msggen)
|
||||||
|
if not isinstance(router, DBusRouter):
|
||||||
|
raise TypeError("Proxy can only be used with DBusRequester")
|
||||||
|
self._router = router
|
||||||
|
|
||||||
|
def _method_call(self, make_msg):
|
||||||
|
async def inner(*args, **kwargs):
|
||||||
|
msg = make_msg(*args, **kwargs)
|
||||||
|
assert msg.header.message_type is MessageType.method_call
|
||||||
|
reply = await self._router.send_and_get_reply(msg)
|
||||||
|
return unwrap_msg(reply)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_dbus_router(bus='SESSION', *, enable_fds=False):
|
||||||
|
"""Open a D-Bus 'router' to send and receive messages.
|
||||||
|
|
||||||
|
Use as an async context manager::
|
||||||
|
|
||||||
|
async with open_dbus_router() as req:
|
||||||
|
...
|
||||||
|
|
||||||
|
:param str bus: 'SESSION' or 'SYSTEM' or a supported address.
|
||||||
|
:return: :class:`DBusRouter`
|
||||||
|
|
||||||
|
This is a shortcut for::
|
||||||
|
|
||||||
|
conn = await open_dbus_connection()
|
||||||
|
async with conn:
|
||||||
|
async with conn.router() as req:
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
conn = await open_dbus_connection(bus, enable_fds=enable_fds)
|
||||||
|
async with conn:
|
||||||
|
async with conn.router() as rtr:
|
||||||
|
yield rtr
|
||||||
608
lib/jeepney/low_level.py
Normal file
608
lib/jeepney/low_level.py
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
import string
|
||||||
|
import struct
|
||||||
|
from collections import deque
|
||||||
|
from enum import Enum, IntEnum, IntFlag
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class SizeLimitError(ValueError):
|
||||||
|
"""Raised when trying to (de-)serialise data exceeding D-Bus' size limit.
|
||||||
|
|
||||||
|
This is currently only implemented for arrays, where the maximum size is
|
||||||
|
64 MiB.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Endianness(Enum):
|
||||||
|
little = 1
|
||||||
|
big = 2
|
||||||
|
|
||||||
|
def struct_code(self):
|
||||||
|
return '<' if (self is Endianness.little) else '>'
|
||||||
|
|
||||||
|
def dbus_code(self):
|
||||||
|
return b'l' if (self is Endianness.little) else b'B'
|
||||||
|
|
||||||
|
|
||||||
|
endian_map = {b'l': Endianness.little, b'B': Endianness.big}
|
||||||
|
|
||||||
|
|
||||||
|
class MessageType(Enum):
|
||||||
|
method_call = 1
|
||||||
|
method_return = 2
|
||||||
|
error = 3
|
||||||
|
signal = 4
|
||||||
|
|
||||||
|
|
||||||
|
class MessageFlag(IntFlag):
|
||||||
|
no_reply_expected = 1
|
||||||
|
no_auto_start = 2
|
||||||
|
allow_interactive_authorization = 4
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderFields(IntEnum):
|
||||||
|
path = 1
|
||||||
|
interface = 2
|
||||||
|
member = 3
|
||||||
|
error_name = 4
|
||||||
|
reply_serial = 5
|
||||||
|
destination = 6
|
||||||
|
sender = 7
|
||||||
|
signature = 8
|
||||||
|
unix_fds = 9
|
||||||
|
|
||||||
|
|
||||||
|
def padding(pos, step):
|
||||||
|
pad = step - (pos % step)
|
||||||
|
if pad == step:
|
||||||
|
return 0
|
||||||
|
return pad
|
||||||
|
|
||||||
|
|
||||||
|
class FixedType:
|
||||||
|
def __init__(self, size, struct_code):
|
||||||
|
self.size = self.alignment = size
|
||||||
|
self.struct_code = struct_code
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
pos += padding(pos, self.alignment)
|
||||||
|
code = endianness.struct_code() + self.struct_code
|
||||||
|
val = struct.unpack(code, buf[pos:pos + self.size])[0]
|
||||||
|
return val, pos + self.size
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
pad = b'\0' * padding(pos, self.alignment)
|
||||||
|
code = endianness.struct_code() + self.struct_code
|
||||||
|
return pad + struct.pack(code, data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'FixedType({!r}, {!r})'.format(self.size, self.struct_code)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(other) is FixedType) and (self.size == other.size) \
|
||||||
|
and (self.struct_code == other.struct_code)
|
||||||
|
|
||||||
|
|
||||||
|
class Boolean(FixedType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(4, 'I') # D-Bus booleans take 4 bytes
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
val, new_pos = super().parse_data(buf, pos, endianness)
|
||||||
|
return bool(val), new_pos
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Boolean()'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return type(other) is Boolean
|
||||||
|
|
||||||
|
|
||||||
|
class FileDescriptor(FixedType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(4, 'I')
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
idx, new_pos = super().parse_data(buf, pos, endianness)
|
||||||
|
return fds[idx], new_pos
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
if fds is None:
|
||||||
|
raise RuntimeError("Sending FDs is not supported or not enabled")
|
||||||
|
|
||||||
|
if hasattr(data, 'fileno'):
|
||||||
|
data = data.fileno()
|
||||||
|
if isinstance(data, bool) or not isinstance(data, int):
|
||||||
|
raise TypeError("Cannot use {data!r} as file descriptor. Expected "
|
||||||
|
"an int or an object with fileno() method")
|
||||||
|
|
||||||
|
if data < 0:
|
||||||
|
raise ValueError(f"File descriptor can't be negative ({data})")
|
||||||
|
|
||||||
|
fds.append(data)
|
||||||
|
return super().serialise(len(fds) - 1, pos, endianness)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'FileDescriptor()'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return type(other) is FileDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
simple_types = {
|
||||||
|
'y': FixedType(1, 'B'), # unsigned 8 bit
|
||||||
|
'n': FixedType(2, 'h'), # signed 16 bit
|
||||||
|
'q': FixedType(2, 'H'), # unsigned 16 bit
|
||||||
|
'b': Boolean(), # bool (32-bit)
|
||||||
|
'i': FixedType(4, 'i'), # signed 32-bit
|
||||||
|
'u': FixedType(4, 'I'), # unsigned 32-bit
|
||||||
|
'x': FixedType(8, 'q'), # signed 64-bit
|
||||||
|
't': FixedType(8, 'Q'), # unsigned 64-bit
|
||||||
|
'd': FixedType(8, 'd'), # double
|
||||||
|
'h': FileDescriptor(), # file descriptor (uint32 index in a separate list)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StringType:
|
||||||
|
def __init__(self, length_type):
|
||||||
|
self.length_type = length_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alignment(self):
|
||||||
|
return self.length_type.size
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
length, pos = self.length_type.parse_data(buf, pos, endianness)
|
||||||
|
end = pos + length
|
||||||
|
val = buf[pos:end].decode('utf-8')
|
||||||
|
assert buf[end:end + 1] == b'\0'
|
||||||
|
return val, end + 1
|
||||||
|
|
||||||
|
def check_data(self, data):
|
||||||
|
if not isinstance(data, str):
|
||||||
|
raise TypeError("Expected str, not {!r}".format(data))
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
self.check_data(data)
|
||||||
|
encoded = data.encode('utf-8')
|
||||||
|
len_data = self.length_type.serialise(len(encoded), pos, endianness)
|
||||||
|
return len_data + encoded + b'\0'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'StringType({!r})'.format(self.length_type)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(other) is StringType) \
|
||||||
|
and (self.length_type == other.length_type)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectPathType(StringType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(simple_types['u'])
|
||||||
|
|
||||||
|
def check_data(self, data):
|
||||||
|
super().check_data(data)
|
||||||
|
if not data.startswith('/'):
|
||||||
|
raise ValueError(f"Object path ({data!r}) must start with /")
|
||||||
|
if data.endswith('/') and len(data) > 1:
|
||||||
|
raise ValueError(f"Object path ({data!r}) cannot end with /")
|
||||||
|
if '//' in data:
|
||||||
|
raise ValueError(f"Object path ({data!r}) cannot contain double /")
|
||||||
|
valid_chars = string.ascii_letters + string.digits + '/_'
|
||||||
|
if any(c not in valid_chars for c in data):
|
||||||
|
raise ValueError(
|
||||||
|
f"Object path ({data!r}) can only contain A-Z, a-z, 0-9, / and _"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
simple_types.update({
|
||||||
|
's': StringType(simple_types['u']), # String
|
||||||
|
'o': ObjectPathType(), # Object path
|
||||||
|
'g': StringType(simple_types['y']), # Signature
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class Struct:
|
||||||
|
alignment = 8
|
||||||
|
|
||||||
|
def __init__(self, fields):
|
||||||
|
if any(isinstance(f, DictEntry) for f in fields):
|
||||||
|
raise TypeError("Found dict entry outside array")
|
||||||
|
self.fields = fields
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
pos += padding(pos, 8)
|
||||||
|
res = []
|
||||||
|
for field in self.fields:
|
||||||
|
v, pos = field.parse_data(buf, pos, endianness, fds=fds)
|
||||||
|
res.append(v)
|
||||||
|
return tuple(res), pos
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
if not isinstance(data, tuple):
|
||||||
|
raise TypeError("Expected tuple, not {!r}".format(data))
|
||||||
|
if len(data) != len(self.fields):
|
||||||
|
raise ValueError("{} entries for {} fields".format(
|
||||||
|
len(data), len(self.fields)
|
||||||
|
))
|
||||||
|
pad = b'\0' * padding(pos, self.alignment)
|
||||||
|
pos += len(pad)
|
||||||
|
res_pieces = []
|
||||||
|
for item, field in zip(data, self.fields):
|
||||||
|
res_pieces.append(field.serialise(item, pos, endianness, fds=fds))
|
||||||
|
pos += len(res_pieces[-1])
|
||||||
|
return pad + b''.join(res_pieces)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "{}({!r})".format(type(self).__name__, self.fields)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(other) is type(self)) and (self.fields == other.fields)
|
||||||
|
|
||||||
|
|
||||||
|
class DictEntry(Struct):
|
||||||
|
def __init__(self, fields):
|
||||||
|
if len(fields) != 2:
|
||||||
|
raise TypeError(
|
||||||
|
"Dict entry must have 2 fields, not %d" % len(fields))
|
||||||
|
if not isinstance(fields[0], (FixedType, StringType)):
|
||||||
|
raise TypeError(
|
||||||
|
"First field in dict entry must be simple type, not {}"
|
||||||
|
.format(type(fields[0])))
|
||||||
|
super().__init__(fields)
|
||||||
|
|
||||||
|
class Array:
|
||||||
|
alignment = 4
|
||||||
|
length_type = FixedType(4, 'I')
|
||||||
|
|
||||||
|
def __init__(self, elt_type):
|
||||||
|
self.elt_type = elt_type
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
# print('Array start', pos)
|
||||||
|
length, pos = self.length_type.parse_data(buf, pos, endianness)
|
||||||
|
pos += padding(pos, self.elt_type.alignment)
|
||||||
|
end = pos + length
|
||||||
|
if self.elt_type == simple_types['y']: # Array of bytes
|
||||||
|
return buf[pos:end], end
|
||||||
|
|
||||||
|
res = []
|
||||||
|
while pos < end:
|
||||||
|
# print('Array elem', pos)
|
||||||
|
v, pos = self.elt_type.parse_data(buf, pos, endianness, fds=fds)
|
||||||
|
res.append(v)
|
||||||
|
if isinstance(self.elt_type, DictEntry):
|
||||||
|
# Convert list of 2-tuples to dict
|
||||||
|
res = dict(res)
|
||||||
|
return res, pos
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
data_is_bytes = False
|
||||||
|
if isinstance(self.elt_type, DictEntry) and isinstance(data, dict):
|
||||||
|
data = data.items()
|
||||||
|
elif (self.elt_type == simple_types['y']) and isinstance(data, bytes):
|
||||||
|
data_is_bytes = True
|
||||||
|
elif not isinstance(data, list):
|
||||||
|
raise TypeError("Not suitable for array: {!r}".format(data))
|
||||||
|
|
||||||
|
# Fail fast if we know in advance that the data is too big:
|
||||||
|
if isinstance(self.elt_type, FixedType):
|
||||||
|
if (self.elt_type.size * len(data)) > 2**26:
|
||||||
|
raise SizeLimitError("Array size exceeds 64 MiB limit")
|
||||||
|
|
||||||
|
pad1 = padding(pos, self.alignment)
|
||||||
|
pos_after_length = pos + pad1 + 4
|
||||||
|
pad2 = padding(pos_after_length, self.elt_type.alignment)
|
||||||
|
|
||||||
|
if data_is_bytes:
|
||||||
|
buf = data
|
||||||
|
else:
|
||||||
|
data_pos = pos_after_length + pad2
|
||||||
|
limit_pos = data_pos + 2 ** 26
|
||||||
|
chunks = []
|
||||||
|
for item in data:
|
||||||
|
chunks.append(self.elt_type.serialise(
|
||||||
|
item, data_pos, endianness, fds=fds
|
||||||
|
))
|
||||||
|
data_pos += len(chunks[-1])
|
||||||
|
if data_pos > limit_pos:
|
||||||
|
raise SizeLimitError("Array size exceeds 64 MiB limit")
|
||||||
|
buf = b''.join(chunks)
|
||||||
|
|
||||||
|
len_data = self.length_type.serialise(len(buf), pos+pad1, endianness)
|
||||||
|
# print('Array ser: pad1={!r}, len_data={!r}, pad2={!r}, buf={!r}'.format(
|
||||||
|
# pad1, len_data, pad2, buf))
|
||||||
|
return (b'\0' * pad1) + len_data + (b'\0' * pad2) + buf
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Array({!r})'.format(self.elt_type)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (type(other) is Array) and (self.elt_type == other.elt_type)
|
||||||
|
|
||||||
|
|
||||||
|
class Variant:
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
def parse_data(self, buf, pos, endianness, fds=()):
|
||||||
|
# print('variant', pos)
|
||||||
|
sig, pos = simple_types['g'].parse_data(buf, pos, endianness)
|
||||||
|
# print('variant sig:', repr(sig), pos)
|
||||||
|
valtype = parse_signature(list(sig))
|
||||||
|
val, pos = valtype.parse_data(buf, pos, endianness, fds=fds)
|
||||||
|
# print('variant done', (sig, val), pos)
|
||||||
|
return (sig, val), pos
|
||||||
|
|
||||||
|
def serialise(self, data, pos, endianness, fds=None):
|
||||||
|
sig, data = data
|
||||||
|
valtype = parse_signature(list(sig))
|
||||||
|
sig_buf = simple_types['g'].serialise(sig, pos, endianness)
|
||||||
|
return sig_buf + valtype.serialise(
|
||||||
|
data, pos + len(sig_buf), endianness, fds=fds
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Variant()'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return type(other) is Variant
|
||||||
|
|
||||||
|
def parse_signature(sig):
|
||||||
|
"""Parse a symbolic signature into objects.
|
||||||
|
"""
|
||||||
|
# Based on http://norvig.com/lispy.html
|
||||||
|
token = sig.pop(0)
|
||||||
|
if token == 'a':
|
||||||
|
return Array(parse_signature(sig))
|
||||||
|
if token == 'v':
|
||||||
|
return Variant()
|
||||||
|
elif token == '(':
|
||||||
|
fields = []
|
||||||
|
while sig[0] != ')':
|
||||||
|
fields.append(parse_signature(sig))
|
||||||
|
sig.pop(0) # )
|
||||||
|
return Struct(fields)
|
||||||
|
elif token == '{':
|
||||||
|
de = []
|
||||||
|
while sig[0] != '}':
|
||||||
|
de.append(parse_signature(sig))
|
||||||
|
sig.pop(0) # }
|
||||||
|
return DictEntry(de)
|
||||||
|
elif token in ')}':
|
||||||
|
raise ValueError('Unexpected end of struct')
|
||||||
|
else:
|
||||||
|
return simple_types[token]
|
||||||
|
|
||||||
|
|
||||||
|
def calc_msg_size(buf):
|
||||||
|
endian, = struct.unpack('c', buf[:1])
|
||||||
|
endian = endian_map[endian]
|
||||||
|
body_length, = struct.unpack(endian.struct_code() + 'I', buf[4:8])
|
||||||
|
fields_array_len, = struct.unpack(endian.struct_code() + 'I', buf[12:16])
|
||||||
|
header_len = 16 + fields_array_len
|
||||||
|
return header_len + padding(header_len, 8) + body_length
|
||||||
|
|
||||||
|
|
||||||
|
_header_fields_type = Array(Struct([simple_types['y'], Variant()]))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_header_fields(buf, endianness):
|
||||||
|
l, pos = _header_fields_type.parse_data(buf, 12, endianness)
|
||||||
|
return {HeaderFields(k): v[1] for (k, v) in l}, pos
|
||||||
|
|
||||||
|
|
||||||
|
header_field_codes = {
|
||||||
|
1: 'o',
|
||||||
|
2: 's',
|
||||||
|
3: 's',
|
||||||
|
4: 's',
|
||||||
|
5: 'u',
|
||||||
|
6: 's',
|
||||||
|
7: 's',
|
||||||
|
8: 'g',
|
||||||
|
9: 'u',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def serialise_header_fields(d, endianness):
|
||||||
|
l = [(i.value, (header_field_codes[i], v)) for (i, v) in sorted(d.items())]
|
||||||
|
return _header_fields_type.serialise(l, 12, endianness)
|
||||||
|
|
||||||
|
|
||||||
|
class Header:
|
||||||
|
def __init__(self, endianness, message_type, flags, protocol_version,
|
||||||
|
body_length, serial, fields):
|
||||||
|
"""A D-Bus message header
|
||||||
|
|
||||||
|
It's not normally necessary to construct this directly: use higher level
|
||||||
|
functions and methods instead.
|
||||||
|
"""
|
||||||
|
self.endianness = endianness
|
||||||
|
self.message_type = MessageType(message_type)
|
||||||
|
self.flags = MessageFlag(flags)
|
||||||
|
self.protocol_version = protocol_version
|
||||||
|
self.body_length = body_length
|
||||||
|
self.serial = serial
|
||||||
|
self.fields = fields
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Header({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, fields={!r})'.format(
|
||||||
|
self.endianness, self.message_type, self.flags,
|
||||||
|
self.protocol_version, self.body_length, self.serial, self.fields)
|
||||||
|
|
||||||
|
def serialise(self, serial=None):
|
||||||
|
s = self.endianness.struct_code() + 'cBBBII'
|
||||||
|
if serial is None:
|
||||||
|
serial = self.serial
|
||||||
|
return struct.pack(s, self.endianness.dbus_code(),
|
||||||
|
self.message_type.value, self.flags,
|
||||||
|
self.protocol_version,
|
||||||
|
self.body_length, serial) \
|
||||||
|
+ serialise_header_fields(self.fields, self.endianness)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_buffer(cls, buf):
|
||||||
|
endian, msgtype, flags, pv = struct.unpack('cBBB', buf[:4])
|
||||||
|
endian = endian_map[endian]
|
||||||
|
bodylen, serial = struct.unpack(endian.struct_code() + 'II', buf[4:12])
|
||||||
|
fields, pos = parse_header_fields(buf, endian)
|
||||||
|
return cls(endian, msgtype, flags, pv, bodylen, serial, fields), pos
|
||||||
|
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
"""Object representing a DBus message.
|
||||||
|
|
||||||
|
It's not normally necessary to construct this directly: use higher level
|
||||||
|
functions and methods instead.
|
||||||
|
"""
|
||||||
|
def __init__(self, header, body):
|
||||||
|
self.header = header
|
||||||
|
self.body = body
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "{}({!r}, {!r})".format(type(self).__name__, self.header, self.body)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_buffer(cls, buf: bytes, fds=()) -> 'Message':
|
||||||
|
header, pos = Header.from_buffer(buf)
|
||||||
|
n_fds = header.fields.get(HeaderFields.unix_fds, 0)
|
||||||
|
if n_fds > len(fds):
|
||||||
|
raise ValueError(
|
||||||
|
f"Message expects {n_fds} FDs, but only {len(fds)} were received"
|
||||||
|
)
|
||||||
|
fds = fds[:n_fds]
|
||||||
|
body = ()
|
||||||
|
if HeaderFields.signature in header.fields:
|
||||||
|
sig = header.fields[HeaderFields.signature]
|
||||||
|
body_type = parse_signature(list('(%s)' % sig))
|
||||||
|
body = body_type.parse_data(buf, pos, header.endianness, fds=fds)[0]
|
||||||
|
return Message(header, body)
|
||||||
|
|
||||||
|
def serialise(self, serial=None, fds=None) -> bytes:
|
||||||
|
"""Convert this message to bytes.
|
||||||
|
|
||||||
|
Specifying *serial* overrides the ``msg.header.serial`` field, so a
|
||||||
|
connection can use its own serial number without modifying the message.
|
||||||
|
|
||||||
|
If file-descriptor support is in use, *fds* should be a
|
||||||
|
:class:`array.array` object with type ``'i'``. Any file descriptors in
|
||||||
|
the message will be added to the array. If the message contains FDs,
|
||||||
|
it can't be serialised without this array.
|
||||||
|
"""
|
||||||
|
endian = self.header.endianness
|
||||||
|
|
||||||
|
if HeaderFields.signature in self.header.fields:
|
||||||
|
sig = self.header.fields[HeaderFields.signature]
|
||||||
|
body_type = parse_signature(list('(%s)' % sig))
|
||||||
|
body_buf = body_type.serialise(self.body, 0, endian, fds=fds)
|
||||||
|
else:
|
||||||
|
body_buf = b''
|
||||||
|
|
||||||
|
self.header.body_length = len(body_buf)
|
||||||
|
if fds:
|
||||||
|
self.header.fields[HeaderFields.unix_fds] = len(fds)
|
||||||
|
|
||||||
|
header_buf = self.header.serialise(serial=serial)
|
||||||
|
pad = b'\0' * padding(len(header_buf), 8)
|
||||||
|
return header_buf + pad + body_buf
|
||||||
|
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
"""Parse DBus messages from a stream of incoming data.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.buf = BufferPipe()
|
||||||
|
self.fds = []
|
||||||
|
self.next_msg_size = None
|
||||||
|
|
||||||
|
def add_data(self, data: bytes, fds=()):
|
||||||
|
"""Provide newly received data to the parser"""
|
||||||
|
self.buf.write(data)
|
||||||
|
self.fds.extend(fds)
|
||||||
|
|
||||||
|
def feed(self, data):
|
||||||
|
"""Feed the parser newly read data.
|
||||||
|
|
||||||
|
Returns a list of messages completed by the new data.
|
||||||
|
"""
|
||||||
|
self.add_data(data)
|
||||||
|
return list(iter(self.get_next_message, None))
|
||||||
|
|
||||||
|
def bytes_desired(self):
|
||||||
|
"""How many bytes can be received without going beyond the next message?
|
||||||
|
|
||||||
|
This is only used with file-descriptor passing, so we don't get too many
|
||||||
|
FDs in a single recvmsg call.
|
||||||
|
"""
|
||||||
|
got = self.buf.bytes_buffered
|
||||||
|
if got < 16: # The first 16 bytes tell us the message size
|
||||||
|
return 16 - got
|
||||||
|
|
||||||
|
if self.next_msg_size is None:
|
||||||
|
self.next_msg_size = calc_msg_size(self.buf.peek(16))
|
||||||
|
return self.next_msg_size - got
|
||||||
|
|
||||||
|
def get_next_message(self) -> Optional[Message]:
|
||||||
|
"""Parse one message, if there is enough data.
|
||||||
|
|
||||||
|
Returns None if it doesn't have a complete message.
|
||||||
|
"""
|
||||||
|
if self.next_msg_size is None:
|
||||||
|
if self.buf.bytes_buffered >= 16:
|
||||||
|
self.next_msg_size = calc_msg_size(self.buf.peek(16))
|
||||||
|
nms = self.next_msg_size
|
||||||
|
if (nms is not None) and self.buf.bytes_buffered >= nms:
|
||||||
|
raw_msg = self.buf.read(nms)
|
||||||
|
msg = Message.from_buffer(raw_msg, fds=self.fds)
|
||||||
|
self.next_msg_size = None
|
||||||
|
fds_consumed = msg.header.fields.get(HeaderFields.unix_fds, 0)
|
||||||
|
self.fds = self.fds[fds_consumed:]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class BufferPipe:
|
||||||
|
"""A place to store received data until we can parse a complete message
|
||||||
|
|
||||||
|
The main difference from io.BytesIO is that read & write operate at
|
||||||
|
opposite ends, like a pipe.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.chunks = deque()
|
||||||
|
self.bytes_buffered = 0
|
||||||
|
|
||||||
|
def write(self, b: bytes):
|
||||||
|
self.chunks.append(b)
|
||||||
|
self.bytes_buffered += len(b)
|
||||||
|
|
||||||
|
def _peek_iter(self, nbytes: int):
|
||||||
|
assert nbytes <= self.bytes_buffered
|
||||||
|
for chunk in self.chunks:
|
||||||
|
chunk = chunk[:nbytes]
|
||||||
|
nbytes -= len(chunk)
|
||||||
|
yield chunk
|
||||||
|
if nbytes <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
def peek(self, nbytes: int) -> bytes:
|
||||||
|
"""Get exactly nbytes bytes from the front without removing them"""
|
||||||
|
return b''.join(self._peek_iter(nbytes))
|
||||||
|
|
||||||
|
def _read_iter(self, nbytes: int):
|
||||||
|
assert nbytes <= self.bytes_buffered
|
||||||
|
while True:
|
||||||
|
chunk = self.chunks.popleft()
|
||||||
|
self.bytes_buffered -= len(chunk)
|
||||||
|
if nbytes <= len(chunk):
|
||||||
|
break
|
||||||
|
nbytes -= len(chunk)
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
# Final chunk
|
||||||
|
chunk, rem = chunk[:nbytes], chunk[nbytes:]
|
||||||
|
if rem:
|
||||||
|
self.chunks.appendleft(rem)
|
||||||
|
self.bytes_buffered += len(rem)
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
def read(self, nbytes: int) -> bytes:
|
||||||
|
"""Take & return exactly nbytes bytes from the front"""
|
||||||
|
return b''.join(self._read_iter(nbytes))
|
||||||
0
lib/jeepney/tests/__init__.py
Normal file
0
lib/jeepney/tests/__init__.py
Normal file
BIN
lib/jeepney/tests/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_auth.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_bus.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_bus.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_fds.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_fds.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_low_level.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_low_level.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lib/jeepney/tests/__pycache__/test_wrappers.cpython-314.pyc
Normal file
BIN
lib/jeepney/tests/__pycache__/test_wrappers.cpython-314.pyc
Normal file
Binary file not shown.
116
lib/jeepney/tests/secrets_introspect.xml
Normal file
116
lib/jeepney/tests/secrets_introspect.xml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<!-- GDBus 2.50.3 -->
|
||||||
|
<node>
|
||||||
|
<interface name="org.freedesktop.DBus.Properties">
|
||||||
|
<method name="Get">
|
||||||
|
<arg type="s" name="interface_name" direction="in"/>
|
||||||
|
<arg type="s" name="property_name" direction="in"/>
|
||||||
|
<arg type="v" name="value" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetAll">
|
||||||
|
<arg type="s" name="interface_name" direction="in"/>
|
||||||
|
<arg type="a{sv}" name="properties" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="Set">
|
||||||
|
<arg type="s" name="interface_name" direction="in"/>
|
||||||
|
<arg type="s" name="property_name" direction="in"/>
|
||||||
|
<arg type="v" name="value" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<signal name="PropertiesChanged">
|
||||||
|
<arg type="s" name="interface_name"/>
|
||||||
|
<arg type="a{sv}" name="changed_properties"/>
|
||||||
|
<arg type="as" name="invalidated_properties"/>
|
||||||
|
</signal>
|
||||||
|
</interface>
|
||||||
|
<interface name="org.freedesktop.DBus.Introspectable">
|
||||||
|
<method name="Introspect">
|
||||||
|
<arg type="s" name="xml_data" direction="out"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
<interface name="org.freedesktop.DBus.Peer">
|
||||||
|
<method name="Ping"/>
|
||||||
|
<method name="GetMachineId">
|
||||||
|
<arg type="s" name="machine_uuid" direction="out"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
<interface name="org.freedesktop.Secret.Service">
|
||||||
|
<method name="OpenSession">
|
||||||
|
<arg type="s" name="algorithm" direction="in"/>
|
||||||
|
<arg type="v" name="input" direction="in"/>
|
||||||
|
<arg type="v" name="output" direction="out"/>
|
||||||
|
<arg type="o" name="result" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="CreateCollection">
|
||||||
|
<arg type="a{sv}" name="properties" direction="in"/>
|
||||||
|
<arg type="s" name="alias" direction="in"/>
|
||||||
|
<arg type="o" name="collection" direction="out"/>
|
||||||
|
<arg type="o" name="prompt" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SearchItems">
|
||||||
|
<arg type="a{ss}" name="attributes" direction="in"/>
|
||||||
|
<arg type="ao" name="unlocked" direction="out"/>
|
||||||
|
<arg type="ao" name="locked" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="Unlock">
|
||||||
|
<arg type="ao" name="objects" direction="in"/>
|
||||||
|
<arg type="ao" name="unlocked" direction="out"/>
|
||||||
|
<arg type="o" name="prompt" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="Lock">
|
||||||
|
<arg type="ao" name="objects" direction="in"/>
|
||||||
|
<arg type="ao" name="locked" direction="out"/>
|
||||||
|
<arg type="o" name="Prompt" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="LockService"/>
|
||||||
|
<method name="ChangeLock">
|
||||||
|
<arg type="o" name="collection" direction="in"/>
|
||||||
|
<arg type="o" name="prompt" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetSecrets">
|
||||||
|
<arg type="ao" name="items" direction="in"/>
|
||||||
|
<arg type="o" name="session" direction="in"/>
|
||||||
|
<arg type="a{o(oayays)}" name="secrets" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="ReadAlias">
|
||||||
|
<arg type="s" name="name" direction="in"/>
|
||||||
|
<arg type="o" name="collection" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetAlias">
|
||||||
|
<arg type="s" name="name" direction="in"/>
|
||||||
|
<arg type="o" name="collection" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<signal name="CollectionCreated">
|
||||||
|
<arg type="o" name="collection"/>
|
||||||
|
</signal>
|
||||||
|
<signal name="CollectionDeleted">
|
||||||
|
<arg type="o" name="collection"/>
|
||||||
|
</signal>
|
||||||
|
<signal name="CollectionChanged">
|
||||||
|
<arg type="o" name="collection"/>
|
||||||
|
</signal>
|
||||||
|
<property type="ao" name="Collections" access="read"/>
|
||||||
|
</interface>
|
||||||
|
<interface name="org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface">
|
||||||
|
<method name="ChangeWithMasterPassword">
|
||||||
|
<arg type="o" name="collection" direction="in"/>
|
||||||
|
<arg type="(oayays)" name="original" direction="in"/>
|
||||||
|
<arg type="(oayays)" name="master" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="ChangeWithPrompt">
|
||||||
|
<arg type="o" name="collection" direction="in"/>
|
||||||
|
<arg type="o" name="prompt" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="CreateWithMasterPassword">
|
||||||
|
<arg type="a{sv}" name="attributes" direction="in"/>
|
||||||
|
<arg type="(oayays)" name="master" direction="in"/>
|
||||||
|
<arg type="o" name="collection" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="UnlockWithMasterPassword">
|
||||||
|
<arg type="o" name="collection" direction="in"/>
|
||||||
|
<arg type="(oayays)" name="master" direction="in"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
<node name="session"/>
|
||||||
|
<node name="collection"/>
|
||||||
|
</node>
|
||||||
24
lib/jeepney/tests/test_auth.py
Normal file
24
lib/jeepney/tests/test_auth.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import auth
|
||||||
|
|
||||||
|
def test_make_auth_external():
|
||||||
|
b = auth.make_auth_external()
|
||||||
|
assert b.startswith(b'AUTH EXTERNAL')
|
||||||
|
|
||||||
|
def test_make_auth_anonymous():
|
||||||
|
b = auth.make_auth_anonymous()
|
||||||
|
assert b.startswith(b'AUTH ANONYMOUS')
|
||||||
|
|
||||||
|
def test_parser():
|
||||||
|
p = auth.SASLParser()
|
||||||
|
p.feed(b'OK 728d62bc2eb394')
|
||||||
|
assert not p.authenticated
|
||||||
|
p.feed(b'1ebbb0b42958b1e0d6\r\n')
|
||||||
|
assert p.authenticated
|
||||||
|
|
||||||
|
def test_parser_rejected():
|
||||||
|
p = auth.SASLParser()
|
||||||
|
with pytest.raises(auth.AuthenticationError):
|
||||||
|
p.feed(b'REJECTED EXTERNAL\r\n')
|
||||||
|
assert not p.authenticated
|
||||||
28
lib/jeepney/tests/test_bindgen.py
Normal file
28
lib/jeepney/tests/test_bindgen.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from io import StringIO
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from jeepney.low_level import MessageType, HeaderFields
|
||||||
|
from jeepney.bindgen import code_from_xml
|
||||||
|
|
||||||
|
sample_file = os.path.join(os.path.dirname(__file__), 'secrets_introspect.xml')
|
||||||
|
|
||||||
|
def test_bindgen():
|
||||||
|
with open(sample_file) as f:
|
||||||
|
xml = f.read()
|
||||||
|
sio = StringIO()
|
||||||
|
n_interfaces = code_from_xml(xml, path='/org/freedesktop/secrets',
|
||||||
|
bus_name='org.freedesktop.secrets',
|
||||||
|
fh=sio)
|
||||||
|
# 5 interfaces defined, but we ignore Properties, Introspectable, Peer
|
||||||
|
assert n_interfaces == 2
|
||||||
|
|
||||||
|
# Run the generated code, defining the message generator classes.
|
||||||
|
binding_ns = {}
|
||||||
|
exec(sio.getvalue(), binding_ns)
|
||||||
|
Service = binding_ns['Service']
|
||||||
|
|
||||||
|
# Check basic functionality of the Service class
|
||||||
|
assert Service.interface == 'org.freedesktop.Secret.Service'
|
||||||
|
msg = Service().SearchItems({"service": "foo", "user": "bar"})
|
||||||
|
assert msg.header.message_type is MessageType.method_call
|
||||||
|
assert msg.header.fields[HeaderFields.destination] == 'org.freedesktop.secrets'
|
||||||
24
lib/jeepney/tests/test_bus.py
Normal file
24
lib/jeepney/tests/test_bus.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
from testpath import modified_env
|
||||||
|
|
||||||
|
from jeepney import bus
|
||||||
|
|
||||||
|
def test_get_connectable_addresses():
|
||||||
|
a = list(bus.get_connectable_addresses('unix:path=/run/user/1000/bus'))
|
||||||
|
assert a == ['/run/user/1000/bus']
|
||||||
|
|
||||||
|
a = list(bus.get_connectable_addresses('unix:abstract=/tmp/foo'))
|
||||||
|
assert a == ['\0/tmp/foo']
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
list(bus.get_connectable_addresses('unix:tmpdir=/tmp'))
|
||||||
|
|
||||||
|
def test_get_bus():
|
||||||
|
with modified_env({
|
||||||
|
'DBUS_SESSION_BUS_ADDRESS':'unix:path=/run/user/1000/bus',
|
||||||
|
'DBUS_SYSTEM_BUS_ADDRESS': 'unix:path=/var/run/dbus/system_bus_socket'
|
||||||
|
}):
|
||||||
|
assert bus.get_bus('SESSION') == '/run/user/1000/bus'
|
||||||
|
assert bus.get_bus('SYSTEM') == '/var/run/dbus/system_bus_socket'
|
||||||
|
|
||||||
|
assert bus.get_bus('unix:path=/run/user/1002/bus') == '/run/user/1002/bus'
|
||||||
112
lib/jeepney/tests/test_bus_messages.py
Normal file
112
lib/jeepney/tests/test_bus_messages.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
from jeepney import DBusAddress, new_signal, new_method_call
|
||||||
|
from jeepney.bus_messages import MatchRule, message_bus
|
||||||
|
|
||||||
|
portal = DBusAddress(
|
||||||
|
object_path='/org/freedesktop/portal/desktop',
|
||||||
|
bus_name='org.freedesktop.portal.Desktop',
|
||||||
|
)
|
||||||
|
portal_req_iface = portal.with_interface('org.freedesktop.portal.Request')
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_rule_simple():
|
||||||
|
rule = MatchRule(
|
||||||
|
type='signal', interface='org.freedesktop.portal.Request',
|
||||||
|
)
|
||||||
|
assert rule.matches(new_signal(portal_req_iface, 'Response'))
|
||||||
|
|
||||||
|
# Wrong message type
|
||||||
|
assert not rule.matches(new_method_call(portal_req_iface, 'Boo'))
|
||||||
|
|
||||||
|
# Wrong interface
|
||||||
|
assert not rule.matches(new_signal(
|
||||||
|
portal.with_interface('org.freedesktop.portal.FileChooser'), 'Response'
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_rule_path_namespace():
|
||||||
|
assert MatchRule(path_namespace='/org/freedesktop/portal').matches(
|
||||||
|
new_signal(portal_req_iface, 'Response')
|
||||||
|
)
|
||||||
|
assert "/freedesktop/" in (
|
||||||
|
MatchRule(path_namespace='/org/freedesktop/portal').serialise()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prefix but not a parent in the path hierarchy
|
||||||
|
assert not MatchRule(path_namespace='/org/freedesktop/por').matches(
|
||||||
|
new_signal(portal_req_iface, 'Response')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_rule_arg():
|
||||||
|
rule = MatchRule(type='method_call')
|
||||||
|
rule.add_arg_condition(0, 'foo')
|
||||||
|
|
||||||
|
assert rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('foo',)
|
||||||
|
))
|
||||||
|
|
||||||
|
assert not rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('foobar',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# No such argument
|
||||||
|
assert not rule.matches(new_method_call(portal_req_iface, 'Boo'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_rule_arg_path():
|
||||||
|
rule = MatchRule(type='method_call')
|
||||||
|
rule.add_arg_condition(0, '/aa/bb/', kind='path')
|
||||||
|
|
||||||
|
# Exact match
|
||||||
|
assert rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('/aa/bb/',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Match a prefix
|
||||||
|
assert rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('/aa/bb/cc',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Argument is a prefix, ending with /
|
||||||
|
assert rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('/aa/',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Argument is a prefix, but NOT ending with /
|
||||||
|
assert not rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('/aa',)
|
||||||
|
))
|
||||||
|
|
||||||
|
assert not rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='s', body=('/aa/bb',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Not a string
|
||||||
|
assert not rule.matches(new_method_call(
|
||||||
|
portal_req_iface, 'Boo', signature='u', body=(12,)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_rule_arg_namespace():
|
||||||
|
rule = MatchRule(member='NameOwnerChanged')
|
||||||
|
rule.add_arg_condition(0, 'com.example.backend1', kind='namespace')
|
||||||
|
|
||||||
|
# Exact match
|
||||||
|
assert rule.matches(new_signal(
|
||||||
|
message_bus, 'NameOwnerChanged', 's', ('com.example.backend1',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Parent of the name
|
||||||
|
assert rule.matches(new_signal(
|
||||||
|
message_bus, 'NameOwnerChanged', 's', ('com.example.backend1.foo.bar',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Prefix but not a parent in the namespace
|
||||||
|
assert not rule.matches(new_signal(
|
||||||
|
message_bus, 'NameOwnerChanged', 's', ('com.example.backend12',)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Not a string
|
||||||
|
assert not rule.matches(new_signal(
|
||||||
|
message_bus, 'NameOwnerChanged', 'u', (1,)
|
||||||
|
))
|
||||||
80
lib/jeepney/tests/test_fds.py
Normal file
80
lib/jeepney/tests/test_fds.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney import FileDescriptor, NoFDError
|
||||||
|
|
||||||
|
def assert_not_fd(fd: int):
|
||||||
|
"""Check that the given number is not open as a file descriptor"""
|
||||||
|
with pytest.raises(OSError) as exc_info:
|
||||||
|
os.stat(fd)
|
||||||
|
assert exc_info.value.errno == errno.EBADF
|
||||||
|
|
||||||
|
|
||||||
|
def test_close(tmp_path):
|
||||||
|
fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR)
|
||||||
|
|
||||||
|
with FileDescriptor(fd) as wfd:
|
||||||
|
assert wfd.fileno() == fd
|
||||||
|
# Leaving the with block is equivalent to calling .close()
|
||||||
|
|
||||||
|
assert 'closed' in repr(wfd)
|
||||||
|
with pytest.raises(NoFDError):
|
||||||
|
wfd.fileno()
|
||||||
|
|
||||||
|
assert_not_fd(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_raw_fd(tmp_path):
|
||||||
|
fd = os.open(tmp_path / 'a', os.O_CREAT)
|
||||||
|
wfd = FileDescriptor(fd)
|
||||||
|
assert wfd.fileno() == fd
|
||||||
|
|
||||||
|
assert wfd.to_raw_fd() == fd
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert 'converted' in repr(wfd)
|
||||||
|
with pytest.raises(NoFDError):
|
||||||
|
wfd.fileno()
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_file(tmp_path):
|
||||||
|
fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR)
|
||||||
|
wfd = FileDescriptor(fd)
|
||||||
|
|
||||||
|
with wfd.to_file('w') as f:
|
||||||
|
assert f.write('abc')
|
||||||
|
|
||||||
|
assert 'converted' in repr(wfd)
|
||||||
|
with pytest.raises(NoFDError):
|
||||||
|
wfd.fileno()
|
||||||
|
|
||||||
|
assert_not_fd(fd) # Check FD was closed by file object
|
||||||
|
|
||||||
|
assert (tmp_path / 'a').read_text() == 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_socket():
|
||||||
|
s1, s2 = socket.socketpair()
|
||||||
|
try:
|
||||||
|
s1.sendall(b'abcd')
|
||||||
|
sfd = s2.detach()
|
||||||
|
wfd = FileDescriptor(sfd)
|
||||||
|
|
||||||
|
with wfd.to_socket() as sock:
|
||||||
|
b = sock.recv(16)
|
||||||
|
assert b and b'abcd'.startswith(b)
|
||||||
|
|
||||||
|
assert 'converted' in repr(wfd)
|
||||||
|
with pytest.raises(NoFDError):
|
||||||
|
wfd.fileno()
|
||||||
|
|
||||||
|
assert_not_fd(sfd) # Check FD was closed by socket object
|
||||||
|
finally:
|
||||||
|
s1.close()
|
||||||
|
|
||||||
|
|
||||||
101
lib/jeepney/tests/test_low_level.py
Normal file
101
lib/jeepney/tests/test_low_level.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import pytest
|
||||||
|
from jeepney.low_level import *
|
||||||
|
|
||||||
|
HELLO_METHOD_CALL = (
|
||||||
|
b'l\x01\x00\x01\x00\x00\x00\x00\x01\x00\x00\x00m\x00\x00\x00\x01\x01o\x00\x15'
|
||||||
|
b'\x00\x00\x00/org/freedesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00'
|
||||||
|
b'org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\x05\x00\x00\x00Hello\x00'
|
||||||
|
b'\x00\x00\x06\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parser_simple():
|
||||||
|
msg = Parser().feed(HELLO_METHOD_CALL)[0]
|
||||||
|
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||||
|
|
||||||
|
def chunks(src, size):
|
||||||
|
pos = 0
|
||||||
|
while pos < len(src):
|
||||||
|
end = pos + size
|
||||||
|
yield src[pos:end]
|
||||||
|
pos = end
|
||||||
|
|
||||||
|
def test_parser_chunks():
|
||||||
|
p = Parser()
|
||||||
|
chunked = list(chunks(HELLO_METHOD_CALL, 16))
|
||||||
|
for c in chunked[:-1]:
|
||||||
|
assert p.feed(c) == []
|
||||||
|
msg = p.feed(chunked[-1])[0]
|
||||||
|
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||||
|
|
||||||
|
def test_multiple():
|
||||||
|
msgs = Parser().feed(HELLO_METHOD_CALL * 6)
|
||||||
|
assert len(msgs) == 6
|
||||||
|
for msg in msgs:
|
||||||
|
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||||
|
|
||||||
|
def test_roundtrip():
|
||||||
|
msg = Parser().feed(HELLO_METHOD_CALL)[0]
|
||||||
|
assert msg.serialise() == HELLO_METHOD_CALL
|
||||||
|
|
||||||
|
def test_serialise_dict():
|
||||||
|
data = {
|
||||||
|
'a': 'b',
|
||||||
|
'de': 'f',
|
||||||
|
}
|
||||||
|
string_type = simple_types['s']
|
||||||
|
sig = Array(DictEntry([string_type, string_type]))
|
||||||
|
print(sig.serialise(data, 0, Endianness.little))
|
||||||
|
assert sig.serialise(data, 0, Endianness.little) == (
|
||||||
|
b'\x1e\0\0\0' + # Length
|
||||||
|
b'\0\0\0\0' + # Padding
|
||||||
|
b'\x01\0\0\0a\0\0\0' +
|
||||||
|
b'\x01\0\0\0b\0\0\0' +
|
||||||
|
b'\x02\0\0\0de\0\0' +
|
||||||
|
b'\x01\0\0\0f\0'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_signature():
|
||||||
|
sig = parse_signature(list('(a{sv}(oayays)b)'))
|
||||||
|
print(sig)
|
||||||
|
assert sig == Struct([
|
||||||
|
Array(DictEntry([simple_types['s'], Variant()])),
|
||||||
|
Struct([
|
||||||
|
simple_types['o'],
|
||||||
|
Array(simple_types['y']),
|
||||||
|
Array(simple_types['y']),
|
||||||
|
simple_types['s']
|
||||||
|
]),
|
||||||
|
simple_types['b'],
|
||||||
|
])
|
||||||
|
|
||||||
|
class fake_list(list):
|
||||||
|
def __init__(self, n):
|
||||||
|
super().__init__()
|
||||||
|
self._n = n
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self._n
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(range(self._n))
|
||||||
|
|
||||||
|
def test_array_limit():
|
||||||
|
# The spec limits arrays to 64 MiB
|
||||||
|
a = Array(FixedType(8, 'Q')) # 'at' - array of uint64
|
||||||
|
a.serialise(fake_list(100), 0, Endianness.little)
|
||||||
|
with pytest.raises(SizeLimitError):
|
||||||
|
a.serialise(fake_list(2**23 + 1), 0, Endianness.little)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_object_path():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ObjectPathType().check_data('org/freedesktop/DBus')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ObjectPathType().check_data('/org/freedesktop/DBus/')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ObjectPathType().check_data('/org//freedesktop/DBus')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ObjectPathType().check_data('/org/freedesktop/DBüs') # Non-ASCII character
|
||||||
74
lib/jeepney/tests/test_wrappers.py
Normal file
74
lib/jeepney/tests/test_wrappers.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from jeepney.wrappers import *
|
||||||
|
|
||||||
|
def test_bad_bus_name():
|
||||||
|
obj = '/com/example/foo'
|
||||||
|
DBusAddress(obj, 'com.example.a') # Valid (well known name)
|
||||||
|
DBusAddress(obj, 'com.example.a-b') # Valid but discouraged
|
||||||
|
DBusAddress(obj, ':1.13') # Valid (unique name)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='too long'):
|
||||||
|
DBusAddress(obj, 'com.example.' + ('a' * 256))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, '.com.example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, 'com..example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, 'com.2example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, 'cöm.example.a') # Non-ASCII character
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, 'com')
|
||||||
|
|
||||||
|
def test_bad_interface():
|
||||||
|
obj = '/com/example/foo'
|
||||||
|
busname = 'com.example.foo'
|
||||||
|
DBusAddress(obj, 'com.example.a', 'com.example.a_b') # Valid
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='too long'):
|
||||||
|
DBusAddress(obj, 'com.example.a', 'com.example.' + ('a' * 256))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, 'com.example.a', 'com.example.a-b') # No hyphens
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, busname, '.com.example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, busname, 'com..example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, busname, 'com.2example.a')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, busname, 'cöm.example.a') # Non-ASCII character
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DBusAddress(obj, busname, 'com')
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_member_name():
|
||||||
|
addr = DBusAddress(
|
||||||
|
'/org/freedesktop/DBus',
|
||||||
|
bus_name='org.freedesktop.DBus',
|
||||||
|
interface='org.freedesktop.DBus',
|
||||||
|
)
|
||||||
|
new_method_call(addr, 'Hello')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='too long'):
|
||||||
|
new_method_call(addr, 'Hell' + ('o' * 256))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
new_method_call(addr, 'org.Hello')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
new_method_call(addr, '9Hello')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
new_method_call(addr, '')
|
||||||
265
lib/jeepney/wrappers.py
Normal file
265
lib/jeepney/wrappers.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import re
|
||||||
|
from typing import Union
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from .low_level import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DBusAddress',
|
||||||
|
'new_method_call',
|
||||||
|
'new_method_return',
|
||||||
|
'new_error',
|
||||||
|
'new_signal',
|
||||||
|
'MessageGenerator',
|
||||||
|
'Properties',
|
||||||
|
'Introspectable',
|
||||||
|
'DBusErrorResponse',
|
||||||
|
]
|
||||||
|
|
||||||
|
bus_name_pat = re.compile(
|
||||||
|
r'([A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+' # Well known name
|
||||||
|
r'|:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+))$', # Unique name
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_bus_name(name):
|
||||||
|
if len(name) > 255:
|
||||||
|
abbr = name[:8] + '...'
|
||||||
|
raise ValueError(f"Bus name ({abbr!r}) is too long (> 255 characters)")
|
||||||
|
if not bus_name_pat.match(name):
|
||||||
|
raise ValueError(f"Bus name ({name!r}) is not valid")
|
||||||
|
|
||||||
|
interface_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$')
|
||||||
|
|
||||||
|
def check_interface(name):
|
||||||
|
if len(name) > 255:
|
||||||
|
abbr = name[:8] + '...'
|
||||||
|
raise ValueError(f"Interface name ({abbr!r}) is too long (> 255 characters)")
|
||||||
|
if not interface_pat.match(name):
|
||||||
|
raise ValueError(f"Interface name ({name!r}) is not valid")
|
||||||
|
|
||||||
|
member_name_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*$')
|
||||||
|
|
||||||
|
def check_member_name(name):
|
||||||
|
if len(name) > 255:
|
||||||
|
abbr = name[:8] + '...'
|
||||||
|
raise ValueError(f"Member name ({abbr!r}) is too long (> 255 characters)")
|
||||||
|
if not member_name_pat.match(name):
|
||||||
|
raise ValueError(f"Member name ({name!r} is not valid")
|
||||||
|
|
||||||
|
|
||||||
|
class DBusAddress:
|
||||||
|
"""This identifies the object and interface a message is for.
|
||||||
|
|
||||||
|
e.g. messages to display desktop notifications would have this address::
|
||||||
|
|
||||||
|
DBusAddress('/org/freedesktop/Notifications',
|
||||||
|
bus_name='org.freedesktop.Notifications',
|
||||||
|
interface='org.freedesktop.Notifications')
|
||||||
|
"""
|
||||||
|
def __init__(self, object_path, bus_name=None, interface=None):
|
||||||
|
ObjectPathType().check_data(object_path)
|
||||||
|
self.object_path = object_path
|
||||||
|
|
||||||
|
if bus_name is not None:
|
||||||
|
check_bus_name(bus_name)
|
||||||
|
self.bus_name = bus_name
|
||||||
|
|
||||||
|
if interface is not None:
|
||||||
|
check_interface(interface)
|
||||||
|
self.interface = interface
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '{}({!r}, bus_name={!r}, interface={!r})'.format(type(self).__name__,
|
||||||
|
self.object_path, self.bus_name, self.interface)
|
||||||
|
|
||||||
|
def with_interface(self, interface):
|
||||||
|
check_interface(interface)
|
||||||
|
return type(self)(self.object_path, self.bus_name, interface)
|
||||||
|
|
||||||
|
class DBusObject(DBusAddress):
|
||||||
|
def __init__(self, object_path, bus_name=None, interface=None):
|
||||||
|
super().__init__(object_path, bus_name, interface)
|
||||||
|
warn('Deprecated alias, use DBusAddress instead', stacklevel=2)
|
||||||
|
|
||||||
|
def new_header(msg_type):
|
||||||
|
return Header(Endianness.little, msg_type, flags=0, protocol_version=1,
|
||||||
|
body_length=-1, serial=-1, fields={})
|
||||||
|
|
||||||
|
def new_method_call(remote_obj, method, signature=None, body=()):
|
||||||
|
"""Construct a new method call message
|
||||||
|
|
||||||
|
This is a relatively low-level method. In many cases, this will be called
|
||||||
|
from a :class:`MessageGenerator` subclass which provides a more convenient
|
||||||
|
API.
|
||||||
|
|
||||||
|
:param DBusAddress remote_obj: The object to call a method on
|
||||||
|
:param str method: The name of the method to call
|
||||||
|
:param str signature: The DBus signature of the body data
|
||||||
|
:param tuple body: Body data (i.e. method parameters)
|
||||||
|
"""
|
||||||
|
check_member_name(method)
|
||||||
|
header = new_header(MessageType.method_call)
|
||||||
|
header.fields[HeaderFields.path] = remote_obj.object_path
|
||||||
|
if remote_obj.bus_name is None:
|
||||||
|
raise ValueError("remote_obj.bus_name cannot be None for method calls")
|
||||||
|
header.fields[HeaderFields.destination] = remote_obj.bus_name
|
||||||
|
if remote_obj.interface is not None:
|
||||||
|
header.fields[HeaderFields.interface] = remote_obj.interface
|
||||||
|
header.fields[HeaderFields.member] = method
|
||||||
|
if signature is not None:
|
||||||
|
header.fields[HeaderFields.signature] = signature
|
||||||
|
|
||||||
|
return Message(header, body)
|
||||||
|
|
||||||
|
def new_method_return(parent_msg, signature=None, body=()):
|
||||||
|
"""Construct a new response message
|
||||||
|
|
||||||
|
:param Message parent_msg: The method call this is a reply to
|
||||||
|
:param str signature: The DBus signature of the body data
|
||||||
|
:param tuple body: Body data
|
||||||
|
"""
|
||||||
|
header = new_header(MessageType.method_return)
|
||||||
|
header.fields[HeaderFields.reply_serial] = parent_msg.header.serial
|
||||||
|
sender = parent_msg.header.fields.get(HeaderFields.sender, None)
|
||||||
|
if sender is not None:
|
||||||
|
header.fields[HeaderFields.destination] = sender
|
||||||
|
if signature is not None:
|
||||||
|
header.fields[HeaderFields.signature] = signature
|
||||||
|
return Message(header, body)
|
||||||
|
|
||||||
|
def new_error(parent_msg, error_name, signature=None, body=()):
|
||||||
|
"""Construct a new error response message
|
||||||
|
|
||||||
|
:param Message parent_msg: The method call this is a reply to
|
||||||
|
:param str error_name: The name of the error
|
||||||
|
:param str signature: The DBus signature of the body data
|
||||||
|
:param tuple body: Body data
|
||||||
|
"""
|
||||||
|
header = new_header(MessageType.error)
|
||||||
|
header.fields[HeaderFields.reply_serial] = parent_msg.header.serial
|
||||||
|
header.fields[HeaderFields.error_name] = error_name
|
||||||
|
sender = parent_msg.header.fields.get(HeaderFields.sender, None)
|
||||||
|
if sender is not None:
|
||||||
|
header.fields[HeaderFields.destination] = sender
|
||||||
|
if signature is not None:
|
||||||
|
header.fields[HeaderFields.signature] = signature
|
||||||
|
return Message(header, body)
|
||||||
|
|
||||||
|
def new_signal(emitter, signal, signature=None, body=()):
|
||||||
|
"""Construct a new signal message
|
||||||
|
|
||||||
|
:param DBusAddress emitter: The object sending the signal
|
||||||
|
:param str signal: The name of the signal
|
||||||
|
:param str signature: The DBus signature of the body data
|
||||||
|
:param tuple body: Body data
|
||||||
|
"""
|
||||||
|
check_member_name(signal)
|
||||||
|
header = new_header(MessageType.signal)
|
||||||
|
header.fields[HeaderFields.path] = emitter.object_path
|
||||||
|
if emitter.interface is None:
|
||||||
|
raise ValueError("emitter.interface cannot be None for signals")
|
||||||
|
header.fields[HeaderFields.interface] = emitter.interface
|
||||||
|
header.fields[HeaderFields.member] = signal
|
||||||
|
if signature is not None:
|
||||||
|
header.fields[HeaderFields.signature] = signature
|
||||||
|
return Message(header, body)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageGenerator:
|
||||||
|
"""Subclass this to define the methods available on a DBus interface.
|
||||||
|
|
||||||
|
jeepney.bindgen can automatically create subclasses using introspection.
|
||||||
|
"""
|
||||||
|
interface: Optional[str] = None
|
||||||
|
|
||||||
|
def __init__(self, object_path, bus_name):
|
||||||
|
ObjectPathType().check_data(object_path)
|
||||||
|
check_bus_name(bus_name)
|
||||||
|
if self.interface is not None:
|
||||||
|
check_interface(self.interface)
|
||||||
|
|
||||||
|
self.object_path = object_path
|
||||||
|
self.bus_name = bus_name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "{}({!r}, bus_name={!r})".format(type(self).__name__,
|
||||||
|
self.object_path, self.bus_name)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyBase:
|
||||||
|
"""A proxy is an IO-aware wrapper around a MessageGenerator
|
||||||
|
|
||||||
|
Calling methods on a proxy object will send a message and wait for the
|
||||||
|
reply. This is a base class for proxy implementations in jeepney.io.
|
||||||
|
"""
|
||||||
|
def __init__(self, msggen):
|
||||||
|
self._msggen = msggen
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item.startswith('__'):
|
||||||
|
raise AttributeError(item)
|
||||||
|
|
||||||
|
make_msg = getattr(self._msggen, item, None)
|
||||||
|
if callable(make_msg):
|
||||||
|
return self._method_call(make_msg)
|
||||||
|
|
||||||
|
raise AttributeError(item)
|
||||||
|
|
||||||
|
def _method_call(self, make_msg):
|
||||||
|
raise NotImplementedError("Needs to be implemented in subclass")
|
||||||
|
|
||||||
|
class Properties:
|
||||||
|
"""Build messages for accessing object properties
|
||||||
|
|
||||||
|
If a D-Bus object has multiple interfaces, each interface has its own
|
||||||
|
set of properties.
|
||||||
|
|
||||||
|
This uses the standard DBus interface ``org.freedesktop.DBus.Properties``
|
||||||
|
"""
|
||||||
|
def __init__(self, obj: Union[DBusAddress, MessageGenerator]):
|
||||||
|
self.obj = obj
|
||||||
|
self.props_if = DBusAddress(obj.object_path, bus_name=obj.bus_name,
|
||||||
|
interface='org.freedesktop.DBus.Properties')
|
||||||
|
|
||||||
|
def get(self, name):
|
||||||
|
"""Get the value of the property *name*"""
|
||||||
|
return new_method_call(self.props_if, 'Get', 'ss',
|
||||||
|
(self.obj.interface, name))
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
"""Get all property values for this interface"""
|
||||||
|
return new_method_call(self.props_if, 'GetAll', 's',
|
||||||
|
(self.obj.interface,))
|
||||||
|
|
||||||
|
def set(self, name, signature, value):
|
||||||
|
"""Set the property *name* to *value* (with appropriate signature)"""
|
||||||
|
return new_method_call(self.props_if, 'Set', 'ssv',
|
||||||
|
(self.obj.interface, name, (signature, value)))
|
||||||
|
|
||||||
|
class Introspectable(MessageGenerator):
|
||||||
|
interface = 'org.freedesktop.DBus.Introspectable'
|
||||||
|
|
||||||
|
def Introspect(self):
|
||||||
|
"""Request D-Bus introspection XML for a remote object"""
|
||||||
|
return new_method_call(self, 'Introspect')
|
||||||
|
|
||||||
|
class DBusErrorResponse(Exception):
|
||||||
|
"""Raised by proxy method calls when the reply is an error message"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.name = msg.header.fields.get(HeaderFields.error_name)
|
||||||
|
self.data = msg.body
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '[{}] {}'.format(self.name, self.data)
|
||||||
|
|
||||||
|
|
||||||
|
def unwrap_msg(msg: Message):
|
||||||
|
"""Get the body of a message, raising DBusErrorResponse for error messages
|
||||||
|
|
||||||
|
This is to be used with replies to method_call messages, which may be
|
||||||
|
method_return or error.
|
||||||
|
"""
|
||||||
|
if msg.header.message_type == MessageType.error:
|
||||||
|
raise DBusErrorResponse(msg)
|
||||||
|
|
||||||
|
return msg.body
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user