[bazel][robotpy] Add mirror for robotpy's wpiuil and wpinet libraries (#8062)

Project import generated by Copybara.

GitOrigin-RevId: 92ea93d1b47a82667044bd0af05f7fdb34d2c2c2
This commit is contained in:
PJ Reiniger
2025-08-30 14:55:11 -04:00
committed by GitHub
parent 96004f9bb5
commit bd1dcc4358
96 changed files with 7271 additions and 64 deletions

View File

@@ -35,6 +35,7 @@ build:build_java --test_tag_filters=allwpilib-build-java --build_tag_filters=all
build:build_cpp --test_tag_filters=+allwpilib-build-cpp --build_tag_filters=+allwpilib-build-cpp
build:no_example --test_tag_filters=-wpi-example --build_tag_filters=-wpi-example
test:no_example --test_tag_filters=-wpi-example --build_tag_filters=-wpi-example
common:skip_robotpy --test_tag_filters=-robotpy --build_tag_filters=-robotpy
# Build Buddy Cache Setup
build:build_buddy --bes_results_url=https://app.buildbuddy.io/invocation/

2
.gitattributes vendored
View File

@@ -28,3 +28,5 @@
# Generated files
*/src/generated/** linguist-generated
*/robotpy_native_build_info.bzl linguist-generated
*/robotpy_pybind_build_info.bzl linguist-generated

View File

@@ -23,6 +23,7 @@ compile_pip_requirements(
extra_args = ["--allow-unsafe"],
requirements_in = "requirements.txt",
requirements_txt = "requirements_lock.txt",
requirements_windows = "//:requirements_windows_lock.txt",
# compile_pip_requirements does not respect target_compatible_with for some of the targets it generates under the hood
tags = ["no-systemcore"],
)

13
README-RobotPy.md Normal file
View File

