Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 59 additions & 49 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,72 @@ name: Python package
on: [ push, pull_request ]

env:
A2L_GRPC_VERSION: v0.1.21
A2L_GRPC_VERSION: v0.2.1

jobs:
generate-grpc-sources:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: 3.11
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install grpcio-tools
# the generated sources are distributed with the package, and the protobuf runtime refuses to load sources
# generated by a more recent version of itself. As protobuf 7.x requires Python 3.10 or newer, the sources
# must be generated with the 6.x code generator to remain loadable on the oldest supported Python version.
# This constraint can be dropped as soon as Python 3.9 is not supported anymore.
pip install "grpcio-tools>=1.71.0,<1.81"
wget https://github.com/Sauci/a2l-grpc/releases/download/$A2L_GRPC_VERSION/a2l_grpc.tar.gz
tar -xf a2l_grpc.tar.gz -C pya2l
- name: Generate gRPC sources
run: |
python -m grpc_tools.protoc -I./pya2l/a2l_grpc --python_out=pya2l --pyi_out=pya2l --grpc_python_out=pya2l ./pya2l/a2l_grpc/protobuf/*.proto
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: protobuf
path: pya2l/protobuf
run-pytest-test-linux:
runs-on: ubuntu-22.04
run-pytest-test:
runs-on: ${{ matrix.target.runner }}
needs:
- generate-grpc-sources
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
# one entry per shared object shipped by the a2l-grpc release, except for the linux/386 and linux/arm targets,
# for which no runner is available.
target:
- { name: linux_amd64, runner: ubuntu-22.04, architecture: x64 }
- { name: linux_arm64, runner: ubuntu-22.04-arm, architecture: arm64 }
- { name: windows_amd64, runner: windows-2022, architecture: x64 }
- { name: windows_386, runner: windows-2022, architecture: x86 }
- { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }
- { name: darwin_arm64, runner: macos-15, architecture: arm64 }
- { name: darwin_amd64, runner: macos-15-intel, architecture: x64 }
python-version: ['3.13', '3.12', '3.11', '3.10', '3.9']
exclude:
# no windows/arm64 build of the Python interpreter is available below 3.11
- { target: { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }, python-version: '3.10' }
- { target: { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }, python-version: '3.9' }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.target.architecture }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# see https://github.com/pytest-dev/pytest/issues/11868
pip install codecov dictdiffer grpcio-tools>=1.71.0 mktestdocs mock pytest==7.4.3 pytest-cases pytest-cov pytest-xdist
wget https://github.com/Sauci/a2l-grpc/releases/download/$A2L_GRPC_VERSION/a2l_grpc.tar.gz
# pytest 9 is not supported by pytest-cases, see https://github.com/pytest-dev/pytest/issues/11868
pip install codecov dictdiffer "grpcio-tools>=1.71.0" mktestdocs mock "pytest<9" pytest-cases pytest-cov pytest-xdist
curl -sSfL -o a2l_grpc.tar.gz https://github.com/Sauci/a2l-grpc/releases/download/$A2L_GRPC_VERSION/a2l_grpc.tar.gz
tar -xf a2l_grpc.tar.gz -C pya2l
- uses: actions/download-artifact@v4
with:
name: protobuf
path: pya2l/protobuf
- name: Test with pytest
run: |
pytest -n 1 --cov-report html --cov pya2l --verbose
codecov
run-pytest-test-windows:
runs-on: windows-2019
needs:
- generate-grpc-sources
strategy:
fail-fast: false
matrix:
python-version: ['3.13', '3.12', '3.11', '3.10', '3.9']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install codecov dictdiffer grpcio-tools mktestdocs mock pytest pytest-cases pytest-cov pytest-xdist
powershell.exe -Command "Invoke-WebRequest -OutFile ./a2l_grpc.tar.gz https://github.com/Sauci/a2l-grpc/releases/download/$env:A2L_GRPC_VERSION/a2l_grpc.tar.gz"
tar -xf a2l_grpc.tar.gz -C pya2l
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: protobuf
path: pya2l/protobuf
Expand All @@ -84,23 +79,22 @@ jobs:
build-distribution:
runs-on: ubuntu-22.04
needs:
- run-pytest-test-linux
- run-pytest-test-windows
- run-pytest-test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
wget https://github.com/Sauci/a2l-grpc/releases/download/$A2L_GRPC_VERSION/a2l_grpc.tar.gz
tar -xf a2l_grpc.tar.gz -C pya2l
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: protobuf
path: pya2l/protobuf
- run: |
python -m build --sdist --wheel
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist
Expand All @@ -114,24 +108,40 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: dist
path: dist
- name: Publish package on Python Package Index
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
test-pip-package:
runs-on: ubuntu-22.04
runs-on: ${{ matrix.target.runner }}
needs:
- publish-package
defaults:
run:
shell: bash
strategy:
matrix:
target:
- { name: linux_amd64, runner: ubuntu-22.04, architecture: x64 }
- { name: linux_arm64, runner: ubuntu-22.04-arm, architecture: arm64 }
- { name: windows_amd64, runner: windows-2022, architecture: x64 }
- { name: windows_386, runner: windows-2022, architecture: x86 }
- { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }
- { name: darwin_arm64, runner: macos-15, architecture: arm64 }
- { name: darwin_amd64, runner: macos-15-intel, architecture: x64 }
python-version: ['3.13', '3.12', '3.11', '3.10', '3.9']
exclude:
# no windows/arm64 build of the Python interpreter is available below 3.11
- { target: { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }, python-version: '3.10' }
- { target: { name: windows_arm64, runner: windows-11-arm, architecture: arm64 }, python-version: '3.9' }
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.target.architecture }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
83 changes: 73 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# pya2l

| branch | build | coverage |
|:------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------:|
| [master](https://github.com/Sauci/pya2l/tree/master) | [![Python package](https://github.com/Sauci/pya2l/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Sauci/pya2l/actions/workflows/build.yml) | [![code coverage](https://codecov.io/gh/Sauci/pya2l/branch/master/graphs/badge.svg?token=Q5aceZRFXh)](https://codecov.io/gh/Sauci/pya2l?branch=master) |
Expand All @@ -7,34 +9,60 @@

## Package description

the purpose of this package is to provide an easy way to access and navigate
in [a2l](https://www.asam.net/standards/detail/mcd-2-mc/) formatted file.
once the file has been loaded, a tree of Python objects is generated, allowing the user to access nodes.
The purpose of this package is to provide an easy way to access and navigate
an [A2L](https://www.asam.net/standards/detail/mcd-2-mc/)-formatted file.
Once the file has been loaded, a tree of Python objects is generated, allowing the user to access nodes.

## Installation

### Using *pip*

Install the last released version of the package by running the following command:
Install the latest released version of the package by running the following command:
`pip install pya2l`

or install the most recent version of the package (master branch) by running the following command:
`pip install git+https://github.com/Sauci/pya2l.git@master`

## Supported platforms

The A2L document itself is processed by the [a2l-grpc](https://github.com/Sauci/a2l-grpc) backend, whose shared objects
are distributed with this package. The one matching the host is selected at runtime, which means that a 32-bit
interpreter running on a 64-bit machine loads the 32-bit shared object.

| operating system | architecture | shared object | tested in CI |
|:-----------------|:-------------|:------------------------------|:--------------------|
| Linux | x86-64 | `a2l_grpc_linux_amd64.so` | Python 3.9 to 3.13 |
| Linux | x86 (32-bit) | `a2l_grpc_linux_386.so` | no runner available |
| Linux | ARM64 | `a2l_grpc_linux_arm64.so` | Python 3.9 to 3.13 |
| Linux | ARM (32-bit) | `a2l_grpc_linux_arm.so` | no runner available |
| Windows | x86-64 | `a2l_grpc_windows_amd64.dll` | Python 3.9 to 3.13 |
| Windows | x86 (32-bit) | `a2l_grpc_windows_386.dll` | Python 3.9 to 3.13 |
| Windows | ARM64 | `a2l_grpc_windows_arm64.dll` | Python 3.11 to 3.13 |
| macOS | x86-64 | `a2l_grpc_darwin_amd64.dylib` | Python 3.9 to 3.13 |
| macOS | ARM64 | `a2l_grpc_darwin_arm64.dylib` | Python 3.9 to 3.13 |

The two platforms which are not covered by the continuous integration are supported, but no GitHub-hosted runner is
available to test them. On Windows ARM64, the tests start at Python 3.11 because no build of the interpreter is
available for that platform below this version.

## Example of usage

### Command line tool

Once the package installed, the `pya2l` command will be available. It provides several different commands:
Once the package is installed, the `pya2l` command is available. It provides several different commands:

- Convert an A2L file to JSON with `pya2l -v <source>.a2l to_json -o <output.json> -i 2`
- Convert an A2L file to A2L with `pya2l -v <source.a2l> to_a2l -o <output.a2l> -i 2`
- Convert a JSON-formatted A2L file to JSON with `pya2l -v <source>.json to_json -o <output.json> -i 2`
- Convert a JSON-formatted A2L file to A2L with `pya2l -v <source>.json to_a2l -o <output.a2l> -i 2`
- Convert an A2L file to JSON with `pya2l -v <source>.a2l to_json -o <output>.json -i 2`
- Convert an A2L file to A2L with `pya2l -v <source>.a2l to_a2l -o <output>.a2l -i 2`
- Convert a JSON-formatted A2L file to JSON with `pya2l -v <source>.json to_json -o <output>.json -i 2`
- Convert a JSON-formatted A2L file to A2L with `pya2l -v <source>.json to_a2l -o <output>.a2l -i 2`

Adding the `-c` option to any of the above commands rejects files which use keywords requiring a newer ASAP2 version
than the one declared by the file itself. Without this option, such keywords are reported as warnings, which are
displayed with the `-v` option.

### Python API

the bellow code snippet shows how properties of a node in an a2l string can be retrieved using this package.
The code snippet below shows how the properties of a node in an A2L string can be retrieved with this package.

```python
from pya2l.parser import A2lParser as Parser
Expand Down Expand Up @@ -100,3 +128,38 @@ with Parser() as p:
}
}"""
```

### Error handling and ASAP2 version check

When the backend is unable to convert a document, an `A2lError` exception is raised, containing the reason for the
failure.

Keywords requiring a more recent ASAP2 version than the one declared by the file are reported in the `warnings`
property of the parser. They can be treated as errors by setting the `enforce_version_check` argument.

```python
from pya2l.parser import A2lError, A2lParser as Parser

a2l_string = """ASAP2_VERSION 1 50
/begin PROJECT project_name "example project"
/begin MODULE first_module "first module long identifier"
/begin MOD_COMMON "example of mod common"
ALIGNMENT_INT64 8
/end MOD_COMMON
/end MODULE
/end PROJECT
"""

with Parser() as p:
# ALIGNMENT_INT64 requires ASAP2 version 1.60, it is only reported as a warning.
ast = p.tree_from_a2l(a2l_string.encode())
assert len(p.warnings) == 1
assert 'ALIGNMENT_INT64' in p.warnings[0]

# the same content is rejected when the version check is enforced.
try:
p.tree_from_a2l(a2l_string.encode(), enforce_version_check=True)
raise AssertionError('the above call should have raised an A2lError exception')
except A2lError as e:
assert 'ALIGNMENT_INT64' in str(e)
```
13 changes: 9 additions & 4 deletions pya2l/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def parse_args(args):
help='TCP port used to connect to the backend')
parser.add_argument('-oe', dest='output_encoding', type=str, help='encoding of the output file',
default='utf-8')
parser.add_argument('-c', dest='enforce_version_check', action='store_true',
help='reject keywords requiring a newer ASAP2 version than the one declared by the file')

subparsers = parser.add_subparsers(dest='sub_command', help='supported commands')

Expand Down Expand Up @@ -72,14 +74,15 @@ def parse_args(args):
return parser.parse_args(args)


def process_input_file(fp, parser: Parser, allow_partial: bool, encoding: str = None) -> RootNodeType:
def process_input_file(fp, parser: Parser, allow_partial: bool, encoding: str = None,
enforce_version_check: bool = False) -> RootNodeType:
data = fp.read()
if encoding is not None:
data = data.decode(encoding).encode()
if os.path.splitext(fp.name)[1].lower() == '.json':
result = parser.tree_from_json(data, allow_partial=allow_partial)
elif os.path.splitext(fp.name)[1].lower() == '.a2l':
result = parser.tree_from_a2l(data)
result = parser.tree_from_a2l(data, enforce_version_check=enforce_version_check)
else:
raise TypeError(f'unsupported file extension "{os.path.splitext(fp.name)[1]}"')
return result
Expand All @@ -98,7 +101,8 @@ def main(args: typing.List[str] = tuple(sys.argv[1:])):

try:
with Parser(port=args.port, logger=log) as parser:
input_tree = process_input_file(args.input_file, parser, args.allow_partial, args.input_encoding)
input_tree = process_input_file(args.input_file, parser, args.allow_partial, args.input_encoding,
args.enforce_version_check)
if args.sub_command == TO_JSON_CMD:
if args.output_file:
log.info(f'start writing to file {os.path.abspath(args.output_file.name)}')
Expand All @@ -125,7 +129,8 @@ def main(args: typing.List[str] = tuple(sys.argv[1:])):
emit_unpopulated=args.emit_unpopulated).decode())
elif args.sub_command == DIFF_CMD:
lhs_tree = input_tree
rhs_tree = process_input_file(args.right_hand_side, parser, args.allow_partial)
rhs_tree = process_input_file(args.right_hand_side, parser, args.allow_partial,
enforce_version_check=args.enforce_version_check)

lhs_string = parser.json_from_tree(lhs_tree)
rhs_string = parser.json_from_tree(rhs_tree)
Expand Down
18 changes: 18 additions & 0 deletions pya2l/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,21 @@ def test_encoding_support(input_encoding,
assert get_call_args(m, 1) == (output_file_name, 'wb', -1, None, None)

assert get_call_args(m.return_value.write, 0)[0] == expected_output_content


@pytest.mark.parametrize('version_check_arg, returned_value', [
pytest.param([], 0, id='version check disabled'),
pytest.param(['-c'], 1, id='version check enabled')])
@pytest.mark.parametrize('input_file_content', [
"""ASAP2_VERSION 1 50
/begin PROJECT _ ""
/begin MODULE _ ""
/begin MOD_COMMON "" ALIGNMENT_INT64 8
/end MOD_COMMON
/end MODULE
/end PROJECT""".encode()])
def test_version_check_command(version_check_arg, input_file_content, returned_value):
input_file_name = 'input.a2l'
with patch("builtins.open", mock_open(read_data=input_file_content)) as m:
m.return_value.name = input_file_name
assert main([*version_check_arg, input_file_name, 'to_json']) == returned_value
Loading
Loading