diff --git a/.bazelrc b/.bazelrc index bedd56544a..2e5abbfb9f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -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/ diff --git a/.gitattributes b/.gitattributes index 08bbbda605..a0d1891c68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -28,3 +28,5 @@ # Generated files */src/generated/** linguist-generated +*/robotpy_native_build_info.bzl linguist-generated +*/robotpy_pybind_build_info.bzl linguist-generated diff --git a/BUILD.bazel b/BUILD.bazel index fe34704d13..dbd1720ce7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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"], ) diff --git a/README-RobotPy.md b/README-RobotPy.md new file mode 100644 index 0000000000..e703065a59 --- /dev/null +++ b/README-RobotPy.md @@ -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. diff --git a/WORKSPACE b/WORKSPACE index ddcfe20501..36b1be850f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -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") diff --git a/requirements.txt b/requirements.txt index 3220185bfd..ca73df8d60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/requirements_lock.txt b/requirements_lock.txt index 332688e377..f8f9e918a9 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -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 diff --git a/requirements_windows_lock.txt b/requirements_windows_lock.txt new file mode 100644 index 0000000000..92bd9fef15 --- /dev/null +++ b/requirements_windows_lock.txt @@ -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 diff --git a/shared/bazel/compiler_flags/linux_flags.rc b/shared/bazel/compiler_flags/linux_flags.rc index 7b596188b7..c424db4bdb 100644 --- a/shared/bazel/compiler_flags/linux_flags.rc +++ b/shared/bazel/compiler_flags/linux_flags.rc @@ -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 diff --git a/shared/bazel/rules/robotpy/BUILD.bazel b/shared/bazel/rules/robotpy/BUILD.bazel new file mode 100644 index 0000000000..fceab14eff --- /dev/null +++ b/shared/bazel/rules/robotpy/BUILD.bazel @@ -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"), +) diff --git a/shared/bazel/rules/robotpy/build_info_gen.bzl b/shared/bazel/rules/robotpy/build_info_gen.bzl new file mode 100644 index 0000000000..bb8a03d7a2 --- /dev/null +++ b/shared/bazel/rules/robotpy/build_info_gen.bzl @@ -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(), + ) diff --git a/shared/bazel/rules/robotpy/compatibility_select.bzl b/shared/bazel/rules/robotpy/compatibility_select.bzl new file mode 100644 index 0000000000..858647873a --- /dev/null +++ b/shared/bazel/rules/robotpy/compatibility_select.bzl @@ -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": [], + }) diff --git a/shared/bazel/rules/robotpy/generate_native_build_file.py b/shared/bazel/rules/robotpy/generate_native_build_file.py new file mode 100644 index 0000000000..256c1f4ec1 --- /dev/null +++ b/shared/bazel/rules/robotpy/generate_native_build_file.py @@ -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() diff --git a/shared/bazel/rules/robotpy/generate_pybind_build_file.py b/shared/bazel/rules/robotpy/generate_pybind_build_file.py new file mode 100644 index 0000000000..b56a682d40 --- /dev/null +++ b/shared/bazel/rules/robotpy/generate_pybind_build_file.py @@ -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() diff --git a/shared/bazel/rules/robotpy/generation_utils.py b/shared/bazel/rules/robotpy/generation_utils.py new file mode 100644 index 0000000000..d136ea905b --- /dev/null +++ b/shared/bazel/rules/robotpy/generation_utils.py @@ -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 diff --git a/shared/bazel/rules/robotpy/hack_pkgcfgs.py b/shared/bazel/rules/robotpy/hack_pkgcfgs.py new file mode 100644 index 0000000000..408a6d8b2e --- /dev/null +++ b/shared/bazel/rules/robotpy/hack_pkgcfgs.py @@ -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) diff --git a/shared/bazel/rules/robotpy/hatchlib_native_port/BUILD.bazel b/shared/bazel/rules/robotpy/hatchlib_native_port/BUILD.bazel new file mode 100644 index 0000000000..3b78e827d7 --- /dev/null +++ b/shared/bazel/rules/robotpy/hatchlib_native_port/BUILD.bazel @@ -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"), + ], +) diff --git a/shared/bazel/rules/robotpy/hatchlib_native_port/README.md b/shared/bazel/rules/robotpy/hatchlib_native_port/README.md new file mode 100644 index 0000000000..0d06d0be98 --- /dev/null +++ b/shared/bazel/rules/robotpy/hatchlib_native_port/README.md @@ -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. diff --git a/shared/bazel/rules/robotpy/hatchlib_native_port/config.py b/shared/bazel/rules/robotpy/hatchlib_native_port/config.py new file mode 100644 index 0000000000..2af52ea3b7 --- /dev/null +++ b/shared/bazel/rules/robotpy/hatchlib_native_port/config.py @@ -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" diff --git a/shared/bazel/rules/robotpy/hatchlib_native_port/generate_native_lib_files.py b/shared/bazel/rules/robotpy/hatchlib_native_port/generate_native_lib_files.py new file mode 100644 index 0000000000..b8f357bef4 --- /dev/null +++ b/shared/bazel/rules/robotpy/hatchlib_native_port/generate_native_lib_files.py @@ -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() diff --git a/shared/bazel/rules/robotpy/hatchlib_native_port/validate.py b/shared/bazel/rules/robotpy/hatchlib_native_port/validate.py new file mode 100644 index 0000000000..f1949ed166 --- /dev/null +++ b/shared/bazel/rules/robotpy/hatchlib_native_port/validate.py @@ -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 diff --git a/shared/bazel/rules/robotpy/pybind_build_file_template.jinja2 b/shared/bazel/rules/robotpy/pybind_build_file_template.jinja2 new file mode 100644 index 0000000000..9edeaf7137 --- /dev/null +++ b/shared/bazel/rules/robotpy/pybind_build_file_template.jinja2 @@ -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", + ) diff --git a/shared/bazel/rules/robotpy/pybind_rules.bzl b/shared/bazel/rules/robotpy/pybind_rules.bzl new file mode 100644 index 0000000000..7b5d91456d --- /dev/null +++ b/shared/bazel/rules/robotpy/pybind_rules.bzl @@ -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: + _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 + - 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: + - 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"], + ) diff --git a/shared/bazel/rules/robotpy/pytest_util.bzl b/shared/bazel/rules/robotpy/pytest_util.bzl new file mode 100644 index 0000000000..322396217e --- /dev/null +++ b/shared/bazel/rules/robotpy/pytest_util.bzl @@ -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 + ) diff --git a/shared/bazel/rules/robotpy/semiwrap_helpers.bzl b/shared/bazel/rules/robotpy/semiwrap_helpers.bzl new file mode 100644 index 0000000000..6c41aee92f --- /dev/null +++ b/shared/bazel/rules/robotpy/semiwrap_helpers.bzl @@ -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"], + ) diff --git a/shared/bazel/rules/robotpy/semiwrap_tool_helpers.bzl b/shared/bazel/rules/robotpy/semiwrap_tool_helpers.bzl new file mode 100644 index 0000000000..fb4840dd18 --- /dev/null +++ b/shared/bazel/rules/robotpy/semiwrap_tool_helpers.bzl @@ -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(), + ) diff --git a/shared/bazel/rules/robotpy/wrapper.py b/shared/bazel/rules/robotpy/wrapper.py new file mode 100644 index 0000000000..a6d0a7cbd7 --- /dev/null +++ b/shared/bazel/rules/robotpy/wrapper.py @@ -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() diff --git a/wpinet/.styleguide b/wpinet/.styleguide index 04f54ebf61..82e0172d37 100644 --- a/wpinet/.styleguide +++ b/wpinet/.styleguide @@ -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 { diff --git a/wpinet/BUILD.bazel b/wpinet/BUILD.bazel index 3543a4edd4..df8e3ac5d8 100644 --- a/wpinet/BUILD.bazel +++ b/wpinet/BUILD.bazel @@ -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", diff --git a/wpinet/robotpy_native_build_info.bzl b/wpinet/robotpy_native_build_info.bzl new file mode 100644 index 0000000000..4b1633b9d2 --- /dev/null +++ b/wpinet/robotpy_native_build_info.bzl @@ -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/", + ) diff --git a/wpinet/robotpy_pybind_build_info.bzl b/wpinet/robotpy_pybind_build_info.bzl new file mode 100644 index 0000000000..d41dabf406 --- /dev/null +++ b/wpinet/robotpy_pybind_build_info.bzl @@ -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", + ) diff --git a/wpinet/src/main/python/README.md b/wpinet/src/main/python/README.md new file mode 100644 index 0000000000..6913b021d0 --- /dev/null +++ b/wpinet/src/main/python/README.md @@ -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) diff --git a/wpinet/src/main/python/examples/portfwd.py b/wpinet/src/main/python/examples/portfwd.py new file mode 100755 index 0000000000..7c4efcb5d4 --- /dev/null +++ b/wpinet/src/main/python/examples/portfwd.py @@ -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) diff --git a/wpinet/src/main/python/native-pyproject.toml b/wpinet/src/main/python/native-pyproject.toml new file mode 100644 index 0000000000..91880f387b --- /dev/null +++ b/wpinet/src/main/python/native-pyproject.toml @@ -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"] diff --git a/wpinet/src/main/python/pyproject.toml b/wpinet/src/main/python/pyproject.toml new file mode 100644 index 0000000000..d7df88c5b1 --- /dev/null +++ b/wpinet/src/main/python/pyproject.toml @@ -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" diff --git a/wpinet/src/main/python/semiwrap/PortForwarder.yml b/wpinet/src/main/python/semiwrap/PortForwarder.yml new file mode 100644 index 0000000000..64e172eae9 --- /dev/null +++ b/wpinet/src/main/python/semiwrap/PortForwarder.yml @@ -0,0 +1,8 @@ +classes: + wpi::PortForwarder: + nodelete: true + methods: + GetInstance: + return_value_policy: reference + Add: + Remove: diff --git a/wpinet/src/main/python/semiwrap/WebServer.yml b/wpinet/src/main/python/semiwrap/WebServer.yml new file mode 100644 index 0000000000..61ac088751 --- /dev/null +++ b/wpinet/src/main/python/semiwrap/WebServer.yml @@ -0,0 +1,8 @@ +classes: + wpi::WebServer: + nodelete: true + methods: + GetInstance: + return_value_policy: reference + Start: + Stop: diff --git a/wpinet/src/main/python/wpinet/__init__.py b/wpinet/src/main/python/wpinet/__init__.py new file mode 100644 index 0000000000..b3261c177a --- /dev/null +++ b/wpinet/src/main/python/wpinet/__init__.py @@ -0,0 +1,7 @@ +from . import _init__wpinet + + +# autogenerated by 'semiwrap create-imports wpinet wpinet._wpinet' +from ._wpinet import PortForwarder, WebServer + +__all__ = ["PortForwarder", "WebServer"] diff --git a/wpinet/src/main/python/wpinet/py.typed b/wpinet/src/main/python/wpinet/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wpinet/src/main/python/wpinet/src/main.cpp b/wpinet/src/main/python/wpinet/src/main.cpp new file mode 100644 index 0000000000..0c1b0adabf --- /dev/null +++ b/wpinet/src/main/python/wpinet/src/main.cpp @@ -0,0 +1,4 @@ + +#include + +SEMIWRAP_PYBIND11_MODULE(m) { initWrapper(m); } diff --git a/wpinet/src/test/python/run_tests.py b/wpinet/src/test/python/run_tests.py new file mode 100755 index 0000000000..2ffc6595d2 --- /dev/null +++ b/wpinet/src/test/python/run_tests.py @@ -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"]) diff --git a/wpinet/src/test/python/test_wpinet.py b/wpinet/src/test/python/test_wpinet.py new file mode 100644 index 0000000000..13a2855ea2 --- /dev/null +++ b/wpinet/src/test/python/test_wpinet.py @@ -0,0 +1,5 @@ +import wpinet + + +def test_existance(): + pass diff --git a/wpiutil/.styleguide b/wpiutil/.styleguide index 6abd1ec7f2..44c2bc5045 100644 --- a/wpiutil/.styleguide +++ b/wpiutil/.styleguide @@ -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$ diff --git a/wpiutil/BUILD.bazel b/wpiutil/BUILD.bazel index 00dbbb62e6..4343b10abc 100644 --- a/wpiutil/BUILD.bazel +++ b/wpiutil/BUILD.bazel @@ -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", diff --git a/wpiutil/robotpy_native_build_info.bzl b/wpiutil/robotpy_native_build_info.bzl new file mode 100644 index 0000000000..7b4c1ef768 --- /dev/null +++ b/wpiutil/robotpy_native_build_info.bzl @@ -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/", + ) diff --git a/wpiutil/robotpy_pybind_build_info.bzl b/wpiutil/robotpy_pybind_build_info.bzl new file mode 100644 index 0000000000..e4f04bdbcc --- /dev/null +++ b/wpiutil/robotpy_pybind_build_info.bzl @@ -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", + ) diff --git a/wpiutil/src/main/python/examples/printlog.py b/wpiutil/src/main/python/examples/printlog.py new file mode 100755 index 0000000000..4db4c33d56 --- /dev/null +++ b/wpiutil/src/main/python/examples/printlog.py @@ -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("") + continue + print(f" [{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) diff --git a/wpiutil/src/main/python/examples/writelog.py b/wpiutil/src/main/python/examples/writelog.py new file mode 100755 index 0000000000..6e35a9e8bd --- /dev/null +++ b/wpiutil/src/main/python/examples/writelog.py @@ -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") diff --git a/wpiutil/src/main/python/native-pyproject.toml b/wpiutil/src/main/python/native-pyproject.toml new file mode 100644 index 0000000000..2f46fbff39 --- /dev/null +++ b/wpiutil/src/main/python/native-pyproject.toml @@ -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'" diff --git a/wpiutil/src/main/python/pyproject.toml b/wpiutil/src/main/python/pyproject.toml new file mode 100644 index 0000000000..f59780a69e --- /dev/null +++ b/wpiutil/src/main/python/pyproject.toml @@ -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"] diff --git a/wpiutil/src/main/python/semiwrap/RawFrame.yml b/wpiutil/src/main/python/semiwrap/RawFrame.yml new file mode 100644 index 0000000000..71b74c2843 --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/RawFrame.yml @@ -0,0 +1,7 @@ +defaults: + ignore: true + +enums: + WPI_TimestampSource: + value_prefix: WPI_TIMESRC + rename: TimestampSource diff --git a/wpiutil/src/main/python/semiwrap/Sendable.yml b/wpiutil/src/main/python/semiwrap/Sendable.yml new file mode 100644 index 0000000000..c146a29110 --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/Sendable.yml @@ -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); + } diff --git a/wpiutil/src/main/python/semiwrap/SendableBuilder.yml b/wpiutil/src/main/python/semiwrap/SendableBuilder.yml new file mode 100644 index 0000000000..1a52df427d --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/SendableBuilder.yml @@ -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: diff --git a/wpiutil/src/main/python/semiwrap/SendableRegistry.yml b/wpiutil/src/main/python/semiwrap/SendableRegistry.yml new file mode 100644 index 0000000000..65e97a169f --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/SendableRegistry.yml @@ -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: diff --git a/wpiutil/src/main/python/semiwrap/StackTrace.yml b/wpiutil/src/main/python/semiwrap/StackTrace.yml new file mode 100644 index 0000000000..867f0c784f --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/StackTrace.yml @@ -0,0 +1,5 @@ +functions: + GetStackTrace: + GetStackTraceDefault: + SetGetStackTraceImpl: + ignore: true diff --git a/wpiutil/src/main/python/semiwrap/Synchronization.yml b/wpiutil/src/main/python/semiwrap/Synchronization.yml new file mode 100644 index 0000000000..633c7d56ca --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/Synchronization.yml @@ -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, std::span: + param_override: + signaled: + ignore: true + cpp_code: | + [](std::span handles) { + py::gil_scoped_release release; + std::vector signaled(handles.size()); + auto result = wpi::WaitForObjects(handles, signaled); + signaled.resize(result.size()); + return signaled; + } + std::initializer_list, std::span: + ignore: true + std::span, std::span, double, bool*: + param_override: + signaled: + ignore: true + timedOut: + ignore: true + cpp_code: | + [](std::span handles, double timeout) { + py::gil_scoped_release release; + std::vector 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, std::span, double, bool*: + ignore: true + CreateSignalObject: + SetSignalObject: + ResetSignalObject: + DestroySignalObject: diff --git a/wpiutil/src/main/python/semiwrap/WPyStruct.yml b/wpiutil/src/main/python/semiwrap/WPyStruct.yml new file mode 100644 index 0000000000..0cad6599e1 --- /dev/null +++ b/wpiutil/src/main/python/semiwrap/WPyStruct.yml @@ -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 diff --git a/wpiutil/src/main/python/wpiutil/__init__.py b/wpiutil/src/main/python/wpiutil/__init__.py new file mode 100644 index 0000000000..6f5e8ab43c --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/__init__.py @@ -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] diff --git a/wpiutil/src/main/python/wpiutil/_stacktrace.py b/wpiutil/src/main/python/wpiutil/_stacktrace.py new file mode 100644 index 0000000000..7d2f847065 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/_stacktrace.py @@ -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 \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) diff --git a/wpiutil/src/main/python/wpiutil/py.typed b/wpiutil/src/main/python/wpiutil/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wpiutil/src/main/python/wpiutil/src/main.cpp b/wpiutil/src/main/python/wpiutil/src/main.cpp new file mode 100644 index 0000000000..c1ab2b42b9 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/main.cpp @@ -0,0 +1,41 @@ + +#include + +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); +} \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/safethread_gil.cpp b/wpiutil/src/main/python/wpiutil/src/safethread_gil.cpp new file mode 100644 index 0000000000..c7bb7ba702 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/safethread_gil.cpp @@ -0,0 +1,65 @@ + +#include +#include +#include + +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 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; } diff --git a/wpiutil/src/main/python/wpiutil/src/stacktracehook.cpp b/wpiutil/src/main/python/wpiutil/src/stacktracehook.cpp new file mode 100644 index 0000000000..97e5602295 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/stacktracehook.cpp @@ -0,0 +1,45 @@ + +#include +#include + +py::object &get_hook_ref() { + static py::object hook; + return hook; +} + +std::string final_py_stack_trace_hook(int offset) { + std::string msg = "\tat \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(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(); + } +} \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_array_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_array_type_caster.h new file mode 100644 index 0000000000..0c3d619225 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_array_type_caster.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +namespace pybind11 { +namespace detail { + +template +struct wpi_array_name_maker { + template + static constexpr auto make(const T &t) { + return concat(t, wpi_array_name_maker::make(t)); + } +}; + +template <> +struct wpi_array_name_maker<1> { + template + static constexpr auto make(const T &t) { + return t; + } +}; + +template +struct type_caster> { + using value_conv = make_caster; + + // Have to copy/paste PYBIND11_TYPE_CASTER implementation because wpi::array + // is not default constructable + // + // begin PYBIND11_TYPE_CASTER +protected: + wpi::array 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::make(value_conv::name) + const_name("]"); + template < + typename T_, + enable_if_t, remove_cv_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 *() { return &value; } + operator wpi::array &() { return value; } + operator wpi::array &&() && { return std::move(value); } + template + using cast_op_type = pybind11::detail::movable_cast_op_type; + // end PYBIND11_TYPE_CASTER + + bool load(handle src, bool convert) { + if (!isinstance(src)) + return false; + auto l = reinterpret_borrow(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(std::move(conv)); + } + return true; + } + + template + 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( + value_conv::cast(forward_like(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 \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_ct_string_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_ct_string_type_caster.h new file mode 100644 index 0000000000..702afcf52a --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_ct_string_type_caster.h @@ -0,0 +1,61 @@ + +#pragma once + +#include + +#include + +namespace pybind11 { +namespace detail { + +template +struct type_caster> { + using str_type = wpi::ct_string; + 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(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 +// struct type_caster> +// : string_caster, false> {}; + +} // namespace detail +} // namespace pybind11 diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_json_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_json_type_caster.h new file mode 100644 index 0000000000..66c87ab713 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_json_type_caster.h @@ -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 +#include + +#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()); + } + else if (j.is_number_unsigned()) + { + return py::int_(j.get()); + } + else if (j.is_number_integer()) + { + return py::int_(j.get()); + } + else if (j.is_number_float()) + { + return py::float_(j.get()); + } + else if (j.is_string()) + { + return py::str(j.get()); + } + 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(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj)) + { + try + { + number_integer_t s = obj.cast(); + if (py::int_(s).equal(obj)) + { + return s; + } + } + catch (...) + { + } + try + { + number_unsigned_t u = obj.cast(); + 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()); + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + // if (py::isinstance(obj)) + // { + // py::module base64 = py::module::import("base64"); + // return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast(); + // } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj) || py::isinstance(obj)) + { + auto out = wpi::json::array(); + for (const py::handle value : obj) + { + out.push_back(to_json(value)); + } + return out; + } + if (py::isinstance(obj)) + { + auto out = wpi::json::object(); + for (const py::handle key : obj) + { + if (py::isinstance(key)) { + out[key.cast()] = to_json(obj[key]); + + } else if (py::isinstance(key) || py::isinstance(key) || + py::isinstance(key) || py::isinstance(key)) { + // only allow the same implicit conversions python allows + out[py::str(key).cast()] = to_json(obj[key]); + } else { + throw py::type_error("JSON keys must be str, int, float, bool, or None, not " + py::repr(key).cast()); + } + } + return out; + } + throw py::type_error("Object of type " + py::type::of(obj).attr("__name__").cast() + " is not JSON serializable"); + } +} + +// nlohmann_json serializers +namespace wpi +{ + #define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \ + template <> \ + struct adl_serializer \ + { \ + 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 \ + { \ + 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 + { + 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(); + } + }; + } +} diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallset_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallset_type_caster.h new file mode 100644 index 0000000000..e3c31e1611 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallset_type_caster.h @@ -0,0 +1,18 @@ + +#pragma once + +#include +#include + +#include + +namespace pybind11 +{ +namespace detail +{ + +template struct type_caster> + : set_caster, Type> { }; + +} // namespace detail +} // namespace pybind11 \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvector_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvector_type_caster.h new file mode 100644 index 0000000000..7c899be47b --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvector_type_caster.h @@ -0,0 +1,18 @@ + +#pragma once + +#include +#include + +#include + +namespace pybind11 +{ +namespace detail +{ + +template struct type_caster> + : list_caster, Type> { }; + +} // namespace detail +} // namespace pybind11 \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvectorimpl_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvectorimpl_type_caster.h new file mode 100644 index 0000000000..5ab35e87ef --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_smallvectorimpl_type_caster.h @@ -0,0 +1,71 @@ + +#pragma once + +#include +#include +#include + +namespace pybind11 +{ +namespace detail +{ + + +template struct type_caster> { + using value_conv = make_caster; + +// Have to copy/paste PYBIND11_TYPE_CASTER implementation because SmallVectorImpl +// is not default constructable +// +// begin PYBIND11_TYPE_CASTER +protected: + wpi::SmallVector value; +public: + static constexpr auto name = _("List[") + value_conv::name + _("]"); + template , remove_cv_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*() { return &value; } + operator wpi::SmallVectorImpl&() { return value; } + operator wpi::SmallVectorImpl&&() && { return std::move(value); } + template using cast_op_type = pybind11::detail::movable_cast_op_type; +// end PYBIND11_TYPE_CASTER + + bool load(handle src, bool convert) { + if (!isinstance(src) || isinstance(src)) + return false; + auto s = reinterpret_borrow(src); + value.reserve(s.size()); + for (auto it : s) { + value_conv conv; + if (!conv.load(it, convert)) + return false; + value.push_back(cast_op(std::move(conv))); + } + return true; + } + + template + static handle cast(T &&src, return_value_policy policy, handle parent) { + if (!std::is_lvalue_reference::value) + policy = return_value_policy_override::policy(policy); + list l(src.size()); + size_t index = 0; + for (auto &&value : src) { + auto value_ = reinterpret_steal(value_conv::cast(forward_like(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 diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_span_type_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_span_type_caster.h new file mode 100644 index 0000000000..99397a5cfd --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_span_type_caster.h @@ -0,0 +1,172 @@ + +#pragma once + +#include +#include + +#include +#include + +namespace pybind11 { +namespace detail { + +template +struct span_name_maker { + template + static constexpr auto make(const T &t) { + return concat(t, span_name_maker::make(t)); + } +}; + +template <> +struct span_name_maker<1> { + template + static constexpr auto make(const T &t) { + return t; + } +}; + +// span with fixed size converts to a tuple +template struct type_caster> { + using span_type = typename std::span; + using value_conv = make_caster; + using value_type = typename std::remove_cv::type; + + value_type backing_array[Extent] = {}; + + PYBIND11_TYPE_CASTER(span_type, _("Tuple[") + span_name_maker::make(value_conv::name) + _("]")); + + type_caster() : value(backing_array) {} + + bool load(handle src, bool convert) { + if (!isinstance(src) || isinstance(src)) + return false; + auto s = reinterpret_borrow(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(std::move(conv)); + i++; + } + return true; + } + +public: + template + static handle cast(T &&src, return_value_policy policy, handle parent) { + if (!std::is_lvalue_reference::value) + policy = return_value_policy_override::policy(policy); + tuple l(Extent); + size_t index = 0; + for (auto &&value : src) { + auto value_ = reinterpret_steal( + value_conv::cast(forward_like(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 struct type_caster> { + using span_type = typename std::span; + using value_conv = make_caster; + using value_type = typename std::remove_cv::type; + PYBIND11_TYPE_CASTER(span_type, _("List[") + value_conv::name + _("]")); + + wpi::SmallVector vec; + bool load(handle src, bool convert) { + if (!isinstance(src) || isinstance(src)) + return false; + auto s = reinterpret_borrow(src); + vec.reserve(s.size()); + for (auto it : s) { + value_conv conv; + if (!conv.load(it, convert)) + return false; + vec.push_back(cast_op(std::move(conv))); + } + value = span_type(std::data(vec), std::size(vec)); + return true; + } + +public: + template + static handle cast(T &&src, return_value_policy policy, handle parent) { + if (!std::is_lvalue_reference::value) + policy = return_value_policy_override::policy(policy); + list l(src.size()); + size_t index = 0; + for (auto &&value : src) { + auto value_ = reinterpret_steal( + value_conv::cast(forward_like(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> { + using span_type = typename std::span; + PYBIND11_TYPE_CASTER(span_type, _("Buffer")); + + bool load(handle src, bool convert) { + if (!isinstance(src)) + return false; + auto buf = reinterpret_borrow(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 + 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> { + using span_type = typename std::span; + PYBIND11_TYPE_CASTER(std::span, _("Buffer")); + + bool load(handle src, bool convert) { + if (!isinstance(src)) + return false; + auto buf = reinterpret_borrow(src); + auto req = buf.request(true); // buffer must be writeable + if (req.ndim != 1) { + return false; + } + + value = std::span((uint8_t*)req.ptr, req.size*req.itemsize); + return true; + } + +public: + template + 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 \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_string_map_caster.h b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_string_map_caster.h new file mode 100644 index 0000000000..6ffc2eb724 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/type_casters/wpi_string_map_caster.h @@ -0,0 +1,19 @@ + +#pragma once + +#include +#include + +#include + +namespace pybind11 +{ +namespace detail +{ + +template +struct type_caster> + : map_caster, std::string, Value> { }; + +} // namespace detail +} // namespace pybind11 \ No newline at end of file diff --git a/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct.h b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct.h new file mode 100644 index 0000000000..f9b3876c89 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct.h @@ -0,0 +1,317 @@ + +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include + +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 { + // 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(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 data, const WPyStruct &value) const = 0; + + virtual WPyStruct Unpack(std::span data) const = 0; + + // virtual void UnpackInto(WPyStruct *pyv, + // std::span data) const = 0; + + virtual void ForEachNested( + const std::function &fn) + const = 0; +}; + +// static C++ converter +template struct WPyStructCppConverter : WPyStructConverter { + std::string_view GetTypeName() const override { + return wpi::Struct::GetTypeName(); + } + + size_t GetSize() const override { return wpi::Struct::GetSize(); } + + std::string_view GetSchema() const override { + return wpi::Struct::GetSchema(); + } + + void Pack(std::span data, const WPyStruct &value) const override { + py::gil_scoped_acquire gil; + const T &v = value.py.cast(); + wpi::Struct::Pack(data, v); + } + + WPyStruct Unpack(std::span data) const override { + py::gil_scoped_acquire gil; + return WPyStruct{py::cast(wpi::UnpackStruct(data))}; + } + + // void UnpackInto(WPyStruct *pyv, + // std::span data) const override { + // py::gil_scoped_acquire gil; + // T *v = pyv->py.cast(); + // wpi::UnpackStructInto(v, data); + // } + + void ForEachNested( + const std::function &fn) + const override { + if constexpr (wpi::HasNestedStruct) { + wpi::Struct::ForEachNested(fn); + } + } +}; + +template void SetupWPyStruct(auto pycls) { + + auto *sptr = + new std::shared_ptr(new WPyStructCppConverter()); + + py::capsule c(sptr, "WPyStruct", [](void *ptr) { + delete (std::shared_ptr *)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(); + m_schema = o.attr("schema").cast(); + m_size = o.attr("size").cast(); + + m_pack = py::reinterpret_borrow(o.attr("pack")); + m_packInto = py::reinterpret_borrow(o.attr("packInto")); + m_unpack = py::reinterpret_borrow(o.attr("unpack")); + // m_unpackInto = py::reinterpret_borrow(o.attr("unpackInto")); + m_forEachNested = + py::reinterpret_borrow(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 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 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 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 &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 *)c; + return; + } + + PyErr_Clear(); + + // Python version + try { + cvt = std::make_shared(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 cvt; +}; + +// Leverages the converter stored in WPyStructInfo to do the actual work +template <> struct wpi::Struct { + 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 data, + const WPyStructInfo &info) { + return info->Unpack(data); + } + + // static void UnpackInto(WPyStruct *v, std::span data, + // const WPyStructInfo &info) { + // info->UnpackInto(v, data); + // } + + static void Pack(std::span data, const WPyStruct &value, + const WPyStructInfo &info) { + info->Pack(data, value); + } + + static void + ForEachNested(std::invocable auto fn, + const WPyStructInfo &info) { + info->ForEachNested(fn); + } +}; + +static_assert(wpi::StructSerializable); +static_assert(wpi::HasNestedStruct); + +// This breaks on readonly structs, so we disable for now +// static_assert(wpi::MutableStructSerializable); diff --git a/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.cpp b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.cpp new file mode 100644 index 0000000000..730cb71938 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.cpp @@ -0,0 +1,166 @@ + +#include "wpystruct.h" + +void forEachNested( + const py::type &t, + const std::function &fn) { + WPyStructInfo info(t); + wpi::ForEachStructSchema(fn, info); +} + +py::str getTypeName(const py::type &t) { + WPyStructInfo info(t); + return wpi::GetStructTypeName(info); +} + +py::str getSchema(const py::type &t) { + WPyStructInfo info(t); + return wpi::GetStructSchema(info); +} + +size_t getSize(const py::type &t) { + WPyStructInfo info(t); + return wpi::GetStructSize(info); +} + +py::bytes pack(const WPyStruct &v) { + WPyStructInfo info(v); + + auto sz = wpi::GetStructSize(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(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(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(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(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(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(s, info); +} + +py::typing::List unpackArray(const py::type &t, const py::buffer &b) { + WPyStructInfo info(t); + py::ssize_t sz = wpi::GetStructSize(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(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(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(v, s, info); +// } diff --git a/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h new file mode 100644 index 0000000000..f03f81463c --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/src/wpistruct/wpystruct_fns.h @@ -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 &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 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); diff --git a/wpiutil/src/main/python/wpiutil/sync/__init__.py b/wpiutil/src/main/python/wpiutil/sync/__init__.py new file mode 100644 index 0000000000..6c6285852d --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/sync/__init__.py @@ -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", +] diff --git a/wpiutil/src/main/python/wpiutil/wpistruct/__init__.py b/wpiutil/src/main/python/wpiutil/wpistruct/__init__.py new file mode 100644 index 0000000000..6cfffe5b64 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/wpistruct/__init__.py @@ -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", +] diff --git a/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py b/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py new file mode 100644 index 0000000000..160db3e7e1 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py @@ -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 diff --git a/wpiutil/src/main/python/wpiutil/wpistruct/desc.py b/wpiutil/src/main/python/wpiutil/wpistruct/desc.py new file mode 100644 index 0000000000..e75a3a5577 --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/wpistruct/desc.py @@ -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] + ] diff --git a/wpiutil/src/main/python/wpiutil/wpistruct/typing.py b/wpiutil/src/main/python/wpiutil/wpistruct/typing.py new file mode 100644 index 0000000000..83359c01be --- /dev/null +++ b/wpiutil/src/main/python/wpiutil/wpistruct/typing.py @@ -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") diff --git a/wpiutil/src/test/python/conftest.py b/wpiutil/src/test/python/conftest.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/wpiutil/src/test/python/conftest.py @@ -0,0 +1 @@ + diff --git a/wpiutil/src/test/python/cpp/pyproject.toml b/wpiutil/src/test/python/cpp/pyproject.toml new file mode 100644 index 0000000000..155ad6e19c --- /dev/null +++ b/wpiutil/src/test/python/cpp/pyproject.toml @@ -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] diff --git a/wpiutil/src/test/python/cpp/wpiutil_test/__init__.py b/wpiutil/src/test/python/cpp/wpiutil_test/__init__.py new file mode 100644 index 0000000000..9eaa5eb370 --- /dev/null +++ b/wpiutil/src/test/python/cpp/wpiutil_test/__init__.py @@ -0,0 +1 @@ +import wpiutil diff --git a/wpiutil/src/test/python/cpp/wpiutil_test/module.cpp b/wpiutil/src/test/python/cpp/wpiutil_test/module.cpp new file mode 100644 index 0000000000..ca045a8a0c --- /dev/null +++ b/wpiutil/src/test/python/cpp/wpiutil_test/module.cpp @@ -0,0 +1,195 @@ +// clang-format off +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +/* +array tests +*/ +wpi::array load_array_int(wpi::array data) { + return data; +} + +wpi::array load_array_int1(wpi::array data) { + return data; +} + +/* +span Tests +*/ +std::span load_span_int(std::span ref) { + return ref; +} + +std::span load_span_bool(std::span ref) { + return ref; +} + +std::span load_span_string(std::span ref) { + return ref; +} + +std::span load_span_string_const(std::span ref) { + return ref; +} + +std::span load_span_string_view(std::span ref) { + return ref; +} + +std::span> load_span_vector(std::span> ref) { + return ref; +} + +std::span load_span_fixed_double(std::span ref) { + return ref; +} + + +std::span cast_span() { + static std::vector vec{1, 2, 3}; + return vec; +} + +std::span make_string_span() { + static std::vector vec{"hi", "there"}; + return vec; +} + +py::object cast_string_span() { + return py::cast(make_string_span()); +} + +std::span load_span_bytes(std::span ref) { + return ref; +} + +void modify_span_buffer(std::span ref) { + ref[0] = 0x4; +} + +/* +SmallSet tests +*/ + +wpi::SmallSet load_smallset_int(wpi::SmallSet ref) { + return ref; +} + +wpi::SmallSet cast_smallset() { + static wpi::SmallSet set; + set.insert(1); + set.insert(2); + set.insert(3); + set.insert(4); + return set; +} + +/* +SmallVector tests +*/ + +wpi::SmallVector load_smallvec_int(wpi::SmallVector ref) { + return ref; +} + +wpi::SmallVector cast_smallvec() { + static wpi::SmallVector 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& load_smallvecimpl_int(wpi::SmallVectorImpl& ref) { + static wpi::SmallVector set(ref.begin(), ref.end()); + return set; +} + +/* +StringMap tests +*/ +wpi::StringMap load_stringmap_int(wpi::StringMap ref) { + return ref; +} + +wpi::StringMap cast_stringmap() { + static wpi::StringMap 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 fn) { + return fn(); +} + +constexpr auto const_string() { + return wpi::ct_string, 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::max(); + m.attr("max_int64") = std::numeric_limits::max(); + m.attr("min_int64") = std::numeric_limits::min(); + // ct_string + m.def("const_string", &const_string); +}; diff --git a/wpiutil/src/test/python/cpp/wpiutil_test/sendable_test.cpp b/wpiutil/src/test/python/cpp/wpiutil_test/sendable_test.cpp new file mode 100644 index 0000000000..bc15a6053f --- /dev/null +++ b/wpiutil/src/test/python/cpp/wpiutil_test/sendable_test.cpp @@ -0,0 +1,154 @@ + +#include +#include +#include +#include +#include + +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 getter, + std::function setter) override {} + + void PublishConstBoolean(std::string_view key, bool value) override {} + + void AddIntegerProperty(std::string_view key, std::function getter, + std::function setter) override {} + + void PublishConstInteger(std::string_view key, int64_t value) override {} + + void AddFloatProperty(std::string_view key, std::function getter, + std::function setter) override {} + + void PublishConstFloat(std::string_view key, float value) override {} + + void AddDoubleProperty(std::string_view key, std::function getter, + std::function 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 getter, + std::function setter) override {} + + void PublishConstString(std::string_view key, + std::string_view value) override {} + + void AddBooleanArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override {} + + void PublishConstBooleanArray(std::string_view key, + std::span value) override {} + + void AddIntegerArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override {} + + void PublishConstIntegerArray(std::string_view key, + std::span value) override {} + + void AddFloatArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override {} + + void PublishConstFloatArray(std::string_view key, + std::span value) override {} + + void AddDoubleArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override {} + + void PublishConstDoubleArray(std::string_view key, + std::span value) override {} + + void AddStringArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override {} + + void PublishConstStringArray(std::string_view key, + std::span value) override {} + + void AddRawProperty( + std::string_view key, std::string_view typeString, + std::function()> getter, + std::function)> setter) override {} + + void PublishConstRaw(std::string_view key, std::string_view typeString, + std::span value) override {} + + void AddSmallStringProperty( + std::string_view key, + std::function &buf)> getter, + std::function setter) override {} + + void AddSmallBooleanArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl &buf)> + getter, + std::function)> setter) override {} + + void AddSmallIntegerArrayProperty( + std::string_view key, + std::function< + std::span(wpi::SmallVectorImpl &buf)> + getter, + std::function)> setter) override {} + + void AddSmallFloatArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl &buf)> + getter, + std::function)> setter) override {} + + void AddSmallDoubleArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl &buf)> + getter, + std::function)> setter) override {} + + void AddSmallStringArrayProperty( + std::string_view key, + std::function< + std::span(wpi::SmallVectorImpl &buf)> + getter, + std::function)> setter) override {} + + void AddSmallRawProperty( + std::string_view key, std::string_view typeString, + std::function(wpi::SmallVectorImpl &buf)> + getter, + std::function)> 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(keys); + wpi::SendableRegistry::Publish(sendableUid, std::move(builder)); +} + +void sendable_test(py::module &m) { m.def("publish", Publish); } \ No newline at end of file diff --git a/wpiutil/src/test/python/cpp/wpiutil_test/struct_test.cpp b/wpiutil/src/test/python/cpp/wpiutil_test/struct_test.cpp new file mode 100644 index 0000000000..200822e9f0 --- /dev/null +++ b/wpiutil/src/test/python/cpp/wpiutil_test/struct_test.cpp @@ -0,0 +1,84 @@ + +#include +#include + +// +// 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 { + 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 data) { + return ThingA{data[0]}; + } + static void Pack(std::span 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 { + static constexpr std::string_view GetTypeName() { return "Outer"; } + static constexpr size_t GetSize() { return wpi::GetStructSize() + 4; } + static constexpr std::string_view GetSchema() { + return "ThingA inner; int32 c"; + } + + static Outer Unpack(std::span data) { + constexpr size_t innerSize = wpi::GetStructSize(); + return {wpi::UnpackStruct(data), + wpi::UnpackStruct(data)}; + } + static void Pack(std::span data, const Outer& value) { + constexpr size_t innerSize = wpi::GetStructSize(); + wpi::PackStruct<0>(data, value.inner); + wpi::PackStruct(data, value.c); + } + static void ForEachNested( + std::invocable auto fn) { + wpi::ForEachStructSchema(fn); + } +}; + +void struct_test(py::module &m) { + + py::class_ thingCls(m, "ThingA"); + thingCls.def(py::init<>()); + thingCls.def(py::init()); + thingCls.def_readonly("x", &ThingA::x); + thingCls.def(py::self == py::self); + + SetupWPyStruct(thingCls); + + py::class_ outerCls(m, "Outer"); + outerCls.def(py::init<>()); + outerCls.def(py::init()); + outerCls.def_readonly("inner", &Outer::inner); + outerCls.def_readwrite("c", &Outer::c); + outerCls.def(py::self == py::self); + + SetupWPyStruct(outerCls); +} \ No newline at end of file diff --git a/wpiutil/src/test/python/run_tests.py b/wpiutil/src/test/python/run_tests.py new file mode 100755 index 0000000000..80c5f49df8 --- /dev/null +++ b/wpiutil/src/test/python/run_tests.py @@ -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"]) diff --git a/wpiutil/src/test/python/test_array.py b/wpiutil/src/test/python/test_array.py new file mode 100644 index 0000000000..1765b24e2f --- /dev/null +++ b/wpiutil/src/test/python/test_array.py @@ -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" + ) diff --git a/wpiutil/src/test/python/test_ct_string.py b/wpiutil/src/test/python/test_ct_string.py new file mode 100644 index 0000000000..014c7f5d60 --- /dev/null +++ b/wpiutil/src/test/python/test_ct_string.py @@ -0,0 +1,5 @@ +from wpiutil_test import module + + +def test_const_string(): + assert module.const_string() == "#12" diff --git a/wpiutil/src/test/python/test_json.py b/wpiutil/src/test/python/test_json.py new file mode 100644 index 0000000000..bbf5580c9f --- /dev/null +++ b/wpiutil/src/test/python/test_json.py @@ -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}) diff --git a/wpiutil/src/test/python/test_sendable.py b/wpiutil/src/test/python/test_sendable.py new file mode 100644 index 0000000000..5f80e33726 --- /dev/null +++ b/wpiutil/src/test/python/test_sendable.py @@ -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 diff --git a/wpiutil/src/test/python/test_smallset.py b/wpiutil/src/test/python/test_smallset.py new file mode 100644 index 0000000000..f36355d763 --- /dev/null +++ b/wpiutil/src/test/python/test_smallset.py @@ -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} diff --git a/wpiutil/src/test/python/test_smallvector.py b/wpiutil/src/test/python/test_smallvector.py new file mode 100644 index 0000000000..e7fbd1b4ea --- /dev/null +++ b/wpiutil/src/test/python/test_smallvector.py @@ -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] diff --git a/wpiutil/src/test/python/test_span.py b/wpiutil/src/test/python/test_span.py new file mode 100644 index 0000000000..eb97a3d151 --- /dev/null +++ b/wpiutil/src/test/python/test_span.py @@ -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]) diff --git a/wpiutil/src/test/python/test_stacktrace.py b/wpiutil/src/test/python/test_stacktrace.py new file mode 100644 index 0000000000..f9584c1d41 --- /dev/null +++ b/wpiutil/src/test/python/test_stacktrace.py @@ -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 diff --git a/wpiutil/src/test/python/test_stringmap.py b/wpiutil/src/test/python/test_stringmap.py new file mode 100644 index 0000000000..7cc2f6d48c --- /dev/null +++ b/wpiutil/src/test/python/test_stringmap.py @@ -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} diff --git a/wpiutil/src/test/python/test_struct.py b/wpiutil/src/test/python/test_struct.py new file mode 100644 index 0000000000..43e70c62aa --- /dev/null +++ b/wpiutil/src/test/python/test_struct.py @@ -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))