15 Commits

Author SHA1 Message Date
84fdd94559 rewird VMError docstring
upd README.
2023-08-18 20:09:59 +01:00
eb66f9e2bc implement a strategy pattern into dsl example
logging mode switched to DEBUG.
2023-08-18 20:02:46 +01:00
ce7cda100f Errors section in README updated.
2.4.8 section added to CHANGELOG

patch bump
2023-08-13 16:52:04 +01:00
2bba0ff67a fixes error with escape character in regex 2023-08-13 16:51:27 +01:00
df473d89ae remove __str__ override in VMError
move error message for code -9 into CAPIError class
2023-08-13 16:50:58 +01:00
5aaa9aab71 '\\' join path parts 2023-08-13 14:18:39 +01:00
e9d1e7ffa2 check vban direction
check that index is numeric

patch bump
2023-08-10 21:07:29 +01:00
08525b086c patch bump 2023-08-10 16:28:59 +01:00
cf88b0a63b add poetry test scripts for each kind 2023-08-10 16:27:49 +01:00
4a397d8d96 avoid using keyword as variable name 2023-08-10 16:25:31 +01:00
65fb8990c9 make better use of pattern matching features
error test updated
2023-08-10 16:24:30 +01:00
8c220eb491 refactor target
add error test for ValueError

test badges updated

patch bump
2023-08-09 16:37:10 +01:00
e38922d8a0 upd test badges 2023-08-07 15:49:44 +01:00
96f3faae28 add error tests 2023-08-07 15:39:45 +01:00
bd57f78e8f fix test names 2023-08-07 15:39:22 +01:00
18 changed files with 297 additions and 101 deletions

View File

@@ -11,6 +11,24 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x] - [x]
## [2.4.8] - 2023-08-13
### Added
- Error tests added in tests/test_errors.py
- fn_name and code set as class attributes for CAPIError
- Errors section in README updated.
### Changed
- InstallError and CAPIError classes now subclass VMError
## [2.3.7] - 2023-08-01
### Changed
- If the configs loader is passed an invalid config TOML it will log an error but continue to load further configs into memory.
## [2.3.2] - 2023-07-12 ## [2.3.2] - 2023-07-12
### Added ### Added

View File

@@ -842,10 +842,13 @@ True iff a level has been updated.
### Errors ### Errors
- `errors.VMError`: Exception raised when general errors occur. - `errors.VMError`: Base custom exception class.
- `errors.InstallError`: Exception raised when installation errors occur. - `errors.InstallError`: Exception raised when installation errors occur.
- `errors.CAPIError`: Exception raised when the C-API returns error values. - `errors.CAPIError`: Exception raised when the C-API returns error values.
- Error codes are stored in {Exception Class}.code. For a full list of error codes [check the VoicemeeterRemote header file][Voicemeeter Remote Header]. - The following attributes are available:
- `fn_name`: C-API function name.
- `code`: error code
- For a full list of error codes check the [VoicemeeterRemote header file][Voicemeeter Remote Header].
### Logging ### Logging

View File

