ConfigTools & Encription manager
This commit is contained in:
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
|
||||
1
lib/keyring-25.7.0.dist-info/INSTALLER
Normal file
1
lib/keyring-25.7.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
||||
pip
|
||||
554
lib/keyring-25.7.0.dist-info/METADATA
Normal file
554
lib/keyring-25.7.0.dist-info/METADATA
Normal file
@@ -0,0 +1,554 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: keyring
|
||||
Version: 25.7.0
|
||||
Summary: Store and access your passwords safely.
|
||||
Author-email: Kang Zhang <jobo.zh@gmail.com>
|
||||
Maintainer-email: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||
License-Expression: MIT
|
||||
Project-URL: Source, https://github.com/jaraco/keyring
|
||||
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: pywin32-ctypes>=0.2.0; sys_platform == "win32"
|
||||
Requires-Dist: SecretStorage>=3.2; sys_platform == "linux"
|
||||
Requires-Dist: jeepney>=0.4.2; sys_platform == "linux"
|
||||
Requires-Dist: importlib_metadata>=4.11.4; python_version < "3.12"
|
||||
Requires-Dist: jaraco.classes
|
||||
Requires-Dist: jaraco.functools
|
||||
Requires-Dist: jaraco.context
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
|
||||
Requires-Dist: pyfakefs; 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: pygobject-stubs; extra == "type"
|
||||
Requires-Dist: shtab; extra == "type"
|
||||
Requires-Dist: types-pywin32; extra == "type"
|
||||
Provides-Extra: completion
|
||||
Requires-Dist: shtab>=1.1.0; extra == "completion"
|
||||
Dynamic: license-file
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/keyring.svg
|
||||
:target: https://pypi.org/project/keyring
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/keyring.svg
|
||||
|
||||
.. image:: https://github.com/jaraco/keyring/actions/workflows/main.yml/badge.svg
|
||||
:target: https://github.com/jaraco/keyring/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/keyring/badge/?version=latest
|
||||
:target: https://keyring.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/keyring
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-keyring?utm_source=pypi-keyring&utm_medium=readme
|
||||
|
||||
.. image:: https://badges.gitter.im/jaraco/keyring.svg
|
||||
:alt: Join the chat at https://gitter.im/jaraco/keyring
|
||||
:target: https://gitter.im/jaraco/keyring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
The Python keyring library provides an easy way to access the
|
||||
system keyring service from python. It can be used in any
|
||||
application that needs safe password storage.
|
||||
|
||||
These recommended keyring backends are supported:
|
||||
|
||||
* macOS `Keychain
|
||||
<https://en.wikipedia.org/wiki/Keychain_%28software%29>`_
|
||||
* Freedesktop `Secret Service
|
||||
<http://standards.freedesktop.org/secret-service/>`_ supports many DE including
|
||||
GNOME (requires `secretstorage <https://pypi.python.org/pypi/secretstorage>`_)
|
||||
* KDE4 & KDE5 `KWallet <https://en.wikipedia.org/wiki/KWallet>`_
|
||||
(requires `dbus <https://pypi.python.org/pypi/dbus-python>`_)
|
||||
* `Windows Credential Locker
|
||||
<https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_
|
||||
|
||||
Other keyring implementations are available through `Third-Party Backends`_.
|
||||
|
||||
Installation - Linux
|
||||
====================
|
||||
|
||||
On Linux, the KWallet backend relies on dbus-python_, which does not always
|
||||
install correctly when using pip (compilation is needed). For best results,
|
||||
install dbus-python as a system package.
|
||||
|
||||
.. _dbus-python: https://gitlab.freedesktop.org/dbus/dbus-python
|
||||
|
||||
Compatibility - macOS
|
||||
=====================
|
||||
|
||||
macOS keychain supports macOS 11 (Big Sur) and later requires Python 3.8.7
|
||||
or later with the "universal2" binary. See
|
||||
`#525 <https://github.com/jaraco/keyring/issues/525>`_ for details.
|
||||
|
||||
Using Keyring
|
||||
=============
|
||||
|
||||
The basic usage of keyring is pretty simple: just call
|
||||
``keyring.set_password`` and ``keyring.get_password``::
|
||||
|
||||
>>> import keyring
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
Command-line Utility
|
||||
--------------------
|
||||
|
||||
Keyring supplies a ``keyring`` command which is installed with the
|
||||
package. After installing keyring in most environments, the
|
||||
command should be available for setting, getting, and deleting
|
||||
passwords. For more usage information, invoke with no arguments
|
||||
or with ``--help`` as so::
|
||||
|
||||
$ keyring --help
|
||||
$ keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ keyring get system username
|
||||
password
|
||||
|
||||
The command-line functionality is also exposed as an executable
|
||||
package, suitable for invoking from Python like so::
|
||||
|
||||
$ python -m keyring --help
|
||||
$ python -m keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ python -m keyring get system username
|
||||
password
|
||||
|
||||
Tab Completion
|
||||
--------------
|
||||
|
||||
If installed via a package manager (apt, pacman, nix, homebrew, etc),
|
||||
these shell completions may already have been distributed with the package
|
||||
(no action required).
|
||||
|
||||
Keyring provides tab completion if the ``completion`` extra is installed::
|
||||
|
||||
$ pip install 'keyring[completion]'
|
||||
|
||||
Then, generate shell completions, something like::
|
||||
|
||||
$ keyring --print-completion bash | sudo tee /usr/share/bash-completion/completions/keyring
|
||||
$ keyring --print-completion zsh | sudo tee /usr/share/zsh/site-functions/_keyring
|
||||
$ keyring --print-completion tcsh | sudo tee /etc/profile.d/keyring.csh
|
||||
|
||||
**Note**: the path of `/usr/share` is mainly for GNU/Linux. For other OSs,
|
||||
consider:
|
||||
|
||||
- macOS (Homebrew x86): /usr/local/share
|
||||
- macOS (Homebrew ARM): /opt/homebrew/share
|
||||
- Android (Termux): /data/data/com.termux/files/usr/share
|
||||
- Windows (mingw64 of msys2): /mingw64/share
|
||||
- ...
|
||||
|
||||
After installing the shell completions, enable them following your shell's
|
||||
recommended instructions. e.g.:
|
||||
|
||||
- bash: install `bash-completion <https://github.com/scop/bash-completion>`_,
|
||||
and ensure ``. /usr/share/bash-completion/bash_completion`` in ``~/.bashrc``.
|
||||
- zsh: ensure ``autoload -Uz compinit && compinit`` appears in ``~/.zshrc``,
|
||||
then ``grep -w keyring ~/.zcompdump`` to verify keyring appears, indicating
|
||||
it was installed correctly.
|
||||
|
||||
Configuring
|
||||
===========
|
||||
|
||||
The python keyring lib contains implementations for several backends. The
|
||||
library will attempt to
|
||||
automatically choose the most suitable backend for the current
|
||||
environment. Users may also specify the preferred keyring in a
|
||||
config file or by calling the ``set_keyring()`` function.
|
||||
|
||||
Config file path
|
||||
----------------
|
||||
|
||||
The configuration is stored in a file named "keyringrc.cfg"
|
||||
found in a platform-specific location. To determine
|
||||
where the config file is stored, run ``keyring diagnose``.
|
||||
|
||||
Config file content
|
||||
-------------------
|
||||
|
||||
To specify a keyring backend, set the **default-keyring** option to the
|
||||
full path of the class for that backend, such as
|
||||
``keyring.backends.macOS.Keyring``.
|
||||
|
||||
If **keyring-path** is indicated, keyring will add that path to the Python
|
||||
module search path before loading the backend.
|
||||
|
||||
For example, this config might be used to load the
|
||||
``SimpleKeyring`` from the ``simplekeyring`` module in
|
||||
the ``./demo`` directory (not implemented)::
|
||||
|
||||
[backend]
|
||||
default-keyring=simplekeyring.SimpleKeyring
|
||||
keyring-path=demo
|
||||
|
||||
Third-Party Backends
|
||||
====================
|
||||
|
||||
In addition to the backends provided by the core keyring package for
|
||||
the most common and secure use cases, there
|
||||
are additional keyring backend implementations available for other
|
||||
use cases. Simply install them to make them available:
|
||||
|
||||
- `keyrings.cryptfile <https://pypi.org/project/keyrings.cryptfile>`_
|
||||
- Encrypted text file storage.
|
||||
- `keyrings.alt <https://pypi.org/project/keyrings.alt>`_ - "alternate",
|
||||
possibly-insecure backends, originally part of the core package, but
|
||||
available for opt-in.
|
||||
- `gsheet-keyring <https://pypi.org/project/gsheet-keyring>`_
|
||||
- a backend that stores secrets in a Google Sheet. For use with
|
||||
`ipython-secrets <https://pypi.org/project/ipython-secrets>`_.
|
||||
- `bitwarden-keyring <https://pypi.org/project/bitwarden-keyring/>`_
|
||||
- a backend that stores secrets in the `BitWarden <https://bitwarden.com/>`_
|
||||
password manager.
|
||||
- `onepassword-keyring <https://pypi.org/project/onepassword-keyring/>`_
|
||||
- a backend that stores secrets in the `1Password <https://1password.com/>`_ password manager.
|
||||
- `sagecipher <https://pypi.org/project/sagecipher>`_ - an encryption
|
||||
backend which uses the ssh agent protocol's signature operation to
|
||||
derive the cipher key.
|
||||
- `keyrings.osx_keychain_keys <https://pypi.org/project/keyrings.osx-keychain-keys>`_
|
||||
- OSX keychain key-management, for private, public, and symmetric keys.
|
||||
- `keyring_pass.PasswordStoreBackend <https://github.com/nazarewk/keyring_pass>`_
|
||||
- Password Store (pass) backend for python's keyring
|
||||
- `keyring_jeepney <https://pypi.org/project/keyring_jeepney>`__ - a
|
||||
pure Python backend using the secret service DBus API for desktop
|
||||
Linux (requires ``keyring<24``).
|
||||
|
||||
|
||||
Write your own keyring backend
|
||||
==============================
|
||||
|
||||
The interface for the backend is defined by ``keyring.backend.KeyringBackend``.
|
||||
Every backend should derive from that base class and define a ``priority``
|
||||
attribute and three functions: ``get_password()``, ``set_password()``, and
|
||||
``delete_password()``. The ``get_credential()`` function may be defined if
|
||||
desired.
|
||||
|
||||
See the ``backend`` module for more detail on the interface of this class.
|
||||
|
||||
Keyring employs entry points to allow any third-party package to implement
|
||||
backends without any modification to the keyring itself. Those interested in
|
||||
creating new backends are encouraged to create new, third-party packages
|
||||
in the ``keyrings`` namespace, in a manner modeled by the `keyrings.alt
|
||||
package <https://github.com/jaraco/keyrings.alt>`_. See the
|
||||
``setup.cfg`` file
|
||||
in that project for hints on how to create the requisite entry points.
|
||||
Backends that prove essential may be considered for inclusion in the core
|
||||
library, although the ease of installing these third-party packages should
|
||||
mean that extensions may be readily available.
|
||||
|
||||
To create an extension for Keyring, please submit a pull request to
|
||||
have your extension mentioned as an available extension.
|
||||
|
||||
Runtime Configuration
|
||||
=====================
|
||||
|
||||
Keyring additionally allows programmatic configuration of the
|
||||
backend calling the api ``set_keyring()``. The indicated backend
|
||||
will subsequently be used to store and retrieve passwords.
|
||||
|
||||
To invoke ``set_keyring``::
|
||||
|
||||
# define a new keyring class which extends the KeyringBackend
|
||||
import keyring.backend
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring which always outputs the same password
|
||||
"""
|
||||
priority = 1
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
return "password from TestKeyring"
|
||||
|
||||
def delete_password(self, servicename, username):
|
||||
pass
|
||||
|
||||
# set the keyring for keyring lib
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
# invoke the keyring lib
|
||||
try:
|
||||
keyring.set_password("demo-service", "tarek", "passexample")
|
||||
print("password stored successfully")
|
||||
except keyring.errors.PasswordSetError:
|
||||
print("failed to store password")
|
||||
print("password", keyring.get_password("demo-service", "tarek"))
|
||||
|
||||
|
||||
Disabling Keyring
|
||||
=================
|
||||
|
||||
In many cases, uninstalling keyring will never be necessary.
|
||||
Especially on Windows and macOS, the behavior of keyring is
|
||||
usually degenerate, meaning it will return empty values to
|
||||
the caller, allowing the caller to fall back to some other
|
||||
behavior.
|
||||
|
||||
In some cases, the default behavior of keyring is undesirable and
|
||||
it would be preferable to disable the keyring behavior altogether.
|
||||
There are several mechanisms to disable keyring:
|
||||
|
||||
- Uninstall keyring. Most applications are tolerant to keyring
|
||||
not being installed. Uninstalling keyring should cause those
|
||||
applications to fall back to the behavior without keyring.
|
||||
This approach affects the Python environment where keyring
|
||||
would otherwise have been installed.
|
||||
|
||||
- Configure the Null keyring in the environment. Set
|
||||
``PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring``
|
||||
in the environment, and the ``Null`` (degenerate) backend
|
||||
will be used. This approach affects all uses of Keyring where
|
||||
that variable is set.
|
||||
|
||||
- Permanently configure the Null keyring for the user by running
|
||||
``keyring --disable`` or ``python -m keyring --disable``.
|
||||
This approach affects all uses of keyring for that user.
|
||||
|
||||
|
||||
Altering Keyring Behavior
|
||||
=========================
|
||||
|
||||
Keyring provides a mechanism to alter the keyring's behavior through
|
||||
environment variables. Each backend implements a
|
||||
``KeyringBackend.set_properties_from_env``, which
|
||||
when invoked will find all environment variables beginning with
|
||||
``KEYRING_PROPERTY_{NAME}`` and will set a property for each
|
||||
``{NAME.lower()}`` on the keyring. This method is invoked during
|
||||
initialization for the default/configured keyring.
|
||||
|
||||
This mechanism may be used to set some useful values on various
|
||||
keyrings, including:
|
||||
|
||||
- keychain; macOS, path to an alternate keychain file
|
||||
- appid; Linux/SecretService, alternate ID for the application
|
||||
|
||||
|
||||
Using Keyring on Ubuntu 16.04
|
||||
=============================
|
||||
|
||||
The following is a complete transcript for installing keyring in a
|
||||
virtual environment on Ubuntu 16.04. No config file was used::
|
||||
|
||||
$ sudo apt install python3-venv libdbus-glib-1-dev
|
||||
$ cd /tmp
|
||||
$ pyvenv py3
|
||||
$ source py3/bin/activate
|
||||
$ pip install -U pip
|
||||
$ pip install secretstorage dbus-python
|
||||
$ pip install keyring
|
||||
$ python
|
||||
>>> import keyring
|
||||
>>> keyring.get_keyring()
|
||||
<keyring.backends.SecretService.Keyring object at 0x7f9b9c971ba8>
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
|
||||
Using Keyring on headless Linux systems
|
||||
=======================================
|
||||
|
||||
It is possible to use the SecretService backend on Linux systems without
|
||||
X11 server available (only D-Bus is required). In this case:
|
||||
|
||||
* Install the `GNOME Keyring`_ daemon.
|
||||
* Start a D-Bus session, e.g. run ``dbus-run-session -- sh`` and run
|
||||
the following commands inside that shell.
|
||||
* Run ``gnome-keyring-daemon`` with ``--unlock`` option. The description of
|
||||
that option says:
|
||||
|
||||
Read a password from stdin, and use it to unlock the login keyring
|
||||
or create it if the login keyring does not exist.
|
||||
|
||||
When that command is started, enter a password into stdin and
|
||||
press Ctrl+D (end of data). After that, the daemon will fork into
|
||||
the background (use ``--foreground`` option to block).
|
||||
* Now you can use the SecretService backend of Keyring. Remember to
|
||||
run your application in the same D-Bus session as the daemon.
|
||||
|
||||
.. _GNOME Keyring: https://wiki.gnome.org/Projects/GnomeKeyring
|
||||
|
||||
Using Keyring on headless Linux systems in a Docker container
|
||||
=============================================================
|
||||
|
||||
It is possible to use keyring with the SecretService backend in Docker containers as well.
|
||||
All you need to do is install the necessary dependencies and add the `--privileged` flag
|
||||
to avoid any `Operation not permitted` errors when attempting to unlock the system's keyring.
|
||||
|
||||
The following is a complete transcript for installing keyring on a Ubuntu 18:04 container::
|
||||
|
||||
docker run -it -d --privileged ubuntu:18.04
|
||||
|
||||
$ apt-get update
|
||||
$ apt install -y gnome-keyring python3-venv python3-dev
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate # source a virtual environment to avoid polluting your system
|
||||
$ pip3 install --upgrade pip
|
||||
$ pip3 install keyring
|
||||
$ dbus-run-session -- sh # this will drop you into a new D-bus shell
|
||||
$ echo 'somecredstorepass' | gnome-keyring-daemon --unlock # unlock the system's keyring
|
||||
|
||||
$ python
|
||||
>>> import keyring
|
||||
>>> keyring.get_keyring()
|
||||
<keyring.backends.SecretService.Keyring object at 0x7f9b9c971ba8>
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
Using Keyring with tox
|
||||
======================
|
||||
|
||||
Some backends rely on environment variables to operate correctly, and ``tox`` filters most environment variables by default.
|
||||
|
||||
For example, when using Keyring to store credentials for pip, one may encounter the following error when
|
||||
running tests under ``tox`` when using a backend reliant on D-Bus:
|
||||
|
||||
RuntimeError: No recommended backend was available. Install the keyrings.alt package if you want to use the non-recommended backends. See README.rst for details.
|
||||
|
||||
This error is caused by Keyring KWallet backend not able to resolve the backing service.
|
||||
|
||||
To work around the issue, add ``DBUS_SESSION_BUS_ADDRESS`` to ``pass_env`` in the
|
||||
``tox`` configuration. Consider adding other necessary variables, such as ``DISPLAY`` and ``WAYLAND_DISPLAY`` (if using ``pinentry``).
|
||||
|
||||
Integration
|
||||
===========
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
The keyring lib has a few functions:
|
||||
|
||||
* ``get_keyring()``: Return the currently-loaded keyring implementation.
|
||||
* ``get_password(service, username)``: Returns the password stored in the
|
||||
active keyring. If the password does not exist, it will return None.
|
||||
* ``get_credential(service, username)``: Return a credential object stored
|
||||
in the active keyring. This object contains at least ``username`` and
|
||||
``password`` attributes for the specified service, where the returned
|
||||
``username`` may be different from the argument.
|
||||
* ``set_password(service, username, password)``: Store the password in the
|
||||
keyring.
|
||||
* ``delete_password(service, username)``: Delete the password stored in
|
||||
keyring. If the password does not exist, it will raise an exception.
|
||||
|
||||
In all cases, the parameters (``service``, ``username``, ``password``)
|
||||
should be Unicode text.
|
||||
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
The keyring lib raises the following exceptions:
|
||||
|
||||
* ``keyring.errors.KeyringError``: Base Error class for all exceptions in keyring lib.
|
||||
* ``keyring.errors.InitError``: Raised when the keyring cannot be initialized.
|
||||
* ``keyring.errors.PasswordSetError``: Raised when the password cannot be set in the keyring.
|
||||
* ``keyring.errors.PasswordDeleteError``: Raised when the password cannot be deleted in the keyring.
|
||||
|
||||
Get Involved
|
||||
============
|
||||
|
||||
Python keyring lib is an open community project and eagerly
|
||||
welcomes contributors.
|
||||
|
||||
* Repository: https://github.com/jaraco/keyring/
|
||||
* Bug Tracker: https://github.com/jaraco/keyring/issues/
|
||||
* Mailing list: http://groups.google.com/group/python-keyring
|
||||
|
||||
Security Considerations
|
||||
=======================
|
||||
|
||||
Each built-in backend may have security considerations to understand
|
||||
before using this library. Authors of tools or libraries utilizing
|
||||
``keyring`` are encouraged to consider these concerns.
|
||||
|
||||
As with any list of known security concerns, this list is not exhaustive.
|
||||
Additional issues can be added as needed.
|
||||
|
||||
- macOS Keychain
|
||||
- Any Python script or application can access secrets created by
|
||||
``keyring`` from that same Python executable without the operating
|
||||
system prompting the user for a password. To cause any specific
|
||||
secret to prompt for a password every time it is accessed, locate
|
||||
the credential using the ``Keychain Access`` application, and in
|
||||
the ``Access Control`` settings, remove ``Python`` from the list
|
||||
of allowed applications.
|
||||
|
||||
- Freedesktop Secret Service
|
||||
- No analysis has been performed
|
||||
|
||||
- KDE4 & KDE5 KWallet
|
||||
- No analysis has been performed
|
||||
|
||||
- Windows Credential Locker
|
||||
- No analysis has been performed
|
||||
|
||||
Making Releases
|
||||
===============
|
||||
|
||||
This project makes use of automated releases and continuous
|
||||
integration. The
|
||||
simple workflow is to tag a commit and push it to Github. If it
|
||||
passes tests in CI, it will be automatically deployed to PyPI.
|
||||
|
||||
Other things to consider when making a release:
|
||||
|
||||
- Check that the changelog is current for the intended release.
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Tests are continuously run in Github Actions.
|
||||
|
||||
To run the tests locally, install and invoke
|
||||
`tox <https://pypi.org/project/tox>`_.
|
||||
|
||||
Background
|
||||
==========
|
||||
|
||||
The project was based on Tarek Ziade's idea in `this post`_. Kang Zhang
|
||||
initially carried it out as a `Google Summer of Code`_ project, and Tarek
|
||||
mentored Kang on this project.
|
||||
|
||||
.. _this post: http://tarekziade.wordpress.com/2009/03/27/pycon-hallway-session-1-a-keyring-library-for-python/
|
||||
.. _Google Summer of Code: http://socghop.appspot.com/
|
||||
|
||||
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-keyring?utm_source=pypi-keyring&utm_medium=referral&utm_campaign=github>`_.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user