Testing¶
Supriya uses an extensive test suite to guarantee stability during the development process.
Install Supriya’s test dependencies with:
josephine@laptop:~$ pip install supriya[test]
josephine@laptop:~$ git clone https://github.com/supriya-project/supriya.git
josephine@laptop:~$ cd supriya
josephine@laptop:~/supriya$ pip install -e .[test]
Running tests¶
Supriya uses pytest as its test runner, both for unit tests and for doctests, along with a variety of pytest_ plugins.
Run pytest against Supriya’s default test paths (supriya/ and tests/) with:
josephine@laptop:~/supriya$ pytest
You can run pytest against a specific test module with:
josephine@laptop:~/supriya$ pytest tests/contexts/test_Server_nodes.py
… or against a specific test function in a specific module with:
josephine@laptop:~/supriya$ pytest tests/contexts/test_Server_nodes.py::test_add_synth
You can also run pytest against any test or test file matching a pattern with:
josephine@laptop:~/supriya$ pytest -k add_synth
See pytest’s complete documentation for a wide variety of invocation options.
Live servers¶
Many unit and doctests run against live SuperCollider servers. Expect to hear audio during the test run.
If you have any running servers online prior to running the test suite, they will be automatically killed once the tests start running.
Supriya provides a kill()
function for killing any
process whose name matches scsynth
, supernova
or their Windows
equivalents.
Coverage¶
pytest can collect code coverage statistics during its test runs via the pytest-cov plugin, installed when you install Supriya’s test dependencies.
Collect coverage against the code in the supriya/
directory during a test run by adding the --cov=supriya
flag:
josephine@laptop:~/supriya$ pytest --cov=supriya
Supriya provides a Makefile
target to simplify this call:
josephine@laptop:~/supriya$ make pytest
The coverage report appear at the end of the test output in your terminal:
Name Stmts Miss Branch BrPart Cover
---------------------------------------------------------------------
supriya/__init__.py 21 4 2 1 78%
supriya/_version.py 2 0 0 0 100%
supriya/clocks/__init__.py 5 0 0 0 100%
supriya/clocks/asynchronous.py 115 6 40 5 93%
supriya/clocks/core.py 431 33 118 18 90%
supriya/clocks/offline.py 97 12 14 4 86%
supriya/clocks/threaded.py 72 3 22 4 93%
supriya/contexts/__init__.py 5 0 0 0 100%
supriya/contexts/allocators.py 154 10 42 8 91%
supriya/contexts/core.py 532 32 186 22 91%
supriya/contexts/entities.py 285 41 72 26 78%
supriya/contexts/nonrealtime.py 113 8 36 5 91%
supriya/contexts/realtime.py 791 28 262 25 95%
supriya/contexts/requests.py 718 29 142 14 95%
supriya/contexts/responses.py 356 12 64 5 96%
supriya/contexts/scopes.py 94 16 30 15 75%
supriya/conversions.py 20 5 0 0 75%
supriya/enums.py 381 8 24 3 97%
supriya/exceptions.py 36 0 0 0 100%
supriya/ext/__init__.py 19 6 10 3 62%
supriya/ext/book.py 99 2 2 0 98%
supriya/ext/ipython.py 23 16 4 0 26%
supriya/ext/mypy.py 63 49 18 0 17%
supriya/io.py 78 25 8 2 64%
supriya/osc/__init__.py 5 0 0 0 100%
supriya/osc/asynchronous.py 89 8 30 8 85%
supriya/osc/messages.py 279 35 116 16 85%
supriya/osc/protocols.py 209 15 68 13 90%
supriya/osc/threaded.py 115 10 36 7 89%
supriya/osc/utils.py 26 5 12 1 84%
supriya/patterns/__init__.py 7 0 0 0 100%
supriya/patterns/eventpatterns.py 131 14 42 3 89%
supriya/patterns/events.py 168 15 52 12 85%
supriya/patterns/noise.py 114 7 38 8 90%
supriya/patterns/patterns.py 252 6 54 2 97%
supriya/patterns/players.py 166 25 60 9 81%
supriya/patterns/structure.py 157 11 58 7 90%
supriya/patterns/testutils.py 60 3 22 0 96%
supriya/sclang.py 29 7 16 6 67%
supriya/scsynth.py 416 29 156 21 91%
supriya/sessions/__init__.py 6 0 0 0 100%
supriya/sessions/components.py 241 14 78 9 93%
supriya/sessions/constants.py 36 2 0 0 94%
supriya/sessions/devices.py 80 1 16 1 98%
supriya/sessions/mixers.py 63 2 14 2 95%
supriya/sessions/sessions.py 200 16 82 15 89%
supriya/sessions/specs.py 386 50 166 24 83%
supriya/sessions/tracks.py 363 8 116 7 97%
supriya/soundfiles.py 48 7 14 7 77%
supriya/typing.py 43 4 0 0 91%
supriya/ugens/__init__.py 31 0 0 0 100%
supriya/ugens/basic.py 107 1 48 1 99%
supriya/ugens/beq.py 56 0 0 0 100%
supriya/ugens/bufio.py 61 0 2 0 100%
supriya/ugens/chaos.py 162 0 0 0 100%
supriya/ugens/compilers.py 0 0 0 0 100%
supriya/ugens/convolution.py 25 0 0 0 100%
supriya/ugens/core.py 1365 67 492 44 94%
supriya/ugens/delay.py 128 0 0 0 100%
supriya/ugens/demand.py 133 2 6 2 97%
supriya/ugens/diskio.py 15 0 0 0 100%
supriya/ugens/dynamics.py 32 0 0 0 100%
supriya/ugens/envelopes.py 259 82 78 18 64%
supriya/ugens/factories.py 195 20 80 11 87%
supriya/ugens/ffsinosc.py 36 2 6 3 88%
supriya/ugens/filters.py 181 0 0 0 100%
supriya/ugens/gendyn.py 50 0 0 0 100%
supriya/ugens/granular.py 37 0 0 0 100%
supriya/ugens/hilbert.py 13 0 0 0 100%
supriya/ugens/info.py 47 0 0 0 100%
supriya/ugens/inout.py 39 0 2 1 98%
supriya/ugens/lines.py 66 1 2 1 97%
supriya/ugens/mac.py 25 0 0 0 100%
supriya/ugens/ml.py 76 0 0 0 100%
supriya/ugens/noise.py 119 0 2 1 99%
supriya/ugens/osc.py 103 0 0 0 100%
supriya/ugens/panning.py 89 3 8 3 94%
supriya/ugens/physical.py 26 0 0 0 100%
supriya/ugens/pv.py 196 6 6 0 95%
supriya/ugens/reverb.py 7 0 0 0 100%
supriya/ugens/safety.py 16 1 2 1 89%
supriya/ugens/system.py 230 5 36 2 97%
supriya/ugens/triggers.py 148 3 6 3 96%
supriya/utils/__init__.py 3 0 0 0 100%
supriya/utils/intervals.py 735 97 324 35 84%
supriya/utils/iterables.py 86 7 50 7 87%
---------------------------------------------------------------------
TOTAL 13106 936 3492 471 90%
Coverage HTML written to dir htmlcov
A line-by-line HTML version of the output can also be found under an htmlcov/
directory at the root of Supriya’s codebase.
Hint
Use coverage reporting to guide your test writing. It will expose branches that haven’t been executed during testing. Aim for 90% coverage, but don’t stress about going significantly higher. Higher percentages (while often possible!) tend to require contortions of logic, and in practice don’t necessarily yield better stability. Rely on static type-checking to fill in the gaps in test coverage.
Writing tests¶
Note
The test examples here are in the official test suite, to ensure that they continue to work and are also therefore subject to the same formatting, linting and static-typing checks as any other code in Supriya. You can find them here.
Some philosophy¶
Whenever possible, keep tests simple:
some setup (pushed into fixtures if at all possible)
possibly validate pre-conditions
perform the operation under test (a single operation!)
validate post-conditions
some teardown (pushed into fixtures if at all possible)
Test every public function, even public class, every public method on every public class. One test per function or method (including initializers if those are complex, which they shouldn’t be if possible). When testing variations on the same category of behavior, use parameterization.
It’s easy to test operations against live, running servers, so make use of that. Don’t mock the server, just use one. In general, assume that SuperCollider will behave as expected, allowing yourself to test communication with the server rather than target server state. SuperCollider doesn’t make it particularly easy to truly validate server state, so prefer to rely on validating communication instead.
Make sure to test not only happy paths, but unhappy ones too. If a function can raise exceptions, test for them. If a function can issue warnings, test for them. If a function explicitly emits logs, test for them. Failure states are part of the API.
Doctests are OK, but keep them very concise. Prefer to push extended testing in docstrings into the unit testsuite instead. Prefer to push extensive exposition into the documentation instead.
Self Criticism
You’ll see in some of Supriya’s older tests - especially for SynthDefs - multiple rounds of operations and validations. Avoid this pattern when writing new ones. These older tests will eventually get refactored, typically into parametrized tests.
An example test¶
Let’s create a simple test that:
as setup, boots a server
queries the server’s node tree
asserts that the node tree matches our expectations
as teardown, quits the server
def test_basic() -> None:
# setup
server = supriya.Server().boot()
# operation
actual_tree = str(server.query_tree())
# validation
expected_tree = textwrap.dedent(
"""\
NODE TREE 0 group
1 group
"""
)
assert actual_tree == expected_tree
# teardown (but might not happen if the assert fails)
server.quit()
Not particularly interesting or necessarily useful, but it’s a good demonstration of what a test might look like.
You can run this test with:
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_basic
Our first test has a problem: if the assert
fails (it won’t, but if it
did), the server won’t quit. Philosophically, booting and quitting the server
aren’t even part of what we want to test. Let’s solve this, and simplify the
test at the same time.
Testing with fixtures¶
One might be tempted to use a try/finally block to ensure the server quits, even if the assertion fails. And it’s true, that will work fine:
def test_try_finally() -> None:
# setup
server = supriya.Server().boot()
# ugly!
try:
# operation
actual_tree = str(server.query_tree())
# validation
expected_tree = textwrap.dedent(
"""\
NODE TREE 0 group
1 group
"""
)
assert actual_tree == expected_tree
finally:
# teardown (even if the assert fails)
server.quit()
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_try_finally
But there’s a simpler way to do this, with less indentation, where we can extract out the server lifecycle from the test entirely into a reusable component: a pytest fixture.
The following demonstrates a fixture that instantiates a server, boots it, yields the server for usage elsewhere, and then quits it when that usage completes:
@pytest.fixture
def server() -> Generator[supriya.Server, None, None]:
# setup
server = supriya.Server(name="test-server").boot()
# yield to test
yield server
# teardown, even if the test fails
server.quit()
Importantly, the code after the yield will run even if the code using the yielded server fails. This single-yield generator effectively describes a setup/test/teardown lifecycle.
We integrate the server
fixture into a test by adding an argument with the
same name as the fixture to the test function:
def test_fixtures(
# note: the server argument name matches the server fixture name exactly
server: supriya.Server,
) -> None:
# operation
actual_tree = str(server.query_tree())
# validation
expected_tree = textwrap.dedent(
"""\
NODE TREE 0 group
1 group
"""
)
assert actual_tree == expected_tree
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_fixtures
The resulting test is a little shorter (not dramatically so, but you can
imagine how this saves a lot of space in more complex scenarios), and a little
clearer. We no longer have to handle setup and teardown inside the test, and
have a re-usable fixture we can integrate with other tests in the same
testsuite. The server
fixture will execute once for each test that
references it, allowing us to re-use server functionality, but with strong
isolation due to separate server objects.
Testing async code¶
Async code typically needs to be run inside async tests. To do this:
Add the
async
keyword to the test function definition, like any other async function.Add the
@pytest.mark.asyncio
decorator to the function (available via the pytest-asyncio plugin)Use the
@pytest_asyncio.fixture
(available via the pytest-asyncio plugin) to decorate async fixtures, in this case to decorate a fixture returning anAsyncServer
@pytest_asyncio.fixture
async def async_server() -> AsyncGenerator[supriya.AsyncServer, None]:
# setup
server = await supriya.AsyncServer(name="test-server").boot()
# yield to test
yield server
# teardown, even if the test fails
await server.quit()
@pytest.mark.asyncio
async def test_async(async_server: supriya.AsyncServer) -> None:
# operation
actual_tree = str(await async_server.query_tree())
# validation
expected_tree = textwrap.dedent(
"""\
NODE TREE 0 group
1 group
"""
)
assert actual_tree == expected_tree
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_async
Note that test_async
looks just like test_fixtures
, just… async.
Testing async and sync code together¶
When testing classes with “mirror” sync and async interfaces, like
Server
and
AsyncServer
we can use a little helper
function to optionally await as necessary:
async def get(x):
# if x is awaitable, await it
if asyncio.iscoroutine(x):
return await x
# otherwise just return it
return x
And we can use a (more complex) async fixture (available via the
pytest-asyncio plugin) to instantiate, boot, yield, then quit either a
Server
or a
AsyncServer
:
@pytest_asyncio.fixture(params=[supriya.AsyncServer, supriya.Server])
async def context(
request,
) -> AsyncGenerator[supriya.AsyncServer | supriya.Server, None]:
# this fixture will cause tests using it to run twice:
# the context class is first AsyncServer, then Server
context_class = request.param
# instantiation looks the same
context = context_class(name="test-server")
# booting might need to be awaited
await get(context.boot())
# yield the context
yield context
# quitting might need to be awaited
await get(context.quit())
The above fixture actually causes tests using it to run once per value in the
fixture’s params
list, effectively parametrizing the test!
Then we can use the fixture, just like we used the server
fixture
previously, along with the get()
helper, to test that both flavors of
server handle querying the node tree the same way:
@pytest.mark.asyncio
async def test_async_and_sync(context: supriya.AsyncServer | supriya.Server) -> None:
# operation
actual_tree = str(await get(context.query_tree()))
# validate they're the same
expected_tree = textwrap.dedent(
"""\
NODE TREE 0 group
1 group
"""
)
assert actual_tree == expected_tree
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_async_and_sync
Note
Note the bracketed arguments in the pytest output. These show (if possible) a representation of the parametrized argument(s) to each individual run of the test.
Testing node trees diffs¶
Many client operations change the state of the server’s node tree, and in those cases the simplest thing to test is often not just “does the node tree look like what we expect?” but “does the diff of the node tree before and after my operation look like what I expect?”
To do this, we can literally compare the node tree before and after an operation, as a diff. First, query the tree as a pre-condition, query it again after performing an operation, diff the two strings, and compare your expected diff against the actual diff.
def test_node_tree_diff(server: supriya.Server) -> None:
# capture the tree before
before_tree = str(server.query_tree())
# perform an operation
with server.at():
for _ in range(5):
server.add_group()
# capture the tree after
after_tree = str(server.query_tree())
# build a diff of the two trees
actual_diff = "".join(
difflib.unified_diff(
before_tree.splitlines(True),
after_tree.splitlines(True),
fromfile="before",
tofile="after",
)
)
# tidy up the expected diff
expected_diff = textwrap.dedent(
"""\
--- before
+++ after
@@ -1,2 +1,7 @@
NODE TREE 0 group
1 group
+ 1004 group
+ 1003 group
+ 1002 group
+ 1001 group
+ 1000 group
"""
)
# validate
assert actual_diff == expected_diff
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_node_tree_diff
Now, note that there’s a lot of boilerplate in that test. Let’s extract out the tree querying and diffing logic into something separate. Because there’s “before” and “after” concepts at play, a context manager is a logical choice for the extracted logic.
And we’ll go one step further, combining the get()
helper we introduced
earlier for testing both sync and async code “homogenously” to create an async
context manager that can validate the node tree diff for both sync and async
servers:
@contextlib.asynccontextmanager
async def assert_node_tree_diff(
context: supriya.AsyncServer | supriya.Server,
expected_diff: str,
) -> AsyncGenerator[None, None]:
# capture the tree before
before_tree = str(await get(context.query_tree()))
# run the body of the context manager block
yield
# capture the tree after
after_tree = str(await get(context.query_tree()))
# build a diff of the two trees
actual_diff = "".join(
difflib.unified_diff(
before_tree.splitlines(True),
after_tree.splitlines(True),
fromfile="before",
tofile="after",
)
)
# tidy up the expected diff so we don't need to dedent inside our tests
expected_diff = textwrap.dedent(expected_diff)
# validate that they match
assert actual_diff == expected_diff
@pytest.mark.asyncio
async def test_node_tree_diff_context_manager(
context: supriya.AsyncServer | supriya.Server,
) -> None:
# the validation happens once we exit this context manager
async with assert_node_tree_diff(
context=context,
expected_diff="""\
--- before
+++ after
@@ -1,2 +1,7 @@
NODE TREE 0 group
1 group
+ 1004 group
+ 1003 group
+ 1002 group
+ 1001 group
+ 1000 group
""",
):
# perform an operation
with context.at():
for _ in range(5):
context.add_group()
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_node_tree_diff_context_manager
Note
If the diff comparisons fail, pytest will show you a diff of diffs in its failure reporting. That’s not the easiest thing to read, but I swear you’ll get used to it.
Testing OSC transcripts¶
We can validate what OSC messages have been sent to the server via a transcript
Capture
. While this doesn’t guarantee that
the resulting server state is correct, it does help us validate that our
communications were what we expected.
Supriya provides a low-level context manager - accessible via the server’s OSC protocol object - for capturing OSC data sent to and received from the server:
def test_osc_transcript(server: supriya.Server) -> None:
# sniff OSC messages going to and coming from the server
with server.osc_protocol.capture() as transcript:
# perform an action that will emit OSC messages
server.add_group()
# validate that sent messages match what we expect
assert transcript.filtered(sent=True, received=False, status=False) == [
supriya.OscMessage("/g_new", 1000, 0, 1),
]
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_osc_transcript
Note
The OSC “transcript” can be filtered via various boolean flags:
If
sent=True
then the filtered messages will include outgoing OSC messages.If
received=True
then the filtered messages will include incoming OSC messages.If
status=False
then nthe filtered messages will omit both/status
and/status.reply
messages, as these are constantly being sent and received during the course of normal operation.
Testing server process transcripts¶
Like the OSX transcript described above, we can also capture stdout
lines
from a server’s underlying scsynth
or supernova
process via a process
Capture
. There are few uses for this, but it’s
available nonetheless:
def test_process_transcript(server: supriya.Server) -> None:
for _ in range(5):
server.add_group()
with server.process_protocol.capture() as transcript:
# tell the server to "trace" the default group
server.default_group.trace()
# the trace has no end-delimiter so we don't know explicitly when to
# stop capturing the output, so we'll just sleep and hope for the best:
time.sleep(0.1)
assert transcript.lines == [
"TRACE Group 1",
" 1004 group",
" 1003 group",
" 1002 group",
" 1001 group",
" 1000 group",
]
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_process_transcript
Testing exceptions¶
Don’t simply test happy paths. If some functionality can and should raise exceptions, validate that those exceptions are raised under the expected conditions. As library authors, we want to treat failure modes as an important part of API design.
pytest provides a pytest.raises(...)
context manager for asserting
exceptions were raised by a code block:
def test_exceptions(server: supriya.Server) -> None:
# if the server is already online,
# booting again will throw an exception
with pytest.raises(supriya.exceptions.ServerOnline) as exception_info:
# the test fails if the exception doesn't raise
server.boot()
# and we can assert even more about the raised exception
# e.g. what the custom message for the exception was
assert "Server already online!" in str(exception_info.value)
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_exceptions
Testing warnings¶
Like exceptions, we should also test for any warnings our code explicitly raises.
Python’s standard library provides a
warnings.catch_warnings(record=True)
context manager for catching warnings
we can inspect later:
def test_warnings(server: supriya.Server) -> None:
# let's create a client-side reference to a group that doesn't exist on the
# server
nonexistent_group = supriya.Group(context=server, id_=666)
# capture warnings
with warnings.catch_warnings(record=True) as warnings_:
# operation: try to free a group that doesn't exist
nonexistent_group.free()
# wait for the server, because the warning arrives asynchronously
server.sync()
# validation
assert len(warnings_) == 1
assert str(warnings_[0].message) == "/n_free Node 666 not found"
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_warnings
Testing logs¶
We can also test for logging output. Why write logging code if you don’t verify that it logged the way you want?
pytest provides a caplog fixture that lets you set logging levels and capture logs during tests:
def test_logging(caplog: pytest.LogCaptureFixture, server: supriya.Server) -> None:
# capture logs from the supriya.osc.out logger
# that are at or higher than logging.DEBUG
caplog.set_level(logging.DEBUG, logger="supriya.osc.out")
# do something that should emit some osc logging
server.add_group()
# validate that the logs are what we expect...
# ``caplog.record_tuples`` is a list of triples: logger, level, message
assert caplog.record_tuples == [
(
"supriya.osc.out",
logging.DEBUG,
f"[127.0.0.1:57110/{server.name}] OscMessage('/g_new', 1000, 0, 1)",
),
]
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_logging
Testing parametrically¶
Last, but not least. Often you’ll write a series of very similar tests, and it’s often the case that with some careful considerations you can transform that series of similar tests into a single test with varying inputs. This is a “parametrized” test: one test body, but a series of one or more varying sets of inputs.
Note
The pytest folks use the British “parametrize” spelling rather than the American “parameterized” spelling. We’ll use the same spelling they do for the sake of consistency.
You can create a parametrized test with the @pytest.mark.parametrize
decorator.
This decorator has two arguments:
A string containing comma-delimited names which must map to arguments to your test function
A list of arguments, or tuples of arguments the same length as the number of names defined in the preceding string
We can demonstrate this with a long - but ultimately simple - test combining
some of the techniques previously described above. What is the node tree diff
when we add a new group relative to an old one, with the add_action
varying
in each test scenario?
To test this we need two parametrized arguments: the add action under test, and the expected output. Then we’ll craft a sequences of test scenarios, one for each add action, with a matching expected node tree diff. The test body will execute its logic with each different add action, and validate that our expected node tree diff matches the actual node tree diff:
@pytest.mark.parametrize(
"add_action, expected_diff",
[
(
supriya.AddAction.ADD_TO_HEAD,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
1000 group
+ 1002 group
1001 group
""",
),
(
supriya.AddAction.ADD_TO_TAIL,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.ADD_BEFORE,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
+ 1002 group
1000 group
1001 group
""",
),
(
supriya.AddAction.ADD_AFTER,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.REPLACE,
"""\
--- before
+++ after
@@ -1,4 +1,3 @@
NODE TREE 0 group
1 group
- 1000 group
- 1001 group
+ 1002 group
""",
),
],
)
@pytest.mark.asyncio
async def test_parametrized(
add_action: supriya.AddAction,
expected_diff: str,
server: supriya.Server,
) -> None:
# add a group
group = server.add_group()
# make sure the group has a child group
group.add_group()
async with assert_node_tree_diff(
context=server,
expected_diff=expected_diff,
):
# add another group relative to the first one
# with the add action varying depending on the scenario
group.add_group(add_action=add_action)
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_parametrized
Parameters are combinatoric. You can use multiple @pytest.mark.parametrize
decorators and/or parametrized fixtures with the same test case, and pytest
will perform combinatoric expansion. Let’s use the context
fixture defined
earlier that can yield either a sync or async server with our initial
parametric test cases:
@pytest.mark.parametrize(
"add_action, expected_diff",
[
(
supriya.AddAction.ADD_TO_HEAD,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
1000 group
+ 1002 group
1001 group
""",
),
(
supriya.AddAction.ADD_TO_TAIL,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.ADD_BEFORE,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
+ 1002 group
1000 group
1001 group
""",
),
(
supriya.AddAction.ADD_AFTER,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.REPLACE,
"""\
--- before
+++ after
@@ -1,4 +1,3 @@
NODE TREE 0 group
1 group
- 1000 group
- 1001 group
+ 1002 group
""",
),
],
)
@pytest.mark.asyncio
async def test_parametrized_combinatoric(
add_action: supriya.AddAction,
expected_diff: str,
context: supriya.AsyncServer | supriya.Server,
) -> None:
# add a group
group = context.add_group()
# make sure the group has a child group
group.add_group()
async with assert_node_tree_diff(
context=context,
expected_diff=expected_diff,
):
# add another group relative to the first one
# with the add action varying depending on the scenario
group.add_group(add_action=add_action)
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_parametrized_combinatoric
Note the expansion in the test report: five add actions times two server types.
Note
Debugging parametrized tests can be difficult because of the verticality of the code, and the non-obvious connection between which parameter block was associated with which failure. In general, try not to stack too many different parametric groups, simply for the sake of legibility.
Testing parametrically with fixtures¶
One thing parametrized tests are not good at is handling parametrized fixtures.
By default, pytest won’t expand fixture references that are arguments to a
parametrized test scenario. However, we can use the pytest-lazy-fixtures
plugin’s lf
function to lazily evaluate fixtures appearing in a
parametrized scenario.
As an example, let’s replace the context
fixture - which expands to either
a Server
or
AsyncServer
- with another
@pytest.mark.parametrize
decorator whose arguments are lazily-evaluated
references to the async_server
and server
fixtures.
@pytest.mark.parametrize(
"add_action, expected_diff",
[
(
supriya.AddAction.ADD_TO_HEAD,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
1000 group
+ 1002 group
1001 group
""",
),
(
supriya.AddAction.ADD_TO_TAIL,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.ADD_BEFORE,
"""\
--- before
+++ after
@@ -1,4 +1,5 @@
NODE TREE 0 group
1 group
+ 1002 group
1000 group
1001 group
""",
),
(
supriya.AddAction.ADD_AFTER,
"""\
--- before
+++ after
@@ -2,3 +2,4 @@
1 group
1000 group
1001 group
+ 1002 group
""",
),
(
supriya.AddAction.REPLACE,
"""\
--- before
+++ after
@@ -1,4 +1,3 @@
NODE TREE 0 group
1 group
- 1000 group
- 1001 group
+ 1002 group
""",
),
],
)
@pytest.mark.parametrize("context_", [lf("async_server"), lf("server")])
@pytest.mark.asyncio
async def test_parametrized_lazy_fixtures(
add_action: supriya.AddAction,
expected_diff: str,
# we use a slightly different name so as to not shadow the context fixture
# otherwise pytest will complain:
context_: supriya.AsyncServer | supriya.Server,
) -> None:
# add a group
group = context_.add_group()
# make sure the group has a child group
group.add_group()
async with assert_node_tree_diff(
context=context_,
expected_diff=expected_diff,
):
# add another group relative to the first one
# with the add action varying depending on the scenario
group.add_group(add_action=add_action)
josephine@laptop:~/supriya$ pytest tests/test_examples.py::test_parametrized_lazy_fixtures
Note
There’s no good reason to do this specifically except for the sake of
pedagogy. The original context
fixture is cleaner, but we need to
demonstrate the lazy technique.
Formatting¶
Supriya follows PEP8 formatting standards. While Supriya originally used a combination of black and isort to handle formatting code and sorting imports, it now just uses ruff.
You can auto-format the codebase with:
josephine@laptop:~/supriya$ make reformat
make[2]: Entering directory '/home/runner/work/supriya/supriya'
ruff check --select I,RUF022 --fix supriya/ docs/ examples/ tests/ *.py
make[2]: Leaving directory '/home/runner/work/supriya/supriya'
Linting¶
While Supriya originally used flake8 for linting, it now uses ruff, for sake of speed and ease of configuration.
You can lint the codebase with:
josephine@laptop:~/supriya$ make ruff-lint
make[2]: Entering directory '/home/runner/work/supriya/supriya'
ruff check --diff supriya/ docs/ examples/ tests/ *.py
make[2]: Leaving directory '/home/runner/work/supriya/supriya'
Type-checking¶
Supriya employs static type-checking extensively (although this is always a work-in-progress) and uses mypy to perform the analysis.
You can run static type-checking with:
josephine@laptop:~/supriya$ make mypy
make[2]: Entering directory '/home/runner/work/supriya/supriya'
mypy supriya/ tests/
supriya/ext/book.py:14: error: Library stubs not installed for "docutils.nodes" [import-untyped]
supriya/ext/book.py:14: note: Hint: "python3 -m pip install types-docutils"
supriya/ext/book.py:14: note: (or run "mypy --install-types" to install all missing stub packages)
supriya/ext/book.py:14: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
tests/patterns/test_UpdatePattern.py:17: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:26: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:36: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:37: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:38: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:39: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:40: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_UpdatePattern.py:41: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:32: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:33: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:39: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:40: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:46: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:47: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:63: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:64: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:68: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:69: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:70: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:83: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:84: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:100: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:101: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:101: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:102: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:103: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:103: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:107: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:107: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:108: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:108: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:109: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:109: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:112: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:113: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:128: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:129: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:129: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:130: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:131: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:131: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_ParallelPattern.py:136: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ParallelPattern.py:138: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:16: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:17: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:18: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:19: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:20: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:21: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:29: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:37: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:38: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:39: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:40: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:48: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:56: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:57: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:58: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:67: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:68: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_MonoEventPattern.py:69: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:26: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:27: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:27: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_GroupPattern.py:28: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:28: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_GroupPattern.py:29: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:38: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:39: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:39: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_GroupPattern.py:40: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_GroupPattern.py:40: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_GroupPattern.py:42: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:40: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:44: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:45: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:46: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:60: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:67: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:68: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_FxPattern.py:69: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:16: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:17: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:18: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:19: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:20: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:21: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:29: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:37: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:38: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:39: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:40: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:48: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:56: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:57: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:58: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:67: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:68: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_EventPattern.py:69: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:23: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:32: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:42: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:43: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:44: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:45: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:46: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_ChainPattern.py:47: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:38: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:42: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:44: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:48: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:50: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:54: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:54: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:54: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:55: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:55: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:55: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:57: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:57: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:72: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:76: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:78: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:82: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:84: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:88: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:88: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:88: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:89: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:89: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:89: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:92: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:94: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:95: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:111: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:115: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:117: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:121: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:123: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:130: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:134: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:134: error: Argument "target_node" to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:136: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:140: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:141: error: Argument "out" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:143: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:147: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:147: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:147: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:148: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:148: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:148: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:151: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:153: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:154: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:159: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:161: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:162: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:187: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:191: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:193: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:198: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:199: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:208: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:212: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:212: error: Argument "target_node" to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:214: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:219: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:220: error: Argument "out" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:221: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:226: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:229: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:230: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:235: error: Argument 1 to "BusAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:239: error: Argument 1 to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:239: error: Argument "target_node" to "GroupAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:241: error: Argument 1 to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:246: error: Argument "in_" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:247: error: Argument "out" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | str | Sequence[SupportsFloat | UUID | str]" [arg-type]
tests/patterns/test_BusPattern.py:248: error: Argument "target_node" to "SynthAllocateEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:253: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:256: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:257: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:265: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:268: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:269: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:272: error: Argument 1 to "NoteEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:275: error: Argument "out" to "NoteEvent" has incompatible type "MockUUID"; expected "SupportsFloat | UUID | Sequence[SupportsFloat | UUID]" [arg-type]
tests/patterns/test_BusPattern.py:276: error: Argument "target_node" to "NoteEvent" has incompatible type "MockUUID"; expected "Node | UUID | None" [arg-type]
tests/patterns/test_BusPattern.py:285: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:287: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:288: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:293: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:295: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:296: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:303: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:305: error: Argument 1 to "NodeFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
tests/patterns/test_BusPattern.py:306: error: Argument 1 to "BusFreeEvent" has incompatible type "MockUUID"; expected "UUID | tuple[UUID, int]" [arg-type]
Found 199 errors in 9 files (checked 158 source files)
make[2]: Leaving directory '/home/runner/work/supriya/supriya'
When writing (or refactoring) code, make sure to add type hints whenever possible. Except in rare cases - typically extremely dynamic programming or low-level systems programming-, most functions and methods can be type hinted. We aim for every method and function to at least have a return value, but more is better.
Note
Also make sure to type hint tests in the test suite, as this makes refactoring the codebase simpler and surfaces errors faster than simply running the tests.
Mypy extensions¶
Because of its use of metaclasses for code generation in UGens, Supriya requires a mypy extension to teach it about the auto-generated methods and properties on those classes.
The extension lives in supriya/ext/mypy.py
and MyPy is already configured to use it via the plugins
field in Supriya’s
pyproject.toml.
Should you need to type check against Supriya’s UGens in another project, you can activate the mypy extension with the following code:
[tool.mypy]
plugins = ["supriya.ext.mypy"]
CI/CD¶
Every push to Supriya’s GitHub repository runs the GitHub Actions test workflow.
This workflow does a lot of different validations:
It builds SuperCollider from source under Ubuntu, OSX and Windows
It configures dummy sound-cards under each of those operating systems and gut-checks that the SuperCollider server can boot
It builds the Sphinx documentation (which requires SuperCollider!) and checks every external link found in the documentation to ensure nothing is broken
It builds the same wheels that would be published to PyPI to prove that package publishing is possible on the current commit
It format-checks, lints and type-checks the entire codebase, including the test suite
It gut-checks that Supriya is installable and importable without any of its optional dependencies
It gut-checks that Supriya’s shared memory works
It runs pytest against both the unit tests and doctests under all operating systems and all major, non-end-of-life, non-alpha versions of Python (e.g. 3.10, 3.11, 3.12, 3.13)