oxiserial 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. oxiserial-0.1.0/.claude/settings.json +58 -0
  2. oxiserial-0.1.0/.github/workflows/build-wheels.yaml +170 -0
  3. oxiserial-0.1.0/.github/workflows/ci.yaml +50 -0
  4. oxiserial-0.1.0/.github/workflows/run-tests.yaml +141 -0
  5. oxiserial-0.1.0/.gitignore +80 -0
  6. oxiserial-0.1.0/CLAUDE.md +51 -0
  7. oxiserial-0.1.0/Cargo.lock +484 -0
  8. oxiserial-0.1.0/Cargo.toml +38 -0
  9. oxiserial-0.1.0/LICENSE +26 -0
  10. oxiserial-0.1.0/LICENSES/pyserial.txt +39 -0
  11. oxiserial-0.1.0/PKG-INFO +125 -0
  12. oxiserial-0.1.0/README.md +103 -0
  13. oxiserial-0.1.0/pyproject.toml +74 -0
  14. oxiserial-0.1.0/python/oxiserial/__init__.py +2 -0
  15. oxiserial-0.1.0/python/oxiserial/__init__.pyi +479 -0
  16. oxiserial-0.1.0/python/oxiserial/_testing.pyi +41 -0
  17. oxiserial-0.1.0/python/oxiserial/aio.pyi +212 -0
  18. oxiserial-0.1.0/python/oxiserial/py.typed +0 -0
  19. oxiserial-0.1.0/python/oxiserial/tools/__init__.pyi +5 -0
  20. oxiserial-0.1.0/python/oxiserial/tools/list_ports.pyi +76 -0
  21. oxiserial-0.1.0/src/aio.rs +131 -0
  22. oxiserial-0.1.0/src/backend/mock.rs +295 -0
  23. oxiserial-0.1.0/src/backend/native.rs +428 -0
  24. oxiserial-0.1.0/src/backend/overlapped.rs +444 -0
  25. oxiserial-0.1.0/src/backend.rs +67 -0
  26. oxiserial-0.1.0/src/errors.rs +165 -0
  27. oxiserial-0.1.0/src/future.rs +424 -0
  28. oxiserial-0.1.0/src/lib.rs +102 -0
  29. oxiserial-0.1.0/src/list_ports.rs +223 -0
  30. oxiserial-0.1.0/src/port.rs +830 -0
  31. oxiserial-0.1.0/src/runtime.rs +39 -0
  32. oxiserial-0.1.0/src/serial.rs +784 -0
  33. oxiserial-0.1.0/src/settings.rs +177 -0
  34. oxiserial-0.1.0/src/testing.rs +65 -0
  35. oxiserial-0.1.0/tests/conftest.py +64 -0
  36. oxiserial-0.1.0/tests/test_aio.py +151 -0
  37. oxiserial-0.1.0/tests/test_future.py +329 -0
  38. oxiserial-0.1.0/tests/test_import.py +7 -0
  39. oxiserial-0.1.0/tests/test_list_ports.py +61 -0
  40. oxiserial-0.1.0/tests/test_real_ports.py +152 -0
  41. oxiserial-0.1.0/tests/test_serial.py +382 -0
  42. oxiserial-0.1.0/tests/test_signatures.py +16 -0
  43. oxiserial-0.1.0/tests/typing/check_types.py +26 -0
  44. oxiserial-0.1.0/uv.lock +473 -0
