Hello community,

here is the log from the commit of package python-pulsectl for 
openSUSE:Leap:15.2 checked in at 2020-03-09 18:10:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Leap:15.2/python-pulsectl (Old)
 and      /work/SRC/openSUSE:Leap:15.2/.python-pulsectl.new.26092 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pulsectl"

Mon Mar  9 18:10:46 2020 rev:11 rq:776941 version:20.1.2

Changes:
--------
--- /work/SRC/openSUSE:Leap:15.2/python-pulsectl/python-pulsectl.changes        
2020-01-15 15:51:43.419538762 +0100
+++ 
/work/SRC/openSUSE:Leap:15.2/.python-pulsectl.new.26092/python-pulsectl.changes 
    2020-03-09 18:10:47.173146735 +0100
@@ -1,0 +2,22 @@
+Mon Jan 20 13:59:09 UTC 2020 - Ondřej Súkup <[email protected]>
+
+- update to 20.1.2
+ * add pulse.play_sample() - server-side stored sample playback
+ * Add pulse.get_peak_sample() func for getting volume peak within timespan
+
+-------------------------------------------------------------------
+Wed Jun  5 08:09:07 UTC 2019 - Marketa Calabkova <[email protected]>
+
+- 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
+
+-------------------------------------------------------------------
+Tue Dec  4 12:51:49 UTC 2018 - Matej Cepl <[email protected]>
+
+- Remove superfluous devel dependency for noarch package
+
+-------------------------------------------------------------------

Old:
----
  pulsectl-17.12.2.tar.gz

New:
----
  pulsectl-20.1.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pulsectl.spec ++++++
--- /var/tmp/diff_new_pack.j5jglk/_old  2020-03-09 18:10:47.533147251 +0100
+++ /var/tmp/diff_new_pack.j5jglk/_new  2020-03-09 18:10:47.537147257 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pulsectl
 #
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -12,32 +12,31 @@
 # license that conforms to the Open Source Definition (Version 1.9)
 # published by the Open Source Initiative.
 
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
+#
 
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %bcond_without test
 Name:           python-pulsectl
-Version:        17.12.2
+Version:        20.1.2 
 Release:        0
-License:        MIT
 Summary:        Python high-level interface and ctypes-based bindings for 
PulseAudio (libpulse)
-Url:            http://github.com/mk-fg/python-pulse-control
+License:        MIT
 Group:          Development/Languages/Python
+URL:            http://github.com/mk-fg/python-pulse-control
 Source:         
https://files.pythonhosted.org/packages/source/p/pulsectl/pulsectl-%{version}.tar.gz
-BuildRequires:  python-rpm-macros
-BuildRequires:  %{python_module devel}
 BuildRequires:  %{python_module setuptools}
+BuildRequires:  fdupes
+BuildRequires:  python-rpm-macros
+Requires:       pulseaudio >= 5.0
+Requires:       python-setuptools
+BuildArch:      noarch
 %if %{with test}
+BuildRequires:  libpulse-devel >= 5.0
 BuildRequires:  pulseaudio
 BuildRequires:  pulseaudio-utils
-BuildRequires:  libpulse-devel >= 5.0
 %endif
-BuildRequires:  fdupes
-Requires:       python-setuptools
-Requires:       pulseaudio >= 5.0
-BuildArch:      noarch
-
 %python_subpackages
 
 %description
@@ -58,12 +57,12 @@
 
 %if %{with test}
 %check
-%python_exec setup.py test
+%python_exec -m unittest pulsectl.tests.all
 %endif
 
 %files %{python_files}
