Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2d0eb35
fix: scope Linux dependency build cleanup
drebbe-intrepid Sep 16, 2026
1084535
fix: include serial in device equality
drebbe-intrepid Sep 16, 2026
b3b0101
fix: handle SpyMessage attribute deletion
drebbe-intrepid Sep 16, 2026
f95804a
fix: honor device settings type override
drebbe-intrepid Sep 16, 2026
babf43e
fix: preserve device handle ownership on open
drebbe-intrepid Sep 16, 2026
e56291f
fix: validate transmit message types
drebbe-intrepid Sep 16, 2026
17e08af
fix: release helper and attribute references
drebbe-intrepid Sep 16, 2026
55411f8
fix: correct radio message argument parsing
drebbe-intrepid Sep 16, 2026
13414b7
fix: release synchronous buffer exports
drebbe-intrepid Sep 16, 2026
7b74272
fix: retain registered reflash callbacks
drebbe-intrepid Sep 16, 2026
d213d8d
fix: release script load buffers on every exit
drebbe-intrepid Sep 16, 2026
8aa997a
test: fix x86 settings mock exports
drebbe-intrepid Sep 16, 2026
8aac41b
test: support x86 device handle mocks
drebbe-intrepid Sep 16, 2026
78642dd
test: export mock APIs on 32-bit Windows
drebbe-intrepid Sep 16, 2026
471d658
fix: bound message payload reads by capacity
drebbe-intrepid Sep 16, 2026
9af3375
Merge branches 'codex/fix-248-linux-build-clean', 'codex/fix-246-devi…
drebbe-intrepid Sep 16, 2026
70a7c6a
fix(build): avoid locking existing extension
drebbe-intrepid Sep 16, 2026
b7fb3d0
Merge branch 'codex/fix-243-reference-leaks' into codex/integration-2…
drebbe-intrepid Sep 16, 2026
6e71040
test: support x86 buffer export mocks
drebbe-intrepid Sep 16, 2026
6982e62
fix: integrate device handle ownership
drebbe-intrepid Sep 16, 2026
d0ea01c
Merge branches 'codex/fix-242-buffer-exports' and 'codex/fix-249-inpl…
drebbe-intrepid Sep 16, 2026
12bca98
test: match script mock to Windows architecture
drebbe-intrepid Sep 16, 2026
911ad6c
Merge branch 'codex/fix-247-script-buffer' into codex/integration-237…
drebbe-intrepid Sep 16, 2026
f0a25f9
test: compare capsule ownership portably
drebbe-intrepid Sep 16, 2026
ea2015c
Merge branch 'codex/fix-243-reference-leaks' into codex/integration-2…
drebbe-intrepid Sep 16, 2026
1d4a2c0
fix: exclude subclass callbacks from rollback
drebbe-intrepid Sep 16, 2026
270875c
fix: integrate payload bounds and ownership
drebbe-intrepid Sep 16, 2026
dac8582
Merge #243 reference ownership dependency
drebbe-intrepid Sep 16, 2026
c8f6977
Merge branch 'codex/fix-245-device-reopen' into codex/integration-237…
drebbe-intrepid Sep 16, 2026
7f5bdf0
fix: integrate reviewed message dependencies
drebbe-intrepid Sep 16, 2026
24fa8ec
Merge branch 'codex/fix-240-payload-capacity' into codex/integration-…
drebbe-intrepid Sep 16, 2026
599f264
test: isolate transmit input ownership checks
drebbe-intrepid Sep 16, 2026
c829a75
Merge branch 'codex/fix-239-transmit-types' into codex/fix-240-payloa…
drebbe-intrepid Sep 16, 2026
54dce86
Merge branch 'codex/fix-240-payload-capacity' into codex/integration-…
drebbe-intrepid Sep 16, 2026
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
11 changes: 10 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ jobs:
run: |
uv sync --group test

- name: Rebuild extension in place twice (Windows)
if: runner.os == 'Windows'
run: |
uv pip install setuptools wheel
uv run --no-sync python setup.py build_ext --inplace --force
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv run --no-sync python setup.py build_ext --inplace --force
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Run tests
run: |
uv run pytest tests/ --verbose
uv run pytest tests/ --verbose
9 changes: 6 additions & 3 deletions build_libicsneo.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

# icspb bootstraps protobuf from source at configure time. Keep the
# bootstrap OUTSIDE the libicsneo build dir so it survives the per-python
# `git clean` in _build_libicsneo_linux and is reused across all
# build-directory cleanup in _build_libicsneo_linux and is reused across all
# cibuildwheel builds in a job (it self-partitions by <system>-<processor>,
# so sharing one root across archs is safe). Building protobuf once per
# arch instead of once per python matters most under QEMU aarch64.
Expand Down Expand Up @@ -125,8 +125,11 @@ def _cmake_ninja_args():

def _build_libicsneo_linux():
print("Cleaning libicsneo...")
subprocess.check_output(["git", "clean", "-xdf"], cwd="libicsneo")
subprocess.check_output(["mkdir", "-p", "libicsneo/build"])
# Source archives have no parent Git checkout. Reset only the CMake
# build tree, preserving the source checkout and protobuf bootstrap.
if os.path.exists(LIBICSNEO_BUILD):
shutil.rmtree(LIBICSNEO_BUILD)
os.makedirs(LIBICSNEO_BUILD, exist_ok=True)

