oxidmol 0.1.1__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 (73) hide show
  1. oxidmol-0.1.1/.claude/commands/check.md +24 -0
  2. oxidmol-0.1.1/.github/workflows/CI.yml +92 -0
  3. oxidmol-0.1.1/.github/workflows/publish.yml +178 -0
  4. oxidmol-0.1.1/.github/workflows/release-plz.yml +47 -0
  5. oxidmol-0.1.1/.gitignore +47 -0
  6. oxidmol-0.1.1/.python-version +1 -0
  7. oxidmol-0.1.1/CHANGELOG.md +5 -0
  8. oxidmol-0.1.1/CLAUDE.md +75 -0
  9. oxidmol-0.1.1/Cargo.lock +1793 -0
  10. oxidmol-0.1.1/Cargo.toml +32 -0
  11. oxidmol-0.1.1/PKG-INFO +46 -0
  12. oxidmol-0.1.1/README.md +24 -0
  13. oxidmol-0.1.1/cliff.toml +33 -0
  14. oxidmol-0.1.1/pyproject.toml +153 -0
  15. oxidmol-0.1.1/python/oxidmol/__init__.py +7 -0
  16. oxidmol-0.1.1/python/oxidmol/app.py +16 -0
  17. oxidmol-0.1.1/python/oxidmol/dsl/__init__.py +0 -0
  18. oxidmol-0.1.1/python/oxidmol/dsl/commands.py +16 -0
  19. oxidmol-0.1.1/python/oxidmol/dsl/grammar.py +33 -0
  20. oxidmol-0.1.1/python/oxidmol/dsl/transformer.py +24 -0
  21. oxidmol-0.1.1/python/oxidmol/lsd/__init__.pyi +32 -0
  22. oxidmol-0.1.1/python/oxidmol/lsd/py.typed +0 -0
  23. oxidmol-0.1.1/python/oxidmol/settings.py +134 -0
  24. oxidmol-0.1.1/python/oxidmol/tests/__init__.py +0 -0
  25. oxidmol-0.1.1/python/oxidmol/tests/test_bindings.py +19 -0
  26. oxidmol-0.1.1/python/oxidmol/tests/test_scene_widget.py +200 -0
  27. oxidmol-0.1.1/python/oxidmol/tests/test_ui.py +46 -0
  28. oxidmol-0.1.1/python/oxidmol/tests/utils/conftest.py +12 -0
  29. oxidmol-0.1.1/python/oxidmol/tests/utils/test_cache.py +118 -0
  30. oxidmol-0.1.1/python/oxidmol/tests/utils/test_fetch.py +135 -0
  31. oxidmol-0.1.1/python/oxidmol/tests/utils/test_loading.py +44 -0
  32. oxidmol-0.1.1/python/oxidmol/ui/__init__.py +0 -0
  33. oxidmol-0.1.1/python/oxidmol/ui/command_bar.py +66 -0
  34. oxidmol-0.1.1/python/oxidmol/ui/main_window.py +109 -0
  35. oxidmol-0.1.1/python/oxidmol/ui/menu/__init__.py +4 -0
  36. oxidmol-0.1.1/python/oxidmol/ui/menu/menu_bar.py +238 -0
  37. oxidmol-0.1.1/python/oxidmol/ui/menu/tool_bar.py +114 -0
  38. oxidmol-0.1.1/python/oxidmol/ui/object_panel.py +117 -0
  39. oxidmol-0.1.1/python/oxidmol/ui/scene.py +174 -0
  40. oxidmol-0.1.1/python/oxidmol/ui/theme.py +175 -0
  41. oxidmol-0.1.1/python/oxidmol/utils/__init__.py +0 -0
  42. oxidmol-0.1.1/python/oxidmol/utils/cache.py +89 -0
  43. oxidmol-0.1.1/python/oxidmol/utils/fetch.py +152 -0
  44. oxidmol-0.1.1/python/oxidmol/utils/loading.py +47 -0
  45. oxidmol-0.1.1/release-plz.toml +9 -0
  46. oxidmol-0.1.1/src/core/element.rs +519 -0
  47. oxidmol-0.1.1/src/core/mesh_builder.rs +212 -0
  48. oxidmol-0.1.1/src/core/molecule.rs +323 -0
  49. oxidmol-0.1.1/src/core.rs +7 -0
  50. oxidmol-0.1.1/src/lib.rs +58 -0
  51. oxidmol-0.1.1/src/python/molecule.rs +46 -0
  52. oxidmol-0.1.1/src/python/renderer.rs +102 -0
  53. oxidmol-0.1.1/src/python.rs +5 -0
  54. oxidmol-0.1.1/src/renderer/camera.rs +146 -0
  55. oxidmol-0.1.1/src/renderer/camera_gpu.rs +37 -0
  56. oxidmol-0.1.1/src/renderer/depth_texture.rs +55 -0
  57. oxidmol-0.1.1/src/renderer/gpu_context.rs +87 -0
  58. oxidmol-0.1.1/src/renderer/pipeline.rs +258 -0
  59. oxidmol-0.1.1/src/renderer/scene.rs +281 -0
  60. oxidmol-0.1.1/src/renderer/scene_object.rs +35 -0
  61. oxidmol-0.1.1/src/renderer/scene_pipeline.rs +38 -0
  62. oxidmol-0.1.1/src/renderer/state.rs +72 -0
  63. oxidmol-0.1.1/src/renderer/test_support.rs +161 -0
  64. oxidmol-0.1.1/src/renderer.rs +16 -0
  65. oxidmol-0.1.1/src/shaders/shader.wgsl +43 -0
  66. oxidmol-0.1.1/src/utils/error.rs +113 -0
  67. oxidmol-0.1.1/src/utils/logging.rs +92 -0
  68. oxidmol-0.1.1/src/utils/qt_window.rs +120 -0
  69. oxidmol-0.1.1/src/utils.rs +7 -0
  70. oxidmol-0.1.1/tests/data/1crn.pdb +354 -0
  71. oxidmol-0.1.1/tests/data/minimal.cif +29 -0
  72. oxidmol-0.1.1/tests/data/minimal.pdb +8 -0
  73. oxidmol-0.1.1/uv.lock +635 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Run every check that CI runs (Rust and Python), without the GPU and app launch steps