@@ -1,6 +1,8 @@
import argparse import argparse
import logging import logging
import time import time
from abc import ABC, abstractmethod
from enum import IntEnum
from pyparsing import ( from pyparsing import (
Combine, Combine,
@@ -10,24 +12,88 @@ from pyparsing import (
Suppress, Suppress,
Word, Word,
alphanums, alphanums,
alphas,
nums, nums,
) )
import voicemeeterlib import voicemeeterlib
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
argparser = argparse.ArgumentParser(description="creates a basic dsl") argparser = argparse.ArgumentParser(description="creates a basic dsl")
argparser.add_argument("-i", action="store_true") argparser.add_argument("-i", action="store_true")
args = argparser.parse_args() args = argparser.parse_args()
ParamKinds = IntEnum(
"ParamKinds",
"bool float string",
)
class Strategy(ABC):
def __init__(self, target, param, val):
self.target = target
self.param = param
self.val = val
@abstractmethod
def run(self):
pass
class BoolStrategy(Strategy):
def run(self):
setattr(self.target, self.param, self.strtobool(self.val))
def strtobool(self, val):
"""Convert a string representation of truth to it's numeric form."""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return 1
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError("invalid truth value %r" % (val,))
class FloatStrategy(Strategy):
def run(self):
setattr(self.target, self.param, float(self.val))
class StringStrategy(Strategy):
def run(self):
setattr(self.target, self.param, " ".join(self.val))
class Context:
def __init__(self, strategy: Strategy) -> None:
self._strategy = strategy
@property
def strategy(self) -> Strategy:
return self._strategy
@strategy.setter
def strategy(self, strategy: Strategy) -> None:
self._strategy = strategy
def run(self):
self.strategy.run()
class Parser: class Parser:
IS_STRING = ("label",)
def __init__(self, vm): def __init__(self, vm):
self.logger = logger.getChild(self.__class__.__name__)
self.vm = vm self.vm = vm
self.kls = Group(OneOrMore(Word(alphanums))) self.kls = Group(OneOrMore(Word(alphanums)))
self.token = Suppress("->") self.token = Suppress("->")
self.param = Word(alphanums) self.param = Group(OneOrMore(Word(alphanums)))
self.value = Combine( self.value = Combine(
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums)) Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
) | Group(OneOrMore(Word(alphanums))) ) | Group(OneOrMore(Word(alphanums)))
@@ -39,27 +105,64 @@ class Parser:
+ Optional(self.value) + Optional(self.value)
) )
def parse(self, cmds): def converter(self, cmds):
"""determines the kind of parameter from the parsed string"""
res = list() res = list()
for cmd in cmds: for cmd in cmds:
if len(self.event.parseString(cmd)) == 2: self.logger.debug(f"running command: {cmd}")
kls, param = self.event.parseString(cmd) match cmd_parsed := self.event.parseString(cmd):
target = getattr(self.vm, kls[0])[int(kls[-1])] case [[kls, index], [param]]:
res.append(getattr(target, param)) target = getattr(self.vm, kls)[int(index)]
elif len(self.event.parseString(cmd)) == 3: res.append(getattr(target, param))
kls, param, val = self.event.parseString(cmd) case [[kls, index], [param], val] if param in self.IS_STRING:
target = getattr(self.vm, kls[0])[int(kls[-1])] target = getattr(self.vm, kls)[int(index)]
if "".join(val) in ["off", "on"]: context = self._get_context(ParamKinds.string, target, param, val)
setattr(target, param, bool(["off", "on"].index("".join(val)))) context.run()
elif param in ["gain", "comp", "gate", "limit", "audibility"]: case [[kls, index], [param], [val] | val]:
setattr(target, param, float("".join(val))) target = getattr(self.vm, kls)[int(index)]
elif param in ["label"]: try:
setattr(target, param, " ".join(val)) context = self._get_context(ParamKinds.bool, target, param, val)
context.run()
except ValueError as e:
self.logger.error(f"{e}... switching to float strategy")
context.strategy = FloatStrategy(target, param, val)
context.run()
case [
[kls, index],
[secondary, param],
[val] | val,
]:
primary = getattr(self.vm, kls)[int(index)]
target = getattr(primary, secondary)
try:
context = self._get_context(ParamKinds.bool, target, param, val)
context.run()
except ValueError as e:
self.logger.error(f"{e}... switching to float strategy")
context.strategy = FloatStrategy(target, param, val)
context.run()
case _:
self.logger.error(
f"unable to determine the kind of parameter from {cmd_parsed}"
)
time.sleep(0.05) time.sleep(0.05)
return res return res
def _get_context(self, kind, *args):
"""
determines a strategy for a kind of parameter and passes it to the context.
"""
match kind:
case ParamKinds.bool:
context = Context(BoolStrategy(*args))
case ParamKinds.float:
context = Context(FloatStrategy(*args))
case ParamKinds.string:
context = Context(StringStrategy(*args))
return context
def interactive_mode(parser): def interactive_mode(parser):
while cmd := input("Please enter command (Press <Enter> to exit)\n"): while cmd := input("Please enter command (Press <Enter> to exit)\n"):
@@ -70,25 +173,23 @@ def interactive_mode(parser):
def main(): def main():
# fmt: off # fmt: off
cmds = ( cmds = (
"strip 0 -> mute -> on", "strip 0 -> mute", "bus 0 -> mute -> on", "strip 0 -> mute -> true", "strip 0 -> mute", "bus 0 -> mute -> true",
"strip 0 -> mute -> off", "bus 0 -> mute -> on", "strip 3 -> solo -> on", "strip 0 -> mute -> false", "bus 0 -> mute -> true", "strip 3 -> solo -> true",
"strip 3 -> solo -> off", "strip 1 -> A1 -> on", "strip 1 -> A1", "strip 3 -> solo -> false", "strip 1 -> A1 -> true", "strip 1 -> A1",
"strip 1 -> A1 -> off", "strip 1 -> A1", "bus 3 -> eq -> on", "strip 1 -> A1 -> false", "strip 1 -> A1", "strip 3 -> eq on -> true",
"bus 3 -> eq -> off", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2", "bus 3 -> eq on -> false", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
"strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28", "strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28",
"strip 2 -> limit", "strip 2 -> limit", "strip 3 -> comp knob -> 3.8"
) )
# fmt: on # fmt: on
KIND_ID = "banana" with voicemeeterlib.api("potato") as vm:
with voicemeeterlib.api(KIND_ID) as vm:
parser = Parser(vm) parser = Parser(vm)
if args.i: if args.i:
interactive_mode(parser) interactive_mode(parser)
return return
if res := parser.parse(cmds): if res := parser.converter(cmds):
print(res) print(res)

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.4.4" version = "2.4.8"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
@@ -33,7 +33,10 @@ levels = "scripts:ex_levels"
midi = "scripts:ex_midi" midi = "scripts:ex_midi"
obs = "scripts:ex_obs" obs = "scripts:ex_obs"
observer = "scripts:ex_observer" observer = "scripts:ex_observer"
test = "scripts:test" basic = "scripts:test_basic"
banana = "scripts:test_banana"
potato = "scripts:test_potato"
all = "scripts:test_all"
[tool.tox] [tool.tox]
legacy_tox_ini = """ legacy_tox_ini = """