print("cmake libicsneo...")
subprocess.check_output(
Expand Down
45 changes: 30 additions & 15 deletions generate_icsneo40_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,24 +737,39 @@ def generate(filename="include/ics/icsnVC40.h"):
f.write(f' "ics.structures.{fname}",\n')
f.write("]\n\n")

# Verify We can at least import all of the modules - quick check to make sure parser worked.
ics_module_path = GEN_ICS_DIR.parent.resolve()
# Add the module to the eval sys.path.
eval("""sys.path.insert(0, f"{ics_module_path}")""")
for file_name in file_names:
if file_name.startswith("__"):
continue
import_line = "from ics.structures import {}".format(
re.sub(r'(\.py)', '', file_name))
try:
print(f"Importing / Verifying {output_dir / file_name}...{' '*20}", end="\r")
exec(import_line)
except Exception as ex:
print(f"""\nERROR: {ex} IMPORT LINE: '{import_line}'""")
raise ex
validate_generated_modules(file_names)
print("\nDone.")


def validate_generated_modules(file_names):
"""Check imports without keeping an existing native extension loaded in setup."""
import json

# Importing ics also loads ics.ics when it exists. Windows cannot replace a
# loaded .pyd, so wait for a separate interpreter to exit before building.
# Pass paths and names as data, including paths containing spaces/backslashes.
validation_code = """
import importlib
import json
import sys

package_path, file_names = json.load(sys.stdin)
sys.path.insert(0, package_path)
for file_name in file_names:
if file_name.startswith("__"):
continue
module_name = "ics.structures." + file_name.removesuffix(".py")
print(f"Importing / Verifying {module_name}...", flush=True)
importlib.import_module(module_name)
"""
run(
[sys.executable, "-c", validation_code],
input=json.dumps([str(GEN_ICS_DIR.parent.resolve()), file_names]),
text=True,
check=True,
)


def _write_c_object(f, c_object):
# Write the header
if c_object.data_type == DataType.Struct:
Expand Down
5 changes: 4 additions & 1 deletion include/methods.h
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,10 @@ extern "C"
MODULE_NAME \
".set_reflash_callback(callback)\n" \
"\n" \
"Sets the reflash display callback.\n" \
"Sets the reflash display callback. The callback is retained until replaced or disabled.\n" \
"An object with a callable reflash_callback method is also accepted.\n" \
"Pass None to disable callbacks, or omit the argument to print progress to stdout.\n" \
"Callback exceptions are reported through sys.unraisablehook.\n" \
"\n" \
"Args:\n" \
"\tcallback (:class:`function`): Must be a callable Python function (`def callback(msg, progress)`)\n\n" \
Expand Down
6 changes: 6 additions & 0 deletions include/object_spy_message.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ typedef struct
{
PyObject_HEAD icsSpyMessage msg;
bool noExtraDataPtrCleanup;
// Private bound, independent of writable protocol/length/ownership fields.
size_t extraDataCapacity;
} spy_message_object;
#pragma pack(pop)

Expand All @@ -47,6 +49,7 @@ typedef struct
{
PyObject_HEAD icsSpyMessageJ1850 msg;
bool noExtraDataPtrCleanup;
size_t extraDataCapacity;
} spy_message_j1850_object;
#pragma pack(pop)

Expand All @@ -63,5 +66,8 @@ extern PyTypeObject spy_message_j1850_object_type;
#define PySpyMessageJ1850_GetObject(obj) ((spy_message_j1850_object*)obj)

bool setup_spy_message_object(PyObject* module);
size_t spy_message_extra_data_length(const icsSpyMessage& msg);
bool spy_message_validate_extra_data(const spy_message_object* obj);
void spy_message_record_received_extra_data(PyObject* obj);

#endif // _OBJECT_SPY_MESSAGE_H_
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ Issues = "https://github.com/intrepidcs/python_ics/issues"
[dependency-groups]
test = [
"pytest>=8.4.2",
"dunamai",
"setuptools",
]

[tool.setuptools.dynamic]
version = { attr = "ics.__version__" }
version = { attr = "ics.__version.__version__" }

[build-system]
requires = [
Expand Down
5 changes: 4 additions & 1 deletion src/ics/py_neo_device_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ def __repr__(self):
return f"<ics.{self.__class__.__name__} {self.Name} {self.serial_number}>"

def __eq__(self, other) -> bool:
if not isinstance(other, PyNeoDeviceEx):
return NotImplemented
return \
self.DeviceType == other.DeviceType and \
self.SerialNumber == other.SerialNumber and \
self.Handle == other.Handle and \
self.NumberOfClients == other.NumberOfClients and \
self.MaxAllowedClients == other.MaxAllowedClients and \
Expand Down Expand Up @@ -514,4 +517,4 @@ def request_set_neovi_miscio(self, *args, **kwargs):

def get_firmware_variant(self, *args, **kwargs):
"see ics.get_firmware_variant for details on arguments."
return ics.get_firmware_variant(self, *args, **kwargs)
return ics.get_firmware_variant(self, *args, **kwargs)
Loading
Loading