diff --git a/noxfile.py b/noxfile.py index 16b9bc64..0807e721 100644 --- a/noxfile.py +++ b/noxfile.py @@ -121,7 +121,7 @@ def test(session): "mypy ~= 1.15.0", "pylint == 3.3.7", ) - session.run("mypy", *map(str, src_dirs), str(compiled_dir)) + session.run("mypy", *map(str, src_dirs)) session.run("pylint", *map(str, src_dirs), env={"PYTHONPATH": str(compiled_dir)}) # Publish coverage statistics. This also has to be run from the test session to access the coverage files. diff --git a/pycyphal/application/register/_value.py b/pycyphal/application/register/_value.py index 36dbf4dc..d5f75e5f 100644 --- a/pycyphal/application/register/_value.py +++ b/pycyphal/application/register/_value.py @@ -350,9 +350,21 @@ def _strictify(s: RelaxedValue) -> Value: if all(isinstance(x, bool) for x in s): return _strictify(Bit(s)) if all(isinstance(x, (int, bool)) for x in s): - return _strictify(Natural64(s)) if all(x >= 0 for x in s) else _strictify(Integer64(s)) - if all(isinstance(x, (float, int, bool)) for x in s): - return _strictify(Real64(s)) + if len(s) <= 32: + return _strictify(Natural64(s)) if all(x >= 0 for x in s) else _strictify(Integer64(s)) + elif len(s) <= 64: + return _strictify(Natural32(s)) if all(x >= 0 for x in s) else _strictify(Integer32(s)) + elif len(s) <= 128: + return _strictify(Natural16(s)) if all(x >= 0 for x in s) else _strictify(Integer16(s)) + elif len(s) <= 256: + return _strictify(Natural8(s)) if all(x >= 0 for x in s) else _strictify(Integer8(s)) + elif all(isinstance(x, (float, int, bool)) for x in s): + if len(s) <= 32: + return _strictify(Real64(s)) + elif len(s) <= 64: + return _strictify(Real32(s)) + elif len(s) <= 128: + return _strictify(Real16(s)) raise ValueConversionError(f"Don't know how to convert {s!r} into {Value}") # pragma: no cover diff --git a/pycyphal/dsdl/_compiler.py b/pycyphal/dsdl/_compiler.py index c2279b8a..1bc199c8 100644 --- a/pycyphal/dsdl/_compiler.py +++ b/pycyphal/dsdl/_compiler.py @@ -15,7 +15,7 @@ import nunavut import nunavut.lang import nunavut.jinja - +from pycyphal.dsdl._lockfile import Locker _AnyPath = Union[str, pathlib.Path] @@ -171,14 +171,27 @@ def compile( # pylint: disable=redefined-builtin (root_namespace_name,) = set(map(lambda x: x.root_namespace, composite_types)) # type: ignore _logger.info("Read %d definitions from root namespace %r", len(composite_types), root_namespace_name) - # Generate code - assert isinstance(output_directory, pathlib.Path) root_ns = nunavut.build_namespace_tree( types=composite_types, root_namespace_dir=str(root_namespace_directory), output_dir=str(output_directory), language_context=language_context, ) + else: + root_ns = nunavut.build_namespace_tree( + types=[], + root_namespace_dir=str(""), + output_dir=str(output_directory), + language_context=language_context, + ) + + lockfile = Locker( + root_namespace_name=root_namespace_name if root_namespace_name else "_support_", + output_directory=output_directory, + ) + if lockfile.create(): + # Generate code + assert isinstance(output_directory, pathlib.Path) code_generator = nunavut.jinja.DSDLCodeGenerator( namespace=root_ns, generate_namespace_types=nunavut.YesNoDefault.YES, @@ -191,36 +204,31 @@ def compile( # pylint: disable=redefined-builtin root_namespace_name, time.monotonic() - started_at, ) - else: - root_ns = nunavut.build_namespace_tree( - types=[], - root_namespace_dir=str(""), - output_dir=str(output_directory), - language_context=language_context, - ) - support_generator = nunavut.jinja.SupportGenerator( - namespace=root_ns, - ) - support_generator.generate_all() + support_generator = nunavut.jinja.SupportGenerator( + namespace=root_ns, + ) + support_generator.generate_all() - # A minor UX improvement; see https://github.com/OpenCyphal/pycyphal/issues/115 - for p in sys.path: - if pathlib.Path(p).resolve() == pathlib.Path(output_directory): - break - else: - if os.name == "nt": - quick_fix = f'Quick fix: `$env:PYTHONPATH += ";{output_directory.resolve()}"`' - elif os.name == "posix": - quick_fix = f'Quick fix: `export PYTHONPATH="{output_directory.resolve()}"`' + # A minor UX improvement; see https://github.com/OpenCyphal/pycyphal/issues/115 + for p in sys.path: + if pathlib.Path(p).resolve() == pathlib.Path(output_directory): + break else: - quick_fix = "Quick fix is not available for this OS." - _logger.info( - "Generated package is stored in %r, which is not in Python module search path list. " - "The package will fail to import unless you add the destination directory to sys.path or PYTHONPATH. %s", - str(output_directory), - quick_fix, - ) + if os.name == "nt": + quick_fix = f'Quick fix: `$env:PYTHONPATH += ";{output_directory.resolve()}"`' + elif os.name == "posix": + quick_fix = f'Quick fix: `export PYTHONPATH="{output_directory.resolve()}"`' + else: + quick_fix = "Quick fix is not available for this OS." + _logger.info( + "Generated package is stored in %r, which is not in Python module search path list. " + "The package will fail to import unless you add the destination directory to sys.path or PYTHONPATH. %s", + str(output_directory), + quick_fix, + ) + + lockfile.remove() return GeneratedPackageInfo( path=pathlib.Path(output_directory) / pathlib.Path(root_namespace_name), diff --git a/pycyphal/dsdl/_lockfile.py b/pycyphal/dsdl/_lockfile.py new file mode 100644 index 00000000..38cc3fc7 --- /dev/null +++ b/pycyphal/dsdl/_lockfile.py @@ -0,0 +1,49 @@ +import logging +import pathlib +import time +from io import TextIOWrapper +from pathlib import Path + +_logger = logging.getLogger(__name__) + + +class Locker: + + def __init__(self, output_directory: pathlib.Path, root_namespace_name: str) -> None: + self._output_directory = output_directory + self._root_namespace_name = root_namespace_name + self._lockfile: TextIOWrapper | None = None + + @property + def _lockfile_path(self) -> Path: + return self._output_directory / f"{self._root_namespace_name}.lock" + + def create(self) -> bool: + """ + True means compilation needs to proceed. + False means another process already compiled the namespace so we just waited for the lockfile to disappear before returning. + """ + # TODO Read about context manager + try: + pathlib.Path(self._output_directory).mkdir(parents=True, exist_ok=True) + self._lockfile = open(self._lockfile_path, "x") + _logger.debug("Created lockfile %s", self._lockfile_path) + return True + except FileExistsError: + pass + while pathlib.Path(self._lockfile_path).exists(): + _logger.debug("Waiting for lockfile %s", self._lockfile_path) + time.sleep(1) + + _logger.debug("Done waiting %s", self._lockfile_path) + + return False + + def remove(self) -> None: + """ + Invoking remove before creating lockfile is not allowed. + """ + assert self._lockfile is not None + self._lockfile.close() + pathlib.Path(self._lockfile_path).unlink() + _logger.debug("Removed lockfile %s", self._lockfile_path) diff --git a/setup.cfg b/setup.cfg index d2c7792d..3d7c22a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -149,6 +149,10 @@ ignore_missing_imports = True ignore_errors = True ignore_missing_imports = True +[mypy-test_dsdl_namespace.*] +ignore_errors = True +ignore_missing_imports = True + [mypy-numpy] ignore_errors = True ignore_missing_imports = True diff --git a/tests/dsdl/_compiler.py b/tests/dsdl/_compiler.py index 26eff3e5..ecc53ec2 100644 --- a/tests/dsdl/_compiler.py +++ b/tests/dsdl/_compiler.py @@ -2,7 +2,10 @@ # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko +import random import sys +import threading +import time import typing import logging import pathlib @@ -10,7 +13,7 @@ import pytest import pycyphal.dsdl from pycyphal.dsdl import remove_import_hooks, add_import_hook - +from pycyphal.dsdl._lockfile import Locker from .conftest import DEMO_DIR @@ -64,3 +67,28 @@ def _unittest_remove_import_hooks() -> None: def _unittest_issue_133() -> None: with pytest.raises(ValueError, match=".*output directory.*"): pycyphal.dsdl.compile(pathlib.Path.cwd() / "irrelevant") + + +def _unittest_lockfile_cant_be_recreated() -> None: + output_directory = pathlib.Path(tempfile.gettempdir()) + root_namespace_name = str(random.getrandbits(64)) + + lockfile1 = Locker(output_directory, root_namespace_name) + lockfile2 = Locker(output_directory, root_namespace_name) + + assert lockfile1.create() is True + + def remove_lockfile1() -> None: + time.sleep(5) + lockfile1.remove() + + threading.Thread(target=remove_lockfile1).start() + assert lockfile2.create() is False + + +def _unittest_lockfile_is_removed() -> None: + output_directory = pathlib.Path(tempfile.gettempdir()) + + pycyphal.dsdl.compile(DEMO_DIR / "public_regulated_data_types" / "uavcan", output_directory=output_directory.name) + + assert pathlib.Path.exists(output_directory / "uavcan.lock") is False