3
+ ---
4
+
5
+ Run these from the repository root in the activated virtual environment and report the result of each one.
6
+ Do not stop at the first failure: run all of them, then summarize what failed and why.
7
+
8
+ ```bash
9
+ source .venv/bin/activate
10
+ cargo fmt -- --check
11
+ cargo clippy --all-targets -- -D warnings
12
+ cargo test
13
+ uv run --no-sync maturin develop
14
+ ruff format --check python/
15
+ ruff check python/
16
+ mypy python/
17
+ QT_QPA_PLATFORM=offscreen pytest -m "not gpu"
18
+ ```
19
+
20
+ Notes:
21
+ - `cargo test` needs the activated `.venv`, otherwise it fails to link with `-lpython3.9`.
22
+ - GPU tests print `skipping: no GPU adapter available` and still pass on machines without a GPU.
23
+ Say so in the summary if you see it.
24
+ - Fix only what the user asked for. If a check fails for an unrelated reason, report it instead of fixing it.
@@ -0,0 +1,92 @@
1
+ # Tests and lint. Wheels and the PyPI upload live in publish.yml, which calls this workflow first.
2
+ name: CI
3
+
4
+ on:
5
+ push:
6
+ branches:
7
+ - master
8
+ pull_request:
9
+ paths-ignore:
10
+ - '**.md'
11
+ workflow_dispatch:
12
+ workflow_call:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ # a new push cancels the previous run on the same branch/PR (never cancel runs for tags)
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.ref }}
20
+ cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
21
+
22
+ env:
23
+ CARGO_INCREMENTAL: 0
24
+ CARGO_TERM_COLOR: always
25
+
26
+ jobs:
27
+ test:
28
+ name: Run Tests
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 20
31
+ strategy:
32
+ matrix:
33
+ python-version: ['3.12']
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - name: Set up Rust
37
+ uses: dtolnay/rust-toolchain@stable
38
+ - name: Cache Rust build
39
+ uses: Swatinem/rust-cache@v2
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v5
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ - name: Install system dependencies
45
+ run: sudo apt-get install -y libegl1 libgles2 libdbus-1-3
46
+ - name: Install uv
47
+ uses: astral-sh/setup-uv@v5
48
+ with:
49
+ enable-cache: true
50
+ - name: Install dependencies
51
+ run: uv sync --group dev --no-install-project
52
+ - name: Run Rust tests
53
+ run: cargo test
54
+ - name: Build Python extension
55
+ run: uv run --no-sync maturin develop
56
+ - name: Run Python tests
57
+ run: uv run --no-sync pytest -m "not gpu"
58
+ env:
59
+ QT_QPA_PLATFORM: offscreen
60
+
61
+ lint:
62
+ name: Lint and Format Check
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 15
65
+ steps:
66
+ - uses: actions/checkout@v4
67
+ - name: Set up Rust
68
+ uses: dtolnay/rust-toolchain@stable
69
+ with:
70
+ components: rustfmt, clippy
71
+ - name: Cache Rust build
72
+ uses: Swatinem/rust-cache@v2
73
+ - name: Set up Python
74
+ uses: actions/setup-python@v5
75
+ with:
76
+ python-version: '3.12'
77
+ - name: Install uv
78
+ uses: astral-sh/setup-uv@v5
79
+ with:
80
+ enable-cache: true
81
+ - name: Install Python dependencies
82
+ run: uv sync --group dev --no-install-project
83
+ - name: Check Rust formatting
84
+ run: cargo fmt -- --check
85
+ - name: Run Clippy
86
+ run: cargo clippy --all-targets -- -D warnings
87
+ - name: Check Python formatting
88
+ run: uv run --no-sync ruff format --check python/
89
+ - name: Run Python linting
90
+ run: uv run --no-sync ruff check python/
91
+ - name: Run mypy
92
+ run: uv run --no-sync mypy python/
@@ -0,0 +1,178 @@
1
+ # Builds the wheels and the sdist and uploads them to PyPI.
2
+ # Runs for version tags (created by release-plz) and manually; a manual run builds everything but never uploads.
3
+ name: Publish
4
+
5
+ on:
6
+ push:
7
+ tags:
8
+ - 'v[0-9]*'
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ # never cancel a release that is running
15
+ concurrency:
16
+ group: release-${{ github.ref }}
17
+ cancel-in-progress: false
18
+
19
+ env:
20
+ CARGO_INCREMENTAL: 0
21
+ CARGO_TERM_COLOR: always
22
+
23
+ jobs:
24
+ checks:
25
+ name: Checks
26
+ uses: ./.github/workflows/CI.yml
27
+
28
+ linux:
29
+ name: Wheels (manylinux ${{ matrix.platform.target }})
30
+ runs-on: ${{ matrix.platform.runner }}
31
+ timeout-minutes: 30
32
+ needs: [checks]
33
+ strategy:
34
+ matrix:
35
+ platform:
36
+ - runner: ubuntu-22.04
37
+ target: x86_64
38
+ - runner: ubuntu-22.04
39
+ target: aarch64
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: actions/setup-python@v5
43
+ with:
44
+ python-version: '3.12'
45
+ - name: Build wheels
46
+ uses: PyO3/maturin-action@v1
47
+ with:
48
+ target: ${{ matrix.platform.target }}
49
+ args: --release --out dist
50
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
51
+ manylinux: auto
52
+ - name: Upload wheels
53
+ uses: actions/upload-artifact@v4
54
+ with:
55
+ name: wheels-linux-${{ matrix.platform.target }}
56
+ path: dist
57
+
58
+ musllinux:
59
+ runs-on: ${{ matrix.platform.runner }}
60
+ timeout-minutes: 30
61
+ needs: [checks]
62
+ strategy:
63
+ matrix:
64
+ platform:
65
+ - runner: ubuntu-22.04
66
+ target: x86_64
67
+ - runner: ubuntu-22.04
68
+ target: aarch64
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ - uses: actions/setup-python@v5
72
+ with:
73
+ python-version: '3.12'
74
+ - name: Build wheels
75
+ uses: PyO3/maturin-action@v1
76
+ with:
77
+ target: ${{ matrix.platform.target }}
78
+ args: --release --out dist
79
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
80
+ manylinux: musllinux_1_2
81
+ - name: Upload wheels
82
+ uses: actions/upload-artifact@v4
83
+ with:
84
+ name: wheels-musllinux-${{ matrix.platform.target }}
85
+ path: dist
86
+
87
+ windows:
88
+ runs-on: ${{ matrix.platform.runner }}
89
+ timeout-minutes: 30
90
+ needs: [checks]
91
+ strategy:
92
+ matrix:
93
+ platform:
94
+ - runner: windows-latest
95
+ target: x64
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+ - uses: actions/setup-python@v5
99
+ with:
100
+ python-version: '3.12'
101
+ architecture: ${{ matrix.platform.target }}
102
+ - name: Build wheels
103
+ uses: PyO3/maturin-action@v1
104
+ with:
105
+ target: ${{ matrix.platform.target }}
106
+ args: --release --out dist
107
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
108
+ - name: Upload wheels
109
+ uses: actions/upload-artifact@v4
110
+ with:
111
+ name: wheels-windows-${{ matrix.platform.target }}
112
+ path: dist
113
+
114
+ macos:
115
+ runs-on: ${{ matrix.platform.runner }}
116
+ timeout-minutes: 30
117
+ needs: [checks]
118
+ strategy:
119
+ matrix:
120
+ platform:
121
+ - runner: macos-14
122
+ target: aarch64
123
+ - runner: macos-14
124
+ target: x86_64
125
+ steps:
126
+ - uses: actions/checkout@v4
127
+ - uses: actions/setup-python@v5
128
+ with:
129
+ python-version: '3.12'
130
+ - name: Build wheels
131
+ uses: PyO3/maturin-action@v1
132
+ with:
133
+ target: ${{ matrix.platform.target }}
134
+ args: --release --out dist
135
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
136
+ - name: Upload wheels
137
+ uses: actions/upload-artifact@v4
138
+ with:
139
+ name: wheels-macos-${{ matrix.platform.target }}
140
+ path: dist
141
+
142
+ sdist:
143
+ name: Build sdist
144
+ runs-on: ubuntu-latest
145
+ timeout-minutes: 10
146
+ needs: [checks]
147
+ steps:
148
+ - uses: actions/checkout@v4
149
+ - name: Build sdist
150
+ uses: PyO3/maturin-action@v1
151
+ with:
152
+ command: sdist
153
+ args: --out dist
154
+ - name: Upload sdist
155
+ uses: actions/upload-artifact@v4
156
+ with:
157
+ name: sdist
158
+ path: dist/*.tar.gz
159
+
160
+ publish-pypi:
161
+ name: Publish to PyPI
162
+ runs-on: ubuntu-latest
163
+ timeout-minutes: 10
164
+ needs: [linux, musllinux, windows, macos, sdist]
165
+ if: startsWith(github.ref, 'refs/tags/')
166
+ environment:
167
+ name: pypi
168
+ url: https://pypi.org/p/oxidmol
169
+ permissions:
170
+ id-token: write
171
+ steps:
172
+ - name: Download all artifacts
173
+ uses: actions/download-artifact@v4
174
+ with:
175
+ path: dist
176
+ merge-multiple: true
177
+ - name: Publish to PyPI
178
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,47 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+
7
+ jobs:
8
+ release-pr:
9
+ name: Create release PR
10
+ runs-on: ubuntu-latest
11
+ timeout-minutes: 10
12
+ concurrency:
13
+ group: release-plz-pr
14
+ cancel-in-progress: false
15
+ permissions:
16
+ contents: write
17
+ pull-requests: write
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+ - uses: dtolnay/rust-toolchain@stable
23
+ - uses: release-plz/action@v0.5
24
+ with:
25
+ command: release-pr
26
+ env:
27
+ GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }}
28
+
29
+ release:
30
+ name: Create tag and GitHub release
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 10
33
+ concurrency:
34
+ group: release-plz-release
35
+ cancel-in-progress: false
36
+ permissions:
37
+ contents: write
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ with:
41
+ fetch-depth: 0
42
+ - uses: dtolnay/rust-toolchain@stable
43
+ - uses: release-plz/action@v0.5
44
+ with:
45
+ command: release
46
+ env:
47
+ GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }}
@@ -0,0 +1,47 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug
4
+ target
5
+
6
+ # These are backup files generated by rustfmt
7
+ **/*.rs.bk
8
+
9
+ # Generated by cargo mutants
10
+ # Contains mutation testing data
11
+ **/mutants.out*/
12
+
13
+ # RustRover
14
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
15
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
16
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
17
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
18
+ #.idea/
19
+
20
+ *.local
21
+
22
+ **/.env*
23
+
24
+ # intellij
25
+ **/.idea
26
+
27
+ # python
28
+ **/venv
29
+ **/__pycache__
30
+ **/*.pyc
31
+ **/*.egg-info
32
+ build/
33
+ .venv/
34
+
35
+ # pyright
36
+ pyrightconfig*.json
37
+
38
+ # mac
39
+ .DS_Store
40
+ target/
41
+
42
+ # unix like
43
+ *.so
44
+
45
+ # vscode
46
+ .vscode/
47
+ node_modules/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ### Bug Fixes
4
+
5
+ - Setting correct workflow.yaml ([60bbd73](https://github.com/ye11owSub/oxidmol/commit/60bbd735146e003954b0112be1f46a85373cfdbd))
@@ -0,0 +1,75 @@
1
+ # OxidMol: working notes for Claude
2
+
3
+ A PyMOL-inspired molecule viewer. The renderer is written in Rust on wgpu and exposed to Python through PyO3 as the module `oxidmol.lsd`; the UI is PySide6.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ source .venv/bin/activate # REQUIRED before cargo test, see "Pitfalls"
9
+ uv sync --group dev --no-install-project # dependencies without building Rust (same as CI)
10
+ uv run --no-sync maturin develop # build the extension into .venv (after every Rust change)
11
+
12
+ cargo fmt -- --check
13
+ cargo clippy --all-targets -- -D warnings # warnings are errors in CI
14
+ cargo test # Rust tests, including the GPU tests
15
+
16
+ ruff format --check python/ && ruff check python/ && mypy python/
17
+ QT_QPA_PLATFORM=offscreen pytest -m "not gpu" # what CI runs
18
+ pytest -m gpu # needs a display and a GPU
19
+
20
+ oxidmol # run the app (RUST_LOG=warn hides the naga/wgpu debug noise)
21
+ ```
22
+
23
+ `/check` runs all of the above except the GPU and app launch steps. Run it before finishing a task: CI fails on any of them.
24
+
25
+ ## Layout
26
+
27
+ ```
28
+ src/
29
+ lib.rs PyO3 module `lsd`, logging setup
30
+ core/ Molecule, Chain/Residue/Atom, FlatAtom, sphere mesh; PDB/mmCIF loading (pdbtbx)
31
+ renderer/
32
+ state.rs State = GpuContext (window + surface) + Scene
33
+ gpu_context.rs instance/surface/adapter/device; everything returns Result<_, WgpuError>
34
+ scene.rs Scene: camera, pipeline, depth, objects; render() into any TextureView
35
+ pipeline.rs PipelineBuilder (takes the shader as a WGSL string)
36
+ scene_pipeline.rs builds the main pipeline; the shader is embedded with include_str!
37
+ test_support.rs (tests only) windowless device, pixel readback, validation error scope
38
+ python/ PyO3 wrappers: WgpuRenderer, PyMolecule
39
+ utils/ errors, qt_window.rs (QWidget.winId() -> raw-window-handle), logging
40
+ shaders/shader.wgsl
41
+ python/oxidmol/
42
+ app.py, settings.py
43
+ ui/ main_window, scene (WgpuWidget), object_panel, command_bar, menu/, theme.py
44
+ dsl/ command grammar (lark) and transformer
45
+ utils/ loading, cache, fetch
46
+ lsd/__init__.pyi type stubs for the Rust module
47
+ tests/data/ PDB/CIF files used by tests
48
+ ```
49
+
50
+ ## Pitfalls
51
+
52
+ - **`cargo test` does not link without the `.venv` activated** (`-lpython3.9`, it picks up Xcode's Python). Always `source .venv/bin/activate` first.
53
+ - **Rebuild with `maturin develop` after every Rust change**, otherwise the Python tests and the app use the old extension.
54
+ - **wgpu 30 requires a display handle on the instance**, even on Metal (`MissingDisplayHandle` otherwise). It is passed through `QtDisplayHandle`.
55
+ - **Use the `directx` projection, not `vulkan`.** The wgpu NDC is Z in [0, 1] with Y up; `vulkan` flips the image. There is no OpenGL-to-wgpu matrix any more.
56
+ - **GPU tests are skipped silently when there is no adapter, and a skip is a pass.** `OXIDMOL_REQUIRE_GPU=1` turns the skip into a failure. CI runners most likely have no GPU.
57
+ - **The windowless GPU tests do not cover the window.** `GpuContext` and the surface are only checked by launching the app and by the `gpu`-marked widget test.
58
+ - **Renderer errors are `RuntimeError`, not panics.** Catch `Exception` in Python, not `BaseException`.
59
+ - **Linux: only X11/XWayland.** Native Wayland is not supported yet. Manual testing so far was done on macOS only; Linux and Windows are not tested by hand.
60
+ - **PySide6 is capped at `<6.12`** because 6.12 has no wheels for macOS 13. Do not upgrade it without checking that the platform support has changed.
61
+ - **`approx` stays on 0.5** (0.6 is still a release candidate). `pollster` was replaced with `tokio` (feature `rt`).
62
+ - **Every molecule load adds an object instead of replacing the previous one.** Known, not fixed.
63
+ - **The extension uses the stable ABI (`pyo3/abi3-py312`)**: one wheel per platform works for every Python >= 3.12. After switching Python versions or build modes, delete stale `python/oxidmol/lsd.*.so` files: an old `lsd.cpython-*.so` is imported before `lsd.abi3.so`.
64
+ - **Releases**: release-plz runs in git-only mode (versions come from git tags, nothing goes to crates.io). A new `v*` tag starts `publish.yml`: it re-runs `CI.yml` through `workflow_call`, builds the wheels and uploads them to PyPI.
65
+ - Workflows: `CI.yml` (tests and lint on push and PR, also called by `publish.yml`), `publish.yml` (wheels, sdist and PyPI upload for tags; a manual run builds but never uploads), `release-plz.yml` (version PRs, tags). They were generated by `maturin generate-ci` once and are now edited by hand, do not regenerate them.
66
+ - The README and the module description in `lib.rs` say "PyQt6" although the project uses PySide6 (not fixed).
67
+
68
+ ## Conventions
69
+
70
+ - Rust 2021, `cargo fmt`, no clippy warnings. Use a targeted `#[allow(dead_code)]` for intentionally unused code (the idiom already exists in the project).
71
+ - `unsafe` needs a `SAFETY:` comment that states the guarantee.
72
+ - Python: ruff (line length 120, rule set in `pyproject.toml`), strict-like mypy (`disallow_untyped_defs`); type the Qt event arguments (`QShowEvent` etc.).
73
+ - UI: styles live only in `ui/theme.py` (palette plus one `STYLESHEET`). Widgets get an `objectName` and no inline QSS. The exception is the per-object dot color in `object_panel.py`, which is dynamic. Get the monospace font from `theme.mono_font()`, never by family name.
74
+ - Tests: Rust tests sit next to the code (`#[cfg(test)]`); Python tests are in `python/oxidmol/tests` with the `ui` and `gpu` markers declared in `pyproject.toml`. Check a new test against a deliberate breakage: break the code temporarily and make sure the test fails.
75
+ - Commits: `release-plz` and `git-cliff` build the changelog from conventional commits only (`feat:`, `fix:`, `refactor:`, `perf:` ...); other commits are filtered out (`filter_unconventional = true`). Do not commit unless asked.