View File

@@ -1,3 +1,4 @@
import os
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -38,5 +39,21 @@ def ex_observer():
subprocess.run([sys.executable, str(scriptpath)]) subprocess.run([sys.executable, str(scriptpath)])
def test(): def test_basic():
os.environ["KIND"] = "basic"
subprocess.run(["tox"]) subprocess.run(["tox"])
def test_banana():
os.environ["KIND"] = "banana"
subprocess.run(["tox"])
def test_potato():
os.environ["KIND"] = "potato"
subprocess.run(["tox"])
def test_all():
steps = [test_basic, test_banana, test_potato]
[step() for step in steps]

View File

@@ -1,3 +1,4 @@
import os
import random import random
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
@@ -30,8 +31,10 @@ class Data:
return (2 * self.phys_in) + (8 * self.virt_in) return (2 * self.phys_in) + (8 * self.virt_in)
# let's keep things random # get KIND_ID from env var, otherwise set to random
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId)) KIND_ID = os.environ.get(
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
)
vm = voicemeeterlib.api(KIND_ID) vm = voicemeeterlib.api(KIND_ID)
kind = kindmap(KIND_ID) kind = kindmap(KIND_ID)

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 155"><title>tests: 155</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">155</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">155</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 159"><title>tests: 159</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">159</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">159</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 112"><title>tests: 112</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">112</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">112</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 116"><title>tests: 116</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">116</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">116</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 164"><title>tests: 164</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">164</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">164</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 184"><title>tests: 184</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">184</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">184</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -25,7 +25,11 @@ Function Get-TimeStamp {
if ($MyInvocation.InvocationName -ne ".") { if ($MyInvocation.InvocationName -ne ".") {
Invoke-Expression ".\.venv\Scripts\Activate.ps1" Invoke-Expression ".\.venv\Scripts\Activate.ps1"
RunTests @("potato") | ForEach-Object {
$env:KIND = $_
RunTests
}
Invoke-Expression "deactivate" Invoke-Expression "deactivate"
} }

View File

@@ -12,37 +12,37 @@ class TestUserConfigs:
def setup_class(cls): def setup_class(cls):
vm.apply_config("example") vm.apply_config("example")
def test_it_vm_config_string(self): def test_it_tests_vm_config_string(self):
assert "PhysStrip" in vm.strip[data.phys_in].label assert "PhysStrip" in vm.strip[data.phys_in].label
assert "VirtStrip" in vm.strip[data.virt_in].label assert "VirtStrip" in vm.strip[data.virt_in].label
assert "PhysBus" in vm.bus[data.phys_out].label assert "PhysBus" in vm.bus[data.phys_out].label
assert "VirtBus" in vm.bus[data.virt_out].label assert "VirtBus" in vm.bus[data.virt_out].label
def test_it_vm_config_bool(self): def test_it_tests_vm_config_bool(self):
assert vm.strip[0].A1 == True assert vm.strip[0].A1 == True
@pytest.mark.skipif( @pytest.mark.skipif(
data.name != "potato", data.name != "potato",
reason="Skip test if kind is not potato", reason="Skip test if kind is not potato",
) )
def test_it_vm_config_bool_strip_eq_on(self): def test_it_tests_vm_config_bool_strip_eq_on(self):
assert vm.strip[data.phys_in].eq.on == True assert vm.strip[data.phys_in].eq.on == True
@pytest.mark.skipif( @pytest.mark.skipif(
data.name != "banana", data.name != "banana",
reason="Skip test if kind is not banana", reason="Skip test if kind is not banana",
) )
def test_it_vm_config_bool_bus_eq_ab(self): def test_it_tests_vm_config_bool_bus_eq_ab(self):
assert vm.bus[data.phys_out].eq.ab == True assert vm.bus[data.phys_out].eq.ab == True
@pytest.mark.skipif( @pytest.mark.skipif(
"not config.getoption('--run-slow')", "not config.getoption('--run-slow')",
reason="Only run when --run-slow is given", reason="Only run when --run-slow is given",
) )
def test_it_vm_config_busmode(self): def test_it_tests_vm_config_busmode(self):
assert vm.bus[data.phys_out].mode.get() == "composite" assert vm.bus[data.phys_out].mode.get() == "composite"
def test_it_vm_config_bass_med_high(self): def test_it_tests_vm_config_bass_med_high(self):
assert vm.strip[data.virt_in].bass == -3.2 assert vm.strip[data.virt_in].bass == -3.2
assert vm.strip[data.virt_in].mid == 1.5 assert vm.strip[data.virt_in].mid == 1.5
assert vm.strip[data.virt_in].high == 2.1 assert vm.strip[data.virt_in].high == 2.1

49
tests/test_errors.py Normal file
View File

@@ -0,0 +1,49 @@
import re
import pytest
import voicemeeterlib
from tests import data, vm
class TestErrors:
__test__ = True
def test_it_tests_an_unknown_kind(self):
with pytest.raises(
voicemeeterlib.error.VMError,
match="Unknown Voicemeeter kind 'unknown_kind'",
):
voicemeeterlib.api("unknown_kind")
def test_it_tests_an_unknown_parameter(self):
with pytest.raises(
voicemeeterlib.error.CAPIError,
match="VBVMR_SetParameterFloat returned -3",
) as exc_info:
vm.set("unknown.parameter", 1)
e = exc_info.value
assert e.code == -3
assert e.fn_name == "VBVMR_SetParameterFloat"
def test_it_tests_an_unknown_config_name(self):
EXPECTED_MSG = (
"No config with name 'unknown' is loaded into memory",
f"Known configs: {list(vm.configs.keys())}",
)
with pytest.raises(
voicemeeterlib.error.VMError, match=re.escape("\n".join(EXPECTED_MSG))
):
vm.apply_config("unknown")
def test_it_tests_an_invalid_config_key(self):
CONFIG = {
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
"bus-0": {"mute": True, "eq": {"on": True}},
"unknown-0": {"state": True},
"vban-out-1": {"name": "streamname"},
}
with pytest.raises(ValueError, match="invalid config key 'unknown-0'"):
vm.apply(CONFIG)

View File

@@ -10,7 +10,7 @@ class TestRemoteFactories:
data.name != "basic", data.name != "basic",
reason="Skip test if kind is not basic", reason="Skip test if kind is not basic",
) )
def test_it_vm_remote_attrs_for_basic(self): def test_it_tests_vm_remote_attrs_for_basic(self):
assert hasattr(vm, "strip") assert hasattr(vm, "strip")
assert hasattr(vm, "bus") assert hasattr(vm, "bus")
assert hasattr(vm, "command") assert hasattr(vm, "command")
@@ -28,7 +28,7 @@ class TestRemoteFactories:
data.name != "banana", data.name != "banana",
reason="Skip test if kind is not banana", reason="Skip test if kind is not banana",
) )
def test_it_vm_remote_attrs_for_banana(self): def test_it_tests_vm_remote_attrs_for_banana(self):
assert hasattr(vm, "strip") assert hasattr(vm, "strip")
assert hasattr(vm, "bus") assert hasattr(vm, "bus")
assert hasattr(vm, "command") assert hasattr(vm, "command")
@@ -48,7 +48,7 @@ class TestRemoteFactories:
data.name != "potato", data.name != "potato",
reason="Skip test if kind is not potato", reason="Skip test if kind is not potato",
) )
def test_it_vm_remote_attrs_for_potato(self): def test_it_tests_vm_remote_attrs_for_potato(self):
assert hasattr(vm, "strip") assert hasattr(vm, "strip")
assert hasattr(vm, "bus") assert hasattr(vm, "bus")
assert hasattr(vm, "command") assert hasattr(vm, "command")

