diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 9ea622c..92996f8 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -1,91 +1,74 @@ -name: 'Test and release Yakut' +name: 'Test & Release' on: [ push, pull_request ] -# Ensures that only one workflow is running at a time -concurrency: - group: ${{ github.workflow_sha }} - cancel-in-progress: true - jobs: - yakut-test: - name: Test Yakut + test: + name: Test # https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=edited#pull_request if: (github.event_name == 'push') || github.event.pull_request.head.repo.fork strategy: fail-fast: false matrix: - # The Windows NPcap runner is an ordinary Windows machine with the NPcap driver installed manually. - # We chose to do it this way because NPcap driver installation requires a reboot, which is difficult to - # automate. The NPcap driver is required for the Cyphal/UDP transport tests to work. - os: [ubuntu-22.04, windows-2019-npcap] - python: ['3.8', '3.9', '3.10', '3.11'] - exclude: # We don't test Windows with old Python versions because it takes too much effort. - - os: windows-2019-npcap - python: 3.8 - - os: windows-2019-npcap - python: 3.9 + os: [ ubuntu-latest ] + py: [ '3.10', '3.11', '3.12', '3.13' ] + # On Windows, we select the configurations we test manually because we only have a few runners, + # and because the infrastructure is hard to maintain using limited resources. + include: + - { os: win-pcap, py: '3.12' } runs-on: ${{ matrix.os }} + env: + FORCE_COLOR: 1 steps: - - name: Check out - uses: actions/checkout@v3 - - - name: Install Python3 - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 with: - python-version: ${{ matrix.python }} + submodules: true - - name: Log Python version - run: python --version + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} - - name: Install dependencies + - name: Configure GNU/Linux + if: ${{ runner.os == 'Linux' }} # language=bash run: | - if [ "$RUNNER_OS" == "Linux" ]; then - sudo apt-get --ignore-missing update || true - sudo apt-get install -y linux-*-extra-$(uname -r) ncat - sudo apt-get install -y libsdl2-2.0-0 # For PySDL2. On Windows/macOS the binaries are pulled from PyPI. - sudo apt-get install -y libasound2-dev # For RtMidi. - fi - git submodule update --init --recursive - python -m pip install --upgrade pip setuptools nox - shell: bash + python --version + sudo apt-get --ignore-missing update || true + sudo apt-get install -y linux-*-extra-$(uname -r) ncat + sudo apt-get install -y libsdl2-2.0-0 # For PySDL2. On Windows/macOS the binaries are pulled from PyPI. + sudo apt-get install -y libasound2-dev # For RtMidi. - - name: Run build and test - # language=bash - run: | - nox --non-interactive --session test --python ${{ matrix.python }} - nox --non-interactive --session lint - shell: bash - env: - FORCE_COLOR: 1 + # Only one statement per step to ensure the error codes are not ignored by PowerShell. + - run: python -m pip install --upgrade attrs pip setuptools nox + - run: nox --non-interactive --session test --python ${{ matrix.py }} + - run: nox --non-interactive --session lint - - name: Upload diagnostics - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: (success() || failure()) with: # The matrix is shown for convenience but this is fragile because the values may not be string-convertible. # Shall it break one day, feel free to remove the matrix from here. - # The job status is per matrix item, which is super convenient. name: ${{github.job}}-#${{strategy.job-index}}-${{job.status}}-${{join(matrix.*, ',')}} path: "**/*.log" - retention-days: 7 + retention-days: 90 + include-hidden-files: true - yakut-release: - name: Release Yakut + release: + name: Release runs-on: ubuntu-latest if: > (github.event_name == 'push') && (contains(github.event.head_commit.message, '#release') || contains(github.ref, '/main')) - needs: yakut-test + needs: test steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: true - name: Create distribution wheel # language=bash run: | - git submodule update --init --recursive - python -m pip install --upgrade pip setuptools wheel twine + python -m pip install --upgrade packaging pip setuptools wheel twine python setup.py sdist bdist_wheel - name: Get release version @@ -99,7 +82,7 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_YAKUT }} - name: Push version tag - uses: mathieudutour/github-tag-action@v6.1 + uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ env.yakut_version }} diff --git a/.gitignore b/.gitignore index 64c63cc..d964ee0 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,3 @@ coverage.xml # Compiled namespaces .*compiled -/uavcan/ -/reg/ -/sirius_cyber_corp/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..22928b4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/deps/public_regulated_data_types"] + path = tests/deps/public_regulated_data_types + url = https://github.com/OpenCyphal/public_regulated_data_types diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51a489b..3f7a449 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ A more interactive approach is as follows: 3. Change directory to `.nox/test-3-8/tmp`, here substitute `test-3-8` for the directory you have. This is one of the environments that Nox creates for testing. 4. Run `source ../bin/activate` to activate the virtualenv. -5. `export PYTHONPATH=.compiled/` +5. Optionally: `export PYCYPHAL_PATH=...compiled/` 6. Run specific commands you need: `pytest ../../../yakut/whatever`, `mypy --strict ../../../yakut ../../../tests`, etc. @@ -74,11 +74,10 @@ To look for manual tests in the codebase, please search for `def _main` under `t We recommend [JetBrains PyCharm](https://www.jetbrains.com/pycharm/) for development. -The test suite stores compiled DSDL into `.compiled/` in the current working directory -(when using Nox, the current working directory may be under a virtualenv private directory). +The test suite stores compiled DSDL into whatever is pointed to by `PYCYPHAL_PATH`; +the Noxfile overrides `PYCYPHAL_PATH` with a directory inside the venv. Make sure to mark it as a source directory to enable code completion and type analysis in the IDE (for PyCharm: right click -> Mark Directory As -> Sources Root). -Alternatively, you can just compile DSDL manually directly in the project root. Configure the IDE to run Black on save. See the Black documentation for integration instructions. diff --git a/README.md b/README.md index 617b5a7..30767f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Yakut +# Yakut – Cyphal CLI tool OpenCyphal logo @@ -23,8 +23,6 @@ Afterward do endeavor to read the docs: **`yakut --help`** Check for new versions every now and then: **`pip install --upgrade yakut`** -Installation & configuration screencasts are available for [Windows](https://forum.opencyphal.org/t/screencast-of-installing-configuring-yakut/1197/2?u=pavel.kirienko), [GNU/Linux](https://forum.opencyphal.org/t/screencast-of-installing-configuring-yakut/1197/1?u=pavel.kirienko), and [macOS](https://www.youtube.com/watch?v=dQw4w9WgXcQ). - ### Additional third-party tools Since Yakut heavily relies on YAML/JSON documents exchanged via stdin/stdout, [**`jq`**](https://stedolan.github.io/jq/) is often needed for any non-trivial usage of the tool, so consider installing it as well. Users of GNU/Linux will likely find it in the default software repositories (`pacman -S jq`, `apt install jq`, etc.). @@ -33,9 +31,9 @@ Since Yakut heavily relies on YAML/JSON documents exchanged via stdin/stdout, [* Transport layer inspection tools: +- [Wireshark](https://www.wireshark.org/) with [Cyphal plugins](https://github.com/OpenCyphal/wireshark_plugins) + (n.b.: some versions of Wireshark may label Cyphal/CAN captures as UAVCAN/CAN due to rebranding). - Cyphal/CAN on GNU/Linux (candump, canbusload, etc.): [`can-utils`](https://github.com/linux-can/can-utils) -- Cyphal/UDP or Cyphal/CAN: [Wireshark](https://www.wireshark.org/) - (n.b.: Wireshark might label Cyphal captures as UAVCAN due to rebranding) ## Invoking commands diff --git a/noxfile.py b/noxfile.py index 72fedad..3d1febc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 OpenCyphal +# Copyright (c) OpenCyphal # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko # type: ignore @@ -14,8 +14,13 @@ DEPS_DIR = ROOT_DIR / "tests" / "deps" assert DEPS_DIR.is_dir(), "Invalid configuration" +CYPHAL_PATH = [ + DEPS_DIR / "public_regulated_data_types", + ROOT_DIR / "tests" / "custom_data_types", +] -PYTHONS = ["3.8", "3.9", "3.10", "3.11"] + +PYTHONS = ["3.10", "3.11", "3.12", "3.13"] @nox.session(python=False) @@ -50,9 +55,9 @@ def test(session): # Now we can install dependencies for the full integration test suite. session.install( - "pytest ~= 7.4", - "pytest-asyncio ~= 0.21.0", - "coverage ~= 7.4", + "pytest ~= 8.3", + "pytest-asyncio ~= 0.26.0", + "coverage ~= 7.8", ) # The test suite generates a lot of temporary files, so we change the working directory. @@ -82,13 +87,12 @@ def test(session): *session.posargs, env={ "PYTHONPATH": str(DEPS_DIR), - "PATH": os.pathsep.join([session.env["PATH"], str(DEPS_DIR)]), + "PATH": os.pathsep.join([os.environ["PATH"], str(DEPS_DIR)]), + "CYPHAL_PATH": os.pathsep.join(map(str, CYPHAL_PATH)), + "PYCYPHAL_PATH": str(tmp_dir / ".compiled"), + "PYCYPHAL_LOGLEVEL": "ERROR", }, ) - - # The coverage threshold is intentionally set low for interactive runs because when running locally - # in a reused virtualenv the DSDL compiler run may be skipped to save time, resulting in a reduced coverage. - # Some features are not available on Windows so the coverage threshold is set low for it. if session.posargs or session.interactive or sys.platform.startswith("win"): fail_under = 1 else: @@ -104,14 +108,14 @@ def test(session): # 1. It requires access to the code generated by the test suite. # 2. It has to be run separately per Python version we support. # If the interpreter is not CPython, this may need to be conditionally disabled. - session.install("mypy ~= 1.8") - session.run("mypy", "--strict", *map(str, src_dirs)) + session.install("mypy ~= 1.15.0") + session.run("mypy", *map(str, src_dirs)) @nox.session(reuse_venv=True) def lint(session): - session.install("pylint ~= 3.0.3") + session.install("pylint ~= 3.3.7") session.run("pylint", "yakut", "tests") - session.install("black ~= 23.12") + session.install("black ~= 25.1") session.run("black", "--check", ".") diff --git a/setup.cfg b/setup.cfg index cc101f5..0c0d5d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,7 @@ zip_safe = False include_package_data = True packages = find: install_requires = - pycyphal[transport-udp,transport-serial,transport-can-pythoncan] ~= 1.8 + pycyphal[transport-udp,transport-serial,transport-can-pythoncan] ~= 1.20 ruamel.yaml < 0.18 requests ~= 2.27 simplejson ~= 3.17 @@ -100,9 +100,10 @@ log_file = pytest.log log_file_level = DEBUG # Unraisable exceptions are filtered because PyTest yields false-positives coming from PyCyphal. addopts = --doctest-modules -v -p no:unraisableexception -asyncio_mode = auto filterwarnings = ignore:.*SDL2.*:UserWarning +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function # ---------------------------------------- MYPY ---------------------------------------- [mypy] @@ -114,15 +115,19 @@ disallow_untyped_defs = True check_untyped_defs = True no_implicit_optional = True warn_redundant_casts = True -warn_unused_ignores = True +warn_unused_ignores = False show_error_context = True -strict_equality = True +strict_equality = False +strict = False implicit_reexport = False # We don't want MyPy to go checking generated code and its dependencies. follow_imports = silent mypy_path = .compiled +[mypy-nunavut_support] +ignore_errors = True + [mypy-pytest.*] ignore_missing_imports = True @@ -167,6 +172,7 @@ exclude_lines = # ---------------------------------------- PYLINT ---------------------------------------- [pylint.MASTER] fail-under=9.9 +ignore-paths=^.*/\.compiled/.*$ [pylint.MESSAGES CONTROL] # Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. diff --git a/tests/cmd/accommodate.py b/tests/cmd/accommodate.py index 768b1d5..648a355 100644 --- a/tests/cmd/accommodate.py +++ b/tests/cmd/accommodate.py @@ -4,14 +4,11 @@ from __future__ import annotations import time -import typing from tests.subprocess import Subprocess, execute_cli -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory -def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_accommodate_swarm(transport_factory: TransportFactory) -> None: # We spawn a lot of processes here, which might strain the test system a little, so beware. I've tested it # with 120 processes and it made my workstation (24 GB RAM ~4 GHz Core i7) struggle to the point of being # unable to maintain sufficiently real-time operation for the test to pass. Hm. @@ -19,7 +16,6 @@ def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_ds pubs = [ Subprocess.cli( f"--transport={transport_factory(idx).expression}", - f"--path={OUTPUT_DIR}", "pub", "--period=0.4", "--count=60", @@ -29,7 +25,6 @@ def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_ds time.sleep(5) # Some time is required for the nodes to start. _, stdout, _ = execute_cli( "-v", - f"--path={OUTPUT_DIR}", f"--transport={transport_factory(None).expression}", "accommodate", timeout=100.0, @@ -42,7 +37,6 @@ def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_ds def _unittest_accommodate_loopback() -> None: _, stdout, _ = execute_cli( "-v", - f"--path={OUTPUT_DIR}", "accommodate", timeout=30.0, environment_variables={"YAKUT_TRANSPORT": "Loopback(None),Loopback(None)"}, @@ -53,7 +47,6 @@ def _unittest_accommodate_loopback() -> None: def _unittest_accommodate_udp_localhost() -> None: _, stdout, _ = execute_cli( "-v", - f"--path={OUTPUT_DIR}", "accommodate", timeout=30.0, environment_variables={"YAKUT_TRANSPORT": 'UDP("127.0.0.1",None)'}, diff --git a/tests/cmd/call.py b/tests/cmd/call.py index 06b6ab8..7c60c77 100644 --- a/tests/cmd/call.py +++ b/tests/cmd/call.py @@ -9,7 +9,6 @@ import typing import pytest from tests.subprocess import Subprocess, execute_cli -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory from yakut.param.transport import construct_transport @@ -18,19 +17,16 @@ @pytest.mark.asyncio -async def _unittest_call_custom(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: +async def _unittest_call_custom(transport_factory: TransportFactory) -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 - - _ = compiled_dsdl env = { "YAKUT_TRANSPORT": transport_factory(88).expression, - "YAKUT_PATH": str(OUTPUT_DIR), "PYCYPHAL_LOGLEVEL": "INFO", # We don't want too much output in the logs. } import pycyphal.application import uavcan.node - from sirius_cyber_corp import PerformLinearLeastSquaresFit_1 + from sirius_cyber_corp import PerformLinearLeastSquaresFit_1 # type: ignore # Set up the server that we will be testing the client against. server_node = pycyphal.application.make_node( @@ -184,13 +180,10 @@ async def handle_request( @pytest.mark.asyncio -async def _unittest_call_fixed(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: +async def _unittest_call_fixed(transport_factory: TransportFactory) -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 - - _ = compiled_dsdl env = { "YAKUT_TRANSPORT": transport_factory(88).expression, - "YAKUT_PATH": str(OUTPUT_DIR), "PYCYPHAL_LOGLEVEL": "INFO", # We don't want too much output in the logs. } @@ -224,18 +217,12 @@ async def _unittest_call_fixed(transport_factory: TransportFactory, compiled_dsd await asyncio.sleep(1.0) -def _unittest_call_errors(compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl - env = { - "YAKUT_PATH": str(OUTPUT_DIR), - } - +def _unittest_call_errors() -> None: # Non-service data type. result, stdout, stderr = execute_cli( "call", "22", "222:sirius_cyber_corp.PointXY", - environment_variables=env, ensure_success=False, log=False, ) @@ -253,11 +240,9 @@ def _unittest_call_errors(compiled_dsdl: typing.Any) -> None: ) assert result != 0 assert stdout == "" - assert "yakut compile" in stderr # Invalid YAML. result, stdout, stderr = execute_cli( - f"--path={OUTPUT_DIR}", "call", "22", "222:sirius_cyber_corp.PerformLinearLeastSquaresFit.1", diff --git a/tests/cmd/execute_command.py b/tests/cmd/execute_command.py index df0d7d2..55d1d7c 100644 --- a/tests/cmd/execute_command.py +++ b/tests/cmd/execute_command.py @@ -9,7 +9,6 @@ import concurrent.futures import pytest import pycyphal -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory from tests.subprocess import execute_cli from yakut.util import EXIT_CODE_UNSUCCESSFUL @@ -48,12 +47,8 @@ def close(self) -> None: @pytest.fixture -async def _context( - compiled_dsdl: Any, - transport_factory: TransportFactory, -) -> AsyncIterable[tuple[Runner, tuple[Remote, Remote]]]: +async def _context(transport_factory: TransportFactory) -> AsyncIterable[tuple[Runner, tuple[Remote, Remote]]]: asyncio.get_running_loop().slow_callback_duration = 10.0 - _ = compiled_dsdl remote_nodes = ( Remote(f"remote_10", env=transport_factory(10).environment), Remote(f"remote_11", env=transport_factory(11).environment), @@ -65,10 +60,7 @@ def call() -> tuple[int, Any]: status, stdout, _stderr = execute_cli( "cmd", *args, - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, timeout=10, ensure_success=False, ) @@ -94,15 +86,15 @@ async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> Non assert await run("10-12", "restart", "--timeout=3") == ( 0, { - "10": {"status": 0}, - "11": {"status": 0}, + "10": {"output": "", "status": 0}, + "11": {"output": "", "status": 0}, }, ) assert await run("10-12", "111", "COMMAND ARGUMENT", "--timeout=3") == ( 0, { - "10": {"status": 0}, - "11": {"status": 0}, + "10": {"output": "", "status": 0}, + "11": {"output": "", "status": 0}, }, ) assert ( @@ -122,15 +114,15 @@ async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> Non assert await run("10-12", "restart", "--timeout=3") == ( EXIT_CODE_UNSUCCESSFUL, { - "10": {"status": 100}, - "11": {"status": 200}, + "10": {"output": "", "status": 100}, + "11": {"output": "", "status": 200}, }, ) assert await run("10-12", "123", "--expect=100,200", "--timeout=3") == ( 0, { - "10": {"status": 100}, - "11": {"status": 200}, + "10": {"output": "", "status": 100}, + "11": {"output": "", "status": 200}, }, ) assert remote_10.last_request and remote_10.last_request.command == 123 @@ -143,14 +135,14 @@ async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> Non EXIT_CODE_UNSUCCESSFUL, { "10": None, - "11": {"status": 0}, + "11": {"output": "", "status": 0}, }, ) assert await run("10-12", "123", "--expect") == ( 0, { "10": None, - "11": {"status": 0}, + "11": {"output": "", "status": 0}, }, ) @@ -158,7 +150,7 @@ async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> Non remote_11.next_response = ExecuteCommand_1.Response(status=210) assert await run("11", "123", "FOO BAR", "--timeout=3") == ( EXIT_CODE_UNSUCCESSFUL, - {"status": 210}, + {"output": "", "status": 210}, ) assert ( remote_11.last_request @@ -167,7 +159,7 @@ async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> Non ) assert await run("11", "222", "--timeout=3", "--expect=0..256") == ( 0, - {"status": 210}, + {"output": "", "status": 210}, ) assert ( remote_11.last_request diff --git a/tests/cmd/file_server.py b/tests/cmd/file_server.py index fe79703..fbb89f7 100644 --- a/tests/cmd/file_server.py +++ b/tests/cmd/file_server.py @@ -10,18 +10,17 @@ import tempfile from pathlib import Path from typing import Tuple, Optional +import pytest import pycyphal from pycyphal.transport.udp import UDPTransport from tests.subprocess import Subprocess -from tests.dsdl import OUTPUT_DIR -async def _unittest_file_server_pnp(compiled_dsdl: typing.Any) -> None: +async def _unittest_file_server_pnp() -> None: from pycyphal.application import make_node, NodeInfo, make_registry - from pycyphal.application.file import FileClient + from pycyphal.application.file import FileClient2 from pycyphal.application.plug_and_play import Allocatee - _ = compiled_dsdl asyncio.get_running_loop().slow_callback_duration = 10.0 root = tempfile.mkdtemp(".file_server", "root.") print("ROOT:", root) @@ -30,26 +29,15 @@ async def _unittest_file_server_pnp(compiled_dsdl: typing.Any) -> None: "file-server", root, f"--plug-and-play={root}/allocation_table.db", - environment_variables={ - "UAVCAN__UDP__IFACE": "127.0.0.1", - "UAVCAN__NODE__ID": "42", - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "42"}, ) cln_node = make_node( NodeInfo(name="org.opencyphal.yakut.test.file.client"), - make_registry( - None, - { - "UAVCAN__UDP__IFACE": "127.0.0.1", - "UAVCAN__NODE__ID": "43", - "YAKUT_PATH": str(OUTPUT_DIR), - }, - ), + make_registry(None, {"UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "43"}), ) try: - fc = FileClient(cln_node, 42, response_timeout=15.0) - await asyncio.sleep(3.0) # Let the server initialize. + fc = FileClient2(cln_node, 42, response_timeout=30.0) + await asyncio.sleep(10.0) # Let the server initialize. assert srv_proc.alive async def ls(path: str) -> typing.List[str]: @@ -60,12 +48,13 @@ async def ls(path: str) -> typing.List[str]: # Check the file server. assert ["allocation_table.db"] == await ls("/") - assert 0 == await fc.touch("/foo") + await fc.touch("/foo") assert ["allocation_table.db", "foo"] == await ls("/") - assert 0 == await fc.write("/foo", b"Hello world!") + await fc.write("/foo", b"Hello world!") assert b"Hello world!" == await fc.read("/foo") - assert 0 == await fc.remove("/foo") - assert 0 != await fc.remove("/foo") + await fc.remove("/foo") + with pytest.raises(pycyphal.application.file.RemoteFileError): + await fc.remove("/foo") assert ["allocation_table.db"] == await ls("/") # Check the allocator. @@ -88,12 +77,11 @@ async def ls(path: str) -> typing.List[str]: shutil.rmtree(root, ignore_errors=True) # Do not remove on failure for diagnostics. -async def _unittest_file_server_update(compiled_dsdl: typing.Any) -> None: +async def _unittest_file_server_update() -> None: from pycyphal.application import make_node, NodeInfo, make_registry, make_transport, Node from pycyphal.application.plug_and_play import Allocatee from uavcan.node import ExecuteCommand_1 as ExecuteCommand - _ = compiled_dsdl asyncio.get_running_loop().slow_callback_duration = 10.0 root = tempfile.mkdtemp(".file_server", "root.") print("ROOT:", root) @@ -138,10 +126,11 @@ async def new( if sw_vcs is not None: info.software_vcs_revision_id = sw_vcs if sw_crc is not None: - info.software_image_crc = [sw_crc] + info.software_image_crc = [sw_crc] # type: ignore - reg = make_registry(None, {"UAVCAN__UDP__IFACE": "127.0.0.1", "YAKUT_PATH": str(OUTPUT_DIR)}) + reg = make_registry(None, {"UAVCAN__UDP__IFACE": "127.0.0.1"}) trans = make_transport(reg) + assert trans if trans.local_node_id is None: print("Starting a node-ID allocator for", info.unique_id.tobytes()) alloc = Allocatee(trans, info.unique_id.tobytes()) @@ -156,6 +145,7 @@ async def new( reg["uavcan.node.id"] = allocated trans.close() trans = make_transport(reg) + assert trans assert trans.local_node_id is not None return RemoteNode(make_node(info, reg, transport=trans), execute_command_response) @@ -171,7 +161,6 @@ def __del__(self) -> None: environment_variables={ "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "42", - "YAKUT_PATH": str(OUTPUT_DIR), }, ) try: diff --git a/tests/cmd/main.py b/tests/cmd/main.py index d634995..33e43a2 100644 --- a/tests/cmd/main.py +++ b/tests/cmd/main.py @@ -13,7 +13,7 @@ def _unittest_help() -> None: """ execute_cli("--help", timeout=10.0, log=False) for cmd in dir(yakut.cmd): - if not cmd.startswith("_") and cmd not in ("pycyphal", "sys"): + if not cmd.startswith("_") and not cmd.startswith("@") and cmd not in ("pycyphal", "sys"): execute_cli(cmd.replace("_", "-"), "--help", timeout=3.0, log=False) diff --git a/tests/cmd/monitor.py b/tests/cmd/monitor.py index fa423f0..90349d1 100755 --- a/tests/cmd/monitor.py +++ b/tests/cmd/monitor.py @@ -3,10 +3,6 @@ # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko -# Disable unused ignore warning for this file only because there appears to be no other way to make MyPy -# accept this file both on Windows and GNU/Linux. -# mypy: warn_unused_ignores=False - from typing import Any, Optional, Awaitable import sys import socket @@ -17,14 +13,15 @@ import pycyphal from pycyphal.transport.udp import UDPTransport from tests.subprocess import Subprocess -from tests.dsdl import OUTPUT_DIR import yakut +if sys.platform.startswith("win"): # pragma: no cover + pytest.skip("These are GNU/Linux-only tests", allow_module_level=True) + # noinspection SpellCheckingInspection @pytest.mark.asyncio -async def _unittest_monitor_nodes(compiled_dsdl: Any) -> None: - _ = compiled_dsdl +async def _unittest_monitor_nodes() -> None: asyncio.get_running_loop().slow_callback_duration = 10.0 asyncio.get_running_loop().set_exception_handler(lambda *_: None) @@ -109,41 +106,6 @@ async def _unittest_monitor_nodes(compiled_dsdl: Any) -> None: await asyncio.sleep(3.0) -# noinspection SpellCheckingInspection -@pytest.mark.asyncio -async def _unittest_monitor_errors(compiled_dsdl: Any) -> None: - _ = compiled_dsdl - asyncio.get_running_loop().slow_callback_duration = 10.0 - asyncio.get_running_loop().set_exception_handler(lambda *_: None) - - # This time the monitor node is anonymous. - task = asyncio.gather( - _run_nodes(), - _run_zombie(), - _delay(_inject_error(), 7.0), - ) - cells = [x.split() for x in (await _monitor_and_get_last_screen(30.0, None)).splitlines()] - task.cancel() - await asyncio.sleep(3.0) - - assert cells[1][0] == "1111" - assert cells[1][9] == "?" # Unable to query - - assert cells[2][0] == "1234" - - assert cells[3][0] == "2571" - assert cells[3][4] == "zombie" - - assert cells[4][0] == "3210" - - assert cells[5][0] == "3333" - - # Error counter - assert cells[-1][2] == "1" - - await asyncio.sleep(3.0) - - async def _monitor_and_get_last_screen(duration: float, node_id: Optional[int]) -> str: args = ["monitor"] if node_id is not None: @@ -151,7 +113,6 @@ async def _monitor_and_get_last_screen(duration: float, node_id: Optional[int]) proc = Subprocess.cli( *args, environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": str(node_id if node_id is not None else 0xFFFF), }, @@ -202,8 +163,8 @@ def instantiate(info: NodeInfo, node_id: int, mode: int, health: int, vssc: int) } ) node = make_node(info, reg) - node.heartbeat_publisher.mode = mode - node.heartbeat_publisher.health = health + node.heartbeat_publisher.mode = mode # type: ignore + node.heartbeat_publisher.health = health # type: ignore node.heartbeat_publisher.vendor_specific_status_code = vssc node.start() return node diff --git a/tests/cmd/orchestrate/doc_examples.py b/tests/cmd/orchestrate/doc_examples.py index 76691c5..e21410e 100644 --- a/tests/cmd/orchestrate/doc_examples.py +++ b/tests/cmd/orchestrate/doc_examples.py @@ -30,7 +30,7 @@ def _unittest_example_basic() -> None: # Premature termination. started_at = time.monotonic() - proc = Subprocess.cli("-v", f"--path={src.absolute().parent}", "orc", str(src.name)) + proc = Subprocess.cli("-v", "orc", str(src.name)) time.sleep(5.0) exit_code, stdout, _stderr = proc.wait(timeout=10, interrupt=True) assert 5 <= time.monotonic() - started_at <= 9 @@ -46,24 +46,16 @@ def _unittest_example_external() -> None: Path("vars.orc.yaml").write_text(EXAMPLE_EXTERNAL_VARS) Path("echo.orc.yaml").write_text(EXAMPLE_EXTERNAL_ECHO) - exit_code, stdout, _ = execute_cli( - "-v", f"--path={Path(__file__).parent}", "orc", "ext.orc.yaml", timeout=60.0, ensure_success=False - ) + exit_code, stdout, _ = execute_cli("-v", "orc", "ext.orc.yaml", timeout=60.0, ensure_success=False) assert EXAMPLE_EXTERNAL_EXIT_CODE == exit_code assert stdout.splitlines() == EXAMPLE_EXTERNAL_STDOUT.splitlines() def _unittest_example_pub_sub() -> None: - from yakut.paths import DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI from yakut.cmd.orchestrate import EXAMPLE_PUB_SUB, EXAMPLE_PUB_SUB_STDOUT Path("pub_sub.orc.yaml").write_text(EXAMPLE_PUB_SUB) - _, stdout, _ = execute_cli( - "orc", - "pub_sub.orc.yaml", - environment_variables={"DSDL_SRC": DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI}, - timeout=300.0, - ) + _, stdout, _ = execute_cli("orc", "pub_sub.orc.yaml", timeout=300.0) # APPLY SORTING TO BATTLE TEMPORAL JITTER AS THE MESSAGE AND THE FIRST HEARTBEAT MAY COME SWAPPED. assert sorted(stdout.splitlines()) == sorted(EXAMPLE_PUB_SUB_STDOUT.splitlines()) diff --git a/tests/cmd/publish/basic.py b/tests/cmd/publish/basic.py index a8bb6fa..16670ef 100644 --- a/tests/cmd/publish/basic.py +++ b/tests/cmd/publish/basic.py @@ -3,22 +3,15 @@ # Author: Pavel Kirienko from __future__ import annotations -import typing -from tests.dsdl import OUTPUT_DIR from tests.subprocess import execute_cli -def _unittest_publish(compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl - env = { - "UAVCAN__LOOPBACK": "1", - "UAVCAN__NODE__ID": "1234", - } +def _unittest_publish() -> None: + env = {"UAVCAN__LOOPBACK": "1", "UAVCAN__NODE__ID": "1234"} # Count zero, nothing to do. _, _, stderr = execute_cli( "-vv", - f"--path={OUTPUT_DIR}", "pub", "4444:uavcan.si.unit.force.Scalar.1.0", "{}", @@ -29,22 +22,9 @@ def _unittest_publish(compiled_dsdl: typing.Any) -> None: ) assert "nothing to do" in stderr.lower() - # Compiled DSDL not found. - result, _, stderr = execute_cli( - "pub", - "4444:uavcan.si.unit.force.Scalar.1.0", - "{}", - timeout=5.0, - ensure_success=False, - environment_variables=env, - ) - assert result != 0 - assert "yakut compile" in stderr.lower() - # Invalid period. result, _, stderr = execute_cli( "-vv", - f"--path={OUTPUT_DIR}", "pub", "4444:uavcan.si.unit.force.Scalar.1.0", "{}", @@ -56,15 +36,3 @@ def _unittest_publish(compiled_dsdl: typing.Any) -> None: assert result != 0 assert "period" in stderr.lower() assert "seconds" in stderr.lower() - - # Transport not configured. - result, _, stderr = execute_cli( - f"--path={OUTPUT_DIR}", - "pub", - "4444:uavcan.si.unit.force.Scalar.1.0", - "{}", - timeout=5.0, - ensure_success=False, - ) - assert result != 0 - assert "transport" in stderr.lower() diff --git a/tests/cmd/publish/expression.py b/tests/cmd/publish/expression.py index 9c71450..725e06e 100644 --- a/tests/cmd/publish/expression.py +++ b/tests/cmd/publish/expression.py @@ -5,21 +5,16 @@ from __future__ import annotations import time import json -import typing from pytest import approx -from tests.dsdl import OUTPUT_DIR from tests.subprocess import execute_cli, Subprocess -def _unittest_publish_expression_a(compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl - +def _unittest_publish_expression_a() -> None: proc_sub = Subprocess.cli( "-j", "sub", "7654:uavcan.primitive.array.Real64.1.0", environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "1234", }, @@ -36,7 +31,6 @@ def _unittest_publish_expression_a(compiled_dsdl: typing.Any) -> None: "--count=2", timeout=10.0, environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "1235", }, @@ -66,14 +60,12 @@ def _unittest_publish_expression_a(compiled_dsdl: typing.Any) -> None: ] -def _unittest_publish_expression_b(compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_publish_expression_b() -> None: proc_sub = Subprocess.cli( "-j", "sub", "7654:uavcan.primitive.String.1.0", environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "1234", }, @@ -87,7 +79,6 @@ def _unittest_publish_expression_b(compiled_dsdl: typing.Any) -> None: "--count=1", timeout=10.0, environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "1235", }, diff --git a/tests/cmd/pubsub.py b/tests/cmd/pubsub.py index 769e0aa..cecdb55 100644 --- a/tests/cmd/pubsub.py +++ b/tests/cmd/pubsub.py @@ -7,21 +7,17 @@ import sys import time import json -import typing import pytest import pycyphal import yakut import yakut.yaml from tests.subprocess import execute_cli, Subprocess -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory -def _unittest_pub_sub_regular(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_pub_sub_regular(transport_factory: TransportFactory) -> None: env = { "YAKUT_TRANSPORT": transport_factory(None).expression, - "YAKUT_PATH": str(OUTPUT_DIR), } proc_sub_heartbeat = Subprocess.cli( "-j", @@ -78,7 +74,6 @@ def _unittest_pub_sub_regular(transport_factory: TransportFactory, compiled_dsdl # Request GetInfo from the publisher we just launched. _, stdout, _ = execute_cli( f"--transport={transport_factory(52).expression}", - f"--path={OUTPUT_DIR}", "-y", "call", "51", @@ -103,9 +98,9 @@ def _unittest_pub_sub_regular(transport_factory: TransportFactory, compiled_dsdl time.sleep(1.0) # Time to sync up # Parse the output from the subscribers and validate it. - out_sub_heartbeat = proc_sub_heartbeat.wait(1.0, interrupt=True)[1].splitlines() - out_sub_diagnostic = proc_sub_diagnostic.wait(1.0, interrupt=True)[1].splitlines() - out_sub_temperature = proc_sub_temperature.wait(1.0, interrupt=True)[1].splitlines() + out_sub_heartbeat = proc_sub_heartbeat.wait(5, interrupt=True)[1].splitlines() + out_sub_diagnostic = proc_sub_diagnostic.wait(5, interrupt=True)[1].splitlines() + out_sub_temperature = proc_sub_temperature.wait(5, interrupt=True)[1].splitlines() heartbeats = list(map(json.loads, out_sub_heartbeat)) diagnostics = list(map(json.loads, out_sub_diagnostic)) @@ -142,14 +137,12 @@ def _unittest_pub_sub_regular(transport_factory: TransportFactory, compiled_dsdl assert all(map(lambda mt: mt["555"]["kelvin"] == pytest.approx(123.456), temperatures)) assert proc_sub_diagnostic_wrong_pid.alive - assert proc_sub_diagnostic_wrong_pid.wait(1.0, interrupt=True)[1].strip() == "" + assert proc_sub_diagnostic_wrong_pid.wait(5, interrupt=True)[1].strip() == "" -def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory) -> None: env = { "YAKUT_TRANSPORT": transport_factory(None).expression, - "YAKUT_PATH": str(OUTPUT_DIR), } proc_sub_heartbeat = Subprocess.cli( "-j", @@ -187,9 +180,9 @@ def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compile time.sleep(2.0) # Time to sync up - assert proc_sub_heartbeat.wait(1.0, interrupt=True)[1].strip() == "", "Anonymous nodes must not broadcast heartbeat" + assert proc_sub_heartbeat.wait(5, interrupt=True)[1].strip() == "", "Anonymous nodes must not broadcast heartbeat" - diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_with_meta.wait(1.0, interrupt=True)[1].splitlines()) + diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_with_meta.wait(5, interrupt=True)[1].splitlines()) print("diagnostics:", diagnostics) # Remember that anonymous transfers over redundant transports are NOT deduplicated. # Hence, to support the case of redundant transports, we use 'greater or equal' here. @@ -201,7 +194,7 @@ def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compile assert m["8184"]["timestamp"]["microsecond"] == 0 assert m["8184"]["text"] == "" - diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_no_meta.wait(1.0, interrupt=True)[1].splitlines()) + diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_no_meta.wait(5, interrupt=True)[1].splitlines()) print("diagnostics:", diagnostics) assert len(diagnostics) >= 2 # >= because see above for m in diagnostics: @@ -209,8 +202,7 @@ def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compile assert m["8184"]["text"] == "" -def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_e2e_discovery_pub(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -221,7 +213,6 @@ def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_ds "--count=3", environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) # Let the subscriber boot up. @@ -235,7 +226,6 @@ def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_ds "--period=3", environment_variables={ "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) proc_pub.wait(30.0) @@ -245,8 +235,7 @@ def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_ds assert msgs == [{"1000": {"value": "hello"}, "2000": {"value": "world"}}] * 3 -def _unittest_e2e_discovery_sub(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_e2e_discovery_sub(transport_factory: TransportFactory) -> None: proc_pub = Subprocess.cli( "pub", "1000:uavcan.primitive.string", @@ -256,7 +245,6 @@ def _unittest_e2e_discovery_sub(transport_factory: TransportFactory, compiled_ds "--period=3", environment_variables={ "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) # Let the publisher boot up. @@ -270,7 +258,6 @@ def _unittest_e2e_discovery_sub(transport_factory: TransportFactory, compiled_ds "--count=3", environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) out_sub = proc_sub.wait(30.0)[1].splitlines() # discovery takes a while diff --git a/tests/cmd/pubsub_sync.py b/tests/cmd/pubsub_sync.py index ca11e4f..9eea9ea 100644 --- a/tests/cmd/pubsub_sync.py +++ b/tests/cmd/pubsub_sync.py @@ -5,14 +5,11 @@ from __future__ import annotations import time import json -import typing from tests.subprocess import Subprocess, execute_cli -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory -def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -23,7 +20,6 @@ def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory, compi "--smcf", # Automatic tolerance setting. environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) @@ -33,10 +29,7 @@ def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory, compi "!$ n * 1e6", "2000:uavcan.si.sample.mass.Scalar", "!$ (n + 0.4) * 1e6", # Introduce intentional divergence to ensure the tolerance is not too tight. - environment_variables={ - "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"YAKUT_TRANSPORT": transport_factory(11).expression}, ) out_sub = proc_sub.wait(30.0)[1].splitlines() proc_pub.wait(10.0, interrupt=True) @@ -57,8 +50,7 @@ def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory, compi ] -def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -68,7 +60,6 @@ def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory, com "--smcf=0.25", # Fixed tolerance setting; count not limited environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) @@ -80,7 +71,6 @@ def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory, com "!$ n * 1.09 * 1e6", # Timestamps will diverge after 3 publication cycles. environment_variables={ "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(9) @@ -104,10 +94,7 @@ def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory, com ] -def _unittest_monoclust_ts_field_type_not_timestamped( - transport_factory: TransportFactory, compiled_dsdl: typing.Any -) -> None: - _ = compiled_dsdl +def _unittest_monoclust_ts_field_type_not_timestamped(transport_factory: TransportFactory) -> None: code, stdout, stderr = execute_cli( "sub", "1000:uavcan.si.unit.mass.Scalar", @@ -115,7 +102,6 @@ def _unittest_monoclust_ts_field_type_not_timestamped( "--smcf", # Require timestamp field matching but the data types have no such field environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, timeout=10.0, ensure_success=False, @@ -126,8 +112,7 @@ def _unittest_monoclust_ts_field_type_not_timestamped( assert "synchro" in stderr -def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -138,7 +123,6 @@ def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory, com "--smca", # Automatic tolerance setting. environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) @@ -150,7 +134,6 @@ def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory, com "!$ str(n)", environment_variables={ "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) out_sub = proc_sub.wait(30.0)[1].splitlines() @@ -163,8 +146,7 @@ def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory, com ] -def _unittest_transfer_id(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_transfer_id(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -175,7 +157,6 @@ def _unittest_transfer_id(transport_factory: TransportFactory, compiled_dsdl: ty "--stid", environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) @@ -187,7 +168,6 @@ def _unittest_transfer_id(transport_factory: TransportFactory, compiled_dsdl: ty "!$ str(n)", environment_variables={ "YAKUT_TRANSPORT": transport_factory(11).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) out_sub = proc_sub.wait(30.0)[1].splitlines() @@ -200,8 +180,7 @@ def _unittest_transfer_id(transport_factory: TransportFactory, compiled_dsdl: ty ] -def _unittest_async(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: - _ = compiled_dsdl +def _unittest_async(transport_factory: TransportFactory) -> None: proc_sub = Subprocess.cli( "-j", "sub", @@ -211,14 +190,10 @@ def _unittest_async(transport_factory: TransportFactory, compiled_dsdl: typing.A "--count=4", environment_variables={ "YAKUT_TRANSPORT": transport_factory(10).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) time.sleep(3.0) - env = { - **transport_factory(11).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - } + env = transport_factory(11).environment execute_cli("pub", "--count=1", "1000:uavcan.primitive.String", "abc", environment_variables=env) execute_cli("pub", "--count=1", "2000:uavcan.primitive.String", "def", environment_variables=env) execute_cli("pub", "--count=2", "1000:uavcan.primitive.String", "ghi", environment_variables=env) diff --git a/tests/cmd/register_access.py b/tests/cmd/register_access.py index 699e770..42fd5de 100644 --- a/tests/cmd/register_access.py +++ b/tests/cmd/register_access.py @@ -8,20 +8,18 @@ import json from typing import Any import pytest -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory from tests.subprocess import execute_cli, Subprocess @pytest.mark.asyncio -async def _unittest_logic(compiled_dsdl: Any) -> None: +async def _unittest_logic() -> None: from pycyphal.transport.loopback import LoopbackTransport import pycyphal.application from pycyphal.application.register import ValueProxy, Natural64 from yakut.cmd.register_access._logic import access, Result from yakut.cmd.register_access._cmd import _make_representer - _ = compiled_dsdl repr_simple = _make_representer(simplify=True, metadata=False) repr_full = _make_representer(simplify=False, metadata=True) @@ -137,16 +135,12 @@ async def once(**kwargs: Any) -> Result: await asyncio.sleep(1) -def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> None: - _ = compiled_dsdl +def _unittest_cmd(transport_factory: TransportFactory) -> None: # Run a dummy node which we can query. bg_node = Subprocess.cli( "sub", "1000:uavcan.primitive.empty", - environment_variables={ - **transport_factory(10).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(10).environment, ) time.sleep(2) expect_register = "uavcan.node.description" @@ -160,7 +154,6 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No expect_register, environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) assert status == 0 @@ -178,7 +171,6 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No expect_register, environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) assert status == 0 @@ -196,7 +188,6 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "Reference value", environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), }, ) assert status == 0 diff --git a/tests/cmd/register_batch.py b/tests/cmd/register_batch.py index 4ff50a9..5334ba0 100644 --- a/tests/cmd/register_batch.py +++ b/tests/cmd/register_batch.py @@ -5,27 +5,23 @@ from __future__ import annotations import asyncio import time -from typing import Any import json import tempfile from pathlib import Path from pprint import pprint import pytest -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory from tests.subprocess import execute_cli, Subprocess @pytest.mark.asyncio -async def _unittest_caller(compiled_dsdl: Any) -> None: +async def _unittest_caller() -> None: from pycyphal.transport.loopback import LoopbackTransport import pycyphal.application from pycyphal.application.register import ValueProxy, Natural64, Value, String from yakut.cmd.register_batch._directive import Directive from yakut.cmd.register_batch._caller import Skipped, Timeout, TypeCoercionFailure, do_calls - _ = compiled_dsdl - node = pycyphal.application.make_node(pycyphal.application.NodeInfo(), transport=LoopbackTransport(10)) try: node.registry.clear() @@ -69,18 +65,14 @@ async def _unittest_caller(compiled_dsdl: Any) -> None: await asyncio.sleep(1) -def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> None: - _ = compiled_dsdl +def _unittest_cmd(transport_factory: TransportFactory) -> None: file = Path(tempfile.mktemp("yakut_register_batch_test.yaml")) # Run dummy nodes which we can query. bg_nodes = [ Subprocess.cli( "sub", "1000:uavcan.primitive.empty", - environment_variables={ - **transport_factory(10 + idx).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(10 + idx).environment, ) for idx in range(2) ] @@ -92,10 +84,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "register-batch", f"--file={file}", "--timeout=10", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -111,10 +100,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "register-batch", f"--file={file}", "--timeout=10", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -130,10 +116,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No f"--file={file}", "--timeout=10", "10", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -149,10 +132,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No f"--file={file}", "--timeout=10", "10,11", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -168,10 +148,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No f"--file={file}", "--timeout=3", # Shorter timeout here because we know one is going to time out. "10-13", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ensure_success=False, ) assert status != 0 @@ -189,10 +166,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No f"--file={file}", "--timeout=10", "10,11", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ensure_success=False, ) assert status != 0 @@ -210,10 +184,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "--timeout=10", "10,11", "--optional-register", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -230,10 +201,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "--timeout=10", "10", "--detailed", - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) @@ -249,10 +217,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "--timeout=10", "10", "--only=iv", # The requested register is not immutable-volatile so it will be skipped. - environment_variables={ - **transport_factory(100).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(100).environment, ) assert status == 0 data = json.loads(stdout.strip()) diff --git a/tests/cmd/register_list.py b/tests/cmd/register_list.py index 625314f..c472157 100644 --- a/tests/cmd/register_list.py +++ b/tests/cmd/register_list.py @@ -6,22 +6,18 @@ import asyncio import time import json -from typing import Any import pytest -from tests.dsdl import OUTPUT_DIR from tests.transport import TransportFactory from tests.subprocess import execute_cli, Subprocess @pytest.mark.asyncio -async def _unittest_logic(compiled_dsdl: Any) -> None: +async def _unittest_logic() -> None: from pycyphal.transport.loopback import LoopbackTransport import pycyphal.application from pycyphal.application.register import ValueProxy from yakut.cmd.register_list._logic import list_names - _ = compiled_dsdl - node = pycyphal.application.make_node(pycyphal.application.NodeInfo(), transport=LoopbackTransport(10)) try: node.registry.clear() @@ -75,16 +71,12 @@ async def _unittest_logic(compiled_dsdl: Any) -> None: await asyncio.sleep(1) -def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> None: - _ = compiled_dsdl +def _unittest_cmd(transport_factory: TransportFactory) -> None: # Run a dummy node which we can query. bg_node = Subprocess.cli( "sub", "1000:uavcan.primitive.empty", - environment_variables={ - **transport_factory(10).environment, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables=transport_factory(10).environment, ) time.sleep(2) expect_register = "uavcan.node.description" @@ -95,10 +87,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "register-list", "--timeout=10", "10", - environment_variables={ - "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"YAKUT_TRANSPORT": transport_factory(100).expression}, ) assert status == 0 data = json.loads(stdout.strip()) @@ -112,10 +101,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "register-list", "--timeout=10", "10,", # Mind the comma! - environment_variables={ - "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"YAKUT_TRANSPORT": transport_factory(100).expression}, ) assert status == 0 data = json.loads(stdout.strip()) @@ -129,10 +115,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "register-list", "--timeout=3", # Shorter timeout because some nodes are expected to not respond. "10..13", - environment_variables={ - "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"YAKUT_TRANSPORT": transport_factory(100).expression}, ensure_success=False, ) assert status != 0 # Because timed out @@ -150,10 +133,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No "--timeout=3", # Shorter timeout because some nodes are expected to not respond. "10..13", "--optional-service", - environment_variables={ - "YAKUT_TRANSPORT": transport_factory(100).expression, - "YAKUT_PATH": str(OUTPUT_DIR), - }, + environment_variables={"YAKUT_TRANSPORT": transport_factory(100).expression}, ) assert status == 0 data = json.loads(stdout.strip()) diff --git a/tests/cmd/subscribe.py b/tests/cmd/subscribe.py index a4a7d15..e4b1c76 100644 --- a/tests/cmd/subscribe.py +++ b/tests/cmd/subscribe.py @@ -3,18 +3,11 @@ # Author: Pavel Kirienko from __future__ import annotations -from typing import Any -from tests.dsdl import OUTPUT_DIR from tests.subprocess import execute_cli -def _unittest_subscribe(compiled_dsdl: Any) -> None: - _ = compiled_dsdl - env = { - "UAVCAN__LOOPBACK": "1", - "UAVCAN__NODE__ID": "1234", - "YAKUT_PATH": str(OUTPUT_DIR), - } +def _unittest_subscribe() -> None: + env = {"UAVCAN__LOOPBACK": "1", "UAVCAN__NODE__ID": "1234"} # No subjects specified. _, _, stderr = execute_cli("-vv", "sub", timeout=5.0, environment_variables=env) assert "nothing to do" in stderr.lower() @@ -27,31 +20,12 @@ def _unittest_subscribe(compiled_dsdl: Any) -> None: assert "count" in stderr.lower() -def _unittest_dsdl_not_found() -> None: +def _unittest_transport_not_specified() -> None: result, _, stderr = execute_cli( "sub", "4444:uavcan.si.unit.force.Scalar", timeout=5.0, ensure_success=False, - environment_variables={ - "UAVCAN__LOOPBACK": "1", - "UAVCAN__NODE__ID": "1234", - }, - ) - assert result != 0 - assert "yakut compile" in stderr.lower() - - -def _unittest_transport_not_specified(compiled_dsdl: Any) -> None: - _ = compiled_dsdl - result, _, stderr = execute_cli( - "sub", - "4444:uavcan.si.unit.force.Scalar", - timeout=5.0, - ensure_success=False, - environment_variables={ - "YAKUT_PATH": str(OUTPUT_DIR), - }, ) assert result != 0 assert "transport" in stderr.lower() diff --git a/tests/conftest.py b/tests/conftest.py index a8acf42..dee9df8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ from typing import Iterator from pathlib import Path import pytest -from .dsdl import compiled_dsdl as compiled_dsdl from .transport import transport_factory as transport_factory diff --git a/tests/deps/public_regulated_data_types b/tests/deps/public_regulated_data_types new file mode 160000 index 0000000..f9f6790 --- /dev/null +++ b/tests/deps/public_regulated_data_types @@ -0,0 +1 @@ +Subproject commit f9f67906cc0ca5d7c1b429924852f6b28f313cbf diff --git a/tests/dsdl.py b/tests/dsdl.py deleted file mode 100644 index 18ca5cc..0000000 --- a/tests/dsdl.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2019 OpenCyphal -# This software is distributed under the terms of the MIT License. -# Author: Pavel Kirienko - -import sys -import pathlib -import importlib -import pytest -from . import TEST_DIR - - -CUSTOM_DATA_TYPES_DIR = TEST_DIR / "custom_data_types" - -OUTPUT_DIR = pathlib.Path.cwd() / pathlib.Path(".compiled") -""" -The output directory needs to be added to YAKUT_PATH in order to use the compiled namespaces. -""" - - -@pytest.fixture() -def compiled_dsdl() -> None: - ensure_compiled_dsdl() - - -def ensure_compiled_dsdl() -> None: - """ - Ensures that the regulated DSDL namespaces are compiled and importable. - To force recompilation, remove the output directory. - """ - output_dir = str(OUTPUT_DIR) - if output_dir not in sys.path: - sys.path.insert(0, output_dir) - try: - import uavcan # pylint: disable=unused-import - import sirius_cyber_corp # pylint: disable=unused-import - except ImportError: - from tests.subprocess import execute_cli - from yakut.paths import DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI - - sirius_cyber_corp_dir = str(CUSTOM_DATA_TYPES_DIR / "sirius_cyber_corp") - - args = [ - "compile", - DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI, - "--lookup", - sirius_cyber_corp_dir, - "-O", - output_dir, - ] - execute_cli(*args, timeout=300.0) - - args = ["compile", sirius_cyber_corp_dir, "--output", output_dir] - execute_cli(*args, timeout=300.0) - - importlib.invalidate_caches() diff --git a/tests/dtype_loader.py b/tests/dtype_loader.py index 8e22eb0..5dcc4c8 100644 --- a/tests/dtype_loader.py +++ b/tests/dtype_loader.py @@ -2,21 +2,12 @@ # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko -import sys -from typing import Any import pytest import pycyphal from yakut.dtype_loader import load_dtype, FormatError, NotFoundError -from .dsdl import OUTPUT_DIR -def _unittest_dtype_loader(compiled_dsdl: Any) -> None: - _ = compiled_dsdl - sys.path.append(str(OUTPUT_DIR)) - - with pytest.raises(NotFoundError, match=r".*yakut compile.*unknown_root_namespace.*"): - _ = load_dtype("unknown_root_namespace.Type.1.0") - +def _unittest_dtype_loader() -> None: with pytest.raises(FormatError): _ = load_dtype("unknown_root_namespace.Type.1.0.0") diff --git a/tests/subject_specifier_processor.py b/tests/subject_specifier_processor.py index 74ae35b..042d2d1 100644 --- a/tests/subject_specifier_processor.py +++ b/tests/subject_specifier_processor.py @@ -14,8 +14,7 @@ @pytest.mark.asyncio -async def _unittest_without_subject_resolver(compiled_dsdl: Any) -> None: - _ = compiled_dsdl +async def _unittest_without_subject_resolver() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 import uavcan.primitive @@ -53,8 +52,7 @@ async def once(specifier: str) -> tuple[int, Any]: @pytest.mark.asyncio -async def _unittest_with_subject_resolver(compiled_dsdl: Any) -> None: - _ = compiled_dsdl +async def _unittest_with_subject_resolver() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 from pycyphal.application import make_node, NodeInfo, register diff --git a/tests/subprocess.py b/tests/subprocess.py index d2a21ca..00d0b99 100644 --- a/tests/subprocess.py +++ b/tests/subprocess.py @@ -225,6 +225,9 @@ def _read_stream(io: typing.Any) -> str: "PROGRAMDATA", "ALLUSERSPROFILE", "PUBLIC", + # Specific to us: + "CYPHAL_PATH", + "PYCYPHAL_PATH", } diff --git a/yakut/VERSION b/yakut/VERSION index 288adf5..a803cc2 100644 --- a/yakut/VERSION +++ b/yakut/VERSION @@ -1 +1 @@ -0.13.3 +0.14.0 diff --git a/yakut/__init__.py b/yakut/__init__.py index 1e6696c..4891ed1 100644 --- a/yakut/__init__.py +++ b/yakut/__init__.py @@ -10,14 +10,9 @@ def _read_package_file(name: str) -> str: - try: - from importlib.resources import files # type: ignore + from importlib.resources import files # type: ignore - return (files(__name__) / name).read_text(encoding="utf8") # type: ignore - except ImportError: # This is for the old Pythons; read_text is deprecated in 3.11 - from importlib.resources import read_text # type: ignore - - return read_text(__name__, name, encoding="utf8") # type: ignore + return (files(__name__) / name).read_text(encoding="utf8") # type: ignore __version__: str = _read_package_file("VERSION").strip() diff --git a/yakut/cmd/accommodate.py b/yakut/cmd/accommodate.py index 1d8f4d8..9338064 100644 --- a/yakut/cmd/accommodate.py +++ b/yakut/cmd/accommodate.py @@ -34,12 +34,7 @@ async def accommodate(purser: yakut.Purser) -> None: The listening duration is determined heuristically at run time; for most use cases it is unlikely to exceed three seconds. """ - try: - import uavcan.node - except (ImportError, AttributeError): - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion("uavcan")) from None + import uavcan.node transport = purser.get_transport() node_id_set_cardinality = transport.protocol_parameters.max_nodes @@ -51,8 +46,8 @@ async def accommodate(purser: yakut.Purser) -> None: return candidates = set(range(node_id_set_cardinality)) - try: - candidates.remove(transport.local_node_id) # Allow non-anonymous transports for consistency. + try: # Allow non-anonymous transports for consistency. + candidates.remove(transport.local_node_id) # type: ignore except LookupError: pass diff --git a/yakut/cmd/call.py b/yakut/cmd/call.py index e1045ab..a75cb69 100644 --- a/yakut/cmd/call.py +++ b/yakut/cmd/call.py @@ -113,12 +113,7 @@ async def call( yakut call 42 least_squares:sirius_cyber_corp.PerformLinearLeastSquaresFit '[[10, 1], [20, 2]]' yakut call 125 435:uavcan.node.ExecuteCommand '{command: 65533, parameter: "firmware.bin"}' """ - try: - from pycyphal.application import Node - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + from pycyphal.application import Node _logger.debug( "server_node_id=%s, service=%r, request_fields=%r, timeout=%.6f, priority=%s, with_metadata=%s", diff --git a/yakut/cmd/compile.py b/yakut/cmd/compile.py deleted file mode 100644 index 40d9481..0000000 --- a/yakut/cmd/compile.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2019 OpenCyphal -# This software is distributed under the terms of the MIT License. -# Author: Pavel Kirienko - -import re -import http -import typing -import zipfile -import tempfile -from pathlib import Path -import pycyphal -import click -import yakut -from yakut.paths import DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI - - -_NAME = "compile" - -_logger = yakut.get_logger(__name__) - - -def make_usage_suggestion(root_namespace_name: typing.Optional[str]) -> str: - """ - When a command is unable to find a compiled DSDL package, this helper can be used to construct a - human-friendly suggestion on how to resolve the problem. - """ - root_namespace_name = root_namespace_name or "" - root_namespace_name = root_namespace_name.split(".")[0] # Transform: `uavcan.node` --> `uavcan` - return f"Run `yakut {_NAME} /{root_namespace_name}` to compile DSDL namespace {root_namespace_name!r}" - - -@yakut.subcommand( - name=_NAME, - aliases="co", - help=f""" -Compile DSDL namespaces for use by Yakut. -This needs to be done before using any data types with pub/sub/call and other commands. - -The command accepts a list of sources where each element is either a local path -or an URI pointing to the source DSDL root namespace(s). - -If a source is a local path, it must point to a local DSDL root namespace directory or to a local archive containing -DSDL root namespace directories at the top level. -If the value is an URI, it must point to an archive containing DSDL root namespace directories at the top level -(this is convenient for generating packages from namespaces hosted in public repositories, e.g., on GitHub). - -See also: top-level option `--path` and related environment variable `YAKUT_PATH`. - -This command may be removed after https://github.com/OpenCyphal/pycyphal/issues/153 is implemented. - -Example path: ~/OpenCyphal/public_regulated_data_types/OpenCyphal/ - -Example URI: {DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI} - -Example command that compiles the root namespace `~/namespace` which depends on the public regulated types: - -\b - yakut {_NAME} ~/namespace --lookup {DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI} -""", -) -@click.argument("source", nargs=-1, required=True, type=str) -@click.option( - "--lookup", - "-L", - multiple=True, - type=str, - metavar="SOURCE", - help=f""" -This is like the sources except that the specified DSDL root namespace(s) will be used only for looking up dependencies. -Both local directories and URIs are accepted. -If a DSDL root namespace is specified as an input, it is automatically added to the look-up list as well. -""", -) -@click.option( - "--output", - "-O", - type=str, - help=f""" -Path to the directory where the compilation outputs will be stored. -If not specified, defaults to the current working directory. -Existing packages will be overwritten entirely. -""", -) -@click.option( - "--allow-unregulated-fixed-port-id", - is_flag=True, - help=""" -Instruct the DSDL front-end to accept unregulated data types with fixed port identifiers. -Make sure you understand the implications before using this option. -If not sure, ask for advice at https://forum.opencyphal.org. -""", -) -def compile_( - source: typing.Tuple[str, ...], - lookup: typing.Tuple[str, ...], - output: typing.Union[str, Path, None], - allow_unregulated_fixed_port_id: bool, -) -> None: - output = Path(output or Path.cwd()).resolve() - _logger.info("Destination: %r", str(output)) - - src_dirs: typing.List[Path] = [] - for location in source: - src_dirs += _fetch_root_namespace_dirs(location) - _logger.info("Source namespace dirs: %r", list(map(str, src_dirs))) - - lookup_dirs: typing.List[Path] = [] - for location in lookup: - lookup_dirs += _fetch_root_namespace_dirs(location) - _logger.info("Lookup namespace dirs: %r", list(map(str, lookup_dirs))) - - gpi_list = _generate_dsdl_packages( - source_root_namespace_dirs=src_dirs, - lookup_root_namespace_dirs=lookup_dirs, - generated_packages_dir=output, - allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id, - ) - for gpi in gpi_list: - _logger.info("Generated package %r with %d data types at %r", gpi.name, len(gpi.models), str(gpi.path)) - - -def _fetch_root_namespace_dirs(location: str) -> typing.List[Path]: - if "://" in location: - dirs = _fetch_archive_dirs(location) - _logger.info( - "Resource %r contains the following root namespace directories: %r", location, list(map(str, dirs)) - ) - return dirs - return [Path(location)] - - -def _fetch_archive_dirs(archive_uri: str) -> typing.List[Path]: - """ - Downloads an archive from the specified URI, unpacks it into a temporary directory, and returns the list of - directories in the root of the unpacked archive. - """ - # The requests package takes over 100 ms to import! Having it in the file scope is a performance disaster. - import requests # type: ignore - - # TODO: autodetect the type of the archive - arch_dir = tempfile.mkdtemp(prefix="yakut-dsdl-") - arch_file = str(Path(arch_dir) / "dsdl.zip") - - _logger.info("Downloading the archive from %r into %r...", archive_uri, arch_file) - response = requests.get(archive_uri) - if response.status_code != http.HTTPStatus.OK: - raise RuntimeError(f"Could not download the archive; HTTP error {response.status_code}") - with open(arch_file, "wb") as f: - f.write(response.content) - - _logger.info("Extracting the archive into %r...", arch_dir) - with zipfile.ZipFile(arch_file) as zf: - zf.extractall(arch_dir) - - (inner,) = [d for d in Path(arch_dir).iterdir() if d.is_dir()] # Strip the outer layer, we don't need it - - assert isinstance(inner, Path) - return [d for d in inner.iterdir() if d.is_dir() and _RE_VALID_ROOT_NAMESPACE_NAME.match(d.name)] - - -_RE_VALID_ROOT_NAMESPACE_NAME = re.compile(r"[a-zA-Z_]\w*") - - -def _generate_dsdl_packages( - source_root_namespace_dirs: typing.Iterable[Path], - lookup_root_namespace_dirs: typing.Iterable[Path], - generated_packages_dir: Path, - allow_unregulated_fixed_port_id: bool, -) -> typing.Sequence[pycyphal.dsdl.GeneratedPackageInfo]: - lookup_root_namespace_dirs = frozenset(list(lookup_root_namespace_dirs) + list(source_root_namespace_dirs)) - generated_packages_dir.mkdir(parents=True, exist_ok=True) - - out: typing.List[pycyphal.dsdl.GeneratedPackageInfo] = [] - for ns in source_root_namespace_dirs: - if ns.name.startswith("."): - _logger.debug("Skipping hidden directory %r", ns) - continue - dest_dir = generated_packages_dir / ns.name - _logger.info( - "Generating DSDL package %r from root namespace %r with lookup dirs: %r", - str(dest_dir), - str(ns), - list(map(str, lookup_root_namespace_dirs)), - ) - gpi = pycyphal.dsdl.compile( - root_namespace_directory=ns, - lookup_directories=list(lookup_root_namespace_dirs), - output_directory=generated_packages_dir, - allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id, - ) - if gpi is not None: - out.append(gpi) - return out diff --git a/yakut/cmd/execute_command/_cmd.py b/yakut/cmd/execute_command/_cmd.py index c2222e3..30e0dee 100644 --- a/yakut/cmd/execute_command/_cmd.py +++ b/yakut/cmd/execute_command/_cmd.py @@ -62,7 +62,8 @@ def _parse_status_set(inp: str) -> set[int] | None: return set(ins) if not isinstance(ins, (int, float)) else {ins} -@yakut.subcommand(aliases="cmd", help=_HELP) +# Click has this inexplicable "feature" where it removes the "_command" suffix from the command name. Oh my god. +@yakut.subcommand(aliases=["execute-command", "cmd"], help=_HELP) @click.argument("node_ids", type=parse_int_set) @click.argument("command") @click.argument("parameter", default="") @@ -112,12 +113,7 @@ async def execute_command( command_parsed = _parse_command(command) del command formatter = purser.make_formatter(FormatterHints(single_document=True)) - try: - from uavcan.node import ExecuteCommand_1 - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) from None + from uavcan.node import ExecuteCommand_1 request = ExecuteCommand_1.Request(command=command_parsed, parameter=parameter) # Ensure the parameters are valid before constructing the node. @@ -180,7 +176,7 @@ async def once(nid: int) -> None: cln = local_node.make_client(ExecuteCommand_1, nid) try: cln.response_timeout = timeout - result[nid] = await cln(request) + result[nid] = await cln(request) # type: ignore finally: cln.close() diff --git a/yakut/cmd/file_server/_app_descriptor.py b/yakut/cmd/file_server/_app_descriptor.py index 53b4766..556028e 100644 --- a/yakut/cmd/file_server/_app_descriptor.py +++ b/yakut/cmd/file_server/_app_descriptor.py @@ -183,10 +183,6 @@ def __str__(self) -> str: def _unittest_app_descriptor_from_node_info() -> None: - from tests.dsdl import ensure_compiled_dsdl - - ensure_compiled_dsdl() - from pycyphal.application import NodeInfo from uavcan.node import Version_1 as Version diff --git a/yakut/cmd/file_server/_cmd.py b/yakut/cmd/file_server/_cmd.py index 6f2f33c..ab30056 100644 --- a/yakut/cmd/file_server/_cmd.py +++ b/yakut/cmd/file_server/_cmd.py @@ -151,16 +151,11 @@ async def file_server( \b yakut file-server --plug-and-play=allocation_table.db --update-software """ - try: - from pycyphal.application import NodeInfo - from pycyphal.application.file import FileServer - from pycyphal.application.node_tracker import NodeTracker, Entry - from uavcan.node import ExecuteCommand_1 as ExecuteCommand - from uavcan.node import Heartbeat_1 as Heartbeat - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + from pycyphal.application import NodeInfo + from pycyphal.application.file import FileServer + from pycyphal.application.node_tracker import NodeTracker, Entry + from uavcan.node import ExecuteCommand_1 as ExecuteCommand + from uavcan.node import Heartbeat_1 as Heartbeat update_all_nodes = any(x < 0 for x in update_software) explicit_nodes = set(filter(lambda x: x >= 0, update_software)) diff --git a/yakut/cmd/monitor/_cmd.py b/yakut/cmd/monitor/_cmd.py index 71b71fa..9fe964a 100644 --- a/yakut/cmd/monitor/_cmd.py +++ b/yakut/cmd/monitor/_cmd.py @@ -78,13 +78,7 @@ async def monitor(purser: yakut.Purser, plug_and_play: Optional[str]) -> None: # pylint: disable=too-many-locals import numpy as np from scipy.sparse import dok_matrix, spmatrix - - try: - import uavcan.node.port # pylint: disable=unused-import - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + import uavcan.node.port # pylint: disable=unused-import allow_anonymous = not plug_and_play diff --git a/yakut/cmd/monitor/_model.py b/yakut/cmd/monitor/_model.py index eaa28e9..9fffb01 100644 --- a/yakut/cmd/monitor/_model.py +++ b/yakut/cmd/monitor/_model.py @@ -192,7 +192,7 @@ def expand_subjects(m: uavcan.node.port.SubjectIDList_1) -> AbstractSet[int]: if m.sparse_list is not None: return frozenset(int(x.value) for x in m.sparse_list) if m.mask is not None and m.mask.any(): - return expand_mask(m.mask) + return expand_mask(m.mask) if m.total: return _COMPLETE_SUBJECT_SET assert False diff --git a/yakut/cmd/monitor/_view.py b/yakut/cmd/monitor/_view.py index cfb070a..a33b1fc 100644 --- a/yakut/cmd/monitor/_view.py +++ b/yakut/cmd/monitor/_view.py @@ -337,8 +337,8 @@ def _render_subject_matrix_contents( xfer_rates: spmatrix, byte_rates: spmatrix, xfer_delta_by_port: NDArray[np.int_], - xfer_rates_by_port: NDArray[np.float_], - byte_rates_by_port: NDArray[np.float_], + xfer_rates_by_port: NDArray[np.float64], + byte_rates_by_port: NDArray[np.float64], ) -> None: recent_by_node: dict[Optional[int], bool] = defaultdict(bool) xfer_rate_by_node: dict[Optional[int], float] = defaultdict(float) @@ -386,8 +386,8 @@ def _render_service_matrix_contents( xfer_rates: tuple[spmatrix, spmatrix], byte_rates: tuple[spmatrix, spmatrix], xfer_delta_by_port: tuple[NDArray[np.int_], NDArray[np.int_]], - xfer_rates_by_port: tuple[NDArray[np.float_], NDArray[np.float_]], - byte_rates_by_port: tuple[NDArray[np.float_], NDArray[np.float_]], + xfer_rates_by_port: tuple[NDArray[np.float64], NDArray[np.float64]], + byte_rates_by_port: tuple[NDArray[np.float64], NDArray[np.float64]], ) -> None: # We used to display two rows per service: separate request and response. It is very informative but a bit # expensive in terms of the screen space, which is very limited when large networks are involved. diff --git a/yakut/cmd/orchestrate/__init__.py b/yakut/cmd/orchestrate/__init__.py index 9da656d..3be340e 100644 --- a/yakut/cmd/orchestrate/__init__.py +++ b/yakut/cmd/orchestrate/__init__.py @@ -83,10 +83,7 @@ def _indent(text: str) -> str: # language=YAML EXAMPLE_PUB_SUB = """ #!/usr/bin/env -S yakut orchestrate -# Compile DSDL and launch a pair of pub/sub. $=: -- yakut compile $DSDL_SRC # "DSDL_SRC" is to be set externally. -- # Wait for the compiler to finish. - yakut --format json sub -M -N2 33:uavcan.si.unit.angle.Scalar.1.0 - yakut --format json sub -M -N1 uavcan.diagnostic.Record.1.1 - $=: > # Inner composition with a single multi-line command. @@ -94,11 +91,7 @@ def _indent(text: str) -> str: 33:uavcan.si.unit.angle.Scalar.1.0 'radian: 4.0' uavcan.diagnostic.Record.1.1 'text: "four radians"' uavcan.node.id: 9 # The publisher is not anonymous unlike others. -.=: -- ?=: rm -r $YAKUT_COMPILE_OUTPUT # Clean up at the exit. uavcan.udp.iface: 127.42.0.1 # Configure the transport via registers. -YAKUT_COMPILE_OUTPUT: pub_sub_compiled_dsdl -YAKUT_PATH: pub_sub_compiled_dsdl """.strip() # language=YAML EXAMPLE_PUB_SUB_STDOUT = """ @@ -219,7 +212,7 @@ def _indent(text: str) -> str: The "external=" directive defines an external orc-file to be executed BEFORE the script directives of the current composition. The path is either absolute or relative; in the latter case, the file will be searched for -in the current working directory and then through the directories specified in YAKUT_PATH. +relative to the current working directory. The composition defined in the external file (the callee) inherits/overrides the environment variables from the caller. Upon successful execution, the caller inherits all environment variables back from the callee. @@ -289,9 +282,7 @@ def on_signal(s: int, _: Any) -> None: if not sys.platform.startswith("win"): signal.signal(signal.SIGHUP, on_signal) - ctx = Context( - lookup_paths=(Path.cwd(), *purser.paths), # Current directory takes precedence. - ) + ctx = Context(lookup_paths=(Path.cwd(),)) res = exec_file(ctx, file, {}, gate=lambda: sig_num == 0) return res if res != 0 else -sig_num diff --git a/yakut/cmd/orchestrate/_executor.py b/yakut/cmd/orchestrate/_executor.py index 10e559e..c05775b 100644 --- a/yakut/cmd/orchestrate/_executor.py +++ b/yakut/cmd/orchestrate/_executor.py @@ -192,9 +192,11 @@ def exec_shell( stack.log_info( *itertools.chain( (f"{prefix}EXECUTING WITH ENVIRONMENT VARIABLES:",), - ((k.ljust(longest_env) + " = " + repr(v.decode("raw_unicode_escape"))) for k, v in env.items()) - if env - else [""], + ( + ((k.ljust(longest_env) + " = " + repr(v.decode("raw_unicode_escape"))) for k, v in env.items()) + if env + else [""] + ), cmd.splitlines(), ), ) diff --git a/yakut/cmd/publish/_cmd.py b/yakut/cmd/publish/_cmd.py index 24f723c..39684b0 100644 --- a/yakut/cmd/publish/_cmd.py +++ b/yakut/cmd/publish/_cmd.py @@ -231,12 +231,7 @@ async def publish( if count <= 0: _logger.warning("Nothing to do because count=%s", count) return - try: - from pycyphal.application import Node - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + from pycyphal.application import Node finalizers: list[Callable[[], None]] = [] try: diff --git a/yakut/cmd/register_access/_logic.py b/yakut/cmd/register_access/_logic.py index e18d307..14a68fa 100644 --- a/yakut/cmd/register_access/_logic.py +++ b/yakut/cmd/register_access/_logic.py @@ -78,18 +78,19 @@ async def _access( timeout: float, ) -> dict[ int, - Union[ - _NoService, - _Timeout, - "Access_1.Response", - tuple["Access_1.Response", "pycyphal.application.register.ValueConversionError"], - ], + _NoService + | _Timeout + | "Access_1.Response" + | tuple["Access_1.Response", "pycyphal.application.register.ValueConversionError"], ]: from uavcan.register import Access_1 out: dict[ int, - Access_1.Response | _NoService | _Timeout | pycyphal.application.register.ValueConversionError, + Access_1.Response + | _NoService + | _Timeout + | tuple["Access_1.Response", "pycyphal.application.register.ValueConversionError"], ] = {} for nid in node_ids: progress(f"{nid: 5}: {reg_name!r}") diff --git a/yakut/cmd/register_batch/_caller.py b/yakut/cmd/register_batch/_caller.py index 30c8d75..5080c30 100644 --- a/yakut/cmd/register_batch/_caller.py +++ b/yakut/cmd/register_batch/_caller.py @@ -115,7 +115,7 @@ async def _process_one( # Send the write request with the updated coerced value. req = Access_1.Request(name=Name_1(register_name), value=coerced) assert isinstance(req.value, Value) - return await client(req) or Timeout() + return await client(req) or Timeout() # type: ignore _logger = yakut.get_logger(__name__) diff --git a/yakut/cmd/register_batch/_cmd.py b/yakut/cmd/register_batch/_cmd.py index 483dd7c..d83077e 100644 --- a/yakut/cmd/register_batch/_cmd.py +++ b/yakut/cmd/register_batch/_cmd.py @@ -159,7 +159,7 @@ async def register_batch( detailed: int, only: str | None, ) -> int: - predicate: Predicate = _PREDICATES[only] if only else lambda _: True + predicate: Predicate = _PREDICATES[only] if only else lambda _: True # type: ignore formatter = purser.make_formatter(FormatterHints(single_document=True)) representer = _make_representer(detail=detailed) with file: diff --git a/yakut/cmd/subscribe/_cmd.py b/yakut/cmd/subscribe/_cmd.py index b5f4321..f9eeb79 100644 --- a/yakut/cmd/subscribe/_cmd.py +++ b/yakut/cmd/subscribe/_cmd.py @@ -236,12 +236,7 @@ async def subscribe( y sub uavcan.node.heartbeat +M | jq '.[]|[._meta_.transfer_id, ._meta_.timestamp.system]' """ config = click.get_current_context().ensure_object(Config) - try: - from pycyphal.application import Node - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + from pycyphal.application import Node finalizers: list[Callable[[], None]] = [] try: diff --git a/yakut/cmd/subscribe/_sync_async.py b/yakut/cmd/subscribe/_sync_async.py index 08c7aef..f94bef6 100644 --- a/yakut/cmd/subscribe/_sync_async.py +++ b/yakut/cmd/subscribe/_sync_async.py @@ -48,10 +48,6 @@ async def fun(output: SynchronizerOutput) -> None: def _unittest_sync_async() -> None: - from tests.dsdl import ensure_compiled_dsdl - - ensure_compiled_dsdl() - from pycyphal.transport.loopback import LoopbackTransport from pycyphal.presentation import Presentation from uavcan.primitive.scalar import Integer8_1 diff --git a/yakut/controller/__init__.py b/yakut/controller/__init__.py index f1e4494..e5ec697 100644 --- a/yakut/controller/__init__.py +++ b/yakut/controller/__init__.py @@ -127,7 +127,7 @@ def handle_import_error(module_name: str, culprit: Exception) -> None: base = Controller # Order controller kinds by class name, but ensure that NullController always comes first. for ty in sorted( - pycyphal.util.iter_descendants(base), + pycyphal.util.iter_descendants(base), # type: ignore key=lambda x: (x is not NullController, x.__name__), ): try: diff --git a/yakut/dtype_loader.py b/yakut/dtype_loader.py index 2164dc0..c5f5e48 100644 --- a/yakut/dtype_loader.py +++ b/yakut/dtype_loader.py @@ -53,8 +53,6 @@ def load_dtype(name: str, allow_minor_version_mismatch: bool = False) -> Type[An def _load(name_components: list[str], major: int | None, minor: int | None) -> Type[Any]: - from yakut.cmd.compile import make_usage_suggestion - namespaces, short_name = name_components[:-1], name_components[-1] try: mod = None @@ -65,7 +63,7 @@ def _load(name_components: list[str], major: int | None, minor: int | None) -> T except ImportError: # We seem to have hit a reserved word; try with an underscore. mod = importlib.import_module(name + "_") except ImportError as ex: - raise NotFoundError(make_usage_suggestion(namespaces[0])) from ex + raise NotFoundError(f"Module not found: {ex.name}") from ex assert mod matches = sorted( ( diff --git a/yakut/main.py b/yakut/main.py index 50e0b48..b32fcbe 100644 --- a/yakut/main.py +++ b/yakut/main.py @@ -7,9 +7,8 @@ import sys import asyncio import functools -from typing import TYPE_CHECKING, Iterable, Optional, Any, Callable, Awaitable +from typing import TYPE_CHECKING, Optional, Any, Callable, Awaitable import logging -from pathlib import Path from shutil import get_terminal_size import click import yakut @@ -41,12 +40,10 @@ def get_logger(name: str) -> logging.Logger: class Purser: def __init__( self, - paths: Iterable[str | Path], formatter_factory: FormatterFactory, transport_factory: TransportFactory, node_factory: NodeFactory, ) -> None: - self._paths = list(Path(x) for x in paths) self._f_formatter = formatter_factory self._f_transport = transport_factory self._f_node = node_factory @@ -55,10 +52,6 @@ def __init__( self._transport: Optional[Transport] = None self._node: Optional["pycyphal.application.Node"] = None - @property - def paths(self) -> list[Path]: - return list(self._paths) - def make_formatter(self, hints: FormatterHints = FormatterHints()) -> Formatter: return self._f_formatter(hints) @@ -82,7 +75,7 @@ def get_transport(self) -> Transport: self._transport = self._f_transport() if self._transport is not None: return self._transport - click.get_current_context().fail("Transport not configured, or the standard DSDL namespace is not compiled") + click.get_current_context().fail("Transport not configured, or the standard DSDL namespace not found") def get_node(self, name_suffix: str, allow_anonymous: bool) -> "pycyphal.application.Node": if self._node is None: # pragma: no branch @@ -149,8 +142,8 @@ def resolve_command( This is a workaround for this bug in v7 and v8: https://github.com/pallets/click/issues/1422. If this is not overridden, then abbreviated commands cause the automatic envvar prefix to be constructed incorrectly, such that instead of the full command name the abbreviated name is used. - For example, if the user invokes `yakut co` meaning `yakut compile`, - the auto-constructed envvar prefix would be `YAKUT_CO_` instead of `YAKUT_COMPILE_`. + For example, if the user invokes `yakut pub` meaning `yakut publish`, + the auto-constructed envvar prefix would be `YAKUT_PUB_` instead of `YAKUT_PUBLISH_`. """ _, cmd, out_args = super().resolve_command(ctx, args) return (cmd.name if cmd else None), cmd, out_args @@ -191,25 +184,6 @@ def _mk_aliases(item: Any) -> set[str]: ) @click.version_option(version=yakut.__version__) @click.option("--verbose", "-v", count=True, help="Emit verbose log messages. Specify twice for extra verbosity.") -@click.option( - "--path", - "-P", - multiple=True, - type=click.Path(resolve_path=True), - help=f""" -In order to use compiled DSDL namespaces, -the directories that contain compilation outputs need to be specified using this option. - -Examples: - -\b - yakut --path ../public_regulated_data_types --path ~/my_namespaces pub ... - -\b - export {_ENV_VAR_PATH}="../public_regulated_data_types:~/my_namespaces" - yakut pub ... -""", -) @formatter_factory_option @transport_factory_option @node_factory_option @@ -217,7 +191,6 @@ def _mk_aliases(item: Any) -> set[str]: def _click_main( ctx: click.Context, verbose: int, - path: tuple[str, ...], formatter_factory: FormatterFactory, transport_factory: TransportFactory, node_factory: NodeFactory, @@ -240,13 +213,7 @@ def _click_main( will be read from `YAKUT_BAZ_FOO_BAR`. """ _configure_logging(verbose) # This should be done in the first order to ensure that we log things correctly. - - _logger.debug("Path: %r", path) - for p in path: - sys.path.append(str(p)) - ctx.obj = Purser( - paths=path, formatter_factory=formatter_factory, transport_factory=transport_factory, node_factory=node_factory, diff --git a/yakut/param/node.py b/yakut/param/node.py index 73bf40f..0fdbe63 100644 --- a/yakut/param/node.py +++ b/yakut/param/node.py @@ -61,19 +61,14 @@ def __call__(self, transport: Transport, name_suffix: str, allow_anonymous: bool if not re.match(r"[a-z][a-z0-9_]*[a-z0-9]", name_suffix): # pragma: no cover raise ValueError(f"Internal error: Poorly chosen node name suffix: {name_suffix!r}") - try: - from pycyphal import application - except ImportError as ex: - from yakut.cmd.compile import make_usage_suggestion - - raise click.ClickException(make_usage_suggestion(ex.name)) + from pycyphal import application try: node_info = pycyphal.dsdl.update_from_builtin(application.NodeInfo(), self.node_info) except (ValueError, TypeError) as ex: raise click.UsageError(f"Node info fields are not valid: {ex}") from ex if len(node_info.name) == 0: - node_info.name = f"org.opencyphal.yakut.{name_suffix}" + node_info.name = f"org.opencyphal.yakut.{name_suffix}" # type: ignore _logger.debug("Node info: %r", node_info) ctx = click.get_current_context() diff --git a/yakut/param/transport.py b/yakut/param/transport.py index 846ed81..a268e4f 100644 --- a/yakut/param/transport.py +++ b/yakut/param/transport.py @@ -26,24 +26,18 @@ def validate(ctx: click.Context, param: object, value: Optional[str]) -> Transpo _ = param _logger.debug("Transport expression: %r", value) - def factory() -> Optional[Transport]: + def factory() -> Transport | None: # Try constructing from the expression if provided: if value: try: - result = construct_transport(value) + result: Transport | None = construct_transport(value) except Exception as ex: raise click.BadParameter(f"Could not initialize transport {value!r}: {ex!r}") from ex _logger.info("Transport %r constructed from expression %r", result, value) return result # If no expression is given, construct from the registers passed via environment variables: - try: - from pycyphal.application import make_transport - except (ImportError, AttributeError): - _logger.info( - "Transport initialization expression is not provided and constructing the transport " - "from registers is not possible because the standard DSDL namespace is not compiled" - ) - return None + from pycyphal.application import make_transport + purser = ctx.find_object(Purser) assert isinstance(purser, Purser) result = make_transport(purser.get_registry()) @@ -63,13 +57,11 @@ def factory() -> Optional[Transport]: The full list of registers that configure the transport is available in the definition of the standard RPC-service "uavcan.register.Access", and in the documentation for PyCyphal: https://pycyphal.readthedocs.io (see "make_transport()"). -This method requires that the standard DSDL namespace "uavcan" is compiled (see command "yakut compile")! If this expression is given, the registers are ignored, and the transport instance is constructed by evaluating it. Upon evaluation, the expression should yield either a single transport instance or a sequence thereof. In the latter case, the multiple transports will be joined under the same redundant transport instance, which may be heterogeneous (e.g., UDP+Serial). -This method does not require any DSDL to be compiled at all. To see supported transports and how they should be initialized, refer to https://pycyphal.readthedocs.io. The expression does not need to explicitly reference the `pycyphal.transport` module @@ -159,7 +151,7 @@ def handle_import_error(parent_module_name: str, ex: ImportError) -> None: context[name] = module # Pre-import transport classes. - for cls in pycyphal.util.iter_descendants(Transport): + for cls in pycyphal.util.iter_descendants(Transport): # type: ignore if not cls.__name__.startswith("_") and cls is not Transport: name = cls.__name__.rpartition(Transport.__name__)[0] assert name diff --git a/yakut/paths.py b/yakut/paths.py index df32509..bc32c2e 100644 --- a/yakut/paths.py +++ b/yakut/paths.py @@ -14,7 +14,3 @@ OUTPUT_TRANSFER_ID_MAP_MAX_AGE = 60.0 # [second] """This is not a path but a related parameter so it's kept here. Files older that this are not used.""" - -DEFAULT_PUBLIC_REGULATED_DATA_TYPES_ARCHIVE_URI = ( - "https://github.com/OpenCyphal/public_regulated_data_types/archive/master.zip" -) diff --git a/yakut/register.py b/yakut/register.py index aa46db8..a6f493b 100644 --- a/yakut/register.py +++ b/yakut/register.py @@ -76,8 +76,6 @@ def unexplode_value(xpl: Any, prototype: Optional["Value"] = None) -> Optional[" is given because simplification erases type information. Some unambiguous simplified forms may be unexploded autonomously. - >>> from tests.dsdl import ensure_compiled_dsdl - >>> ensure_compiled_dsdl() >>> from pycyphal.application.register import Value, Natural16 >>> ux = unexplode_value @@ -150,8 +148,6 @@ def _simplify_value(msg: "Value") -> Any: Designed for use with commands that output compact register values in YAML/JSON/TSV/whatever, discarding the detailed type information. - >>> from tests.dsdl import ensure_compiled_dsdl - >>> ensure_compiled_dsdl() >>> from pycyphal.application.register import Value, Empty >>> from pycyphal.application.register import Integer8, Natural8, Integer32, String, Unstructured diff --git a/yakut/subject_resolver.py b/yakut/subject_resolver.py index c131378..92259c5 100644 --- a/yakut/subject_resolver.py +++ b/yakut/subject_resolver.py @@ -113,9 +113,6 @@ def _register_dtypes_by_id( def _unittest_register_dtypes_by_id() -> None: - from tests.dsdl import ensure_compiled_dsdl - - ensure_compiled_dsdl() from pycyphal.application.register import ValueProxy, Natural16, String assert _register_dtypes_by_id({}, 123) == set() diff --git a/yakut/util.py b/yakut/util.py index 54fd0fa..cf7f912 100644 --- a/yakut/util.py +++ b/yakut/util.py @@ -35,7 +35,7 @@ def convert_transfer_metadata_to_builtin( transfer: pycyphal.transport.TransferFrom, *, dtype: Any, - **extra_fields: dict[str, Any], + **extra_fields: Any, ) -> dict[str, dict[str, Any]]: return { METADATA_KEY: {