From 941c024b2a385671e1b1da2ce1d097421b3b86dd Mon Sep 17 00:00:00 2001 From: thanks4opensource <31165662+thanks4opensource@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:09:16 -0700 Subject: [PATCH 1/4] client: add get_address() and fd() Parses various input overloads such as 'client-name:port_name', ('client-name','port-name), etc., returns Address. Raises exception if Address(client_id, port_id) doesn't exist. Added new fd() public accessor. Added tests/test_sequencer_address.py. Modified examples/connect.py to accept 'client-name:port-name'. --- alsa_midi/client.py | 183 +++++++++++- examples/connect.py | 11 +- tests/test_sequencer_address.py | 485 ++++++++++++++++++++++++++++++++ 3 files changed, 674 insertions(+), 5 deletions(-) create mode 100644 tests/test_sequencer_address.py diff --git a/alsa_midi/client.py b/alsa_midi/client.py index 030559d..045ff8b 100644 --- a/alsa_midi/client.py +++ b/alsa_midi/client.py @@ -419,6 +419,10 @@ def _get_fds(self): assert (pfds[0].events & select.POLLIN) and (pfds[0].events & select.POLLOUT) self._fd = pfds[0].fd + def fd(self): + "Returns the ALSA sequencer file descriptor number." + return self._fd + def _get_event_parser(self): parser = self._event_parser if parser is None: @@ -1120,7 +1124,7 @@ def list_ports(self, *, The result is sorted in a way that the first returned entry should be the 'most usable' one for the selected purpose. E.g. when `output` = `True` then the first entry will be - a synthesizer input port rather than the dummy 'Midi Through' port. This is still a guess, + a synthesizer input port rather than the dummy 'Midi Through' port. Thisa is still a guess, though, so in the end the user should be able to choose. Wraps :alsa:`snd_seq_query_next_client` and :alsa:`snd_seq_query_next_port`. @@ -1232,6 +1236,183 @@ def list_ports(self, *, return result + + @overload + def get_address(self, arg1: str) -> 'Address': + ... + + @overload + def get_address(self, arg1: str, arg2: str = '') -> 'Address': + ... + + @overload + def get_address(self, arg1: int, arg2: int = 0) -> 'Address': + ... + + @overload + def get_address(self, arg1: tuple) -> 'Address': + ... + + @overload + def get_address(self, arg1: AddressType) -> 'Address': + ... + + + def get_address(self, + arg1: Union[int, str, tuple, AddressType], + arg2: Union[int, str, None] = None) -> Address: + """Parse client:port address string + + Similar to Address() but parses client and port by names + in addition numeric to ids. + + Raises exception if client:port does not exist. + + Note finds addresses in clients other than self (as per + ALSA semantics). + + As per alsa-util library's snd_seq_parse_address(): + - Accepts "client:port" and "client.port" interchangeably + - Returns client_id:0 if found for input without ":" or '.' + - Chooses last of possible multiple prefix-matches (if any) with + existing client names unless superceded by full exact match. + - Will use first of possible multiple exact matching client + names (yes, ALSA allows this). + - Likewise chooses last of possible multiple prefix-match + port names in chosen client unless superceded by exact + match, or first of possible multiple exact matches (ALSA + allows this, too, even if all are of the same READ, WRITE, + READ+WRITE gender). + - Does nothing fancy such as searching for the best port name + match within multiple candidate client matches. + - Raises TypeError for incorrect arguments, FileNotFoundError + if specified client:port doesn't exist. + + :param spec: address specification, 'client', 'client:port', 'client.port', etc. + :return: Address + + :exceptions: FileNotFoundError, TypeError + """ + + def type_error(): + raise TypeError( "(%s, %s) is not one or two ints and/or " + "strings, or 2-tuple of same, or Address" + % (arg1, arg2)) + + if arg2 is None: + if isinstance(arg1, str): + # remove enclosing single or double quotes if any + if arg1.startswith("'"): + arg1 = arg1.strip("'") + elif arg1.startswith('"'): + arg1 = arg1.strip('"') + # normalize "client.port" to "client:port" + if '.' in arg1: + arg1 = arg1.replace('.', ':') + # split client and port + client_port = arg1.split(':') + if len(client_port) == 2: + (client_name,port_name) = client_port + else: + client_name = arg1 + port_name = None + elif isinstance(arg1, int): + client_name = arg1 + port_name = None + elif isinstance(arg1, tuple): + if arg2 is not None: + type_error() + if len(arg1) == 2: + (client_name,port_name) = arg1 + else: + type_error() + elif isinstance(arg1, Address): + if arg2 is not None: + type_error() + client_name = arg2.client_id # int will be converted below + port_name = arg2.port_id # " " " " " + else: + type_error() + else: + client_name = arg1 + port_name = arg2 + + + if not ( isinstance(client_name, (int,str )) + and isinstance(port_name, (int,str,type(None)))): + type_error() + + ports = self.list_ports(include_system=True) + + # confirm client exists + client_id = None + if isinstance(client_name, int): + client_id = client_name + elif isinstance(client_name, str) and client_name.isdecimal(): + client_id = int(client_name) + else: + client_id = None + + best_client = None + if client_id is None: + for port_info in ports: + if port_info.client_name == client_name: + # exact match supercedes prefix matches + # but note can have multiple clients with same name + client_id = port_info.client_id + best_client = port_info + break + elif port_info.client_name.startswith(client_name): + # last of multiples will overwrite + client_id = port_info.client_id + best_client = port_info + else: + for port_info in ports: + if port_info.client_id == client_id: + best_client = port_info + break # can't have multiple clients with same numeric id + + # only return client if exists + if client_id is None: + raise FileNotFoundError("No ALSA client '%s'" % client_name) + elif best_client is None: + raise FileNotFoundError("No ALSA client %d" % client_id) + + # find specified port of specified client + if isinstance(port_name, int): + port_id = port_name + elif isinstance(port_name, str) and port_name.isdecimal(): + port_id = int(port_name) + elif port_name is None: + port_id = 0 # but still must check that exists + else: + port_id = None # find by name + + best_port = None + for port_info in ports: + if port_info.client_id == best_client.client_id: + if port_info.port_id == port_id: + # port id match supercedes name/prefix match + best_port = port_info + break + elif port_info.name == port_name: + # exact match supercedes prefix matches + port_id = port_info.port_id + best_port = port_info + break + elif isinstance(port_name, str) \ + and port_name in port_info.name: + # last of multiples will overwrite + port_id = port_info.port_id + best_port = port_info + + if best_port is None: + raise FileNotFoundError( "No ALSA client:port %d:%s" + % (client_id, port_name) ) + else: + return Address(client_id, best_port.port_id) + + def _subunsub_port(self, func, sender: AddressType, dest: AddressType, *, queue: Optional[Union[Queue, int]] = None, diff --git a/examples/connect.py b/examples/connect.py index 1f52c63..01ef3b6 100755 --- a/examples/connect.py +++ b/examples/connect.py @@ -15,19 +15,22 @@ def main(): help="Use real-time-stamp") parser.add_argument("--tick", "-t", action="store_false", dest="real", help="Use tick-time-stamp") - parser.add_argument("sender", type=Address, metavar="CLIENT:PORT", + parser.add_argument("sender", metavar="CLIENT:PORT", help="Source side of the connection") - parser.add_argument("dest", type=Address, metavar="CLIENT:PORT", + parser.add_argument("dest", metavar="CLIENT:PORT", help="Sink side of the connection") args = parser.parse_args() client = SequencerClient("dump_events.py") + sender = client.get_address(args.sender) + dest = client.get_address(args.dest) + if args.disconnect: - client.unsubscribe_port(args.sender, args.dest) + client.unsubscribe_port(sender, dest) else: - client.subscribe_port(args.sender, args.dest, + client.subscribe_port(sender, dest, exclusive=args.exclusive, time_update=(args.real is not None), time_real=args.real) diff --git a/tests/test_sequencer_address.py b/tests/test_sequencer_address.py new file mode 100644 index 0000000..a38d047 --- /dev/null +++ b/tests/test_sequencer_address.py @@ -0,0 +1,485 @@ +import argparse +import sys + +from collections import namedtuple + +import alsa_midi +from alsa_midi import Address + + + +"""Tests SequencerClientBase.get_address() + +Tests many permutations of "numeric-or-named-client:numeric-or-named-port", +"client:port", "client.port", "'client:port'", (client, port), ((client, port)), +etc. + +Runs silently by default, returns 0 on all tests successful or raised +expected exception. Otherwise exits with uncaught exception. + +-v/--verbose option to output results and fail-with-expected-errors +-p/--pause to pause until killed (to use `aconnect -oil` to view test + client/ports + +Outputs test/pass/fail counts if either -v or -p +""" + + +test_count = 0 # total number of permutations tested +pass_count = 0 # successful tests +fail_count = 0 # test which failed with expected error + + +# clients/ports for tests +# + +# port name, requested port id, actual port id, test substrings, capabilities +port_spec = namedtuple('port_spec', "name req_id id substr caps") + +# mixture of capabilities (not used, but to make sure don't affect results) +INPUT_CAPS = alsa_midi.WRITE_PORT +OUTPUT_CAPS = alsa_midi.READ_PORT +IN_OUT_CAPS = INPUT_CAPS | OUTPUT_CAPS + +# all tests use this for client.get_address(...) +tester_ports = ( + port_spec("zero", -1, 0, False, IN_OUT_CAPS), +) + +# default sequentially-numbered ports +# tests port can be accessed by substring of name +sequential_ports = ( + port_spec("zero", -1, 0, False, INPUT_CAPS ), + port_spec("One", -1, 1, False, OUTPUT_CAPS ), + port_spec("two" , -1, 2, False, IN_OUT_CAPS ), + port_spec("three", -1, 3, True, OUTPUT_CAPS ), # test by substring + port_spec("FOUR" , -1, 4, False, INPUT_CAPS ), + port_spec("five" , -1, 5, False, IN_OUT_CAPS ), +) + +# find above ports by substrings of names +prefix_ports = ( + port_spec("er", -1, 0, False, IN_OUT_CAPS ), + port_spec("On", -1, 1, False, INPUT_CAPS ), + port_spec("wo", -1, 2, False, OUTPUT_CAPS ), +) + +# explicit non-default, non-sequential port numbers +# tests exact match of port name overrides substring match +explicit_ports = ( + port_spec("THREE", 3, 3, False, OUTPUT_CAPS ), + port_spec("six", 6, 6, False, INPUT_CAPS ), + port_spec("Eight", 8, 8, False, IN_OUT_CAPS ), + port_spec("ele", 12, 12, False, INPUT_CAPS ), # exact match + port_spec("lev", 13, 13, False, INPUT_CAPS ), # " " + port_spec("eleven", 11, 11, False, INPUT_CAPS ), + port_spec("eve", 14, 14, False, INPUT_CAPS ), # exact match + port_spec("ven" , 15, 15, False, INPUT_CAPS ), # " " + port_spec("twenTY", 20, 20, False, OUTPUT_CAPS ), +) + + + +# at program exit if -v/--verbose, -p/--pause, or uncaught exception +def print_stats(stream=sys.stdout): + global test_count, pass_count, fail_count + stream.write( "%d tests\n" + "%d passed\n" + "%d failed with expected errors \n" + % (test_count, + pass_count, + fail_count)) + + + +# create clients/ports ports from above tuples of port_specs +def create_ports(client, port_descs): + global clients_ports + + client_name = client.get_client_info().name + + for port_desc in port_descs: + if port_desc.req_id == -1: + port = client.create_port(name=port_desc.name, + caps=port_desc.caps) + else: + port = client.create_port(name=port_desc.name, + caps=port_desc.caps, + port_id=port_desc.req_id) + clients_ports.setdefault(client_name, []).append(port) + + + +# Parse address of various forms ('client:port', (client, port), +# ((client, port),), etc. checking for success or expected error +# exception. +def address_parse(*args): + # one or two address arguments, then expected client_id, port_id, + # and expected-error-or-None + + assert len(args) > 3, \ + "internal test program bug: address_parse(%s) <= 3\n" % (args,) + + expected_error = args[ -1] + port_id = args[ -2] + client_id = args[ -3] + address_args = args[:-3] + + global test_count, pass_count, fail_count + + # input of ((a,b),c,d,e) -> ((a,b),) not (a,b) + if len(address_args) == 1: # and isinstance(address_args[0], tuple): + address_args = address_args[0] + is_tuple = False # for arg_name() + else: + is_tuple = True # for arg_name() + + # format address argument(s) into more readable form + def arg_name(arg): + if hasattr(arg, 'client_id'): + return 'Address(%s)' % str(arg) + elif is_tuple: + return str(arg)[1:-1].replace(" ",'') # strip('()') strips all + elif isinstance(arg, str): + if arg.startswith("'"): + return '"%s"' % arg + else: + return "'%s'" % arg + else: + return str(arg).replace(" ", '') + + # test successful client.get_address() + if expected_error is None: + try: + addr = tester.get_address(address_args) + except Exception as e: + print_stats(sys.stderr) + sys.stderr.write( "%s raises unexpected %s\n" + % (arg_name(address_args), type(e).__name__)) + raise + else: + try: + addr = tester.get_address(address_args) + except expected_error: + test_count += 1 + fail_count += 1 + if opts.verbose: + sys.stdout.write( "%s raises %s (as expected)\n" + % (arg_name(address_args), + expected_error.__name__)) + return; + except Exception as e: + print_stats(sys.stderr) + sys.stderr.write( "%s raises %s (not expected %s)\n" + % (arg_name(address_args), + type(e).__name__, + expected_error.__name__)) + raise + else: + print_stats(sys.stderr) + sys.stdout.write( "%s -> %d:%d without raising " + "expected error %s\n" + % (arg_name(address_args), + addr.client_id, + addr.port_id, + expected_error.__name__)) + raise AssertionError + + test_count += 1 + pass_count += 1 + + if addr.client_id != client_id or addr.port_id != port_id: + raise FileNotFoundError( "%s -> %s, not expected (%d,%d)" + % (arg_name(address_args), + addr, + client_id, + port_id ) ) + + if opts.verbose: + sys.stdout.write( "%s -> %d:%d" + % (arg_name(address_args), + addr.client_id, + addr.port_id)) + if expected_error is not None: + sys.stdout.write(" with expected exception: %s\n" % expect_except) + else: + sys.stdout.write('\n') + + return addr + + +# Tests permutations of "numeric-or-named-client:numeric-or-named-port", +# "client:port", "client.port", "'client:port'", (client, port), +# ((client, port)), etc. +def test_variants(client, + port, + separator, + quotes, + client_id, + port_id, + expected_error, + has_port_0=True): + + # just client + just_client_error = None if has_port_0 else FileNotFoundError + spec = "%s%s%s" % (quotes, client, quotes) + address_parse(spec, client_id, 0, just_client_error) + address_parse(client, client_id, 0, just_client_error) + + # client and port + spec = "%s%s%s%s%s" % (quotes, client, separator, port, quotes) + address_parse( spec, client_id, port_id, expected_error) + address_parse( client, port, client_id, port_id, expected_error) + address_parse((client, port), client_id, port_id, expected_error) + address_parse( client_id, port, client_id, port_id, expected_error) + address_parse((client_id, port), client_id, port_id, expected_error) + address_parse( client, port_id, client_id, port_id, expected_error) + address_parse((client, port_id), client_id, port_id, expected_error) + address_parse( client_id, port_id, client_id, port_id, expected_error) + address_parse((client_id, port_id), client_id, port_id, expected_error) + + if isinstance(client_id, int) and isinstance(port, int) : + address_parse(Address(client_id, port_id), + client_id, + port_id, + expected_error) + + + +# test all ports of client +def test_addresses(client_name, client_id, ports): + if opts.verbose: + sys.stdout.write( "# --- test_addresses(%s, %s) ---\n" + % (client_name, client_id)) + + # Not a rigorous test (ports could be explicit with first + # one not zero) but works for all test cases here where + # most are implicitly-ordered (so zero is first), and the + # one that isn't has no port 0 at all much less not + # as first port. + has_port_0 = (ports[0].id == 0) + + # permutations of address specifications + for tests_bits in range(1<<4): + if (tests_bits & 1): separator = ':' + else: separator = '.' + + quotes = ("", "'", '"', None)[(tests_bits >> 1) & 0x3] + if quotes is None: continue + + no_ports = not(tests_bits << 3) + + for port in ports: + test_variants(client_name, + port.name, + separator, + quotes, + client_id, + port.id, + None, + has_port_0) + + # test that flagged ports can be accessed via substring + for (port_ndx,port) in enumerate(ports): + if port.substr: + # test all 3 character substrings of port name + for char_ndx in range(0, len(port.name) - 3): + substring = port.name[char_ndx:char_ndx+3] + test_variants(client_name, + substring, + separator, + quotes, + client_id, + port.id, + None, + has_port_0) + + + +# Test numeric client and port +# +# Note first client created by create_ports() (gets assigned i.d. 128) +# must have port id 0 (explicit or default assigned) +def test_simple(): + if opts.verbose: sys.stdout.write("# --- test_simple() ---\n") + + address_parse( 128, 128, 0, None) + address_parse( '128', 128, 0, None) + address_parse( 128, 0, 128, 0, None) + address_parse( '128:0', 128, 0, None) + address_parse( (128, 0), 128, 0, None) + address_parse( Address(128, 0), 128, 0, None) + address_parse((Address(128, 0),), 128, 0, TypeError) + address_parse( 128, Address(128, 0), 0, 0, TypeError) + address_parse((128, Address(128, 0)), 0, 0, TypeError) + + + +# Test of client.get_address() overloads +def test_overloads(client_name, client_id, port_name, port_id): + if opts.verbose: sys.stdout.write("# --- test_overloads() ---\n") + + overloads = [] + + def test(*args): + overloads.append(address_parse(*(args + (client_id, port_id, None)))) + + test( client_id , port_id ) # 2 ints + test((client_id , port_id )) # tuple, same + test( client_id , port_name) # int, str + test((client_id , port_name)) # tuple, same + test( client_name, port_id ) # str, int + test((client_name, port_id )) # tuple, same + test( client_name, port_name) # 2 str + test((client_name, port_name)) # tuple, same + + test('%d:%d' % (client_id , port_id )) # int:int + test('%d:%s' % (client_id , port_name)) # int:str + test('%s:%d' % (client_name, port_id )) # str:iin + test('%s:%s' % (client_name, port_name)) # str:str + + overloads.append(tester.get_address(overloads[0])) # address + + assert(all(addr == overloads[0] for addr in overloads)) + + + +def test_bad_arguments(): + if opts.verbose: sys.stdout.write("# --- test_bad_arguments() ---\n") + + address_parse(3.14, 0, 0, TypeError) + address_parse([], 0, 0, TypeError) + address_parse(tester, 0, 0, TypeError) + + address_parse((128,), 0, 0, TypeError) + address_parse(('explicit',), 0, 0, TypeError) + address_parse((3.14,), 0, 0, TypeError) + address_parse((Address(128,0),), 0, 0, TypeError) + + address_parse(('explicit', None), 0, 0, FileNotFoundError) + address_parse((3.14, None), 0, 0, TypeError) + address_parse((Address(128,0), None), 0, 0, TypeError) + + address_parse( 128, 2, 3, 0, 0, TypeError) + address_parse((128, 2, 3), 0, 0, TypeError) + address_parse( 128, 2, 3, 4, 0, 0, TypeError) + address_parse((128, 2, 3, 4), 0, 0, TypeError) + + address_parse( 3.14, 128, 0, 0, TypeError) + address_parse((3.14, 128), 0, 0, TypeError) + address_parse( 128, 3.14, 0, 0, TypeError) + address_parse((128, 3.14), 0, 0, TypeError) + + address_parse( 128, 'explicit', 0, 0, FileNotFoundError) + address_parse((128, 'explicit'), 0, 0, FileNotFoundError) + address_parse( 'explicit', 128, 0, 0, FileNotFoundError) + address_parse(('explicit', 128), 0, 0, FileNotFoundError) + + address_parse( 'explicit', 3.14, 0, 0, TypeError) + address_parse(('explicit', 3.14), 0, 0, TypeError) + address_parse( 3.14, 'explicit', 0, 0, TypeError) + address_parse((3.14, 'explicit'), 0, 0, TypeError) + + address_parse( Address(128, 0), 128, 0, 0, TypeError) + address_parse((Address(128, 0)), 128, 0, 0, TypeError) + + address_parse( 128, Address(128, 0), 0, 0, TypeError) + address_parse((128, Address(128, 0)), 0, 0, TypeError) + + + +def test_bad_clients_ports(): + if opts.verbose: sys.stdout.write("# --- test_bad_clients_ports() ---\n") + for client in (-1, 256, 'bad_client'): + for port in (0, -1, 256, 'bad_port'): + test_variants(client, + port, + ':', + '', + client, + port, + FileNotFoundError, + has_port_0=False) + + + +def test_good_client_bad_ports(client_name, client_id): + for port in (-1, 256, 'bad_port'): + for separator in (':', '.'): + for quotes in ('', "'", '"'): + spec = "%s%s%s%s%s" % (quotes, + client_name, + separator, + port, + quotes) + address_parse(spec, + client_name, + port, + client_id, + 0, + TypeError) + address_parse( client_name, port, client_id, 0, FileNotFoundError) + address_parse((client_name, port), client_id, 0, FileNotFoundError) + address_parse( client_id, port, client_id, 0, FileNotFoundError) + address_parse((client_id, port), client_id, 0, FileNotFoundError) + + + +if __name__ == '__main__': + tester = alsa_midi.SequencerClient("tester" ) # all tests use this + sequential = alsa_midi.SequencerClient("sequential") + prefix = alsa_midi.SequencerClient("seq" ) + explicit = alsa_midi.SequencerClient("explicit" ) + + # First one gets assigned client id 128, must have port + # with id 0 for test_simple() + test_id = tester .get_client_info().client_id + seqn_id = sequential.get_client_info().client_id + prfx_id = prefix .get_client_info().client_id + expl_id = explicit .get_client_info().client_id + + # commandline options + parser = argparse.ArgumentParser() + parser.add_argument('-v', '--verbose', + action="store_true", + help="Ouput results of each test (note: thousands)") + parser.add_argument('-p', '--pause', + action="store_true", + help="Pause after tests") + opts = parser.parse_args() + + clients_ports = {} # have to store ports somewhere else go out of scope + # and get deleted on return from create_ports() + create_ports(tester , tester_ports ) + create_ports(sequential, sequential_ports) + create_ports(prefix , prefix_ports ) + create_ports(explicit , explicit_ports ) + + test_simple() + test_bad_arguments() + test_overloads('explicit', expl_id, 'eleven', 11) # only need to do one + test_bad_clients_ports() + test_good_client_bad_ports('explicit', expl_id) # only need to do one + + test_addresses("tester", test_id, tester_ports) + test_addresses("sequential", seqn_id, sequential_ports) + test_addresses("seq", prfx_id, prefix_ports) # client,port substrings + test_addresses("explicit", expl_id, explicit_ports) + test_addresses("expl", expl_id, explicit_ports) # substrings + + if opts.verbose or opts.pause: + print_stats() + + if opts.pause: + sys.stdout.write("`aconnect -oil` in separate shell for " + "clients+ports, type ^C here to exit\n") + try: + while True: pass + except KeyboardInterrupt: + sys.stdout.write("exiting\n") + + explicit.close() + sequential.close() + tester.close() + + sys.exit(0) From 9784b975c68447c91c34baff449126254660476a Mon Sep 17 00:00:00 2001 From: thanks4opensource <31165662+thanks4opensource@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:44:02 -0700 Subject: [PATCH 2/4] utils/aconnect.py and associated client.py added features New aconnect.py, improved replacement for alsa-utils aconnect (aconnect.c). New SequencerClientBase.list_clients() and modified .list_ports() with include_kernel parameter. SequencerClientBase.fd property and .get_address() changed to .resolve_address(). Code formatting changes to pass flake8 checks. Added port.py PortDirection. Revert examples/connect.py to accept client/port by id only. Modified tests/test_sequencer_address() to run under pytest, and excluded from flake8. --- alsa_midi/_ffi_defs.py | 7 + alsa_midi/client.py | 110 ++++++--- alsa_midi/port.py | 17 +- examples/connect.py | 11 +- tests/test_port_query.py | 18 +- tests/test_sequencer_address.py | 174 +++++++------- tox.ini | 1 + utils/aconnect.py | 404 ++++++++++++++++++++++++++++++++ 8 files changed, 617 insertions(+), 125 deletions(-) create mode 100755 utils/aconnect.py diff --git a/alsa_midi/_ffi_defs.py b/alsa_midi/_ffi_defs.py index cde0598..9db4c38 100644 --- a/alsa_midi/_ffi_defs.py +++ b/alsa_midi/_ffi_defs.py @@ -563,6 +563,11 @@ #define SND_SEQ_PORT_SYSTEM_TIMER 0 #define SND_SEQ_PORT_SYSTEM_ANNOUNCE 1 +#define SND_SEQ_PORT_DIR_UNKNOWN 0 +#define SND_SEQ_PORT_DIR_INPUT 1 +#define SND_SEQ_PORT_DIR_OUTPUT 2 +#define SND_SEQ_PORT_DIR_BIDIRECTION 3 + #define SND_SEQ_PORT_CAP_READ 1 #define SND_SEQ_PORT_CAP_WRITE 2 @@ -600,6 +605,7 @@ int snd_seq_port_info_get_port(const snd_seq_port_info_t *info); const snd_seq_addr_t *snd_seq_port_info_get_addr(const snd_seq_port_info_t *info); const char *snd_seq_port_info_get_name(const snd_seq_port_info_t *info); +unsigned int snd_seq_port_info_get_direction(const snd_seq_port_info_t *info); unsigned int snd_seq_port_info_get_capability(const snd_seq_port_info_t *info); unsigned int snd_seq_port_info_get_type(const snd_seq_port_info_t *info); int snd_seq_port_info_get_midi_channels(const snd_seq_port_info_t *info); @@ -616,6 +622,7 @@ void snd_seq_port_info_set_port(snd_seq_port_info_t *info, int port); void snd_seq_port_info_set_addr(snd_seq_port_info_t *info, const snd_seq_addr_t *addr); void snd_seq_port_info_set_name(snd_seq_port_info_t *info, const char *name); +void snd_seq_port_info_set_direction(snd_seq_port_info_t *info, int direction); void snd_seq_port_info_set_capability(snd_seq_port_info_t *info, unsigned int capability); void snd_seq_port_info_set_type(snd_seq_port_info_t *info, unsigned int type); void snd_seq_port_info_set_midi_channels(snd_seq_port_info_t *info, int channels); diff --git a/alsa_midi/client.py b/alsa_midi/client.py index 045ff8b..68b025b 100644 --- a/alsa_midi/client.py +++ b/alsa_midi/client.py @@ -419,6 +419,7 @@ def _get_fds(self): assert (pfds[0].events & select.POLLIN) and (pfds[0].events & select.POLLOUT) self._fd = pfds[0].fd + @property def fd(self): "Returns the ALSA sequencer file descriptor number." return self._fd @@ -1108,11 +1109,58 @@ def query_next_port(self, result = PortInfo._from_alsa(info) return result + def list_clients(self, *, + include_system: bool = False, + include_kernel: bool = False, + include_midi_through: bool = True, + ) -> list[ClientInfo]: + """More friendly interface to list available clients.. + :param include_system: include system clients + :param include_midi_through: include 'midi through' ports + + :return: list of client information + """ + + result = [] + self._check_handle() + + client_ainfo_p = ffi.new("snd_seq_client_info_t **") + err = alsa.snd_seq_client_info_malloc(client_ainfo_p) + _check_alsa_error(err) + client_ainfo = ffi.gc(client_ainfo_p[0], alsa.snd_seq_client_info_free) + + alsa.snd_seq_client_info_set_client(client_ainfo, -1) + while True: + err = alsa.snd_seq_query_next_client(self.handle, client_ainfo) + if err == -errno.ENOENT: + break + _check_alsa_error(err) + + client_id = alsa.snd_seq_client_info_get_client(client_ainfo) + if client_id == 0 and not include_system: + continue + + client_name = alsa.snd_seq_client_info_get_name(client_ainfo) + client_name = ffi.string(client_name).decode() + + if client_name == "Midi Through" and not include_midi_through: + continue + + client_info = ClientInfo._from_alsa(client_ainfo) + + if client_info.type == ClientType.KERNEL and not include_kernel: + continue + + result.append(client_info) + + return result + def list_ports(self, *, input: bool = None, output: bool = None, type: PortType = PortType.MIDI_GENERIC, include_system: bool = False, + include_kernel: bool = False, include_midi_through: bool = True, include_no_export: bool = True, only_connectable: bool = True, @@ -1133,6 +1181,7 @@ def list_ports(self, *, :param output: return ports usable for event output (`PortCaps.WRITE`) :param type: limit ouput to ports of this type :param include_system: include system ports + :param include_kernel: include kernel ports :param include_midi_through: include 'midi through' ports :param include_no_export: include 'no export' ports :param only_connectable: only list ports that can be connected to/from @@ -1162,9 +1211,14 @@ def list_ports(self, *, _check_alsa_error(err) client_id = alsa.snd_seq_client_info_get_client(client_ainfo) + if client_id == 0 and not include_system: continue + client_type = alsa.snd_seq_client_info_get_type(client_ainfo) + if client_type & alsa.SND_SEQ_KERNEL_CLIENT and not include_kernel: + continue + client_name = alsa.snd_seq_client_info_get_name(client_ainfo) client_name = ffi.string(client_name).decode() @@ -1236,31 +1290,29 @@ def list_ports(self, *, return result - @overload - def get_address(self, arg1: str) -> 'Address': + def resolve_address(self, arg1: str) -> 'Address': ... @overload - def get_address(self, arg1: str, arg2: str = '') -> 'Address': + def resolve_address(self, arg1: str, arg2: str = '') -> 'Address': ... @overload - def get_address(self, arg1: int, arg2: int = 0) -> 'Address': + def resolve_address(self, arg1: int, arg2: int = 0) -> 'Address': ... @overload - def get_address(self, arg1: tuple) -> 'Address': + def resolve_address(self, arg1: tuple) -> 'Address': ... @overload - def get_address(self, arg1: AddressType) -> 'Address': + def resolve_address(self, arg1: AddressType) -> 'Address': ... - - def get_address(self, - arg1: Union[int, str, tuple, AddressType], - arg2: Union[int, str, None] = None) -> Address: + def resolve_address(self, + arg1: Union[int, str, tuple, AddressType], + arg2: Union[int, str, None] = None) -> Address: """Parse client:port address string Similar to Address() but parses client and port by names @@ -1295,8 +1347,8 @@ def get_address(self, """ def type_error(): - raise TypeError( "(%s, %s) is not one or two ints and/or " - "strings, or 2-tuple of same, or Address" + raise TypeError("(%s, %s) is not one or two ints and/or " + "strings, or 2-tuple of same, or Address" % (arg1, arg2)) if arg2 is None: @@ -1312,34 +1364,33 @@ def type_error(): # split client and port client_port = arg1.split(':') if len(client_port) == 2: - (client_name,port_name) = client_port + (client_name, port_name) = client_port else: client_name = arg1 - port_name = None + port_name = None elif isinstance(arg1, int): client_name = arg1 - port_name = None + port_name = None elif isinstance(arg1, tuple): if arg2 is not None: type_error() if len(arg1) == 2: - (client_name,port_name) = arg1 + (client_name, port_name) = arg1 else: type_error() elif isinstance(arg1, Address): if arg2 is not None: type_error() client_name = arg2.client_id # int will be converted below - port_name = arg2.port_id # " " " " " + port_name = arg2.port_id # same else: type_error() else: client_name = arg1 - port_name = arg2 + port_name = arg2 - - if not ( isinstance(client_name, (int,str )) - and isinstance(port_name, (int,str,type(None)))): + if not (isinstance(client_name, (int, str)) + and isinstance(port_name, (int, str, type(None)))): type_error() ports = self.list_ports(include_system=True) @@ -1359,12 +1410,12 @@ def type_error(): if port_info.client_name == client_name: # exact match supercedes prefix matches # but note can have multiple clients with same name - client_id = port_info.client_id + client_id = port_info.client_id best_client = port_info break elif port_info.client_name.startswith(client_name): # last of multiples will overwrite - client_id = port_info.client_id + client_id = port_info.client_id best_client = port_info else: for port_info in ports: @@ -1397,22 +1448,21 @@ def type_error(): break elif port_info.name == port_name: # exact match supercedes prefix matches - port_id = port_info.port_id + port_id = port_info.port_id best_port = port_info break - elif isinstance(port_name, str) \ - and port_name in port_info.name: + elif isinstance(port_name, str) \ + and port_name in port_info.name: # last of multiples will overwrite - port_id = port_info.port_id + port_id = port_info.port_id best_port = port_info if best_port is None: - raise FileNotFoundError( "No ALSA client:port %d:%s" - % (client_id, port_name) ) + raise FileNotFoundError("No ALSA client:port %d:%s" + % (client_id, port_name)) else: return Address(client_id, best_port.port_id) - def _subunsub_port(self, func, sender: AddressType, dest: AddressType, *, queue: Optional[Union[Queue, int]] = None, diff --git a/alsa_midi/port.py b/alsa_midi/port.py index 75b4e30..57d8f8a 100644 --- a/alsa_midi/port.py +++ b/alsa_midi/port.py @@ -10,6 +10,16 @@ if TYPE_CHECKING: from .client import SequencerClientBase, SubscriptionQuery, SubscriptionQueryType, _snd_seq_t +# + + +class PortDirection(IntFlag): + """Port directions.""" + UNKNOWN = alsa.SND_SEQ_PORT_DIR_UNKNOWN + INPUT = alsa.SND_SEQ_PORT_DIR_INPUT + OUTPUT = alsa.SND_SEQ_PORT_DIR_OUTPUT + BIDIR = alsa.SND_SEQ_PORT_DIR_BIDIRECTION + class PortCaps(IntFlag): """Port capability flags.""" @@ -198,6 +208,7 @@ class PortInfo: :ivar client_id: client identifier :ivar port_id: port identifier :ivar name: port name + :ivar direction: port direction :ivar capability: port capabilities :ivar type: port type :ivar midi_channels: number of MIDI channels @@ -219,6 +230,7 @@ class PortInfo: client_id: int = 0 port_id: Optional[int] = None name: str = "" + direction: PortDirection = PortDirection.UNKNOWN capability: PortCaps = PortCaps._NONE type: PortType = PortType.ANY midi_channels: int = 0 @@ -247,12 +259,14 @@ def __repr__(self): @classmethod def _from_alsa(cls, info: _snd_seq_port_info_t): name = ffi.string(alsa.snd_seq_port_info_get_name(info)) + direction = alsa.snd_seq_port_info_get_direction(info) caps = alsa.snd_seq_port_info_get_capability(info) p_type = alsa.snd_seq_port_info_get_type(info) return cls( client_id=alsa.snd_seq_port_info_get_client(info), port_id=alsa.snd_seq_port_info_get_port(info), name=name.decode(), + direction=PortDirection(direction), capability=PortCaps(caps), type=PortType(p_type), midi_channels=alsa.snd_seq_port_info_get_midi_channels(info), @@ -273,6 +287,7 @@ def _to_alsa(self) -> _snd_seq_port_info_t: alsa.snd_seq_port_info_set_client(info, self.client_id) alsa.snd_seq_port_info_set_port(info, self.port_id if self.port_id is not None else 0) alsa.snd_seq_port_info_set_name(info, self.name.encode()) + alsa.snd_seq_port_info_set_direction(info, self.direction) alsa.snd_seq_port_info_set_capability(info, self.capability) alsa.snd_seq_port_info_set_type(info, self.type) alsa.snd_seq_port_info_set_midi_channels(info, self.midi_channels) @@ -300,7 +315,7 @@ def key(info: PortInfo): return key -__all__ = ["PortCaps", "PortType", "Port", +__all__ = ["PortDirection", "PortCaps", "PortType", "Port", "READ_PORT", "WRITE_PORT", "RW_PORT", "DEFAULT_PORT_TYPE", "READ_PORT_PREFERRED_TYPES", "WRITE_PORT_PREFERRED_TYPES", "RW_PORT_PREFERRED_TYPES", "PortInfo"] diff --git a/examples/connect.py b/examples/connect.py index 01ef3b6..1f52c63 100755 --- a/examples/connect.py +++ b/examples/connect.py @@ -15,22 +15,19 @@ def main(): help="Use real-time-stamp") parser.add_argument("--tick", "-t", action="store_false", dest="real", help="Use tick-time-stamp") - parser.add_argument("sender", metavar="CLIENT:PORT", + parser.add_argument("sender", type=Address, metavar="CLIENT:PORT", help="Source side of the connection") - parser.add_argument("dest", metavar="CLIENT:PORT", + parser.add_argument("dest", type=Address, metavar="CLIENT:PORT", help="Sink side of the connection") args = parser.parse_args() client = SequencerClient("dump_events.py") - sender = client.get_address(args.sender) - dest = client.get_address(args.dest) - if args.disconnect: - client.unsubscribe_port(sender, dest) + client.unsubscribe_port(args.sender, args.dest) else: - client.subscribe_port(sender, dest, + client.subscribe_port(args.sender, args.dest, exclusive=args.exclusive, time_update=(args.real is not None), time_real=args.real) diff --git a/tests/test_port_query.py b/tests/test_port_query.py index ab21345..1d6cb61 100644 --- a/tests/test_port_query.py +++ b/tests/test_port_query.py @@ -131,7 +131,7 @@ def test_list_ports(alsa_seq_state): # test defaults alsa_seq_state.load() alsa_client_names = [client.name for client in alsa_seq_state.clients.values()] - ports = client.list_ports() + ports = client.list_ports(include_system=True, include_kernel=True) for port in ports: assert isinstance(port, PortInfo) @@ -237,7 +237,7 @@ def test_list_ports(alsa_seq_state): assert s_inout_port_a not in port_addrs # test include_system=True - ports = client.list_ports(include_system=True, type=PortType.ANY) + ports = client.list_ports(include_system=True, include_kernel=True, type=PortType.ANY) port_addrs = {(p.client_id, p.port_id) for p in ports} client_names = {p.client_name for p in ports} if "Midi Through" in alsa_client_names: @@ -253,7 +253,7 @@ def test_list_ports(alsa_seq_state): assert inout_port_ne_a in port_addrs # test include_system=False - ports = client.list_ports(include_system=False, type=PortType.ANY) + ports = client.list_ports(include_system=False, include_kernel=True, type=PortType.ANY) port_addrs = {(p.client_id, p.port_id) for p in ports} client_names = {p.client_name for p in ports} if "Midi Through" in alsa_client_names: @@ -269,7 +269,7 @@ def test_list_ports(alsa_seq_state): assert inout_port_ne_a in port_addrs # test include_midi_through=True - ports = client.list_ports(include_midi_through=True) + ports = client.list_ports(include_midi_through=True, include_kernel=True) port_addrs = {(p.client_id, p.port_id) for p in ports} client_names = {p.client_name for p in ports} if "Midi Through" in alsa_client_names: @@ -378,7 +378,7 @@ def test_list_ports_sorting(alsa_seq_state): # test defaults alsa_seq_state.load() alsa_client_names = [client.name for client in alsa_seq_state.clients.values()] - ports = client.list_ports(type=PortType.ANY) + ports = client.list_ports(type=PortType.ANY, include_kernel=True) for port in ports: assert isinstance(port, PortInfo) @@ -444,7 +444,7 @@ def test_list_ports_sorting(alsa_seq_state): # default sorting ports = client.list_ports(only_connectable=False, include_system=True, - type=PortType.ANY) + type=PortType.ANY, include_kernel=True) pa = [(p.client_id, p.port_id) for p in ports] assert pa.index(specific_inout_a) > pa.index(midi_generic_input_a) @@ -459,7 +459,8 @@ def test_list_ports_sorting(alsa_seq_state): assert ports[-1].client_name == "Midi Through" # default input port sorting - ports = client.list_ports(only_connectable=False, include_system=True, input=True, + ports = client.list_ports(only_connectable=False, include_system=True, + include_kernel=True, input=True, type=PortType.ANY) pa = [(p.client_id, p.port_id) for p in ports] @@ -472,7 +473,8 @@ def test_list_ports_sorting(alsa_seq_state): assert ports[-1].client_name == "Midi Through" # default output port sorting - ports = client.list_ports(only_connectable=False, include_system=True, output=True, + ports = client.list_ports(only_connectable=False, include_system=True, + include_kernel=True, output=True, type=PortType.ANY) pa = [(p.client_id, p.port_id) for p in ports] diff --git a/tests/test_sequencer_address.py b/tests/test_sequencer_address.py index a38d047..6860738 100644 --- a/tests/test_sequencer_address.py +++ b/tests/test_sequencer_address.py @@ -1,14 +1,17 @@ +# flake8: noqa + import argparse import sys +import time -from collections import namedtuple +from collections import namedtuple # isort: skip -import alsa_midi -from alsa_midi import Address +import alsa_midi # isort: skip +from alsa_midi import Address # isort: skip -"""Tests SequencerClientBase.get_address() +"""Tests SequencerClientBase.resolve_address() Tests many permutations of "numeric-or-named-client:numeric-or-named-port", "client:port", "client.port", "'client:port'", (client, port), ((client, port)), @@ -25,11 +28,17 @@ """ +verbose = False + +clients_ports = {} # have to store ports somewhere else go out of scope + # and get deleted on return from create_ports() + test_count = 0 # total number of permutations tested pass_count = 0 # successful tests fail_count = 0 # test which failed with expected error + # clients/ports for tests # @@ -41,7 +50,7 @@ OUTPUT_CAPS = alsa_midi.READ_PORT IN_OUT_CAPS = INPUT_CAPS | OUTPUT_CAPS -# all tests use this for client.get_address(...) +# all tests use this for client.resolve_address(...) tester_ports = ( port_spec("zero", -1, 0, False, IN_OUT_CAPS), ) @@ -148,10 +157,10 @@ def arg_name(arg): else: return str(arg).replace(" ", '') - # test successful client.get_address() + # test successful client.resolve_address() if expected_error is None: try: - addr = tester.get_address(address_args) + addr = tester.resolve_address(address_args) except Exception as e: print_stats(sys.stderr) sys.stderr.write( "%s raises unexpected %s\n" @@ -159,11 +168,11 @@ def arg_name(arg): raise else: try: - addr = tester.get_address(address_args) + addr = tester.resolve_address(address_args) except expected_error: test_count += 1 fail_count += 1 - if opts.verbose: + if verbose: sys.stdout.write( "%s raises %s (as expected)\n" % (arg_name(address_args), expected_error.__name__)) @@ -195,7 +204,7 @@ def arg_name(arg): client_id, port_id ) ) - if opts.verbose: + if verbose: sys.stdout.write( "%s -> %d:%d" % (arg_name(address_args), addr.client_id, @@ -211,14 +220,14 @@ def arg_name(arg): # Tests permutations of "numeric-or-named-client:numeric-or-named-port", # "client:port", "client.port", "'client:port'", (client, port), # ((client, port)), etc. -def test_variants(client, - port, - separator, - quotes, - client_id, - port_id, - expected_error, - has_port_0=True): +def do_variants(client, + port, + separator, + quotes, + client_id, + port_id, + expected_error, + has_port_0=True): # just client just_client_error = None if has_port_0 else FileNotFoundError @@ -247,9 +256,9 @@ def test_variants(client, # test all ports of client -def test_addresses(client_name, client_id, ports): - if opts.verbose: - sys.stdout.write( "# --- test_addresses(%s, %s) ---\n" +def do_addresses(client_name, client_id, ports): + if verbose: + sys.stdout.write( "# --- do_addresses(%s, %s) ---\n" % (client_name, client_id)) # Not a rigorous test (ports could be explicit with first @@ -270,14 +279,14 @@ def test_addresses(client_name, client_id, ports): no_ports = not(tests_bits << 3) for port in ports: - test_variants(client_name, - port.name, - separator, - quotes, - client_id, - port.id, - None, - has_port_0) + do_variants(client_name, + port.name, + separator, + quotes, + client_id, + port.id, + None, + has_port_0) # test that flagged ports can be accessed via substring for (port_ndx,port) in enumerate(ports): @@ -285,14 +294,14 @@ def test_addresses(client_name, client_id, ports): # test all 3 character substrings of port name for char_ndx in range(0, len(port.name) - 3): substring = port.name[char_ndx:char_ndx+3] - test_variants(client_name, - substring, - separator, - quotes, - client_id, - port.id, - None, - has_port_0) + do_variants(client_name, + substring, + separator, + quotes, + client_id, + port.id, + None, + has_port_0) @@ -300,8 +309,8 @@ def test_addresses(client_name, client_id, ports): # # Note first client created by create_ports() (gets assigned i.d. 128) # must have port id 0 (explicit or default assigned) -def test_simple(): - if opts.verbose: sys.stdout.write("# --- test_simple() ---\n") +def do_simple(): + if verbose: sys.stdout.write("# --- do_simple() ---\n") address_parse( 128, 128, 0, None) address_parse( '128', 128, 0, None) @@ -315,9 +324,9 @@ def test_simple(): -# Test of client.get_address() overloads -def test_overloads(client_name, client_id, port_name, port_id): - if opts.verbose: sys.stdout.write("# --- test_overloads() ---\n") +# Test of client.resolve_address() overloads +def do_overloads(client_name, client_id, port_name, port_id): + if verbose: sys.stdout.write("# --- do_overloads() ---\n") overloads = [] @@ -338,14 +347,14 @@ def test(*args): test('%s:%d' % (client_name, port_id )) # str:iin test('%s:%s' % (client_name, port_name)) # str:str - overloads.append(tester.get_address(overloads[0])) # address + overloads.append(tester.resolve_address(overloads[0])) # address assert(all(addr == overloads[0] for addr in overloads)) -def test_bad_arguments(): - if opts.verbose: sys.stdout.write("# --- test_bad_arguments() ---\n") +def do_bad_arguments(): + if verbose: sys.stdout.write("# --- do_bad_arguments() ---\n") address_parse(3.14, 0, 0, TypeError) address_parse([], 0, 0, TypeError) @@ -388,22 +397,22 @@ def test_bad_arguments(): -def test_bad_clients_ports(): - if opts.verbose: sys.stdout.write("# --- test_bad_clients_ports() ---\n") +def do_bad_clients_ports(): + if verbose: sys.stdout.write("# --- do_bad_clients_ports() ---\n") for client in (-1, 256, 'bad_client'): for port in (0, -1, 256, 'bad_port'): - test_variants(client, - port, - ':', - '', - client, - port, - FileNotFoundError, - has_port_0=False) + do_variants(client, + port, + ':', + '', + client, + port, + FileNotFoundError, + has_port_0=False) -def test_good_client_bad_ports(client_name, client_id): +def do_good_client_bad_ports(client_name, client_id): for port in (-1, 256, 'bad_port'): for separator in (':', '.'): for quotes in ('', "'", '"'): @@ -425,19 +434,45 @@ def test_good_client_bad_ports(client_name, client_id): -if __name__ == '__main__': +def test_sequencer_address(): + global tester + tester = alsa_midi.SequencerClient("tester" ) # all tests use this sequential = alsa_midi.SequencerClient("sequential") prefix = alsa_midi.SequencerClient("seq" ) explicit = alsa_midi.SequencerClient("explicit" ) # First one gets assigned client id 128, must have port - # with id 0 for test_simple() + # with id 0 for do_simple() test_id = tester .get_client_info().client_id seqn_id = sequential.get_client_info().client_id prfx_id = prefix .get_client_info().client_id expl_id = explicit .get_client_info().client_id + create_ports(tester , tester_ports ) + create_ports(sequential, sequential_ports) + create_ports(prefix , prefix_ports ) + create_ports(explicit , explicit_ports ) + + # sometimes delay before clients/ports actually exist + # particularly if run in tox/pytest environment + time.sleep(2) + + do_simple() + do_bad_arguments() + do_overloads('explicit', expl_id, 'eleven', 11) # only need to do one + do_bad_clients_ports() + do_good_client_bad_ports('explicit', expl_id) # only need to do one + + do_addresses("tester", test_id, tester_ports) + do_addresses("sequential", seqn_id, sequential_ports) + do_addresses("seq", prfx_id, prefix_ports) # client,port substrings + do_addresses("explicit", expl_id, explicit_ports) + do_addresses("expl", expl_id, explicit_ports) # substrings + + + +if __name__ == '__main__': # commandline options parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', @@ -448,24 +483,9 @@ def test_good_client_bad_ports(client_name, client_id): help="Pause after tests") opts = parser.parse_args() - clients_ports = {} # have to store ports somewhere else go out of scope - # and get deleted on return from create_ports() - create_ports(tester , tester_ports ) - create_ports(sequential, sequential_ports) - create_ports(prefix , prefix_ports ) - create_ports(explicit , explicit_ports ) + verbose = opts.verbose - test_simple() - test_bad_arguments() - test_overloads('explicit', expl_id, 'eleven', 11) # only need to do one - test_bad_clients_ports() - test_good_client_bad_ports('explicit', expl_id) # only need to do one - - test_addresses("tester", test_id, tester_ports) - test_addresses("sequential", seqn_id, sequential_ports) - test_addresses("seq", prfx_id, prefix_ports) # client,port substrings - test_addresses("explicit", expl_id, explicit_ports) - test_addresses("expl", expl_id, explicit_ports) # substrings + test_sequencer_address() if opts.verbose or opts.pause: print_stats() @@ -478,8 +498,4 @@ def test_good_client_bad_ports(client_name, client_id): except KeyboardInterrupt: sys.stdout.write("exiting\n") - explicit.close() - sequential.close() - tester.close() - sys.exit(0) diff --git a/tox.ini b/tox.ini index 9e7c555..c4caf40 100644 --- a/tox.ini +++ b/tox.ini @@ -44,3 +44,4 @@ commands = [flake8] max-line-length=99 +exclude = tests/test_sequencer_address.py diff --git a/utils/aconnect.py b/utils/aconnect.py new file mode 100755 index 0000000..ef51479 --- /dev/null +++ b/utils/aconnect.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 + +import argparse +import errno +import os +from pprint import pprint # DEBUG +import re +import shutil +import sys +import textwrap + +from alsa_midi import SequencerClient, \ + Address, \ + ALSAError, \ + ClientType, \ + SubscriptionQueryType + +program_name = None +clients = {} +ports = {} + + + +def direction(raw): + # The perfection that is ALSA sequencer + if raw.name == 'INPUT' : return 'OUTPUT' + elif raw.name == 'OUTPUT': return 'INPUT' + else: return raw.name + + +def indent(text): + indenter.subsequent_indent = " " * re.search(r"^.*'.*'\s*", text).span()[1] + sys.stdout.write('%s\n' % indenter.fill(text)) + + + +def init(term_width): + global program_name, aconnect, indenter + aconnect = SequencerClient(program_name) + width = term_width \ + if term_width > 0 \ + else shutil.get_terminal_size().columns - 5 + indenter = textwrap.TextWrapper(width=width, break_long_words=False) + + + +def get_clients(client_list=None): + # Need include_system for client:port names of connections + # from non-system to system clients. + client_list = aconnect.list_clients(include_system=True, + include_kernel=True) + for client in client_list: + if client.name != program_name: + clients[client.client_id] = client + + + +def get_ports(): + global aconnect, ports + + # Need ports for system clients in to get client:port names + # for non-system ports connected to system ones. + port_list = aconnect.list_ports(include_system=True, + include_kernel=True, + type=None, + only_connectable=False) + + for port in port_list: + ports.setdefault(port.client_id, {}) + ports[port.client_id][port.port_id] = port + + + +def print_connections(client, address): + for sub in aconnect.list_port_subscribers(address): + # other = sub.addr + sub_clnt = clients[sub.addr.client_id].name + sub_port = ports[sub.addr.client_id][sub.addr.port_id].name + if sub.type == SubscriptionQueryType.READ: + sys.stdout.write( "\tconnected to " + f"'{sub_clnt}:{sub_port}'" + f"({sub.addr})\n") + elif sub.type == SubscriptionQueryType.WRITE: + sys.stdout.write( "\tconnected from " + f"'{sub_clnt}:{sub_port}'" + f"({sub.addr})\n") + + + +def report(clients_only = False, + all_clients = False, + pipewire_clients = False, + full_info = False): + global aconnect, clients, ports + + if not clients: + get_clients() + + if clients_only: + ports = None + max_name_len = max([len(client.name) for client in clients.values()]) + max_type_len = max([ len(client.type.name) + for client + in clients.values()]) + else: + if not ports: + get_ports() + max_port_len = max([ len(port.name) + for portval in ports.values() + for port + in portval.values() + if not ( not all_clients + and + clients[port.client_id].type + == ClientType.KERNEL) + ]) + + for (client_id, client) in clients.items(): + # if client.name == program_name: + # continue + + if client.type == ClientType.KERNEL and not all_clients: + continue + + if 'PipeWire' in client.name and not pipewire_clients: + continue + + # If all ports are of same type report on client line + port_type = None + if ports and client_id in ports and len(ports[client_id].values()) > 0: + port_type_0 = list(ports[client_id].values())[0].type + # pprint([port.type for port in ports[client_id].values()]) + if all( port.type == port_type_0 + for port + in ports[client_id].values()): + port_type = port_type_0 + + if clients_only: + client_str = f"{client_id:>3}: {client.name!r:<{max_name_len+4}}" + if full_info: + client_str += f"{client.type.name} " \ + + f"ports={client.num_ports} " \ + + f"pid={client.pid}" + else: + client_str += f"{client.type.name:<{max_type_len}} " \ + + f"ports={client.num_ports:<3} " \ +"pid={client.pid}" + else: + client_str = f"{client_id}: '{client.name}' " \ + f"{client.type.name} pid={client.pid}" + + if port_type is not None: + client_str += f" ports={port_type.name}" + + if full_info: + indent(f"{client_str} " + f"event_filter={client.event_filter} " + f"broadcast_filter={client.broadcast_filter} " + f"error_bounce={client.error_bounce} " + f"event_lost={client.event_lost} " + f"card_id={client.card_id} " + f"num_ports={client.num_ports}") + else: + indent(f"{client_str}\n") + + if ports and client_id in ports: + for port in ports[client_id].values(): + port_str = (f" {port.port_id:>3}: " + f"{port.name!r:<{max_port_len+3}} " + f"{direction(port.direction)}") + if port_type is None: + port_str += f" {port_type.name}" + if full_info: + indent(f"{port_str} " + f"capability={port.capability.name} " + f"client_id={port.client_id} " + f"midi_channels={port.midi_channels} " + f"midi_voices={port.midi_voices} " + f"port_specified={port.port_specified} " + f"read_use={port.read_use} " + f"synth_voices={port.synth_voices} " + f"timestamp_queue_id= " + f"{port.timestamp_queue_id} " + f"timestamp_real={port.timestamp_real} " + f"timestamping={port.timestamping} " + f"write_use={port.write_use}") + else: + sys.stdout.write(f"{port_str}\n") + print_connections(client, Address(port.client_id, port.port_id)) + + + +def connect_disconnect(disconnect, source, sink, exclusive=None, real=None): + "Connect or disconnect client:port to/from another_client:another_port" + + global aconnect + + source_addr = aconnect.resolve_address(source) + sink_addr = aconnect.resolve_address(sink ) + + if disconnect: + try: + aconnect.unsubscribe_port(source_addr, sink_addr) + except ALSAError as e: + raise ALSAError(f"Can't disconnect {source} -> {sink} " + "(not connected?)", + errno.EINVAL) + else: + try: + aconnect.subscribe_port(source_addr, + sink_addr, + exclusive=opts.exclusive, + time_update=(opts.real is not None), + time_real=opts.real) + except ALSAError as e: + raise ALSAError(f"Can't connect {source} -> {sink} " + "(input->output?) (or already connected?)", + errno.ENOENT) + + + +def disconnect(client, no_safety): + "Disconnect all ports of one client" + + global aconnect, clients, ports + + # check that is just client (no port) + if not isinstance(client, int): + assert not any(char in client for char in '.:'), \ + "Specify client, not client:port" + + # Can't use aconnect.resolve_address() because no port specified + # and client might not have default port 0. + if isinstance(client, int): + client_id = client + client_name = str(client) + else: + client_name = client + try: + client_id = int(client) + except ValueError: + client_id = None + + client_list = aconnect.list_clients(include_system=True, + include_kernel=True) + found_id = None + for search in client_list: + if client_id is not None and search.client_id == client_id: + found_id = search.client_id + break + elif search.name == client_name: + found_id = search.client_id + break + + assert found_id is not None, \ + f"No client named '{client_name}'" \ + if client_id is None \ + else f"No client number {client_id}" + + if not clients: get_clients(client_list) + if not ports: get_ports() + + if found_id == 0 \ + or clients[found_id].type == ClientType.KERNEL \ + or 'PipeWire' in clients[found_id].name: + raise ALSAError(f"Can't disconnect client {found_id}'s ports " + "(is system, KERNEL, and/or PipeWire)", + errno.EINVAL) + + if not no_safety: + max_port_len = max([ len(port.name) + for port + in ports[found_id].values()]) + for port in ports[found_id].values(): + sys.stdout.write(f" {port.port_id:>3}: " + f"{port.name!r:<{max_port_len+3}} " + f"{direction(port.direction)}\n") + print_connections(clients[found_id], + Address(found_id, port.port_id)) + if input("Remove all (if any) connections? n/Y: ") != 'Y': + sys.stdout.write("Aborting\n") + return + + for port in ports[found_id].values(): + address = Address(found_id, port.port_id) + for sub in aconnect.list_port_subscribers(address): + sub_clnt = clients[sub.addr.client_id].name + sub_port = ports[sub.addr.client_id][sub.addr.port_id].name + if sub.type == SubscriptionQueryType.READ: + connect_disconnect(True, address, sub.addr) + elif sub.type == SubscriptionQueryType.WRITE: + connect_disconnect(True, sub.addr, address) + + + +def remove_all(no_safety): + global aconnect, program_name + + if not no_safety: + report() + if input("Remove all (if any) connections? n/Y: ") != 'Y': + sys.stdout.write("Aborting\n") + return + + for client in aconnect.list_clients(include_system=False, + include_kernel=False): + if client.name == program_name or "PipeWire" in client.name: + continue + disconnect(client.client_id, no_safety=True) + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="List, connect, disconnect " + "ALSA ports") + + parser.add_argument('-d', '--disconnect', + action="store_true", + help="Disconnect existing one connection or " + "all of one client's connections") + parser.add_argument('-D', '--DISCONNECT', + action="store_true", + help="Disconnect all of one client's connections " + "without 'Are you sure?'") + parser.add_argument('-x', '--removeall', + action='store_true', + help="Disconnect all clients/ports connections") + parser.add_argument('-X', '--REMOVEALL', + action='store_true', + help="Disconnect all clients/ports connections " + "without 'Are you sure?'") + parser.add_argument('-e', '--exclusive', + action='store_true', + help="Exclusive connection") + parser.add_argument('-r', '--real', + action='store_true', + help="Use real-time-stamp") + parser.add_argument('-t', '--tick', + action='store_false', + dest='real', + help="Use tick-time-stamp") + + parser.add_argument('-c', '--clients', + action='store_true', + help="list only clients (no port info)") + parser.add_argument('-i', '--input', + action='store_true', + help="list input (readable) ports") + parser.add_argument('-o', '--output', + action='store_true', + help="list output (writable) ports") + parser.add_argument('-a', '--all', + action='store_true', + help="include kernel/system clients and ports") + parser.add_argument('-p', '--pipewire', + action='store_true', + help="include pipewire clients and ports") + parser.add_argument('-f', '--full', + action='store_true', + help="full client and port info") + parser.add_argument('-w', '--width', + type=int, + default=0, + help="override terminal width (or provide if " + "not tty) if not 0") + + parser.add_argument('source', + nargs='?', + metavar='CLIENT:PORT', + help="Source side of the connection") + parser.add_argument('dest', + nargs='?', + metavar='CLIENT:PORT', + help="Sink side of the connection") + + try: + program_name = os.path.basename(sys.argv[0]) + opts = parser.parse_args() + # pprint(opts) # DEBUG + if opts.DISCONNECT: opts.disconnect = True + if opts.REMOVEALL: opts.removeall = True + + + init(opts.width) + + if opts.source: + if opts.dest: + connect_disconnect(opts.disconnect, + opts.source, + opts.dest, + opts.exclusive, + opts.real) + else: + disconnect(opts.source, opts.DISCONNECT) + elif opts.removeall: + remove_all(opts.REMOVEALL) + else: + report(opts.clients, opts.all, opts.pipewire, opts.full) + sys.stdout.write(f"'{program_name} -h' for options\n") + except Exception as e: + sys.stderr.write(f"{e}\n") + sys.exit(1) + + sys.exit(0) From af603256b4bcd3ce9875b7b64d0626373c450a30 Mon Sep 17 00:00:00 2001 From: thanks4opensource <31165662+thanks4opensource@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:38:59 -0700 Subject: [PATCH 3/4] utils/aconnect.py and supporting changes Adds new aconnect.py, full replacement for alsa-utils/.../aconnect.c, including SequencerClientBase.resolve_address(), and output and usability improvments. New SequencerClientBase::list_clients(), port.py class PortDirection, and other additions to support aconnect.py functionality. Small updates to tests/test_port_query.py for new SequencerClientBase::list_clients() parameter. Incorporates large Jajcus commit 716751 (typing modernizations). Reverted examples/connect.py to original without resolve_address(). --- alsa_midi/__init__.py | 4 +- alsa_midi/address.py | 2 +- alsa_midi/client.py | 256 ++++++----- alsa_midi/event.py | 52 +-- alsa_midi/mido_backend.py | 12 +- alsa_midi/port.py | 4 +- alsa_midi/queue.py | 18 +- setup.cfg | 2 +- tests/conftest.py | 5 +- tests/test_client.py | 40 ++ tests/test_sequencer_address.py | 256 ++++++----- tox.ini | 1 - utils/aconnect.py | 768 +++++++++++++++++++++----------- 13 files changed, 884 insertions(+), 536 deletions(-) diff --git a/alsa_midi/__init__.py b/alsa_midi/__init__.py index 0914ff3..d288428 100644 --- a/alsa_midi/__init__.py +++ b/alsa_midi/__init__.py @@ -19,7 +19,7 @@ TimeSignatureEvent, TuneRequestEvent, UserVar0Event, UserVar1Event, UserVar2Event, UserVar3Event, UserVar4Event) from .exceptions import ALSAError, Error, StateError -from .port import READ_PORT, RW_PORT, WRITE_PORT, Port, PortCaps, PortInfo, PortType +from .port import READ_PORT, RW_PORT, WRITE_PORT, Port, PortCaps, PortDirection, PortInfo, PortType from .queue import Queue, QueueInfo, QueueStatus, QueueTempo, QueueTimer, QueueTimerType __all__ = [ @@ -29,7 +29,7 @@ "RemoveCondition", "RealTime", "EventType", "EventFlags", "Event", "MidiBytesEvent", "Error", "StateError", "ALSAError", - "Port", "PortCaps", "PortType", "PortInfo", + "Port", "PortCaps", "PortDirection", "PortType", "PortInfo", "READ_PORT", "WRITE_PORT", "RW_PORT", "Queue", "QueueInfo", "QueueStatus", "QueueTempo", "QueueTimer", "QueueTimerType", "alsa", "ffi", diff --git a/alsa_midi/address.py b/alsa_midi/address.py index e6338d3..1a3da46 100644 --- a/alsa_midi/address.py +++ b/alsa_midi/address.py @@ -56,7 +56,7 @@ def __new__(cls, arg1: 'SequencerClient', arg2: int = 0) -> 'Address': ... def __new__(cls, - arg1: Union[int, str, AddressType, 'SequencerClient'], + arg1: int | str | AddressType | 'SequencerClient', arg2: int = 0) -> 'Address': if isinstance(arg1, str): tple: Any = cls._parse(arg1) diff --git a/alsa_midi/client.py b/alsa_midi/client.py index 68b025b..671e994 100644 --- a/alsa_midi/client.py +++ b/alsa_midi/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import errno import select @@ -6,7 +8,7 @@ from dataclasses import dataclass, field from enum import IntEnum, IntFlag from functools import partial -from typing import Any, Callable, NewType, Optional, Union, overload +from typing import Any, Callable, NewType, overload from weakref import WeakValueDictionary from ._ffi import alsa, ffi @@ -84,12 +86,12 @@ class ClientInfo: name: str = "" broadcast_filter: bool = False error_bounce: bool = False - type: Optional[ClientType] = None - card_id: Optional[int] = None - pid: Optional[int] = None + type: ClientType | None = None + card_id: int | None = None + pid: int | None = None num_ports: int = 0 event_lost: int = 0 - event_filter: Optional[set[EventType]] = None + event_filter: set[EventType] | None = None @classmethod def _from_alsa(cls, info: _snd_seq_client_info_t): @@ -336,7 +338,7 @@ class RemoveEvents: Represents :alsa:`snd_seq_remove_events_t` data.""" condition: RemoveCondition = RemoveCondition(0) queue_id: int = 0 - time: Union[RealTime, int] = field(default_factory=RealTime) + time: RealTime | int = field(default_factory=RealTime) dest: AddressType = Address(0, 0) channel: int = 0 event_type: EventType = EventType.NONE @@ -359,7 +361,7 @@ class SequencerClientBase: handle: _snd_seq_t _handle_p: _snd_seq_t_p _fd: int = -1 - _event_parser: Optional[_snd_midi_event_t] = None + _event_parser: _snd_midi_event_t | None = None _queues: MutableMapping[int, Queue] def __init__( @@ -404,6 +406,7 @@ def close(self): self._handle_p = None # type: ignore self.handle = None # type: ignore self._event_parser = None + self._fd = -1 def _get_fds(self): """Get the file descriptor number for the client connection into :data:`self._fd`.""" @@ -419,11 +422,6 @@ def _get_fds(self): assert (pfds[0].events & select.POLLIN) and (pfds[0].events & select.POLLOUT) self._fd = pfds[0].fd - @property - def fd(self): - "Returns the ALSA sequencer file descriptor number." - return self._fd - def _get_event_parser(self): parser = self._event_parser if parser is None: @@ -435,6 +433,21 @@ def _get_event_parser(self): self._event_parser = parser return parser + def fileno(self): + """Return file descriptor assigned to this connection. + + This file descriptor can be used with select, poll, or other event + loop mechanisms for monitoring ALSA sequencer events. + + Wraps :alsa:`snd_seq_poll_descriptors`. + + :return: file descriptor number + :raises OSError: if the connection is closed or file descriptor is not available + """ + if self._fd >= 0: + return self._fd + raise OSError("File descriptor not available") + def get_sequencer_name(self) -> str: """Get sequencer name. @@ -490,13 +503,13 @@ def create_port(self, caps: PortCaps = RW_PORT, type: PortType = DEFAULT_PORT_TYPE, *, - port_id: Optional[int] = None, - midi_channels: Optional[int] = None, - midi_voices: Optional[int] = None, - synth_voices: Optional[int] = None, - timestamping: Optional[bool] = None, - timestamp_real: Optional[bool] = None, - timestamp_queue: Optional[Union[Queue, int]] = None, + port_id: int | None = None, + midi_channels: int | None = None, + midi_voices: int | None = None, + synth_voices: int | None = None, + timestamping: bool | None = None, + timestamp_real: bool | None = None, + timestamp_queue: Queue | int | None = None, ) -> Port: """Create a sequencer port. @@ -665,7 +678,7 @@ def drop_output_buffer(self): err = alsa.snd_seq_drop_output_buffer(self.handle) _check_alsa_error(err) - def _event_input(self, prefer_bytes: bool = False) -> tuple[int, Optional[Event]]: + def _event_input(self, prefer_bytes: bool = False) -> tuple[int, Event | None]: buf = ffi.new("snd_seq_event_t**", ffi.NULL) result = alsa.snd_seq_event_input(self.handle, buf) if result < 0: @@ -731,10 +744,10 @@ def event_input_pending(self, fetch_sequencer: bool = False) -> int: def _prepare_event(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None, - remainder: Optional[Any] = None) -> tuple[_snd_seq_event_t, Any]: + remainder: Any = None) -> tuple[_snd_seq_event_t, Any]: """Prepare ALSA :alsa:`snd_seq_event_t` for given `event` object for output. For :class:`alsa_midi.MidiBytesEvent` may need to be called more than @@ -781,10 +794,10 @@ def _prepare_event(self, def _event_output(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None, - remainder: Optional[Any] = None) -> tuple[int, Any]: + remainder: Any = None) -> tuple[int, Any]: alsa_event, remainder = self._prepare_event(event, queue=queue, port=port, dest=dest, remainder=remainder) @@ -795,8 +808,8 @@ def _event_output(self, def event_output(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event. @@ -828,8 +841,8 @@ def event_output(self, def event_output_buffer(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event to a buffer. @@ -865,10 +878,10 @@ def event_output_buffer(self, def _event_output_direct(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None, - remainder: Optional[Any] = None) -> tuple[int, Any]: + remainder: Any = None) -> tuple[int, Any]: alsa_event, remainder = self._prepare_event(event, queue=queue, port=port, dest=dest, remainder=remainder) @@ -879,8 +892,8 @@ def _event_output_direct(self, def event_output_direct(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event directly to the sequencer. @@ -949,7 +962,7 @@ def get_system_info(self) -> SystemInfo: result = SystemInfo._from_alsa(info) return result - def get_client_info(self, client_id: Optional[int] = None) -> ClientInfo: + def get_client_info(self, client_id: int | None = None) -> ClientInfo: """Obtain information about a client. Wraps :alsa:`snd_seq_get_client_info` or :alsa:`snd_seq_get_any_client_info`. @@ -994,15 +1007,15 @@ def set_client_event_filter(self, event_type: EventType): _check_alsa_error(err) @overload - def query_next_client(self, previous: ClientInfo) -> Optional[ClientInfo]: + def query_next_client(self, previous: ClientInfo) -> ClientInfo | None: ... @overload - def query_next_client(self, previous: Optional[int] = None) -> Optional[ClientInfo]: + def query_next_client(self, previous: int | None = None) -> ClientInfo | None: ... - def query_next_client(self, previous: Optional[Union[ClientInfo, int]] = None - ) -> Optional[ClientInfo]: + def query_next_client(self, previous: ClientInfo | int | None = None + ) -> ClientInfo | None: """Obtain information about the first or the next sequencer client. Wraps :alsa:`snd_seq_query_next_client`. @@ -1027,7 +1040,7 @@ def query_next_client(self, previous: Optional[Union[ClientInfo, int]] = None result = ClientInfo._from_alsa(info) return result - def get_port_info(self, port: Union[int, AddressType]) -> PortInfo: + def get_port_info(self, port: int | AddressType) -> PortInfo: """Obtain information about a specific port. Wraps :alsa:`snd_seq_get_port_info` or :alsa:`snd_seq_get_any_port_info`. @@ -1053,7 +1066,7 @@ def get_port_info(self, port: Union[int, AddressType]) -> PortInfo: result = PortInfo._from_alsa(info) return result - def set_port_info(self, port: Union[int, Port], info: PortInfo): + def set_port_info(self, port: int | Port, info: PortInfo): """Set information about a specific own port. Wraps :alsa:`snd_seq_set_port_info`. @@ -1068,18 +1081,18 @@ def set_port_info(self, port: Union[int, Port], info: PortInfo): @overload def query_next_port(self, client_id: int, previous: PortInfo - ) -> Optional[PortInfo]: + ) -> PortInfo | None: ... @overload - def query_next_port(self, client_id: int, previous: Optional[int] = None - ) -> Optional[PortInfo]: + def query_next_port(self, client_id: int, previous: int | None = None + ) -> PortInfo | None: ... def query_next_port(self, client_id: int, - previous: Optional[Union[PortInfo, int]] = None - ) -> Optional[PortInfo]: + previous: PortInfo | int | None = None + ) -> PortInfo | None: """Obtain information about the first or the next port of a sequencer client. Wraps :alsa:`snd_seq_query_next_port`. @@ -1164,7 +1177,7 @@ def list_ports(self, *, include_midi_through: bool = True, include_no_export: bool = True, only_connectable: bool = True, - sort: Union[bool, Callable[[PortInfo], Any]] = True, + sort: bool | Callable[[PortInfo], Any] = True, ) -> list[PortInfo]: """More friendly interface to list available ports. @@ -1172,7 +1185,7 @@ def list_ports(self, *, The result is sorted in a way that the first returned entry should be the 'most usable' one for the selected purpose. E.g. when `output` = `True` then the first entry will be - a synthesizer input port rather than the dummy 'Midi Through' port. Thisa is still a guess, + a synthesizer input port rather than the dummy 'Midi Through' port. This is still a guess, though, so in the end the user should be able to choose. Wraps :alsa:`snd_seq_query_next_client` and :alsa:`snd_seq_query_next_port`. @@ -1311,8 +1324,8 @@ def resolve_address(self, arg1: AddressType) -> 'Address': ... def resolve_address(self, - arg1: Union[int, str, tuple, AddressType], - arg2: Union[int, str, None] = None) -> Address: + arg1: int | str | tuple | AddressType, + arg2: int | str | None = None) -> Address: """Parse client:port address string Similar to Address() but parses client and port by names @@ -1347,52 +1360,75 @@ def resolve_address(self, """ def type_error(): - raise TypeError("(%s, %s) is not one or two ints and/or " - "strings, or 2-tuple of same, or Address" + raise TypeError("resolve_address(%s, %s) is not a single string " + "argument of form 'client:port' (or " + "'client.port') with string client:port names " + "or numeric IDs, or a single Address argument, " + "or two string or int client,port arguments " + "(no ':' 'or '.'), or a single 2-tuple argument " + "of same." % (arg1, arg2)) - if arg2 is None: + def strip_quotes(arg): + # remove enclosing single or double quotes if any + if arg.startswith("'"): + arg = arg.strip("'") + elif arg.startswith('"'): + arg = arg.strip('"') + return arg + + # Validate arguments ... + args_parsed = False + + if isinstance(arg1, Address): + if arg2 is not None: + type_error() + client_name = arg1.client_id # will be tested and converted + port_name = arg1.port_id # to int below + args_parsed = True + elif isinstance(arg1, tuple): + if arg2 is not None: + type_error() + if len(arg1) == 2: + # treat as separate args so don't have to duplicate + # parsing below + (arg1, arg2) = arg1 + else: + type_error() + + if not args_parsed: + if isinstance(arg2, str): + if ':' in arg2 or '.' in arg2: + type_error() + arg2 = strip_quotes(arg2) if isinstance(arg1, str): - # remove enclosing single or double quotes if any - if arg1.startswith("'"): - arg1 = arg1.strip("'") - elif arg1.startswith('"'): - arg1 = arg1.strip('"') - # normalize "client.port" to "client:port" - if '.' in arg1: - arg1 = arg1.replace('.', ':') - # split client and port - client_port = arg1.split(':') - if len(client_port) == 2: - (client_name, port_name) = client_port + arg1 = strip_quotes(arg1) + if ':' in arg1 or '.' in arg1: + if arg2 is not None: + type_error() + # normalize "client.port" to "client:port" + if '.' in arg1: + arg1 = arg1.replace('.', ':') + # split client and port + client_port = arg1.split(':') + if len(client_port) == 2: + (client_name, port_name) = client_port + else: + client_name = arg1 + port_name = None else: client_name = arg1 - port_name = None - elif isinstance(arg1, int): - client_name = arg1 - port_name = None - elif isinstance(arg1, tuple): - if arg2 is not None: - type_error() - if len(arg1) == 2: - (client_name, port_name) = arg1 - else: - type_error() - elif isinstance(arg1, Address): - if arg2 is not None: - type_error() - client_name = arg2.client_id # int will be converted below - port_name = arg2.port_id # same + port_name = arg2 # OK if None else: - type_error() - else: - client_name = arg1 - port_name = arg2 + client_name = arg1 + port_name = arg2 # OK if None if not (isinstance(client_name, (int, str)) and isinstance(port_name, (int, str, type(None)))): type_error() + # Arguments validated. Continue. + ports = self.list_ports(include_system=True) # confirm client exists @@ -1465,7 +1501,7 @@ def type_error(): def _subunsub_port(self, func, sender: AddressType, dest: AddressType, *, - queue: Optional[Union[Queue, int]] = None, + queue: Queue | int | None = None, exclusive: bool = False, time_update: bool = False, time_real: bool = False): @@ -1493,7 +1529,7 @@ def _subunsub_port(self, func, _check_alsa_error(err) def subscribe_port(self, sender: AddressType, dest: AddressType, *, - queue: Optional[Union[Queue, int]] = None, + queue: Queue | int | None = None, exclusive: bool = False, time_update: bool = False, time_real: bool = False): @@ -1556,7 +1592,7 @@ def query_port_subscribers(self, def list_port_subscribers(self, port: AddressType, - type: Optional[SubscriptionQueryType] = None, + type: SubscriptionQueryType | None = None, ) -> list[SubscriptionQuery]: """Lists subscribers accessing a port. @@ -1719,8 +1755,8 @@ def remove_events(self, condition: RemoveEvents): @overload def remove_events(self, condition: RemoveCondition = None, - queue: Union[Queue, int] = None, - time: RealTime = None, + queue: Queue | int = None, + time: RealTime | int = None, dest: AddressType = None, channel: int = None, event_type: EventType = None, @@ -1731,9 +1767,9 @@ def remove_events(self, ... def remove_events(self, - condition: Union[RemoveEvents, RemoveCondition] = None, - queue: Union[Queue, int] = None, - time: Union[RealTime, int] = None, + condition: RemoveEvents | RemoveCondition = None, + queue: Queue | int = None, + time: RealTime | int = None, dest: AddressType = None, channel: int = None, event_type: EventType = None, @@ -1855,8 +1891,8 @@ def __init__(self, client_name: str, **kwargs): self._write_poll = select.poll() self._write_poll.register(self._fd, select.POLLOUT) - def event_input(self, prefer_bytes: bool = False, timeout: Optional[float] = None - ) -> Optional[Event]: + def event_input(self, prefer_bytes: bool = False, timeout: float | None = None + ) -> Event | None: """Wait for and receive an incoming event. Wraps :alsa:`snd_seq_event_input` and :alsa:`snd_midi_event_decode` when `prefer_bytes` is @@ -1892,10 +1928,10 @@ def _event_output_wait(self, func) -> int: ... @overload - def _event_output_wait(self, func, timeout: float) -> Union[int, None]: + def _event_output_wait(self, func, timeout: float) -> int | None: ... - def _event_output_wait(self, func, timeout: Optional[float] = None) -> Union[int, None]: + def _event_output_wait(self, func, timeout: float | None = None) -> int | None: if timeout: until = time.monotonic() + timeout else: @@ -1937,8 +1973,8 @@ def func(remainder=None): def event_output(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event. @@ -1964,8 +2000,8 @@ def event_output(self, def event_output_direct(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event without buffering. @@ -2015,8 +2051,8 @@ async def aclose(self): """ self.close() - async def event_input(self, prefer_bytes: bool = False, timeout: Optional[float] = None - ) -> Optional[Event]: + async def event_input(self, prefer_bytes: bool = False, timeout: float | None = None + ) -> Event | None: """Wait for and receive an incoming event. Wraps :alsa:`snd_seq_event_input` and alsa:`snd_midi_event_decode` when `prefer_bytes` is @@ -2068,10 +2104,10 @@ async def _event_output_wait(self, func) -> int: ... @overload - async def _event_output_wait(self, func, timeout: float) -> Union[int, None]: + async def _event_output_wait(self, func, timeout: float) -> int | None: ... - async def _event_output_wait(self, func, timeout: Optional[float] = None) -> Union[int, None]: + async def _event_output_wait(self, func, timeout: float | None = None) -> int | None: result, remainder = func() if result != -errno.EAGAIN and remainder is None: _check_alsa_error(result) @@ -2129,8 +2165,8 @@ def func(remainder=None): async def event_output(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event. @@ -2156,8 +2192,8 @@ async def event_output(self, async def event_output_direct(self, event: Event, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None) -> int: """Output an event without buffering. diff --git a/alsa_midi/event.py b/alsa_midi/event.py index b8eb6ca..e7e98de 100644 --- a/alsa_midi/event.py +++ b/alsa_midi/event.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from collections.abc import Iterable from enum import IntEnum, IntFlag from functools import total_ordering -from typing import TYPE_CHECKING, Any, NewType, Optional, Union +from typing import TYPE_CHECKING, Any, NewType from ._ffi import alsa, ffi from .address import Address, AddressType @@ -30,7 +32,7 @@ class RealTime: seconds: int nanoseconds: int - def __init__(self, seconds: Union[float, int, str, 'RealTime'] = 0, nanoseconds: int = 0): + def __init__(self, seconds: float | int | str | 'RealTime' = 0, nanoseconds: int = 0): if isinstance(seconds, RealTime): self.seconds = seconds.seconds self.nanoseconds = seconds.nanoseconds + nanoseconds @@ -229,28 +231,28 @@ class Event: _specialized = {} type = None - flags: Optional[EventFlags] + flags: EventFlags | None tag: int - queue_id: Optional[int] - time: Optional[RealTime] - tick: Optional[int] - source: Optional[Address] - dest: Optional[Address] - relative: Optional[bool] - raw_data: Optional[bytes] + queue_id: int | None + time: RealTime | None + tick: int | None + source: Address | None + dest: Address | None + relative: bool | None + raw_data: bytes | None def __init__(self, - type: Optional[EventType] = None, + type: EventType | None = None, *, - flags: Optional[Union[EventFlags, int]] = 0, + flags: EventFlags | int = 0, tag: int = 0, - queue_id: Optional[int] = None, - time: Optional[Union[RealTime, float, int]] = None, - tick: Optional[int] = None, - source: Optional[AddressType] = None, - dest: Optional[AddressType] = None, - relative: Optional[bool] = None, - raw_data: bytes = None, + queue_id: int | None = None, + time: RealTime | float | int | None = None, + tick: int | None = None, + source: AddressType | None = None, + dest: AddressType | None = None, + relative: bool | None = None, + raw_data: bytes | None = None, ): if type is None and self.__class__.type is not None: @@ -323,8 +325,8 @@ def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs): **kwargs) def _to_alsa(self, a_event: _snd_seq_event_t, *, - queue: Union['Queue', int] = None, - port: Union['Port', int] = None, + queue: 'Queue' | int = None, + port: 'Port' | int = None, dest: AddressType = None ) -> _snd_seq_event_t: if self.type is not None: @@ -404,7 +406,7 @@ class MidiBytesEvent(Event): midi_bytes: bytes def __init__(self, - midi_bytes: Union[bytes, Iterable[int]], + midi_bytes: bytes | Iterable[int], **kwargs): super().__init__(**kwargs) self.midi_bytes = bytes(midi_bytes) @@ -603,10 +605,10 @@ class QueueControlEventBase(Event): :ivar control_queue: affected queue id """ - control_queue: Optional[int] + control_queue: int | None def __init__(self, - control_queue: Optional[Union[int, 'Queue']] = None, + control_queue: int | 'Queue' | None = None, **kwargs): assert self.type is not None super().__init__(self.type, **kwargs) @@ -945,7 +947,7 @@ class SetQueuePositionTimeEvent(QueueControlEventBase): position: RealTime def __init__(self, - position: Union[RealTime, int, float], + position: RealTime | int | float, *args, **kwargs): super().__init__(*args, **kwargs) self.position = RealTime(position) diff --git a/alsa_midi/mido_backend.py b/alsa_midi/mido_backend.py index f1bb198..efdbe79 100644 --- a/alsa_midi/mido_backend.py +++ b/alsa_midi/mido_backend.py @@ -9,12 +9,14 @@ .. _MIDO: https://mido.readthedocs.io/ """ +from __future__ import annotations + import logging import queue import sys import threading from collections.abc import MutableMapping -from typing import Any, Callable, Optional +from typing import Any, Callable from weakref import WeakValueDictionary from mido.messages import Message @@ -27,7 +29,7 @@ class _Client: - instance: Optional['_Client'] = None + instance: '_Client' | None = None client: alsa_midi.SequencerClient in_thread: threading.Thread @@ -142,10 +144,10 @@ class PortCommon: _parser: Parser _virtual: bool = False - _callback: Optional[Callable[[Message], Any]] = None + _callback: Callable[[Message], Any] | None = None - _port: Optional[alsa_midi.Port] = None - _dest_port: Optional[alsa_midi.PortInfo] = None + _port: alsa_midi.Port | None = None + _dest_port: alsa_midi.PortInfo | None = None name: str @classmethod diff --git a/alsa_midi/port.py b/alsa_midi/port.py index 57d8f8a..2835aea 100644 --- a/alsa_midi/port.py +++ b/alsa_midi/port.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import InitVar, dataclass, field from enum import IntFlag from typing import TYPE_CHECKING, Any, Callable, NewType, Optional @@ -83,7 +85,7 @@ class Port: :ivar port_id: port identifier """ - client: Optional['SequencerClientBase'] + client: 'SequencerClientBase' | None client_id: int port_id: int diff --git a/alsa_midi/queue.py b/alsa_midi/queue.py index 2a9af38..a39465b 100644 --- a/alsa_midi/queue.py +++ b/alsa_midi/queue.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from dataclasses import dataclass, field from enum import IntEnum -from typing import TYPE_CHECKING, NamedTuple, NewType, Optional, Union +from typing import TYPE_CHECKING, NamedTuple, NewType from ._ffi import alsa, ffi from .event import EventType, RealTime @@ -128,8 +130,8 @@ class QueueTempo: tempo: int = 500000 ppq: int = 96 - skew: Optional[int] = None - skew_base: Optional[int] = None + skew: int | None = None + skew_base: int | None = None @classmethod def _from_alsa(cls, tempo: _snd_seq_queue_tempo_t): @@ -245,11 +247,11 @@ class Queue: release or free on close). """ - client: Optional['SequencerClientBase'] + client: 'SequencerClientBase' | None queue_id: int - _own: Optional[bool] + _own: bool | None - def __init__(self, client: 'SequencerClientBase', queue_id: int, *, _own: bool = None): + def __init__(self, client: 'SequencerClientBase', queue_id: int, *, _own: bool | None = None): self.client = client self.queue_id = queue_id self._own = _own @@ -287,8 +289,8 @@ def close(self): else: alsa.snd_seq_set_queue_usage(handle, queue, 0) - def set_tempo(self, tempo: Union[int, QueueTempo] = None, ppq: int = None, - skew=None, skew_base=None, bpm=None): + def set_tempo(self, tempo: int | QueueTempo = None, ppq: int | None = None, + skew: int | None = None, skew_base: int | None = None, bpm: float | None = None): """Set the tempo of the queue. :param tempo: MIDI tempo – microseconds per quarter note diff --git a/setup.cfg b/setup.cfg index ce75527..3554231 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,6 @@ license = MIT classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 @@ -27,6 +26,7 @@ project_urls = packages = alsa_midi zip_safe = False +python_requires = >=3.10 setup_requires = cffi>=1.16.0 install_requires = diff --git a/tests/conftest.py b/tests/conftest.py index 3508341..4c0d6d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ import subprocess import sys from statistics import mean -from typing import Optional import pytest import pytest_asyncio @@ -42,7 +41,7 @@ class AlsaClientState: client_id: int name: str type: str - version: Optional[str] + version: str | None ports: dict[int, AlsaPortState] queues: dict[int, 'AlsaQueueState'] @@ -326,7 +325,7 @@ def aseqdump(): from alsa_midi import Address class Aseqdump: - process: Optional[subprocess.Popen] + process: subprocess.Popen | None output: list[tuple[Address, str]] port: Address diff --git a/tests/test_client.py b/tests/test_client.py index b809ff5..fc20e6f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -611,3 +611,43 @@ def test_set_client_pool_input(): client.set_client_pool_input(200) pool = client.get_client_pool() assert pool.input_pool == 200 + + +@pytest.mark.require_alsa_seq +def test_fileno(): + client = SequencerClient("test") + fd = client.fileno() + assert isinstance(fd, int) + assert fd >= 0 + client.close() + + +@pytest.mark.require_alsa_seq +def test_fileno_after_close(): + client = SequencerClient("test") + fd = client.fileno() + assert fd >= 0 + client.close() + with pytest.raises(OSError) as exc_info: + client.fileno() + assert "File descriptor not available" in str(exc_info.value) + + +@pytest.mark.require_alsa_seq +def test_fileno_async_client(): + client = AsyncSequencerClient("test") + fd = client.fileno() + assert isinstance(fd, int) + assert fd >= 0 + client.close() + + +@pytest.mark.require_alsa_seq +def test_fileno_async_client_after_close(): + client = AsyncSequencerClient("test") + fd = client.fileno() + assert fd >= 0 + client.close() + with pytest.raises(OSError) as exc_info: + client.fileno() + assert "File descriptor not available" in str(exc_info.value) diff --git a/tests/test_sequencer_address.py b/tests/test_sequencer_address.py index 6860738..994f963 100644 --- a/tests/test_sequencer_address.py +++ b/tests/test_sequencer_address.py @@ -1,16 +1,3 @@ -# flake8: noqa - -import argparse -import sys -import time - -from collections import namedtuple # isort: skip - -import alsa_midi # isort: skip -from alsa_midi import Address # isort: skip - - - """Tests SequencerClientBase.resolve_address() Tests many permutations of "numeric-or-named-client:numeric-or-named-port", @@ -28,17 +15,29 @@ """ +import argparse +import sys +import time + +from collections import namedtuple # isort: skip +import pytest # isort: skip + +import alsa_midi # isort: skip +from alsa_midi import Address # isort: skip + + verbose = False -clients_ports = {} # have to store ports somewhere else go out of scope - # and get deleted on return from create_ports() +# Have to store ports somewhere else go out of scope +# and get deleted on return from create_ports() +clients_ports = {} + test_count = 0 # total number of permutations tested pass_count = 0 # successful tests fail_count = 0 # test which failed with expected error - # clients/ports for tests # @@ -46,7 +45,7 @@ port_spec = namedtuple('port_spec', "name req_id id substr caps") # mixture of capabilities (not used, but to make sure don't affect results) -INPUT_CAPS = alsa_midi.WRITE_PORT +INPUT_CAPS = alsa_midi.WRITE_PORT OUTPUT_CAPS = alsa_midi.READ_PORT IN_OUT_CAPS = INPUT_CAPS | OUTPUT_CAPS @@ -58,52 +57,50 @@ # default sequentially-numbered ports # tests port can be accessed by substring of name sequential_ports = ( - port_spec("zero", -1, 0, False, INPUT_CAPS ), - port_spec("One", -1, 1, False, OUTPUT_CAPS ), - port_spec("two" , -1, 2, False, IN_OUT_CAPS ), - port_spec("three", -1, 3, True, OUTPUT_CAPS ), # test by substring - port_spec("FOUR" , -1, 4, False, INPUT_CAPS ), - port_spec("five" , -1, 5, False, IN_OUT_CAPS ), + port_spec("zero", -1, 0, False, INPUT_CAPS), + port_spec("One", -1, 1, False, OUTPUT_CAPS), + port_spec("two", -1, 2, False, IN_OUT_CAPS), + port_spec("three", -1, 3, True, OUTPUT_CAPS), # test by substring + port_spec("FOUR", -1, 4, False, INPUT_CAPS), + port_spec("five", -1, 5, False, IN_OUT_CAPS), ) # find above ports by substrings of names prefix_ports = ( - port_spec("er", -1, 0, False, IN_OUT_CAPS ), - port_spec("On", -1, 1, False, INPUT_CAPS ), - port_spec("wo", -1, 2, False, OUTPUT_CAPS ), + port_spec("er", -1, 0, False, IN_OUT_CAPS), + port_spec("On", -1, 1, False, INPUT_CAPS), + port_spec("wo", -1, 2, False, OUTPUT_CAPS), ) # explicit non-default, non-sequential port numbers # tests exact match of port name overrides substring match explicit_ports = ( - port_spec("THREE", 3, 3, False, OUTPUT_CAPS ), - port_spec("six", 6, 6, False, INPUT_CAPS ), - port_spec("Eight", 8, 8, False, IN_OUT_CAPS ), - port_spec("ele", 12, 12, False, INPUT_CAPS ), # exact match - port_spec("lev", 13, 13, False, INPUT_CAPS ), # " " - port_spec("eleven", 11, 11, False, INPUT_CAPS ), - port_spec("eve", 14, 14, False, INPUT_CAPS ), # exact match - port_spec("ven" , 15, 15, False, INPUT_CAPS ), # " " - port_spec("twenTY", 20, 20, False, OUTPUT_CAPS ), + port_spec("THREE", 3, 3, False, OUTPUT_CAPS), + port_spec("six", 6, 6, False, INPUT_CAPS), + port_spec("Eight", 8, 8, False, IN_OUT_CAPS), + port_spec("ele", 12, 12, False, INPUT_CAPS), # exact match + port_spec("lev", 13, 13, False, INPUT_CAPS), # " " + port_spec("eleven", 11, 11, False, INPUT_CAPS), + port_spec("eve", 14, 14, False, INPUT_CAPS), # exact match + port_spec("ven", 15, 15, False, INPUT_CAPS), # " " + port_spec("twenTY", 20, 20, False, OUTPUT_CAPS), ) - # at program exit if -v/--verbose, -p/--pause, or uncaught exception def print_stats(stream=sys.stdout): - global test_count, pass_count, fail_count - stream.write( "%d tests\n" - "%d passed\n" - "%d failed with expected errors \n" + global test_count, pass_count, fail_count # noqa: F824 + stream.write("%d tests\n" + "%d passed\n" + "%d failed with expected errors \n" % (test_count, pass_count, fail_count)) - # create clients/ports ports from above tuples of port_specs def create_ports(client, port_descs): - global clients_ports + global clients_ports # noqa: F824 client_name = client.get_client_info().name @@ -118,7 +115,6 @@ def create_ports(client, port_descs): clients_ports.setdefault(client_name, []).append(port) - # Parse address of various forms ('client:port', (client, port), # ((client, port),), etc. checking for success or expected error # exception. @@ -127,17 +123,17 @@ def address_parse(*args): # and expected-error-or-None assert len(args) > 3, \ - "internal test program bug: address_parse(%s) <= 3\n" % (args,) + "internal test program bug: address_parse(%s) <= 3\n" % (args,) - expected_error = args[ -1] - port_id = args[ -2] - client_id = args[ -3] - address_args = args[:-3] + expected_error = args[-1] + port_id = args[-2] + client_id = args[-3] + address_args = args[:-3] global test_count, pass_count, fail_count # input of ((a,b),c,d,e) -> ((a,b),) not (a,b) - if len(address_args) == 1: # and isinstance(address_args[0], tuple): + if len(address_args) == 1: address_args = address_args[0] is_tuple = False # for arg_name() else: @@ -148,7 +144,7 @@ def arg_name(arg): if hasattr(arg, 'client_id'): return 'Address(%s)' % str(arg) elif is_tuple: - return str(arg)[1:-1].replace(" ",'') # strip('()') strips all + return str(arg)[1:-1].replace(" ", '') # strip('()') strips all elif isinstance(arg, str): if arg.startswith("'"): return '"%s"' % arg @@ -163,7 +159,7 @@ def arg_name(arg): addr = tester.resolve_address(address_args) except Exception as e: print_stats(sys.stderr) - sys.stderr.write( "%s raises unexpected %s\n" + sys.stderr.write("%s raises unexpected %s\n" % (arg_name(address_args), type(e).__name__)) raise else: @@ -173,21 +169,21 @@ def arg_name(arg): test_count += 1 fail_count += 1 if verbose: - sys.stdout.write( "%s raises %s (as expected)\n" + sys.stdout.write("%s raises %s (as expected)\n" % (arg_name(address_args), expected_error.__name__)) - return; + return except Exception as e: print_stats(sys.stderr) - sys.stderr.write( "%s raises %s (not expected %s)\n" + sys.stderr.write("%s raises %s (not expected %s)\n" % (arg_name(address_args), type(e).__name__, expected_error.__name__)) raise else: print_stats(sys.stderr) - sys.stdout.write( "%s -> %d:%d without raising " - "expected error %s\n" + sys.stdout.write("%s -> %d:%d without raising " + "expected error %s\n" % (arg_name(address_args), addr.client_id, addr.port_id, @@ -198,19 +194,20 @@ def arg_name(arg): pass_count += 1 if addr.client_id != client_id or addr.port_id != port_id: - raise FileNotFoundError( "%s -> %s, not expected (%d,%d)" + raise FileNotFoundError("%s -> %s, not expected (%d,%d)" % (arg_name(address_args), addr, client_id, - port_id ) ) + port_id)) if verbose: - sys.stdout.write( "%s -> %d:%d" + sys.stdout.write("%s -> %d:%d" % (arg_name(address_args), addr.client_id, addr.port_id)) if expected_error is not None: - sys.stdout.write(" with expected exception: %s\n" % expect_except) + sys.stdout.write(" with expected exception: %s\n" + % expected_error) else: sys.stdout.write('\n') @@ -237,28 +234,27 @@ def do_variants(client, # client and port spec = "%s%s%s%s%s" % (quotes, client, separator, port, quotes) - address_parse( spec, client_id, port_id, expected_error) - address_parse( client, port, client_id, port_id, expected_error) + address_parse(spec, client_id, port_id, expected_error) + address_parse(client, port, client_id, port_id, expected_error) address_parse((client, port), client_id, port_id, expected_error) - address_parse( client_id, port, client_id, port_id, expected_error) + address_parse(client_id, port, client_id, port_id, expected_error) address_parse((client_id, port), client_id, port_id, expected_error) - address_parse( client, port_id, client_id, port_id, expected_error) + address_parse(client, port_id, client_id, port_id, expected_error) address_parse((client, port_id), client_id, port_id, expected_error) - address_parse( client_id, port_id, client_id, port_id, expected_error) + address_parse(client_id, port_id, client_id, port_id, expected_error) address_parse((client_id, port_id), client_id, port_id, expected_error) - if isinstance(client_id, int) and isinstance(port, int) : + if isinstance(client_id, int) and isinstance(port, int): address_parse(Address(client_id, port_id), client_id, port_id, expected_error) - # test all ports of client def do_addresses(client_name, client_id, ports): if verbose: - sys.stdout.write( "# --- do_addresses(%s, %s) ---\n" + sys.stdout.write("# --- do_addresses(%s, %s) ---\n" % (client_name, client_id)) # Not a rigorous test (ports could be explicit with first @@ -269,14 +265,15 @@ def do_addresses(client_name, client_id, ports): has_port_0 = (ports[0].id == 0) # permutations of address specifications - for tests_bits in range(1<<4): - if (tests_bits & 1): separator = ':' - else: separator = '.' + for tests_bits in range(1 << 4): + if (tests_bits & 1): + separator = ':' + else: + separator = '.' quotes = ("", "'", '"', None)[(tests_bits >> 1) & 0x3] - if quotes is None: continue - - no_ports = not(tests_bits << 3) + if quotes is None: + continue for port in ports: do_variants(client_name, @@ -289,7 +286,7 @@ def do_addresses(client_name, client_id, ports): has_port_0) # test that flagged ports can be accessed via substring - for (port_ndx,port) in enumerate(ports): + for (port_ndx, port) in enumerate(ports): if port.substr: # test all 3 character substrings of port name for char_ndx in range(0, len(port.name) - 3): @@ -304,57 +301,57 @@ def do_addresses(client_name, client_id, ports): has_port_0) - # Test numeric client and port # # Note first client created by create_ports() (gets assigned i.d. 128) # must have port id 0 (explicit or default assigned) def do_simple(): - if verbose: sys.stdout.write("# --- do_simple() ---\n") - - address_parse( 128, 128, 0, None) - address_parse( '128', 128, 0, None) - address_parse( 128, 0, 128, 0, None) - address_parse( '128:0', 128, 0, None) - address_parse( (128, 0), 128, 0, None) - address_parse( Address(128, 0), 128, 0, None) + if verbose: + sys.stdout.write("# --- do_simple() ---\n") + + address_parse(128, 128, 0, None) + address_parse('128', 128, 0, None) + address_parse(128, 0, 128, 0, None) + address_parse('128:0', 128, 0, None) + address_parse((128, 0), 128, 0, None) + address_parse(Address(128, 0), 128, 0, None) address_parse((Address(128, 0),), 128, 0, TypeError) - address_parse( 128, Address(128, 0), 0, 0, TypeError) + address_parse(128, Address(128, 0), 0, 0, TypeError) address_parse((128, Address(128, 0)), 0, 0, TypeError) - # Test of client.resolve_address() overloads def do_overloads(client_name, client_id, port_name, port_id): - if verbose: sys.stdout.write("# --- do_overloads() ---\n") + if verbose: + sys.stdout.write("# --- do_overloads() ---\n") overloads = [] def test(*args): overloads.append(address_parse(*(args + (client_id, port_id, None)))) - test( client_id , port_id ) # 2 ints - test((client_id , port_id )) # tuple, same - test( client_id , port_name) # int, str - test((client_id , port_name)) # tuple, same - test( client_name, port_id ) # str, int - test((client_name, port_id )) # tuple, same - test( client_name, port_name) # 2 str + test(client_id, port_id) # 2 ints + test((client_id, port_id)) # tuple, same + test(client_id, port_name) # int, str + test((client_id, port_name)) # tuple, same + test(client_name, port_id) # str, int + test((client_name, port_id)) # tuple, same + test(client_name, port_name) # 2 str test((client_name, port_name)) # tuple, same - test('%d:%d' % (client_id , port_id )) # int:int - test('%d:%s' % (client_id , port_name)) # int:str - test('%s:%d' % (client_name, port_id )) # str:iin - test('%s:%s' % (client_name, port_name)) # str:str + test('%d:%d' % (client_id, port_id)) # int:int + test('%d:%s' % (client_id, port_name)) # int:str + test('%s:%d' % (client_name, port_id)) # str:iin + test('%s:%s' % (client_name, port_name)) # str:str overloads.append(tester.resolve_address(overloads[0])) # address - assert(all(addr == overloads[0] for addr in overloads)) - + assert (all(addr == overloads[0] for addr in overloads)) def do_bad_arguments(): - if verbose: sys.stdout.write("# --- do_bad_arguments() ---\n") + if verbose: + sys.stdout.write("# --- do_bad_arguments() ---\n") address_parse(3.14, 0, 0, TypeError) address_parse([], 0, 0, TypeError) @@ -363,42 +360,42 @@ def do_bad_arguments(): address_parse((128,), 0, 0, TypeError) address_parse(('explicit',), 0, 0, TypeError) address_parse((3.14,), 0, 0, TypeError) - address_parse((Address(128,0),), 0, 0, TypeError) + address_parse((Address(128, 0),), 0, 0, TypeError) address_parse(('explicit', None), 0, 0, FileNotFoundError) address_parse((3.14, None), 0, 0, TypeError) - address_parse((Address(128,0), None), 0, 0, TypeError) + address_parse((Address(128, 0), None), 0, 0, TypeError) - address_parse( 128, 2, 3, 0, 0, TypeError) + address_parse(128, 2, 3, 0, 0, TypeError) address_parse((128, 2, 3), 0, 0, TypeError) - address_parse( 128, 2, 3, 4, 0, 0, TypeError) + address_parse(128, 2, 3, 4, 0, 0, TypeError) address_parse((128, 2, 3, 4), 0, 0, TypeError) - address_parse( 3.14, 128, 0, 0, TypeError) + address_parse(3.14, 128, 0, 0, TypeError) address_parse((3.14, 128), 0, 0, TypeError) - address_parse( 128, 3.14, 0, 0, TypeError) + address_parse(128, 3.14, 0, 0, TypeError) address_parse((128, 3.14), 0, 0, TypeError) - address_parse( 128, 'explicit', 0, 0, FileNotFoundError) + address_parse(128, 'explicit', 0, 0, FileNotFoundError) address_parse((128, 'explicit'), 0, 0, FileNotFoundError) - address_parse( 'explicit', 128, 0, 0, FileNotFoundError) + address_parse('explicit', 128, 0, 0, FileNotFoundError) address_parse(('explicit', 128), 0, 0, FileNotFoundError) - address_parse( 'explicit', 3.14, 0, 0, TypeError) + address_parse('explicit', 3.14, 0, 0, TypeError) address_parse(('explicit', 3.14), 0, 0, TypeError) - address_parse( 3.14, 'explicit', 0, 0, TypeError) + address_parse(3.14, 'explicit', 0, 0, TypeError) address_parse((3.14, 'explicit'), 0, 0, TypeError) - address_parse( Address(128, 0), 128, 0, 0, TypeError) + address_parse(Address(128, 0), 128, 0, 0, TypeError) address_parse((Address(128, 0)), 128, 0, 0, TypeError) - address_parse( 128, Address(128, 0), 0, 0, TypeError) + address_parse(128, Address(128, 0), 0, 0, TypeError) address_parse((128, Address(128, 0)), 0, 0, TypeError) - def do_bad_clients_ports(): - if verbose: sys.stdout.write("# --- do_bad_clients_ports() ---\n") + if verbose: + sys.stdout.write("# --- do_bad_clients_ports() ---\n") for client in (-1, 256, 'bad_client'): for port in (0, -1, 256, 'bad_port'): do_variants(client, @@ -411,7 +408,6 @@ def do_bad_clients_ports(): has_port_0=False) - def do_good_client_bad_ports(client_name, client_id): for port in (-1, 256, 'bad_port'): for separator in (':', '.'): @@ -427,20 +423,20 @@ def do_good_client_bad_ports(client_name, client_id): client_id, 0, TypeError) - address_parse( client_name, port, client_id, 0, FileNotFoundError) + address_parse(client_name, port, client_id, 0, FileNotFoundError) address_parse((client_name, port), client_id, 0, FileNotFoundError) - address_parse( client_id, port, client_id, 0, FileNotFoundError) + address_parse(client_id, port, client_id, 0, FileNotFoundError) address_parse((client_id, port), client_id, 0, FileNotFoundError) - +@pytest.mark.require_alsa_seq def test_sequencer_address(): global tester - tester = alsa_midi.SequencerClient("tester" ) # all tests use this + tester = alsa_midi.SequencerClient("tester") # all tests use this sequential = alsa_midi.SequencerClient("sequential") - prefix = alsa_midi.SequencerClient("seq" ) - explicit = alsa_midi.SequencerClient("explicit" ) + prefix = alsa_midi.SequencerClient("seq") + explicit = alsa_midi.SequencerClient("explicit") # First one gets assigned client id 128, must have port # with id 0 for do_simple() @@ -449,10 +445,10 @@ def test_sequencer_address(): prfx_id = prefix .get_client_info().client_id expl_id = explicit .get_client_info().client_id - create_ports(tester , tester_ports ) + create_ports(tester, tester_ports) create_ports(sequential, sequential_ports) - create_ports(prefix , prefix_ports ) - create_ports(explicit , explicit_ports ) + create_ports(prefix, prefix_ports) + create_ports(explicit, explicit_ports) # sometimes delay before clients/ports actually exist # particularly if run in tox/pytest environment @@ -466,10 +462,9 @@ def test_sequencer_address(): do_addresses("tester", test_id, tester_ports) do_addresses("sequential", seqn_id, sequential_ports) - do_addresses("seq", prfx_id, prefix_ports) # client,port substrings + do_addresses("seq", prfx_id, prefix_ports) # client,port substrings do_addresses("explicit", expl_id, explicit_ports) - do_addresses("expl", expl_id, explicit_ports) # substrings - + do_addresses("expl", expl_id, explicit_ports) # substrings if __name__ == '__main__': @@ -492,9 +487,10 @@ def test_sequencer_address(): if opts.pause: sys.stdout.write("`aconnect -oil` in separate shell for " - "clients+ports, type ^C here to exit\n") + "clients+ports, type ^C here to exit\n") try: - while True: pass + while True: + pass except KeyboardInterrupt: sys.stdout.write("exiting\n") diff --git a/tox.ini b/tox.ini index c4caf40..9e7c555 100644 --- a/tox.ini +++ b/tox.ini @@ -44,4 +44,3 @@ commands = [flake8] max-line-length=99 -exclude = tests/test_sequencer_address.py diff --git a/utils/aconnect.py b/utils/aconnect.py index ef51479..dcd65cb 100755 --- a/utils/aconnect.py +++ b/utils/aconnect.py @@ -3,236 +3,482 @@ import argparse import errno import os -from pprint import pprint # DEBUG import re import shutil import sys import textwrap -from alsa_midi import SequencerClient, \ - Address, \ - ALSAError, \ - ClientType, \ - SubscriptionQueryType - -program_name = None -clients = {} -ports = {} - - - -def direction(raw): - # The perfection that is ALSA sequencer - if raw.name == 'INPUT' : return 'OUTPUT' - elif raw.name == 'OUTPUT': return 'INPUT' - else: return raw.name +from alsa_midi import SequencerClient, \ + Address, \ + ALSAError, \ + ClientInfo, \ + ClientType, \ + PortDirection, \ + PortInfo, \ + PortType, \ + SubscriptionQueryType + +program_name: str = "" +aconnect: SequencerClient +clients: dict[int, ClientInfo] = {} # client_id: CLientInfo +ports: dict[int, list[PortInfo]] = {} # client_id: [PortInfos] +indenter: textwrap.TextWrapper + + +def direction(raw: PortDirection) -> str: + "ALSA names for input vs output are backwards." + if raw.name == 'INPUT': + return 'OUTPUT' + elif raw.name == 'OUTPUT': + return 'INPUT' + elif raw.name is None: + return '???' + else: + return raw.name -def indent(text): - indenter.subsequent_indent = " " * re.search(r"^.*'.*'\s*", text).span()[1] +def indent(text: str) -> None: + "Indent lines after first line to past ID and padded name." + dot_star = re.search(r"^.*'.*'\s*", text) + if dot_star is not None: + indenter.subsequent_indent = " " * dot_star.span()[1] sys.stdout.write('%s\n' % indenter.fill(text)) - -def init(term_width): +def init(term_width: int) -> None: + "Inititialize global client and textwrap object." global program_name, aconnect, indenter aconnect = SequencerClient(program_name) - width = term_width \ - if term_width > 0 \ - else shutil.get_terminal_size().columns - 5 + width = term_width \ + if term_width > 0 \ + else shutil.get_terminal_size().columns - 5 indenter = textwrap.TextWrapper(width=width, break_long_words=False) +def get_clients(client_list: list[ClientInfo] | None = None) -> None: + """Populate clients dict with provided or function-fetched + list of ClientInfos. + """ -def get_clients(client_list=None): - # Need include_system for client:port names of connections - # from non-system to system clients. - client_list = aconnect.list_clients(include_system=True, - include_kernel=True) + if client_list is None: # caller may have already fetched + # Need include_system for client:port names of connections + # from non-system to system clients. + client_list = aconnect.list_clients(include_system=True, + include_kernel=True) for client in client_list: if client.name != program_name: clients[client.client_id] = client - -def get_ports(): +def get_ports() -> None: + "Populate ports dict" global aconnect, ports # Need ports for system clients in to get client:port names # for non-system ports connected to system ones. port_list = aconnect.list_ports(include_system=True, include_kernel=True, - type=None, + type=PortType.ANY, only_connectable=False) for port in port_list: - ports.setdefault(port.client_id, {}) - ports[port.client_id][port.port_id] = port - - - -def print_connections(client, address): - for sub in aconnect.list_port_subscribers(address): - # other = sub.addr + ports.setdefault(port.client_id, []) + ports[port.client_id].append(port) + + +def print_client(client: ClientInfo, + client_id: int, + max_name_len: int = 0, + max_type_len: int = 0, + client_only: bool = False, + inputs_only: bool = False, + outputs_only: bool = False, + full_info: bool = False): + """Print info for single client, with/without port and or full + information. + + :param client: client to print + :param client_id: ID of client + :param max_name_len: maximum length of port (normal) or + client (clients_only) names + :param max_name_len: maximum length of client type strings + :param client_only: do not print client's ports + :param inputs_only: print only input ports + :param outputs_only: print only output ports + :param full_info: print full client and port info + + :return: Total number of connections for all ports of client + """ + + global ports + + if not client_only and not ports: + get_ports() + + # If all ports are of same type report on client line + port_type = None + if ports and client_id in ports and len(ports[client_id]) > 0: + port_type_0 = ports[client_id][0].type + if all(port.type == port_type_0 for port in ports[client_id]): + port_type = port_type_0 + + client_type_name = "" if client.type is None else client.type.name + + if client_only: + client_str = f"{client_id:>3}: {client.name!r:<{max_name_len+4}}" + if full_info: + client_str += f"{client_type_name:<{max_type_len}} " \ + + f"ports={client.num_ports} " \ + + f"pid={client.pid}" + else: + client_str += f"{client_type_name:<{max_type_len}} " \ + + f"ports={client.num_ports:<3} " \ + f"pid={client.pid}" + else: + client_str = f"{client_id}: '{client.name}' " \ + f"{client_type_name} pid={client.pid}" + + if port_type is not None: + client_str += f" ports={port_type.name}" + + if full_info: + indent(f"{client_str} " + f"event_filter={client.event_filter} " + f"broadcast_filter={client.broadcast_filter} " + f"error_bounce={client.error_bounce} " + f"event_lost={client.event_lost} " + f"card_id={client.card_id} " + f"num_ports={client.num_ports}") + else: + indent(f"{client_str}\n") + + if client_only: + return 0 + + num_connections = 0 + if ports and client_id in ports: + for port in ports[client_id]: + if inputs_only and port.direction == PortDirection.INPUT: # sic + continue + if outputs_only and port.direction == PortDirection.OUTPUT: # sic + continue + port_str = (f" {port.port_id:>3}: " + f"{port.name!r:<{max_name_len+3}} " + f"{direction(port.direction)}") + if port_type is None: + port_str += f" {port.type.name}" + if full_info: + indent(f"{port_str} " + f"capability={port.capability.name} " + f"client_id={port.client_id} " + f"midi_channels={port.midi_channels} " + f"midi_voices={port.midi_voices} " + f"read_use={port.read_use} " + f"synth_voices={port.synth_voices} " + f"timestamp_queue_id= " + f"{port.timestamp_queue_id} " + f"timestamp_real={port.timestamp_real} " + f"timestamping={port.timestamping} " + f"write_use={port.write_use}") + else: + sys.stdout.write(f"{port_str}\n") + if port.port_id is not None: + num_connections += print_connections(Address(port.client_id, + port.port_id), + full_info) + return num_connections + + +def print_connections(address: Address, + full_info: bool = False) -> int: + """Print port's connections. + :return: Number of connections + """ + global indenter + + subs = aconnect.list_port_subscribers(address) + for sub in subs: sub_clnt = clients[sub.addr.client_id].name - sub_port = ports[sub.addr.client_id][sub.addr.port_id].name + sub_port = next((port + for port + in ports[sub.addr.client_id] + if port.port_id == sub.addr.port_id), + None) + sub_port_name = "???" if sub_port is None else sub_port.name if sub.type == SubscriptionQueryType.READ: - sys.stdout.write( "\tconnected to " - f"'{sub_clnt}:{sub_port}'" - f"({sub.addr})\n") + text = ("\tconnected to " + f"'{sub_clnt}:{sub_port_name}'" + f"({sub.addr})") + indenter.subsequent_indent = " " * 21 # len("\tconnected to ") elif sub.type == SubscriptionQueryType.WRITE: - sys.stdout.write( "\tconnected from " - f"'{sub_clnt}:{sub_port}'" - f"({sub.addr})\n") - - + text = ("\tconnected from " + f"'{sub_clnt}:{sub_port_name}'" + f"({sub.addr})") + indenter.subsequent_indent = " " * 23 # len("\tconnected from ") + if sub.time_update or sub.time_real: + text += f" real queue={sub.queue_id}" + if full_info: + text += (f" root={sub.root}" + f" index={sub.index}" + f" num_subs={sub.num_subs}" + f" addr={sub.addr}" + f" queue_id={sub.queue_id}" + f" exclusive={bool(sub.exclusive)}" + f" time_update={bool(sub.time_update)}" + f" time_real={bool(sub.time_real)}") + sys.stdout.write('%s\n' % indenter.fill(text)) + return len(subs) + + +def print_clients( + client_list: list[str] = [], + clients_only: bool = False, + kernel_clients: bool = False, + pipewire_clients: bool = False, + inputs_only: bool = False, + outputs_only: bool = False, + full_info: bool = False # print additional client+port info +) -> int: + """Print all clients. See print_client(). + :param client_list: list of clients to print (or all if empty) + :param clients_only: don't print port info + :param kernel_clients: include system and kernel clients + :param pipewire_clients: include pipewire clients + :inputs_only: only input ports + :outputs_only: only output ports + :param full_info: print additional client+port info + + :return: Total number of connections for all ports of all clients + """ -def report(clients_only = False, - all_clients = False, - pipewire_clients = False, - full_info = False): global aconnect, clients, ports if not clients: get_clients() - if clients_only: - ports = None - max_name_len = max([len(client.name) for client in clients.values()]) - max_type_len = max([ len(client.type.name) - for client - in clients.values()]) + if not clients_only: + get_ports() + + if client_list: + client_ids = [] + for client_arg in client_list: + colons = client_arg.replace('.', ':') + splits = colons.split(':') + assert len(splits) <= 2, \ + f"Bad client or client:port \"{client_arg}\"" + client_arg = splits[0] + client_id: int | None + if client_arg.isdigit(): + client_id = int(client_arg) + else: + client_id = next((client_id + for (client_id, client_info) + in clients.items() + if client_arg in client_info.name), + None) + assert client_id is not None, \ + f"Can't find client \"{client_arg}\"" + client_ids.append(client_id) else: - if not ports: - get_ports() - max_port_len = max([ len(port.name) - for portval in ports.values() - for port - in portval.values() - if not ( not all_clients - and - clients[port.client_id].type - == ClientType.KERNEL) - ]) - - for (client_id, client) in clients.items(): - # if client.name == program_name: - # continue - - if client.type == ClientType.KERNEL and not all_clients: + client_ids = list(clients.keys()) + + displayed_clients = [] + max_name_len = 0 + max_type_len = 0 + for client_id in client_ids: + client = clients[client_id] + if client.type == ClientType.KERNEL and not kernel_clients: continue - if 'PipeWire' in client.name and not pipewire_clients: continue - - # If all ports are of same type report on client line - port_type = None - if ports and client_id in ports and len(ports[client_id].values()) > 0: - port_type_0 = list(ports[client_id].values())[0].type - # pprint([port.type for port in ports[client_id].values()]) - if all( port.type == port_type_0 - for port - in ports[client_id].values()): - port_type = port_type_0 - + displayed_clients.append(client_id) + client_type_name = "" if client.type is None else client.type.name + if len(client_type_name) > max_type_len: + max_type_len = len(client_type_name) if clients_only: - client_str = f"{client_id:>3}: {client.name!r:<{max_name_len+4}}" - if full_info: - client_str += f"{client.type.name} " \ - + f"ports={client.num_ports} " \ - + f"pid={client.pid}" - else: - client_str += f"{client.type.name:<{max_type_len}} " \ - + f"ports={client.num_ports:<3} " \ -"pid={client.pid}" + if len(client.name) > max_name_len: + max_name_len = len(client.name) else: - client_str = f"{client_id}: '{client.name}' " \ - f"{client.type.name} pid={client.pid}" + max_client_port_len = max([len(port.name) + for port + in ports[client_id]]) + if max_client_port_len > max_name_len: + max_name_len = max_client_port_len + + if not displayed_clients: + return 0 + + num_connections = 0 + for client_id in displayed_clients: + num_connections += print_client(clients[client_id], + client_id, + max_name_len, + max_type_len, + clients_only, + inputs_only, + outputs_only, + full_info) + return num_connections + + +def connect(clients_ports: list[str], + exclusive: bool = False, + time_real: bool = False, + time_update: bool = False, + queue: int = 0) -> None: + "Connect one client:port to or from another_client:another_port" - if port_type is not None: - client_str += f" ports={port_type.name}" + global aconnect - if full_info: - indent(f"{client_str} " - f"event_filter={client.event_filter} " - f"broadcast_filter={client.broadcast_filter} " - f"error_bounce={client.error_bounce} " - f"event_lost={client.event_lost} " - f"card_id={client.card_id} " - f"num_ports={client.num_ports}") + assert len(clients_ports) == 2, \ + "Must have exactly two clients:ports to connect" + + source = clients_ports[0] + sink = clients_ports[1] + + try: + source_addr = aconnect.resolve_address(source) + sink_addr = aconnect.resolve_address(sink) + except ALSAError as e: + raise ALSAError("Bad or non-existent source and/or sink", errno.ENOENT) + + try: + aconnect.subscribe_port(source_addr, + sink_addr, + queue=queue, + exclusive=exclusive, + time_update=time_update, + time_real=time_real) + except ALSAError as e: + raise ALSAError(f"Can't connect {source}({source_addr}) -> " + f"{sink}({sink_addr}) (not output->input? " + "already connected?)", + errno.ENOENT) + + +def disconnect(clients_ports: list[str], + no_safety: bool) -> None: + """"Disconnect a single connection between two ports, all connections + to/from a single port, all connections to/from all of a single + client's ports, or all connections. + + :param no_safety: Do not print connections to be removed and input + user confirmation (not used for single connection + between two ports case). + + """ + + if not clients_ports: + remove_all_connections(no_safety) + elif len(clients_ports) == 1: + if '.' in clients_ports[0] or ':' in clients_ports[0]: + remove_port_connections(clients_ports[0], no_safety) else: - indent(f"{client_str}\n") - - if ports and client_id in ports: - for port in ports[client_id].values(): - port_str = (f" {port.port_id:>3}: " - f"{port.name!r:<{max_port_len+3}} " - f"{direction(port.direction)}") - if port_type is None: - port_str += f" {port_type.name}" - if full_info: - indent(f"{port_str} " - f"capability={port.capability.name} " - f"client_id={port.client_id} " - f"midi_channels={port.midi_channels} " - f"midi_voices={port.midi_voices} " - f"port_specified={port.port_specified} " - f"read_use={port.read_use} " - f"synth_voices={port.synth_voices} " - f"timestamp_queue_id= " - f"{port.timestamp_queue_id} " - f"timestamp_real={port.timestamp_real} " - f"timestamping={port.timestamping} " - f"write_use={port.write_use}") - else: - sys.stdout.write(f"{port_str}\n") - print_connections(client, Address(port.client_id, port.port_id)) - - - -def connect_disconnect(disconnect, source, sink, exclusive=None, real=None): - "Connect or disconnect client:port to/from another_client:another_port" + remove_client_connections(clients_ports[0], no_safety) + elif len(clients_ports) == 2: + remove_connection(*clients_ports) + else: + raise TypeError("-d,--disconnect requires 0, 1, or 2 arguments.") + + +def remove_connection(source: str | Address, + sink: str | Address): + "Disconnect single clientA:portA -> clientB:portB connection." global aconnect - source_addr = aconnect.resolve_address(source) - sink_addr = aconnect.resolve_address(sink ) + try: + source_addr = aconnect.resolve_address(source) + sink_addr = aconnect.resolve_address(sink) + except ALSAError as e: + raise ALSAError("Bad or non-existent source and/or sink", errno.EINVAL) - if disconnect: - try: - aconnect.unsubscribe_port(source_addr, sink_addr) - except ALSAError as e: - raise ALSAError(f"Can't disconnect {source} -> {sink} " - "(not connected?)", - errno.EINVAL) - else: - try: - aconnect.subscribe_port(source_addr, - sink_addr, - exclusive=opts.exclusive, - time_update=(opts.real is not None), - time_real=opts.real) - except ALSAError as e: - raise ALSAError(f"Can't connect {source} -> {sink} " - "(input->output?) (or already connected?)", - errno.ENOENT) + try: + aconnect.unsubscribe_port(source_addr, sink_addr) + except ALSAError as e: + raise ALSAError(f"Can't disconnect {source}->{sink} " + f"(not connected? or reversed {sink}->" + f"{source}?)", + errno.EINVAL) + + +def remove_port_connections(client_port: str, + no_safety: bool) -> None: + """"Remove all connections to/from one client:port + + :param no_safety: Do not print connections to be removed and input + user confirmation. + """ + + global aconnect, clients, ports + + try: + address = aconnect.resolve_address(client_port) + except Exception as e: + raise ALSAError(f"Bad or non-existent client:port '{client_port}'", + errno.EINVAL) + + if not no_safety: + if not clients: + get_clients() + if not ports: + get_ports() + + max_port_len = max([len(port.name) + for port + in ports[address.client_id]]) + + print_client(clients[address.client_id], + address.client_id, + client_only=True) + + port = next((port + for port + in ports[address.client_id] + if port.port_id == address.port_id), + None) + assert port is not None, \ + f"{program_name}: Internal error, can't find port." + + sys.stdout.write(f" {port.port_id:>3}: " + f"{port.name!r:<{max_port_len+3}} " + f"{direction(port.direction)}\n") + count = print_connections(address) + + if count == 0: + sys.stdout.write("No connections to delete\n") + return + + if input(f"Remove {count} connection(s)? n/Y: ") != 'Y': + sys.stdout.write("Aborting\n") + return + for sub in aconnect.list_port_subscribers(address): + if sub.type == SubscriptionQueryType.READ: + remove_connection(address, sub.addr) + elif sub.type == SubscriptionQueryType.WRITE: + remove_connection(sub.addr, address) -def disconnect(client, no_safety): - "Disconnect all ports of one client" +def remove_client_connections(client: str | int, + no_safety: bool) -> None: + """Remove all connections to/from all of one client's ports + + :param no_safety: Do not print connections to be removed and input + user confirmation. + """ global aconnect, clients, ports # check that is just client (no port) if not isinstance(client, int): assert not any(char in client for char in '.:'), \ - "Specify client, not client:port" + "Specify client, not client:port" # Can't use aconnect.resolve_address() because no port specified # and client might not have default port 0. if isinstance(client, int): - client_id = client + client_id = client client_name = str(client) else: client_name = client @@ -243,62 +489,71 @@ def disconnect(client, no_safety): client_list = aconnect.list_clients(include_system=True, include_kernel=True) - found_id = None + found_id = None for search in client_list: if client_id is not None and search.client_id == client_id: + found_client = search found_id = search.client_id break - elif search.name == client_name: + elif search.name.startswith(client_name): + found_client = search found_id = search.client_id break assert found_id is not None, \ - f"No client named '{client_name}'" \ - if client_id is None \ - else f"No client number {client_id}" + f"No client named '{client_name}'" \ + if client_id is None \ + else f"No client number {client_id}" - if not clients: get_clients(client_list) - if not ports: get_ports() + if not clients: + get_clients(client_list) + if not ports: + get_ports() - if found_id == 0 \ - or clients[found_id].type == ClientType.KERNEL \ + if found_id == 0 \ + or clients[found_id].type == ClientType.KERNEL \ or 'PipeWire' in clients[found_id].name: raise ALSAError(f"Can't disconnect client {found_id}'s ports " - "(is system, KERNEL, and/or PipeWire)", + "(is system, KERNEL, and/or PipeWire)", errno.EINVAL) if not no_safety: - max_port_len = max([ len(port.name) - for port - in ports[found_id].values()]) - for port in ports[found_id].values(): - sys.stdout.write(f" {port.port_id:>3}: " - f"{port.name!r:<{max_port_len+3}} " - f"{direction(port.direction)}\n") - print_connections(clients[found_id], - Address(found_id, port.port_id)) - if input("Remove all (if any) connections? n/Y: ") != 'Y': + max_port_len = max([len(port.name) for port in ports[found_id]]) + count = print_client(found_client, found_id, max_port_len) + + if count == 0: + sys.stdout.write("No connections to delete\n") + return + + if input(f"Remove all {count} connection(s)? n/Y: ") != 'Y': sys.stdout.write("Aborting\n") return - for port in ports[found_id].values(): + for port in ports[found_id]: + if port.port_id is None: + continue address = Address(found_id, port.port_id) for sub in aconnect.list_port_subscribers(address): - sub_clnt = clients[sub.addr.client_id].name - sub_port = ports[sub.addr.client_id][sub.addr.port_id].name if sub.type == SubscriptionQueryType.READ: - connect_disconnect(True, address, sub.addr) + remove_connection(address, sub.addr) elif sub.type == SubscriptionQueryType.WRITE: - connect_disconnect(True, sub.addr, address) + remove_connection(sub.addr, address) +def remove_all_connections(no_safety: bool) -> None: + """"Remove all connections between all ports of all clients. + :param no_safety: Do not print connections to be removed and input + user confirmation. + """ -def remove_all(no_safety): global aconnect, program_name if not no_safety: - report() - if input("Remove all (if any) connections? n/Y: ") != 'Y': + count = print_clients() + if count == 0: + sys.stdout.write("No connections to delete\n") + return + if input(f"Remove all {count} connection(s)? n/Y: ") != 'Y': sys.stdout.write("Aborting\n") return @@ -306,99 +561,114 @@ def remove_all(no_safety): include_kernel=False): if client.name == program_name or "PipeWire" in client.name: continue - disconnect(client.client_id, no_safety=True) - + remove_client_connections(client.client_id, no_safety=True) if __name__ == '__main__': - parser = argparse.ArgumentParser(description="List, connect, disconnect " - "ALSA ports") + DESCRIPTION = """List, connect, disconnect ALSA ports. + Specify 'client:port' or 'client.port' by numeric IDs or + names, or 'client' for no port or implicit port 0. List + clients with ports (or without if -c,--clients) if 0, 1, + or 3+ clients:ports. Connect two client:ports unless + -d,--delete or -D,--DELETE. + """ + + parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument('-d', '--disconnect', action="store_true", - help="Disconnect existing one connection or " - "all of one client's connections") + help="Disconnect one existing connection ('clientA:" + "portA clientB:PortB'), all connections to/from " + "a port ('client:port'), all connections to/from " + "a client's ports ('client'), or all connections " + "(no clients,ports specified)") parser.add_argument('-D', '--DISCONNECT', action="store_true", - help="Disconnect all of one client's connections " - "without 'Are you sure?'") - parser.add_argument('-x', '--removeall', - action='store_true', - help="Disconnect all clients/ports connections") - parser.add_argument('-X', '--REMOVEALL', - action='store_true', - help="Disconnect all clients/ports connections " - "without 'Are you sure?'") + help="Same as -d,--delete without listing connections " + "to be disconnected and requiring 'n/Y' " + "response. Implicit if -d,--delete with two " + "clients:ports.") + parser.add_argument('-e', '--exclusive', action='store_true', - help="Exclusive connection") + help="Add exclusive to connect.") + parser.add_argument('-u', '--update', + action='store_true', + help="Add time_update to connect.") parser.add_argument('-r', '--real', action='store_true', - help="Use real-time-stamp") - parser.add_argument('-t', '--tick', - action='store_false', - dest='real', - help="Use tick-time-stamp") + help="Add time_real to connect.") + parser.add_argument('-q', '--queue', + type=int, + default=0, + help="Specify queue ID to connect.") + parser.add_argument('-l', '--list', + action='store_true', + help="List clients (implicit if 0, 1, or 3+ clients).") parser.add_argument('-c', '--clients', action='store_true', - help="list only clients (no port info)") - parser.add_argument('-i', '--input', + help="List only clients (no ports)") + parser.add_argument('-i', '--inputs', action='store_true', - help="list input (readable) ports") - parser.add_argument('-o', '--output', + help="List only input (readable) ports") + parser.add_argument('-o', '--outputs', action='store_true', - help="list output (writable) ports") - parser.add_argument('-a', '--all', + help="List only output (writable) ports") + parser.add_argument('-k', '--kernel', action='store_true', - help="include kernel/system clients and ports") + help="Include kernel/system clients") parser.add_argument('-p', '--pipewire', action='store_true', - help="include pipewire clients and ports") + help="Include pipewire clients") + parser.add_argument('-a', '--all', + action='store_true', + help="Include all (-k,-p) clients") parser.add_argument('-f', '--full', action='store_true', - help="full client and port info") + help="Full client and port info") parser.add_argument('-w', '--width', type=int, default=0, - help="override terminal width (or provide if " + help="Override terminal width (or provide if " "not tty) if not 0") - parser.add_argument('source', - nargs='?', - metavar='CLIENT:PORT', - help="Source side of the connection") - parser.add_argument('dest', - nargs='?', - metavar='CLIENT:PORT', - help="Sink side of the connection") + parser.add_argument('addresses', + nargs='*', + metavar="CLIENT[:PORT] ...", + help="Clients or client:ports") try: program_name = os.path.basename(sys.argv[0]) - opts = parser.parse_args() - # pprint(opts) # DEBUG - if opts.DISCONNECT: opts.disconnect = True - if opts.REMOVEALL: opts.removeall = True + opts = parser.parse_args() + if opts.DISCONNECT: + opts.disconnect = True + if opts.all: + opts.kernel = True + opts.pipewire = True init(opts.width) - if opts.source: - if opts.dest: - connect_disconnect(opts.disconnect, - opts.source, - opts.dest, - opts.exclusive, - opts.real) - else: - disconnect(opts.source, opts.DISCONNECT) - elif opts.removeall: - remove_all(opts.REMOVEALL) + if opts.disconnect: + disconnect(opts.addresses, opts.DISCONNECT) + elif len(opts.addresses) == 2 and not opts.list: + connect(opts.addresses, + opts.exclusive, + opts.real, + opts.update, + opts.queue) else: - report(opts.clients, opts.all, opts.pipewire, opts.full) - sys.stdout.write(f"'{program_name} -h' for options\n") + print_clients(opts.addresses, + opts.clients, + opts.kernel, + opts.pipewire, + opts.inputs, + opts.outputs, + opts.full) except Exception as e: sys.stderr.write(f"{e}\n") + sys.stderr.write(f"'{program_name} -h' for help and options\n") sys.exit(1) sys.exit(0) From c001ba92075a916e6111bbcbeda2b798447c02ca Mon Sep 17 00:00:00 2001 From: thanks4opensource <31165662+thanks4opensource@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:03:48 -0800 Subject: [PATCH 4/4] utils/aconnect.py and supporting changes Adds new aconnect.py, full replacement for alsa-utils/.../aconnect.c, including SequencerClientBase.resolve_address(), and output and usability improvments. New SequencerClientBase::list_clients(), port.py class PortDirection, and other additions to support aconnect.py functionality. Small updates to tests/test_port_query.py for new SequencerClientBase::list_clients() parameter. Incorporates large Jajcus commit 716751 (typing modernizations). Reverted examples/connect.py to original without resolve_address(). Update: - Reverted additions of PortDirection, direction, snd_seq_port_info_get_direction(), PortDirection(), snd_seq_port_info_set_direction(), SND_SEQ_PORT_DIR_*, "PortDirection" as per request to allow building with alsa-lib < v1.2.10. - Replaced with `PortCaps.READ` and `.WRITE` for determining port input/output/bidirectional. - Removed redundant `global` declarations and unused `Exception as e` variables. - Improved error messages in aconnect.py --- alsa_midi/__init__.py | 4 +-- alsa_midi/_ffi_defs.py | 7 ---- alsa_midi/port.py | 15 +-------- utils/aconnect.py | 72 +++++++++++++++++++----------------------- 4 files changed, 35 insertions(+), 63 deletions(-) diff --git a/alsa_midi/__init__.py b/alsa_midi/__init__.py index d288428..0914ff3 100644 --- a/alsa_midi/__init__.py +++ b/alsa_midi/__init__.py @@ -19,7 +19,7 @@ TimeSignatureEvent, TuneRequestEvent, UserVar0Event, UserVar1Event, UserVar2Event, UserVar3Event, UserVar4Event) from .exceptions import ALSAError, Error, StateError -from .port import READ_PORT, RW_PORT, WRITE_PORT, Port, PortCaps, PortDirection, PortInfo, PortType +from .port import READ_PORT, RW_PORT, WRITE_PORT, Port, PortCaps, PortInfo, PortType from .queue import Queue, QueueInfo, QueueStatus, QueueTempo, QueueTimer, QueueTimerType __all__ = [ @@ -29,7 +29,7 @@ "RemoveCondition", "RealTime", "EventType", "EventFlags", "Event", "MidiBytesEvent", "Error", "StateError", "ALSAError", - "Port", "PortCaps", "PortDirection", "PortType", "PortInfo", + "Port", "PortCaps", "PortType", "PortInfo", "READ_PORT", "WRITE_PORT", "RW_PORT", "Queue", "QueueInfo", "QueueStatus", "QueueTempo", "QueueTimer", "QueueTimerType", "alsa", "ffi", diff --git a/alsa_midi/_ffi_defs.py b/alsa_midi/_ffi_defs.py index 9db4c38..cde0598 100644 --- a/alsa_midi/_ffi_defs.py +++ b/alsa_midi/_ffi_defs.py @@ -563,11 +563,6 @@ #define SND_SEQ_PORT_SYSTEM_TIMER 0 #define SND_SEQ_PORT_SYSTEM_ANNOUNCE 1 -#define SND_SEQ_PORT_DIR_UNKNOWN 0 -#define SND_SEQ_PORT_DIR_INPUT 1 -#define SND_SEQ_PORT_DIR_OUTPUT 2 -#define SND_SEQ_PORT_DIR_BIDIRECTION 3 - #define SND_SEQ_PORT_CAP_READ 1 #define SND_SEQ_PORT_CAP_WRITE 2 @@ -605,7 +600,6 @@ int snd_seq_port_info_get_port(const snd_seq_port_info_t *info); const snd_seq_addr_t *snd_seq_port_info_get_addr(const snd_seq_port_info_t *info); const char *snd_seq_port_info_get_name(const snd_seq_port_info_t *info); -unsigned int snd_seq_port_info_get_direction(const snd_seq_port_info_t *info); unsigned int snd_seq_port_info_get_capability(const snd_seq_port_info_t *info); unsigned int snd_seq_port_info_get_type(const snd_seq_port_info_t *info); int snd_seq_port_info_get_midi_channels(const snd_seq_port_info_t *info); @@ -622,7 +616,6 @@ void snd_seq_port_info_set_port(snd_seq_port_info_t *info, int port); void snd_seq_port_info_set_addr(snd_seq_port_info_t *info, const snd_seq_addr_t *addr); void snd_seq_port_info_set_name(snd_seq_port_info_t *info, const char *name); -void snd_seq_port_info_set_direction(snd_seq_port_info_t *info, int direction); void snd_seq_port_info_set_capability(snd_seq_port_info_t *info, unsigned int capability); void snd_seq_port_info_set_type(snd_seq_port_info_t *info, unsigned int type); void snd_seq_port_info_set_midi_channels(snd_seq_port_info_t *info, int channels); diff --git a/alsa_midi/port.py b/alsa_midi/port.py index 2835aea..b30e218 100644 --- a/alsa_midi/port.py +++ b/alsa_midi/port.py @@ -15,14 +15,6 @@ # -class PortDirection(IntFlag): - """Port directions.""" - UNKNOWN = alsa.SND_SEQ_PORT_DIR_UNKNOWN - INPUT = alsa.SND_SEQ_PORT_DIR_INPUT - OUTPUT = alsa.SND_SEQ_PORT_DIR_OUTPUT - BIDIR = alsa.SND_SEQ_PORT_DIR_BIDIRECTION - - class PortCaps(IntFlag): """Port capability flags.""" _NONE = 0 @@ -210,7 +202,6 @@ class PortInfo: :ivar client_id: client identifier :ivar port_id: port identifier :ivar name: port name - :ivar direction: port direction :ivar capability: port capabilities :ivar type: port type :ivar midi_channels: number of MIDI channels @@ -232,7 +223,6 @@ class PortInfo: client_id: int = 0 port_id: Optional[int] = None name: str = "" - direction: PortDirection = PortDirection.UNKNOWN capability: PortCaps = PortCaps._NONE type: PortType = PortType.ANY midi_channels: int = 0 @@ -261,14 +251,12 @@ def __repr__(self): @classmethod def _from_alsa(cls, info: _snd_seq_port_info_t): name = ffi.string(alsa.snd_seq_port_info_get_name(info)) - direction = alsa.snd_seq_port_info_get_direction(info) caps = alsa.snd_seq_port_info_get_capability(info) p_type = alsa.snd_seq_port_info_get_type(info) return cls( client_id=alsa.snd_seq_port_info_get_client(info), port_id=alsa.snd_seq_port_info_get_port(info), name=name.decode(), - direction=PortDirection(direction), capability=PortCaps(caps), type=PortType(p_type), midi_channels=alsa.snd_seq_port_info_get_midi_channels(info), @@ -289,7 +277,6 @@ def _to_alsa(self) -> _snd_seq_port_info_t: alsa.snd_seq_port_info_set_client(info, self.client_id) alsa.snd_seq_port_info_set_port(info, self.port_id if self.port_id is not None else 0) alsa.snd_seq_port_info_set_name(info, self.name.encode()) - alsa.snd_seq_port_info_set_direction(info, self.direction) alsa.snd_seq_port_info_set_capability(info, self.capability) alsa.snd_seq_port_info_set_type(info, self.type) alsa.snd_seq_port_info_set_midi_channels(info, self.midi_channels) @@ -317,7 +304,7 @@ def key(info: PortInfo): return key -__all__ = ["PortDirection", "PortCaps", "PortType", "Port", +__all__ = ["PortCaps", "PortType", "Port", "READ_PORT", "WRITE_PORT", "RW_PORT", "DEFAULT_PORT_TYPE", "READ_PORT_PREFERRED_TYPES", "WRITE_PORT_PREFERRED_TYPES", "RW_PORT_PREFERRED_TYPES", "PortInfo"] diff --git a/utils/aconnect.py b/utils/aconnect.py index dcd65cb..325242d 100755 --- a/utils/aconnect.py +++ b/utils/aconnect.py @@ -13,28 +13,28 @@ ALSAError, \ ClientInfo, \ ClientType, \ - PortDirection, \ + PortCaps, \ PortInfo, \ PortType, \ SubscriptionQueryType -program_name: str = "" +program_name: str = __name__ aconnect: SequencerClient clients: dict[int, ClientInfo] = {} # client_id: CLientInfo ports: dict[int, list[PortInfo]] = {} # client_id: [PortInfos] indenter: textwrap.TextWrapper -def direction(raw: PortDirection) -> str: +def direction(raw: PortCaps) -> str: "ALSA names for input vs output are backwards." - if raw.name == 'INPUT': - return 'OUTPUT' - elif raw.name == 'OUTPUT': + if raw & (PortCaps.READ | PortCaps.WRITE) == PortCaps.READ | PortCaps.WRITE: + return 'BIDIR' + elif raw & PortCaps.WRITE == PortCaps.WRITE: return 'INPUT' - elif raw.name is None: - return '???' + elif raw & PortCaps.READ == PortCaps.READ: + return 'OUTPUT' else: - return raw.name + return '???' def indent(text: str) -> None: @@ -45,9 +45,9 @@ def indent(text: str) -> None: sys.stdout.write('%s\n' % indenter.fill(text)) -def init(term_width: int) -> None: +def init(term_width: int = 0) -> None: "Inititialize global client and textwrap object." - global program_name, aconnect, indenter + global aconnect, indenter aconnect = SequencerClient(program_name) width = term_width \ if term_width > 0 \ @@ -72,7 +72,6 @@ def get_clients(client_list: list[ClientInfo] | None = None) -> None: def get_ports() -> None: "Populate ports dict" - global aconnect, ports # Need ports for system clients in to get client:port names # for non-system ports connected to system ones. @@ -110,8 +109,6 @@ def print_client(client: ClientInfo, :return: Total number of connections for all ports of client """ - global ports - if not client_only and not ports: get_ports() @@ -158,13 +155,13 @@ def print_client(client: ClientInfo, num_connections = 0 if ports and client_id in ports: for port in ports[client_id]: - if inputs_only and port.direction == PortDirection.INPUT: # sic + if inputs_only and (port.capability & PortCaps.WRITE != PortCaps.WRITE): # sic continue - if outputs_only and port.direction == PortDirection.OUTPUT: # sic + if outputs_only and (port.capability & PortCaps.READ != PortCaps.READ): # sic continue port_str = (f" {port.port_id:>3}: " f"{port.name!r:<{max_name_len+3}} " - f"{direction(port.direction)}") + f"{direction(port.capability)}") if port_type is None: port_str += f" {port.type.name}" if full_info: @@ -194,7 +191,6 @@ def print_connections(address: Address, """Print port's connections. :return: Number of connections """ - global indenter subs = aconnect.list_port_subscribers(address) for sub in subs: @@ -251,8 +247,6 @@ def print_clients( :return: Total number of connections for all ports of all clients """ - global aconnect, clients, ports - if not clients: get_clients() @@ -286,7 +280,10 @@ def print_clients( max_name_len = 0 max_type_len = 0 for client_id in client_ids: - client = clients[client_id] + try: + client = clients[client_id] + except KeyError: + raise Exception(f"No client with ID {client_id}") if client.type == ClientType.KERNEL and not kernel_clients: continue if 'PipeWire' in client.name and not pipewire_clients: @@ -328,8 +325,6 @@ def connect(clients_ports: list[str], queue: int = 0) -> None: "Connect one client:port to or from another_client:another_port" - global aconnect - assert len(clients_ports) == 2, \ "Must have exactly two clients:ports to connect" @@ -338,9 +333,14 @@ def connect(clients_ports: list[str], try: source_addr = aconnect.resolve_address(source) + except Exception: + raise ALSAError(f"Bad or non-existent sender '{source}'", errno.ENOENT) + + try: sink_addr = aconnect.resolve_address(sink) - except ALSAError as e: - raise ALSAError("Bad or non-existent source and/or sink", errno.ENOENT) + except Exception: + raise ALSAError(f"Bad or non-existent receiver '{sink}'", + errno.ENOENT) try: aconnect.subscribe_port(source_addr, @@ -349,7 +349,7 @@ def connect(clients_ports: list[str], exclusive=exclusive, time_update=time_update, time_real=time_real) - except ALSAError as e: + except Exception: raise ALSAError(f"Can't connect {source}({source_addr}) -> " f"{sink}({sink_addr}) (not output->input? " "already connected?)", @@ -385,17 +385,15 @@ def remove_connection(source: str | Address, sink: str | Address): "Disconnect single clientA:portA -> clientB:portB connection." - global aconnect - try: source_addr = aconnect.resolve_address(source) sink_addr = aconnect.resolve_address(sink) - except ALSAError as e: + except Exception: raise ALSAError("Bad or non-existent source and/or sink", errno.EINVAL) try: aconnect.unsubscribe_port(source_addr, sink_addr) - except ALSAError as e: + except Exception: raise ALSAError(f"Can't disconnect {source}->{sink} " f"(not connected? or reversed {sink}->" f"{source}?)", @@ -410,11 +408,9 @@ def remove_port_connections(client_port: str, user confirmation. """ - global aconnect, clients, ports - try: address = aconnect.resolve_address(client_port) - except Exception as e: + except Exception: raise ALSAError(f"Bad or non-existent client:port '{client_port}'", errno.EINVAL) @@ -442,7 +438,7 @@ def remove_port_connections(client_port: str, sys.stdout.write(f" {port.port_id:>3}: " f"{port.name!r:<{max_port_len+3}} " - f"{direction(port.direction)}\n") + f"{direction(port.capability)}\n") count = print_connections(address) if count == 0: @@ -468,10 +464,8 @@ def remove_client_connections(client: str | int, user confirmation. """ - global aconnect, clients, ports - # check that is just client (no port) - if not isinstance(client, int): + if isinstance(client, str): assert not any(char in client for char in '.:'), \ "Specify client, not client:port" @@ -546,8 +540,6 @@ def remove_all_connections(no_safety: bool) -> None: user confirmation. """ - global aconnect, program_name - if not no_safety: count = print_clients() if count == 0: @@ -667,7 +659,7 @@ def remove_all_connections(no_safety: bool) -> None: opts.outputs, opts.full) except Exception as e: - sys.stderr.write(f"{e}\n") + sys.stderr.write(f"{program_name}: {e}\n") sys.stderr.write(f"'{program_name} -h' for help and options\n") sys.exit(1)