-%defattr(-,root,root,-)
-%doc CHANGES.rst COPYING README.rst
+%license COPYING
+%doc CHANGES.rst README.rst
 %{python_sitelib}/*
 
 %changelog

++++++ pulsectl-17.12.2.tar.gz -> pulsectl-20.1.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/CHANGES.rst 
new/pulsectl-20.1.2/CHANGES.rst
--- old/pulsectl-17.12.2/CHANGES.rst    2017-12-14 21:32:25.000000000 +0100
+++ new/pulsectl-20.1.2/CHANGES.rst     2020-01-11 17:20:39.000000000 +0100
@@ -8,13 +8,22 @@
 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: 20.1.2
 
 ---------------------------------------------------------------------------
 
-- 17.12.2: Use pa_card_profile_info2 / profiles2 introspection API.
+- 20.1.1: Add pulse.play_sample() - server-side stored sample playback [#36].
+
+  Loading is not implemented, would suggest something like libcanberra for 
that.
+
+- 19.9.1: Add pulse.get_peak_sample() func for getting volume peak within 
timespan [#33].
+
+- 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 [#19].
 
   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-20.1.2/PKG-INFO
--- old/pulsectl-17.12.2/PKG-INFO       2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/PKG-INFO        2020-01-11 17:24:10.035188400 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pulsectl
-Version: 17.12.2
+Version: 20.1.2
 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
         
         
@@ -318,11 +323,14 @@
         issues with pulseaudio (or its daemon.conf) and underlying 
dependencies.
         There are no "expected" test case failures.
         
+        All tests can run for up to 10 seconds currently (v19.9.6), due to some
+        involving playback (using paplay from /dev/urandom) being 
time-sensitive.
+        
         
         Changelog and versioning scheme
         ```````````````````````````````
         
-        This package uses one-version-per commit scheme (updated by pre-commit 
hook)
+        This package uses one-version-per-commit scheme (updated by pre-commit 
hook)
         and pretty much one release per git commit, unless more immediate 
follow-up
         commits are planned or too lazy to run ``py setup.py sdist bdist_wheel 
upload``
         for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/README.rst 
new/pulsectl-20.1.2/README.rst
--- old/pulsectl-17.12.2/README.rst     2017-10-26 12:33:28.000000000 +0200
+++ new/pulsectl-20.1.2/README.rst      2019-09-24 01:03:43.000000000 +0200
@@ -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
 
 
@@ -310,11 +315,14 @@
 issues with pulseaudio (or its daemon.conf) and underlying dependencies.
 There are no "expected" test case failures.
 
+All tests can run for up to 10 seconds currently (v19.9.6), due to some
+involving playback (using paplay from /dev/urandom) being time-sensitive.
+
 
 Changelog and versioning scheme
 ```````````````````````````````
 
-This package uses one-version-per commit scheme (updated by pre-commit hook)
+This package uses one-version-per-commit scheme (updated by pre-commit hook)
 and pretty much one release per git commit, unless more immediate follow-up
 commits are planned or too lazy to run ``py setup.py sdist bdist_wheel upload``
 for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/__init__.py 
new/pulsectl-20.1.2/pulsectl/__init__.py
--- old/pulsectl-17.12.2/pulsectl/__init__.py   2017-07-13 12:23:26.000000000 
+0200
+++ new/pulsectl-20.1.2/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-20.1.2/pulsectl/_pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/_pulsectl.py  2017-12-14 21:29:35.000000000 
+0100
+++ new/pulsectl-20.1.2/pulsectl/_pulsectl.py   2020-01-11 17:06:48.000000000 
+0100
@@ -4,7 +4,7 @@
 
 # C Bindings
 
-import os, sys, functools as ft
+import os, sys, ctypes.util, functools as ft
 from ctypes import *
 
 
@@ -95,6 +95,13 @@
 PA_SUBSCRIPTION_EVENT_REMOVE = 0x0020
 PA_SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030
 
+PA_SAMPLE_FLOAT32BE = 5
+
+PA_STREAM_DONT_MOVE = 0x0200
+PA_STREAM_PEAK_DETECT = 0x0800
+PA_STREAM_ADJUST_LATENCY = 0x2000
+PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND = 0x8000
+
 def c_enum_map(**values):
        return dict((v, force_str(k)) for k,v in values.items())
 
@@ -264,6 +271,7 @@
                ('name', c_char_p),
                ('owner_module', c_uint32),
                ('driver', c_char_p),
+               ('proplist', POINTER(PA_PROPLIST)),
        ]
 
 class PA_SERVER_INFO(Structure):
@@ -339,6 +347,15 @@
                ('mute', c_int),
        ]
 
+class PA_BUFFER_ATTR(Structure):
+       _fields_ = [
+               ('maxlength', c_uint32),
+               ('tlength', c_uint32),
+               ('prebuf', c_uint32),
+               ('minreq', c_uint32),
+               ('fragsize', c_uint32),
+       ]
+
 
 class POLLFD(Structure):
        _fields_ = [
@@ -442,6 +459,15 @@
        c_int,
        c_void_p)
 
+PA_STREAM_REQUEST_CB_T = CFUNCTYPE(c_void_p,
+       POINTER(PA_STREAM),
+       c_int,
+       c_void_p)
+
+PA_STREAM_NOTIFY_CB_T = CFUNCTYPE(c_void_p,
+       POINTER(PA_STREAM),
+       c_void_p)
+
 
 class LibPulse(object):
 
@@ -473,6 +499,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',
@@ -535,7 +562,7 @@
                        [POINTER(PA_CONTEXT), c_uint32, PA_CLIENT_INFO_CB_T, 
c_void_p] ),
                pa_context_get_server_info=( 'pa_op',
                        [POINTER(PA_CONTEXT), PA_SERVER_INFO_CB_T, c_void_p] ),
-               pa_operation_unref=([POINTER(PA_OPERATION)], c_int),
+               pa_operation_unref=[POINTER(PA_OPERATION)],
                pa_context_get_card_info_by_index=( 'pa_op',
                        [POINTER(PA_CONTEXT), c_uint32, PA_CARD_INFO_CB_T, 
c_void_p] ),
                pa_context_get_card_info_list=( 'pa_op',
@@ -570,13 +597,33 @@
                        [POINTER(PA_CHANNEL_MAP)], (POINTER(PA_CHANNEL_MAP), 
'not_null') ),
                pa_channel_map_snprint=([c_str_p, c_int, 
POINTER(PA_CHANNEL_MAP)], c_str_p),
                pa_channel_map_parse=(
-                       [POINTER(PA_CHANNEL_MAP), c_str_p], 
(POINTER(PA_CHANNEL_MAP), 'not_null') ) )
+                       [POINTER(PA_CHANNEL_MAP), c_str_p], 
(POINTER(PA_CHANNEL_MAP), 'not_null') ),
+               pa_proplist_from_string=([c_str_p], POINTER(PA_PROPLIST)),
+               pa_proplist_free=[POINTER(PA_PROPLIST)],
+               pa_stream_new_with_proplist=(
+                       [ POINTER(PA_CONTEXT), c_str_p,
+                               POINTER(PA_SAMPLE_SPEC), 
POINTER(PA_CHANNEL_MAP), POINTER(PA_PROPLIST) ],
+                       POINTER(PA_STREAM) ),
+               pa_stream_set_monitor_stream=([POINTER(PA_STREAM), c_uint32], 
'int_check_ge0'),
+               pa_stream_set_read_callback=[POINTER(PA_STREAM), 
PA_STREAM_REQUEST_CB_T, c_void_p],
+               pa_stream_connect_record=(
+                       [POINTER(PA_STREAM), c_str_p, POINTER(PA_BUFFER_ATTR), 
c_int], 'int_check_ge0' ),
+               pa_stream_unref=[POINTER(PA_STREAM)],
+               pa_stream_peek=(
+                       [POINTER(PA_STREAM), POINTER(c_void_p), 
POINTER(c_int)], 'int_check_ge0' ),
+               pa_stream_drop=([POINTER(PA_STREAM)], 'int_check_ge0'),
+               pa_stream_disconnect=([POINTER(PA_STREAM)], 'int_check_ge0'),
+               pa_context_play_sample=( 'pa_op',
+                       [POINTER(PA_CONTEXT), c_str_p, c_str_p, c_uint32, 
PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
+               pa_context_play_sample_with_proplist=( 'pa_op',
+                       [ POINTER(PA_CONTEXT), c_str_p, c_str_p, c_uint32,
+                               POINTER(PA_PROPLIST), PA_CONTEXT_SUCCESS_CB_T, 
c_void_p ] ) )
 
        class CallError(Exception): pass
 
 
        def __init__(self):
-               p = CDLL('libpulse.so.0')
+               p = CDLL(ctypes.util.find_library('libpulse') or 
'libpulse.so.0')
 
                self.funcs = dict()
                for k, spec in self.func_defs.items():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/lookup.py 
new/pulsectl-20.1.2/pulsectl/lookup.py
--- old/pulsectl-17.12.2/pulsectl/lookup.py     1970-01-01 01:00:00.000000000 
+0100
+++ new/pulsectl-20.1.2/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-20.1.2/pulsectl/pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/pulsectl.py   2017-12-14 21:27:45.000000000 
+0100
+++ new/pulsectl-20.1.2/pulsectl/pulsectl.py    2020-01-11 17:23:05.000000000 
+0100
@@ -31,6 +31,10 @@
                raise TypeError( 'Pulse<something>Info'
                        ' object is required instead of value: [{}] {}', 
type(obj), obj )
 
+class FakeLock():
+       def __enter__(self): return self
+       def __exit__(self, *err): pass
+
 
 @ft.total_ordering
 class EnumValue(object):
@@ -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)
 
@@ -473,7 +488,7 @@
                        ts = c.mono_time()
                        ts_deadline = timeout and (ts + timeout)
                        while True:
-                               delay = max(0, int((ts_deadline - ts) * 
1000000)) if ts_deadline else -1
+                               delay = max(0, int((ts_deadline - ts) * 1000)) 
if ts_deadline else -1
                                c.pa.mainloop_prepare(loop, delay) # usec
                                c.pa.mainloop_poll(loop)
                                if self._loop_closed: break # interrupted by 
close() or such
@@ -484,6 +499,14 @@
 
 
        def _pulse_info_cb(self, info_cls, data_list, done_cb, ctx, info, eof, 
userdata):
+               # No idea where callbacks with "userdata != NULL" come from,
+               #  but "info" pointer in them is always invalid, so they are 
discarded here.
+               # Looks like some kind of mixup or corruption in libpulse 
memory?
+               # See also: 
https://github.com/mk-fg/python-pulse-control/issues/35
+               if userdata is not None: return
+               # Empty result list and conn issues are checked elsewhere.
+               # Errors here are non-descriptive (errno), so should not be 
useful anyway.
+               # if eof < 0: done_cb(s=False)
                if eof: done_cb()
                else: data_list.append(info_cls(info[0]))
 
@@ -494,7 +517,9 @@
                                cb = cb_t(
                                        ft.partial(self._pulse_info_cb, 
info_cls, data, cb) if not singleton else
                                        lambda ctx, info, userdata, cb=cb: 
data.append(info_cls(info[0])) or cb() )
-                               pulse_func(self._ctx, *([index, cb, None] if 
index is not None else [cb, None]))
+                               pa_op = pulse_func( self._ctx,
+                                       *([index, cb, None] if index is not 
None else [cb, None]) )
+                       c.pa.operation_unref(pa_op)
                        data = data or list()
                        if index is not None or singleton:
                                if not data: raise PulseIndexError(index)
@@ -519,7 +544,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(
@@ -800,6 +825,76 @@
                c.pa.mainloop_set_poll_func(self._loop, self._pa_poll_cb, None)
 
 
+       def get_peak_sample(self, source, timeout, stream_idx=None):
+               '''Returns peak (max) value in 0-1.0 range for samples in 
source/stream within timespan.
+                       "source" can be either int index of pulseaudio source
+                               (i.e. source.index), its name (source.name), or 
None to use default source.
+                       Resulting value is what pulseaudio returns as
+                               PA_SAMPLE_FLOAT32BE float after "timeout" 
seconds.
+                       If specified source does not exist, 0 should be 
returned after timeout.
+                       This can be used to detect if there's any sound
+                               on the microphone or any sound played through a 
sink via its monitor_source index,
+                               or same for any specific stream connected to 
these (if "stream_idx" is passed).
+                       Sample stream masquerades as
+                               application.id=org.PulseAudio.pavucontrol to 
avoid being listed in various mixer apps.
+                       Example - get peak for specific sink input "si" for 0.8 
seconds:
+                               
pulse.get_peak_sample(pulse.sink_info(si.sink).monitor_source, 0.8, si.index)'''
+               samples, proplist = [0], 
c.pa.proplist_from_string('application.id=org.PulseAudio.pavucontrol')
+               ss = c.PA_SAMPLE_SPEC(format=c.PA_SAMPLE_FLOAT32BE, rate=25, 
channels=1)
+               s = c.pa.stream_new_with_proplist(self._ctx, 'peak detect', 
c.byref(ss), None, proplist)
+               c.pa.proplist_free(proplist)
+
+               @c.PA_STREAM_REQUEST_CB_T
+               def read_cb(s, bs, userdata):
+                       buff, bs = c.c_void_p(), c.c_int(bs)
+                       c.pa.stream_peek(s, buff, c.byref(bs))
+                       try:
+                               if not buff or bs.value < 4: return
+                               # This assumes that native byte order for 
floats is BE, same as pavucontrol
+                               samples[0] = max(samples[0], c.cast(buff, 
c.POINTER(c.c_float))[0])
+                       finally:
+                               # stream_drop() flushes buffered data (incl. 
buff=NULL "hole" data)
+                               # stream.h: "should not be called if the buffer 
is empty"
+                               if bs.value: c.pa.stream_drop(s)
+
+               if stream_idx is not None: c.pa.stream_set_monitor_stream(s, 
stream_idx)
+               c.pa.stream_set_read_callback(s, read_cb, None)
+               if source is not None: source = unicode(source).encode('utf-8')
+               try:
+                       c.pa.stream_connect_record( s, source,
+                               c.PA_BUFFER_ATTR(fragsize=4, maxlength=2**32-1),
+                               c.PA_STREAM_DONT_MOVE | c.PA_STREAM_PEAK_DETECT 
|
+                                       c.PA_STREAM_ADJUST_LATENCY | 
c.PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND )
+               except c.pa.CallError:
+                       c.pa.stream_unref(s)
+                       raise
+
+               try: self._pulse_poll(timeout)
+               finally:
+                       try: c.pa.stream_disconnect(s)
+                       except c.pa.CallError: pass # stream was removed
+
+               return min(1.0, samples[0])
+
+       def play_sample(self, name, sink=None, volume=1.0, proplist_str=None):
+               '''Play specified sound sample,
+                               with an optional sink object/name/index, volume 
and proplist string parameters.
+                       Sample must be stored on the server in advance, see 
e.g. "pacmd list-samples".
+                       See also libcanberra for an easy XDG theme sample 
loading, storage and playback API.'''
+               if isinstance(sink, PulseSinkInfo): sink = sink.index
+               sink = str(sink) if sink is not None else None
+               proplist = c.pa.proplist_from_string(proplist_str) if 
proplist_str else None
+               volume = int(round(volume*c.PA_VOLUME_NORM))
+               with self._pulse_op_cb() as cb:
+                       try:
+                               if not proplist:
+                                       c.pa.context_play_sample(self._ctx, 
name, sink, volume, cb, None)
+                               else:
+                                       c.pa.context_play_sample_with_proplist(
+                                               self._ctx, name, sink, volume, 
proplist, cb, None )
+                       except c.pa.CallError as err: raise 
PulseOperationInvalid(err.args[-1])
+
+
 def connect_to_cli(server=None, as_file=True, socket_timeout=1.0, attempts=5, 
retry_delay=0.3):
        '''Returns connected CLI interface socket (as file object, unless 
as_file=False),
                        where one can send same commands (as lines) as to 
"pacmd" tool
@@ -850,7 +945,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-20.1.2/pulsectl/tests/all.py
--- old/pulsectl-17.12.2/pulsectl/tests/all.py  2017-07-13 12:23:26.000000000 
+0200
+++ new/pulsectl-20.1.2/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-20.1.2/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-20.1.2/pulsectl/tests/dummy_instance.py        2020-01-11 
16:52:54.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,16 @@
                        
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(), pulse.sink_list()
+                       src_nx, sink_nx = max(s.index for s in src)+1, 
max(s.index for s in sink)+1
+                       src, sink = src[0], sink[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)
+                       with self.assertRaises(pulsectl.PulseIndexError): 
pulse.source_info(src_nx)
+                       with self.assertRaises(pulsectl.PulseIndexError): 
pulse.sink_info(sink_nx)
+
        def test_module_funcs(self):
                with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
                        self.assertEqual(len(pulse.sink_list()), 2)
@@ -323,6 +375,8 @@
                                if paplay.poll() is None: paplay.kill()
                                paplay.wait()
 
+                       with self.assertRaises(pulsectl.PulseIndexError): 
pulse.sink_input_info(stream.index)
+
        def test_ext_stream_restore(self):
                sr_name1 = 'sink-input-by-application-name:pulsectl-test-1'
                sr_name2 = 'sink-input-by-application-name:pulsectl-test-2'
@@ -376,5 +430,141 @@
                        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()
+
+       def test_get_peak_sample(self):
+               # Note: this test takes at least multiple seconds to run
+               with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+                       source_any = max(s.index for s in pulse.source_list())
+                       source_nx = source_any + 1
+
+                       time.sleep(0.3) # make sure previous streams die
+                       peak = pulse.get_peak_sample(source_any, 0.3)
+                       self.assertEqual(peak, 0)
+
+                       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/urandom'], 
env=dict(XDG_RUNTIME_DIR=self.tmp_dir) )
+                       try:
+                               if not stream_started: pulse.event_listen()
+                               stream_idx, = stream_started
+                               si = pulse.sink_input_info(stream_idx)
+                               sink = pulse.sink_info(si.sink)
+                               source = pulse.source_info(sink.monitor_source)
+
+                               # First poll can randomly fail if too short, 
probably due to latency or such
+                               peak = 
pulse.get_peak_sample(sink.monitor_source, 3)
+                               self.assertGreater(peak, 0)
+
+                               peak = pulse.get_peak_sample(source.index, 0.3, 
si.index)
+                               self.assertGreater(peak, 0)
+                               peak = pulse.get_peak_sample(source.name, 0.3, 
si.index)
+                               self.assertGreater(peak, 0)
+                               peak = pulse.get_peak_sample(source_nx, 0.3)
+                               self.assertEqual(peak, 0)
+
+                               paplay.terminate()
+                               paplay.wait()
+
+                               peak = pulse.get_peak_sample(source.index, 0.3, 
si.index)
+                               self.assertEqual(peak, 0)
+
+                       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-20.1.2/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-20.1.2/pulsectl.egg-info/PKG-INFO      2020-01-11 
17:24:09.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pulsectl
-Version: 17.12.2
+Version: 20.1.2
 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
         
         
@@ -318,11 +323,14 @@
         issues with pulseaudio (or its daemon.conf) and underlying 
dependencies.
         There are no "expected" test case failures.
         
+        All tests can run for up to 10 seconds currently (v19.9.6), due to some
+        involving playback (using paplay from /dev/urandom) being 
time-sensitive.
+        
         
         Changelog and versioning scheme
         ```````````````````````````````
         
-        This package uses one-version-per commit scheme (updated by pre-commit 
hook)
+        This package uses one-version-per-commit scheme (updated by pre-commit 
hook)
         and pretty much one release per git commit, unless more immediate 
follow-up
         commits are planned or too lazy to run ``py setup.py sdist bdist_wheel 
upload``
         for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl.egg-info/SOURCES.txt 
new/pulsectl-20.1.2/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-20.1.2/pulsectl.egg-info/SOURCES.txt   2020-01-11 
17:24:09.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.cfg 
new/pulsectl-20.1.2/setup.cfg
--- old/pulsectl-17.12.2/setup.cfg      2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/setup.cfg       2020-01-11 17:24:10.037188500 +0100
@@ -4,5 +4,4 @@
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pulsectl-17.12.2/setup.py 
new/pulsectl-20.1.2/setup.py
--- old/pulsectl-17.12.2/setup.py       2017-12-14 21:29:52.000000000 +0100
+++ new/pulsectl-20.1.2/setup.py        2020-01-11 17:22:41.000000000 +0100
@@ -13,7 +13,7 @@
 setup(
 
        name = 'pulsectl',
-       version = '17.12.2',
+       version = '20.1.2',
        author = 'George Filipkin, Mike Kazantsev',
        author_email = '[email protected]',
        license = 'MIT',


Reply via email to