@@ -0,0 +1,13 @@
# robotpy in allwpilb
allwpilib hosts a mirror of RobotPy that can be built with bazel on Linux. The intent of the mirror is to have breaking changes identified early and fixed by the PR creator so that when wpilib releases are made there is much less work required to release a RobotPy version that wraps it. It is not a goal for allwpilib to replace the RobotPy repo; it will still be considered the "source of truth" for python builds and will be responsible for building against all of the applicable architectures and multiple versions of python.
## Build Process
The upstream RobotPy repository uses toml configuration files and semiwrap to produce Meson build scripts. The allwpilib fork uses these toml configuration files to auto generate bazel build scripts. In general, each project (wpiutil, wpimath, etc) defines two pybind extensions; one that simply wraps the native library, and another that adds extension(s) that and contains all of the python files for the library. Both of these subprojects have auto-generated build files; a `robotpy_native_build_info.bzl` for the lidar wraper and `robotpy_pybind_build_info.bzl` which defines the extensions and python library.
## Disabling robotpy builds
Building the robotpy software on top of the standard C++/Java software can result in more than doubling the amount of time it takes to compile. To skip building the robotpy tooling you can add `--config=skip_robotpy` to the command line or to your `user.bazelrc`
# Syncing with robotpy
NOTE: This process is currently unlanded while robotpy gets the 2027 branch stable
[Copybara](https://github.com/google/copybara) is used to maintin synchronization between the upstream robotpy repositories and the allwpilib mirror. Github actions can be manually run which will create pull requests that will update all of the robotpy files between the two repositories. The ideal process is that the allwpilib mirror is always building in CI, and once a release is created the RobotPy team can run the `wpilib -> robotpy` copybara task, make any fine tuned adjustements and create their release. In the event that additional changes are made on the robotpy side, they can run the `robotpy -> wpilib` task to push the updates back to the mirror. However the goal of the mirroring the software here is to be able to more rapidly test changes and will hopefully overwhelmingly eliminate the need for syncs this direction.

View File

@@ -79,6 +79,27 @@ http_archive(
url = "https://github.com/wpilibsuite/rules_bzlmodrio_toolchains/releases/download/2025-1.bcr6/rules_bzlmodrio_toolchains-2025-1.bcr6.tar.gz",
)
http_archive(
name = "pybind11_bazel",
integrity = "sha256-iwRj1wuX2pDS6t6DqiCfhIXisv4y+7CvxSJtZoSAzGw=",
strip_prefix = "pybind11_bazel-2b6082a4d9d163a52299718113fa41e4b7978db5",
urls = ["https://github.com/pybind/pybind11_bazel/archive/2b6082a4d9d163a52299718113fa41e4b7978db5.tar.gz"],
)
http_archive(
name = "pybind11",
build_file = "@pybind11_bazel//:pybind11-BUILD.bazel",
strip_prefix = "pybind11-dfe7e65b4527eeb11036402aac3a394130960bb2",
urls = ["https://github.com/pybind/pybind11/archive/dfe7e65b4527eeb11036402aac3a394130960bb2.zip"],
)
http_archive(
name = "rules_python_pytest",
sha256 = "e2556404ef56ea3ec938597616afc51d78e1832cfe511b196e9f2b8fd7f8f149",
strip_prefix = "rules_python_pytest-1.1.1",
url = "https://github.com/caseyduquettesc/rules_python_pytest/releases/download/v1.1.1/rules_python_pytest-v1.1.1.tar.gz",
)
http_archive(
name = "bazel_skylib",
sha256 = "51b5105a760b353773f904d2bbc5e664d0987fbaf22265164de65d43e910d8ac",
@@ -138,6 +159,7 @@ pip_parse(
name = "allwpilib_pip_deps",
python_interpreter_target = "@python_3_10_host//:python",
requirements_lock = "//:requirements_lock.txt",
requirements_windows = "//:requirements_windows_lock.txt",
)
load("@allwpilib_pip_deps//:requirements.bzl", "install_deps")
@@ -371,6 +393,10 @@ load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
rules_pkg_dependencies()
load("@rules_python_pytest//python_pytest:repositories.bzl", "rules_python_pytest_dependencies")
rules_python_pytest_dependencies()
# Capture the repository environmental variables which specify the filter list for what architectures to build in CI.
load("//shared/bazel/rules:publishing_rule.bzl", "publishing_repo")

View File

@@ -1,3 +1,7 @@
jinja2==3.0.0a1
jinja2==3.1.6
protobuf==5.28.3
grpcio-tools==1.68.0
semiwrap==0.1.8
pytest
numpy
opencv-python~=4.6

View File

@@ -4,62 +4,70 @@
#
# bazel run //:requirements.update
#
grpcio==1.68.0 \
--hash=sha256:0d230852ba97654453d290e98d6aa61cb48fa5fafb474fb4c4298d8721809354 \
--hash=sha256:0efbbd849867e0e569af09e165363ade75cf84f5229b2698d53cf22c7a4f9e21 \
--hash=sha256:14331e5c27ed3545360464a139ed279aa09db088f6e9502e95ad4bfa852bb116 \
--hash=sha256:15327ab81131ef9b94cb9f45b5bd98803a179c7c61205c8c0ac9aff9d6c4e82a \
--hash=sha256:15377bce516b1c861c35e18eaa1c280692bf563264836cece693c0f169b48829 \
--hash=sha256:15fa1fe25d365a13bc6d52fcac0e3ee1f9baebdde2c9b3b2425f8a4979fccea1 \
--hash=sha256:18668e36e7f4045820f069997834e94e8275910b1f03e078a6020bd464cb2363 \
--hash=sha256:2af76ab7c427aaa26aa9187c3e3c42f38d3771f91a20f99657d992afada2294a \
--hash=sha256:2bddd04a790b69f7a7385f6a112f46ea0b34c4746f361ebafe9ca0be567c78e9 \
--hash=sha256:32a9cb4686eb2e89d97022ecb9e1606d132f85c444354c17a7dbde4a455e4a3b \
--hash=sha256:3ac7f10850fd0487fcce169c3c55509101c3bde2a3b454869639df2176b60a03 \
--hash=sha256:3b2b559beb2d433129441783e5f42e3be40a9e1a89ec906efabf26591c5cd415 \
--hash=sha256:4028b8e9a3bff6f377698587d642e24bd221810c06579a18420a17688e421af7 \
--hash=sha256:44bcbebb24363d587472089b89e2ea0ab2e2b4df0e4856ba4c0b087c82412121 \
--hash=sha256:46a2d74d4dd8993151c6cd585594c082abe74112c8e4175ddda4106f2ceb022f \
--hash=sha256:4df81d78fd1646bf94ced4fb4cd0a7fe2e91608089c522ef17bc7db26e64effd \
--hash=sha256:4e300e6978df0b65cc2d100c54e097c10dfc7018b9bd890bbbf08022d47f766d \
--hash=sha256:4f1931c7aa85be0fa6cea6af388e576f3bf6baee9e5d481c586980c774debcb4 \
--hash=sha256:50992f214264e207e07222703c17d9cfdcc2c46ed5a1ea86843d440148ebbe10 \
--hash=sha256:55d3b52fd41ec5772a953612db4e70ae741a6d6ed640c4c89a64f017a1ac02b5 \
--hash=sha256:5a180328e92b9a0050958ced34dddcb86fec5a8b332f5a229e353dafc16cd332 \
--hash=sha256:619b5d0f29f4f5351440e9343224c3e19912c21aeda44e0c49d0d147a8d01544 \
--hash=sha256:6b2f98165ea2790ea159393a2246b56f580d24d7da0d0342c18a085299c40a75 \
--hash=sha256:6f9c7ad1a23e1047f827385f4713b5b8c6c7d325705be1dd3e31fb00dcb2f665 \
--hash=sha256:79f81b7fbfb136247b70465bd836fa1733043fdee539cd6031cb499e9608a110 \
--hash=sha256:7e0a3e72c0e9a1acab77bef14a73a416630b7fd2cbd893c0a873edc47c42c8cd \
--hash=sha256:7e7483d39b4a4fddb9906671e9ea21aaad4f031cdfc349fec76bdfa1e404543a \
--hash=sha256:88fb2925789cfe6daa20900260ef0a1d0a61283dfb2d2fffe6194396a354c618 \
--hash=sha256:8af6137cc4ae8e421690d276e7627cfc726d4293f6607acf9ea7260bd8fc3d7d \
--hash=sha256:8b0ff09c81e3aded7a183bc6473639b46b6caa9c1901d6f5e2cba24b95e59e30 \
--hash=sha256:8c73f9fbbaee1a132487e31585aa83987ddf626426d703ebcb9a528cf231c9b1 \
--hash=sha256:99f06232b5c9138593ae6f2e355054318717d32a9c09cdc5a2885540835067a1 \
--hash=sha256:9fe1b141cda52f2ca73e17d2d3c6a9f3f3a0c255c216b50ce616e9dca7e3441d \
--hash=sha256:a17278d977746472698460c63abf333e1d806bd41f2224f90dbe9460101c9796 \
--hash=sha256:a59f5822f9459bed098ffbceb2713abbf7c6fd13f2b9243461da5c338d0cd6c3 \
--hash=sha256:a6213d2f7a22c3c30a479fb5e249b6b7e648e17f364598ff64d08a5136fe488b \
--hash=sha256:a831dcc343440969aaa812004685ed322cdb526cd197112d0db303b0da1e8659 \
--hash=sha256:afbf45a62ba85a720491bfe9b2642f8761ff348006f5ef67e4622621f116b04a \
--hash=sha256:b0cf343c6f4f6aa44863e13ec9ddfe299e0be68f87d68e777328bff785897b05 \
--hash=sha256:c03d89df516128febc5a7e760d675b478ba25802447624edf7aa13b1e7b11e2a \
--hash=sha256:c1245651f3c9ea92a2db4f95d37b7597db6b246d5892bca6ee8c0e90d76fb73c \
--hash=sha256:cc5f0a4f5904b8c25729a0498886b797feb817d1fd3812554ffa39551112c161 \
--hash=sha256:dba037ff8d284c8e7ea9a510c8ae0f5b016004f13c3648f72411c464b67ff2fb \
--hash=sha256:def1a60a111d24376e4b753db39705adbe9483ef4ca4761f825639d884d5da78 \
--hash=sha256:e0d2f68eaa0a755edd9a47d40e50dba6df2bceda66960dee1218da81a2834d27 \
--hash=sha256:e0d30f3fee9372796f54d3100b31ee70972eaadcc87314be369360248a3dcffe \
--hash=sha256:e18589e747c1e70b60fab6767ff99b2d0c359ea1db8a2cb524477f93cdbedf5b \
--hash=sha256:e1e7ed311afb351ff0d0e583a66fcb39675be112d61e7cfd6c8269884a98afbc \
--hash=sha256:e46541de8425a4d6829ac6c5d9b16c03c292105fe9ebf78cb1c31e8d242f9155 \
--hash=sha256:e694b5928b7b33ca2d3b4d5f9bf8b5888906f181daff6b406f4938f3a997a490 \
--hash=sha256:f60fa2adf281fd73ae3a50677572521edca34ba373a45b457b5ebe87c2d01e1d \
--hash=sha256:f84890b205692ea813653ece4ac9afa2139eae136e419231b0eec7c39fdbe4c2 \
--hash=sha256:f8f695d9576ce836eab27ba7401c60acaf9ef6cf2f70dfe5462055ba3df02cc3 \
--hash=sha256:fc05759ffbd7875e0ff2bd877be1438dfe97c9312bbc558c8284a9afa1d0f40e \
--hash=sha256:fd2c2d47969daa0e27eadaf15c13b5e92605c5e5953d23c06d0b5239a2f176d3
cxxheaderparser[pcpp]==1.5.0 \
--hash=sha256:0b9600f817d7378794a0f5df10972fd85f73ba9d3ea0090a5b6b5c12be3b1f01 \
--hash=sha256:2a93fc81c62d2e4de3e92a697336557debe13db44bfef0f2d4fa81501cd1f36f
# via semiwrap
dictdiffer==0.9.0 \
--hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \
--hash=sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595
# via semiwrap
exceptiongroup==1.3.0 \
--hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \
--hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88
# via pytest
grpcio==1.73.1 \
--hash=sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b \
--hash=sha256:07f08705a5505c9b5b0cbcbabafb96462b5a15b7236bbf6bbcc6b0b91e1cbd7e \
--hash=sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1 \
--hash=sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854 \
--hash=sha256:105492124828911f85127e4825d1c1234b032cb9d238567876b5515d01151379 \
--hash=sha256:10af9f2ab98a39f5b6c1896c6fc2036744b5b41d12739d48bed4c3e15b6cf900 \
--hash=sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182 \
--hash=sha256:1c502c2e950fc7e8bf05c047e8a14522ef7babac59abbfde6dbf46b7a0d9c71e \
--hash=sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642 \
--hash=sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887 \
--hash=sha256:2d70f4ddd0a823436c2624640570ed6097e40935c9194482475fe8e3d9754d55 \
--hash=sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646 \
--hash=sha256:3841a8a5a66830261ab6a3c2a3dc539ed84e4ab019165f77b3eeb9f0ba621f26 \
--hash=sha256:42f0660bce31b745eb9d23f094a332d31f210dcadd0fc8e5be7e4c62a87ce86b \
--hash=sha256:45cf17dcce5ebdb7b4fe9e86cb338fa99d7d1bb71defc78228e1ddf8d0de8cbb \
--hash=sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f \
--hash=sha256:5b9b1805a7d61c9e90541cbe8dfe0a593dfc8c5c3a43fe623701b6a01b01d710 \
--hash=sha256:610e19b04f452ba6f402ac9aa94eb3d21fbc94553368008af634812c4a85a99e \
--hash=sha256:628c30f8e77e0258ab788750ec92059fc3d6628590fb4b7cea8c102503623ed7 \
--hash=sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b \
--hash=sha256:67a0468256c9db6d5ecb1fde4bf409d016f42cef649323f0a08a72f352d1358b \
--hash=sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668 \
--hash=sha256:68b84d65bbdebd5926eb5c53b0b9ec3b3f83408a30e4c20c373c5337b4219ec5 \
--hash=sha256:6957025a4608bb0a5ff42abd75bfbb2ed99eda29d5992ef31d691ab54b753643 \
--hash=sha256:6a2b372e65fad38842050943f42ce8fee00c6f2e8ea4f7754ba7478d26a356ee \
--hash=sha256:6a6037891cd2b1dd1406b388660522e1565ed340b1fea2955b0234bdd941a862 \
--hash=sha256:6abfc0f9153dc4924536f40336f88bd4fe7bd7494f028675e2e04291b8c2c62a \
--hash=sha256:75fc8e543962ece2f7ecd32ada2d44c0c8570ae73ec92869f9af8b944863116d \
--hash=sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87 \
--hash=sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2 \
--hash=sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4 \
--hash=sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5 \
--hash=sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf \
--hash=sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582 \
--hash=sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2 \
--hash=sha256:ad5c958cc3d98bb9d71714dc69f1c13aaf2f4b53e29d4cc3f1501ef2e4d129b2 \
--hash=sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9 \
--hash=sha256:b3215f69a0670a8cfa2ab53236d9e8026bfb7ead5d4baabe7d7dc11d30fda967 \
--hash=sha256:b4adc97d2d7f5c660a5498bda978ebb866066ad10097265a5da0511323ae9f50 \
--hash=sha256:ba2cea9f7ae4bc21f42015f0ec98f69ae4179848ad744b210e7685112fa507a1 \
--hash=sha256:bc5eccfd9577a5dc7d5612b2ba90cca4ad14c6d949216c68585fdec9848befb1 \
--hash=sha256:c45a28a0cfb6ddcc7dc50a29de44ecac53d115c3388b2782404218db51cb2df3 \
--hash=sha256:c54796ca22b8349cc594d18b01099e39f2b7ffb586ad83217655781a350ce4da \
--hash=sha256:cce7265b9617168c2d08ae570fcc2af4eaf72e84f8c710ca657cc546115263af \
--hash=sha256:d60588ab6ba0ac753761ee0e5b30a29398306401bfbceffe7d68ebb21193f9d4 \
--hash=sha256:d74c3f4f37b79e746271aa6cdb3a1d7e4432aea38735542b23adcabaaee0c097 \
--hash=sha256:dc7d7fd520614fce2e6455ba89791458020a39716951c7c07694f9dbae28e9c0 \
--hash=sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8 \
--hash=sha256:ed451a0e39c8e51eb1612b78686839efd1a920666d1666c1adfdb4fd51680c0f \
--hash=sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5 \
--hash=sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918
# via grpcio-tools
grpcio-tools==1.68.0 \
--hash=sha256:01ace351a51d7ee120963a4612b1f00e964462ec548db20d17f8902e238592c8 \
@@ -118,9 +126,13 @@ grpcio-tools==1.68.0 \
--hash=sha256:f65942fab440e99113ce14436deace7554d5aa554ea18358e3a5f3fc47efe322 \
--hash=sha256:f95103e3e4e7fee7c6123bc9e4e925e07ad24d8d09d7c1c916fb6c8d1cb9e726
# via -r requirements.txt
jinja2==3.0.0a1 \
--hash=sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484 \
--hash=sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668
iniconfig==2.1.0 \
--hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
--hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
# via pytest
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via -r requirements.txt
markupsafe==3.0.2 \
--hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
@@ -185,6 +197,105 @@ markupsafe==3.0.2 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
--hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
# via jinja2
numpy==2.2.6 \
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
# via
# -r requirements.txt
# opencv-python
opencv-python==4.11.0.86 \
--hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \
--hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \
--hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \
--hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \
--hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \
--hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \
--hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66
# via -r requirements.txt
packaging==25.0 \
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
# via
# pytest
# semiwrap
pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via cxxheaderparser
pkgconf==2.4.3.post1 \
--hash=sha256:101bed059939c26b04dfba1226a9c0ebf1f08b9bee98354797c2d887a08a2d7a \
--hash=sha256:1f334bd2eaf2cb07feb09be439b62ecca1ac2a0aaa447587d5a31029bb43bf69 \
--hash=sha256:36b7be7658296663d67151d2dbb5895721e6a66d5bcc903d7caae1ac6316456d \
--hash=sha256:3b5a1905dd2f08396f1e5a8bab6d0c35e9cb7f3087f1a27f089dcc09ae126f09 \
--hash=sha256:4346e011187ceff0856e1c472a759790b225856da68c60b806e051c84c6ac9ed \
--hash=sha256:442b3aa06ddeb20e5cefc8cbc5811a02db128295a215f497d817cc0f0d358f71 \
--hash=sha256:4e8fe5abadf9c64d4cae927445da5172310cdec300a9c3e49716a95e61848a5f \
--hash=sha256:564a84be78f62605f39a8f45d5449a3549647e6488b8133b8a05281d4cba8aed \
--hash=sha256:6f77ac67af2fac4947ab436e0b6f80db73cca22c87ad3abc6948e096a68370d1 \
--hash=sha256:86857d46fef3c6ee1011a11fe20717803e9c40e004a1347a0876b6e39485288d \
--hash=sha256:8b49ac5d034be5f5e22ec0dd8d6e40f0ae69974299bf84368f4dcffa1ffa5633 \
--hash=sha256:a95610a629818290305860f666bab82b53039746a44e36de35ecf55275345e66 \
--hash=sha256:b52a01db329f8541f9f9e7c69c48b62dbe326658fc67b66ebdfb4aeccc7ccc60 \
--hash=sha256:defe70c329df7d7992b64a105e78d97f154727b595271fd97a70f3ce33b05478 \
--hash=sha256:ec31ce85eab01f7a41d2c86a43827556fe8238f7c5b51ccca42bfd01762d84ca
# via semiwrap
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
# via pytest
protobuf==5.28.3 \
--hash=sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24 \
--hash=sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535 \
@@ -200,9 +311,190 @@ protobuf==5.28.3 \
# via
# -r requirements.txt
# grpcio-tools
pybind11-stubgen==2.5.4 \
--hash=sha256:8625f25da48cf96eea24ba7cae673b5f49b45847b6ef01eead60c4eb762fe5c5 \
--hash=sha256:b6bd44a6d4ba55cef80bd8af92f1f8195b1c6bb0f7bd2f6d785c9530ce6bcae9
# via semiwrap
pygments==2.19.2 \
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
# via pytest
pytest==8.4.1 \
--hash=sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 \
--hash=sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c
# via -r requirements.txt
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via semiwrap
ruamel-yaml==0.18.14 \
--hash=sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2 \
--hash=sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7
# via semiwrap
ruamel-yaml-clib==0.2.12 \
--hash=sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b \
--hash=sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4 \
--hash=sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef \
--hash=sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5 \
--hash=sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3 \
--hash=sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632 \
--hash=sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6 \
--hash=sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7 \
--hash=sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680 \
--hash=sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf \
--hash=sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da \
--hash=sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6 \
--hash=sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a \
--hash=sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01 \
--hash=sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519 \
--hash=sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6 \
--hash=sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f \
--hash=sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd \
--hash=sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2 \
--hash=sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52 \
--hash=sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd \
--hash=sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d \
--hash=sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c \
--hash=sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6 \
--hash=sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb \
--hash=sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a \
--hash=sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969 \
--hash=sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28 \
--hash=sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d \
--hash=sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e \
--hash=sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45 \
--hash=sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4 \
--hash=sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12 \
--hash=sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31 \
--hash=sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642 \
--hash=sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e \
--hash=sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285 \
--hash=sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed \
--hash=sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1 \
--hash=sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7 \
--hash=sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3 \
--hash=sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475 \
--hash=sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5 \
--hash=sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76 \
--hash=sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987 \
--hash=sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df
# via ruamel-yaml
semiwrap==0.1.8 \
--hash=sha256:af5fe5aa3fb9c39b9924ab2f763f41a7a8128ffbaf743a3cb0c3bef6a30c8233 \
--hash=sha256:e176f9f4cca2409a104fab7d14956e1e371ee36264c8478b78a2d142e104537d
# via -r requirements.txt
sphinxify==0.12 \
--hash=sha256:3ec299e78babac7d3457f47bf263411b48e10b9c8add18d7159fa0327cc4a061 \
--hash=sha256:ec97af947884bacd8e18f14ff2b6030b6da829a6a5bf7a32421b633b10c6f7e8
# via semiwrap
tomli==2.2.1 \
--hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
--hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
--hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
--hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
--hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
--hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
--hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
--hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
--hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
--hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
--hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
--hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
--hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
--hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
--hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
--hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
--hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
--hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
--hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
--hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
--hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
--hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
--hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
--hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
--hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
--hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
--hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
--hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
--hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
--hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
--hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
--hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
# via
# pytest
# semiwrap
tomli-w==1.2.0 \
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
# via semiwrap
toposort==1.10 \
--hash=sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd \
--hash=sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87
# via semiwrap
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
# via
# exceptiongroup
# semiwrap
validobj==1.3 \
--hash=sha256:0ddb2e73693763e2014620327486f9e458fcf1d016ce286a146111dc8493e298 \
--hash=sha256:b5a6f79f76064dc1a4e3b2239bf40ea1c4f4ce8d742c9a78784174f784c9cb38
# via semiwrap
# The following packages are considered to be unsafe in a requirements file:
setuptools==75.6.0 \
--hash=sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6 \
--hash=sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
# via grpcio-tools

View File

@@ -0,0 +1,506 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# bazel run //:requirements.update
#
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# pkgconf
# pytest
cxxheaderparser[pcpp]==1.5.0 \
--hash=sha256:0b9600f817d7378794a0f5df10972fd85f73ba9d3ea0090a5b6b5c12be3b1f01 \
--hash=sha256:2a93fc81c62d2e4de3e92a697336557debe13db44bfef0f2d4fa81501cd1f36f
# via semiwrap
dictdiffer==0.9.0 \
--hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \
--hash=sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595
# via semiwrap
exceptiongroup==1.3.0 \
--hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \
--hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88
# via pytest
grpcio==1.73.1 \
--hash=sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b \
--hash=sha256:07f08705a5505c9b5b0cbcbabafb96462b5a15b7236bbf6bbcc6b0b91e1cbd7e \
--hash=sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1 \
--hash=sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854 \
--hash=sha256:105492124828911f85127e4825d1c1234b032cb9d238567876b5515d01151379 \
--hash=sha256:10af9f2ab98a39f5b6c1896c6fc2036744b5b41d12739d48bed4c3e15b6cf900 \
--hash=sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182 \
--hash=sha256:1c502c2e950fc7e8bf05c047e8a14522ef7babac59abbfde6dbf46b7a0d9c71e \
--hash=sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642 \
--hash=sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887 \
--hash=sha256:2d70f4ddd0a823436c2624640570ed6097e40935c9194482475fe8e3d9754d55 \
--hash=sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646 \
--hash=sha256:3841a8a5a66830261ab6a3c2a3dc539ed84e4ab019165f77b3eeb9f0ba621f26 \
--hash=sha256:42f0660bce31b745eb9d23f094a332d31f210dcadd0fc8e5be7e4c62a87ce86b \
--hash=sha256:45cf17dcce5ebdb7b4fe9e86cb338fa99d7d1bb71defc78228e1ddf8d0de8cbb \
--hash=sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f \
--hash=sha256:5b9b1805a7d61c9e90541cbe8dfe0a593dfc8c5c3a43fe623701b6a01b01d710 \
--hash=sha256:610e19b04f452ba6f402ac9aa94eb3d21fbc94553368008af634812c4a85a99e \
--hash=sha256:628c30f8e77e0258ab788750ec92059fc3d6628590fb4b7cea8c102503623ed7 \
--hash=sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b \
--hash=sha256:67a0468256c9db6d5ecb1fde4bf409d016f42cef649323f0a08a72f352d1358b \
--hash=sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668 \
--hash=sha256:68b84d65bbdebd5926eb5c53b0b9ec3b3f83408a30e4c20c373c5337b4219ec5 \
--hash=sha256:6957025a4608bb0a5ff42abd75bfbb2ed99eda29d5992ef31d691ab54b753643 \
--hash=sha256:6a2b372e65fad38842050943f42ce8fee00c6f2e8ea4f7754ba7478d26a356ee \
--hash=sha256:6a6037891cd2b1dd1406b388660522e1565ed340b1fea2955b0234bdd941a862 \
--hash=sha256:6abfc0f9153dc4924536f40336f88bd4fe7bd7494f028675e2e04291b8c2c62a \
--hash=sha256:75fc8e543962ece2f7ecd32ada2d44c0c8570ae73ec92869f9af8b944863116d \
--hash=sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87 \
--hash=sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2 \
--hash=sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4 \
--hash=sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5 \
--hash=sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf \
--hash=sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582 \
--hash=sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2 \
--hash=sha256:ad5c958cc3d98bb9d71714dc69f1c13aaf2f4b53e29d4cc3f1501ef2e4d129b2 \
--hash=sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9 \
--hash=sha256:b3215f69a0670a8cfa2ab53236d9e8026bfb7ead5d4baabe7d7dc11d30fda967 \
--hash=sha256:b4adc97d2d7f5c660a5498bda978ebb866066ad10097265a5da0511323ae9f50 \
--hash=sha256:ba2cea9f7ae4bc21f42015f0ec98f69ae4179848ad744b210e7685112fa507a1 \
--hash=sha256:bc5eccfd9577a5dc7d5612b2ba90cca4ad14c6d949216c68585fdec9848befb1 \
--hash=sha256:c45a28a0cfb6ddcc7dc50a29de44ecac53d115c3388b2782404218db51cb2df3 \
--hash=sha256:c54796ca22b8349cc594d18b01099e39f2b7ffb586ad83217655781a350ce4da \
--hash=sha256:cce7265b9617168c2d08ae570fcc2af4eaf72e84f8c710ca657cc546115263af \
--hash=sha256:d60588ab6ba0ac753761ee0e5b30a29398306401bfbceffe7d68ebb21193f9d4 \
--hash=sha256:d74c3f4f37b79e746271aa6cdb3a1d7e4432aea38735542b23adcabaaee0c097 \
--hash=sha256:dc7d7fd520614fce2e6455ba89791458020a39716951c7c07694f9dbae28e9c0 \
--hash=sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8 \
--hash=sha256:ed451a0e39c8e51eb1612b78686839efd1a920666d1666c1adfdb4fd51680c0f \
--hash=sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5 \
--hash=sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918
# via grpcio-tools
grpcio-tools==1.68.0 \
--hash=sha256:01ace351a51d7ee120963a4612b1f00e964462ec548db20d17f8902e238592c8 \
--hash=sha256:061345c0079b9471f32230186ab01acb908ea0e577bc1699a8cf47acef8be4af \
--hash=sha256:0f77957e3a0916a0dd18d57ce6b49d95fc9a5cfed92310f226339c0fda5394f6 \
--hash=sha256:10d03e3ad4af6284fd27cb14f5a3d52045913c1253e3e24a384ed91bc8adbfcd \
--hash=sha256:1117a81592542f0c36575082daa6413c57ca39188b18a4c50ec7332616f4b97e \
--hash=sha256:1769d7f529de1cc102f7fb900611e3c0b69bdb244fca1075b24d6e5b49024586 \
--hash=sha256:17d0c9004ea82b4213955a585401e80c30d4b37a1d4ace32ccdea8db4d3b7d43 \
--hash=sha256:196cd8a3a5963a4c9e424314df9eb573b305e6f958fe6508d26580ce01e7aa56 \
--hash=sha256:19bafb80948eda979b1b3a63c1567162d06249f43068a0e46a028a448e6f72d4 \
--hash=sha256:261d98fd635595de42aadee848f9af46da6654d63791c888891e94f66c5d0682 \
--hash=sha256:26335eea976dfc1ff5d90b19c309a9425bd53868112a0507ad20f297f2c21d3e \
--hash=sha256:28ebdbad2ef16699d07400b65260240851049a75502eff69a59b127d3ab960f1 \
--hash=sha256:2919faae04fe47bad57fc9b578aeaab527da260e851f321a253b6b11862254a8 \
--hash=sha256:2ec3a2e0afa4866ccc5ba33c071aebaa619245dfdd840cbb74f2b0591868d085 \
--hash=sha256:3aa40958355920ae2846c6fb5cadac4f2c8e33234a2982fef8101da0990e3968 \
--hash=sha256:453ee3193d59c974c678d91f08786f43c25ef753651b0825dc3d008c31baf68d \
--hash=sha256:46b537480b8fd2195d988120a28467601a2a3de2e504043b89fb90318e1eb754 \
--hash=sha256:4fe611d89a1836df8936f066d39c7eb03d4241806449ec45d4b8e1c843ae8011 \
--hash=sha256:511224a99726eb84db9ddb84dc8a75377c3eae797d835f99e80128ec618376d5 \
--hash=sha256:51e5a090849b30c99a2396d42140b8a3e558eff6cdfa12603f9582e2cd07724e \
--hash=sha256:533ce6791a5ba21e35d74c6c25caf4776f5692785a170c01ea1153783ad5af31 \
--hash=sha256:56842a0ce74b4b92eb62cd5ee00181b2d3acc58ba0c4fd20d15a5db51f891ba6 \
--hash=sha256:57e29e78c33fb1b1d557fbe7650d722d1f2b0a9f53ea73beb8ea47e627b6000b \
--hash=sha256:59a885091bf29700ba0e14a954d156a18714caaa2006a7f328b18e1ac4b1e721 \
--hash=sha256:5afd2f3f7257b52228a7808a2b4a765893d4d802d7a2377d9284853e67d045c6 \
--hash=sha256:5d3150d784d8050b10dcf5eb06e04fb90747a1547fed3a062a608d940fe57066 \
--hash=sha256:66b70b37184d40806844f51c2757c6b852511d4ea46a3bf2c7e931a47b455bc6 \
--hash=sha256:6950725bf7a496f81d3ec3324334ffc9dbec743b510dd0e897f51f8627eeb6ac \
--hash=sha256:6dd69c9f3ff85eee8d1f71adf7023c638ca8d465633244ac1b7f19bc3668612d \
--hash=sha256:700f171cd3293ee8d50cd43171562ff07b14fa8e49ee471cd91c6924c7da8644 \
--hash=sha256:737804ec2225dd4cc27e633b4ca0e963b0795161bf678285fab6586e917fd867 \
--hash=sha256:766c2cd2e365e0fc0e559af56f2c2d144d95fd7cb8668a34d533e66d6435eb34 \
--hash=sha256:795f2cd76f68a12b0b5541b98187ba367dd69b49d359cf98b781ead742961370 \
--hash=sha256:7dc5195dc02057668cc22da1ff1aea1811f6fa0deb801b3194dec1fe0bab1cf0 \
--hash=sha256:80b733014eb40d920d836d782e5cdea0dcc90d251a2ffb35ab378ef4f8a42c14 \
--hash=sha256:849b12bec2320e49e988df104c92217d533e01febac172a4495caab36d9f0edc \
--hash=sha256:88640d95ee41921ac7352fa5fadca52a06d7e21fbe53e6a706a9a494f756be7d \
--hash=sha256:8fefc6d000e169a97336feded23ce614df3fb9926fc48c7a9ff8ea459d93b5b0 \
--hash=sha256:92a09afe64fe26696595de2036e10967876d26b12c894cc9160f00152cacebe7 \
--hash=sha256:9509a5c3ed3d54fa7ac20748d501cb86668f764605a0a68f275339ee0f1dc1a6 \
--hash=sha256:ab93fab49fa1e699e577ff5fbb99aba660164d710d4c33cfe0aa9d06f585539f \
--hash=sha256:b094b22919b786ad73c20372ef5e546330e7cd2c6dc12293b7ed586975f35d38 \
--hash=sha256:b47ae076ffb29a68e517bc03552bef0d9c973f8e18adadff180b123e973a26ea \
--hash=sha256:b4ca81770cd729a9ea536d871aacedbde2b732bb9bb83c9d993d63f58502153d \
--hash=sha256:c10f3faa0cc4d89eb546f53b623837af23e86dc495d3b89510bcc0e0a6c0b8b2 \
--hash=sha256:c77ecc5164bb413a613bdac9091dcc29d26834a2ac42fcd1afdfcda9e3003e68 \
--hash=sha256:cad40c3164ee9cef62524dea509449ea581b17ea493178beef051bf79b5103ca \
--hash=sha256:d0470ffc6a93c86cdda48edd428d22e2fef17d854788d60d0d5f291038873157 \
--hash=sha256:d3e678162e1d7a8720dc05fdd537fc8df082a50831791f7bb1c6f90095f8368b \
--hash=sha256:dd9a654af8536b3de8525bff72a245fef62d572eabf96ac946fe850e707cb27d \
--hash=sha256:e31be6dc61496a59c1079b0a669f93dfcc2cdc4b1dbdc4374247cd09cee1329b \
--hash=sha256:e903d07bc65232aa9e7704c829aec263e1e139442608e473d7912417a9908e29 \
--hash=sha256:ee86157ef899f58ba2fe1055cce0d33bd703e99aa6d5a0895581ac3969f06bfa \
--hash=sha256:f65942fab440e99113ce14436deace7554d5aa554ea18358e3a5f3fc47efe322 \
--hash=sha256:f95103e3e4e7fee7c6123bc9e4e925e07ad24d8d09d7c1c916fb6c8d1cb9e726
# via -r requirements.txt
iniconfig==2.1.0 \
--hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
--hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
# via pytest
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via -r requirements.txt
markupsafe==3.0.2 \
--hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
--hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
--hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
--hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
--hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
--hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
--hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
--hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
--hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
--hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
--hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
--hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
--hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
--hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
--hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
--hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
--hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
--hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
--hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
--hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
--hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
--hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
--hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
--hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
--hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
--hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
--hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
--hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
--hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
--hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
--hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
--hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
--hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
--hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
--hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
--hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
--hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
--hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
--hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
--hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
# via jinja2
numpy==2.2.6 \
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
# via
# -r requirements.txt
# opencv-python
opencv-python==4.11.0.86 \
--hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \
--hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \
--hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \
--hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \
--hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \
--hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \
--hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66
# via -r requirements.txt
packaging==25.0 \
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
# via
# pytest
# semiwrap
pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via cxxheaderparser
pkgconf==2.4.3.post1 \
--hash=sha256:101bed059939c26b04dfba1226a9c0ebf1f08b9bee98354797c2d887a08a2d7a \
--hash=sha256:1f334bd2eaf2cb07feb09be439b62ecca1ac2a0aaa447587d5a31029bb43bf69 \
--hash=sha256:36b7be7658296663d67151d2dbb5895721e6a66d5bcc903d7caae1ac6316456d \
--hash=sha256:3b5a1905dd2f08396f1e5a8bab6d0c35e9cb7f3087f1a27f089dcc09ae126f09 \
--hash=sha256:4346e011187ceff0856e1c472a759790b225856da68c60b806e051c84c6ac9ed \
--hash=sha256:442b3aa06ddeb20e5cefc8cbc5811a02db128295a215f497d817cc0f0d358f71 \
--hash=sha256:4e8fe5abadf9c64d4cae927445da5172310cdec300a9c3e49716a95e61848a5f \
--hash=sha256:564a84be78f62605f39a8f45d5449a3549647e6488b8133b8a05281d4cba8aed \
--hash=sha256:6f77ac67af2fac4947ab436e0b6f80db73cca22c87ad3abc6948e096a68370d1 \
--hash=sha256:86857d46fef3c6ee1011a11fe20717803e9c40e004a1347a0876b6e39485288d \
--hash=sha256:8b49ac5d034be5f5e22ec0dd8d6e40f0ae69974299bf84368f4dcffa1ffa5633 \
--hash=sha256:a95610a629818290305860f666bab82b53039746a44e36de35ecf55275345e66 \
--hash=sha256:b52a01db329f8541f9f9e7c69c48b62dbe326658fc67b66ebdfb4aeccc7ccc60 \
--hash=sha256:defe70c329df7d7992b64a105e78d97f154727b595271fd97a70f3ce33b05478 \
--hash=sha256:ec31ce85eab01f7a41d2c86a43827556fe8238f7c5b51ccca42bfd01762d84ca
# via semiwrap
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
# via pytest
protobuf==5.28.3 \
--hash=sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24 \
--hash=sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535 \
--hash=sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b \
--hash=sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548 \
--hash=sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584 \
--hash=sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b \
--hash=sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36 \
--hash=sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135 \
--hash=sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868 \
--hash=sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687 \
--hash=sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed
# via
# -r requirements.txt
# grpcio-tools
pybind11-stubgen==2.5.4 \
--hash=sha256:8625f25da48cf96eea24ba7cae673b5f49b45847b6ef01eead60c4eb762fe5c5 \
--hash=sha256:b6bd44a6d4ba55cef80bd8af92f1f8195b1c6bb0f7bd2f6d785c9530ce6bcae9
# via semiwrap
pygments==2.19.2 \
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
# via pytest
pytest==8.4.1 \
--hash=sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 \
--hash=sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c
# via -r requirements.txt
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via semiwrap
ruamel-yaml==0.18.14 \
--hash=sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2 \
--hash=sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7
# via semiwrap
ruamel-yaml-clib==0.2.12 \
--hash=sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b \
--hash=sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4 \
--hash=sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef \
--hash=sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5 \
--hash=sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3 \
--hash=sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632 \
--hash=sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6 \
--hash=sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7 \
--hash=sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680 \
--hash=sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf \
--hash=sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da \
--hash=sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6 \
--hash=sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a \
--hash=sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01 \
--hash=sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519 \
--hash=sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6 \
--hash=sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f \
--hash=sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd \
--hash=sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2 \
--hash=sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52 \
--hash=sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd \
--hash=sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d \
--hash=sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c \
--hash=sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6 \
--hash=sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb \
--hash=sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a \
--hash=sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969 \
--hash=sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28 \
--hash=sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d \
--hash=sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e \
--hash=sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45 \
--hash=sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4 \
--hash=sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12 \
--hash=sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31 \
--hash=sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642 \
--hash=sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e \
--hash=sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285 \
--hash=sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed \
--hash=sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1 \
--hash=sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7 \
--hash=sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3 \
--hash=sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475 \
--hash=sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5 \
--hash=sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76 \
--hash=sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987 \
--hash=sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df
# via ruamel-yaml
semiwrap==0.1.8 \
--hash=sha256:af5fe5aa3fb9c39b9924ab2f763f41a7a8128ffbaf743a3cb0c3bef6a30c8233 \
--hash=sha256:e176f9f4cca2409a104fab7d14956e1e371ee36264c8478b78a2d142e104537d
# via -r requirements.txt
sphinxify==0.12 \
--hash=sha256:3ec299e78babac7d3457f47bf263411b48e10b9c8add18d7159fa0327cc4a061 \
--hash=sha256:ec97af947884bacd8e18f14ff2b6030b6da829a6a5bf7a32421b633b10c6f7e8
# via semiwrap
tomli==2.2.1 \
--hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
--hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
--hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
--hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
--hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
--hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
--hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
--hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
--hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
--hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
--hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
--hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
--hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
--hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
--hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
--hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
--hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
--hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
--hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
--hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
--hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
--hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
--hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
--hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
--hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
--hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
--hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
--hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
--hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
--hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
--hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
--hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
# via
# pytest
# semiwrap
tomli-w==1.2.0 \
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
# via semiwrap
toposort==1.10 \
--hash=sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd \
--hash=sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87
# via semiwrap
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
# via
# exceptiongroup
# semiwrap
validobj==1.3 \
--hash=sha256:0ddb2e73693763e2014620327486f9e458fcf1d016ce286a146111dc8493e298 \
--hash=sha256:b5a6f79f76064dc1a4e3b2239bf40ea1c4f4ce8d742c9a78784174f784c9cb38
# via semiwrap
# The following packages are considered to be unsafe in a requirements file:
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
# via grpcio-tools

View File

@@ -29,3 +29,7 @@ build:linux --host_cxxopt=-Wno-missing-requires
build:linux --host_cxxopt=-Wno-implicit-fallthrough
build:linux --host_per_file_copt=external/zlib/.*\.c@-Wno-deprecated-non-prototype
# Set soname. Needed for robotpy
build:linux --features=set_soname
build:linux --host_features=set_soname

View File

@@ -0,0 +1,85 @@
load("@allwpilib_pip_deps//:requirements.bzl", "requirement", "whl_requirement")
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("@rules_python//python:pip.bzl", "whl_filegroup")
exports_files(["wrapper.py"])
py_library(
name = "hack_pkgcfgs",
srcs = ["hack_pkgcfgs.py"],
visibility = ["//visibility:public"],
)
py_library(
name = "generation_utils",
srcs = ["generation_utils.py"],
visibility = ["//visibility:public"],
deps = [
requirement("semiwrap"),
requirement("jinja2"),
],
)
py_binary(
name = "generate_native_build_file",
srcs = ["generate_native_build_file.py"],
visibility = ["//visibility:public"],
deps = [
":generation_utils",
":hack_pkgcfgs",
requirement("semiwrap"),
requirement("jinja2"),
],
)
filegroup(
name = "jinja_templates",
srcs = glob(["*.jinja2"]),
visibility = ["//visibility:public"],
)
py_binary(
name = "generate_pybind_build_file",
srcs = ["generate_pybind_build_file.py"],
data = [
":jinja_templates",
],
visibility = ["//visibility:public"],
deps = [
":generation_utils",
":hack_pkgcfgs",
requirement("semiwrap"),
requirement("jinja2"),
],
)
py_binary(
name = "wrapper",
srcs = ["wrapper.py"],
visibility = ["//visibility:public"],
deps = [
"//shared/bazel/rules/robotpy:hack_pkgcfgs",
requirement("semiwrap"),
],
)
whl_filegroup(
name = "semiwrap_header_files",
pattern = "semiwrap/include",
whl = whl_requirement("semiwrap"),
)
cc_library(
name = "semiwrap_headers",
hdrs = [":semiwrap_header_files"],
includes = ["semiwrap_header_files/semiwrap/include"],
visibility = ["//visibility:public"],
)
whl_filegroup(
name = "semiwrap_casters_files",
pattern = "semiwrap/semiwrap.pybind11.json",
visibility = ["//visibility:public"],
whl = whl_requirement("semiwrap"),
)

View File

@@ -0,0 +1,94 @@
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select")
def generate_robotpy_native_wrapper_build_info(name, pyproject_toml, third_party_dirs = []):
"""
This function will generate the bazel file necessary to declare a library that wraps a standard allwpilib library.
Params:
pyproject_toml - Path to the native library wrappers definition file
third_party_dirs - Any directories under src/main/native/thirdparty that should be used by semiwrap
"""
cmd = "$(location //shared/bazel/rules/robotpy:generate_native_build_file) --output_file=$(OUTS)"
cmd += " --project_cfg=$(location " + pyproject_toml + ")"
if third_party_dirs:
cmd += " --third_party_dirs "
for d in third_party_dirs:
cmd += " " + d
native.genrule(
name = "{}.gen_build_info".format(name),
tools = ["//shared/bazel/rules/robotpy:generate_native_build_file"],
srcs = [pyproject_toml],
outs = ["{}-generated_build_info.bzl".format(name)],
cmd = cmd,
tags = ["robotpy"],
target_compatible_with = robotpy_compatibility_select(),
)
write_source_files(
name = "{}.generate_build_info".format(name),
files = {
"robotpy_native_build_info.bzl": "{}-generated_build_info.bzl".format(name),
},
visibility = ["//visibility:public"],
suggested_update_target = "//:write_robotpy_generated_native_files",
tags = ["robotpy"],
target_compatible_with = robotpy_compatibility_select(),
)
def generate_robotpy_pybind_build_info(
name,
package_root_file,
yaml_files = [],
pkgcfgs = [],
additional_srcs = [],
generated_file_name = "robotpy_pybind_build_info.bzl",
pyproject_toml = "src/main/python/pyproject.toml",
stripped_include_prefix = None,
yml_prefix = None):
"""
This function will generate the bazel file necessary to build a pybind library with all of its extensions.
Params:
package_root_file - An __init__.py file used to key the semiwrap wrappers on the project root.
yaml_files - All of the yaml files used by semi wrap to run library wrapping
pkgcfgs - Local files used to trick semiwrap into thinking a library is installed
additional_srcs - Any additional sources needed by the semiwrap process
generated_file_name - Indicates the path of the auto-generated file
pyproject_toml - Location of the pyproject.toml file that defines this project
yml_prefix - Optional. Used in the event that the yml files are in a non-standard location
"""
cmd = "$(location //shared/bazel/rules/robotpy:generate_pybind_build_file) --project_file=$(location " + pyproject_toml + ") --output_file=$(OUTS)"
cmd += " --package_root_file=" + package_root_file
if stripped_include_prefix:
cmd += " --stripped_include_prefix=" + stripped_include_prefix
if yml_prefix:
cmd += " --yml_prefix=" + yml_prefix
if pkgcfgs:
cmd += " --pkgcfgs "
for x in pkgcfgs:
cmd += " $(locations " + x + ")"
native.genrule(
name = "{}.gen_build_info".format(name),
tools = ["//shared/bazel/rules/robotpy:generate_pybind_build_file"],
srcs = [pyproject_toml, package_root_file] + yaml_files + pkgcfgs + additional_srcs + ["//shared/bazel/rules/robotpy:jinja_templates"],
outs = ["{}-generated_build_info.bzl".format(name)],
cmd = cmd,
tags = ["robotpy"],
target_compatible_with = robotpy_compatibility_select(),
)
write_source_files(
name = "{}.generate_build_info".format(name),
files = {
generated_file_name: "{}-generated_build_info.bzl".format(name),
},
suggested_update_target = "//:write_robotpy_generated_pybind_files",
visibility = ["//visibility:public"],
tags = ["robotpy"],
target_compatible_with = robotpy_compatibility_select(),
)

View File

@@ -0,0 +1,6 @@
def robotpy_compatibility_select():
return select({
"@bazel_tools//src/conditions:windows": ["@platforms//:incompatible"],
"@rules_bzlmodrio_toolchains//constraints/is_systemcore:systemcore": ["@platforms//:incompatible"],
"//conditions:default": [],
})

View File

@@ -0,0 +1,110 @@
import argparse
import json
import tomli
from jinja2 import BaseLoader, Environment
from shared.bazel.rules.robotpy.generation_utils import (
fixup_python_dep_name,
fixup_root_package_name,
fixup_shared_lib_name,
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--project_cfg")
parser.add_argument("--output_file")
parser.add_argument("--third_party_dirs", nargs="+")
args = parser.parse_args()
with open(args.project_cfg, "rb") as fp:
raw_config = tomli.load(fp)
def double_quotes(data):
if data:
return json.dumps(data)
return None
def get_pc_dep(library):
base_project = library.replace("robotpy-native-", "")
wpilib_project = fixup_root_package_name(base_project)
return f"//{wpilib_project}:native/{base_project}/{library}.pc"
def get_python_dep(library):
base_project = library.replace("robotpy-native-", "")
wpilib_project = fixup_root_package_name(base_project)
return f"//{fixup_root_package_name(wpilib_project)}:{fixup_python_dep_name(library)}"
env = Environment(loader=BaseLoader)
env.filters["double_quotes"] = double_quotes
env.filters["get_pc_dep"] = get_pc_dep
env.filters["get_python_dep"] = get_python_dep
template = env.from_string(BUILD_FILE_TEMPLATE)
nativelib_config = raw_config["tool"]["hatch"]["build"]["hooks"]["nativelib"]
project_name = nativelib_config["pcfile"][0]["name"]
root_package = fixup_root_package_name(project_name)
shared_library_name = fixup_shared_lib_name(project_name)
with open(args.output_file, "w") as f:
f.write(
template.render(
raw_project_config=raw_config["project"],
nativelib_config=nativelib_config,
root_package=root_package,
shared_library_name=shared_library_name,
third_party_dirs=args.third_party_dirs or [],
)
)
BUILD_FILE_TEMPLATE = """# THIS FILE IS AUTO GENERATED
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "native_wrappery_library")
def define_native_wrapper(name, pyproject_toml = None):
copy_to_directory(
name = "{}.copy_headers".format(name),
srcs = native.glob(["src/main/native/include/**"]) + native.glob(["src/generated/main/native/include/**"], allow_empty = True){% if third_party_dirs %} + native.glob([
{%- for dir in third_party_dirs %}
"src/main/native/thirdparty/{{dir}}/include/**",
{%- endfor %}
]){%- endif %},
out = "native/{{nativelib_config.pcfile[0].name}}/include",
root_paths = ["src/main/native/include/"],
replace_prefixes = {
"{{root_package}}/src/generated/main/native/include": "",
"{{root_package}}/src/main/native/include": "",
{%- for dir in third_party_dirs %}
"{{root_package}}/src/main/native/thirdparty/{{dir}}/include": "",
{%- endfor %}
},
verbose = False,
visibility = ["//visibility:public"],
)
native_wrappery_library(
name = name,
pyproject_toml = pyproject_toml or "src/main/python/native-pyproject.toml",
libinit_file = "native/{{nativelib_config.pcfile[0].name}}/_init_{{raw_project_config.name.replace("-", "_")}}.py",
pc_file = "native/{{nativelib_config.pcfile[0].name}}/{{raw_project_config.name}}.pc",
pc_deps = [
{%- for dep in nativelib_config.pcfile[0].requires | sort %}
"{{dep | get_pc_dep}}",
{%- endfor %}
],
deps = [
{%- for dep in nativelib_config.pcfile[0].requires | sort %}
"{{dep | get_python_dep}}",
{%- endfor %}
],
headers = "{}.copy_headers".format(name),
native_shared_library = "shared/{{shared_library_name}}",
install_path = "native/{{nativelib_config.pcfile[0].name}}/",
)
"""
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,428 @@
import argparse
import collections
import json
import pathlib
import re
from typing import Dict, List, Union
import jinja2
import tomli
from jinja2 import BaseLoader, Environment
from semiwrap.makeplan import (
BuildTarget,
BuildTargetOutput,
CppMacroValue,
Entrypoint,
ExtensionModule,
LocalDependency,
makeplan,
)
from semiwrap.pkgconf_cache import PkgconfCache
from semiwrap.pyproject import PyProject
from shared.bazel.rules.robotpy.generation_utils import (
fixup_native_lib_name,
fixup_python_dep_name,
fixup_root_package_name,
fixup_shared_lib_name,
)
from shared.bazel.rules.robotpy.hack_pkgcfgs import hack_pkgconfig
class HeaderToDatConfig:
def __init__(self, header_to_dat_args: BuildTarget):
includes = []
defines = []
idx = 0
while True:
if header_to_dat_args.args[idx] == "-I":
includes.append(header_to_dat_args.args[idx + 1])
elif header_to_dat_args.args[idx] == "-D":
defines.append(header_to_dat_args.args[idx + 1])
else:
break
idx += 2
if header_to_dat_args.args[idx] == "--cpp":
idx += 2
args = header_to_dat_args.args[idx:]
self.class_name = args[0]
self.yml_file = args[1].path
self.defines = defines
include_root = str(args[3])
if "native" in include_root:
root_dir = pathlib.Path(
include_root[: include_root.find("__main__/") + len("__main__/")]
)
base_include_root = pathlib.Path(*args[3].relative_to(root_dir).parts[3:])
base_include_file = args[2].relative_to(include_root)
base_library = re.search("native/(.*?)/", include_root).groups(1)[0]
self.include_file = f"$(execpath :{fixup_native_lib_name('robotpy-native-' + base_library)}.copy_headers)/{base_include_file}"
self.include_root = f"$(execpath :{fixup_native_lib_name('robotpy-native-' + base_library)}.copy_headers)"
else:
root_dir = pathlib.Path(
include_root[: include_root.find("__main__/") + len("__main__/")]
)
if root_dir.is_absolute():
self.include_file = args[2].relative_to(root_dir)
self.include_root = args[3].relative_to(root_dir)
else:
self.include_file = args[2]
self.include_root = args[3]
# type casters = 4
# dat file = 5
# d file = 6
# compiler info = 7
self.templates = []
self.trampolines = []
args = args[8:]
assert 0 == len(args)
class ResolveCastersConfig:
def __init__(self, item: BuildTarget):
self.pkl_file = item.args[0].name
self.dep_file = item.args[1].name
# semiwrap casters = 2
self.caster_files = []
caster_deps = set()
for dep_path in item.args[3:]:
if isinstance(dep_path, BuildTargetOutput):
output_file = dep_path.target.args[2]
caster_deps.add(
f":src/main/python/{dep_path.target.install_path}/{output_file.name}"
)
else:
relevant_parts = dep_path.parts[3:]
caster_deps.add(
f"//{relevant_parts[0]}:" + "/".join(relevant_parts[1:])
)
self.caster_deps = sorted(caster_deps)
class GenLibInitPyConfig:
def __init__(self, item: BuildTarget):
self.output_file = item.args[0].name
self.modules = item.args[1:]
self.install_path = item.install_path
class GenPkgConfConfig:
def __init__(self, item: BuildTarget):
self.module_pkg_name = item.args[0]
self.pkg_name = item.args[1]
self.project_file = item.args[2].path
self.output_file = item.args[3].name
# --libinit-py = 4
self.libinit_py = item.args[5]
assert 0 == len(item.args[6:])
self.install_path = item.install_path
class GenModInitHpp:
def __init__(self, item: BuildTarget):
self.lib_name = item.args[0]
self.output_file = item.args[1].name
idx = 2
while idx < len(item.args):
if item.args[idx].command != "header2dat":
break
idx += 1
assert 0 == len(item.args[idx:])
class PublishCastersConfig:
def __init__(self, projectcfg, item: BuildTarget):
self.project_file = item.args[0].path
self.casters_name = item.args[1]
self.json_output = item.args[2].name
self.pc_output = item.args[3].name
assert 0 == len(item.args[4:])
self.install_path = item.install_path
self.include_paths = []
caster_cfg = projectcfg.export_type_casters[self.casters_name]
for inc_dir in caster_cfg.includedir:
self.include_paths.append(f"src/main/python/{inc_dir}")
class BazelExtensionModule:
def __init__(
self,
extension_module: ExtensionModule,
additional_extension_targets: Dict[str, BuildTarget],
):
self.name = extension_module.name
self.package_name = extension_module.package_name
self.install_path = extension_module.install_path
self.generation_data = self._extract_header_generation(extension_module.sources)
self.resolve_casters = ResolveCastersConfig(
additional_extension_targets["resolve-casters"]
)
self.gen_libinit = GenLibInitPyConfig(
additional_extension_targets["gen-libinit-py"]
)
self.gen_pkgconf = GenPkgConfConfig(additional_extension_targets["gen-pkgconf"])
self.gen_modinit = GenModInitHpp(
additional_extension_targets["gen-modinit-hpp"]
)
self.pkgcache = PkgconfCache()
all_dependencies = set()
for d in extension_module.depends:
if isinstance(d, LocalDependency):
all_dependencies.add(d.name)
self._collect_local_dependency_names(d, all_dependencies)
native_wrapper_dependencies = set()
local_extension_dependencies = set()
dynamic_dependencies = set()
for dep_name in all_dependencies:
if "native" in dep_name:
transative_deps = set()
self._get_transative_native_dependencies(dep_name, transative_deps)
for d in transative_deps:
base_library = fixup_root_package_name(
d.replace("robotpy-native-", "")
)
native_wrapper_dependencies.add(
f"//{base_library}:{fixup_native_lib_name(d)}.copy_headers"
)
elif "-casters" in dep_name:
base_library = dep_name.split("-")[0]
local_extension_dependencies.add(f"//{base_library}:{dep_name}")
else:
base_library = fixup_root_package_name(dep_name.split("_")[0])
local_extension_dependencies.add(
f"//{base_library}:{fixup_shared_lib_name(base_library)}"
)
dynamic_dependencies.add(
f"//{base_library}:shared/{fixup_shared_lib_name(base_library)}"
)
if dep_name != self.name:
local_extension_dependencies.add(
f"//{base_library}:{dep_name}_pybind_library"
)
self.native_wrapper_dependencies = sorted(native_wrapper_dependencies)
self.local_extension_dependencies = sorted(local_extension_dependencies)
self.dynamic_dependencies = sorted(dynamic_dependencies)
def get_defines(self):
defines = set()
for h2d_def in self.generation_data.values():
defines.update(h2d_def.defines)
return sorted(defines)
def _get_transative_native_dependencies(self, dep_name, transative_deps):
entry = self.pkgcache.get(dep_name)
transative_deps.add(dep_name)
for req in entry.requires:
if req not in transative_deps:
transative_deps.add(req)
self._get_transative_native_dependencies(req, transative_deps)
def _collect_local_dependency_names(self, dep, all_dependencies):
for child_dep in dep.depends:
if isinstance(child_dep, str):
if child_dep != "semiwrap":
all_dependencies.add(child_dep)
elif isinstance(child_dep, LocalDependency):
all_dependencies.add(child_dep.name)
self._collect_local_dependency_names(child_dep, all_dependencies)
else:
raise
def _extract_header_generation(self, sources) -> Dict[str, HeaderToDatConfig]:
generation_data: Dict[str, HeaderToDatConfig] = {}
def get_h2d_config(target_info: BuildTarget) -> HeaderToDatConfig:
config = HeaderToDatConfig(target_info)
if config.class_name not in generation_data:
generation_data[config.class_name] = config
return generation_data[config.class_name]
for source in sources:
if source.command == "dat2cpp":
h2d_config = get_h2d_config(source.args[0])
elif source.command == "dat2trampoline":
h2d_config = get_h2d_config(source.args[0])
name, out_file = source.args[1:]
h2d_config.trampolines.append((name, out_file.name))
elif source.command == "dat2tmplcpp":
h2d_config = get_h2d_config(source.args[0])
name, out_file = source.args[1:]
h2d_config.templates.append((out_file.name[:-4], name))
elif source.command == "dat2tmplhpp":
# Handled by dat2tmplcpp
continue
elif source.command == "gen-modinit-hpp":
# Handled elsewhere
continue
else:
raise Exception("Unknown command", source.command)
return generation_data
def generate_pybind_build_file(
pkgcfgs: List[pathlib.Path],
project_file: pathlib.Path,
package_root_file: str,
stripped_include_prefix: str,
yml_prefix: Union[str, None],
output_file: pathlib.Path,
):
project_dir = project_file.parent
plan = makeplan(project_dir)
hack_pkgconfig(pkgcfgs)
extension_modules = []
entry_points = collections.defaultdict(list)
pyproject = PyProject(project_file)
projectcfg = pyproject.project
# Cache built up for an extension module. Gets reset when an ExtensionModule is encountered
additional_extension_targets: Dict[str, BuildTarget] = {}
publish_casters_targets = []
for item in plan:
if isinstance(item, ExtensionModule):
extension_modules.append(
BazelExtensionModule(item, additional_extension_targets)
)
additional_extension_targets = {}
elif isinstance(item, BuildTarget):
if item.command in [
"resolve-casters",
"gen-libinit-py",
"gen-pkgconf",
"gen-modinit-hpp",
]:
if item.command in additional_extension_targets:
raise Exception(f"Repeated target {item.command}")
additional_extension_targets[item.command] = item
elif item.command in [
"header2dat",
"dat2cpp",
"dat2tmplcpp",
"dat2tmplhpp",
"dat2trampoline",
"make-pyi",
]:
pass
elif item.command == "publish-casters":
publish_casters_targets.append(PublishCastersConfig(projectcfg, item))
else:
raise Exception(f"Unhandled build target {item.command}")
elif isinstance(item, Entrypoint):
entry_points[item.group].append(f"{item.name} = {item.package}")
elif isinstance(item, LocalDependency):
pass
elif isinstance(item, CppMacroValue):
pass
else:
raise Exception(f"Unknown item {type(item)}")
with open(project_file, "rb") as fp:
raw_config = tomli.load(fp)
try:
top_level_name = raw_config["tool"]["hatch"]["build"]["targets"]["wheel"][
"packages"
]
except KeyError:
top_level_name = [raw_config["project"]["name"]]
assert len(top_level_name) == 1
top_level_name = top_level_name[0]
template_file = "shared/bazel/rules/robotpy/pybind_build_file_template.jinja2"
with open(template_file, "r") as f:
template_contents = f.read()
def jsonify(item):
if isinstance(item, jinja2.runtime.Undefined):
return "None"
return json.dumps(item)
def target_from_python_dep(python_dep):
if "native" in python_dep:
base_library = python_dep.replace("robotpy-native-", "")
return f"//{fixup_root_package_name(base_library)}:{fixup_python_dep_name(python_dep)}"
else:
base_library = python_dep.replace("robotpy-", "")
return f"//{fixup_root_package_name(base_library)}:{fixup_python_dep_name(python_dep)}"
python_deps = []
if "dependencies" in raw_config["project"]:
for d in raw_config["project"]["dependencies"]:
if "robotpy-cli" in d:
continue
pd = target_from_python_dep(d.split("==")[0])
python_deps.append(pd)
env = Environment(loader=BaseLoader)
env.filters["jsonify"] = jsonify
template = env.from_string(template_contents)
with open(output_file, "w") as f:
f.write(
template.render(
extension_modules=extension_modules,
top_level_name=top_level_name,
publish_casters_targets=publish_casters_targets,
python_deps=sorted(python_deps),
stripped_include_prefix=stripped_include_prefix,
yml_prefix=yml_prefix,
package_root_file=package_root_file,
raw_project_config=raw_config["project"],
entry_points=entry_points,
)
+ "\n"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--project_file", type=pathlib.Path, required=True)
parser.add_argument("--output_file", type=pathlib.Path, required=True)
parser.add_argument(
"--stripped_include_prefix", type=str, default="src/main/python"
)
parser.add_argument("--yml_prefix", type=str)
parser.add_argument("--package_root_file", type=str)
parser.add_argument("--pkgcfgs", type=pathlib.Path, nargs="+")
args = parser.parse_args()
generate_pybind_build_file(
args.pkgcfgs,
args.project_file,
args.package_root_file,
args.stripped_include_prefix,
args.yml_prefix,
args.output_file,
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
def fixup_root_package_name(name):
if name == "wpihal":
return "hal"
if name == "wpilib":
return "wpilibc"
if name == "wpilog":
return "datalog"
if name == "xrp":
return "xrpVendordep"
if name == "romi":
return "romiVendordep"
if name == "pyntcore":
return "ntcore"
return name
def fixup_native_lib_name(name):
return name
def fixup_shared_lib_name(name):
if name == "wpihal":
return "wpiHal"
if name == "hal":
return "wpiHal"
if name == "wpilib":
return "wpilibc"
if name == "xrp":
return "xrpVendordep"
if name == "romi":
return "romiVendordep"
return name
def fixup_python_dep_name(name):
if name == "robotpy-datalog":
return "robotpy-wpilog"
if name == "robotpy-ntcore":
return "pyntcore"
if name == "wpilib":
return "robotpy-wpilib"
return name

View File

@@ -0,0 +1,19 @@
import os
import pathlib
from typing import List
def hack_pkgconfig(pkgcfgs: List[pathlib.Path]):
"""
This will place the given files in the PKG_CONFIG_PATH in such a way that will trick
semiwrap into thinking the libraries have been installed
"""
pkg_config_paths = os.environ.get("PKG_CONFIG_PATH", "").split(os.pathsep)
if pkgcfgs:
for pc in pkgcfgs:
# pkg_config_paths.append(str(pc.parent.absolute()))
pkg_config_paths.append(str(pc.parent))
os.environ["PKG_CONFIG_PATH"] = os.pathsep.join(pkg_config_paths)

View File

@@ -0,0 +1,12 @@
load("@allwpilib_pip_deps//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_binary")
py_binary(
name = "generate_native_lib_files",
srcs = glob(["*.py"]),
visibility = ["//visibility:public"],
deps = [
"//shared/bazel/rules/robotpy:hack_pkgcfgs",
requirement("semiwrap"),
],
)

View File

@@ -0,0 +1 @@
This is a port of [hatch_nativelib](https://github.com/robotpy/hatch-nativelib/tree/main/src/hatch_nativelib). That tool is not librar-icized and required a fork.

View File

@@ -0,0 +1,106 @@
import dataclasses
import pathlib
import typing as T
@dataclasses.dataclass
class PcFileConfig:
"""
Contents of [[tool.hatch.build.hooks.nativelib.pcfile]] items
"""
pcfile: str
"""
File to write pkgconf file to (relative to pyproject.toml)
"""
description: T.Optional[str] = None
"""Description of this package. If not specified, uses the first line of the package description."""
name: T.Optional[str] = None
"""Name of this package. If not specified, is basename of pcfile without extension"""
version: T.Optional[str] = None
"""If not specified, set to package version"""
includedir: T.Optional[str] = None
"""Where include files can be found (relative to pyproject.toml)"""
libdir: T.Optional[str] = None
"""Where the library is located. If not specified, it is next to pcfile"""
shared_libraries: T.Optional[T.List[str]] = None
"""Name of shared libraries located in libdir (without extension)"""
libs_private: T.Optional[str] = None
"""The link flags for private libraries not exposed to applications"""
requires: T.Optional[T.List[str]] = None
"""
Names of other packages this package requires. They must be installed
at build time.
"""
requires_private: T.Optional[T.List[str]] = None
"""
Names of private packages this package requires. They must be installed
at build time.
"""
extra_cflags: T.Optional[str] = None
"""A list of extra compiler flags to be added to Cflags after header search path"""
extra_link_flags: T.Optional[str] = None
"""A list of extra link flags to be added to Libs"""
variables: T.Optional[T.Dict[str, str]] = None
"""
Custom variables to add to the generated file. Prefix, libdir, includedir must not be specified."""
init_module: str = "auto"
"""
If specified, the name of the python module that will be written next to
the .pc file which will load the shared_libraries
"""
enable_if: T.Optional[str] = None
"""
This is a PEP 508 environment marker specification.
This pcfile will only be generated if the environment marker matches the current
build environment
"""
def get_name(self) -> str:
if self.name:
return self.name
return self.get_pc_path().name[:-3]
def get_out_path(self) -> pathlib.Path:
return self.get_pc_path().parent
def get_pc_path(self) -> pathlib.Path:
pc_path = pathlib.PurePosixPath(self.pcfile)
if pc_path.is_absolute():
raise ValueError(f"pcfile must not be absolute (is {pc_path})")
if not pc_path.name.endswith(".pc"):
raise ValueError(f"pcfile must end with .pc (is {pc_path})")
return pathlib.Path(pc_path)
def get_init_module(self) -> str:
if self.init_module == "auto":
name = self.get_pc_path().name[:-3]
name = name.replace("-", "_").replace(".", "_")
module = f"_init_{name}"
else:
module = self.init_module
if not module.isidentifier():
raise ValueError(
f"init_module must be a valid python identifier (got {module})"
)
return module
def get_init_module_path(self) -> pathlib.Path:
module = self.get_init_module()
return self.get_out_path() / f"{module}.py"

View File

@@ -0,0 +1,318 @@
import functools
import pathlib
import platform
import sys
import typing as T
import pkgconf
import tomli
from packaging.markers import Marker
from shared.bazel.rules.robotpy.hack_pkgcfgs import hack_pkgconfig
from shared.bazel.rules.robotpy.hatchlib_native_port.config import PcFileConfig
from shared.bazel.rules.robotpy.hatchlib_native_port.validate import parse_input
# Port of https://github.com/robotpy/hatch-nativelib/blob/main/src/hatch_nativelib/plugin.py
INITPY_VARNAME = "pkgconf_pypi_initpy"
platform_sys = platform.system()
is_windows = platform_sys == "Windows"
is_macos = platform_sys == "Darwin"
class NativelibHook:
def __init__(self, output_pcfile, output_libinit, config, metadata):
self.output_pcfile = output_pcfile
self.output_libinit = output_libinit
self.config = config
self.root_pth = output_pcfile.parent.parent.parent
self.metadata = metadata
def initialize(self):
for pcfg in self._pcfiles:
self._generate_pcfile(pcfg, {})
def _get_pkg_from_path(self, path: pathlib.Path) -> str:
rel = path.relative_to(self.root_pth)
return str(rel).replace("/", ".").replace("\\", ".")
def _generate_pcfile(
self, pcfg: PcFileConfig, build_data: T.Dict[str, T.Any]
) -> pathlib.Path:
pcfile_rel = pcfg.get_pc_path()
pcfile = self.output_pcfile
prefix_rel = pcfile_rel.parent
prefix_path = pcfile.parent
prefix = "${pcfiledir}"
# variables first
variables = {}
variables["prefix"] = prefix
if pcfg.includedir:
increl = pathlib.PurePosixPath(pcfg.includedir).relative_to(
prefix_rel.as_posix()
)
variables["includedir"] = f"${{prefix}}/{increl}"
if pcfg.shared_libraries:
if pcfg.libdir:
librel = pathlib.PurePosixPath(pcfg.libdir).relative_to(
prefix_rel.as_posix()
)
variables["libdir"] = f"${{prefix}}/{librel}"
else:
variables["libdir"] = "${prefix}"
if pcfg.variables:
for n in ("prefix", "includedir", "libdir", INITPY_VARNAME):
if n in pcfg.variables:
raise ValueError(f"variables may not contain {n}")
variables.update(variables)
# If there are libraries, generate _init_NAME.py for each
if pcfg.shared_libraries:
package = self._get_pkg_from_path(prefix_path)
variables[INITPY_VARNAME] = f"{package}.{pcfg.get_init_module()}"
self._generate_init_py(pcfg, prefix_path, build_data)
# .. not documented but it works?
# eps = self.metadata.core.entry_points.setdefault("pkg_config", {})
# eps[pcfg.get_name()] = package
contents = [f"{k}={v}" for k, v in variables.items()]
contents.append("")
description = pcfg.description
if description is None:
description = self.metadata["description"]
if not description:
raise ValueError(
f"tool.hatch.build.hooks.nativelib.pcfile: description not provided for {pcfg.get_name()}"
)
contents += [
f"Name: {pcfg.get_name()}",
f"Description: {description}",
]
version = pcfg.version or self.metadata["version"]
if version:
contents.append(f"Version: {version}")
libs = []
if pcfg.shared_libraries:
libs.append("-L${libdir}")
libs.extend(f"-l{lib}" for lib in pcfg.shared_libraries)
cflags = []
if pcfg.includedir:
cflags.append("-I${includedir}")
if pcfg.extra_cflags:
cflags.append(pcfg.extra_cflags)
if pcfg.requires:
contents.append(f"Requires: {' '.join(pcfg.requires)}")
if pcfg.requires_private:
contents.append(f"Requires.private: {' '.join(pcfg.requires_private)}")
if libs:
contents.append(f"Libs: {' '.join(libs)}")
if pcfg.libs_private:
contents.append(f"Libs.private: {pcfg.libs_private}")
if cflags:
contents.append(f"Cflags: {' '.join(cflags)}")
content = ("\n".join(contents)) + "\n"
pcfile.parent.mkdir(parents=True, exist_ok=True)
with open(pcfile, "w") as fp:
fp.write(content)
return pcfile
def _generate_init_py(
self,
pcfg: PcFileConfig,
prefix_path: pathlib.Path,
build_data: T.Dict[str, T.Any],
):
libinit_py_rel = pcfg.get_init_module_path()
self.root_pth / libinit_py_rel
libdir = prefix_path
if pcfg.libdir:
libdir = self.root_pth / pathlib.PurePosixPath(pcfg.libdir)
libdir = pathlib.Path(str(libdir).replace("src/", "").replace("src\\", ""))
lib_paths = []
assert pcfg.shared_libraries is not None
for lib in pcfg.shared_libraries:
lib_path = libdir / self._make_shared_lib_fname(lib)
lib_paths.append(lib_path)
if pcfg.requires:
requires = pcfg.requires
else:
requires = []
_write_libinit_py(self.output_libinit, lib_paths, requires)
def _make_shared_lib_fname(self, lib: str):
if is_windows:
return f"{lib}.dll"
elif is_macos:
return f"lib{lib}.dylib"
else:
return f"lib{lib}.so"
@functools.cached_property
def _pcfiles(self) -> T.List[PcFileConfig]:
pcfiles = []
for i, raw_pc in enumerate(self.config.get("pcfile", [])):
pcfile = parse_input(
raw_pc,
PcFileConfig,
"pyproject.toml",
f"tool.hatch.build.hooks.nativelib.pcfile[{i}]",
)
if pcfile.enable_if and not Marker(pcfile.enable_if).evaluate():
print(
f"{pcfile.pcfile} skipped because enable_if did not match current environment"
)
continue
pcfiles.append(pcfile)
return pcfiles
# TODO: this belongs in a separate script/api that can be used from multiple tools
def _write_libinit_py(
init_py: pathlib.Path,
libs: T.List[pathlib.Path],
requires: T.List[str],
):
"""
:param init_py: the _init module for the library(ies) that is written out
:param libs: for each library that is being initialized, this is the
path to that library
:param requires: other pkgconf packages that these libraries depend on.
Their init_py will be looked up and imported first.
"""
contents = [
"# This file is automatically generated, DO NOT EDIT",
"# fmt: off",
"",
]
for req in requires:
r = pkgconf.run_pkgconf(
req, f"--variable={INITPY_VARNAME}", capture_output=True
)
# TODO: should this be a fatal error
if r.returncode == 0:
module = r.stdout.decode("utf-8").strip() # type: ignore[arg-type, union-attr]
contents.append(f"import {module}")
else:
raise Exception("Could not find ", req)
if contents[-1] != "":
contents.append("")
if libs:
contents += [
"def __load_library():",
" from os.path import abspath, join, dirname, exists",
]
if is_macos:
contents += [" from ctypes import CDLL, RTLD_GLOBAL"]
else:
contents += [" from ctypes import cdll", ""]
if len(libs) > 1:
contents.append(" libs = []")
contents.append(" root = abspath(dirname(__file__))")
for lib in libs:
rel = lib.relative_to(init_py.parent)
components = ", ".join(map(repr, rel.parts))
contents += [
"",
f" lib_path = join(root, {components})",
"",
" try:",
]
if is_macos:
load = "CDLL(lib_path, mode=RTLD_GLOBAL)"
else:
load = "cdll.LoadLibrary(lib_path)"
if len(libs) > 1:
contents.append(f" libs.append({load})")
else:
contents.append(f" return {load}")
contents += [
" except FileNotFoundError:",
f" if not exists(lib_path):",
f' raise FileNotFoundError("{lib.name} was not found on your system. Is this package correctly installed?")',
]
if is_windows:
contents.append(
f' raise Exception("{lib.name} could not be loaded. Do you have Visual Studio C++ Redistributible installed?")'
)
else:
contents.append(
f' raise FileNotFoundError("{lib.name} could not be loaded. There is a missing dependency.")'
)
if len(libs) > 1:
contents += [" return libs"]
contents += ["", "__lib = __load_library()", ""]
content = ("\n".join(contents)) + "\n"
init_py.parent.mkdir(parents=True, exist_ok=True)
with open(init_py, "w") as fp:
fp.write(content)
def main():
pyproject_toml = sys.argv[1]
libinit_file = pathlib.Path(sys.argv[2])
pc_file = pathlib.Path(sys.argv[3])
pkgcfgs = [pathlib.Path(x) for x in sys.argv[4:]]
hack_pkgconfig(pkgcfgs)
with open(pyproject_toml, "rb") as fp:
raw_config = tomli.load(fp)
nativelib_cfg = raw_config["tool"]["hatch"]["build"]["hooks"]["nativelib"]
metadata = raw_config["project"]
generator = NativelibHook(pc_file, libinit_file, nativelib_cfg, metadata)
generator.initialize()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,43 @@
import typing
import validobj.validation
from validobj import errors
T = typing.TypeVar("T")
class ValidationError(Exception):
pass
def _convert_validation_error(
fname, ve: errors.ValidationError, prefix: str
) -> ValidationError:
locs = []
msgs = []
e: typing.Optional[BaseException] = ve
while e is not None:
if isinstance(e, errors.WrongFieldError):
locs.append(f".{e.wrong_field}")
elif isinstance(e, errors.WrongListItemError):
locs.append(f"[{e.wrong_index}]")
else:
msgs.append(str(e))
e = e.__cause__
loc = "".join(locs)
if loc.startswith("."):
loc = loc[1:]
msg = "\n ".join(msgs)
vmsg = f"{fname}: {prefix}{loc}:\n {msg}"
return ValidationError(vmsg)
def parse_input(value: typing.Any, spec: typing.Type[T], fname, prefix: str) -> T:
try:
return validobj.validation.parse_input(value, spec)
except errors.ValidationError as ve:
raise _convert_validation_error(fname, ve, prefix) from None

View File

@@ -0,0 +1,236 @@
# THIS FILE IS AUTO GENERATED
{% if publish_casters_targets %}
load("@rules_cc//cc:cc_library.bzl", "cc_library")
{%- endif %}
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "create_pybind_library", "robotpy_library")
load("//shared/bazel/rules/robotpy:semiwrap_helpers.bzl", "gen_libinit", "gen_modinit_hpp", "gen_pkgconf", {% if publish_casters_targets %}"publish_casters", {% endif %}"resolve_casters", "run_header_gen")
load("//shared/bazel/rules/robotpy:semiwrap_tool_helpers.bzl", "scan_headers", "update_yaml_files")
{% for extension_module in extension_modules%}
def {{extension_module.name}}_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includes = [], extra_pyi_deps = []):
{{extension_module.name|upper}}_HEADER_GEN = [
{%- for header_cfg in extension_module.generation_data.values() %}
struct(
class_name = "{{header_cfg.class_name}}",
yml_file = "{{header_cfg.yml_file}}",
header_root = "{{header_cfg.include_root}}",
header_file = "{{header_cfg.include_file}}",
{%- if header_cfg.templates|length > 0 %}
tmpl_class_names = [
{%- for tmpl in header_cfg.templates %}
("{{ tmpl[0] }}", "{{ tmpl[1] }}"),
{%- endfor %}
],
{%- else %}
tmpl_class_names = [],
{%- endif %}
{%- if header_cfg.trampolines|length > 0 %}
trampolines = [
{%- for trampoline in header_cfg.trampolines %}
("{{ trampoline[0] }}", "{{ trampoline[1] }}"),
{%- endfor %}
],
{%- else %}
trampolines = [],
{%- endif %}
),
{%- endfor %}
]
resolve_casters(
name = "{{extension_module.name}}.resolve_casters",
caster_deps = {{ extension_module.resolve_casters.caster_deps | jsonify }},
casters_pkl_file = "{{ extension_module.resolve_casters.pkl_file }}",
dep_file = "{{ extension_module.resolve_casters.dep_file }}",
)
gen_libinit(
name = "{{extension_module.name}}.gen_lib_init",
output_file = "{{stripped_include_prefix}}/{{extension_module.gen_libinit.install_path}}/{{extension_module.gen_libinit.output_file}}",
modules = {{extension_module.gen_libinit.modules | jsonify}},
)
gen_pkgconf(
name = "{{extension_module.name}}.gen_pkgconf",
libinit_py = "{{ extension_module.gen_pkgconf.libinit_py }}",
module_pkg_name = "{{ extension_module.gen_pkgconf.module_pkg_name }}",
output_file = "{{ extension_module.gen_pkgconf.output_file }}",
pkg_name = "{{ extension_module.gen_pkgconf.pkg_name }}",
install_path = "{{stripped_include_prefix}}/{{ extension_module.gen_pkgconf.install_path }}",
project_file = "{{ stripped_include_prefix }}/{{ extension_module.gen_pkgconf.project_file }}",
package_root = "{{package_root_file}}",
)
gen_modinit_hpp(
name = "{{extension_module.name}}.gen_modinit_hpp",
input_dats = [x.class_name for x in {{extension_module.name|upper}}_HEADER_GEN],
libname = "{{ extension_module.gen_modinit.lib_name }}",
output_file = "{{ extension_module.gen_modinit.output_file }}",
)
run_header_gen(
name = "{{extension_module.name}}",
casters_pickle = "{{ extension_module.resolve_casters.pkl_file }}",
header_gen_config = {{extension_module.name|upper}}_HEADER_GEN,
trampoline_subpath = "{{stripped_include_prefix}}/{{ extension_module.install_path }}",
deps = header_to_dat_deps,
local_native_libraries = [
{%- for header_path in extension_module.native_wrapper_dependencies|sort %}
"{{header_path}}",
{%- endfor %}
],
{%- if extension_module.get_defines() %}
generation_defines = [{%-for da in extension_module.get_defines() %}"{{da.replace("=", " ")}}"{% endfor %}],
{%- endif %}
{%- if yml_prefix %}
yml_prefix = "{{yml_prefix}}",
{%- endif %}
)
create_pybind_library(
name = "{{extension_module.name}}",
install_path = "{{stripped_include_prefix}}/{{extension_module.install_path}}/",
extension_name = "{{ extension_module.gen_modinit.lib_name }}",
generated_srcs = [":{{extension_module.name}}.generated_srcs"],
semiwrap_header = [":{{extension_module.name}}.gen_modinit_hpp"],
deps = [
":{{extension_module.name}}.tmpl_hdrs",
":{{extension_module.name}}.trampoline_hdrs",
{%- for dep in extension_module.local_extension_dependencies %}
"{{dep}}",
{%- endfor %}
],
dynamic_deps = [
{%- for dep in extension_module.dynamic_dependencies %}
"{{dep}}",
{%- endfor %}
],
extra_hdrs = extra_hdrs,
extra_srcs = srcs,
includes = includes,
{%- if extension_module.get_defines() %}
local_defines = [{%-for da in extension_module.get_defines() %}"{{da.replace(' ', '=')}}"{% endfor %}],
{%- endif %}
)
native.filegroup(
name = "{{extension_module.name}}.generated_files",
srcs = [
"{{extension_module.name}}.gen_modinit_hpp.gen",
"{{extension_module.name}}.header_gen_files",
"{{extension_module.name}}.gen_pkgconf",
"{{extension_module.name}}.gen_lib_init",
],
tags = ["manual", "robotpy"],
)
{% endfor %}
{%- for caster_info in publish_casters_targets %}
def publish_library_casters():
publish_casters(
name = "publish_casters",
caster_name = "{{caster_info.casters_name}}",
output_json = "{{stripped_include_prefix}}/{{caster_info.install_path}}/{{caster_info.json_output}}",
output_pc = "{{stripped_include_prefix}}/{{caster_info.install_path}}/{{caster_info.pc_output}}",
project_config = "{{ stripped_include_prefix }}/{{caster_info.project_file}}",
package_root = "{{package_root_file}}",
typecasters_srcs = native.glob([{% for inc in caster_info.include_paths %}"{{ inc }}/**"{% if not loop.last%}, {% endif %}{% endfor %}]),
)
cc_library(
name = "{{caster_info.casters_name}}",
hdrs = native.glob([{% for inc in caster_info.include_paths %}"{{ inc }}/*.h"{% if not loop.last%}, {% endif %}{% endfor %}]),
includes = [{% for inc in caster_info.include_paths %}"{{ inc }}"{% if not loop.last%}, {% endif %}{% endfor %}],
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
{% endfor %}
def define_pybind_library(name, pkgcfgs = []):
# Helper used to generate all files with one target.
native.filegroup(
name = "{}.generated_files".format(name),
srcs = [
{%- for em in extension_modules %}
"{{em.name}}.generated_files",
{%- endfor %}
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Files that will be included in the wheel as data deps
native.filegroup(
name = "{}.generated_pkgcfg_files".format(name),
srcs = [
{%- for em in extension_modules %}
"{{stripped_include_prefix}}/{{em.gen_pkgconf.install_path}}/{{ em.gen_pkgconf.output_file }}",
{%- endfor %}
{%- for caster_info in publish_casters_targets %}
"{{stripped_include_prefix}}/{{caster_info.install_path}}/{{caster_info.pc_output}}",
"{{stripped_include_prefix}}/{{caster_info.install_path}}/{{caster_info.json_output}}",
{%- endfor %}
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Contains all of the non-python files that need to be included in the wheel
native.filegroup(
name = "{}.extra_files".format(name),
srcs = native.glob(["{{stripped_include_prefix}}/{{top_level_name}}/**"], exclude = ["{{stripped_include_prefix}}/{{top_level_name}}/**/*.py"], allow_empty = True),
tags = ["manual", "robotpy"],
)
robotpy_library(
name = name,
srcs = native.glob(["{{stripped_include_prefix}}/{{top_level_name}}/**/*.py"]) + [
{%- for em in extension_modules %}
"{{stripped_include_prefix}}/{{ em.gen_pkgconf.libinit_py.replace(".", "/") }}.py",
{%- endfor %}
],
data = [
"{}.generated_pkgcfg_files".format(name),
"{}.extra_files".format(name),
{%- for em in extension_modules %}
":{{stripped_include_prefix}}/{{em.install_path}}/{{em.gen_modinit.lib_name}}",
{%- endfor %}
{%- for em in extension_modules %}
":{{em.name}}.trampoline_hdr_files",
{%- endfor %}
],
imports = ["{{stripped_include_prefix}}"],
deps = [
{%- for d in python_deps %}
"{{d}}",
{%- endfor %}
],
visibility = ["//visibility:public"],
)
update_yaml_files(
name = "{}-update-yaml".format(name),
yaml_output_directory = "{{ stripped_include_prefix }}/semiwrap",
extra_hdrs = native.glob(["{{stripped_include_prefix}}/**/*.h"], allow_empty = True) + [
{%- if python_deps %}
{% for d in python_deps %}
{%- if "native" in d %}"{{d}}.copy_headers",{%- endif %}
{%- endfor %}
{%- endif %}
],
package_root_file = "{{package_root_file}}",
pkgcfgs = pkgcfgs,
pyproject_toml = "{{ stripped_include_prefix }}/pyproject.toml",
yaml_files = native.glob(["{{stripped_include_prefix}}/semiwrap/**"]),
)
scan_headers(
name = "{}-scan-headers".format(name),
extra_hdrs = native.glob(["{{stripped_include_prefix}}/**/*.h"], allow_empty = True) + [
{%- if python_deps %}
{% for d in python_deps %}
{%- if "native" in d %}"{{d}}.copy_headers",{%- endif %}
{%- endfor %}
{%- endif %}
],
package_root_file = "{{package_root_file}}",
pkgcfgs = pkgcfgs,
pyproject_toml = "{{ stripped_include_prefix }}/pyproject.toml",
)

View File

@@ -0,0 +1,197 @@
load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension", "pybind_library")
load("@rules_python//python:defs.bzl", "py_library")
load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select")
def create_pybind_library(
name,
extension_name,
install_path,
generated_srcs = [],
extra_hdrs = [],
extra_srcs = [],
deps = [],
dynamic_deps = [],
semiwrap_header = [],
includes = [],
local_defines = []):
"""
Function to create a pybind C++ extension library that has been defined by a semiwrap config
Outputs:
<name>_pybind_library - A pybind_library that functions like a header-only cc_library. It will include all
of the extra_hdrs, resolve the include paths, and add a dependency on the semiwrap headrs
<install_path + extension_name> - A pybind_extension that wraps the pybind_library and compiles all the source files.
Params:
extension_name - The name of the pybind extension. Should be sourced from pyproject
install_path - The subpath where the library will be installed. Should be source from pyproject
generated_srcs - List of auto-generated sources to be compiled into the extension.
extra_hdrs - Any non-autogenerated headers
extra_srcs - Any non-autogenerated sources
deps - cc_library deps used to create the pybind_library
dynamic_deps - cc_shared_library deps used to filter objects from the pybind_extension
semiwrap_header - Auto-generated file used to initialize the extension
includes - see cc_library#includes. Used during the creating of the pybind_library
local_defines - see cc_library#local_defines. Used during the compilation of the extension
"""
pybind_library(
name = "{}_pybind_library".format(name),
hdrs = extra_hdrs,
target_compatible_with = robotpy_compatibility_select(),
deps = deps + [
"//shared/bazel/rules/robotpy:semiwrap_headers",
],
includes = includes,
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
extension_name = extension_name or "_{}".format(name)
pybind_extension(
name = install_path + extension_name,
srcs = extra_srcs + generated_srcs,
deps = [":{}_pybind_library".format(name)] + semiwrap_header,
dynamic_deps = dynamic_deps,
copts = select({
"@bazel_tools//src/conditions:darwin": [
"-Wno-deprecated-declarations",
"-Wno-overloaded-virtual",
"-Wno-pessimizing-move",
"-Wno-unused-value",
],
"@bazel_tools//src/conditions:linux_x86_64": [
"-Wno-attributes",
"-Wno-unused-value",
"-Wno-deprecated",
"-Wno-deprecated-declarations",
"-Wno-unused-parameter",
"-Wno-redundant-move",
"-Wno-unused-but-set-variable",
"-Wno-unused-variable",
"-Wno-pessimizing-move",
"-Wno-overloaded-virtual",
],
"@bazel_tools//src/conditions:windows": [
],
}),
target_compatible_with = robotpy_compatibility_select(),
local_defines = local_defines,
tags = ["robotpy"],
)
def robotpy_library(
name,
data = [],
**kwargs):
"""
Defines a python library that is wrapping a series of pybind extensions.
Outputs:
<name> - The python library
"""
py_library(
name = name,
data = data,
tags = ["robotpy"],
**kwargs
)
def copy_native_file(name, library, base_path):
"""
Copies a compiled shared library into a naming format that can be used by robotpy rules. The libraries are named
differently on OSX / Linux / Windows and this creates a handy alias to for easier use downstream
"""
copy_file(
name = name + ".win_copy_lib",
src = library,
out = "{}lib/{}.dll".format(base_path, name),
tags = ["manual"],
visibility = ["//visibility:public"],
)
copy_file(
name = name + ".osx_copy_lib",
src = library,
out = "{}lib/lib{}.dylib".format(base_path, name),
tags = ["manual"],
visibility = ["//visibility:public"],
)
copy_file(
name = name + ".linux_copy_lib",
src = library,
out = "{}lib/lib{}.so".format(base_path, name),
tags = ["manual"],
visibility = ["//visibility:public"],
)
native.alias(
name = "{}.copy_lib".format(name),
actual = select({
"@bazel_tools//src/conditions:darwin": name + ".osx_copy_lib",
"@bazel_tools//src/conditions:windows": name + ".win_copy_lib",
"//conditions:default": name + ".linux_copy_lib",
}),
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
def _folder_prefix(name):
if "/" in name:
last_slash = name.rfind("/")
return (name[0:last_slash], name[last_slash + 1:])
else:
return ("", name)
def native_wrappery_library(
name,
pyproject_toml,
libinit_file,
pc_file,
pc_deps,
native_shared_library,
install_path,
headers,
deps = []):
"""
This function provides a sugar wrapper for defining a python library that wraps an allwpilib native library
"""
cmd = "$(locations //shared/bazel/rules/robotpy/hatchlib_native_port:generate_native_lib_files) "
cmd += " $(location " + pyproject_toml + ")"
cmd += " $(OUTS) "
for pc_dep in pc_deps:
cmd += " $(location " + pc_dep + ")"
native.genrule(
name = name + ".gen",
srcs = [pyproject_toml],
outs = [libinit_file, pc_file],
cmd = cmd,
tools = ["//shared/bazel/rules/robotpy/hatchlib_native_port:generate_native_lib_files"] + pc_deps,
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
prefix, libname = _folder_prefix(native_shared_library)
copy_native_file(
name = libname,
library = native_shared_library,
base_path = install_path,
)
native.filegroup(
name = name + ".pc_wrapper",
srcs = [pc_file],
)
py_library(
name = name,
srcs = [libinit_file],
data = [pc_file, ":{}.copy_lib".format(libname), headers],
deps = deps,
imports = ["."],
visibility = ["//visibility:public"],
tags = ["robotpy"],
)

View File

@@ -0,0 +1,17 @@
load("@rules_python_pytest//python_pytest:defs.bzl", "py_pytest_test")
load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select")
def robotpy_py_test(name, srcs, **kwargs):
py_pytest_test(
name = name,
size = "small",
srcs = srcs,
target_compatible_with = robotpy_compatibility_select(),
tags = [
"no-asan",
"no-tsan",
"robotpy",
],
legacy_create_init = 0,
**kwargs
)

View File

@@ -0,0 +1,357 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select")
RESOLVE_CASTERS_DIR = "generated/resolve_casters/"
HEADER_DAT_DIR = "generated/header_to_dat/"
DAT_TO_CC_DIR = "generated/dat_to_cc/"
DAT_TO_TMPL_CC_DIR = "generated/dat_to_tmpl_cc/"
DAT_TO_TMPL_HDR_DIR = "generated/dat_to_tmpl_hdr/"
GEN_MODINIT_HDR_DIR = "generated/gen_modinit_hdr/"
def _location_helper(filename):
return " $(locations " + filename + ")"
def _wrapper():
return "$(locations //shared/bazel/rules/robotpy:wrapper) "
def _wrapper_dep():
return ["//shared/bazel/rules/robotpy:wrapper"]
def _semiwrap_caster():
return "//shared/bazel/rules/robotpy:semiwrap_casters_files"
def publish_casters(
name,
project_config,
caster_name,
output_json,
output_pc,
typecasters_srcs,
package_root):
"""
Sugar wrapper for the semiwrap.cmd.publish_casters tool
"""
cmd = _wrapper() + " semiwrap.cmd.publish_casters"
cmd += " $(SRCS) " + caster_name + " $(OUTS)"
native.genrule(
name = name,
srcs = [project_config],
outs = [output_json, output_pc],
cmd = cmd,
tools = _wrapper_dep() + typecasters_srcs + [package_root],
target_compatible_with = robotpy_compatibility_select(),
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
def resolve_casters(
name,
casters_pkl_file,
dep_file,
caster_files = [],
caster_deps = []):
"""
Sugar wrapper for the semiwrap.cmd.resolve_casters tool
"""
cmd = _wrapper() + " semiwrap.cmd.resolve_casters "
cmd += " $(OUTS)"
cmd += _location_helper(_semiwrap_caster()) + "/semiwrap/semiwrap.pybind11.json"
resolved_caster_files = []
deps = []
for dep in caster_deps:
deps.append(dep)
cmd += _location_helper(dep)
for cfd in caster_files:
if cfd.startswith(":"):
resolved_caster_files.append(cfd)
cmd += _location_helper(cfd)
else:
cmd += " " + cfd
native.genrule(
name = name,
srcs = resolved_caster_files + deps,
outs = [RESOLVE_CASTERS_DIR + casters_pkl_file, RESOLVE_CASTERS_DIR + dep_file],
cmd = cmd,
tools = _wrapper_dep() + [_semiwrap_caster()],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def gen_libinit(
name,
output_file,
modules):
"""
Sugar wrapper for the semiwrap.cmd.gen_libinit tool
"""
cmd = _wrapper() + " semiwrap.cmd.gen_libinit "
cmd += " $(OUTS) "
cmd += " ".join(modules)
native.genrule(
name = name,
outs = [output_file],
cmd = cmd,
tools = _wrapper_dep(),
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def gen_pkgconf(
name,
project_file,
module_pkg_name,
pkg_name,
output_file,
libinit_py,
install_path,
package_root):
"""
Sugar wrapper for the semiwrap.cmd.gen_pkgconf tool
"""
cmd = _wrapper() + " semiwrap.cmd.gen_pkgconf "
cmd += " " + module_pkg_name + " " + pkg_name
cmd += _location_helper(project_file)
cmd += " $(OUTS)"
if libinit_py:
cmd += " --libinit-py " + libinit_py
OUT_FILE = install_path + "/" + output_file
native.genrule(
name = name,
srcs = [package_root],
outs = [OUT_FILE],
cmd = cmd,
tools = _wrapper_dep() + [project_file],
target_compatible_with = robotpy_compatibility_select(),
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
def header_to_dat(
name,
casters_pickle,
include_root,
class_name,
yml_file,
header_location,
generation_includes = [],
header_to_dat_deps = [],
extra_defines = [],
deps = []):
"""
Sugar wrapper for the semiwrap.cmd.header2dat tool
"""
cmd = _wrapper() + " semiwrap.cmd.header2dat "
cmd += "--cpp 202002L " # TODO(pj) - This is the option when I ran on linux. Does its value really matter?
cmd += class_name
cmd += _location_helper(yml_file)
cmd += " -I " + include_root
for inc in generation_includes:
cmd += " -I " + inc
for d in extra_defines:
cmd += " -D '" + d + "'"
cmd += " " + header_location
cmd += " " + include_root
cmd += _location_helper(RESOLVE_CASTERS_DIR + casters_pickle)
cmd += " $(OUTS)"
cmd += " bogus c++20 ccache c++ -- -std=c++20" # TODO(pj) Does it matter what these values are?
native.genrule(
name = name + "." + class_name,
srcs = [RESOLVE_CASTERS_DIR + casters_pickle] + deps + header_to_dat_deps,
outs = [HEADER_DAT_DIR + class_name + ".dat", HEADER_DAT_DIR + class_name + ".d"],
cmd = cmd,
tools = _wrapper_dep() + [yml_file],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def dat_to_cc(
name,
class_name):
dat_file = HEADER_DAT_DIR + class_name + ".dat"
cmd = _wrapper() + " semiwrap.cmd.dat2cpp "
cmd += _location_helper(dat_file)
cmd += " $(OUTS)"
native.genrule(
name = name + "." + class_name,
outs = [DAT_TO_CC_DIR + class_name + ".cpp"],
cmd = cmd,
tools = _wrapper_dep() + [dat_file],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def dat_to_tmpl_cpp(name, base_class_name, specialization, tmp_class_name):
cmd = _wrapper() + " semiwrap.cmd.dat2tmplcpp "
cmd += _location_helper(HEADER_DAT_DIR + base_class_name + ".dat")
cmd += " " + specialization
cmd += " $(OUTS)"
native.genrule(
name = name + "." + tmp_class_name,
outs = [DAT_TO_TMPL_CC_DIR + tmp_class_name + ".cpp"],
cmd = cmd,
tools = _wrapper_dep() + [HEADER_DAT_DIR + base_class_name + ".dat"],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def dat_to_tmpl_hpp(name, class_name):
dat_file = HEADER_DAT_DIR + class_name + ".dat"
cmd = _wrapper() + " semiwrap.cmd.dat2tmplhpp "
cmd += _location_helper(dat_file)
cmd += " $(OUTS)"
native.genrule(
name = name + "." + class_name,
outs = [DAT_TO_TMPL_HDR_DIR + class_name + "_tmpl.hpp"],
cmd = cmd,
tools = _wrapper_dep() + [dat_file],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def dat_to_trampoline(name, dat_file, class_name, output_file):
cmd = _wrapper() + " semiwrap.cmd.dat2trampoline "
cmd += _location_helper(HEADER_DAT_DIR + dat_file)
cmd += " " + class_name
cmd += " $(OUTS)"
native.genrule(
name = name + "." + output_file,
outs = [output_file],
cmd = cmd,
tools = _wrapper_dep() + [HEADER_DAT_DIR + dat_file],
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
def gen_modinit_hpp(
name,
libname,
input_dats,
output_file):
input_dats = [HEADER_DAT_DIR + x + ".dat" for x in input_dats]
cmd = _wrapper() + " semiwrap.cmd.gen_modinit_hpp "
cmd += " " + libname
cmd += " $(OUTS)"
for input_dat in input_dats:
cmd += _location_helper(input_dat)
native.genrule(
name = name + ".gen",
outs = [GEN_MODINIT_HDR_DIR + output_file],
cmd = cmd,
tools = _wrapper_dep() + input_dats,
target_compatible_with = robotpy_compatibility_select(),
tags = ["robotpy"],
)
cc_library(
name = name,
hdrs = [GEN_MODINIT_HDR_DIR + output_file],
strip_include_prefix = GEN_MODINIT_HDR_DIR,
tags = ["robotpy"],
target_compatible_with = robotpy_compatibility_select(),
)
def run_header_gen(name, casters_pickle, trampoline_subpath, header_gen_config, deps = [], generation_defines = [], local_native_libraries = [], header_to_dat_deps = [], yml_prefix = "src/main/python/"):
generation_includes = []
header_to_dat_deps = list(header_to_dat_deps)
for dep in local_native_libraries:
header_to_dat_deps.append(dep)
generation_includes.append("$(execpath " + dep + ")")
for header_gen in header_gen_config:
header_to_dat(
name = name + ".header_to_dat",
casters_pickle = casters_pickle,
include_root = header_gen.header_root,
class_name = header_gen.class_name,
yml_file = yml_prefix + header_gen.yml_file,
header_location = header_gen.header_file,
deps = deps,
generation_includes = generation_includes,
extra_defines = generation_defines,
header_to_dat_deps = header_to_dat_deps,
)
generated_cc_files = []
for header_gen in header_gen_config:
dat_to_cc(
name = name + ".dat_to_cc",
class_name = header_gen.class_name,
)
generated_cc_files.append(DAT_TO_CC_DIR + header_gen.class_name + ".cpp")
tmpl_hdrs = []
for header_gen in header_gen_config:
if header_gen.tmpl_class_names:
dat_to_tmpl_hpp(
name = name + ".dat_to_tmpl_hpp",
class_name = header_gen.class_name,
)
tmpl_hdrs.append(DAT_TO_TMPL_HDR_DIR + header_gen.class_name + "_tmpl.hpp")
for tmpl_class_name, specialization in header_gen.tmpl_class_names:
dat_to_tmpl_cpp(
name = name + ".dat_to_tmpl_cpp",
base_class_name = header_gen.class_name,
specialization = specialization,
tmp_class_name = tmpl_class_name,
)
generated_cc_files.append(DAT_TO_TMPL_CC_DIR + tmpl_class_name + ".cpp")
trampoline_hdrs = []
for header_gen in header_gen_config:
for trampoline_symbol, trampoline_header in header_gen.trampolines:
output_path = trampoline_subpath + "/trampolines/" + trampoline_header
dat_to_trampoline(
name = name + ".dat2trampoline",
dat_file = header_gen.class_name + ".dat",
class_name = trampoline_symbol,
output_file = output_path,
)
trampoline_hdrs.append(output_path)
cc_library(
name = name + ".tmpl_hdrs",
hdrs = tmpl_hdrs,
strip_include_prefix = DAT_TO_TMPL_HDR_DIR,
tags = ["robotpy"],
)
cc_library(
name = name + ".trampoline_hdrs",
hdrs = trampoline_hdrs,
strip_include_prefix = trampoline_subpath,
tags = ["robotpy"],
)
native.filegroup(
name = name + ".generated_srcs",
srcs = generated_cc_files,
tags = ["manual", "robotpy"],
)
native.filegroup(
name = name + ".trampoline_hdr_files",
srcs = trampoline_hdrs,
tags = ["manual", "robotpy"],
)
native.filegroup(
name = name + ".header_gen_files",
srcs = tmpl_hdrs + trampoline_hdrs + generated_cc_files,
tags = ["manual", "robotpy"],
)

View File

@@ -0,0 +1,92 @@
load("@allwpilib_pip_deps//:requirements.bzl", "requirement")
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
load("@rules_python//python:defs.bzl", "py_test")
load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select")
def __update_yaml_files_impl(ctx):
output_dir = ctx.actions.declare_directory(ctx.attr.gen_dir)
args = ctx.actions.args()
args.add("semiwrap.tool")
args.add("update-yaml")
args.add("--write")
args.add("-v")
args.add("--project_file=" + ctx.files.pyproject_toml[0].path)
args.add("--override_output_directory=" + output_dir.path)
if ctx.files.pkgcfgs:
args.add("--pkgcfgs")
for f in ctx.files.pkgcfgs:
args.add(str(f.path))
ctx.actions.run(
inputs = ctx.files.package_root_file + ctx.files.pyproject_toml + ctx.files.pkgcfgs + ctx.files.extra_hdrs + ctx.files.yaml_files,
outputs = [output_dir],
executable = ctx.executable._tool,
arguments = [args],
)
return [DefaultInfo(files = depset([output_dir]))]
__update_yaml_files = rule(
implementation = __update_yaml_files_impl,
attrs = {
"extra_hdrs": attr.label_list(allow_files = True),
"gen_dir": attr.string(mandatory = True),
"package_root_file": attr.label(mandatory = True, allow_files = True),
"pkgcfgs": attr.label_list(allow_files = True),
"pyproject_toml": attr.label(mandatory = True, allow_files = True),
"yaml_files": attr.label_list(mandatory = True, allow_files = True),
"_tool": attr.label(
default = Label("//shared/bazel/rules/robotpy:wrapper"),
cfg = "exec",
executable = True,
),
},
)
def update_yaml_files(name, yaml_output_directory = "src/main/python/semiwrap", **kwargs):
__update_yaml_files(
name = name,
gen_dir = "{}_gen_update_yaml".format(name),
target_compatible_with = robotpy_compatibility_select(),
**kwargs
)
write_source_files(
name = "write_{}".format(name),
files = {
yaml_output_directory: ":" + name,
},
suggested_update_target = "//:write_all",
target_compatible_with = robotpy_compatibility_select(),
visibility = ["//visibility:public"],
)
def scan_headers(name, pyproject_toml, package_root_file, extra_hdrs, pkgcfgs):
if pkgcfgs:
pkgcfg_args = ["--pkgcfgs"]
for pkgcfg in pkgcfgs:
pkgcfg_args.append(" $(locations " + pkgcfg + ")")
else:
pkgcfg_args = []
py_test(
name = name,
srcs = [
"//shared/bazel/rules/robotpy:wrapper.py",
],
deps = [
"//shared/bazel/rules/robotpy:hack_pkgcfgs",
requirement("semiwrap"),
],
args = [
"semiwrap.tool",
"scan-headers",
"--pyproject=$(location " + pyproject_toml + ")",
] + pkgcfg_args,
data = extra_hdrs + pkgcfgs + [pyproject_toml, package_root_file],
main = "shared/bazel/rules/robotpy/wrapper.py",
size = "small",
target_compatible_with = robotpy_compatibility_select(),
)

View File

@@ -0,0 +1,55 @@
import importlib
import os
import pathlib
import sys
from shared.bazel.rules.robotpy.hack_pkgcfgs import hack_pkgconfig
"""
This file will wrap various semiwrap.tools executables. In the event that it fails
it will provide more helpful debug information for bazel users.
It can also "hack" the PKG_CONFIG_PATH environment variable. This allows us to use
generated package config files without having to install the libraries which decreases
build dependencies and increases the amount of parallelization that can happen during
the build.
"""
def main():
tool = sys.argv[1]
if "--pkgcfgs" in sys.argv[2:]:
pkgcfg_index = sys.argv.index("--pkgcfgs")
args = sys.argv[2:pkgcfg_index]
pkgcfgs = [pathlib.Path(x) for x in sys.argv[pkgcfg_index + 1 :]]
else:
args = sys.argv[2:]
pkgcfgs = []
hack_pkgconfig(pkgcfgs)
module = importlib.import_module(tool)
tool_main = getattr(module, "main")
sys.argv = [""] + args
try:
tool_main()
except SystemExit as e:
if e.code != 0:
raise Exception(
"sys.exit() explicitly called with a non-zero error code", e
)
except:
print("-------------------------------------")
print("Failed to run wrapped tool.")
print(f"CWD: {os.getcwd()}")
print(f"Tool: {tool}, Args:")
for a in args:
print(" ", a)
print("-------------------------------------")
raise
if __name__ == "__main__":
main()

View File

@@ -15,6 +15,9 @@ generatedFileExclude {
src/main/native/include/wpinet/http_parser\.h$
src/main/native/resources/
src/main/native/linux/AvahiClient
src/main/python/
src/test/python/
}
licenseUpdateExclude {

View File

@@ -1,3 +1,4 @@
load("@allwpilib_pip_deps//:requirements.bzl", "requirement")
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
load("@rules_java//java:defs.bzl", "java_binary")
load("@rules_pkg//:mappings.bzl", "pkg_files")
@@ -6,6 +7,10 @@ load("//shared/bazel/rules:java_rules.bzl", "wpilib_java_junit5_test")
load("//shared/bazel/rules:jni_rules.bzl", "wpilib_jni_cc_library", "wpilib_jni_java_library")
load("//shared/bazel/rules:packaging.bzl", "package_minimal_jni_project")
load("//shared/bazel/rules/gen:gen-resources.bzl", "generate_resources")
load("//shared/bazel/rules/robotpy:build_info_gen.bzl", "generate_robotpy_native_wrapper_build_info", "generate_robotpy_pybind_build_info")
load("//shared/bazel/rules/robotpy:pytest_util.bzl", "robotpy_py_test")
load("//wpinet:robotpy_native_build_info.bzl", "define_native_wrapper")
load("//wpinet:robotpy_pybind_build_info.bzl", "define_pybind_library", "wpinet_extension")
filegroup(
name = "doxygen-files",
@@ -304,6 +309,55 @@ cc_binary(
],
)
generate_robotpy_native_wrapper_build_info(
name = "robotpy-native-wpinet-generator",
pyproject_toml = "src/main/python/native-pyproject.toml",
third_party_dirs = [
"libuv",
"tcpsockets",
],
)
define_native_wrapper(
name = "robotpy-native-wpinet",
pyproject_toml = "src/main/python/native-pyproject.toml",
)
PYBIND_PKGCFG_DEPS = [
"//wpinet:native/wpinet/robotpy-native-wpinet.pc",
"//wpiutil:native/wpiutil/robotpy-native-wpiutil.pc",
"//wpiutil:robotpy-wpiutil.generated_pkgcfg_files",
]
generate_robotpy_pybind_build_info(
name = "robotpy-wpinet-generator",
additional_srcs = [":robotpy-native-wpinet.copy_headers"],
package_root_file = "src/main/python/wpinet/__init__.py",
pkgcfgs = PYBIND_PKGCFG_DEPS,
yaml_files = glob(["src/main/python/semiwrap/*.yml"]),
)
wpinet_extension(
srcs = ["src/main/python/wpinet/src/main.cpp"],
includes = [
"src/main/python/wpinet/",
],
)
define_pybind_library(
name = "robotpy-wpinet",
pkgcfgs = PYBIND_PKGCFG_DEPS,
)
robotpy_py_test(
"python_tests",
srcs = glob(["src/test/python/**/*.py"]),
deps = [
":robotpy-wpinet",
requirement("pytest"),
],
)
package_minimal_jni_project(
name = "wpinet",
maven_artifact_name = "wpinet-cpp",

39
wpinet/robotpy_native_build_info.bzl generated Normal file
View File

@@ -0,0 +1,39 @@
# THIS FILE IS AUTO GENERATED
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "native_wrappery_library")
def define_native_wrapper(name, pyproject_toml = None):
copy_to_directory(
name = "{}.copy_headers".format(name),
srcs = native.glob(["src/main/native/include/**"]) + native.glob(["src/generated/main/native/include/**"], allow_empty = True) + native.glob([
"src/main/native/thirdparty/libuv/include/**",
"src/main/native/thirdparty/tcpsockets/include/**",
]),
out = "native/wpinet/include",
root_paths = ["src/main/native/include/"],
replace_prefixes = {
"wpinet/src/generated/main/native/include": "",
"wpinet/src/main/native/include": "",
"wpinet/src/main/native/thirdparty/libuv/include": "",
"wpinet/src/main/native/thirdparty/tcpsockets/include": "",
},
verbose = False,
visibility = ["//visibility:public"],
)
native_wrappery_library(
name = name,
pyproject_toml = pyproject_toml or "src/main/python/native-pyproject.toml",
libinit_file = "native/wpinet/_init_robotpy_native_wpinet.py",
pc_file = "native/wpinet/robotpy-native-wpinet.pc",
pc_deps = [
"//wpiutil:native/wpiutil/robotpy-native-wpiutil.pc",
],
deps = [
"//wpiutil:robotpy-native-wpiutil",
],
headers = "{}.copy_headers".format(name),
native_shared_library = "shared/wpinet",
install_path = "native/wpinet/",
)

174
wpinet/robotpy_pybind_build_info.bzl generated Normal file
View File

@@ -0,0 +1,174 @@
# THIS FILE IS AUTO GENERATED
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "create_pybind_library", "robotpy_library")
load("//shared/bazel/rules/robotpy:semiwrap_helpers.bzl", "gen_libinit", "gen_modinit_hpp", "gen_pkgconf", "resolve_casters", "run_header_gen")
load("//shared/bazel/rules/robotpy:semiwrap_tool_helpers.bzl", "scan_headers", "update_yaml_files")
def wpinet_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includes = [], extra_pyi_deps = []):
WPINET_HEADER_GEN = [
struct(
class_name = "PortForwarder",
yml_file = "semiwrap/PortForwarder.yml",
header_root = "$(execpath :robotpy-native-wpinet.copy_headers)",
header_file = "$(execpath :robotpy-native-wpinet.copy_headers)/wpinet/PortForwarder.h",
tmpl_class_names = [],
trampolines = [
("wpi::PortForwarder", "wpi__PortForwarder.hpp"),
],
),
struct(
class_name = "WebServer",
yml_file = "semiwrap/WebServer.yml",
header_root = "$(execpath :robotpy-native-wpinet.copy_headers)",
header_file = "$(execpath :robotpy-native-wpinet.copy_headers)/wpinet/WebServer.h",
tmpl_class_names = [],
trampolines = [
("wpi::WebServer", "wpi__WebServer.hpp"),
],
),
]
resolve_casters(
name = "wpinet.resolve_casters",
caster_deps = ["//wpiutil:src/main/python/wpiutil/wpiutil-casters.pybind11.json"],
casters_pkl_file = "wpinet.casters.pkl",
dep_file = "wpinet.casters.d",
)
gen_libinit(
name = "wpinet.gen_lib_init",
output_file = "src/main/python/wpinet/_init__wpinet.py",
modules = ["native.wpinet._init_robotpy_native_wpinet", "wpiutil._init__wpiutil"],
)
gen_pkgconf(
name = "wpinet.gen_pkgconf",
libinit_py = "wpinet._init__wpinet",
module_pkg_name = "wpinet._wpinet",
output_file = "wpinet.pc",
pkg_name = "wpinet",
install_path = "src/main/python/wpinet",
project_file = "src/main/python/pyproject.toml",
package_root = "src/main/python/wpinet/__init__.py",
)
gen_modinit_hpp(
name = "wpinet.gen_modinit_hpp",
input_dats = [x.class_name for x in WPINET_HEADER_GEN],
libname = "_wpinet",
output_file = "semiwrap_init.wpinet._wpinet.hpp",
)
run_header_gen(
name = "wpinet",
casters_pickle = "wpinet.casters.pkl",
header_gen_config = WPINET_HEADER_GEN,
trampoline_subpath = "src/main/python/wpinet",
deps = header_to_dat_deps,
local_native_libraries = [
"//wpinet:robotpy-native-wpinet.copy_headers",
"//wpiutil:robotpy-native-wpiutil.copy_headers",
],
)
create_pybind_library(
name = "wpinet",
install_path = "src/main/python/wpinet/",
extension_name = "_wpinet",
generated_srcs = [":wpinet.generated_srcs"],
semiwrap_header = [":wpinet.gen_modinit_hpp"],
deps = [
":wpinet.tmpl_hdrs",
":wpinet.trampoline_hdrs",
"//wpinet:wpinet",
"//wpiutil:wpiutil",
"//wpiutil:wpiutil_pybind_library",
],
dynamic_deps = [
"//wpinet:shared/wpinet",
"//wpiutil:shared/wpiutil",
],
extra_hdrs = extra_hdrs,
extra_srcs = srcs,
includes = includes,
)
native.filegroup(
name = "wpinet.generated_files",
srcs = [
"wpinet.gen_modinit_hpp.gen",
"wpinet.header_gen_files",
"wpinet.gen_pkgconf",
"wpinet.gen_lib_init",
],
tags = ["manual", "robotpy"],
)
def define_pybind_library(name, pkgcfgs = []):
# Helper used to generate all files with one target.
native.filegroup(
name = "{}.generated_files".format(name),
srcs = [
"wpinet.generated_files",
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Files that will be included in the wheel as data deps
native.filegroup(
name = "{}.generated_pkgcfg_files".format(name),
srcs = [
"src/main/python/wpinet/wpinet.pc",
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Contains all of the non-python files that need to be included in the wheel
native.filegroup(
name = "{}.extra_files".format(name),
srcs = native.glob(["src/main/python/wpinet/**"], exclude = ["src/main/python/wpinet/**/*.py"], allow_empty = True),
tags = ["manual", "robotpy"],
)
robotpy_library(
name = name,
srcs = native.glob(["src/main/python/wpinet/**/*.py"]) + [
"src/main/python/wpinet/_init__wpinet.py",
],
data = [
"{}.generated_pkgcfg_files".format(name),
"{}.extra_files".format(name),
":src/main/python/wpinet/_wpinet",
":wpinet.trampoline_hdr_files",
],
imports = ["src/main/python"],
deps = [
"//wpinet:robotpy-native-wpinet",
"//wpiutil:robotpy-wpiutil",
],
visibility = ["//visibility:public"],
)
update_yaml_files(
name = "{}-update-yaml".format(name),
yaml_output_directory = "src/main/python/semiwrap",
extra_hdrs = native.glob(["src/main/python/**/*.h"], allow_empty = True) + [
"//wpinet:robotpy-native-wpinet.copy_headers",
],
package_root_file = "src/main/python/wpinet/__init__.py",
pkgcfgs = pkgcfgs,
pyproject_toml = "src/main/python/pyproject.toml",
yaml_files = native.glob(["src/main/python/semiwrap/**"]),
)
scan_headers(
name = "{}-scan-headers".format(name),
extra_hdrs = native.glob(["src/main/python/**/*.h"], allow_empty = True) + [
"//wpinet:robotpy-native-wpinet.copy_headers",
],
package_root_file = "src/main/python/wpinet/__init__.py",
pkgcfgs = pkgcfgs,
pyproject_toml = "src/main/python/pyproject.toml",
)

View File

@@ -0,0 +1,7 @@
robotpy-wpinet
==============
Python wrappers for WPILib's wpinet library.
* Installation instructions can be found in the [RobotPy documentation](https://robotpy.readthedocs.io/en/latest/getting_started.html)
* Documentation can be found at [readthedocs](https://robotpy.readthedocs.io/projects/wpinet/en/stable/api.html)

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
import argparse
import time
import wpinet
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("port", type=int, help="Local port number")
parser.add_argument("remoteHost", help="Remote IP address / DNS name")
parser.add_argument("remotePort", type=int, help="remote port number")
args = parser.parse_args()
wpinet.PortForwarder.getInstance().add(args.port, args.remoteHost, args.remotePort)
while True:
time.sleep(1)

View File

@@ -0,0 +1,39 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatchling",
"hatch-nativelib~=0.2.0",
"hatch-robotpy~=0.2.1",
"robotpy-native-wpiutil==2027.0.0a2",
]
[project]
name = "robotpy-native-wpinet"
version = "2027.0.0a2"
description = "WPILib Networking Library"
license = "BSD-3-Clause"
dependencies = [
"robotpy-native-wpiutil==2027.0.0a2",
]
[tool.hatch.build.targets.wheel]
packages = ["src/native"]
[[tool.hatch.build.hooks.robotpy.maven_lib_download]]
artifact_id = "wpinet-cpp"
group_id = "edu.wpi.first.wpinet"
repo_url = "https://frcmaven.wpi.edu/artifactory/release-2027"
version = "2027.0.0-alpha-2"
extract_to = "src/native/wpinet"
libs = ["wpinet"]
[[tool.hatch.build.hooks.nativelib.pcfile]]
pcfile = "src/native/wpinet/robotpy-native-wpinet.pc"
name = "wpinet"
includedir = "src/native/wpinet/include"
libdir = "src/native/wpinet/lib"
shared_libraries = ["wpinet"]
requires = ["robotpy-native-wpiutil"]

View File

@@ -0,0 +1,54 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"semiwrap~=0.1.7",
"hatch-meson~=0.1.0b2",
"hatchling",
"robotpy-native-wpinet==2027.0.0a2",
"robotpy-wpiutil==2027.0.0a2"
]
[project]
name = "robotpy-wpinet"
version = "2027.0.0a2"
description = "Binary wrapper for FRC wpinet library"
authors = [
{name = "RobotPy Development Team", email = "robotpy@googlegroups.com"},
]
license = "BSD-3-Clause"
dependencies = [
"robotpy-native-wpinet==2027.0.0a2",
"robotpy-wpiutil==2027.0.0a2"
]
[project.urls]
"Source code" = "https://github.com/robotpy/mostrobotpy"
[tool.hatch.build.hooks.robotpy]
version_file = "wpinet/version.py"
[tool.hatch.build.hooks.semiwrap]
[tool.hatch.build.hooks.meson]
[tool.hatch.build.targets.wheel]
packages = ["wpinet"]
[tool.semiwrap]
update_init = [
"wpinet"
]
# we don't wrap anything here
scan_headers_ignore = ["*"]
[tool.semiwrap.extension_modules."wpinet._wpinet"]
name = "wpinet"
wraps = ["robotpy-native-wpinet"]
depends = ["wpiutil"]
[tool.semiwrap.extension_modules."wpinet._wpinet".headers]
# wpinet
PortForwarder = "wpinet/PortForwarder.h"
WebServer = "wpinet/WebServer.h"

View File

@@ -0,0 +1,8 @@
classes:
wpi::PortForwarder:
nodelete: true
methods:
GetInstance:
return_value_policy: reference
Add:
Remove:

View File

@@ -0,0 +1,8 @@
classes:
wpi::WebServer:
nodelete: true
methods:
GetInstance:
return_value_policy: reference
Start:
Stop:

View File

@@ -0,0 +1,7 @@
from . import _init__wpinet
# autogenerated by 'semiwrap create-imports wpinet wpinet._wpinet'
from ._wpinet import PortForwarder, WebServer
__all__ = ["PortForwarder", "WebServer"]

View File

View File

@@ -0,0 +1,4 @@
#include <semiwrap_init.wpinet._wpinet.hpp>
SEMIWRAP_PYBIND11_MODULE(m) { initWrapper(m); }

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env python3
import os
from os.path import abspath, dirname
import sys
import subprocess
if __name__ == "__main__":
root = abspath(dirname(__file__))
os.chdir(root)
subprocess.check_call([sys.executable, "-m", "pytest"])

View File

@@ -0,0 +1,5 @@
import wpinet
def test_existance():
pass

View File

@@ -20,6 +20,9 @@ modifiableFileExclude {
generatedFileExclude {
src/main/native/thirdparty/
src/main/python/
src/test/python/
src/main/native/include/wpi/fs\.h$
src/main/native/include/wpi/FastQueue\.h$
src/main/native/cpp/fs\.cpp$

View File

@@ -2,13 +2,18 @@ load("@allwpilib_pip_deps//:requirements.bzl", "requirement")
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
load("@rules_java//java:defs.bzl", "java_binary")
load("@rules_python//python:defs.bzl", "py_binary")
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("//shared/bazel/rules:cc_rules.bzl", "third_party_cc_lib_helper", "wpilib_cc_library", "wpilib_cc_shared_library", "wpilib_cc_static_library")
load("//shared/bazel/rules:java_rules.bzl", "wpilib_java_junit5_test")
load("//shared/bazel/rules:jni_rules.bzl", "wpilib_jni_cc_library", "wpilib_jni_java_library")
load("//shared/bazel/rules:packaging.bzl", "package_default_jni_project")
load("//shared/bazel/rules/gen:gen-resources.bzl", "generate_resources")
load("//shared/bazel/rules/robotpy:build_info_gen.bzl", "generate_robotpy_native_wrapper_build_info", "generate_robotpy_pybind_build_info")
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "create_pybind_library")
load("//shared/bazel/rules/robotpy:pytest_util.bzl", "robotpy_py_test")
load("//wpiutil:generate.bzl", "generate_wpiutil")
load("//wpiutil:robotpy_native_build_info.bzl", "define_native_wrapper")
load("//wpiutil:robotpy_pybind_build_info.bzl", "define_pybind_library", "publish_library_casters", "wpiutil_extension")
filegroup(
name = "doxygen-files",
@@ -315,6 +320,95 @@ java_binary(
],
)
generate_robotpy_native_wrapper_build_info(
name = "robotpy-native-wpiutil-generator",
pyproject_toml = "src/main/python/native-pyproject.toml",
third_party_dirs = [
"argparse",
"debugging",
"expected",
"fmtlib",
"json",
"llvm",
"mpack",
"nanopb",
"sigslot",
"upb",
],
)
define_native_wrapper(
name = "robotpy-native-wpiutil",
)
PYBIND_PKGCFG_DEPS = ["//wpiutil:native/wpiutil/robotpy-native-wpiutil.pc"]
generate_robotpy_pybind_build_info(
name = "robotpy-wpiutil-generator",
additional_srcs = ["src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h"] + [":robotpy-native-wpiutil.copy_headers"],
package_root_file = "src/main/python/wpiutil/__init__.py",
pkgcfgs = PYBIND_PKGCFG_DEPS,
pyproject_toml = "src/main/python/pyproject.toml",
yaml_files = glob(["src/main/python/semiwrap/*.yml"]),
)
publish_library_casters()
wpiutil_extension(
srcs = glob(["src/main/python/wpiutil/src/**/*.cpp"]),
extra_hdrs = glob([
"src/main/python/wpiutil/src/type_casters/*.h",
"src/main/python/wpiutil/src/wpistruct/*.h",
]),
header_to_dat_deps = ["src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h"],
includes = [
"src/main/python/wpiutil/",
"src/main/python/wpiutil/src/type_casters",
"src/main/python/wpiutil/src/wpistruct",
],
)
define_pybind_library(
name = "robotpy-wpiutil",
pkgcfgs = PYBIND_PKGCFG_DEPS,
)
create_pybind_library(
name = "module",
dynamic_deps = [
":shared/wpiutil",
],
extension_name = "module",
extra_srcs = glob(["src/test/python/cpp/wpiutil_test/*.cpp"]),
install_path = "src/test/python/cpp/wpiutil_test/",
deps = [
":wpiutil_pybind_library",
],
)
py_library(
name = "wpiutil_test",
srcs = glob(["src/test/python/cpp/wpiutil_test/*.py"]),
data = [
":src/test/python/cpp/wpiutil_test/module",
],
imports = ["src/test/python/cpp"],
visibility = ["//visibility:public"],
)
robotpy_py_test(
"wpiutil_tests",
srcs = glob(
["src/test/python/**/*.py"],
exclude = ["src/test/python/cpp/**"],
),
deps = [
":robotpy-wpiutil",
":wpiutil_test",
requirement("pytest"),
],
)
package_default_jni_project(
name = "wpiutil",
maven_artifact_name = "wpiutil-cpp",

53
wpiutil/robotpy_native_build_info.bzl generated Normal file
View File

@@ -0,0 +1,53 @@
# THIS FILE IS AUTO GENERATED
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "native_wrappery_library")
def define_native_wrapper(name, pyproject_toml = None):
copy_to_directory(
name = "{}.copy_headers".format(name),
srcs = native.glob(["src/main/native/include/**"]) + native.glob(["src/generated/main/native/include/**"], allow_empty = True) + native.glob([
"src/main/native/thirdparty/argparse/include/**",
"src/main/native/thirdparty/debugging/include/**",
"src/main/native/thirdparty/expected/include/**",
"src/main/native/thirdparty/fmtlib/include/**",
"src/main/native/thirdparty/json/include/**",
"src/main/native/thirdparty/llvm/include/**",
"src/main/native/thirdparty/mpack/include/**",
"src/main/native/thirdparty/nanopb/include/**",
"src/main/native/thirdparty/sigslot/include/**",
"src/main/native/thirdparty/upb/include/**",
]),
out = "native/wpiutil/include",
root_paths = ["src/main/native/include/"],
replace_prefixes = {
"wpiutil/src/generated/main/native/include": "",
"wpiutil/src/main/native/include": "",
"wpiutil/src/main/native/thirdparty/argparse/include": "",
"wpiutil/src/main/native/thirdparty/debugging/include": "",
"wpiutil/src/main/native/thirdparty/expected/include": "",
"wpiutil/src/main/native/thirdparty/fmtlib/include": "",
"wpiutil/src/main/native/thirdparty/json/include": "",
"wpiutil/src/main/native/thirdparty/llvm/include": "",
"wpiutil/src/main/native/thirdparty/mpack/include": "",
"wpiutil/src/main/native/thirdparty/nanopb/include": "",
"wpiutil/src/main/native/thirdparty/sigslot/include": "",
"wpiutil/src/main/native/thirdparty/upb/include": "",
},
verbose = False,
visibility = ["//visibility:public"],
)
native_wrappery_library(
name = name,
pyproject_toml = pyproject_toml or "src/main/python/native-pyproject.toml",
libinit_file = "native/wpiutil/_init_robotpy_native_wpiutil.py",
pc_file = "native/wpiutil/robotpy-native-wpiutil.pc",
pc_deps = [
],
deps = [
],
headers = "{}.copy_headers".format(name),
native_shared_library = "shared/wpiutil",
install_path = "native/wpiutil/",
)

234
wpiutil/robotpy_pybind_build_info.bzl generated Normal file
View File

@@ -0,0 +1,234 @@
# THIS FILE IS AUTO GENERATED
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("//shared/bazel/rules/robotpy:pybind_rules.bzl", "create_pybind_library", "robotpy_library")
load("//shared/bazel/rules/robotpy:semiwrap_helpers.bzl", "gen_libinit", "gen_modinit_hpp", "gen_pkgconf", "publish_casters", "resolve_casters", "run_header_gen")
load("//shared/bazel/rules/robotpy:semiwrap_tool_helpers.bzl", "scan_headers", "update_yaml_files")
def wpiutil_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includes = [], extra_pyi_deps = []):
WPIUTIL_HEADER_GEN = [
struct(
class_name = "StackTrace",
yml_file = "semiwrap/StackTrace.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/StackTrace.h",
tmpl_class_names = [],
trampolines = [],
),
struct(
class_name = "Synchronization",
yml_file = "semiwrap/Synchronization.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/Synchronization.h",
tmpl_class_names = [],
trampolines = [],
),
struct(
class_name = "RawFrame",
yml_file = "semiwrap/RawFrame.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/RawFrame.h",
tmpl_class_names = [],
trampolines = [],
),
struct(
class_name = "Sendable",
yml_file = "semiwrap/Sendable.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/sendable/Sendable.h",
tmpl_class_names = [],
trampolines = [
("wpi::Sendable", "wpi__Sendable.hpp"),
],
),
struct(
class_name = "SendableBuilder",
yml_file = "semiwrap/SendableBuilder.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/sendable/SendableBuilder.h",
tmpl_class_names = [],
trampolines = [
("wpi::SendableBuilder", "wpi__SendableBuilder.hpp"),
],
),
struct(
class_name = "SendableRegistry",
yml_file = "semiwrap/SendableRegistry.yml",
header_root = "$(execpath :robotpy-native-wpiutil.copy_headers)",
header_file = "$(execpath :robotpy-native-wpiutil.copy_headers)/wpi/sendable/SendableRegistry.h",
tmpl_class_names = [],
trampolines = [
("wpi::SendableRegistry", "wpi__SendableRegistry.hpp"),
],
),
struct(
class_name = "WPyStruct",
yml_file = "semiwrap/WPyStruct.yml",
header_root = "wpiutil/src/main/python/wpiutil",
header_file = "wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h",
tmpl_class_names = [],
trampolines = [],
),
]
resolve_casters(
name = "wpiutil.resolve_casters",
caster_deps = [":src/main/python/wpiutil/wpiutil-casters.pybind11.json"],
casters_pkl_file = "wpiutil.casters.pkl",
dep_file = "wpiutil.casters.d",
)
gen_libinit(
name = "wpiutil.gen_lib_init",
output_file = "src/main/python/wpiutil/_init__wpiutil.py",
modules = ["native.wpiutil._init_robotpy_native_wpiutil"],
)
gen_pkgconf(
name = "wpiutil.gen_pkgconf",
libinit_py = "wpiutil._init__wpiutil",
module_pkg_name = "wpiutil._wpiutil",
output_file = "wpiutil.pc",
pkg_name = "wpiutil",
install_path = "src/main/python/wpiutil",
project_file = "src/main/python/pyproject.toml",
package_root = "src/main/python/wpiutil/__init__.py",
)
gen_modinit_hpp(
name = "wpiutil.gen_modinit_hpp",
input_dats = [x.class_name for x in WPIUTIL_HEADER_GEN],
libname = "_wpiutil",
output_file = "semiwrap_init.wpiutil._wpiutil.hpp",
)
run_header_gen(
name = "wpiutil",
casters_pickle = "wpiutil.casters.pkl",
header_gen_config = WPIUTIL_HEADER_GEN,
trampoline_subpath = "src/main/python/wpiutil",
deps = header_to_dat_deps,
local_native_libraries = [
"//wpiutil:robotpy-native-wpiutil.copy_headers",
],
)
create_pybind_library(
name = "wpiutil",
install_path = "src/main/python/wpiutil/",
extension_name = "_wpiutil",
generated_srcs = [":wpiutil.generated_srcs"],
semiwrap_header = [":wpiutil.gen_modinit_hpp"],
deps = [
":wpiutil.tmpl_hdrs",
":wpiutil.trampoline_hdrs",
"//wpiutil:wpiutil",
"//wpiutil:wpiutil-casters",
],
dynamic_deps = [
"//wpiutil:shared/wpiutil",
],
extra_hdrs = extra_hdrs,
extra_srcs = srcs,
includes = includes,
)
native.filegroup(
name = "wpiutil.generated_files",
srcs = [
"wpiutil.gen_modinit_hpp.gen",
"wpiutil.header_gen_files",
"wpiutil.gen_pkgconf",
"wpiutil.gen_lib_init",
],
tags = ["manual", "robotpy"],
)
def publish_library_casters():
publish_casters(
name = "publish_casters",
caster_name = "wpiutil-casters",
output_json = "src/main/python/wpiutil/wpiutil-casters.pybind11.json",
output_pc = "src/main/python/wpiutil/wpiutil-casters.pc",
project_config = "src/main/python/pyproject.toml",
package_root = "src/main/python/wpiutil/__init__.py",
typecasters_srcs = native.glob(["src/main/python/wpiutil/src/type_casters/**", "src/main/python/wpiutil/src/wpistruct/**"]),
)
cc_library(
name = "wpiutil-casters",
hdrs = native.glob(["src/main/python/wpiutil/src/type_casters/*.h", "src/main/python/wpiutil/src/wpistruct/*.h"]),
includes = ["src/main/python/wpiutil/src/type_casters", "src/main/python/wpiutil/src/wpistruct"],
visibility = ["//visibility:public"],
tags = ["robotpy"],
)
def define_pybind_library(name, pkgcfgs = []):
# Helper used to generate all files with one target.
native.filegroup(
name = "{}.generated_files".format(name),
srcs = [
"wpiutil.generated_files",
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Files that will be included in the wheel as data deps
native.filegroup(
name = "{}.generated_pkgcfg_files".format(name),
srcs = [
"src/main/python/wpiutil/wpiutil.pc",
"src/main/python/wpiutil/wpiutil-casters.pc",
"src/main/python/wpiutil/wpiutil-casters.pybind11.json",
],
tags = ["manual", "robotpy"],
visibility = ["//visibility:public"],
)
# Contains all of the non-python files that need to be included in the wheel
native.filegroup(
name = "{}.extra_files".format(name),
srcs = native.glob(["src/main/python/wpiutil/**"], exclude = ["src/main/python/wpiutil/**/*.py"], allow_empty = True),
tags = ["manual", "robotpy"],
)
robotpy_library(
name = name,
srcs = native.glob(["src/main/python/wpiutil/**/*.py"]) + [
"src/main/python/wpiutil/_init__wpiutil.py",
],
data = [
"{}.generated_pkgcfg_files".format(name),
"{}.extra_files".format(name),
":src/main/python/wpiutil/_wpiutil",
":wpiutil.trampoline_hdr_files",
],
imports = ["src/main/python"],
deps = [
"//wpiutil:robotpy-native-wpiutil",
],
visibility = ["//visibility:public"],
)
update_yaml_files(
name = "{}-update-yaml".format(name),
yaml_output_directory = "src/main/python/semiwrap",
extra_hdrs = native.glob(["src/main/python/**/*.h"], allow_empty = True) + [
"//wpiutil:robotpy-native-wpiutil.copy_headers",
],
package_root_file = "src/main/python/wpiutil/__init__.py",
pkgcfgs = pkgcfgs,
pyproject_toml = "src/main/python/pyproject.toml",
yaml_files = native.glob(["src/main/python/semiwrap/**"]),
)
scan_headers(
name = "{}-scan-headers".format(name),
extra_hdrs = native.glob(["src/main/python/**/*.h"], allow_empty = True) + [
"//wpiutil:robotpy-native-wpiutil.copy_headers",
],
package_root_file = "src/main/python/wpiutil/__init__.py",
pkgcfgs = pkgcfgs,
pyproject_toml = "src/main/python/pyproject.toml",
)

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
#
# Copyright (c) FIRST and other WPILib contributors.
# Open Source Software; you can modify and/or share it under the terms of
# the WPILib BSD license file in the root directory of this project.
import argparse
import datetime
from wpiutil.log import DataLogReader
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("infile")
args = parser.parse_args()
reader = DataLogReader(args.infile)
entries = {}
for record in reader:
timestamp = record.getTimestamp() / 1000000
if record.isStart():
try:
data = record.getStartData()
print(f"{data} [{timestamp}]")
if data.entry in entries:
print("...DUPLICATE entry ID, overriding")
entries[data.entry] = data
except TypeError as e:
print("Start(INVALID)")
elif record.isFinish():
try:
entry = record.getFinishEntry()
print(f"Finish({entry}) [{timestamp}]")
if entry not in entries:
print("...ID not found")
else:
del entries[entry]
except TypeError as e:
print("Finish(INVALID)")
elif record.isSetMetadata():
try:
data = record.getSetMetadataData()
print(f"{data} [{timestamp}]")
if data.entry not in entries:
print("...ID not found")
except TypeError as e:
print("SetMetadata(INVALID)")
elif record.isControl():
print("Unrecognized control record")
else:
print(f"Data({record.getEntry()}, size={record.getSize()}) ", end="")
entry = entries.get(record.getEntry(), None)
if entry is None:
print("<ID not found>")
continue
print(f"<name='{entry.name}', type='{entry.type}'> [{timestamp}]")
try:
# handle systemTime specially
if entry.name == "systemTime" and entry.type == "int64":
dt = datetime.fromtimestamp(record.getInteger() / 1000000)
print(" {:%Y-%m-%d %H:%M:%S.%f}".format(dt))
continue
if entry.type == "double":
print(f" {record.getDouble()}")
elif entry.type == "int64":
print(f" {record.getInteger()}")
elif entry.type == "string" or entry.type == "json":
print(f" '{record.getString()}'")
elif entry.type == "boolean":
print(f" {record.getBoolean()}")
elif entry.type == "boolean[]":
arr = record.getBooleanArray()
print(f" {arr}")
elif entry.type == "double[]":
arr = record.getDoubleArray()
print(f" {arr}")
elif entry.type == "float[]":
arr = record.getFloatArray()
print(f" {arr}")
elif entry.type == "int64[]":
arr = record.getIntegerArray()
print(f" {arr}")
elif entry.type == "string[]":
arr = record.getStringArray()
print(f" {arr}")
elif entry.type == "raw":
print(f" {record.getRaw()}")
except TypeError as e:
print(" invalid", e)

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import argparse
import pathlib
from wpiutil.log import DataLog, BooleanLogEntry, StringArrayLogEntry, RawLogEntry
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("out", type=pathlib.Path)
args = parser.parse_args()
if args.out.is_dir():
datalog = DataLog(str(args.out))
else:
datalog = DataLog(str(args.out.parent), args.out.name)
bools = BooleanLogEntry(datalog, "/bools")
bools.append(True)
bools.append(False)
strings = StringArrayLogEntry(datalog, "/strings")
strings.append(["a", "b", "c"])
strings.append(["d", "e", "f"])
raw = RawLogEntry(datalog, "/raws")
raw.append(b"\x01\x02\x03")
raw.append(b"\x04\x05\x06")

View File

@@ -0,0 +1,52 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatchling",
"hatch-nativelib~=0.2.0",
"hatch-robotpy~=0.2.1",
]
[project]
name = "robotpy-native-wpiutil"
version = "2027.0.0a2"
description = "WPILib Utility Library"
license = "BSD-3-Clause"
dependencies = [
"msvc-runtime>=14.42.34433; platform_system == 'Windows'"
]
[tool.hatch.build.targets.wheel]
packages = ["src/native"]
[[tool.hatch.build.hooks.robotpy.maven_lib_download]]
artifact_id = "wpiutil-cpp"
group_id = "edu.wpi.first.wpiutil"
repo_url = "https://frcmaven.wpi.edu/artifactory/release-2027"
version = "2027.0.0-alpha-2"
extract_to = "src/native/wpiutil"
libs = ["wpiutil"]
[[tool.hatch.build.hooks.nativelib.pcfile]]
pcfile = "src/native/wpiutil/robotpy-native-wpiutil.pc"
name = "wpiutil"
includedir = "src/native/wpiutil/include"
libdir = "src/native/wpiutil/lib"
shared_libraries = ["wpiutil"]
enable_if = "platform_system != 'Windows'"
[[tool.hatch.build.hooks.nativelib.pcfile]]
pcfile = "src/native/wpiutil/robotpy-native-wpiutil.pc"
name = "wpiutil"
includedir = "src/native/wpiutil/include"
libdir = "src/native/wpiutil/lib"
shared_libraries = ["wpiutil"]
# All wpilib projects require this flag
extra_cflags = "/Zc:preprocessor"
enable_if = "platform_system == 'Windows'"

View File

@@ -0,0 +1,121 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"semiwrap~=0.1.7",
"hatch-meson~=0.1.0b2",
"hatch-robotpy~=0.2.1",
"hatchling",
"robotpy-native-wpiutil==2027.0.0a2",
]
[project]
name = "robotpy-wpiutil"
version = "2027.0.0a2"
description = "Binary wrapper for FRC WPIUtil library"
authors = [
{name = "RobotPy Development Team", email = "robotpy@googlegroups.com"},
]
license = "BSD-3-Clause"
dependencies = [
"robotpy-native-wpiutil==2027.0.0a2",
]
[project.urls]
"Source code" = "https://github.com/robotpy/mostrobotpy"
[tool.hatch.build.hooks.robotpy]
version_file = "wpiutil/version.py"
[tool.hatch.build.hooks.semiwrap]
[tool.hatch.build.hooks.meson]
[tool.hatch.build.targets.wheel]
packages = ["wpiutil"]
[tool.semiwrap]
update_init = [
"wpiutil",
# "wpiutil.log wpiutil._wpiutil.log",
"wpiutil.sync wpiutil._wpiutil.sync",
"wpiutil.wpistruct wpiutil._wpiutil.wpistruct",
]
scan_headers_ignore = [
"debugging.hpp",
"debugging/*",
"fmt/*",
"google/*",
"wpi/*",
"wpystruct_fns.h",
"pb.h",
"pb_common.h",
"pb_decode.h",
"pb_encode.h",
]
[tool.semiwrap.extension_modules."wpiutil._wpiutil"]
name = "wpiutil"
includes = [
"wpiutil/src/wpistruct",
]
wraps = ["robotpy-native-wpiutil"]
depends = ["wpiutil-casters"]
[tool.semiwrap.extension_modules."wpiutil._wpiutil".headers]
# wpi
StackTrace = "wpi/StackTrace.h"
Synchronization = "wpi/Synchronization.h"
RawFrame = "wpi/RawFrame.h"
# wpi/sendable
Sendable = "wpi/sendable/Sendable.h"
SendableBuilder = "wpi/sendable/SendableBuilder.h"
#SendableHelper = "wpi/sendable/SendableHelper.h"
SendableRegistry = "wpi/sendable/SendableRegistry.h"
WPyStruct = "src/wpistruct/wpystruct_fns.h"
[tool.semiwrap.export_type_casters.wpiutil-casters]
pypackage = "wpiutil"
includedir = [
"wpiutil/src/type_casters",
"wpiutil/src/wpistruct",
]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_array_type_caster.h"
types = ["wpi::array"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_json_type_caster.h"
types = ["wpi::json"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_span_type_caster.h"
types = ["std::span"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_smallset_type_caster.h"
types = ["wpi::SmallSet"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_smallvector_type_caster.h"
types = ["wpi::SmallVector"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_smallvectorimpl_type_caster.h"
types = ["wpi::SmallVectorImpl"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_string_map_caster.h"
types = ["wpi::StringMap"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpi_ct_string_type_caster.h"
types = ["wpi::ct_string"]
[[tool.semiwrap.export_type_casters.wpiutil-casters.headers]]
header = "wpystruct.h"
types = ["WPyStruct"]

View File

@@ -0,0 +1,7 @@
defaults:
ignore: true
enums:
WPI_TimestampSource:
value_prefix: WPI_TIMESRC
rename: TimestampSource

View File

@@ -0,0 +1,12 @@
extra_includes:
- wpi/sendable/SendableBuilder.h
classes:
wpi::Sendable:
methods:
InitSendable:
virtual_xform: |-
[&](py::function fn) {
auto builderHandle = py::cast(builder, py::return_value_policy::reference);
fn(builderHandle);
}

View File

@@ -0,0 +1,40 @@
classes:
wpi::SendableBuilder:
enums:
BackendKind:
methods:
SetSmartDashboardType:
SetActuator:
AddBooleanProperty:
PublishConstBoolean:
AddIntegerProperty:
PublishConstInteger:
AddFloatProperty:
PublishConstFloat:
AddDoubleProperty:
PublishConstDouble:
AddStringProperty:
PublishConstString:
AddBooleanArrayProperty:
PublishConstBooleanArray:
AddIntegerArrayProperty:
PublishConstIntegerArray:
AddFloatArrayProperty:
PublishConstFloatArray:
AddDoubleArrayProperty:
PublishConstDoubleArray:
AddStringArrayProperty:
PublishConstStringArray:
AddRawProperty:
PublishConstRaw:
AddSmallStringProperty:
AddSmallBooleanArrayProperty:
AddSmallIntegerArrayProperty:
AddSmallFloatArrayProperty:
AddSmallDoubleArrayProperty:
AddSmallStringArrayProperty:
AddSmallRawProperty:
GetBackendKind:
IsPublished:
Update:
ClearProperties:

View File

@@ -0,0 +1,54 @@
extra_includes:
- wpi/sendable/Sendable.h
- wpi/sendable/SendableBuilder.h
classes:
wpi::SendableRegistry:
nodelete: true
methods:
Add:
overloads:
Sendable*, std::string_view:
keepalive:
- [1, 2]
Sendable*, std::string_view, int:
keepalive:
- [1, 2]
Sendable*, std::string_view, int, int:
keepalive:
- [1, 2]
Sendable*, std::string_view, std::string_view:
keepalive:
- [1, 2]
AddChild:
overloads:
Sendable*, Sendable*:
keepalive:
- [1, 2]
- [2, 3]
Sendable*, void*:
ignore: true
Remove:
Move:
ignore: true
Contains:
GetName:
SetName:
overloads:
Sendable*, std::string_view:
Sendable*, std::string_view, int:
Sendable*, std::string_view, int, int:
Sendable*, std::string_view, std::string_view:
GetSubsystem:
SetSubsystem:
GetDataHandle:
ignore: true
SetData:
ignore: true
GetData:
ignore: true
GetUniqueId:
GetSendable:
Publish:
Update:
EnsureInitialized:

View File

@@ -0,0 +1,5 @@
functions:
GetStackTrace:
GetStackTraceDefault:
SetGetStackTraceImpl:
ignore: true

View File

@@ -0,0 +1,59 @@
defaults:
ignore: true
subpackage: sync
extra_includes:
- pybind11/stl.h
functions:
CreateEvent:
DestroyEvent:
SetEvent:
ResetEvent:
CreateSemaphore:
DestroySemaphore:
ReleaseSemaphore:
param_override:
prevCount:
default: "0"
WaitForObject:
overloads:
WPI_Handle:
WPI_Handle, double, bool*:
WaitForObjects:
overloads:
std::span<const WPI_Handle>, std::span<WPI_Handle>:
param_override:
signaled:
ignore: true
cpp_code: |
[](std::span<const WPI_Handle> handles) {
py::gil_scoped_release release;
std::vector<WPI_Handle> signaled(handles.size());
auto result = wpi::WaitForObjects(handles, signaled);
signaled.resize(result.size());
return signaled;
}
std::initializer_list<WPI_Handle>, std::span<WPI_Handle>:
ignore: true
std::span<const WPI_Handle>, std::span<WPI_Handle>, double, bool*:
param_override:
signaled:
ignore: true
timedOut:
ignore: true
cpp_code: |
[](std::span<const WPI_Handle> handles, double timeout) {
py::gil_scoped_release release;
std::vector<WPI_Handle> signaled(handles.size());
bool timedOut = false;
auto result = wpi::WaitForObjects(handles, signaled, timeout, &timedOut);
signaled.resize(result.size());
return std::make_tuple(signaled, timedOut);
}
std::initializer_list<WPI_Handle>, std::span<WPI_Handle>, double, bool*:
ignore: true
CreateSignalObject:
SetSignalObject:
ResetSignalObject:
DestroySignalObject:

View File

@@ -0,0 +1,22 @@
defaults:
subpackage: wpistruct
functions:
forEachNested:
no_release_gil: true
getTypeName:
no_release_gil: true
getSchema:
no_release_gil: true
getSize:
no_release_gil: true
pack:
no_release_gil: true
packArray:
no_release_gil: true
packInto:
no_release_gil: true
unpack:
no_release_gil: true
unpackArray:
no_release_gil: true

View File

@@ -0,0 +1,28 @@
from . import _init__wpiutil
# autogenerated by 'semiwrap create-imports wpiutil wpiutil._wpiutil'
from ._wpiutil import (
Sendable,
SendableBuilder,
SendableRegistry,
TimestampSource,
getStackTrace,
getStackTraceDefault,
)
__all__ = [
"Sendable",
"SendableBuilder",
"SendableRegistry",
"TimestampSource",
"getStackTrace",
"getStackTraceDefault",
]
# Imported for side effects only
from . import _stacktrace
# Type alias
import typing
json = typing.Union[None, bool, int, float, str, typing.List, typing.Dict]

View File

@@ -0,0 +1,27 @@
from traceback import extract_stack, format_list
from ._wpiutil import getStackTraceDefault, _setup_stack_trace_hook
from os.path import join
_start_py = join("wpilib", "_impl", "start.py")
def _stack_trace_hook(offset: int) -> str:
# note: this implementation ignores offset because it's not
# actually meaningful when crossing the python/C++ boundary
stack = extract_stack()[:-1]
if not stack:
return "\tat <no python frames>\n" + getStackTraceDefault(offset)
# filter out any frames before start.py (except for one of them) to
# make stack frames more useful for users
for i in range(len(stack) - 1, 0, -1):
if stack[i].filename.endswith(_start_py):
stack = stack[i:]
break
trace = format_list(stack)
return "\n".join(trace)
_setup_stack_trace_hook(_stack_trace_hook)

View File

View File

@@ -0,0 +1,41 @@
#include <semiwrap_init.wpiutil._wpiutil.hpp>
void setup_stack_trace_hook(py::object fn);
void cleanup_stack_trace_hook();
void setup_safethread_gil();
void cleanup_safethread_gil();
#ifndef __FRC_SYSTEMCORE__
namespace wpi::impl {
void ResetSendableRegistry();
} // namespace wpi::impl
void cleanup_sendable_registry() {
py::gil_scoped_release unlock;
wpi::impl::ResetSendableRegistry();
}
#else
void cleanup_sendable_registry() {}
#endif
SEMIWRAP_PYBIND11_MODULE(m) {
initWrapper(m);
static int unused;
py::capsule cleanup(&unused, [](void *) {
cleanup_sendable_registry();
cleanup_stack_trace_hook();
cleanup_safethread_gil();
});
setup_safethread_gil();
m.def("_setup_stack_trace_hook", &setup_stack_trace_hook);
m.add_object("_st_cleanup", cleanup);
}

View File

@@ -0,0 +1,65 @@
#include <atomic>
#include <gilsafe_object.h>
#include <semiwrap.h>
using OnThreadStartFn = void *(*)();
using OnThreadEndFn = void (*)(void *);
namespace wpi::impl {
void SetSafeThreadNotifiers(OnThreadStartFn OnStart, OnThreadEndFn OnEnd);
}
struct SafeThreadState {
py::gil_scoped_acquire *acquire = nullptr;
py::gil_scoped_release *release = nullptr;
};
std::atomic<bool> g_gilstate_managed = false;
void *on_safe_thread_start() {
if (Py_IsFinalizing() // python is shutting down
|| !g_gilstate_managed.load() // python has shutdown)
) {
return nullptr;
}
auto *st = new SafeThreadState;
// acquires the GIL and creates pybind11's thread state for this thread
st->acquire = new py::gil_scoped_acquire;
// releases the GIL so the thread can start without it
st->release = new py::gil_scoped_release;
return st;
}
void on_safe_thread_end(void *opaque) {
// on entry, GIL should not be acquired
// don't cleanup if it's unsafe to do so. Several possibilities here:
if (!opaque // internal error?
|| Py_IsFinalizing() // python is shutting down
|| !g_gilstate_managed.load() // python has shutdown
) {
return;
}
auto *st = (SafeThreadState *)opaque;
delete st->release; // causes GIL to be acquired
delete st->acquire; // causes GIL to be released and thread state deleted
delete st;
}
void setup_safethread_gil() {
g_gilstate_managed = true;
// atexit handlers get called before the interpreter finalizes -- so
// we disable on_safe_thread_end before finalizing starts
auto atexit = py::module_::import("atexit");
atexit.attr("register")(
py::cpp_function([]() { g_gilstate_managed = false; }));
wpi::impl::SetSafeThreadNotifiers(on_safe_thread_start, on_safe_thread_end);
}
void cleanup_safethread_gil() { g_gilstate_managed = false; }

View File

@@ -0,0 +1,45 @@
#include <semiwrap.h>
#include <wpi/StackTrace.h>
py::object &get_hook_ref() {
static py::object hook;
return hook;
}
std::string final_py_stack_trace_hook(int offset) {
std::string msg = "\tat <python stack trace not available due to interpreter shutdown>\n";
msg += wpi::GetStackTraceDefault(offset);
return msg;
}
std::string py_stack_trace_hook(int offset) {
py::gil_scoped_acquire gil;
try {
auto &hook = get_hook_ref();
if (hook) {
return py::cast<std::string>(hook(offset));
}
} catch (py::error_already_set &e) {
e.discard_as_unraisable("wpiutil._stacktrace._stack_trace_hook");
}
return wpi::GetStackTraceDefault(offset);
}
void setup_stack_trace_hook(py::object fn) {
get_hook_ref() = fn;
wpi::SetGetStackTraceImpl(py_stack_trace_hook);
}
void cleanup_stack_trace_hook() {
wpi::SetGetStackTraceImpl(final_py_stack_trace_hook);
// release the function during interpreter shutdown
auto &hook = get_hook_ref();
if (hook) {
hook.dec_ref();
hook.release();
}
}

View File

@@ -0,0 +1,97 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/array.h>
namespace pybind11 {
namespace detail {
template <size_t N>
struct wpi_array_name_maker {
template <typename T>
static constexpr auto make(const T &t) {
return concat(t, wpi_array_name_maker<N-1>::make(t));
}
};
template <>
struct wpi_array_name_maker<1> {
template <typename T>
static constexpr auto make(const T &t) {
return t;
}
};
template <typename Type, size_t Size>
struct type_caster<wpi::array<Type, Size>> {
using value_conv = make_caster<Type>;
// Have to copy/paste PYBIND11_TYPE_CASTER implementation because wpi::array
// is not default constructable
//
// begin PYBIND11_TYPE_CASTER
protected:
wpi::array<Type, Size> value{wpi::empty_array_t{}};
// An empty tuple is pretty useless
static_assert(Size > 0, "empty array not supported");
public:
static constexpr auto name = const_name("Tuple[") + wpi_array_name_maker<Size>::make(value_conv::name) + const_name("]");
template <
typename T_,
enable_if_t<std::is_same<wpi::array<Type, Size>, remove_cv_t<T_>>::value,
int> = 0>
static handle cast(T_ *src, return_value_policy policy, handle parent) {
if (!src)
return none().release();
if (policy == return_value_policy::take_ownership) {
auto h = cast(std::move(*src), policy, parent);
delete src;
return h;
} else {
return cast(*src, policy, parent);
}
}
operator wpi::array<Type, Size> *() { return &value; }
operator wpi::array<Type, Size> &() { return value; }
operator wpi::array<Type, Size> &&() && { return std::move(value); }
template <typename T_>
using cast_op_type = pybind11::detail::movable_cast_op_type<T_>;
// end PYBIND11_TYPE_CASTER
bool load(handle src, bool convert) {
if (!isinstance<sequence>(src))
return false;
auto l = reinterpret_borrow<sequence>(src);
if (l.size() != Size)
return false;
size_t ctr = 0;
for (auto it : l) {
value_conv conv;
if (!conv.load(it, convert))
return false;
value[ctr++] = cast_op<Type &&>(std::move(conv));
}
return true;
}
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
tuple l(src.size());
size_t index = 0;
for (auto &&value : src) {
auto value_ = reinterpret_steal<object>(
value_conv::cast(forward_like<T>(value), policy, parent));
if (!value_)
return handle();
PyTuple_SET_ITEM(l.ptr(), (ssize_t)index++,
value_.release().ptr()); // steals a reference
}
return l.release();
}
};
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,61 @@
#pragma once
#include <pybind11/pybind11.h>
#include <wpi/ct_string.h>
namespace pybind11 {
namespace detail {
template <typename CharT, typename Traits, size_t N>
struct type_caster<wpi::ct_string<CharT, Traits, N>> {
using str_type = wpi::ct_string<CharT, Traits, N>;
PYBIND11_TYPE_CASTER(str_type, const_name(PYBIND11_STRING_NAME));
// TODO
bool load(handle src, bool convert) {
return false;
}
static handle cast(const str_type& src,
py::return_value_policy policy,
py::handle parent) {
const char *buffer = reinterpret_cast<const char *>(src.data());
auto nbytes = ssize_t(src.size() * sizeof(CharT));
handle s = decode_utfN(buffer, nbytes);
if (!s) {
throw error_already_set();
}
return s;
}
// copied from py::string_caster
static constexpr size_t UTF_N = 8 * sizeof(CharT);
static handle decode_utfN(const char *buffer, ssize_t nbytes) {
#if !defined(PYPY_VERSION)
return UTF_N == 8 ? PyUnicode_DecodeUTF8(buffer, nbytes, nullptr)
: UTF_N == 16 ? PyUnicode_DecodeUTF16(buffer, nbytes, nullptr, nullptr)
: PyUnicode_DecodeUTF32(buffer, nbytes, nullptr, nullptr);
#else
// PyPy segfaults when on PyUnicode_DecodeUTF16 (and possibly on PyUnicode_DecodeUTF32 as
// well), so bypass the whole thing by just passing the encoding as a string value, which
// works properly:
return PyUnicode_Decode(buffer,
nbytes,
UTF_N == 8 ? "utf-8"
: UTF_N == 16 ? "utf-16"
: "utf-32",
nullptr);
#endif
}
};
// template <typename Char, typename Traits, size_t N>
// struct type_caster<wpi::ct_string<Char, Traits, N>>
// : string_caster<wpi::ct_string<Char, Traits, N>, false> {};
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,259 @@
/***************************************************************************
* Copyright (c) 2019, Martin Renou *
* *
Copyright (c) 2019,
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
****************************************************************************/
#pragma once
#include <string>
#include <vector>
#include "wpi/json.h"
#include "pybind11/pybind11.h"
namespace py = pybind11;
namespace pyjson
{
using number_unsigned_t = uint64_t;
using number_integer_t = int64_t;
inline py::object from_json(const wpi::json& j)
{
if (j.is_null())
{
return py::none();
}
else if (j.is_boolean())
{
return py::bool_(j.get<bool>());
}
else if (j.is_number_unsigned())
{
return py::int_(j.get<number_unsigned_t>());
}
else if (j.is_number_integer())
{
return py::int_(j.get<number_integer_t>());
}
else if (j.is_number_float())
{
return py::float_(j.get<double>());
}
else if (j.is_string())
{
return py::str(j.get<std::string>());
}
else if (j.is_array())
{
py::list obj(j.size());
for (std::size_t i = 0; i < j.size(); i++)
{
obj[i] = from_json(j[i]);
}
return std::move(obj);
}
else // Object
{
py::dict obj;
for (wpi::json::const_iterator it = j.cbegin(); it != j.cend(); ++it)
{
obj[py::str(it.key())] = from_json(it.value());
}
return std::move(obj);
}
}
inline wpi::json to_json(const py::handle& obj)
{
if (obj.ptr() == nullptr || obj.is_none())
{
return nullptr;
}
if (py::isinstance<py::bool_>(obj))
{
return obj.cast<bool>();
}
if (py::isinstance<py::int_>(obj))
{
try
{
number_integer_t s = obj.cast<number_integer_t>();
if (py::int_(s).equal(obj))
{
return s;
}
}
catch (...)
{
}
try
{
number_unsigned_t u = obj.cast<number_unsigned_t>();
if (py::int_(u).equal(obj))
{
return u;
}
}
catch (...)
{
}
throw py::value_error("to_json received an integer out of range for both number_integer_t and number_unsigned_t type: " + py::repr(obj).cast<std::string>());
}
if (py::isinstance<py::float_>(obj))
{
return obj.cast<double>();
}
// if (py::isinstance<py::bytes>(obj))
// {
// py::module base64 = py::module::import("base64");
// return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast<std::string>();
// }
if (py::isinstance<py::str>(obj))
{
return obj.cast<std::string>();
}
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
{
auto out = wpi::json::array();
for (const py::handle value : obj)
{
out.push_back(to_json(value));
}
return out;
}
if (py::isinstance<py::dict>(obj))
{
auto out = wpi::json::object();
for (const py::handle key : obj)
{
if (py::isinstance<py::str>(key)) {
out[key.cast<std::string>()] = to_json(obj[key]);
} else if (py::isinstance<py::int_>(key) || py::isinstance<py::float_>(key) ||
py::isinstance<py::bool_>(key) || py::isinstance<py::none>(key)) {
// only allow the same implicit conversions python allows
out[py::str(key).cast<std::string>()] = to_json(obj[key]);
} else {
throw py::type_error("JSON keys must be str, int, float, bool, or None, not " + py::repr(key).cast<std::string>());
}
}
return out;
}
throw py::type_error("Object of type " + py::type::of(obj).attr("__name__").cast<std::string>() + " is not JSON serializable");
}
}
// nlohmann_json serializers
namespace wpi
{
#define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(json& j, const T& obj) \
{ \
j = pyjson::to_json(obj); \
} \
\
inline static T from_json(const json& j) \
{ \
return pyjson::from_json(j); \
} \
}
#define MAKE_NLJSON_SERIALIZER_ONLY(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(json& j, const T& obj) \
{ \
j = pyjson::to_json(obj); \
} \
}
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict);
MAKE_NLJSON_SERIALIZER_ONLY(py::handle);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor);
#undef MAKE_NLJSON_SERIALIZER
#undef MAKE_NLJSON_SERIALIZER_ONLY
}
// pybind11 caster
namespace pybind11
{
namespace detail
{
template <> struct type_caster<wpi::json>
{
public:
PYBIND11_TYPE_CASTER(wpi::json, _("wpiutil.json"));
bool load(handle src, bool convert)
{
// TODO: raising errors gives the user informative error messages,
// but at the expense of proper argument parsing..
// try
// {
value = pyjson::to_json(src);
return true;
// }
// catch (...)
// {
// return false;
// }
}
static handle cast(wpi::json src, return_value_policy /* policy */, handle /* parent */)
{
object obj = pyjson::from_json(src);
return obj.release();
}
};
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/SmallSet.h>
namespace pybind11
{
namespace detail
{
template <typename Type, unsigned Size> struct type_caster<wpi::SmallSet<Type, Size>>
: set_caster<wpi::SmallSet<Type, Size>, Type> { };
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,18 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/SmallVector.h>
namespace pybind11
{
namespace detail
{
template <typename Type, unsigned Size> struct type_caster<wpi::SmallVector<Type, Size>>
: list_caster<wpi::SmallVector<Type, Size>, Type> { };
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,71 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/SmallVector.h>
namespace pybind11
{
namespace detail
{
template <typename Type> struct type_caster<wpi::SmallVectorImpl<Type>> {
using value_conv = make_caster<Type>;
// Have to copy/paste PYBIND11_TYPE_CASTER implementation because SmallVectorImpl
// is not default constructable
//
// begin PYBIND11_TYPE_CASTER
protected:
wpi::SmallVector<Type, 16> value;
public:
static constexpr auto name = _("List[") + value_conv::name + _("]");
template <typename T_, enable_if_t<std::is_same<wpi::SmallVectorImpl<Type>, remove_cv_t<T_>>::value, int> = 0>
static handle cast(T_ *src, return_value_policy policy, handle parent) {
if (!src) return none().release();
if (policy == return_value_policy::take_ownership) {
auto h = cast(std::move(*src), policy, parent); delete src; return h;
} else {
return cast(*src, policy, parent);
}
}
operator wpi::SmallVectorImpl<Type>*() { return &value; }
operator wpi::SmallVectorImpl<Type>&() { return value; }
operator wpi::SmallVectorImpl<Type>&&() && { return std::move(value); }
template <typename T_> using cast_op_type = pybind11::detail::movable_cast_op_type<T_>;
// end PYBIND11_TYPE_CASTER
bool load(handle src, bool convert) {
if (!isinstance<sequence>(src) || isinstance<str>(src))
return false;
auto s = reinterpret_borrow<sequence>(src);
value.reserve(s.size());
for (auto it : s) {
value_conv conv;
if (!conv.load(it, convert))
return false;
value.push_back(cast_op<Type &&>(std::move(conv)));
}
return true;
}
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
if (!std::is_lvalue_reference<T>::value)
policy = return_value_policy_override<Type>::policy(policy);
list l(src.size());
size_t index = 0;
for (auto &&value : src) {
auto value_ = reinterpret_steal<object>(value_conv::cast(forward_like<T>(value), policy, parent));
if (!value_)
return handle();
PyList_SET_ITEM(l.ptr(), (ssize_t) index++, value_.release().ptr()); // steals a reference
}
return l.release();
}
};
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,172 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/SmallVector.h>
#include <span>
namespace pybind11 {
namespace detail {
template <size_t N>
struct span_name_maker {
template <typename T>
static constexpr auto make(const T &t) {
return concat(t, span_name_maker<N-1>::make(t));
}
};
template <>
struct span_name_maker<1> {
template <typename T>
static constexpr auto make(const T &t) {
return t;
}
};
// span with fixed size converts to a tuple
template <typename Type, size_t Extent> struct type_caster<std::span<Type, Extent>> {
using span_type = typename std::span<Type, Extent>;
using value_conv = make_caster<Type>;
using value_type = typename std::remove_cv<Type>::type;
value_type backing_array[Extent] = {};
PYBIND11_TYPE_CASTER(span_type, _("Tuple[") + span_name_maker<Extent>::make(value_conv::name) + _("]"));
type_caster() : value(backing_array) {}
bool load(handle src, bool convert) {
if (!isinstance<sequence>(src) || isinstance<str>(src))
return false;
auto s = reinterpret_borrow<sequence>(src);
if (s.size() != Extent)
return false;
size_t i = 0;
for (auto it : s) {
value_conv conv;
if (!conv.load(it, convert))
return false;
backing_array[i] = cast_op<Type &&>(std::move(conv));
i++;
}
return true;
}
public:
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
if (!std::is_lvalue_reference<T>::value)
policy = return_value_policy_override<Type>::policy(policy);
tuple l(Extent);
size_t index = 0;
for (auto &&value : src) {
auto value_ = reinterpret_steal<object>(
value_conv::cast(forward_like<T>(value), policy, parent));
if (!value_)
return handle();
PyTuple_SET_ITEM(l.ptr(), (ssize_t)index++,
value_.release().ptr()); // steals a reference
}
return l.release();
}
};
// span with dynamic extent
template <typename Type> struct type_caster<std::span<Type, std::dynamic_extent>> {
using span_type = typename std::span<Type, std::dynamic_extent>;
using value_conv = make_caster<Type>;
using value_type = typename std::remove_cv<Type>::type;
PYBIND11_TYPE_CASTER(span_type, _("List[") + value_conv::name + _("]"));
wpi::SmallVector<value_type, 32> vec;
bool load(handle src, bool convert) {
if (!isinstance<sequence>(src) || isinstance<str>(src))
return false;
auto s = reinterpret_borrow<sequence>(src);
vec.reserve(s.size());
for (auto it : s) {
value_conv conv;
if (!conv.load(it, convert))
return false;
vec.push_back(cast_op<Type &&>(std::move(conv)));
}
value = span_type(std::data(vec), std::size(vec));
return true;
}
public:
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
if (!std::is_lvalue_reference<T>::value)
policy = return_value_policy_override<Type>::policy(policy);
list l(src.size());
size_t index = 0;
for (auto &&value : src) {
auto value_ = reinterpret_steal<object>(
value_conv::cast(forward_like<T>(value), policy, parent));
if (!value_)
return handle();
PyList_SET_ITEM(l.ptr(), (ssize_t)index++,
value_.release().ptr()); // steals a reference
}
return l.release();
}
};
// span specialization: accepts any readonly buffers
template <> struct type_caster<std::span<const uint8_t, std::dynamic_extent>> {
using span_type = typename std::span<const uint8_t, std::dynamic_extent>;
PYBIND11_TYPE_CASTER(span_type, _("Buffer"));
bool load(handle src, bool convert) {
if (!isinstance<buffer>(src))
return false;
auto buf = reinterpret_borrow<buffer>(src);
auto req = buf.request();
if (req.ndim != 1) {
return false;
}
value = span_type((const uint8_t*)req.ptr, req.size*req.itemsize);
return true;
}
public:
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
return bytes((char*)src.data(), src.size()).release();
}
};
// span specialization: writeable buffer
template <> struct type_caster<std::span<uint8_t, std::dynamic_extent>> {
using span_type = typename std::span<const uint8_t, std::dynamic_extent>;
PYBIND11_TYPE_CASTER(std::span<uint8_t>, _("Buffer"));
bool load(handle src, bool convert) {
if (!isinstance<buffer>(src))
return false;
auto buf = reinterpret_borrow<buffer>(src);
auto req = buf.request(true); // buffer must be writeable
if (req.ndim != 1) {
return false;
}
value = std::span<uint8_t>((uint8_t*)req.ptr, req.size*req.itemsize);
return true;
}
public:
template <typename T>
static handle cast(T &&src, return_value_policy policy, handle parent) {
// TODO: should this be a memoryview instead?
return bytes((char*)src.data(), src.size()).release();
}
};
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,19 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <wpi/StringMap.h>
namespace pybind11
{
namespace detail
{
template <typename Value>
struct type_caster<wpi::StringMap<Value>>
: map_caster<wpi::StringMap<Value>, std::string, Value> { };
} // namespace detail
} // namespace pybind11

View File

@@ -0,0 +1,317 @@
#pragma once
#include <functional>
#include <memory>
#include <string_view>
#include <fmt/format.h>
#include <wpi/struct/Struct.h>
#include <pybind11/functional.h>
#include <pybind11/typing.h>
#include <semiwrap.h>
static inline std::string pytypename(const py::type &t) {
return ((PyTypeObject *)t.ptr())->tp_name;
}
//
// Dynamic struct + type caster
//
// This merely holds the python object being operated on, the actual
// serialization work is done in WPyStructConverter
struct WPyStruct {
WPyStruct() = default;
WPyStruct(const WPyStruct &other) {
py::gil_scoped_acquire gil;
py = other.py;
}
WPyStruct &operator=(const WPyStruct &other) {
{
py::gil_scoped_acquire gil;
py = other.py;
}
return *this;
}
WPyStruct(WPyStruct &&) = default;
WPyStruct(const py::object &py) : py(py) {}
~WPyStruct() {
py::gil_scoped_acquire gil;
py.release().dec_ref();
}
py::object py;
};
namespace pybind11 {
namespace detail {
template <> struct type_caster<WPyStruct> {
// TODO: wpiutil.struct.T/TV?
PYBIND11_TYPE_CASTER(WPyStruct, const_name("object"));
bool load(handle src, bool convert) {
// TODO: validation?
value.py = py::reinterpret_borrow<py::object>(src);
return true;
}
static handle cast(const WPyStruct &src, py::return_value_policy policy,
py::handle parent) {
py::handle v = src.py;
v.inc_ref();
return v;
}
};
} // namespace detail
} // namespace pybind11
//
// Struct info class implementation
//
// two types of converters: static C++ converter, and dynamic python converter
struct WPyStructConverter {
virtual ~WPyStructConverter() = default;
virtual std::string_view GetTypeName() const = 0;
virtual size_t GetSize() const = 0;
virtual std::string_view GetSchema() const = 0;
virtual void Pack(std::span<uint8_t> data, const WPyStruct &value) const = 0;
virtual WPyStruct Unpack(std::span<const uint8_t> data) const = 0;
// virtual void UnpackInto(WPyStruct *pyv,
// std::span<const uint8_t> data) const = 0;
virtual void ForEachNested(
const std::function<void(std::string_view, std::string_view)> &fn)
const = 0;
};
// static C++ converter
template <typename T> struct WPyStructCppConverter : WPyStructConverter {
std::string_view GetTypeName() const override {
return wpi::Struct<T>::GetTypeName();
}
size_t GetSize() const override { return wpi::Struct<T>::GetSize(); }
std::string_view GetSchema() const override {
return wpi::Struct<T>::GetSchema();
}
void Pack(std::span<uint8_t> data, const WPyStruct &value) const override {
py::gil_scoped_acquire gil;
const T &v = value.py.cast<const T &>();
wpi::Struct<T>::Pack(data, v);
}
WPyStruct Unpack(std::span<const uint8_t> data) const override {
py::gil_scoped_acquire gil;
return WPyStruct{py::cast(wpi::UnpackStruct<T>(data))};
}
// void UnpackInto(WPyStruct *pyv,
// std::span<const uint8_t> data) const override {
// py::gil_scoped_acquire gil;
// T *v = pyv->py.cast<T *>();
// wpi::UnpackStructInto(v, data);
// }
void ForEachNested(
const std::function<void(std::string_view, std::string_view)> &fn)
const override {
if constexpr (wpi::HasNestedStruct<T>) {
wpi::Struct<T>::ForEachNested(fn);
}
}
};
template <typename T> void SetupWPyStruct(auto pycls) {
auto *sptr =
new std::shared_ptr<WPyStructConverter>(new WPyStructCppConverter<T>());
py::capsule c(sptr, "WPyStruct", [](void *ptr) {
delete (std::shared_ptr<WPyStructConverter> *)ptr;
});
pycls.def_property_readonly_static("WPIStruct",
[c](py::object pycls) { return c; });
}
// dynamic python converter
struct WPyStructPyConverter : WPyStructConverter {
WPyStructPyConverter(py::object o) {
m_typename = o.attr("typename").cast<std::string>();
m_schema = o.attr("schema").cast<std::string>();
m_size = o.attr("size").cast<size_t>();
m_pack = py::reinterpret_borrow<py::function>(o.attr("pack"));
m_packInto = py::reinterpret_borrow<py::function>(o.attr("packInto"));
m_unpack = py::reinterpret_borrow<py::function>(o.attr("unpack"));
// m_unpackInto = py::reinterpret_borrow<py::function>(o.attr("unpackInto"));
m_forEachNested =
py::reinterpret_borrow<py::function>(o.attr("forEachNested"));
}
// copy all the relevant attributes locally
std::string m_typename;
std::string m_schema;
size_t m_size;
py::function m_pack;
py::function m_packInto;
py::function m_unpack;
// py::function m_unpackInto;
py::function m_forEachNested; // might be none
std::string_view GetTypeName() const override { return m_typename; }
size_t GetSize() const override { return m_size; }
std::string_view GetSchema() const override { return m_schema; }
void Pack(std::span<uint8_t> data, const WPyStruct &value) const override {
py::gil_scoped_acquire gil;
py::bytes result = m_pack(value.py);
std::string_view rview = result;
if (rview.size() != data.size()) {
std::string msg = fmt::format(
"{} pack did not return {} bytes (returned {})",
pytypename(py::type::of(value.py)), data.size(), rview.size());
throw py::value_error(msg);
}
rview.copy((char *)data.data(), rview.size());
}
WPyStruct Unpack(std::span<const uint8_t> data) const override {
py::gil_scoped_acquire gil;
auto view =
py::memoryview::from_memory((const void *)data.data(), data.size());
return WPyStruct(m_unpack(view));
}
// void UnpackInto(WPyStruct *pyv,
// std::span<const uint8_t> data) const override {
// py::gil_scoped_acquire gil;
// auto view =
// py::memoryview::from_memory((const void *)data.data(), data.size());
// m_unpackInto(pyv->py, view);
// }
void ForEachNested(
const std::function<void(std::string_view, std::string_view)> &fn)
const override {
py::gil_scoped_acquire gil;
if (!m_forEachNested.is_none()) {
m_forEachNested(fn);
}
}
};
// passed as I... to the wpi::Struct methods
struct WPyStructInfo {
WPyStructInfo() = default;
WPyStructInfo(const py::type &t) {
if (!py::hasattr(t, "WPIStruct")) {
throw py::type_error(
fmt::format("{} is not struct serializable (does not have WPIStruct)",
pytypename(t)));
}
py::object s = t.attr("WPIStruct");
// C++ version
void *c = PyCapsule_GetPointer(s.ptr(), "WPyStruct");
if (c != NULL) {
cvt = *(std::shared_ptr<WPyStructConverter> *)c;
return;
}
PyErr_Clear();
// Python version
try {
cvt = std::make_shared<WPyStructPyConverter>(s);
} catch (py::error_already_set &e) {
std::string msg = fmt::format(
"{} is not struct serializable (invalid WPIStruct)", pytypename(t));
py::raise_from(e, PyExc_TypeError, msg.c_str());
throw py::error_already_set();
}
}
WPyStructInfo(const WPyStruct &v) : WPyStructInfo(py::type::of(v.py)) {}
const WPyStructConverter* operator->() const {
const auto *c = cvt.get();
if (c == nullptr) {
// TODO: would be nice to have a better error here, but we don't have
// a good way to know our current context
throw py::value_error("Object is closed");
}
return c;
}
private:
// holds something used to do serialization
std::shared_ptr<WPyStructConverter> cvt;
};
// Leverages the converter stored in WPyStructInfo to do the actual work
template <> struct wpi::Struct<WPyStruct, WPyStructInfo> {
static std::string_view GetTypeName(const WPyStructInfo &info) {
return info->GetTypeName();
}
static size_t GetSize(const WPyStructInfo &info) {
return info->GetSize();
}
static std::string_view GetSchema(const WPyStructInfo &info) {
return info->GetSchema();
}
static WPyStruct Unpack(std::span<const uint8_t> data,
const WPyStructInfo &info) {
return info->Unpack(data);
}
// static void UnpackInto(WPyStruct *v, std::span<const uint8_t> data,
// const WPyStructInfo &info) {
// info->UnpackInto(v, data);
// }
static void Pack(std::span<uint8_t> data, const WPyStruct &value,
const WPyStructInfo &info) {
info->Pack(data, value);
}
static void
ForEachNested(std::invocable<std::string_view, std::string_view> auto fn,
const WPyStructInfo &info) {
info->ForEachNested(fn);
}
};
static_assert(wpi::StructSerializable<WPyStruct, WPyStructInfo>);
static_assert(wpi::HasNestedStruct<WPyStruct, WPyStructInfo>);
// This breaks on readonly structs, so we disable for now
// static_assert(wpi::MutableStructSerializable<WPyStruct, WPyStructInfo>);

View File

@@ -0,0 +1,166 @@
#include "wpystruct.h"
void forEachNested(
const py::type &t,
const std::function<void(std::string_view, std::string_view)> &fn) {
WPyStructInfo info(t);
wpi::ForEachStructSchema<WPyStruct, WPyStructInfo>(fn, info);
}
py::str getTypeName(const py::type &t) {
WPyStructInfo info(t);
return wpi::GetStructTypeName<WPyStruct, WPyStructInfo>(info);
}
py::str getSchema(const py::type &t) {
WPyStructInfo info(t);
return wpi::GetStructSchema<WPyStruct, WPyStructInfo>(info);
}
size_t getSize(const py::type &t) {
WPyStructInfo info(t);
return wpi::GetStructSize<WPyStruct>(info);
}
py::bytes pack(const WPyStruct &v) {
WPyStructInfo info(v);
auto sz = wpi::GetStructSize<WPyStruct>(info);
PyObject *b = PyBytes_FromStringAndSize(NULL, sz);
if (b == NULL) {
throw py::error_already_set();
}
char *pybuf;
py::ssize_t pysz;
if (PyBytes_AsStringAndSize(b, &pybuf, &pysz) != 0) {
Py_DECREF(b);
throw py::error_already_set();
}
auto s = std::span((uint8_t *)pybuf, pysz);
wpi::PackStruct(s, v, info);
return py::reinterpret_steal<py::bytes>(b);
}
py::bytes packArray(const py::sequence &seq) {
auto len = seq.size();
if (len == 0) {
return {};
}
WPyStructInfo info(py::type::of(seq[0]));
auto sz = wpi::GetStructSize<WPyStruct>(info);
auto total = sz*len;
PyObject *b = PyBytes_FromStringAndSize(NULL, total);
if (b == NULL) {
throw py::error_already_set();
}
char *pybuf;
py::ssize_t pysz;
if (PyBytes_AsStringAndSize(b, &pybuf, &pysz) != 0) {
Py_DECREF(b);
throw py::error_already_set();
}
auto bytes_obj = py::reinterpret_steal<py::bytes>(b);
for (const auto &v: seq) {
WPyStruct wv(v);
auto s = std::span((uint8_t *)pybuf, sz);
wpi::PackStruct(s, wv, info);
pybuf += sz;
}
return bytes_obj;
}
void packInto(const WPyStruct &v, py::buffer &b) {
WPyStructInfo info(v);
py::ssize_t sz = wpi::GetStructSize<WPyStruct>(info);
auto req = b.request();
if (req.itemsize != 1) {
throw py::value_error("buffer must only contain bytes");
} else if (req.ndim != 1) {
throw py::value_error("buffer must only have a single dimension");
}
if (req.size != sz) {
throw py::value_error("buffer must be " + std::to_string(sz) + " bytes");
}
auto s = std::span((uint8_t *)req.ptr, req.size);
wpi::PackStruct(s, v, info);
}
WPyStruct unpack(const py::type &t, const py::buffer &b) {
WPyStructInfo info(t);
py::ssize_t sz = wpi::GetStructSize<WPyStruct>(info);
auto req = b.request();
if (req.itemsize != 1) {
throw py::value_error("buffer must only contain bytes");
} else if (req.ndim != 1) {
throw py::value_error("buffer must only have a single dimension");
}
if (req.size != sz) {
throw py::value_error("buffer must be " + std::to_string(sz) + " bytes");
}
auto s = std::span((const uint8_t *)req.ptr, req.size);
return wpi::UnpackStruct<WPyStruct, WPyStructInfo>(s, info);
}
py::typing::List<WPyStruct> unpackArray(const py::type &t, const py::buffer &b) {
WPyStructInfo info(t);
py::ssize_t sz = wpi::GetStructSize<WPyStruct>(info);
auto req = b.request();
if (req.itemsize != 1) {
throw py::value_error("buffer must only contain bytes");
} else if (req.ndim != 1) {
throw py::value_error("buffer must only have a single dimension");
}
if (req.size % sz != 0) {
throw py::value_error("buffer must be multiple of " + std::to_string(sz) + " bytes");
}
auto items = req.size / sz;
py::list a(items);
const uint8_t *ptr = (const uint8_t *)req.ptr;
for (py::ssize_t i = 0; i < items; i++) {
auto s = std::span(ptr, sz);
auto v = wpi::UnpackStruct<WPyStruct, WPyStructInfo>(s, info);
// steals a reference
PyList_SET_ITEM(a.ptr(), i, v.py.inc_ref().ptr());
ptr += sz;
}
return a;
}
// void unpackInto(const py::buffer &b, WPyStruct *v) {
// WPyStructInfo info(*v);
// py::ssize_t sz = wpi::GetStructSize<WPyStruct>(info);
// auto req = b.request();
// if (req.itemsize != 1) {
// throw py::value_error("buffer must only contain bytes");
// } else if (req.ndim != 1) {
// throw py::value_error("buffer must only have a single dimension");
// }
// if (req.size != sz) {
// throw py::value_error("buffer must be " + std::to_string(sz) + " bytes");
// }
// auto s = std::span((const uint8_t *)req.ptr, req.size);
// wpi::UnpackStructInto<WPyStruct, WPyStructInfo>(v, s, info);
// }

View File

@@ -0,0 +1,59 @@
#pragma once
#include "wpystruct.h"
/**
Call a function to retrieve the (type string, schema) for each nested struct
*/
void forEachNested(
const py::type &t,
const std::function<void(std::string_view, std::string_view)> &fn);
/**
Retrieve the type name for the specified type
*/
py::str getTypeName(const py::type &t);
/**
Retrieve schema for the specified type
*/
py::str getSchema(const py::type &t);
/**
Returns the serialized size in bytes
*/
size_t getSize(const py::type &t);
/**
Serialize object into byte buffer
*/
py::bytes pack(const WPyStruct &v);
/**
Serialize objects into byte buffer
*/
py::bytes packArray(const py::sequence &seq);
/**
Serialize object into byte buffer. Buffer must be exact size.
*/
void packInto(const WPyStruct &v, py::buffer &b);
/**
Convert byte buffer into object of specified type. Buffer must be exact
size.
*/
WPyStruct unpack(const py::type &t, const py::buffer &b);
/**
Convert byte buffer into list of objects of specified type. Buffer must be
exact size.
*/
py::typing::List<WPyStruct> unpackArray(const py::type &t, const py::buffer &b);
// /**
// Convert byte buffer into passed in object. Buffer must be exact
// size.
// */
// void unpackInto(const py::buffer &b, WPyStruct *v);

View File

@@ -0,0 +1,32 @@
# autogenerated by 'semiwrap create-imports wpiutil.sync wpiutil._wpiutil.sync'
from .._wpiutil.sync import (
createEvent,
createSemaphore,
createSignalObject,
destroyEvent,
destroySemaphore,
destroySignalObject,
releaseSemaphore,
resetEvent,
resetSignalObject,
setEvent,
setSignalObject,
waitForObject,
waitForObjects,
)
__all__ = [
"createEvent",
"createSemaphore",
"createSignalObject",
"destroyEvent",
"destroySemaphore",
"destroySignalObject",
"releaseSemaphore",
"resetEvent",
"resetSignalObject",
"setEvent",
"setSignalObject",
"waitForObject",
"waitForObjects",
]

View File

@@ -0,0 +1,58 @@
"""
This package contains the WPILib Struct serialization functions, and a
mechanism to implement your own custom structs using Python (see :func:`wpiutil.wpistruct.make_wpistruct`).
"""
# autogenerated by 'semiwrap create-imports wpiutil.wpistruct wpiutil._wpiutil.wpistruct'
from .._wpiutil.wpistruct import (
forEachNested,
getSchema,
getSize,
getTypeName,
pack,
packArray,
packInto,
unpack,
unpackArray,
)
__all__ = [
"forEachNested",
"getSchema",
"getSize",
"getTypeName",
"pack",
"packArray",
"packInto",
"unpack",
"unpackArray",
]
from .desc import StructDescriptor
from .dataclass import (
make_wpistruct,
int8,
uint8,
int16,
uint16,
int32,
uint32,
int64,
uint64,
double,
)
__all__ += [
"StructDescriptor",
"make_wpistruct",
"int8",
"uint8",
"int16",
"uint16",
"int32",
"uint32",
"int64",
"uint64",
"double",
]

View File

@@ -0,0 +1,229 @@
import dataclasses
import inspect
import struct
import typing
from .desc import StructDescriptor
from .._wpiutil import wpistruct
#
# Use these types to specify explicitly sized integers, but you can
# also use int/bool/float
#
# fmt: off
if typing.TYPE_CHECKING:
int8 = int
uint8 = int
int16 = int
uint16 = int
int32 = int
uint32 = int
int64 = int
uint64 = int
double = float
else:
class int8(int): pass
class uint8(int): pass
class int16(int): pass
class uint16(int): pass
class int32(int): pass
class uint32(int): pass
class int64(int): pass
class uint64(int): pass
class double(float): pass
# fmt: on
def make_wpistruct(cls=None, /, *, name: typing.Optional[str] = None):
"""
This decorator allows you to easily define a custom type that can be
used with wpilib's custom serialization protocol (for use in datalog
and networktables). Just create a normal python dataclass, and apply
this decorator to the class.
For example, here's how you define a dataclass that contains an integer,
a boolean, and a double::
@wpiutil.wpistruct.make_wpistruct(name="mystruct")
@dataclasses.dataclass
class MyStruct:
x: wpiutil.wpistruct.int32
y: bool
z: wpiutil.struct.double
The types defined in the dataclass can be another WPIStruct compatible class
(either builtin or user defined); one of int, bool, or float; or you can
use one of the ``wpiutil.wpistruct.[u]int*`` values for explicitly sized
integer types.
"""
def wrap(cls):
return _process_class(cls, name)
if cls is None:
return wrap
return wrap(cls)
#
# Internals
#
_type_to_fmt = {
bool: ("?", "bool"),
int8: ("b", "int8"),
uint8: ("B", "uint8"),
int16: ("h", "int16"),
uint16: ("H", "uint16"),
int: ("i", "int32"),
int32: ("i", "int32"),
uint32: ("I", "uint32"),
int64: ("q", "int64"),
uint64: ("Q", "uint64"),
float: ("f", "float"),
double: ("d", "double"),
}
def _process_class(cls, struct_name: typing.Optional[str]):
resolved_hints = typing.get_type_hints(cls)
field_names = [field.name for field in dataclasses.fields(cls)]
resolved_field_types = {name: resolved_hints[name] for name in field_names}
name_parts = []
name_parts.append(getattr(cls, "__module__", None))
name_parts.append(getattr(cls, "__qualname__", cls.__name__))
cls_name = ".".join([n for n in name_parts if n])
if struct_name is None:
struct_name = cls.__name__
err_name = cls_name
else:
err_name = f"{struct_name} ({cls_name})"
fmts = []
schema = []
cvvals = []
vvals = []
packs = []
unpacks = []
# unpackIntos = []
forEachNested = []
ctx: typing.Dict[str, typing.Any] = {"cls": cls}
for name, ftype in resolved_field_types.items():
if ftype in _type_to_fmt:
fmt, stype = _type_to_fmt[ftype]
fmts.append(fmt)
schema.append(f"{stype} {name}")
cvvals.append(f"arg_{name}")
vvals.append(f"v.{name}")
elif hasattr(ftype, "WPIStruct"):
# nested struct
argn = f"arg_{name}"
typn = f"type_{name}"
ctx[typn] = ftype
ts = wpistruct.getTypeName(ftype)
schema.append(f"{ts} {name}")
sz = wpistruct.getSize(ftype)
fmts.append(f"{sz}s")
vvals.append(argn)
cvvals.append(argn)
packs.append(f"{argn} = wpistruct.pack(v.{name})")
unpacks.append(f"{argn} = wpistruct.unpack({typn}, {argn})")
# unpackIntos.append(f"wpistruct.unpackInto(v.{name}, {argn})")
forEachNested.append(f"wpistruct.forEachNested({typn}, fn)")
else:
supported_names = ", ".join(t.__name__ for t in _type_to_fmt.keys())
raise TypeError(
f"{cls_name}.{name} is not a wpistruct or does not have a supported type hint "
f"(supported: {supported_names})"
) from None
s = struct.Struct(f"<{''.join(fmts)}")
cvals = ", ".join(cvvals)
vals = ", ".join(vvals)
padding = "\n" + " " * 16
pack_stmts = padding.join(packs)
unpack_stmts = padding.join(unpacks)
# unpackInto_stmts = padding.join(unpackIntos)
if not forEachNested:
forEachNested_stmt = "_forEachNested = None"
else:
forEachNested_stmt = f"def _forEachNested(fn):"
forEachNested_stmt += "\n" + " " * 12
forEachNested_stmt += f"try:{padding}"
forEachNested_stmt += padding.join(forEachNested)
forEachNested_stmt += "\n" + " " * 12
forEachNested_stmt += f"except Exception as e:"
forEachNested_stmt += (
f"{padding}raise ValueError(f'{err_name}: error in forEachNested') from e"
)
ctx["_s"] = s
# Construct the serialization functions using the same hack NamedTuple uses
fnsrc = inspect.cleandoc(
f"""
from wpiutil import wpistruct
def _pack(v):
try:
{pack_stmts}
return _s.pack({vals})
except Exception as e:
raise ValueError(f"{err_name}: error packing data") from e
def _packInto(v, b):
try:
{pack_stmts}
return _s.pack_into(b, 0, {vals})
except Exception as e:
raise ValueError(f"{err_name}: error packing data") from e
def _unpack(b):
try:
{cvals} = _s.unpack(b)
{unpack_stmts}
return cls({cvals})
except Exception as e:
raise ValueError(f"{err_name}: error unpacking data") from e
#def _unpackInto(v, b):
# try:
# {vals} = _s.unpack(b)
# {{unpackInto_stmts}}
# except Exception as e:
# raise ValueError(f"{err_name}: error unpacking data") from e
{forEachNested_stmt}
"""
)
exec(fnsrc, ctx, ctx)
cls.WPIStruct = StructDescriptor(
typename=struct_name,
schema="; ".join(schema),
size=s.size,
pack=ctx["_pack"],
packInto=ctx["_packInto"],
unpack=ctx["_unpack"],
# unpackInto=ctx["_unpackInto"],
forEachNested=ctx["_forEachNested"],
)
return cls

View File

@@ -0,0 +1,48 @@
import typing
if typing.TYPE_CHECKING:
from typing_extensions import Buffer
else:
# Avoiding typing_extensions runtime dependency
Buffer = bytearray
class StructDescriptor(typing.NamedTuple):
"""
To define a type in python that can use the wpilib serialization, the type must
have an attribute `WPIStruct` that contains this class (but C++ classes
do not have this).
It is not intended that you should create this class directly, something
else should generate it for you.
See :func:`wpiutil.wpistruct.make_wpistruct` for a easy to use mechanism
for defining custom structs using a dataclass.
"""
#: The type name
typename: str
#: The struct schema
schema: str
#: Size in bytes of binary representation of this struct
size: int
#: A function that converts the type to bytes
pack: typing.Callable[[typing.Any], bytes]
#: A function that converts the type to bytes
packInto: typing.Callable[[typing.Any, Buffer], None]
#: A function that converts bytes to an instance
unpack: typing.Callable[[Buffer], typing.Any]
#: A function that updates the given instance using the deserialized bytes
#: .. not supported
# unpackInto: typing.Callable[[typing.Any, Buffer], None]
#: If this contains nested structs, calls wpiutil.wpistruct.forEachNested for each
forEachNested: typing.Optional[
typing.Callable[[typing.Callable[[str, str], None]], None]
]

View File

@@ -0,0 +1,23 @@
from typing import ClassVar, Protocol
try:
from typing import TypeGuard
except ImportError:
try:
from typing_extensions import TypeGuard
except ImportError:
# Runtime fallback for Python 3.9 without typing_extensions
class TypeGuard:
def __class_getitem__(cls, key):
return bool
class StructSerializable(Protocol):
"""Any type that can be serialized or deserialized as a WPILib Struct."""
WPIStruct: ClassVar
def is_wpistruct_type(cls: type) -> TypeGuard[type[StructSerializable]]:
"""Returns True if the given type supports WPILib Struct serialization."""
return hasattr(cls, "WPIStruct")

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,11 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatch-meson", "hatchling"
]
[project]
name = "wpiutil_test"
version = "0.0.1"
[tool.hatch.build.hooks.meson]

View File

@@ -0,0 +1 @@
import wpiutil

View File

@@ -0,0 +1,195 @@
// clang-format off
#include <pybind11/pybind11.h>
#include <wpi_array_type_caster.h>
#include <wpi_span_type_caster.h>
#include <wpi_smallset_type_caster.h>
#include <wpi_smallvector_type_caster.h>
#include <wpi_smallvectorimpl_type_caster.h>
#include <wpi_string_map_caster.h>
#include <wpi_json_type_caster.h>
#include <wpi_ct_string_type_caster.h>
#include <limits>
#include <functional>
#include <pybind11/functional.h>
/*
array tests
*/
wpi::array<int, 4> load_array_int(wpi::array<int, 4> data) {
return data;
}
wpi::array<int, 1> load_array_int1(wpi::array<int, 1> data) {
return data;
}
/*
span Tests
*/
std::span<const int> load_span_int(std::span<const int> ref) {
return ref;
}
std::span<const bool> load_span_bool(std::span<const bool> ref) {
return ref;
}
std::span<std::string> load_span_string(std::span<std::string> ref) {
return ref;
}
std::span<const std::string> load_span_string_const(std::span<const std::string> ref) {
return ref;
}
std::span<std::string_view> load_span_string_view(std::span<std::string_view> ref) {
return ref;
}
std::span<std::vector<std::string>> load_span_vector(std::span<std::vector<std::string>> ref) {
return ref;
}
std::span<const double, 3> load_span_fixed_double(std::span<const double, 3> ref) {
return ref;
}
std::span<int> cast_span() {
static std::vector<int> vec{1, 2, 3};
return vec;
}
std::span<const std::string> make_string_span() {
static std::vector<std::string> vec{"hi", "there"};
return vec;
}
py::object cast_string_span() {
return py::cast(make_string_span());
}
std::span<const uint8_t> load_span_bytes(std::span<const uint8_t> ref) {
return ref;
}
void modify_span_buffer(std::span<uint8_t> ref) {
ref[0] = 0x4;
}
/*
SmallSet tests
*/
wpi::SmallSet<int, 4> load_smallset_int(wpi::SmallSet<int, 4> ref) {
return ref;
}
wpi::SmallSet<int, 4> cast_smallset() {
static wpi::SmallSet<int, 4> set;
set.insert(1);
set.insert(2);
set.insert(3);
set.insert(4);
return set;
}
/*
SmallVector tests
*/
wpi::SmallVector<int, 4> load_smallvec_int(wpi::SmallVector<int, 4> ref) {
return ref;
}
wpi::SmallVector<int, 4> cast_smallvec() {
static wpi::SmallVector<int, 4> set;
set.append({1, 2, 3, 4});
return set;
}
/*
SmallVectorImpl tests
.. seems like references are the only useful things to do with them
*/
wpi::SmallVectorImpl<int>& load_smallvecimpl_int(wpi::SmallVectorImpl<int>& ref) {
static wpi::SmallVector<int, 4> set(ref.begin(), ref.end());
return set;
}
/*
StringMap tests
*/
wpi::StringMap<int> load_stringmap_int(wpi::StringMap<int> ref) {
return ref;
}
wpi::StringMap<int> cast_stringmap() {
static wpi::StringMap<int> m;
m["one"] = 1;
m["two"] = 2;
return m;
}
/* JSON tests */
wpi::json cast_json_arg(const wpi::json &j) {
return j;
}
wpi::json cast_json_val(std::function<wpi::json()> fn) {
return fn();
}
constexpr auto const_string() {
return wpi::ct_string<char, std::char_traits<char>, 3>{{'#', '1', '2'}};
}
void sendable_test(py::module &m);
void struct_test(py::module &m);
PYBIND11_MODULE(module, m) {
sendable_test(m);
struct_test(m);
// array
m.def("load_array_int", &load_array_int);
m.def("load_array_int1", &load_array_int1);
// span
m.def("load_span_int", &load_span_int);
m.def("load_span_bool", &load_span_bool);
m.def("load_span_fixed_double", &load_span_fixed_double);
m.def("load_span_string", &load_span_string);
m.def("load_span_string_const", &load_span_string_const);
m.def("load_span_string_view", &load_span_string_view);
m.def("load_span_vector", &load_span_vector);
m.def("cast_span", &cast_span);
m.def("cast_string_span", &cast_string_span);
m.def("load_span_bytes", &load_span_bytes);
m.def("modify_span_buffer", &modify_span_buffer);
// SmallSet
m.def("load_smallset_int", &load_smallset_int);
m.def("cast_smallset", &cast_smallset);
// SmallVector
m.def("load_smallvec_int", &load_smallvec_int);
m.def("cast_smallvec", &cast_smallvec);
// SmallVectorImpl
m.def("load_smallvecimpl_int", &load_smallvecimpl_int);
// StringMap
m.def("load_stringmap_int", &load_stringmap_int);
m.def("cast_stringmap", &cast_stringmap);
// JSON
m.def("cast_json_arg", &cast_json_arg);
m.def("cast_json_val", &cast_json_val);
m.attr("max_uint64") = std::numeric_limits<uint64_t>::max();
m.attr("max_int64") = std::numeric_limits<int64_t>::max();
m.attr("min_int64") = std::numeric_limits<int64_t>::min();
// ct_string
m.def("const_string", &const_string);
};

View File

@@ -0,0 +1,154 @@
#include <pybind11/functional.h>
#include <pybind11/stl.h>
#include <semiwrap.h>
#include <wpi/sendable/SendableBuilder.h>
#include <wpi/sendable/SendableRegistry.h>
class MySendableBuilder : public wpi::SendableBuilder {
public:
MySendableBuilder(py::dict keys) : keys(keys) {}
~MySendableBuilder() {
// leak this so the python interpreter doesn't crash on shutdown
keys.release();
}
void SetSmartDashboardType(std::string_view type) override {}
void SetActuator(bool value) override {}
void AddBooleanProperty(std::string_view key, std::function<bool()> getter,
std::function<void(bool)> setter) override {}
void PublishConstBoolean(std::string_view key, bool value) override {}
void AddIntegerProperty(std::string_view key, std::function<int64_t()> getter,
std::function<void(int64_t)> setter) override {}
void PublishConstInteger(std::string_view key, int64_t value) override {}
void AddFloatProperty(std::string_view key, std::function<float()> getter,
std::function<void(float)> setter) override {}
void PublishConstFloat(std::string_view key, float value) override {}
void AddDoubleProperty(std::string_view key, std::function<double()> getter,
std::function<void(double)> setter) override {
py::gil_scoped_acquire gil;
py::object pykey = py::cast(key);
keys[pykey] = std::make_tuple(getter, setter);
}
void PublishConstDouble(std::string_view key, double value) override {}
void
AddStringProperty(std::string_view key, std::function<std::string()> getter,
std::function<void(std::string_view)> setter) override {}
void PublishConstString(std::string_view key,
std::string_view value) override {}
void AddBooleanArrayProperty(
std::string_view key, std::function<std::vector<int>()> getter,
std::function<void(std::span<const int>)> setter) override {}
void PublishConstBooleanArray(std::string_view key,
std::span<const int> value) override {}
void AddIntegerArrayProperty(
std::string_view key, std::function<std::vector<int64_t>()> getter,
std::function<void(std::span<const int64_t>)> setter) override {}
void PublishConstIntegerArray(std::string_view key,
std::span<const int64_t> value) override {}
void AddFloatArrayProperty(
std::string_view key, std::function<std::vector<float>()> getter,
std::function<void(std::span<const float>)> setter) override {}
void PublishConstFloatArray(std::string_view key,
std::span<const float> value) override {}
void AddDoubleArrayProperty(
std::string_view key, std::function<std::vector<double>()> getter,
std::function<void(std::span<const double>)> setter) override {}
void PublishConstDoubleArray(std::string_view key,
std::span<const double> value) override {}
void AddStringArrayProperty(
std::string_view key, std::function<std::vector<std::string>()> getter,
std::function<void(std::span<const std::string>)> setter) override {}
void PublishConstStringArray(std::string_view key,
std::span<const std::string> value) override {}
void AddRawProperty(
std::string_view key, std::string_view typeString,
std::function<std::vector<uint8_t>()> getter,
std::function<void(std::span<const uint8_t>)> setter) override {}
void PublishConstRaw(std::string_view key, std::string_view typeString,
std::span<const uint8_t> value) override {}
void AddSmallStringProperty(
std::string_view key,
std::function<std::string_view(wpi::SmallVectorImpl<char> &buf)> getter,
std::function<void(std::string_view)> setter) override {}
void AddSmallBooleanArrayProperty(
std::string_view key,
std::function<std::span<const int>(wpi::SmallVectorImpl<int> &buf)>
getter,
std::function<void(std::span<const int>)> setter) override {}
void AddSmallIntegerArrayProperty(
std::string_view key,
std::function<
std::span<const int64_t>(wpi::SmallVectorImpl<int64_t> &buf)>
getter,
std::function<void(std::span<const int64_t>)> setter) override {}
void AddSmallFloatArrayProperty(
std::string_view key,
std::function<std::span<const float>(wpi::SmallVectorImpl<float> &buf)>
getter,
std::function<void(std::span<const float>)> setter) override {}
void AddSmallDoubleArrayProperty(
std::string_view key,
std::function<std::span<const double>(wpi::SmallVectorImpl<double> &buf)>
getter,
std::function<void(std::span<const double>)> setter) override {}
void AddSmallStringArrayProperty(
std::string_view key,
std::function<
std::span<const std::string>(wpi::SmallVectorImpl<std::string> &buf)>
getter,
std::function<void(std::span<const std::string>)> setter) override {}
void AddSmallRawProperty(
std::string_view key, std::string_view typeString,
std::function<std::span<uint8_t>(wpi::SmallVectorImpl<uint8_t> &buf)>
getter,
std::function<void(std::span<const uint8_t>)> setter) override {}
wpi::SendableBuilder::BackendKind GetBackendKind() const override {
return wpi::SendableBuilder::BackendKind::kUnknown;
}
bool IsPublished() const override { return false; }
void Update() override {}
void ClearProperties() override {}
py::dict keys;
};
void Publish(wpi::SendableRegistry::UID sendableUid, py::dict keys) {
auto builder = std::make_unique<MySendableBuilder>(keys);
wpi::SendableRegistry::Publish(sendableUid, std::move(builder));
}
void sendable_test(py::module &m) { m.def("publish", Publish); }

View File

@@ -0,0 +1,84 @@
#include <pybind11/operators.h>
#include <wpystruct.h>
//
// Thing to serialize
//
struct ThingA {
ThingA() = default;
ThingA(int x) : x(x) {}
const int x = 0;
bool operator==(const ThingA &other) const { return x == other.x; }
};
template <> struct wpi::Struct<ThingA> {
static constexpr std::string_view GetTypeName() { return "ThingA"; }
static constexpr size_t GetSize() { return 1; }
static constexpr std::string_view GetSchema() { return "uint8 value"; }
static ThingA Unpack(std::span<const uint8_t> data) {
return ThingA{data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingA &value) {
data[0] = value.x;
}
};
struct Outer {
Outer() = default;
Outer(const ThingA &t, int c) : inner(t), c(c) {}
ThingA inner;
int c = 0;
bool operator==(const Outer &other) const {
return inner == other.inner && c == other.c;
}
};
template <>
struct wpi::Struct<Outer> {
static constexpr std::string_view GetTypeName() { return "Outer"; }
static constexpr size_t GetSize() { return wpi::GetStructSize<ThingA>() + 4; }
static constexpr std::string_view GetSchema() {
return "ThingA inner; int32 c";
}
static Outer Unpack(std::span<const uint8_t> data) {
constexpr size_t innerSize = wpi::GetStructSize<ThingA>();
return {wpi::UnpackStruct<ThingA, 0>(data),
wpi::UnpackStruct<int32_t, innerSize>(data)};
}
static void Pack(std::span<uint8_t> data, const Outer& value) {
constexpr size_t innerSize = wpi::GetStructSize<ThingA>();
wpi::PackStruct<0>(data, value.inner);
wpi::PackStruct<innerSize>(data, value.c);
}
static void ForEachNested(
std::invocable<std::string_view, std::string_view> auto fn) {
wpi::ForEachStructSchema<ThingA>(fn);
}
};
void struct_test(py::module &m) {
py::class_<ThingA> thingCls(m, "ThingA");
thingCls.def(py::init<>());
thingCls.def(py::init<int>());
thingCls.def_readonly("x", &ThingA::x);
thingCls.def(py::self == py::self);
SetupWPyStruct<ThingA>(thingCls);
py::class_<Outer> outerCls(m, "Outer");
outerCls.def(py::init<>());
outerCls.def(py::init<ThingA, int>());
outerCls.def_readonly("inner", &Outer::inner);
outerCls.def_readwrite("c", &Outer::c);
outerCls.def(py::self == py::self);
SetupWPyStruct<Outer>(outerCls);
}

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import os
from os.path import abspath, dirname
import sys
import subprocess
if __name__ == "__main__":
root = abspath(dirname(__file__))
os.chdir(root)
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"--disable-pip-version-check",
"install",
"-v",
"--force-reinstall",
"--no-build-isolation",
"./cpp",
],
)
subprocess.check_call([sys.executable, "-m", "pytest"])

View File

@@ -0,0 +1,17 @@
from wpiutil_test import module
def test_load_array_int():
assert module.load_array_int((1, 2, 3, 4)) == (1, 2, 3, 4)
assert module.load_array_int([1, 2, 3, 4]) == (1, 2, 3, 4)
def test_load_array_annotation():
assert (
module.load_array_int.__doc__
== "load_array_int(arg0: Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt, typing.SupportsInt]) -> Tuple[int, int, int, int]\n"
)
assert (
module.load_array_int1.__doc__
== "load_array_int1(arg0: Tuple[typing.SupportsInt]) -> Tuple[int]\n"
)

View File

@@ -0,0 +1,5 @@
from wpiutil_test import module
def test_const_string():
assert module.const_string() == "#12"

View File

@@ -0,0 +1,71 @@
from wpiutil_test.module import (
cast_json_arg,
cast_json_val,
max_int64,
min_int64,
max_uint64,
)
import pytest
import math
def test_json_invalid():
with pytest.raises(TypeError):
cast_json_val(lambda: object())
def test_json_none():
assert cast_json_arg(None) == None
def test_json_bool():
assert cast_json_arg(True) == True
assert cast_json_arg(False) == False
def test_json_int():
assert cast_json_arg(36) == 36
assert cast_json_arg(min_int64) == min_int64
with pytest.raises(ValueError):
cast_json_arg(min_int64 - 1)
assert cast_json_arg(max_int64) == max_int64
assert cast_json_arg(max_uint64) == max_uint64
with pytest.raises(ValueError):
cast_json_arg(max_uint64 + 1)
def test_json_float():
assert cast_json_arg(36.37) == 36.37
assert cast_json_arg(math.inf) == math.inf
assert math.isnan(cast_json_arg(math.nan))
def test_json_string():
assert cast_json_arg("hi") == "hi"
def test_json_list():
v = [36, "hello", False]
assert cast_json_arg(v) == v
assert cast_json_arg([]) == []
tv = (36, "hello", False)
assert cast_json_arg(tv) == v
def test_json_dict():
d = {"number": 1234, "hello": "world"}
assert cast_json_arg(d) == d
assert cast_json_arg({}) == {}
assert cast_json_arg({1: 2}) == {"1": 2}
assert cast_json_arg({None: 2}) == {"None": 2}
assert cast_json_arg({1.2: 2}) == {"1.2": 2}
assert cast_json_arg({False: 2}) == {"False": 2}
with pytest.raises(TypeError):
cast_json_arg({object(): 2})

View File

@@ -0,0 +1,35 @@
import typing
import wpiutil
from wpiutil_test import module
class MySendable(wpiutil.Sendable):
def __init__(self):
super().__init__()
wpiutil.SendableRegistry.add(self, "Test", 1)
self.value = 0
def initSendable(self, builder: wpiutil.SendableBuilder):
builder.addDoubleProperty("key", self._get, self._set)
def _set(self, value: float):
self.value = value
def _get(self) -> float:
return self.value
def test_custom_sendable():
ms = MySendable()
uid = wpiutil.SendableRegistry.getUniqueId(ms)
keys = {}
module.publish(uid, keys)
assert ms.value == 0
getter, setter = keys["key"]
assert getter() == 0
setter(1)
assert getter() == 1
assert ms.value == 1

View File

@@ -0,0 +1,18 @@
from wpiutil_test import module
def test_smallset_load():
assert module.load_smallset_int({1, 2, 3, 4}) == {1, 2, 3, 4}
def test_smallsetbool_load():
assert module.load_smallset_int({True, True, False, True}) == {
True,
True,
False,
True,
}
def test_smallset_cast():
assert module.cast_smallset() == {1, 2, 3, 4}

View File

@@ -0,0 +1,22 @@
from wpiutil_test import module
def test_smallvec_load():
assert module.load_smallvec_int([1, 2, 3, 4]) == [1, 2, 3, 4]
def test_smallvecbool_load():
assert module.load_smallvec_int([True, True, False, True]) == [
True,
True,
False,
True,
]
def test_smallvec_cast():
assert module.cast_smallvec() == [1, 2, 3, 4]
def test_smallvecimpl_load():
assert module.load_smallvecimpl_int([1, 2, 3, 4]) == [1, 2, 3, 4]

View File

@@ -0,0 +1,75 @@
import pytest
from wpiutil_test import module
import array
def test_span_load_int():
assert module.load_span_int([1, 2, 3, 4]) == [1, 2, 3, 4]
def test_span_load_int():
assert module.load_span_int([1, 2, 3]) == [1, 2, 3]
def test_span_load_bool():
assert module.load_span_bool([True, False, True]) == [True, False, True]
def test_span_load_string():
assert module.load_span_string(["a", "b", "c"]) == ["a", "b", "c"]
def test_span_load_string_const():
assert module.load_span_string_const(["a", "b", "c"]) == ["a", "b", "c"]
def test_span_load_stringview():
assert module.load_span_string_view(["a", "b", "c"]) == ["a", "b", "c"]
def test_span_load_vector():
assert module.load_span_vector([["a"], ["b"], ["c"]]) == [["a"], ["b"], ["c"]]
def test_span_load_buffer_bytes():
assert module.load_span_bytes(b"abc") == b"abc"
def test_span_modify_buffer_bytes():
b = b"abc"
with pytest.raises(BufferError):
module.modify_span_buffer(b)
def test_span_load_buffer_bytearray():
assert module.load_span_bytes(bytearray([1, 2, 3])) == b"\x01\x02\x03"
def test_span_modify_buffer_bytearray():
b = bytearray([1, 2, 3])
module.modify_span_buffer(b)
assert b == bytearray([4, 2, 3])
def test_span_load_buffer_array():
a = array.array("l")
a.append(1)
a2 = array.array("l")
a2.frombytes(module.load_span_bytes(a))
assert len(a2) == 1
assert a2[0] == 1
def test_span_cast():
assert module.cast_span() == [1, 2, 3]
def test_string_span():
assert module.cast_string_span() == ["hi", "there"]
def test_fixed_double_span():
assert module.load_span_fixed_double([1, 2, 3]) == (1, 2, 3)
with pytest.raises(TypeError):
assert module.load_span_fixed_double([1, 2, 3, 4])

View File

@@ -0,0 +1,9 @@
import wpiutil
def test_python_stack_trace():
st = wpiutil._wpiutil.getStackTrace(0)
assert __file__ in st
st = wpiutil._wpiutil.getStackTraceDefault(0)
assert __file__ not in st

View File

@@ -0,0 +1,13 @@
from wpiutil_test import module
def test_stringmap_load():
assert module.load_stringmap_int({"one": 11, "two": 22, "three": 33}) == {
"one": 11,
"two": 22,
"three": 33,
}
def test_stringmap_cast():
assert module.cast_stringmap() == {"one": 1, "two": 2}

View File

@@ -0,0 +1,242 @@
import dataclasses
import re
import pytest
from wpiutil import wpistruct
from wpiutil_test import module
#
# Static serialization
#
# ensure that a type that doesn't work has a sane error message
def test_invalid_type():
with pytest.raises(
TypeError,
match=re.escape("str is not struct serializable (does not have WPIStruct)"),
):
wpistruct.getSchema(str)
def test_for_each_nested():
l = []
def _fn(*args):
l.append(args)
wpistruct.forEachNested(module.ThingA, _fn)
assert l == [("struct:ThingA", "uint8 value")]
def test_get_type_string():
assert wpistruct.getTypeName(module.ThingA) == "ThingA"
def test_get_schema():
assert wpistruct.getSchema(module.ThingA) == "uint8 value"
def test_get_size():
assert wpistruct.getSize(module.ThingA) == 1
def test_pack():
assert wpistruct.pack(module.ThingA(1)) == b"\x01"
def test_pack_array():
assert wpistruct.packArray([module.ThingA(1), module.ThingA(2)]) == b"\x01\x02"
def test_pack_into():
buf = bytearray(1)
wpistruct.packInto(module.ThingA(1), buf)
assert buf == b"\x01"
def test_pack_into_err():
buf = bytearray(2)
with pytest.raises(ValueError, match=re.escape("buffer must be 1 bytes")):
wpistruct.packInto(module.ThingA(1), buf)
def test_unpack():
assert wpistruct.unpack(module.ThingA, b"\x01") == module.ThingA(1)
def test_unpack_array():
assert wpistruct.unpackArray(module.ThingA, b"\x01\x02") == [
module.ThingA(1),
module.ThingA(2),
]
# def test_unpack_into():
# r1 = module.ThingA(1)
# r2 = module.ThingA(2)
# assert r1 != r2
# wpistruct.unpackInto(b"\x01", r2)
# assert r1 == r2
#
# Nested struct
#
def test_nested_for_each_nested():
l = []
def _fn(*args):
l.append(args)
wpistruct.forEachNested(module.Outer, _fn)
assert l == [
("struct:ThingA", "uint8 value"),
("struct:Outer", "ThingA inner; int32 c"),
]
def test_nested_get_type_string():
assert wpistruct.getTypeName(module.ThingA) == "ThingA"
def test_nested_get_schema():
assert wpistruct.getSchema(module.Outer) == "ThingA inner; int32 c"
def test_nested_get_size():
assert wpistruct.getSize(module.Outer) == 5
def test_nested_pack():
v = module.Outer(module.ThingA(2), 4)
assert wpistruct.pack(v) == b"\x02\x04\x00\x00\x00"
def test_nested_pack_into():
v = module.Outer(module.ThingA(3), 5)
buf = bytearray(5)
wpistruct.packInto(v, buf)
assert buf == b"\x03\x05\x00\x00\x00"
def test_nested_unpack():
assert wpistruct.unpack(module.ThingA, b"\x01") == module.ThingA(1)
#
# User defined serialization
#
@wpistruct.make_wpistruct(name="mystruct")
@dataclasses.dataclass
class MyStruct:
x: int
y: bool
z: float
def test_user_for_each_nested():
l = []
def _fn(*args):
l.append(args)
wpistruct.forEachNested(MyStruct, _fn)
assert l == [("struct:mystruct", "int32 x; bool y; float z")]
def test_user_get_type_string():
assert wpistruct.getTypeName(MyStruct) == "mystruct"
def test_user_get_schema():
assert wpistruct.getSchema(MyStruct) == "int32 x; bool y; float z"
def test_user_get_size():
assert wpistruct.getSize(MyStruct) == 9
def test_user_pack():
v = MyStruct(2, True, 3.5)
assert wpistruct.pack(v) == b"\x02\x00\x00\x00\x01\x00\x00\x60\x40"
def test_user_pack_into():
v = MyStruct(2, True, 3.5)
buf = bytearray(9)
wpistruct.packInto(v, buf)
assert buf == b"\x02\x00\x00\x00\x01\x00\x00\x60\x40"
def test_user_unpack():
v = MyStruct(2, True, 3.5)
assert wpistruct.unpack(MyStruct, b"\x02\x00\x00\x00\x01\x00\x00\x60\x40") == v
# def test_user_unpack_into():
# v1 = MyStruct(2, True, 3.5)
# v2 = MyStruct(3, True, 4.5)
# assert v1 != v2
# wpistruct.unpackInto(b"\x02\x00\x00\x00\x01\x00\x00\x60\x40", v2)
# assert v1 == v2
#
# User defined serialization (nested)
#
@wpistruct.make_wpistruct
@dataclasses.dataclass
class Outer:
x: int
inner: MyStruct
def test_user_nested_for_each_nested():
l = []
def _fn(*args):
l.append(args)
wpistruct.forEachNested(Outer, _fn)
assert l == [
("struct:mystruct", "int32 x; bool y; float z"),
("struct:Outer", "int32 x; mystruct inner"),
]
def test_user_nested_get_type_string():
assert wpistruct.getTypeName(Outer) == "Outer"
def test_user_nested_get_schema():
assert wpistruct.getSchema(Outer) == "int32 x; mystruct inner"
def test_user_nested_get_size():
assert wpistruct.getSize(Outer) == 4 + 9
def test_user_nested_pack():
v = Outer(2, MyStruct(3, True, 4.0))
assert wpistruct.pack(v) == b"\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x80\x40"
def test_user_nested_pack_into():
v = Outer(2, MyStruct(3, True, 4.0))
buf = bytearray(4 + 9)
wpistruct.packInto(v, buf)
assert buf == b"\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x80\x40"
def test_user_nested_unpack():
assert wpistruct.unpack(
Outer, b"\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x80\x40"
) == Outer(2, MyStruct(3, True, 4.0))