View File

@@ -121,5 +121,5 @@ class CBindings(metaclass=ABCMeta):
raise CAPIError(func.__name__, res) raise CAPIError(func.__name__, res)
return res return res
except CAPIError as e: except CAPIError as e:
self.logger_cbindings.exception(str(e)) self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
raise raise

View File

@@ -1,12 +1,5 @@
class VMError(Exception): class VMError(Exception):
"""Base VM Exception class. Raised when general errors occur.""" """Base voicemeeterlib exception class."""
def __init__(self, msg):
self.message = msg
super().__init__(self.message)
def __str__(self):
return f"{type(self).__name__}: {self.message}"
class InstallError(VMError): class InstallError(VMError):
@@ -16,7 +9,16 @@ class InstallError(VMError):
class CAPIError(VMError): class CAPIError(VMError):
"""Exception raised when the C-API returns an error code""" """Exception raised when the C-API returns an error code"""
def __init__(self, fn_name, code, msg=None): def __init__(self, fn_name, code):
self.fn_name = fn_name self.fn_name = fn_name
self.code = code self.code = code
super(CAPIError, self).__init__(msg if msg else f"{fn_name} returned {code}") if self.code == -9:
message = " ".join(
(
f"no bind for {self.fn_name}.",
"are you using an old version of the API?",
)
)
else:
message = f"{self.fn_name} returned {self.code}"
super().__init__(message)

