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

Draft
wants to merge 14 commits into
base: dev
Choose a base branch
from
Draft
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
18 changes: 15 additions & 3 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 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

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