@@ -0,0 +1,58 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run:*)",
5
+ "Bash(uv sync:*)",
6
+ "Bash(cargo fmt:*)",
7
+ "Bash(cargo clippy:*)",
8
+ "Bash(cargo test:*)",
9
+ "Bash(cargo build:*)",
10
+ "Bash(cargo check:*)",
11
+ "Bash(git commit:*)",
12
+ "Bash(git status:*)",
13
+ "Bash(git diff:*)",
14
+ "Bash(git log:*)",
15
+ "Bash(git show:*)",
16
+ "Bash(git rev-parse:*)",
17
+ "Bash(git branch:*)",
18
+ "Bash(git merge-base:*)",
19
+ "Bash(git ls-files:*)",
20
+ "Bash(git add:*)",
21
+ "Bash(gh pr view:*)",
22
+ "Bash(ls:*)",
23
+ "Bash(cat:*)",
24
+ "Bash(echo:*)",
25
+ "Bash(printf:*)",
26
+ "Bash(grep:*)",
27
+ "Bash(sed -n:*)",
28
+ "Bash(head:*)",
29
+ "Bash(tail:*)",
30
+ "Bash(find:*)",
31
+ "Bash(wc:*)",
32
+ "Bash(mkdir:*)",
33
+ "Edit",
34
+ "Write"
35
+ ],
36
+ "deny": [
37
+ "Bash(git push:*)",
38
+ "Bash(git push --force:*)",
39
+ "Bash(git reset --hard:*)",
40
+ "Bash(rm -rf:*)",
41
+ "Bash(rmdir /s:*)",
42
+ "Bash(Remove-Item:*)",
43
+ "Bash(gh pr merge:*)",
44
+ "Read(./.env)",
45
+ "Read(./.env.*)"
46
+ ]
47
+ },
48
+ "hooks": {
49
+ "PostToolUse": [
50
+ {
51
+ "matcher": "Edit|Write",
52
+ "hooks": [
53
+ { "type": "command", "command": "uv run ruff format python tests && uv run ruff check --fix python tests" }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,170 @@
1
+ name: Build Wheels
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ jobs:
7
+ linux:
8
+ runs-on: ${{ matrix.platform.runner }}
9
+ strategy:
10
+ matrix:
11
+ platform:
12
+ - runner: ubuntu-22.04
13
+ target: x86_64
14
+ - runner: ubuntu-22.04
15
+ target: x86
16
+ - runner: ubuntu-22.04
17
+ target: aarch64
18
+ - runner: ubuntu-22.04
19
+ target: armv7
20
+ - runner: ubuntu-22.04
21
+ target: s390x
22
+ - runner: ubuntu-22.04
23
+ target: ppc64le
24
+ steps:
25
+ - uses: actions/checkout@v6
26
+ - &set-version
27
+ name: Set version from tag
28
+ if: startsWith(github.ref, 'refs/tags/v')
29
+ shell: bash
30
+ run: |
31
+ # PEP 440 pre-release (1.2.0rc1) -> semver (1.2.0-rc.1); maturin maps it back.
32
+ v="$(echo "${GITHUB_REF_NAME#v}" | sed -E 's/(a|b|rc)([0-9]+)$/-\1.\2/')"
33
+ sed -i.bak -E '/^\[package\]/,/^\[/ s/^version = ".*"/version = "'"$v"'"/' Cargo.toml
34
+ rm Cargo.toml.bak
35
+ cargo update --workspace
36
+ - uses: actions/setup-python@v6
37
+ with:
38
+ python-version: "3.x"
39
+ - name: Build wheels
40
+ uses: PyO3/maturin-action@v1
41
+ with:
42
+ target: ${{ matrix.platform.target }}
43
+ args: --release --out dist -i python3.11 -i python3.14t
44
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
45
+ manylinux: auto
46
+ - name: Upload wheels
47
+ uses: actions/upload-artifact@v6
48
+ with:
49
+ name: wheels-linux-${{ matrix.platform.target }}
50
+ path: dist
51
+
52
+ musllinux:
53
+ runs-on: ${{ matrix.platform.runner }}
54
+ strategy:
55
+ matrix:
56
+ platform:
57
+ - runner: ubuntu-22.04
58
+ target: x86_64
59
+ - runner: ubuntu-22.04
60
+ target: x86
61
+ - runner: ubuntu-22.04
62
+ target: aarch64
63
+ - runner: ubuntu-22.04
64
+ target: armv7
65
+ steps:
66
+ - uses: actions/checkout@v6
67
+ - *set-version
68
+ - uses: actions/setup-python@v6
69
+ with:
70
+ python-version: "3.x"
71
+ - name: Build wheels
72
+ uses: PyO3/maturin-action@v1
73
+ with:
74
+ target: ${{ matrix.platform.target }}
75
+ args: --release --out dist -i python3.11 -i python3.14t
76
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
77
+ manylinux: musllinux_1_2
78
+ - name: Upload wheels
79
+ uses: actions/upload-artifact@v6
80
+ with:
81
+ name: wheels-musllinux-${{ matrix.platform.target }}
82
+ path: dist
83
+
84
+ windows:
85
+ runs-on: ${{ matrix.platform.runner }}
86
+ strategy:
87
+ matrix:
88
+ platform:
89
+ - runner: windows-latest
90
+ target: x64
91
+ python_arch: x64
92
+ - runner: windows-latest
93
+ target: x86
94
+ python_arch: x86
95
+ - runner: windows-11-arm
96
+ target: aarch64
97
+ python_arch: arm64
98
+ steps:
99
+ - uses: actions/checkout@v6
100
+ - *set-version
101
+ - uses: actions/setup-python@v6
102
+ id: py311
103
+ with:
104
+ python-version: "3.11"
105
+ architecture: ${{ matrix.platform.python_arch }}
106
+ - uses: actions/setup-python@v6
107
+ id: py314t
108
+ with:
109
+ python-version: "3.14t"
110
+ architecture: ${{ matrix.platform.python_arch }}
111
+ - name: Build wheels
112
+ uses: PyO3/maturin-action@v1
113
+ with:
114
+ target: ${{ matrix.platform.target }}
115
+ args: --release --out dist -i ${{ steps.py311.outputs.python-path }} -i ${{ steps.py314t.outputs.python-path }}
116
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
117
+ - name: Upload wheels
118
+ uses: actions/upload-artifact@v6
119
+ with:
120
+ name: wheels-windows-${{ matrix.platform.target }}
121
+ path: dist
122
+
123
+ macos:
124
+ runs-on: ${{ matrix.platform.runner }}
125
+ strategy:
126
+ matrix:
127
+ platform:
128
+ - runner: macos-15-intel
129
+ target: x86_64
130
+ - runner: macos-latest
131
+ target: aarch64
132
+ steps:
133
+ - uses: actions/checkout@v6
134
+ - *set-version
135
+ - uses: actions/setup-python@v6
136
+ id: py311
137
+ with:
138
+ python-version: "3.11"
139
+ - uses: actions/setup-python@v6
140
+ id: py314t
141
+ with:
142
+ python-version: "3.14t"
143
+ - name: Build wheels
144
+ uses: PyO3/maturin-action@v1
145
+ with:
146
+ target: ${{ matrix.platform.target }}
147
+ args: --release --out dist -i ${{ steps.py311.outputs.python-path }} -i ${{ steps.py314t.outputs.python-path }}
148
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
149
+ - name: Upload wheels
150
+ uses: actions/upload-artifact@v6
151
+ with:
152
+ name: wheels-macos-${{ matrix.platform.target }}
153
+ path: dist
154
+
155
+ sdist:
156
+ if: startsWith(github.ref, 'refs/tags/v')
157
+ runs-on: ubuntu-latest
158
+ steps:
159
+ - uses: actions/checkout@v6
160
+ - *set-version
161
+ - name: Build sdist
162
+ uses: PyO3/maturin-action@v1
163
+ with:
164
+ command: sdist
165
+ args: --out dist
166
+ - name: Upload sdist
167
+ uses: actions/upload-artifact@v6
168
+ with:
169
+ name: wheels-sdist
170
+ path: dist
@@ -0,0 +1,50 @@
1
+ # Started from `maturin generate-ci github`; maintained by hand.
2
+ name: CI
3
+
4
+ on:
5
+ push:
6
+ branches:
7
+ - main
8
+ - master
9
+ tags:
10
+ - 'v*'
11
+ pull_request:
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ name: Run Tests
20
+ uses: ./.github/workflows/run-tests.yaml
21
+
22
+ build:
23
+ name: Build Wheels
24
+ uses: ./.github/workflows/build-wheels.yaml
25
+
26
+ release:
27
+ name: Release
28
+ runs-on: ubuntu-latest
29
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
30
+ needs: [test, build]
31
+ environment: pypi
32
+ permissions:
33
+ # Used for PyPI trusted publishing and to sign the release artifacts
34
+ id-token: write
35
+ # Used to upload release artifacts
36
+ contents: write
37
+ # Used to generate artifact attestation
38
+ attestations: write
39
+ steps:
40
+ - uses: actions/download-artifact@v7
41
+ - name: Generate artifact attestation
42
+ uses: actions/attest@v4
43
+ with:
44
+ subject-path: 'wheels-*/*'
45
+ - name: Install uv
46
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
47
+ uses: astral-sh/setup-uv@v7
48
+ - name: Publish to PyPI
49
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
50
+ run: uv publish --trusted-publishing always 'wheels-*/*'
@@ -0,0 +1,141 @@
1
+ name: Run Tests
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ jobs:
7
+ test:
8
+ name: test (${{ matrix.os }}, ${{ matrix.python }}${{ matrix.python == '3.14' && ', release' || '' }})
9
+ runs-on: ${{ matrix.os }}
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest, windows-latest]
14
+ python: ["3.11", "3.14", "3.14t"]
15
+ env:
16
+ UV_PYTHON: ${{ matrix.python }}
17
+ UV_PYTHON_PREFERENCE: only-managed
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - uses: astral-sh/setup-uv@v7
21
+ - run: uv sync --locked
22
+ - name: Point PyO3 at the test interpreter
23
+ shell: bash
24
+ run: |
25
+ base="$(uv run python -c 'import sys; print(sys.base_prefix)')"
26
+ echo "PYO3_PYTHON=$(uv python find)" >> "$GITHUB_ENV"
27
+ echo "$base" >> "$GITHUB_PATH"
28
+ - run: cargo fmt --check
29
+ - run: cargo clippy --all-targets -- -D warnings
30
+ - run: cargo clippy --all-targets --features test-backend -- -D warnings
31
+ - name: cargo test
32
+ shell: bash
33
+ run: |
34
+ base="$(uv run python -c 'import sys; print(sys.base_prefix)')"
35
+ LD_LIBRARY_PATH="$base/lib" DYLD_LIBRARY_PATH="$base/lib" cargo test --features test-backend
36
+ - run: uv run maturin develop --uv --features test-backend ${{ matrix.python == '3.14' && '--release' || '' }}
37
+ - name: Virtual port pair (Linux)
38
+ if: runner.os == 'Linux'
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y socat
42
+ # socat exits when a slave is closed, so restart it to keep both links alive between tests.
43
+ nohup sh -c 'while true; do socat pty,raw,echo=0,link=/tmp/ttyA pty,raw,echo=0,link=/tmp/ttyB; done' > socat.log 2>&1 &
44
+ for _ in $(seq 20); do
45
+ [ -e /tmp/ttyA ] && [ -e /tmp/ttyB ] && break
46
+ sleep 0.5
47
+ done
48
+ test -e /tmp/ttyA && test -e /tmp/ttyB
49
+ echo "OXISERIAL_PORT_A=/tmp/ttyA" >> "$GITHUB_ENV"
50
+ echo "OXISERIAL_PORT_B=/tmp/ttyB" >> "$GITHUB_ENV"
51
+ - name: Download com0com
52
+ id: com0com-download
53
+ if: runner.os == 'Windows'
54
+ continue-on-error: true
55
+ shell: pwsh
56
+ run: |
57
+ $ErrorActionPreference = 'Stop'
58
+ $dir = Join-Path $env:RUNNER_TEMP "com0com"
59
+ Invoke-WebRequest -UserAgent "oxiserial-ci" -OutFile "$dir.zip" `
60
+ -Uri "https://sourceforge.net/projects/com0com/files/com0com/2.2.2.0/com0com-2.2.2.0-x64-fre-signed.zip/download"
61
+ - name: Extract com0com
62
+ id: com0com-extract
63
+ if: runner.os == 'Windows' && steps.com0com-download.outcome == 'success'
64
+ continue-on-error: true
65
+ shell: pwsh
66
+ run: |
67
+ $ErrorActionPreference = 'Stop'
68
+ $dir = Join-Path $env:RUNNER_TEMP "com0com"
69
+ Expand-Archive "$dir.zip" $dir
70
+ $setup = Get-ChildItem $dir -Recurse -Filter setup.exe | Select-Object -First 1
71
+ if (-not $setup) { throw "setup.exe not found in $dir" }
72
+ 7z x -y "-o$dir\driver" $setup.FullName
73
+ if ($LASTEXITCODE -ne 0) { throw "7z failed with exit code $LASTEXITCODE" }
74
+ foreach ($f in "com0com.inf", "com0com.cat", "setupc.exe") {
75
+ if (-not (Test-Path "$dir\driver\$f")) { throw "$f missing from the extracted driver" }
76
+ }
77
+ "COM0COM_DIR=$dir\driver" >> $env:GITHUB_ENV
78
+ - name: Trust the com0com signer
79
+ id: com0com-trust
80
+ if: runner.os == 'Windows' && steps.com0com-extract.outcome == 'success'
81
+ continue-on-error: true
82
+ shell: pwsh
83
+ run: |
84
+ $ErrorActionPreference = 'Stop'
85
+ # A silent driver install needs the publisher certificate trusted first; the driver catalog, not setup.exe, carries the signature.
86
+ $sig = Get-AuthenticodeSignature (Join-Path $env:COM0COM_DIR 'com0com.cat')
87
+ if ($sig.Status -ne 'Valid' -or -not $sig.SignerCertificate) { throw "com0com.cat signature is $($sig.Status)" }
88
+ $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("TrustedPublisher", "LocalMachine")
89
+ $store.Open("ReadWrite")
90
+ $store.Add($sig.SignerCertificate)
91
+ $store.Close()
92
+ - name: Create the port pair
93
+ id: com0com-pair
94
+ if: runner.os == 'Windows' && steps.com0com-trust.outcome == 'success'
95
+ continue-on-error: true
96
+ shell: pwsh
97
+ env:
98
+ PORT_A: COM90
99
+ PORT_B: COM91
100
+ run: |
101
+ $ErrorActionPreference = 'Stop'
102
+ # setupc.exe installs the driver from the .inf in its current directory.
103
+ Push-Location $env:COM0COM_DIR
104
+ try {
105
+ .\setupc.exe --silent install PortName=$env:PORT_A PortName=$env:PORT_B
106
+ if ($LASTEXITCODE -ne 0) { throw "setupc install failed with exit code $LASTEXITCODE" }
107
+ } finally {
108
+ Pop-Location
109
+ }
110
+ - name: Check the ports exist
111
+ id: com0com-check
112
+ if: runner.os == 'Windows' && steps.com0com-pair.outcome == 'success'
113
+ continue-on-error: true
114
+ shell: pwsh
115
+ env:
116
+ PORT_A: COM90
117
+ PORT_B: COM91
118
+ run: |
119
+ $ErrorActionPreference = 'Stop'
120
+ $names = [System.IO.Ports.SerialPort]::GetPortNames()
121
+ Write-Host "Ports: $($names -join ', ')"
122
+ foreach ($p in $env:PORT_A, $env:PORT_B) {
123
+ if ($names -notcontains $p) { throw "Port $p was not created" }
124
+ }
125
+ - name: Export the ports
126
+ id: com0com-export
127
+ if: runner.os == 'Windows' && steps.com0com-check.outcome == 'success'
128
+ continue-on-error: true
129
+ shell: pwsh
130
+ env:
131
+ PORT_A: COM90
132
+ PORT_B: COM91
133
+ run: |
134
+ $ErrorActionPreference = 'Stop'
135
+ "OXISERIAL_PORT_A=$env:PORT_A" >> $env:GITHUB_ENV
136
+ "OXISERIAL_PORT_B=$env:PORT_B" >> $env:GITHUB_ENV
137
+ - run: uv run pytest -v
138
+ - run: uv run mypy
139
+ - run: uv run python -m mypy.stubtest oxiserial
140
+ - run: uv run ruff check python tests
141
+ - run: uv run ruff format --check python tests
@@ -0,0 +1,80 @@
1
+ /target
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ .pytest_cache/
6
+ *.py[cod]
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ .venv/
14
+ env/
15
+ bin/
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ include/
26
+ man/
27
+ venv/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+ pip-selfcheck.json
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+
45
+ # Translations
46
+ *.mo
47
+
48
+ # Mr Developer
49
+ .mr.developer.cfg
50
+ .project
51
+ .pydevproject
52
+
53
+ # Rope
54
+ .ropeproject
55
+
56
+ # Django stuff:
57
+ *.log
58
+ *.pot
59
+
60
+ .DS_Store
61
+
62
+ # Sphinx documentation
63
+ docs/_build/
64
+
65
+ # PyCharm
66
+ .idea/
67
+
68
+ # VSCode
69
+ .vscode/
70
+
71
+ # Pyenv
72
+ .python-version
73
+
74
+ # claude
75
+ .claude/settings.local.json
76
+ .claude/plans
77
+ .superpowers/
78
+ .mypy_cache/
79
+ *.pyd
80
+ *.pdb
@@ -0,0 +1,51 @@
1
+ # oxiserial
2
+
3
+ Serial port library for Python, implemented in Rust with PyO3 and built with
4
+ maturin. `import oxiserial as serial` replaces `pyserial` 3.5.
5
+ `oxiserial.aio.Serial` has the same API, but its I/O methods return a future
6
+ that can be waited on (`.wait()`) or awaited.
7
+
8
+ ## Layout
9
+
10
+ - `src/`: all runtime code (Rust). The extension module is
11
+ `oxiserial._oxiserial`.
12
+ - `python/oxiserial/`: `.pyi` stubs, `py.typed`, and an `__init__.py` that
13
+ only re-exports `oxiserial._oxiserial`.
14
+ - `.github/workflows/`: `ci.yaml` calls `run-tests.yaml` and
15
+ `build-wheels.yaml`, then publishes. Started from `maturin generate-ci
16
+ github`, now maintained by hand.
17
+
18
+ ## Rules
19
+
20
+ - `pyserial` 3.5 is the specification for the public API: names, signatures,
21
+ defaults, exception types and behaviour.
22
+ - Code or text taken from pyserial gets a one-line comment saying so; the
23
+ notice is in `LICENSES/pyserial.txt`.
24
+ - OS access goes through crates (`serialport`, `tokio-serial`, and `nix` or
25
+ `windows-sys` for what those lack). No hand-written FFI.
26
+ - Every item exposed to Python has a matching stub entry; `stubtest` checks
27
+ they agree.
28
+ - Blocking calls release the GIL. Shared state is behind a `Mutex` or
29
+ atomics, since the module supports free-threaded CPython.
30
+ - CPython 3.11 or newer. Wheels are `abi3-py311` plus `cp314t`.
31
+ - Releases are cut by pushing a `vX.Y.Z` tag; CI sets the version from it.
32
+ - Dependencies go through `uv add` and `cargo add`, never by editing
33
+ `pyproject.toml` or `Cargo.toml` by hand.
34
+
35
+ ## Commands
36
+
37
+ ```sh
38
+ uv sync
39
+ uv run maturin develop --uv --features test-backend
40
+ cargo fmt --check
41
+ cargo clippy --all-targets --features test-backend -- -D warnings
42
+ cargo test --features test-backend
43
+ uv run pytest
44
+ uv run ruff check python tests
45
+ uv run ruff format --check python tests
46
+ uv run mypy
47
+ uv run python -m mypy.stubtest oxiserial
48
+ ```
49
+
50
+ Run all of them before reporting a change as done. Real-port tests need a
51
+ connected pair: `uv run pytest --port-a <A> --port-b <B>`.