mapcan 0.23.2__tar.gz → 0.23.3__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.
- {mapcan-0.23.2 → mapcan-0.23.3}/.gitignore +4 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/PKG-INFO +4 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/README.md +3 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/ci-example.md +42 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/cli.md +18 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/web.md +5 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/pyproject.toml +1 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/__init__.py +1 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/cli.py +14 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/diff.py +53 -3
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/launcher.py +4 -4
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/locale/ru.py +5 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/lsp/server.py +20 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/app.py +38 -5
- mapcan-0.23.3/src/mapcan/web/static/assets/index-B4RJ5ml1.js +36 -0
- mapcan-0.23.2/src/mapcan/web/static/assets/index-qvZIhvUc.css → mapcan-0.23.3/src/mapcan/web/static/assets/index-BXiCvm0s.css +1 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/static/index.html +2 -2
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_diff.py +44 -1
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_lsp.py +15 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_workspace.py +32 -1
- mapcan-0.23.2/src/mapcan/web/static/assets/index-B4ld-hyP.js +0 -35
- {mapcan-0.23.2 → mapcan-0.23.3}/LICENSE +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/NOTICE +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/THIRD_PARTY_NOTICES.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/format-changelog.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/format.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/demo-dark.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/demo-light.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/document-pages-dark.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/document-pages-light.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-demo-dark.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-demo-light.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-hover-dark.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-hover-light.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-map-dark.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-map-light.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-problems-dark.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-problems-light.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-quickfix-dark.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/vscode-quickfix-light.gif +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/web-frame-dark.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/images/web-frame-light.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/logo-dark.svg +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/logo-light.svg +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/templates.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/docs/zephyr.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/canopen-node.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/j1939.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/minimal.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/tractor/powertrain.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/tractor/tractor.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/vehicle-can.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/zephyr/CMakeLists.txt +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/zephyr/prj.conf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/examples/zephyr/src/main.c +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/schema/mapcan-part.schema.json +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/schema/mapcan.schema.json +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/bits.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/canopen.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/codegen/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/codegen/c.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/codegen/common.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/codegen/py.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/codegen/zephyr.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/diagnostics.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/docmodel.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/dbc.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/eds.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/pdf.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/text.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/export/xlsx.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/extras.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fmt.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/OFL-PTSerif.txt +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/OFL.txt +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Sans-Web-Bold.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Sans-Web-BoldItalic.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Sans-Web-Italic.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Sans-Web-Regular.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Serif-Web-Bold.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Serif-Web-BoldItalic.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Serif-Web-Italic.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/fonts/PT_Serif-Web-Regular.ttf +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/i18n.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/importers/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/importers/dbc.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/layout.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/loader.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/locale/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/lsp/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/lsp/analysis.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/lsp/edits.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/lsp/exports.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/model.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/protocols.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/resolve.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/schema.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/document.html.j2 +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/document.md.j2 +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/pdf/gost-19.typ +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/pdf/gost-2.105.typ +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/pdf/gost-7.32.typ +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/pdf/lib.typ +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/pdf/mapcan.typ +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/templates/xlsx_style.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/units.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/updates.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/upgrade.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/userconfig.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/validate.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/devicefile.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/examples.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/newmap.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/session.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/static/OFL-Sora.txt +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/static/favicon.svg +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/src/mapcan/web/yamlops.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/__init__.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/conftest.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/invalid_map.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/legacy.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/logo.png +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/logo.svg +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/reference.yaml +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/zephyr_stub/zephyr/drivers/can.h +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/data/zephyr_stub/zephyr/kernel.h +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/golden/reference.md +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/golden/reference_bus1.h +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_auth.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_cli.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_codegen.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_device_file.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_direction.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_eds.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_exchange.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_export.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_extras.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_fmt.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_i18n.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_import.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_include.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_j1939.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_launcher.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_loader_fast.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_lsp_edits.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_model.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_new_signal.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_source.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_templates.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_titles.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_units.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_upgrade.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_validate.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_web.py +0 -0
- {mapcan-0.23.2 → mapcan-0.23.3}/tests/test_yamlops.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mapcan
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.3
|
|
4
4
|
Summary: A YAML map of CAN signals as the single source of truth: validation, Excel/PDF/Markdown/HTML, DBC, C/Python code
|
|
5
5
|
Project-URL: Homepage, https://github.com/ordu1453/mapcan
|
|
6
6
|
Project-URL: Documentation, https://github.com/ordu1453/mapcan/blob/main/docs/format.md
|
|
@@ -125,6 +125,9 @@ mapcan build vehicle.yaml -o build/ # PDF, Excel, HTML, DBC, C and Pytho
|
|
|
125
125
|
mapcan app vehicle.yaml # edit it on the bit layout
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
The app, the web editor (**File → Import DBC…**) and the VS Code extension (**mapcan: Import DBC…**)
|
|
129
|
+
import a DBC too.
|
|
130
|
+
|
|
128
131
|
You keep your tools: the DBC exported back works in CANalyzer, SavvyCAN, PCAN-View and Wireshark.
|
|
129
132
|
|
|
130
133
|
## Why not just a DBC file?
|
|
@@ -53,6 +53,9 @@ mapcan build vehicle.yaml -o build/ # PDF, Excel, HTML, DBC, C and Pytho
|
|
|
53
53
|
mapcan app vehicle.yaml # edit it on the bit layout
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
The app, the web editor (**File → Import DBC…**) and the VS Code extension (**mapcan: Import DBC…**)
|
|
57
|
+
import a DBC too.
|
|
58
|
+
|
|
56
59
|
You keep your tools: the DBC exported back works in CANalyzer, SavvyCAN, PCAN-View and Wireshark.
|
|
57
60
|
|
|
58
61
|
## Why not just a DBC file?
|
|
@@ -45,6 +45,48 @@ jobs:
|
|
|
45
45
|
merge. `mapcan fmt --check` fails when the keys of the map are not in the canonical order, so that maps
|
|
46
46
|
edited by different people and editors stay easy to review; `mapcan fmt my-map.yaml` fixes it.
|
|
47
47
|
|
|
48
|
+
## Breaking changes in pull requests
|
|
49
|
+
|
|
50
|
+
A map describes devices already in the field: moving a signal, changing its scale or reassigning an
|
|
51
|
+
ID breaks them, while a new signal in free bits does not. `mapcan diff --fail-on breaking` tells the
|
|
52
|
+
two apart (see [cli.md](cli.md#comparing-versions)), so a pull request that breaks the map fails
|
|
53
|
+
unless it says so on purpose: here with the label `breaking`, or by raising the major version of
|
|
54
|
+
`document.version`.
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
name: mapcan-compat
|
|
58
|
+
on:
|
|
59
|
+
pull_request:
|
|
60
|
+
paths: ["my-map.yaml"]
|
|
61
|
+
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
compat:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'breaking') }}
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0
|
|
71
|
+
- uses: astral-sh/setup-uv@v5
|
|
72
|
+
- run: uv tool install "mapcan==X.Y.Z"
|
|
73
|
+
- name: Compare with the base branch
|
|
74
|
+
shell: bash # with pipefail, so that tee keeps the exit code of mapcan diff
|
|
75
|
+
run: |
|
|
76
|
+
git show "origin/${{ github.base_ref }}:my-map.yaml" > base.yaml
|
|
77
|
+
old=$(yq '.document.version' base.yaml | cut -d. -f1)
|
|
78
|
+
new=$(yq '.document.version' my-map.yaml | cut -d. -f1)
|
|
79
|
+
if [ "$new" != "$old" ]; then
|
|
80
|
+
echo "major version raised ($old → $new): breaking changes allowed"
|
|
81
|
+
mapcan diff base.yaml my-map.yaml --lang en
|
|
82
|
+
else
|
|
83
|
+
mapcan diff base.yaml my-map.yaml --lang en --format md --fail-on breaking | tee -a "$GITHUB_STEP_SUMMARY"
|
|
84
|
+
fi
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`git show` gives only the main file: a map split into included files needs the base branch checked
|
|
88
|
+
out next to it (`git worktree add ../base origin/main`) and `mapcan diff ../base/my-map.yaml my-map.yaml`.
|
|
89
|
+
|
|
48
90
|
## GitLab CI
|
|
49
91
|
|
|
50
92
|
`.gitlab-ci.yml`, using the published Docker image:
|
|
@@ -16,7 +16,7 @@ mapcan export eds FILE [-o DIR] [--bus NAME]
|
|
|
16
16
|
mapcan gen c FILE [-o DIR] [--prefix my_] [--bus NAME]
|
|
17
17
|
mapcan gen py FILE [-o DIR] [--bus NAME]
|
|
18
18
|
mapcan build FILE [-o DIR] [--formats xlsx,pdf,md,html,dbc,eds,c,py] [--pdf-template NAME]
|
|
19
|
-
mapcan diff OLD NEW [--format text|md|json]
|
|
19
|
+
mapcan diff OLD NEW [--format text|md|json] [--fail-on breaking|any]
|
|
20
20
|
mapcan import dbc FILE.dbc [-o FILE.yaml] [--title TEXT] [--lang en] [--force]
|
|
21
21
|
mapcan migrate FILE… [--check] # former field names → current ones
|
|
22
22
|
mapcan fmt FILE… [--check] # keys in one order, comments and layout kept
|
|
@@ -188,6 +188,23 @@ becomes a `can` bus with explicit IDs. Value names become identifiers (`Not avai
|
|
|
188
188
|
`NOT_AVAILABLE`, the original text goes to the description), and whatever DBC cannot express is
|
|
189
189
|
reported as a warning.
|
|
190
190
|
|
|
191
|
+
## Comparing versions
|
|
192
|
+
|
|
193
|
+
`mapcan diff OLD NEW` lists what changed between two versions of a map and marks the changes that
|
|
194
|
+
break devices built against the old one (`[breaking]` in text, **breaking:** in Markdown, a
|
|
195
|
+
`breaking` field per change in JSON):
|
|
196
|
+
|
|
197
|
+
| Breaking | Compatible |
|
|
198
|
+
|---|---|
|
|
199
|
+
| a bus, frame, signal, flag or value removed | anything added: buses, devices, frames, signals in free bits, values |
|
|
200
|
+
| a frame's ID, CANopen function, PGN, priority, length or IDE changed; a device's node ID or address | timing, transmission mode, sender and receivers |
|
|
201
|
+
| a signal moved or resized; its byte order, representation, encoding, factor or offset changed | titles, descriptions, units of display, initial values |
|
|
202
|
+
| a value renamed or renumbered; a flag moved; the multiplexer or a multiplexer value changed | limits widened or removed |
|
|
203
|
+
| a limit narrowed or added | |
|
|
204
|
+
|
|
205
|
+
`--fail-on breaking` exits with code 1 when a breaking change is found, `--fail-on any` on any change;
|
|
206
|
+
[ci-example.md](ci-example.md#breaking-changes-in-pull-requests) uses it to check pull requests.
|
|
207
|
+
|
|
191
208
|
## CI
|
|
192
209
|
|
|
193
210
|
[ci-example.md](ci-example.md) shows a pipeline for map repositories: when the YAML changes, the map
|
|
@@ -47,6 +47,10 @@ Save as… / Close map** all work in the browser. The file dialog can browse any
|
|
|
47
47
|
accepts a typed path and lists recent files. If the current map has unsaved changes, the editor
|
|
48
48
|
offers to save them before switching.
|
|
49
49
|
|
|
50
|
+
**File → Import DBC…** (also on the start screen) turns a DBC file into a map, as
|
|
51
|
+
`mapcan import dbc` does: the map is written next to the DBC with its name (the editor asks before
|
|
52
|
+
overwriting) and opens; whatever the DBC cannot express is listed once it is open.
|
|
53
|
+
|
|
50
54
|
The start screen also lists the bundled examples: pick one, choose where to save a copy, and it
|
|
51
55
|
opens. The mapcan version is shown in the top bar next to the title and on the start screen.
|
|
52
56
|
**File → Report an issue** (and the link on the start screen) opens a new GitHub issue with the
|
|
@@ -177,7 +181,7 @@ available at `http://127.0.0.1:8765/api/docs`.
|
|
|
177
181
|
| GET | `/api/update` | Newer version on GitHub Releases: `{current, latest, newer, url, installer}` (`mapcan app` and local `serve` only) |
|
|
178
182
|
| POST | `/api/instance/activate` | `{token, path}`: a second launch hands the file to the running instance (the token is in `~/.mapcan/instance.json`) |
|
|
179
183
|
| GET | `/api/fs/list?dir=` | Folders and YAML files for the file dialog |
|
|
180
|
-
| POST | `/api/file/open`, `/api/file/new`, `/api/file/example`, `/api/file/save_as`, `/api/file/close` | File operations; `409 unsaved` means there are unsaved changes (pass `force: true` to proceed) |
|
|
184
|
+
| POST | `/api/file/open`, `/api/file/new`, `/api/file/example`, `/api/file/import_dbc`, `/api/file/save_as`, `/api/file/close` | File operations; `409 unsaved` means there are unsaved changes (pass `force: true` to proceed) |
|
|
181
185
|
| GET | `/api/state` | Text, data, computed layout and diagnostics |
|
|
182
186
|
| POST | `/api/ops` | `{"ops": [...]}`: apply operations atomically |
|
|
183
187
|
| POST | `/api/device_file` | `{"bus": ["buses", 0], "device": {...}, "file": "devices/node.yaml"}`: a new device in a new file, added to `include` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mapcan"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.3"
|
|
4
4
|
description = "A YAML map of CAN signals as the single source of truth: validation, Excel/PDF/Markdown/HTML, DBC, C/Python code"
|
|
5
5
|
# readme: built from README.md with absolute links for PyPI (see fancy-pypi-readme below)
|
|
6
6
|
dynamic = ["readme"]
|
|
@@ -208,12 +208,20 @@ class DiffFormat(str, Enum):
|
|
|
208
208
|
json = "json"
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
class FailOn(str, Enum):
|
|
212
|
+
breaking = "breaking"
|
|
213
|
+
any = "any"
|
|
214
|
+
|
|
215
|
+
|
|
211
216
|
@app.command(help=_("Compare two versions of a map (frames, signals, values) and print a changelog."))
|
|
212
217
|
def diff(
|
|
213
218
|
old: Path = typer.Argument(..., exists=True, dir_okay=False, help=_("Old version of the map.")),
|
|
214
219
|
new: Path = typer.Argument(..., exists=True, dir_okay=False, help=_("New version of the map.")),
|
|
215
220
|
fmt: DiffFormat = typer.Option(DiffFormat.text, "--format", help="text | md | json"),
|
|
216
221
|
lang: Optional[str] = LangOpt,
|
|
222
|
+
fail_on: Optional[FailOn] = typer.Option(
|
|
223
|
+
None, "--fail-on",
|
|
224
|
+
help=_("Exit code 1 on breaking changes (breaking) or on any change (any).")),
|
|
217
225
|
) -> None:
|
|
218
226
|
from .diff import diff_maps, render_diff
|
|
219
227
|
from .loader import load
|
|
@@ -228,6 +236,12 @@ def diff(
|
|
|
228
236
|
maps.append(res.map)
|
|
229
237
|
d = diff_maps(maps[0], maps[1], lang)
|
|
230
238
|
typer.echo(render_diff(d, fmt.value, lang or maps[1].lang))
|
|
239
|
+
if fail_on is FailOn.breaking and d.breaking:
|
|
240
|
+
typer.secho(_("breaking changes: {count}", count=len(d.breaking)), fg=typer.colors.RED, err=True)
|
|
241
|
+
raise typer.Exit(1)
|
|
242
|
+
if fail_on is FailOn.any and not d.empty:
|
|
243
|
+
typer.secho(_("changes: {count}", count=len(d.changes)), fg=typer.colors.RED, err=True)
|
|
244
|
+
raise typer.Exit(1)
|
|
231
245
|
|
|
232
246
|
|
|
233
247
|
@app.command(help=_("Rewrite the former names of fields (deprecated-key) to the current ones, keeping comments and "
|
|
@@ -27,6 +27,7 @@ class Change:
|
|
|
27
27
|
field: str = "" # changed field
|
|
28
28
|
old: Any = None
|
|
29
29
|
new: Any = None
|
|
30
|
+
breaking: bool = False # devices built against the old version misread the bus (see is_breaking)
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass
|
|
@@ -39,6 +40,10 @@ class MapDiff:
|
|
|
39
40
|
def empty(self) -> bool:
|
|
40
41
|
return not self.changes
|
|
41
42
|
|
|
43
|
+
@property
|
|
44
|
+
def breaking(self) -> list[Change]:
|
|
45
|
+
return [c for c in self.changes if c.breaking]
|
|
46
|
+
|
|
42
47
|
def to_dict(self) -> dict:
|
|
43
48
|
return {"old_version": self.old_version, "new_version": self.new_version,
|
|
44
49
|
"changes": [asdict(c) for c in self.changes]}
|
|
@@ -56,6 +61,45 @@ SIGNAL_FIELDS = ("start_bit", "bit_length", "byte_order", "representation", "enc
|
|
|
56
61
|
"min", "max", "initial", "source", "invalid_raw", "multiplexer", "mux", "description")
|
|
57
62
|
FLAG_FIELDS = ("bit", "bit_length", "description")
|
|
58
63
|
|
|
64
|
+
# Fields whose change makes a device built against the old version send or read the bus differently
|
|
65
|
+
BREAKING_FIELDS = {
|
|
66
|
+
"device": {"node_id", "address"},
|
|
67
|
+
"frame": {"can_id", "canopen", "length", "extended", "pgn", "priority"},
|
|
68
|
+
"signal": {"start_bit", "bit_length", "byte_order", "representation", "encoding", "factor", "offset",
|
|
69
|
+
"multiplexer", "mux"},
|
|
70
|
+
"flag": {"bit", "bit_length"},
|
|
71
|
+
"value": {"name"},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _narrowed(fld: str, old: Any, new: Any) -> bool:
|
|
76
|
+
"""A limit (min or max) admits fewer values than before; a limit set where there was none does too."""
|
|
77
|
+
if new is None:
|
|
78
|
+
return False
|
|
79
|
+
if old is None:
|
|
80
|
+
return True
|
|
81
|
+
try:
|
|
82
|
+
return new > old if fld == "min" else new < old
|
|
83
|
+
except TypeError:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_breaking(c: Change) -> bool:
|
|
88
|
+
"""The change breaks devices built against the old version of the map.
|
|
89
|
+
|
|
90
|
+
Removing a bus, frame, signal, flag or value breaks them; so do moving or rescaling a signal, changing the
|
|
91
|
+
identifier or length of a frame, renaming or renumbering a value and narrowing a limit. Additions, texts,
|
|
92
|
+
units of display, timing and widened limits do not. A removed device is not breaking by itself: its frames
|
|
93
|
+
are listed as removed.
|
|
94
|
+
"""
|
|
95
|
+
if c.kind == "added":
|
|
96
|
+
return False
|
|
97
|
+
if c.kind == "removed":
|
|
98
|
+
return c.scope != "device"
|
|
99
|
+
if c.scope == "signal" and c.field in ("min", "max"):
|
|
100
|
+
return _narrowed(c.field, c.old, c.new)
|
|
101
|
+
return c.field in BREAKING_FIELDS.get(c.scope, ())
|
|
102
|
+
|
|
59
103
|
|
|
60
104
|
def _invalid_text(s: Signal) -> str | None:
|
|
61
105
|
"""invalid_raw as one comparable text: "not_available 0xFF; error 0xFE00..0xFEFF"."""
|
|
@@ -104,6 +148,8 @@ def diff_maps(old: MapCan, new: MapCan, lang: str | None = None) -> MapDiff:
|
|
|
104
148
|
d.changes.append(_frame_change("added", name, f))
|
|
105
149
|
continue
|
|
106
150
|
_diff_bus(d, ob[name], nb[name], langs)
|
|
151
|
+
for c in d.changes:
|
|
152
|
+
c.breaking = is_breaking(c)
|
|
107
153
|
return d
|
|
108
154
|
|
|
109
155
|
|
|
@@ -254,14 +300,14 @@ _T = {
|
|
|
254
300
|
"bus": "шина", "device": "устройство", "frame": "кадр", "signal": "сигнал",
|
|
255
301
|
"flag": "флаг", "value": "значение", "bits": "биты", "byte": "байт",
|
|
256
302
|
"added_n": "добавлено", "removed_n": "удалено", "changed_n": "изменено",
|
|
257
|
-
"none": "Изменений нет.",
|
|
303
|
+
"none": "Изменений нет.", "breaking": "несовместимо",
|
|
258
304
|
},
|
|
259
305
|
"en": {
|
|
260
306
|
"added": "added", "removed": "removed", "changed": "changed",
|
|
261
307
|
"bus": "bus", "device": "device", "frame": "frame", "signal": "signal",
|
|
262
308
|
"flag": "flag", "value": "value", "bits": "bits", "byte": "byte",
|
|
263
309
|
"added_n": "added", "removed_n": "removed", "changed_n": "changed",
|
|
264
|
-
"none": "No changes.",
|
|
310
|
+
"none": "No changes.", "breaking": "breaking",
|
|
265
311
|
},
|
|
266
312
|
}
|
|
267
313
|
|
|
@@ -375,9 +421,13 @@ def release_message(version: str, changes: list[str]) -> str:
|
|
|
375
421
|
def render_diff(diff: MapDiff, fmt: str = "text", lang: str = "ru") -> str:
|
|
376
422
|
if fmt == "json":
|
|
377
423
|
return diff.to_json()
|
|
424
|
+
T = _T.get(lang, _T["ru"])
|
|
378
425
|
lines = changelog_lines(diff, lang)
|
|
379
426
|
if not lines:
|
|
380
|
-
return
|
|
427
|
+
return T["none"]
|
|
428
|
+
# changelog_lines gives one line per change, in the same order
|
|
429
|
+
mark = f"**{T['breaking']}:** " if fmt == "md" else f"[{T['breaking']}] "
|
|
430
|
+
lines = [mark + line if c.breaking else line for c, line in zip(diff.changes, lines, strict=True)]
|
|
381
431
|
if fmt == "md":
|
|
382
432
|
head = f"## {diff.old_version} → {diff.new_version}\n\n"
|
|
383
433
|
return head + "\n".join(f"- {line}" for line in lines)
|
|
@@ -179,13 +179,13 @@ def _webview():
|
|
|
179
179
|
class DialogApi:
|
|
180
180
|
"""Methods for the window's JS (window.pywebview.api): the system open and save file dialogs."""
|
|
181
181
|
|
|
182
|
-
FILE_TYPES = ("YAML (*.yaml;*.yml)", "All files (*.*)")
|
|
182
|
+
FILE_TYPES = {"yaml": ("YAML (*.yaml;*.yml)", "All files (*.*)"), "dbc": ("DBC (*.dbc)", "All files (*.*)")}
|
|
183
183
|
|
|
184
184
|
def __init__(self, get_window):
|
|
185
185
|
self._get_window = get_window # with "_": pywebview does not expose the attribute to JS
|
|
186
186
|
|
|
187
|
-
def pick_file(self, mode: str, directory: str = "", filename: str = "") -> str | None:
|
|
188
|
-
"""``mode``: "open" or "save". Returns the chosen path or None (cancelled)."""
|
|
187
|
+
def pick_file(self, mode: str, directory: str = "", filename: str = "", types: str = "yaml") -> str | None:
|
|
188
|
+
"""``mode``: "open" or "save"; ``types``: a key of FILE_TYPES. Returns the chosen path or None (cancelled)."""
|
|
189
189
|
import webview
|
|
190
190
|
|
|
191
191
|
win = self._get_window()
|
|
@@ -193,7 +193,7 @@ class DialogApi:
|
|
|
193
193
|
return None
|
|
194
194
|
kind = webview.FileDialog.SAVE if mode == "save" else webview.FileDialog.OPEN
|
|
195
195
|
result = win.create_file_dialog(kind, directory=directory or "", save_filename=filename or "",
|
|
196
|
-
file_types=self.FILE_TYPES)
|
|
196
|
+
file_types=self.FILE_TYPES.get(types, self.FILE_TYPES["yaml"]))
|
|
197
197
|
if not result:
|
|
198
198
|
return None
|
|
199
199
|
return result if isinstance(result, str) else result[0]
|
|
@@ -21,6 +21,10 @@ RU: dict[str, str] = {
|
|
|
21
21
|
"Редактор в отдельном окне (pywebview); повторный запуск открывает файл в уже запущенном окне.",
|
|
22
22
|
"Compare two versions of a map (frames, signals, values) and print a changelog.":
|
|
23
23
|
"Сравнить две версии карты (кадры, сигналы, значения) и вывести changelog.",
|
|
24
|
+
"Exit code 1 on breaking changes (breaking) or on any change (any).":
|
|
25
|
+
"Код возврата 1 при несовместимых изменениях (breaking) или при любых (any).",
|
|
26
|
+
"breaking changes: {count}": "несовместимых изменений: {count}",
|
|
27
|
+
"changes: {count}": "изменений: {count}",
|
|
24
28
|
"List validation rules.": "Список правил проверки.",
|
|
25
29
|
"Print the format's JSON Schema (for IDE hints).": "Вывести JSON Schema формата (для подсказок в IDE).",
|
|
26
30
|
"List PDF templates or copy a built-in template to customize the layout.":
|
|
@@ -623,6 +627,7 @@ RU: dict[str, str] = {
|
|
|
623
627
|
"{fmt} export error: {error}": "ошибка экспорта {fmt}: {error}",
|
|
624
628
|
"the file is not UTF-8 encoded": "файл не в UTF-8",
|
|
625
629
|
"could not read {name}: {error}": "не удалось прочитать {name}: {error}",
|
|
630
|
+
"could not import {name}: {error}": "не удалось импортировать {name}: {error}",
|
|
626
631
|
"could not create {path}: {error}": "не удалось создать {path}: {error}",
|
|
627
632
|
"could not save {path}: {error}": "не удалось сохранить {path}: {error}",
|
|
628
633
|
"could not save the logo: {error}": "не удалось сохранить логотип: {error}",
|
|
@@ -404,6 +404,26 @@ def new_map_command(ls: MapcanServer, *args) -> dict:
|
|
|
404
404
|
return {"error": str(e)}
|
|
405
405
|
|
|
406
406
|
|
|
407
|
+
@server.command("mapcan.lsp.importDbc")
|
|
408
|
+
@server.thread()
|
|
409
|
+
def import_dbc_command(ls: MapcanServer, *args) -> dict:
|
|
410
|
+
"""Argument: {path, title, language}: a DBC file. Response: {text, warnings} (the YAML of the map and what the
|
|
411
|
+
import could not bring over) or {error}."""
|
|
412
|
+
from ..i18n import normalize_language
|
|
413
|
+
from ..importers.dbc import import_dbc
|
|
414
|
+
|
|
415
|
+
params = _arg(args)
|
|
416
|
+
path = Path(str(params.get("path") or ""))
|
|
417
|
+
if not path.is_file():
|
|
418
|
+
return {"error": _("file not found: {path}", path=path)}
|
|
419
|
+
try:
|
|
420
|
+
text, warnings = import_dbc(path, params.get("title") or None,
|
|
421
|
+
normalize_language(str(params.get("language") or "en")) or "en")
|
|
422
|
+
except Exception as e: # noqa: BLE001 — cantools raises many kinds of errors on a broken DBC
|
|
423
|
+
return {"error": _("could not import {name}: {error}", name=path.name, error=e)}
|
|
424
|
+
return {"text": text, "warnings": warnings}
|
|
425
|
+
|
|
426
|
+
|
|
407
427
|
@server.command("mapcan.lsp.example")
|
|
408
428
|
def example_command(ls: MapcanServer, *args) -> dict:
|
|
409
429
|
"""Argument: {name, mapFile}: an example and the file name of its copy. Response: {text, includes: {path
|
|
@@ -120,6 +120,14 @@ class ExampleBody(BaseModel):
|
|
|
120
120
|
overwrite: bool = False
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
class ImportBody(BaseModel):
|
|
124
|
+
source: str # the DBC file
|
|
125
|
+
path: str = "" # the new map; next to the DBC, with its name, by default
|
|
126
|
+
language: str = "en" # document language of the new map
|
|
127
|
+
force: bool = False
|
|
128
|
+
overwrite: bool = False
|
|
129
|
+
|
|
130
|
+
|
|
123
131
|
class SaveAsBody(BaseModel):
|
|
124
132
|
path: str
|
|
125
133
|
overwrite: bool = False
|
|
@@ -314,8 +322,12 @@ class Workspace:
|
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
FILE_KINDS = {"yaml": (".yaml", ".yml"), "dbc": (".dbc",)}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def list_dir(d: Path, kind: str = "yaml") -> dict:
|
|
329
|
+
"""Contents of a folder for the file dialog: subfolders and the files of ``kind`` (FILE_KINDS)."""
|
|
330
|
+
suffixes = FILE_KINDS.get(kind, FILE_KINDS["yaml"])
|
|
319
331
|
if not d.is_dir():
|
|
320
332
|
raise AppError(404, "not-found", _("folder not found: {path}", path=d))
|
|
321
333
|
try:
|
|
@@ -328,7 +340,7 @@ def list_dir(d: Path) -> dict:
|
|
|
328
340
|
continue
|
|
329
341
|
try:
|
|
330
342
|
is_dir = it.is_dir()
|
|
331
|
-
if not is_dir and it.suffix.lower() not in
|
|
343
|
+
if not is_dir and it.suffix.lower() not in suffixes:
|
|
332
344
|
continue
|
|
333
345
|
st = it.stat()
|
|
334
346
|
except OSError:
|
|
@@ -680,8 +692,8 @@ def create_app(session: EditorSession | None = None, poll_interval: float = 1.0,
|
|
|
680
692
|
return path
|
|
681
693
|
|
|
682
694
|
@app.get("/api/fs/list")
|
|
683
|
-
async def fs_list(dir: str = "") -> dict:
|
|
684
|
-
return list_dir(wsp.resolve(dir) if dir else wsp.cwd)
|
|
695
|
+
async def fs_list(dir: str = "", kind: str = "yaml") -> dict:
|
|
696
|
+
return list_dir(wsp.resolve(dir) if dir else wsp.cwd, kind)
|
|
685
697
|
|
|
686
698
|
@app.post("/api/file/open")
|
|
687
699
|
async def file_open(body: OpenBody) -> dict:
|
|
@@ -738,6 +750,27 @@ def create_app(session: EditorSession | None = None, poll_interval: float = 1.0,
|
|
|
738
750
|
return await create_file(body.path, body.force, body.overwrite, lambda _path: example_text(body.name),
|
|
739
751
|
included)
|
|
740
752
|
|
|
753
|
+
@app.post("/api/file/import_dbc")
|
|
754
|
+
async def file_import_dbc(body: ImportBody) -> dict:
|
|
755
|
+
from ..importers.dbc import import_dbc
|
|
756
|
+
|
|
757
|
+
source = wsp.resolve(body.source)
|
|
758
|
+
if not source.is_file():
|
|
759
|
+
raise AppError(404, "not-found", _("file not found: {path}", path=source))
|
|
760
|
+
warnings: list[str] = []
|
|
761
|
+
|
|
762
|
+
def make_text(path: Path) -> str:
|
|
763
|
+
try:
|
|
764
|
+
text, found = import_dbc(source, path.stem, normalize_language(body.language) or "en")
|
|
765
|
+
except Exception as e: # noqa: BLE001 — cantools raises many kinds of errors on a broken DBC
|
|
766
|
+
raise AppError(400, "import", _("could not import {name}: {error}", name=source.name, error=e)) from e
|
|
767
|
+
warnings.extend(found)
|
|
768
|
+
return text
|
|
769
|
+
|
|
770
|
+
result = await create_file(body.path or str(source.with_suffix(".yaml")), body.force, body.overwrite,
|
|
771
|
+
make_text)
|
|
772
|
+
return {**result, "warnings": warnings}
|
|
773
|
+
|
|
741
774
|
@app.post("/api/file/save_as")
|
|
742
775
|
async def file_save_as(body: SaveAsBody) -> dict:
|
|
743
776
|
writable()
|