View File

@@ -12,27 +12,32 @@ if platform.system() != "Windows":
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}" VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
REG_KEY = "".join( REG_KEY = "\\".join(
[ filter(
"SOFTWARE", None,
("\\WOW6432Node" if bits == 64 else ""), (
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE",
] "WOW6432Node" if bits == 64 else "",
"Microsoft",
"Windows",
"CurrentVersion",
"Uninstall",
),
)
) )
def get_vmpath(): def get_vmpath():
with winreg.OpenKey( with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, r"{}".format(REG_KEY + "\\" + VM_KEY) winreg.HKEY_LOCAL_MACHINE, r"{}".format("\\".join((REG_KEY, VM_KEY)))
) as vm_key: ) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0] return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
try: try:
vm_path = Path(get_vmpath()) vm_parent = Path(get_vmpath()).parent
except FileNotFoundError as e: except FileNotFoundError as e:
raise InstallError(f"Unable to fetch DLL path from the registry") from e raise InstallError(f"Unable to fetch DLL path from the registry") from e
vm_parent = vm_path.parent
DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll' DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll'

View File

@@ -121,13 +121,7 @@ class Remote(CBindings):
return self.call(self.bind_macro_button_is_dirty, ok=(0, 1)) == 1 return self.call(self.bind_macro_button_is_dirty, ok=(0, 1)) == 1
except AttributeError as e: except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f"{type(e).__name__}: {e}")
ERR_MSG = ( raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
"no bind for VBVMR_MacroButton_IsDirty.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_IsDirty", -9, msg=" ".join(ERR_MSG)
) from e
@property @property
def ldirty(self) -> bool: def ldirty(self) -> bool:
@@ -187,13 +181,7 @@ class Remote(CBindings):
) )
except AttributeError as e: except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f"{type(e).__name__}: {e}")
ERR_MSG = ( raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
"no bind for VBVMR_MacroButton_GetStatus.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_GetStatus", -9, msg=" ".join(ERR_MSG)
) from e
return int(c_state.value) return int(c_state.value)
def set_buttonstatus(self, id_: int, val: int, mode: int) -> None: def set_buttonstatus(self, id_: int, val: int, mode: int) -> None:
@@ -208,13 +196,7 @@ class Remote(CBindings):
) )
except AttributeError as e: except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f"{type(e).__name__}: {e}")
ERR_MSG = ( raise CAPIError("VBVMR_MacroButton_SetStatus", -9) from e
"no bind for VBVMR_MacroButton_SetStatus.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_SetStatus", -9, msg=" ".join(ERR_MSG)
) from e
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value) self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
def get_num_devices(self, direction: str = None) -> int: def get_num_devices(self, direction: str = None) -> int:
@@ -299,16 +281,25 @@ class Remote(CBindings):
minor delay between each recursion minor delay between each recursion
""" """
def param(key): def target(key):
obj, m2, *rem = key.split("-") match key.split("-"):
index = int(m2) if m2.isnumeric() else int(*rem) case ["strip" | "bus" | "button" as kls, index] if index.isnumeric():
if obj in ("strip", "bus", "button"): target = getattr(self, kls)
return getattr(self, obj)[index] case [
elif obj == "vban": "vban",
return getattr(getattr(self, obj), f"{m2}stream")[index] "in" | "instream" | "out" | "outstream" as direction,
raise ValueError(obj) index,
] if index.isnumeric():
target = getattr(
self.vban, f"{direction.removesuffix('stream')}stream"
)
case _:
ERR_MSG = f"invalid config key '{key}'"
self.logger.error(ERR_MSG)
raise ValueError(ERR_MSG)
return target[int(index)]
[param(key).apply(datum).then_wait() for key, datum in data.items()] [target(key).apply(di).then_wait() for key, di in data.items()]
def apply_config(self, name): def apply_config(self, name):
"""applies a config from memory""" """applies a config from memory"""

View File

@@ -172,8 +172,8 @@ class VbanMidiOutstream(VbanOutstream):
def _make_stream_pair(remote, kind): def _make_stream_pair(remote, kind):
num_instream, num_outstream, num_midi, num_text = kind.vban num_instream, num_outstream, num_midi, num_text = kind.vban
def _make_cls(i, dir): def _make_cls(i, direction):
match dir: match direction:
case "in": case "in":
if i < num_instream: if i < num_instream:
return VbanAudioInstream(remote, i) return VbanAudioInstream(remote, i)