Skip to content

355 convert long num arrays #358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1690f2a
first try on lockfile logic
songmeo Jun 6, 2025
980601b
Merge branch 'dev' of github.com:OpenCyphal/pycyphal into 335-create-…
songmeo Jun 6, 2025
aa158ea
fix couple path problems
songmeo Jun 6, 2025
3bd33cb
create lockfile class
songmeo Jun 9, 2025
5be4ba5
remove compiled_dir from nox
songmeo Jun 9, 2025
c22ac68
address comments
songmeo Jun 10, 2025
f4c07c4
add tests
songmeo Jun 11, 2025
adacb45
fix tests
songmeo Jun 11, 2025
3ddafbc
Merge branch '335-create-lockfile' of github.com:OpenCyphal/pycyphal …
songmeo Jun 12, 2025
657d727
address comments
songmeo Jun 12, 2025
ed79fd3
Merge branch '335-create-lockfile' of github.com:OpenCyphal/pycyphal …
songmeo Jun 12, 2025
3fcb454
add context manager
songmeo Jun 14, 2025
3892925
Add a function for removing import hooks (#356)
songmeo Jun 5, 2025
e0423d9
fix couple path problems
songmeo Jun 6, 2025
541e1b1
create lockfile class
songmeo Jun 9, 2025
6ef87ae
remove compiled_dir from nox
songmeo Jun 9, 2025
37fb27f
address comments
songmeo Jun 10, 2025
ee9f06a
add tests
songmeo Jun 11, 2025
2540db3
fix tests
songmeo Jun 11, 2025
106b4ba
address comments
songmeo Jun 12, 2025
5d79b75
add context manager
songmeo Jun 14, 2025
7df9e8c
choose value type based on the list length
songmeo Jun 14, 2025
b55b456
address comments
songmeo Jun 16, 2025
f331a60
remove the improve UX code
songmeo Jun 16, 2025
171bb12
create separate lockfile for support
songmeo Jun 16, 2025
50a414b
remove test related to old UX code
songmeo Jun 16, 2025
64dd205
change to elif
songmeo Jun 16, 2025
22a4dca
Trigger CI rebuild
songmeo Jun 17, 2025
7d978d7
choose value type based on the list length
songmeo Jun 14, 2025
c587a23
change to elif
songmeo Jun 16, 2025
d4a7a45
Trigger CI rebuild
songmeo Jun 17, 2025
d3ef982
Merge branch '355-convert-long-num-arrays' of github.com:OpenCyphal/p…
songmeo Jun 27, 2025
b7a23e0
merge changes from dev
songmeo Jun 30, 2025
e83708b
first tests
songmeo Jul 7, 2025
ad67509
attempt to fix uavcan import error
songmeo Jul 8, 2025
b0109c9
Merge branch 'master' of github.com:OpenCyphal/pycyphal into 355-conv…
songmeo Jul 8, 2025
f0992a3
fix mypy
songmeo Jul 8, 2025
53ce134
Merge branch 'dev' of github.com:OpenCyphal/pycyphal into 355-convert…
songmeo Jul 9, 2025
52c43e9
remove redundant condition
songmeo Jul 9, 2025
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
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions pycyphal/application/register/_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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))
if all(isinstance(x, (float, int, bool)) for x in s):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need either this, or raise an exception in the previous if

Suggested change
if all(isinstance(x, (float, int, bool)) for x in s):
elif all(isinstance(x, (float, int, bool)) for x in s):

return _strictify(Real64(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

Expand Down
68 changes: 38 additions & 30 deletions pycyphal/dsdl/_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import nunavut
import nunavut.lang
import nunavut.jinja

from pycyphal.dsdl._lockfile import Locker

_AnyPath = Union[str, pathlib.Path]

Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down
49 changes: 49 additions & 0 deletions pycyphal/dsdl/_lockfile.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion tests/dsdl/_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
# This software is distributed under the terms of the MIT License.
# Author: Pavel Kirienko <[email protected]>

import random
import sys
import threading
import time
import typing
import logging
import pathlib
import tempfile
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


Expand Down Expand Up @@ -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