Hello community,
here is the log from the commit of package python-pulsectl for openSUSE:Factory checked in at 2019-06-06 18:17:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pulsectl (Old)
and /work/SRC/openSUSE:Factory/.python-pulsectl.new.4811 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pulsectl"
Thu Jun 6 18:17:17 2019 rev:3 rq:707817 version:18.12.5
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pulsectl/python-pulsectl.changes 2018-12-24 11:41:33.717443086 +0100
+++ /work/SRC/openSUSE:Factory/.python-pulsectl.new.4811/python-pulsectl.changes 2019-06-06 18:17:20.512685684 +0200
@@ -1,0 +2,10 @@
+Wed Jun 5 08:09:07 UTC 2019 - Marketa Calabkova
+
+- update to version 18.12.5
+ * pulse.connect() can now be used to reconnect to same server
+ * _pulse_op_cb: check connected state instead of _loop_stop
+ * _pulse_op_cb: fix hang if daemon dies
+ * tests: use "-F /dev/stdin" instead of -C for dummy pulse instance
+ * Add pulsectl.lookup util submodule
+
+-------------------------------------------------------------------
Old:
----
pulsectl-17.12.2.tar.gz
New:
----
pulsectl-18.12.5.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-pulsectl.spec ++++++
--- /var/tmp/diff_new_pack.3tIX5o/_old 2019-06-06 18:17:21.012685538 +0200
+++ /var/tmp/diff_new_pack.3tIX5o/_new 2019-06-06 18:17:21.012685538 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-pulsectl
#
-# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%bcond_without test
Name: python-pulsectl
-Version: 17.12.2
+Version: 18.12.5
Release: 0
Summary: Python high-level interface and ctypes-based bindings for PulseAudio (libpulse)
License: MIT
@@ -57,7 +57,7 @@
%if %{with test}
%check
-%python_exec setup.py test
+%python_exec -m unittest pulsectl.tests.all
%endif
%files %{python_files}
++++++ pulsectl-17.12.2.tar.gz -> pulsectl-18.12.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/CHANGES.rst new/pulsectl-18.12.5/CHANGES.rst
--- old/pulsectl-17.12.2/CHANGES.rst 2017-12-14 21:32:25.000000000 +0100
+++ new/pulsectl-18.12.5/CHANGES.rst 2018-12-10 18:48:02.000000000 +0100
@@ -8,13 +8,16 @@
Each entry is a package version which change first appears in, followed by
description of the change itself.
-Last synced/updated: 17.12.2
+Last synced/updated: 18.12.2
---------------------------------------------------------------------------
+- 18.10.5: pulse.connect() can now be used to reconnect to same server.
+
- 17.12.2: Use pa_card_profile_info2 / profiles2 introspection API.
Only adds one "available" property to PulseCardProfileInfo.
+ Requires pulseaudio/libpulse 5.0+.
- 17.9.3: Add wrappers for Pulse.get_sink_by_name / Pulse.get_source_by_name [#17].
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/PKG-INFO new/pulsectl-18.12.5/PKG-INFO
--- old/pulsectl-17.12.2/PKG-INFO 2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-18.12.5/PKG-INFO 2018-12-18 23:49:04.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pulsectl
-Version: 17.12.2
+Version: 18.12.5
Summary: Python high-level interface and ctypes-based bindings for PulseAudio (libpulse)
Home-page: http://github.com/mk-fg/python-pulse-control
Author: George Filipkin, Mike Kazantsev
@@ -104,13 +104,18 @@
and everything returned from these are "Pulse-Something-Info" objects - thin
wrappers around C structs that describe the thing, without any methods attached.
+ Aside from a few added convenience methods, most of them should have similar
+ signature and do same thing as their C libpulse API counterparts, so see
+ `pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio, twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a separate
thread.
Somewhat extended usage example can be found in `pulseaudio-mixer-cli`_ project
- code.
+ code, as well as tests here.
+ .. _pulseaudio doxygen documentation: https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli: https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/README.rst new/pulsectl-18.12.5/README.rst
--- old/pulsectl-17.12.2/README.rst 2017-10-26 12:33:28.000000000 +0200
+++ new/pulsectl-18.12.5/README.rst 2018-12-18 23:46:49.000000000 +0100
@@ -96,13 +96,18 @@
and everything returned from these are "Pulse-Something-Info" objects - thin
wrappers around C structs that describe the thing, without any methods attached.
+Aside from a few added convenience methods, most of them should have similar
+signature and do same thing as their C libpulse API counterparts, so see
+`pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio, twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a separate
thread.
Somewhat extended usage example can be found in `pulseaudio-mixer-cli`_ project
-code.
+code, as well as tests here.
+.. _pulseaudio doxygen documentation: https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli: https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/__init__.py new/pulsectl-18.12.5/pulsectl/__init__.py
--- old/pulsectl-17.12.2/pulsectl/__init__.py 2017-07-13 12:23:26.000000000 +0200
+++ new/pulsectl-18.12.5/pulsectl/__init__.py 2018-03-03 01:03:48.000000000 +0100
@@ -4,8 +4,9 @@
from . import _pulsectl
from .pulsectl import (
- PulseCardInfo, PulseClientInfo, PulsePortInfo, PulseVolumeInfo,
+ PulsePortInfo, PulseClientInfo, PulseServerInfo, PulseModuleInfo,
PulseSinkInfo, PulseSinkInputInfo, PulseSourceInfo, PulseSourceOutputInfo,
+ PulseCardProfileInfo, PulseCardPortInfo, PulseCardInfo, PulseVolumeInfo,
PulseExtStreamRestoreInfo, PulseEventInfo,
PulseEventTypeEnum, PulseEventFacilityEnum, PulseEventMaskEnum,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/_pulsectl.py new/pulsectl-18.12.5/pulsectl/_pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/_pulsectl.py 2017-12-14 21:29:35.000000000 +0100
+++ new/pulsectl-18.12.5/pulsectl/_pulsectl.py 2018-12-18 22:26:30.000000000 +0100
@@ -264,6 +264,7 @@
('name', c_char_p),
('owner_module', c_uint32),
('driver', c_char_p),
+ ('proplist', POINTER(PA_PROPLIST)),
]
class PA_SERVER_INFO(Structure):
@@ -473,6 +474,7 @@
pa_context_connect=([POINTER(PA_CONTEXT), c_str_p, c_int, POINTER(c_int)], 'int_check_ge0'),
pa_context_get_state=([POINTER(PA_CONTEXT)], c_int),
pa_context_disconnect=[POINTER(PA_CONTEXT)],
+ pa_context_unref=[POINTER(PA_CONTEXT)],
pa_context_drain=( 'pa_op',
[POINTER(PA_CONTEXT), PA_CONTEXT_DRAIN_CB_T, c_void_p] ),
pa_context_set_default_sink=( 'pa_op',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/lookup.py new/pulsectl-18.12.5/pulsectl/lookup.py
--- old/pulsectl-17.12.2/pulsectl/lookup.py 1970-01-01 01:00:00.000000000 +0100
+++ new/pulsectl-18.12.5/pulsectl/lookup.py 2018-04-23 18:48:50.000000000 +0200
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function, unicode_literals
+
+import itertools as it, operator as op, functools as ft
+import re
+
+
+lookup_types = {
+ 'sink': 'sink_list', 'source': 'source_list',
+ 'sink-input': 'sink_input_list', 'source-output': 'source_output_list' }
+lookup_types.update(it.chain.from_iterable(
+ ((v, lookup_types[k]) for v in v) for k,v in
+ { 'source': ['src'], 'sink-input': ['si', 'playback', 'play'],
+ 'source-output': ['so', 'record', 'rec', 'mic'] }.items() ))
+
+lookup_key_defaults = dict(
+ # No default keys for type = no implicit matches for that type
+ sink_input_list=[ # match sink_input_list objects with these keys by default
+ 'media.name', 'media.icon_name', 'media.role',
+ 'application.name', 'application.process.binary', 'application.icon_name' ] )
+
+
+def pulse_obj_lookup(pulse, obj_lookup, prop_default=None):
+ '''Return set of pulse object(s) with proplist values matching lookup-string.
+
+ Pattern syntax:
+ [ { 'sink' | 'source' | 'sink-input' | 'source-output' } [ / ... ] ':' ]
+ [ proplist-key-name (non-empty) [ / ... ] ':' ] [ ':' (for regexp match) ]
+ [ proplist-key-value ]
+
+ Examples:
+ - sink:alsa.driver_name:snd_hda_intel
+ Match sink(s) with alsa.driver_name=snd_hda_intel (exact match!).
+ - sink/source:device.bus:pci
+ Match all sinks and sources with device.bus=pci.
+ - myprop:somevalue
+ Match any object (of all 4 supported types) that has myprop=somevalue.
+ - mpv
+ Match any object with any of the "default lookup props" (!!!) being equal to "mpv".
+ "default lookup props" are specified per-type in lookup_key_defaults above.
+ For example, sink input will be looked-up by media.name, application.name, etc.
+ - sink-input/source-output:mpv
+ Same as above, but lookup streams only (not sinks/sources).
+ Note that "sink-input/source-output" matches type spec, and parsed as such, not as key.
+ - si/so:mpv
+ Same as above - see aliases for types in lookup_types.
+ - application.binary/application.icon:mpv
+ Lookup by multiple keys with "any match" logic, same as with multiple object types.
+ - key\/with\/slashes\:and\:colons:somevalue
+ Lookup by key that has slashes and colons in it.
+ "/" and ":" must only be escaped in the proplist key part, used as-is in values.
+ Backslash itself can be escaped as well, i.e. as "\\".
+ - module-stream-restore.id:sink-input-by-media-role:music
+ Value has ":" in it, but there's no need to escape it in any way.
+ - device.description::Analog
+ Value lookup starting with : is interpreted as a regexp,
+ i.e. any object with device.description *containing* "Analog" in this case.
+ - si/so:application.name::^mpv\b
+ Return all sink-inputs/source-outputs ("si/so") where
+ "application.name" proplist value matches regexp "^mpv\b".
+ - :^mpv\b
+ Regexp lookup (stuff starting with "mpv" word) without type or key specification.
+
+ For python2, lookup string should be unicode type.
+ "prop_default" keyword arg can be used to specify
+ default proplist value for when key is not found there.'''
+
+ # \ue000-\uf8ff - private use area, never assigned to symbols
+ obj_lookup = obj_lookup.replace('\\\\', '\ue000').replace('\\:', '\ue001')
+ obj_types_re = '({0})(/({0}))*'.format('|'.join(lookup_types))
+ m = re.search(
+ ( r'^((?P<t>{}):)?'.format(obj_types_re) +
+ r'((?P<k>.+?):)?' r'(?P<v>.*)$' ), obj_lookup, re.IGNORECASE )
+ if not m: raise ValueError(obj_lookup)
+ lookup_type, lookup_keys, lookup_re = op.itemgetter('t', 'k', 'v')(m.groupdict())
+ if lookup_keys:
+ lookup_keys = list(
+ v.replace('\ue000', '\\\\').replace('\ue001', ':').replace('\ue002', '/')
+ for v in lookup_keys.replace('\\/', '\ue002').split('/') )
+ lookup_re = lookup_re.replace('\ue000', '\\\\').replace('\ue001', '\\:')
+ obj_list_res, lookup_re = list(), re.compile( lookup_re[1:]
+ if lookup_re.startswith(':') else '^{}$'.format(re.escape(lookup_re)) )
+ for k in set( lookup_types[k] for k in
+ (lookup_type.split('/') if lookup_type else lookup_types.keys()) ):
+ if not lookup_keys: lookup_keys = lookup_key_defaults.get(k)
+ if not lookup_keys: continue
+ obj_list = getattr(pulse, k)()
+ if not obj_list: continue
+ for obj, k in it.product(obj_list, lookup_keys):
+ v = obj.proplist.get(k, prop_default)
+ if v is None: continue
+ if lookup_re.search(v): obj_list_res.append(obj)
+ return set(obj_list_res)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/pulsectl.py new/pulsectl-18.12.5/pulsectl/pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/pulsectl.py 2017-12-14 21:27:45.000000000 +0100
+++ new/pulsectl-18.12.5/pulsectl/pulsectl.py 2018-10-31 11:58:47.000000000 +0100
@@ -81,6 +81,10 @@
def __repr__(self):
return ''.format(self._name, ' '.join(sorted(self._values.keys())))
+class FakeLock():
+ def __enter__(self): return self
+ def __exit__(self, *err): pass
+
PulseEventTypeEnum = Enum('event-type', c.PA_EVENT_TYPE_MAP)
PulseEventFacilityEnum = Enum('event-facility', c.PA_EVENT_FACILITY_MAP)
@@ -229,9 +233,10 @@
c_struct_fields = 'name description n_sinks n_sources priority available'
class PulseCardPortInfo(PulsePortInfo):
- c_struct_fields = 'name description priority direction latency_offset'
+ c_struct_fields = 'name description available priority direction latency_offset'
def _init_from_struct(self, struct):
+ super(PulseCardPortInfo, self)._init_from_struct(struct)
self.direction = PulseDirectionEnum._c_val(struct.direction)
class PulseCardInfo(PulseObject):
@@ -327,6 +332,8 @@
class Pulse(object):
+ _ctx = None
+
def __init__(self, client_name=None, server=None, connect=True, threading_lock=False):
'''Connects to specified pulse server by default.
Specifying "connect=False" here prevents that, but be sure to call connect() later.
@@ -357,19 +364,26 @@
self._pa_state_cb = c.PA_STATE_CB_T(self._pulse_state_cb)
self._pa_subscribe_cb = c.PA_SUBSCRIBE_CB_T(self._pulse_subscribe_cb)
- self._loop, self._loop_lock = c.pa.mainloop_new(), None
+ self._loop, self._loop_lock = c.pa.mainloop_new(), FakeLock()
self._loop_running = self._loop_closed = False
self._api = c.pa.mainloop_get_api(self._loop)
+ self._ret = c.pa.return_value()
- self._ctx, self._ret = c.pa.context_new(self._api, self.name), c.pa.return_value()
- c.pa.context_set_state_callback(self._ctx, self._pa_state_cb, None)
-
- c.pa.context_set_subscribe_callback(self._ctx, self._pa_subscribe_cb, None)
+ self._ctx_init()
self.event_types = sorted(PulseEventTypeEnum._values.values())
self.event_facilities = sorted(PulseEventFacilityEnum._values.values())
self.event_masks = sorted(PulseEventMaskEnum._values.values())
self.event_callback = None
+ def _ctx_init(self):
+ if self._ctx:
+ with self._loop_lock:
+ self.disconnect()
+ c.pa.context_unref(self._ctx)
+ self._ctx = c.pa.context_new(self._api, self.name)
+ c.pa.context_set_state_callback(self._ctx, self._pa_state_cb, None)
+ c.pa.context_set_subscribe_callback(self._ctx, self._pa_subscribe_cb, None)
+
def connect(self, autospawn=False, wait=False):
'''Connect to pulseaudio server.
"autospawn" option will start new pulse daemon, if necessary.
@@ -377,6 +391,7 @@
if self._loop_closed:
raise PulseError('Eventloop object was already'
' destroyed and cannot be reused from this instance.')
+ if self.connected is not None: self._ctx_init()
flags, self.connected = 0, None
if not autospawn: flags |= c.PA_CONTEXT_NOAUTOSPAWN
if wait: flags |= c.PA_CONTEXT_NOFAIL
@@ -390,13 +405,15 @@
c.pa.context_disconnect(self._ctx)
def close(self):
- if self._loop:
- if self._loop_running:
- self._loop_closed = True
- c.pa.mainloop_quit(self._loop, 0)
- return
+ if not self._loop: return
+ if self._loop_running: # called from another thread
+ self._loop_closed = True
+ c.pa.mainloop_quit(self._loop, 0)
+ return # presumably will be closed in a thread that's running it
+ with self._loop_lock:
try:
self.disconnect()
+ c.pa.context_unref(self._ctx)
c.pa.mainloop_free(self._loop)
finally: self._ctx = self._loop = None
@@ -430,8 +447,8 @@
@contextmanager
def _pulse_loop(self):
- if self._loop_lock: self._loop_lock.acquire()
- try:
+ with self._loop_lock:
+ if not self._loop: return
if self._loop_running:
raise PulseError(
'Running blocking pulse operations from pulse eventloop callbacks'
@@ -445,8 +462,6 @@
finally:
self._loop_running = False
if self._loop_closed: self.close() # to free() after stopping it
- finally:
- if self._loop_lock: self._loop_lock.release()
def _pulse_run(self):
with self._pulse_loop() as loop: c.pa.mainloop_run(loop, self._ret)
@@ -462,7 +477,7 @@
cb = lambda s=True,k=act_id: self._actions.update({k: bool(s)})
if not raw: cb = c.PA_CONTEXT_SUCCESS_CB_T(lambda ctx,s,d,cb=cb: cb(s))
yield cb
- while self._actions[act_id] is None: self._pulse_iterate()
+ while self.connected and self._actions[act_id] is None: self._pulse_iterate()
if not self._actions[act_id]: raise PulseOperationFailed(act_id)
finally: self._actions.pop(act_id, None)
@@ -519,7 +534,7 @@
c.PA_SINK_INFO_CB_T,
c.pa.context_get_sink_info_by_name, PulseSinkInfo )
get_source_by_name = _pulse_get_list(
- c.PA_SINK_INFO_CB_T,
+ c.PA_SOURCE_INFO_CB_T,
c.pa.context_get_source_info_by_name, PulseSourceInfo )
sink_input_list = _pulse_get_list(
@@ -850,7 +865,9 @@
with open(pid_path) as src: os.kill(int(src.read().strip()), signal.SIGUSR2)
time.sleep(max(0, retry_delay - (c.mono_time() - ts)))
- return s.makefile('rw', 1) if as_file else s
+ if as_file: res = s.makefile('rw', 1)
+ else: res, s = s, None # to avoid closing this socket
+ return res
except Exception as err: # CallError, socket.error, IOError (pidfile), OSError (os.kill)
raise PulseError( 'Failed to connect to pulse'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/tests/all.py new/pulsectl-18.12.5/pulsectl/tests/all.py
--- old/pulsectl-17.12.2/pulsectl/tests/all.py 2017-07-13 12:23:26.000000000 +0200
+++ new/pulsectl-18.12.5/pulsectl/tests/all.py 2018-10-28 16:08:52.000000000 +0100
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-from .dummy_instance import DummyTests
+from .dummy_instance import DummyTests, PulseCrashTests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/tests/dummy_instance.py new/pulsectl-18.12.5/pulsectl/tests/dummy_instance.py
--- old/pulsectl-17.12.2/pulsectl/tests/dummy_instance.py 2017-07-13 12:23:26.000000000 +0200
+++ new/pulsectl-18.12.5/pulsectl/tests/dummy_instance.py 2018-12-18 23:26:25.000000000 +0100
@@ -13,107 +13,142 @@
import pulsectl
-def setup_teardown(cls):
- for sig in 'hup', 'term', 'int':
- signal.signal(getattr(signal, 'sig{}'.format(sig).upper()), lambda sig,frm: sys.exit())
- atexit.register(cls.tearDownClass)
-class DummyTests(unittest.TestCase):
-
- tmp_dir = proc = None
- sock_unix = sock_tcp4 = sock_tcp6 = None
-
- @classmethod
- def setUpClass(cls):
- setup_teardown(cls)
+class adict(dict):
+ def __init__(self, *args, **kws):
+ super(adict, self).__init__(*args, **kws)
+ self.__dict__ = self
- # These are to allow starting pulse with debug logging
- # or using pre-started (e.g. with gdb attached) instance
- # For example:
- # t1% env -i XDG_RUNTIME_DIR=/tmp/pulsectl-tests \
- # gdb --args /usr/bin/pulseaudio --daemonize=no --fail \
- # -nF /tmp/pulsectl-tests/conf.pa --exit-idle-time=-1 --log-level=debug
- # t2% PA_TMPDIR=/tmp/pulsectl-tests PA_REUSE=t python -m -m unittest pulsectl.tests.all
- env_tmpdir, env_debug, env_reuse = map(
- os.environ.get, ['PA_TMPDIR', 'PA_DEBUG', 'PA_REUSE'] )
-
- tmp_base = env_tmpdir or cls.tmp_dir
- if not tmp_base: tmp_base = cls.tmp_dir = tempfile.mkdtemp(prefix='pulsectl-tests.')
- tmp_base = os.path.realpath(tmp_base)
- tmp_path = ft.partial(os.path.join, tmp_base)
+def dummy_pulse_init(info=None):
+ if not info: info = adict(proc=None, tmp_dir=None)
+ try: _dummy_pulse_init(info)
+ except Exception:
+ dummy_pulse_cleanup(info)
+ raise
+ return info
+
+def _dummy_pulse_init(info):
+ # These are to allow starting pulse with debug logging
+ # or using pre-started (e.g. with gdb attached) instance.
+ # Note: PA_REUSE=1234:1234:1235 are localhost tcp ports for tcp modules.
+ # For example:
+ # t1% env -i XDG_RUNTIME_DIR=/tmp/pulsectl-tests \
+ # gdb --args /usr/bin/pulseaudio --daemonize=no --fail \
+ # -nF /tmp/pulsectl-tests/conf.pa --exit-idle-time=-1 --log-level=debug
+ # t2% PA_TMPDIR=/tmp/pulsectl-tests PA_REUSE=1234,1235 python -m -m unittest pulsectl.tests.all
+ env_tmpdir, env_debug, env_reuse = map(
+ os.environ.get, ['PA_TMPDIR', 'PA_DEBUG', 'PA_REUSE'] )
+
+ tmp_base = env_tmpdir or info.get('tmp_dir')
+ if not tmp_base:
+ tmp_base = info.tmp_dir = tempfile.mkdtemp(prefix='pulsectl-tests.')
+ info.sock_unix = None
+ tmp_base = os.path.realpath(tmp_base)
+ tmp_path = ft.partial(os.path.join, tmp_base)
- # Pick some random available localhost ports
+ # Pick some random available localhost ports
+ if not info.get('sock_unix'):
bind = ( ['127.0.0.1', 0, socket.AF_INET],
['::1', 0, socket.AF_INET6], ['127.0.0.1', 0, socket.AF_INET] )
- for spec in bind:
+ for n, spec in enumerate(bind):
+ if env_reuse:
+ spec[1] = int(env_reuse.split(':')[n])
+ continue
addr, p, af = spec
with contextlib.closing(socket.socket(af, socket.SOCK_STREAM)) as s:
s.bind((addr, p))
s.listen(1)
spec[1] = s.getsockname()[1]
- cls.sock_unix = 'unix:{}'.format(tmp_path('pulse', 'native'))
- cls.sock_tcp4 = 'tcp4:{}:{}'.format(bind[0][0], bind[0][1])
- cls.sock_tcp6 = 'tcp6:[{}]:{}'.format(bind[1][0], bind[1][1])
- cls.sock_tcp_cli = tuple(bind[2][:2])
-
- if not env_reuse and not cls.proc:
- env = dict(XDG_RUNTIME_DIR=tmp_base, PULSE_STATE_PATH=tmp_base)
- log_level = 'error' if not env_debug else 'debug'
- cls.proc = subprocess.Popen(
- [ 'pulseaudio', '--daemonize=no', '--fail',
- '-nC', '--exit-idle-time=-1', '--log-level={}'.format(log_level) ],
- env=env, stdin=subprocess.PIPE )
- for line in [
- 'module-augment-properties',
-
- 'module-default-device-restore',
- 'module-rescue-streams',
- 'module-always-sink',
- 'module-intended-roles',
- 'module-suspend-on-idle',
- 'module-position-event-sounds',
- 'module-role-cork',
- 'module-filter-heuristics',
- 'module-filter-apply',
- 'module-switch-on-port-available',
- 'module-stream-restore',
-
- 'module-native-protocol-tcp auth-anonymous=true'
- ' listen={addr4} port={port4}'.format(addr4=bind[0][0], port4=bind[0][1]),
- 'module-native-protocol-tcp auth-anonymous=true'
- ' listen={addr6} port={port6}'.format(addr6=bind[1][0], port6=bind[1][1]),
- 'module-native-protocol-unix',
-
- 'module-null-sink',
- 'module-null-sink' ]:
- if line.startswith('module-'): line = 'load-module {}'.format(line)
- cls.proc.stdin.write('{}\n'.format(line).encode('utf-8'))
- cls.proc.stdin.flush()
- timeout, checks, p = 4, 10, cls.sock_unix.split(':', 1)[-1]
- for n in range(checks):
- if not os.path.exists(p):
- time.sleep(float(timeout) / checks)
- continue
- break
- else: raise AssertionError(p)
+ info.update(
+ sock_unix='unix:{}'.format(tmp_path('pulse', 'native')),
+ sock_tcp4='tcp4:{}:{}'.format(bind[0][0], bind[0][1]),
+ sock_tcp6='tcp6:[{}]:{}'.format(bind[1][0], bind[1][1]),
+ sock_tcp_cli=tuple(bind[2][:2]) )
+
+ if info.proc and info.proc.poll() is not None: info.proc = None
+ if not env_reuse and not info.get('proc'):
+ env = dict(XDG_RUNTIME_DIR=tmp_base, PULSE_STATE_PATH=tmp_base)
+ log_level = 'error' if not env_debug else 'debug'
+ info.proc = subprocess.Popen(
+ [ 'pulseaudio', '--daemonize=no', '--fail',
+ '-nF', '/dev/stdin', '--exit-idle-time=-1', '--log-level={}'.format(log_level) ],
+ env=env, stdin=subprocess.PIPE )
+ bind4, bind6 = info.sock_tcp4.split(':'), info.sock_tcp6.rsplit(':', 1)
+ bind4, bind6 = (bind4[1], bind4[2]), (bind6[0].split(':', 1)[1].strip('[]'), bind6[1])
+ for line in [
+ 'module-augment-properties',
+
+ 'module-default-device-restore',
+ 'module-rescue-streams',
+ 'module-always-sink',
+ 'module-intended-roles',
+ 'module-suspend-on-idle',
+ 'module-position-event-sounds',
+ 'module-role-cork',
+ 'module-filter-heuristics',
+ 'module-filter-apply',
+ 'module-switch-on-port-available',
+ 'module-stream-restore',
+
+ 'module-native-protocol-tcp auth-anonymous=true'
+ ' listen={} port={}'.format(*bind4),
+ 'module-native-protocol-tcp auth-anonymous=true'
+ ' listen={} port={}'.format(*bind6),
+ 'module-native-protocol-unix',
+
+ 'module-null-sink',
+ 'module-null-sink' ]:
+ if line.startswith('module-'): line = 'load-module {}'.format(line)
+ info.proc.stdin.write('{}\n'.format(line).encode('utf-8'))
+ info.proc.stdin.close()
+ timeout, checks, p = 4, 10, info.sock_unix.split(':', 1)[-1]
+ for n in range(checks):
+ if not os.path.exists(p):
+ time.sleep(float(timeout) / checks)
+ continue
+ break
+ else: raise AssertionError(p)
+
+def dummy_pulse_cleanup(info=None, proc=None, tmp_dir=None):
+ if not info: info = adict(proc=proc, tmp_dir=tmp_dir)
+ if info.proc:
+ try: info.proc.terminate()
+ except OSError: pass
+ timeout, checks = 4, 10
+ for n in range(checks):
+ if info.proc.poll() is None:
+ time.sleep(float(timeout) / checks)
+ continue
+ break
+ else:
+ try: info.proc.kill()
+ except OSError: pass
+ info.proc.wait()
+ info.proc = None
+ if info.tmp_dir:
+ shutil.rmtree(info.tmp_dir, ignore_errors=True)
+ info.tmp_dir = None
+
+
+class DummyTests(unittest.TestCase):
+
+ proc = tmp_dir = None
+
+ @classmethod
+ def setUpClass(cls):
+ assert not cls.proc and not cls.tmp_dir, [cls.proc, cls.tmp_dir]
+
+ for sig in 'hup', 'term', 'int':
+ signal.signal(getattr(signal, 'sig{}'.format(sig).upper()), lambda sig,frm: sys.exit())
+ atexit.register(cls.tearDownClass)
+
+ cls.instance_info = dummy_pulse_init()
+ for k, v in cls.instance_info.items(): setattr(cls, k, v)
@classmethod
def tearDownClass(cls):
- if cls.proc:
- cls.proc.stdin.close()
- timeout, checks = 4, 10
- for n in range(checks):
- if cls.proc.poll() is None:
- time.sleep(float(timeout) / checks)
- continue
- break
- else: cls.proc.kill()
- cls.proc.wait()
- cls.proc = None
- if cls.tmp_dir:
- shutil.rmtree(cls.tmp_dir)
- cls.tmp_dir = None
+ dummy_pulse_cleanup(cls.instance_info)
# Fuzzy float comparison is necessary for volume,
@@ -215,6 +250,13 @@
xdg_dir_prev = os.environ.get('XDG_RUNTIME_DIR')
try:
os.environ['XDG_RUNTIME_DIR'] = self.tmp_dir
+ with contextlib.closing(pulsectl.connect_to_cli(as_file=False)) as s:
+ s.send(b'dump\n')
+ while True:
+ try: buff = s.recv(2**20)
+ except socket.error: buff = None
+ if not buff: raise AssertionError
+ if b'### EOF' in buff.splitlines(): break
with contextlib.closing(pulsectl.connect_to_cli()) as s:
s.write('dump\n')
for line in s:
@@ -271,6 +313,12 @@
self.assertEqual(pulse.sink_info(sink.index).volume.values, sink.volume.values)
pulse.volume_set_all_chans(sink, 1.0)
+ def test_get_sink_src(self):
+ with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+ src, sink = pulse.source_list()[0], pulse.sink_list()[0]
+ self.assertEqual(sink.index, pulse.get_sink_by_name(sink.name).index)
+ self.assertEqual(src.index, pulse.get_source_by_name(src.name).index)
+
def test_module_funcs(self):
with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
self.assertEqual(len(pulse.sink_list()), 2)
@@ -376,5 +424,93 @@
self.assertNotIn(sr_name1, sr_dict)
self.assertNotIn(sr_name2, sr_dict)
+ def test_stream_move(self):
+ with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+ stream_started = list()
+ def stream_ev_cb(ev):
+ if ev.t != 'new': return
+ stream_started.append(ev.index)
+ raise pulsectl.PulseLoopStop
+ pulse.event_mask_set('sink_input')
+ pulse.event_callback_set(stream_ev_cb)
+
+ paplay = subprocess.Popen(
+ ['paplay', '--raw', '/dev/zero'], env=dict(XDG_RUNTIME_DIR=self.tmp_dir) )
+ try:
+ if not stream_started: pulse.event_listen()
+ stream_idx, = stream_started
+ stream = pulse.sink_input_info(stream_idx)
+ sink_indexes = set(s.index for s in pulse.sink_list())
+ sink1 = stream.sink
+ sink2 = sink_indexes.difference([sink1]).pop()
+ sink_nx = max(sink_indexes) + 1
+
+ pulse.sink_input_move(stream.index, sink2)
+ stream_new = pulse.sink_input_info(stream.index)
+ self.assertEqual(stream.sink, sink1) # old info doesn't get updated
+ self.assertEqual(stream_new.sink, sink2)
+
+ pulse.sink_input_move(stream.index, sink1) # move it back
+ stream_new = pulse.sink_input_info(stream.index)
+ self.assertEqual(stream_new.sink, sink1)
+
+ with self.assertRaises(pulsectl.PulseOperationFailed):
+ pulse.sink_input_move(stream.index, sink_nx)
+
+ finally:
+ if paplay.poll() is None: paplay.kill()
+ paplay.wait()
+
+
+class PulseCrashTests(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ for sig in 'hup', 'term', 'int':
+ signal.signal(getattr(signal, 'sig{}'.format(sig).upper()), lambda sig,frm: sys.exit())
+
+ def test_crash_after_connect(self):
+ info = dummy_pulse_init()
+ try:
+ with pulsectl.Pulse('t', server=info.sock_unix) as pulse:
+ for si in pulse.sink_list(): self.assertTrue(si)
+ info.proc.terminate()
+ info.proc.wait()
+ with self.assertRaises(pulsectl.PulseOperationFailed):
+ for si in pulse.sink_list(): raise AssertionError(si)
+ self.assertFalse(pulse.connected)
+ finally: dummy_pulse_cleanup(info)
+
+ def test_reconnect(self):
+ info = dummy_pulse_init()
+ try:
+ with pulsectl.Pulse('t', server=info.sock_unix, connect=False) as pulse:
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise AssertionError(si)
+
+ pulse.connect(autospawn=False)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+ info.proc.terminate()
+ info.proc.wait()
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise AssertionError(si)
+ self.assertFalse(pulse.connected)
+
+ dummy_pulse_init(info)
+ pulse.connect(autospawn=False, wait=True)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+
+ pulse.disconnect()
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise AssertionError(si)
+ self.assertFalse(pulse.connected)
+ pulse.connect(autospawn=False)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+
+ finally: dummy_pulse_cleanup(info)
+
if __name__ == '__main__': unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl.egg-info/PKG-INFO new/pulsectl-18.12.5/pulsectl.egg-info/PKG-INFO
--- old/pulsectl-17.12.2/pulsectl.egg-info/PKG-INFO 2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-18.12.5/pulsectl.egg-info/PKG-INFO 2018-12-18 23:49:03.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pulsectl
-Version: 17.12.2
+Version: 18.12.5
Summary: Python high-level interface and ctypes-based bindings for PulseAudio (libpulse)
Home-page: http://github.com/mk-fg/python-pulse-control
Author: George Filipkin, Mike Kazantsev
@@ -104,13 +104,18 @@
and everything returned from these are "Pulse-Something-Info" objects - thin
wrappers around C structs that describe the thing, without any methods attached.
+ Aside from a few added convenience methods, most of them should have similar
+ signature and do same thing as their C libpulse API counterparts, so see
+ `pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio, twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a separate
thread.
Somewhat extended usage example can be found in `pulseaudio-mixer-cli`_ project
- code.
+ code, as well as tests here.
+ .. _pulseaudio doxygen documentation: https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli: https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl.egg-info/SOURCES.txt new/pulsectl-18.12.5/pulsectl.egg-info/SOURCES.txt
--- old/pulsectl-17.12.2/pulsectl.egg-info/SOURCES.txt 2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-18.12.5/pulsectl.egg-info/SOURCES.txt 2018-12-18 23:49:03.000000000 +0100
@@ -6,6 +6,7 @@
setup.py
pulsectl/__init__.py
pulsectl/_pulsectl.py
+pulsectl/lookup.py
pulsectl/pulsectl.py
pulsectl.egg-info/PKG-INFO
pulsectl.egg-info/SOURCES.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pulsectl-17.12.2/setup.py new/pulsectl-18.12.5/setup.py
--- old/pulsectl-17.12.2/setup.py 2017-12-14 21:29:52.000000000 +0100
+++ new/pulsectl-18.12.5/setup.py 2018-12-18 23:45:14.000000000 +0100
@@ -13,7 +13,7 @@
setup(
name = 'pulsectl',
- version = '17.12.2',
+ version = '18.12.5',
author = 'George Filipkin, Mike Kazantsev',
author_email = 'mk.fraggod@gmail.com',
license = 'MIT',