ConfigTools & Encription manager

This commit is contained in:
2026-02-19 00:55:27 +02:00
parent f8d45d156b
commit 0b1cd52df1
203 changed files with 18643 additions and 0 deletions

View File

@@ -0,0 +1 @@
pip

View 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.

View 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>`_.

View 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

View File

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

View File

@@ -0,0 +1 @@
jaraco

View File

Binary file not shown.

View 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)

View 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

View 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]

View File

View 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'

View File

View 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)
"""

View 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]: ...

View File

View File

@@ -0,0 +1 @@
pip

View 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>`_.

View 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

View File

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

View 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.

View File

@@ -0,0 +1 @@
jaraco

View File

@@ -0,0 +1 @@
pip

View 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>`_.

View 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

View File

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

View 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.

View File

@@ -0,0 +1 @@
jaraco

View File

@@ -0,0 +1 @@
pip

View 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/>`__

View 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

View File

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

View 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
View 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'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

144
lib/jeepney/auth.py Normal file
View 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
View 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
View 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
View 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
View 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

View File

@@ -0,0 +1 @@
from .common import RouterClosed

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

233
lib/jeepney/io/asyncio.py Normal file
View 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
View 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
View 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")

View File

Binary file not shown.

View 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

View 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()

View 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

View 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

View 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

View 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
View 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
View 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
View 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))

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>

View 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

View 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'

View 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'

View 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,)
))

View 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()

View 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

View 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
View 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

View File

@@ -0,0 +1 @@
pip

View 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