From a86fe15f5e35fb0ff86067b8985873eec158a295 Mon Sep 17 00:00:00 2001 From: lmbsog0 Date: Fri, 31 Jul 2026 11:50:48 +0200 Subject: [PATCH 1/4] update backend to 0.2.0 --- .github/workflows/build.yml | 4 +- README.md | 39 ++++++++++++++++++ pya2l/cli.py | 13 ++++-- pya2l/cli_test.py | 18 +++++++++ pya2l/parser.py | 43 +++++++++++++++----- pya2l/parser_test.py | 81 ++++++++++++++++++++++++++++++++++++- setup.cfg | 2 +- 7 files changed, 180 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6265285..456f22b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Python package on: [ push, pull_request ] env: - A2L_GRPC_VERSION: v0.1.21 + A2L_GRPC_VERSION: v0.2.0 jobs: generate-grpc-sources: @@ -55,7 +55,7 @@ jobs: pytest -n 1 --cov-report html --cov pya2l --verbose codecov run-pytest-test-windows: - runs-on: windows-2019 + runs-on: windows-2022 needs: - generate-grpc-sources strategy: diff --git a/README.md b/README.md index 4953c85..5bafc34 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ Once the package installed, the `pya2l` command will be available. It provides s - Convert a JSON-formatted A2L file to JSON with `pya2l -v .json to_json -o -i 2` - Convert a JSON-formatted A2L file to A2L with `pya2l -v .json to_a2l -o -i 2` +Adding the `-c` option to any of the above commands rejects files using keywords which require a newer ASAP2 version +than the one declared by the file itself. Without this option, such keywords are reported as warnings (visible 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. @@ -100,3 +104,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 of 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) +``` diff --git a/pya2l/cli.py b/pya2l/cli.py index 4bd5156..b0d2a89 100644 --- a/pya2l/cli.py +++ b/pya2l/cli.py @@ -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') @@ -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 @@ -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)}') @@ -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) diff --git a/pya2l/cli_test.py b/pya2l/cli_test.py index 2aed173..8b7918b 100644 --- a/pya2l/cli_test.py +++ b/pya2l/cli_test.py @@ -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 diff --git a/pya2l/parser.py b/pya2l/parser.py index 95d12d3..537f55e 100644 --- a/pya2l/parser.py +++ b/pya2l/parser.py @@ -20,6 +20,11 @@ protocol_size_margin = 256 + +class A2lError(Exception): + """raised when the backend reports an error while converting a document.""" + + def chunk_generator(_data: bytes, _chunk_size: int): """ Generates chunks of the given size from the input data. @@ -52,11 +57,12 @@ class A2lParser(object): def __init__(self, port=3333, max_msg_size=4*1024*1024, logger: logging.Logger = None): self._logger = logger self.chunk_size = max_msg_size - protocol_size_margin + self.warnings: typing.List[str] = [] if os.name == 'nt': shared_object = f'a2l_grpc_windows_{get_architecture()}.dll' elif os.name == 'posix': if sys.platform == 'darwin': - shared_object = 'a2l_grpc_darwin_arm64.dylib' + shared_object = f'a2l_grpc_darwin_{get_architecture()}.dylib' else: shared_object = f'a2l_grpc_linux_{get_architecture()}.so' else: @@ -90,6 +96,12 @@ def _request_generator(self, kwargs[key] = chunk yield request(**kwargs) + def _add_warnings(self, warnings: typing.Iterable[str]) -> None: + for warning in warnings: + self.warnings.append(warning) + if self._logger: + self._logger.warning(warning) + def __enter__(self): return self @@ -112,19 +124,25 @@ def get_if_data_by_name_and_index(node, name: str, index: typing.Union[int, None index in range(len(if_data_node.Blob))] return None - def tree_from_a2l(self, a2l_data: bytes) -> RootNodeType: + def tree_from_a2l(self, a2l_data: bytes, enforce_version_check: bool = False) -> RootNodeType: """ Converts an A2L into gRPC object. :param a2l_data: the A2L data to deserialize + :param enforce_version_check: rejects keywords requiring a newer ASAP2 version than the one declared by the + file with ASAP2_VERSION. When disabled, such keywords are reported in the warnings property :return: a gRPC object """ if self._logger: self._logger.info('start parsing A2L') + self.warnings = [] response_tree_data = bytearray() - for response in self._client.GetTreeFromA2L(self._request_generator(TreeFromA2LRequest, a2l_data)): - if response.error and self._logger: - self._logger.error(response.error) + for response in self._client.GetTreeFromA2L( + self._request_generator(TreeFromA2LRequest, a2l_data, enforce_version_check=enforce_version_check)): + if response.HasField('error'): + raise A2lError(response.error) + + self._add_warnings(response.warnings) if response.serializedTreeChunk: response_tree_data.extend(response.serializedTreeChunk) @@ -150,11 +168,14 @@ def tree_from_json(self, json_data: bytes, allow_partial: bool = False) -> RootN if self._logger: self._logger.info('start parsing JSON A2L') + self.warnings = [] response_tree_data = bytearray() for response in self._client.GetTreeFromJSON(self._request_generator(TreeFromJSONRequest, json_data, allow_partial=allow_partial)): - if response.error and self._logger: - self._logger.error(response.error) + if response.HasField('error'): + raise A2lError(response.error) + + self._add_warnings(response.warnings) if response.serializedTreeChunk: response_tree_data.extend(response.serializedTreeChunk) @@ -188,8 +209,8 @@ def json_from_tree(self, json_data = bytearray() for response in self._client.GetJSONFromTree(self._request_generator(JSONFromTreeRequest, tree_bytes, indent=indent, allow_partial=allow_partial, emit_unpopulated=emit_unpopulated)): - if response.error and self._logger: - self._logger.error(response.error) + if response.HasField('error'): + raise A2lError(response.error) if response.json: json_data.extend(response.json) @@ -214,8 +235,8 @@ def a2l_from_tree(self, tree: RootNodeType, sorted: bool = False, indent: int = a2l_data = bytearray() for response in self._client.GetA2LFromTree(self._request_generator(A2LFromTreeRequest, tree_bytes, sorted=sorted, indent=indent)): - if response.error and self._logger: - self._logger.error(response.error) + if response.HasField('error'): + raise A2lError(response.error) if response.a2l: a2l_data.extend(response.a2l) diff --git a/pya2l/parser_test.py b/pya2l/parser_test.py index 6905f62..ac96586 100644 --- a/pya2l/parser_test.py +++ b/pya2l/parser_test.py @@ -7,7 +7,7 @@ import pytest -from .parser import A2lParser as Parser +from .parser import A2lError, A2lParser as Parser def is_iterable(e): @@ -339,7 +339,9 @@ def module_string(request): ] project_strings = [ - pytest.param('/begin PROJECT {ident} {string} {header} {module} /end PROJECT', id='PROJECT HEADER MODULE'), + pytest.param('/begin PROJECT {ident} {string} {header} {module} /end PROJECT', id='PROJECT HEADER MODULE') +] +project_invalid_strings = [ pytest.param('/begin PROJECT {ident} {string} {module} {header} /end PROJECT', id='PROJECT MODULE HEADER'), pytest.param('/begin PROJECT {ident} {string} {module} {header} {module} /end PROJECT', id='PROJECT MODULE HEADER MODULE') ] @@ -2606,6 +2608,18 @@ def test_project(s, ident_string, ident_value, string_string, string_value, head assert ast.PROJECT.LongIdentifier.Value == string_value +@pytest.mark.parametrize('s', project_invalid_strings) +@pytest.mark.parametrize('header_string', [pytest.param(1, id='one HEADER')], indirect=True) +@pytest.mark.parametrize('module_string', [pytest.param(1, id='one MODULE')], indirect=True) +def test_project_header_after_module(s, header_string, module_string): + with Parser() as p: + with pytest.raises(A2lError): + p.tree_from_a2l(s.format(ident='name', + string='""', + header=header_string, + module=module_string).encode()) + + @pytest.mark.parametrize('project', [pytest.param(['HEADER', 'PROJECT_NO'], id='HEADER')], indirect=True) @pytest.mark.parametrize('e', [pytest.param('PROJECT_NO {}')]) @pytest.mark.parametrize('s, v', idents) @@ -3338,6 +3352,69 @@ def test_system_constant(module, e, s, v): assert system_constant.Value.Value == v +version_check_a2l_string = """ + ASAP2_VERSION 1 50 + /begin PROJECT project_name "project long identifier" + /begin MODULE module_name "module long identifier" + /begin MOD_COMMON "mod common long identifier" + ALIGNMENT_INT64 8 + /end MOD_COMMON + /end MODULE + /end PROJECT""" + + +def test_version_check_disabled(): + with Parser() as p: + ast = p.tree_from_a2l(version_check_a2l_string.encode()) + assert ast.PROJECT.MODULE[0].MOD_COMMON.ALIGNMENT_INT64.AlignmentBorder.Value == 8 + assert len(p.warnings) == 1 + assert 'ALIGNMENT_INT64' in p.warnings[0] + + +def test_version_check_enabled(): + with Parser() as p: + with pytest.raises(A2lError, match='ALIGNMENT_INT64'): + p.tree_from_a2l(version_check_a2l_string.encode(), enforce_version_check=True) + + +def test_invalid_a2l_string(): + with Parser() as p: + with pytest.raises(A2lError): + p.tree_from_a2l(b'this is not an a2l file') + assert p.warnings == [] + + +def test_invalid_json_string(): + with Parser() as p: + with pytest.raises(A2lError): + p.tree_from_json(b'this is not a json file') + + +def test_float_value_is_dumped_as_defined(): + a2l_string = """ + /begin PROJECT project_name "project long identifier" + /begin MODULE module_name "module long identifier" + /begin CHARACTERISTIC + characteristic_name + "characteristic long identifier" + VALUE + 0 + DAMOS_SST + 0 + characteristic_conversion + -4.50 + 1.2E+3 + /end CHARACTERISTIC + /end MODULE + /end PROJECT""" + with Parser() as p: + characteristic = p.tree_from_a2l(a2l_string.encode()).PROJECT.MODULE[0].CHARACTERISTIC[0] + assert characteristic.LowerLimit.Value == -4.5 + assert characteristic.LowerLimit.Source == '-4.50' + assert characteristic.UpperLimit.Value == 1200.0 + assert characteristic.UpperLimit.Source == '1.2E+3' + + def test_get_properties(): a2l_string = """ /begin PROJECT project_name "project long identifier" diff --git a/setup.cfg b/setup.cfg index b4237dc..adebb1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pya2l -version = 0.1.10 +version = 0.2.0 author = Guillaume Sottas author_email = guillaumesottas@gmail.com description = utility for a2l files From 1cf0be8f43810f4496b76f6def0f124bcd6fb321 Mon Sep 17 00:00:00 2001 From: lmbsog0 Date: Fri, 31 Jul 2026 16:31:59 +0200 Subject: [PATCH 2/4] add cross-platform/OS test execution --- .github/workflows/build.yml | 78 ++++++++++++++++++++----------------- README.md | 56 ++++++++++++++++++-------- pya2l/parser.py | 3 +- pya2l/parser_test.py | 25 +++++++++++- 4 files changed, 108 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 456f22b..6c9fd11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Python package on: [ push, pull_request ] env: - A2L_GRPC_VERSION: v0.2.0 + A2L_GRPC_VERSION: v0.2.1 jobs: generate-grpc-sources: @@ -26,52 +26,43 @@ jobs: with: name: protobuf path: pya2l/protobuf - run-pytest-test-linux: - runs-on: ubuntu-22.04 - 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 - # 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 - 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-2022 + 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 with: python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.target.architecture }} - 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" + # 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: @@ -84,8 +75,7 @@ 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 - name: Install dependencies @@ -122,16 +112,32 @@ jobs: 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 with: python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.target.architecture }} - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.md b/README.md index 5bafc34..28556e0 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -7,38 +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 .a2l to_json -o -i 2` -- Convert an A2L file to A2L with `pya2l -v to_a2l -o -i 2` -- Convert a JSON-formatted A2L file to JSON with `pya2l -v .json to_json -o -i 2` -- Convert a JSON-formatted A2L file to A2L with `pya2l -v .json to_a2l -o -i 2` +- Convert an A2L file to JSON with `pya2l -v .a2l to_json -o .json -i 2` +- Convert an A2L file to A2L with `pya2l -v .a2l to_a2l -o .a2l -i 2` +- Convert a JSON-formatted A2L file to JSON with `pya2l -v .json to_json -o .json -i 2` +- Convert a JSON-formatted A2L file to A2L with `pya2l -v .json to_a2l -o .a2l -i 2` -Adding the `-c` option to any of the above commands rejects files using keywords which require a newer ASAP2 version -than the one declared by the file itself. Without this option, such keywords are reported as warnings (visible with the -`-v` option). +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 @@ -107,11 +131,11 @@ 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 of the +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. +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 diff --git a/pya2l/parser.py b/pya2l/parser.py index 537f55e..bf2e079 100644 --- a/pya2l/parser.py +++ b/pya2l/parser.py @@ -45,7 +45,8 @@ def get_architecture() -> str: return 'amd64' else: raise RuntimeError('Unsupported architecture') - elif machine == 'aarch64': + elif machine in ('aarch64', 'arm64'): + # 'aarch64' is reported on Linux, 'arm64' on macOS and Windows return 'arm64' elif machine.startswith('arm'): return 'arm' diff --git a/pya2l/parser_test.py b/pya2l/parser_test.py index ac96586..1406fb7 100644 --- a/pya2l/parser_test.py +++ b/pya2l/parser_test.py @@ -5,9 +5,11 @@ @date: 06.04.2018 """ +from unittest.mock import patch + import pytest -from .parser import A2lError, A2lParser as Parser +from .parser import A2lError, A2lParser as Parser, get_architecture def is_iterable(e): @@ -3415,6 +3417,27 @@ def test_float_value_is_dumped_as_defined(): assert characteristic.UpperLimit.Source == '1.2E+3' +@pytest.mark.parametrize('machine, pointer_size, architecture', [ + # note that on windows, platform.machine() reports the architecture of the machine, not the one of the process, + # which is why the pointer size is used to detect a 32 bits process running on a 64 bits machine. + pytest.param('AMD64', 8, 'amd64', id='windows amd64'), + pytest.param('AMD64', 4, '386', id='windows 386'), + pytest.param('ARM64', 8, 'arm64', id='windows arm64'), + pytest.param('x86_64', 8, 'amd64', id='linux/darwin amd64'), + pytest.param('aarch64', 8, 'arm64', id='linux arm64'), + pytest.param('armv7l', 4, 'arm', id='linux arm'), + pytest.param('arm64', 8, 'arm64', id='darwin arm64')]) +def test_get_architecture(machine, pointer_size, architecture): + with patch('platform.machine', return_value=machine), patch('struct.calcsize', return_value=pointer_size): + assert get_architecture() == architecture + + +def test_get_architecture_unsupported(): + with patch('platform.machine', return_value='unsupported'): + with pytest.raises(RuntimeError): + get_architecture() + + def test_get_properties(): a2l_string = """ /begin PROJECT project_name "project long identifier" From 0886fe542655bd8c79f62e1b0cfb585b430e82a4 Mon Sep 17 00:00:00 2001 From: lmbsog0 Date: Fri, 31 Jul 2026 16:40:22 +0200 Subject: [PATCH 3/4] fix protobuf version issue with Python 3.9 --- .github/workflows/build.yml | 5 ++++- setup.cfg | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c9fd11..5d231ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,10 @@ jobs: - 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. + 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 diff --git a/setup.cfg b/setup.cfg index adebb1d..675435d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ python_requires = >= 3.9 install_requires = grpcio>=1.71.0 grpcio-tools>=1.71.0 + protobuf>=6.31.1 dictdiffer packages = pya2l From 2c4ffe4eeb45949f8507fb4a5a6b79d14d104c54 Mon Sep 17 00:00:00 2001 From: lmbsog0 Date: Fri, 31 Jul 2026 16:47:22 +0200 Subject: [PATCH 4/4] update Action versions to remove deprecation warnings in CI --- .github/workflows/build.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d231ab..f44697f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,23 +9,24 @@ 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 # 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 @@ -55,8 +56,8 @@ jobs: - { 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 }} @@ -67,7 +68,7 @@ jobs: 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 + - uses: actions/download-artifact@v8 with: name: protobuf path: pya2l/protobuf @@ -80,20 +81,20 @@ jobs: needs: - 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 @@ -107,7 +108,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: dist path: dist @@ -137,7 +138,7 @@ jobs: - { 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 }}