main: initial library code

This commit is contained in:
2026-05-09 17:48:09 +03:00
commit 17e99e2f06
13 changed files with 37608 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
.cache/
.coverage
.coverage-report/
.pdm.lock/
.vscode/
**/__pycache__/
**/*.pyc
.pdm-build/
dist/
build/
pytest_cache/
.venv/
var/
.env

19
.style.yapf Normal file
View File

@@ -0,0 +1,19 @@
[style]
based_on_style = pep8
column_limit = 240
blank_line_before_class_docstring = False
blank_lines_around_top_level_definition = 2
blank_line_before_nested_class_or_def = False
continuation_align_style = Fixed
continuation_indent_width = 4
dedent_closing_brackets = True
align_closing_bracket_with_visual_indent = False
split_before_closing_bracket = True
split_before_first_argument = False
coalesce_brackets = True
each_dict_entry_on_separate_line = True
spaces_around_dict_delimiters = True
spaces_before_comment = 1
split_all_comma_separated_values = True
split_before_dict_set_generator = True
allow_split_before_dict_value = False

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# dragiyski-vulkan-registry
Extracts static type data for vulkan objects from Khronos registry XML.

280
pdm.lock generated Normal file
View File

@@ -0,0 +1,280 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default", "dev"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:ea778e61d15d5929eb362fb1ff3225f8e279fe85642318bfdcbca607601d2a8a"
[[metadata.targets]]
requires_python = ">=3.12"
[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."
groups = ["dev"]
marker = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.13.5"
requires_python = ">=3.10"
summary = "Code coverage measurement for Python"
groups = ["dev"]
files = [
{file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
{file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
{file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
{file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
{file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
{file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
{file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
{file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
{file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
{file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
{file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
{file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
{file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
{file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
{file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
{file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
{file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
{file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
{file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
{file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
{file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
{file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
{file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
{file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
{file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
{file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
]
[[package]]
name = "coverage"
version = "7.13.5"
extras = ["toml"]
requires_python = ">=3.10"
summary = "Code coverage measurement for Python"
groups = ["dev"]
dependencies = [
"coverage==7.13.5",
"tomli; python_full_version <= \"3.11.0a6\"",
]
files = [
{file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
{file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
{file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
{file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
{file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
{file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
{file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
{file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
{file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
{file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
{file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
{file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
{file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
{file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
{file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
{file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
{file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
{file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
{file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
{file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
{file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
{file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
{file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
{file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
{file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
{file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
]
[[package]]
name = "iniconfig"
version = "2.3.0"
requires_python = ">=3.10"
summary = "brain-dead simple config-ini parsing"
groups = ["dev"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "packaging"
version = "26.2"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
groups = ["dev"]
files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
]
[[package]]
name = "pluggy"
version = "1.6.0"
requires_python = ">=3.9"
summary = "plugin and hook calling mechanisms for python"
groups = ["dev"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[[package]]
name = "pygments"
version = "2.20.0"
requires_python = ">=3.9"
summary = "Pygments is a syntax highlighting package written in Python."
groups = ["dev"]
files = [
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[[package]]
name = "pytest"
version = "9.0.3"
requires_python = ">=3.10"
summary = "pytest: simple powerful testing with Python"
groups = ["dev"]
dependencies = [
"colorama>=0.4; sys_platform == \"win32\"",
"exceptiongroup>=1; python_version < \"3.11\"",
"iniconfig>=1.0.1",
"packaging>=22",
"pluggy<2,>=1.5",
"pygments>=2.7.2",
"tomli>=1; python_version < \"3.11\"",
]
files = [
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
]
[[package]]
name = "pytest-cov"
version = "7.1.0"
requires_python = ">=3.9"
summary = "Pytest plugin for measuring coverage."
groups = ["dev"]
dependencies = [
"coverage[toml]>=7.10.6",
"pluggy>=1.2",
"pytest>=7",
]
files = [
{file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
{file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
]

44
pyproject.toml Normal file
View File

@@ -0,0 +1,44 @@
[build-system]
requires = ["pdm-backend", "build"]
build-backend = "pdm.backend"
[project]
name = "dragiyski-vulkan-registry"
version = "0.0.1"
description = "Extracts and parses the Vulkan API registry XML file, providing a Python interface to access the data."
readme = "README.md"
authors = [ { name = "Plamen Dragiyski", email = "plamen@dragiyski.org" } ]
requires-python = ">=3.12"
[dependency-groups]
dev = [
"pytest>=9.0.1",
"pytest-cov>=7.0.0",
]
[tool.pdm]
package-dir = "src"
[tool.pdm.build]
includes = ["src"]
excludes = ["tests"]
[tool.pdm.scripts]
test = "pytest"
coverage = "pytest --cov=dragiyski.vulkan --cov-report=html"
[tool.pytest]
minversion="0.9"
testpaths = [
"tests"
]
python_files = [
"test_*.py"
]
pythonpath = ["src"]
[tool.coverage.run]
branch = true
[tool.coverage.html]
directory = ".coverage-report/html"

View File

@@ -0,0 +1,15 @@
from .taxonomy import Taxonomy
def __init__(globals):
import importlib.resources
from .xml import Node, parse_xml
roots: list[Node] = [
parse_xml(file, is_file=True)
for file in (importlib.resources.files() / 'data').iterdir()
]
globals['taxonomy'] = Taxonomy(*roots)
__init__(globals())
del __init__

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,979 @@
from collections import OrderedDict
import re
from .xml import Node
PREFIX_OBJECT_TYPE = 'VK_OBJECT_TYPE_'
PREFIX_DEBUG_REPORT_OBJECT_TYPE = 'VK_DEBUG_REPORT_OBJECT_TYPE_'
class Taxonomy:
def __init__(self, *roots: Node, api: str = 'vulkan'):
self.api = api
self.nodes: dict[str, list] = {}
self.ctypes: set = {
'int8_t',
'int16_t',
'int32_t',
'int64_t',
'uint8_t',
'uint16_t',
'uint32_t',
'uint64_t',
'size_t',
}
self.external_types: set[str] = set(self.ctypes)
self.vulkan_names: dict[str, str] = dict()
self.vulkan_node_info: dict[str, dict] = {
'define': {},
'basetype': {},
'enum': {},
'bitmask': {},
'handle': {},
'struct': {},
'union': {},
'callback': {},
'value': {},
'command': {},
}
self.tags = set()
self.bitmask_bit_map: dict[str, str] = dict()
self.bit_bitmask_map: dict[str, str] = dict()
self.alias_map: dict[str, str] = dict()
self.alias_target_map: dict[str, set[str]] = dict()
self.error_nodes: dict[str, list[Node]] = dict()
self.value_group_map: dict[str, str | None] = dict()
self.group_value_map: dict[str, set] = dict()
self.handle_parent = {}
self.feature_names = set()
self.feature_dependent_map: dict[str, set[str]] = dict()
self.feature_dependency_map: dict[str, set[str]] = dict()
self.feature_require_map: dict[str, set[str]] = dict()
self.feature_deprecate_map: dict[str, set[str]] = dict()
self.feature_remove_map: dict[str, set[str]] = dict()
self.name_feature_map: dict[str, set[str]] = dict()
self.extension_map: dict[str, int] = dict()
self.name_extension_map: dict[int, str] = dict()
self.extension_require_map: dict[str, set[str]] = dict()
self.extension_deprecate_map: dict[str, set[str]] = dict()
self.extension_remove_map: dict[str, set[str]] = dict()
self.api_unsupported_names = set()
self._init_type_node_by_category = {
None: self._init_ctype_node,
'basetype': self._init_basetype_node,
'define': self._init_define_type_node,
'enum': self._init_enum_type_node,
'bitmask': self._init_bitmask_type_node,
'handle': self._init_handle_type_node,
'struct': self._init_struct_type_node,
'union': self._init_union_type_node,
'funcpointer': self._init_callback_type_node,
}
for root in roots:
self._init_tags(root)
self._init_types_nodes(root)
self._init_enums_nodes(root)
self._init_command_nodes(root)
self._init_feature_nodes(root)
self._init_extension_nodes(root)
self._reverse_feature_dependecies()
self._resolve_usage()
self._manage_type_dependencies()
self._resolve_extends()
self._apply_ctype_aliases()
self._init_members()
self._init_arguments()
self._init_handles()
self._init_handle_output()
self._init_handle_destroy()
self._init_io()
def is_valid_api(self, node: Node) -> bool:
if node.has_attribute('api'):
api_set = {x.strip().lower() for x in node.get_attribute('api').split(',')}
if self.api not in api_set:
return False
if node.node_name == 'command' and node.has_attribute('export'):
export_set = {x.strip().lower() for x in node.get_attribute('export').split(',')}
if self.api not in export_set:
return False
return True
def _init_tags(self, root: Node):
for tags_node in root.get_all('tags'):
for tag_node in tags_node.get_all('tag'):
if not tag_node.has_attribute('name'):
self.error_nodes.setdefault('missing_name', []).append(tag_node)
continue
name = tag_node.get_attribute('name')
self.tags.add(name)
def _parse_node_name_from_attribute(self, node: Node) -> str | None:
if not node.has_attribute('name'):
self.error_nodes.setdefault('missing_name', []).append(node)
return None
return node.get_attribute('name')
def _parse_node_name_from_child(self, node: Node) -> str | None:
if 'name' not in node.children:
self.error_nodes.setdefault('missing_name', []).append(node)
return None
return node.get('name').get_text()
def _parse_node_name(self, node: Node) -> str | None:
if node.has_attribute('name'):
return node.get_attribute('name')
if 'name' in node.children:
return node.get('name').get_text()
self.error_nodes.setdefault('missing_name', []).append(node)
return None
def _parse_node_name_for_function(self, node: Node) -> str | None:
if node.has_attribute('name'):
return node.get_attribute('name')
if 'proto' in node.children:
proto_node = node.get('proto')
if 'name' in proto_node.children:
return proto_node.get('name').get_text()
self.error_nodes.setdefault('missing_name', []).append(node)
return None
def _init_types_nodes(self, root: Node):
for types_node in root.get_all('types'):
if not self.is_valid_api(types_node):
continue
for type_node in types_node.get_all('type'):
if not self.is_valid_api(type_node):
continue
if type_node.has_attribute('alias'):
self._init_alias_node(type_node)
continue
category = None
if type_node.has_attribute('category'):
category = type_node.get_attribute('category')
if category in self._init_type_node_by_category:
self._init_type_node_by_category[category](type_node)
def _init_node(self, node: Node, name: str):
self.nodes.setdefault(name, []).append(node)
def _init_alias_node(self, type_node: Node):
if not type_node.has_attribute('name'):
self.error_nodes.setdefault('missing_name', []).append(type_node)
return
name = type_node.get_attribute('name')
target_name = type_node.get_attribute('alias')
self.alias_map[name] = target_name
self.alias_target_map.setdefault(target_name, set()).add(name)
self._init_node(type_node, name)
def _init_ctype_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self._init_node(type_node, name)
self.ctypes.add(name)
self.external_types.add(name)
def _init_basetype_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self._init_node(type_node, name)
self.ctypes.add(name)
self.external_types.add(name)
self._init_node_info(name, 'basetype')
def _init_node_info(self, name: str, type: str):
node_info = self.vulkan_node_info[type].setdefault(name, {})
node_info.setdefault('used_by', set())
self.external_types.discard(name)
def _init_define_type_node(self, type_node: Node):
if '#define' not in type_node.get_text():
return
name = self._parse_node_name(type_node)
if name is None:
return
self._init_node(type_node, name)
self.vulkan_names[name] = 'define'
self._init_node_info(name, 'define')
def _init_enum_type_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
if 'FlagBits' in name:
bitmask_name = name.replace('FlagBits', 'Flags')
self.bitmask_bit_map[bitmask_name] = name
self.bit_bitmask_map[name] = bitmask_name
self.ctypes.add(bitmask_name)
self.vulkan_names[bitmask_name] = 'bitmask'
self._init_node_info(bitmask_name, 'bitmask')
else:
self._init_node(type_node, name)
self.ctypes.add(name)
self.vulkan_names[name] = 'enum'
self._init_node_info(name, 'enum')
def _init_bitmask_type_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self._init_node(type_node, name)
self.ctypes.add(name)
self.vulkan_names[name] = 'bitmask'
self.vulkan_node_info['enum'].pop(name, None)
self._init_node_info(name, 'bitmask')
def _init_handle_type_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self._init_node(type_node, name)
self.ctypes.add(name)
self.vulkan_names[name] = 'handle'
self._init_node_info(name, 'handle')
self.vulkan_node_info['handle'][name].setdefault('uses', set())
def _init_struct_type_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self.vulkan_names[name] = 'struct'
self._init_node_info(name, 'struct')
self.vulkan_node_info['struct'][name]['extends'] = set()
self.vulkan_node_info['struct'][name]['extended_by'] = set()
self._init_complex_type_node(type_node, name, self.vulkan_node_info['struct'][name])
def _init_union_type_node(self, type_node: Node):
name = self._parse_node_name(type_node)
if name is None:
return
self.vulkan_names[name] = 'union'
self._init_node_info(name, 'union')
self._init_complex_type_node(type_node, name, self.vulkan_node_info['union'][name])
def _init_complex_type_node(self, type_node: Node, name: str, node_info: dict):
self.ctypes.add(name)
self._init_node(type_node, name)
node_info.setdefault('uses', set())
node_info.setdefault('input_of', set())
node_info.setdefault('output_of', set())
def _init_callback_type_node(self, type_node: Node):
type_name = self._parse_node_name_for_function(type_node)
if type_name is None:
return
name = type_name
if name.startswith('PFN_'):
name = name[4:]
self._init_node(type_node, name)
self.ctypes.add(type_name)
self.ctypes.add(name)
self.vulkan_names[name] = 'callback'
self._init_node_info(name, 'callback')
self.vulkan_node_info['callback'][name].setdefault('uses', set())
self.vulkan_node_info['callback'][name].setdefault('return_use', None)
self.vulkan_node_info['callback'][name].setdefault('argument_use', set())
if 'proto' in type_node.children:
proto_node = type_node.get('proto')
if 'type' in proto_node.children:
return_type = proto_node.get('type').get_text()
self.vulkan_node_info['callback'][name]['return_use'] = return_type
for param_node in type_node.get_all('param'):
if 'type' not in param_node.children:
continue
param_type = param_node.get('type').get_text()
self.vulkan_node_info['callback'][name]['argument_use'].add(param_type)
def _init_value_node(self, value_node: Node, name: str, group_name: str | None):
self.value_group_map[name] = group_name
self.group_value_map.setdefault(group_name, set()).add(name)
self.vulkan_names[name] = 'value'
self._init_node_info(name, 'value')
self._init_node(value_node, name)
def _init_enums_nodes(self, root: Node):
for enums_node in root.get_all('enums'):
if not self.is_valid_api(enums_node):
continue
if enums_node.has_attribute('alias'):
self._init_alias_node(enums_node)
continue
enums_name = enums_node.get_attribute('name')
if enums_name is None:
self.error_nodes.setdefault('missing_name', []).append(enums_node)
continue
enums_type = enums_node.get_attribute('type')
if enums_type is not None:
if enums_type == 'enum':
self._init_enums_node(enums_node, enums_name)
elif enums_type == 'bitmask':
self._init_bitmask_node(enums_node, enums_name)
else:
enums_type = None
for enum_node in enums_node.get_all('enum'):
if not self.is_valid_api(enum_node):
continue
if enum_node.has_attribute('alias'):
self._init_alias_node(enum_node)
continue
enum_name = enum_node.get_attribute('name')
if enum_name is None:
self.error_nodes.setdefault('missing_name', []).append(enum_node)
continue
group_name = None if enums_type is None else enums_name
self._init_value_node(enum_node, enum_name, group_name)
def _init_enums_node(self, enums_node: Node, enums_name: str):
self._init_node(enums_node, enums_name)
self.ctypes.add(enums_name)
self.vulkan_names[enums_name] = 'enum'
self._init_node_info(enums_name, 'enum')
def _init_bitmask_node(self, enums_node: Node, enums_name: str):
bitmask_name = enums_name
if 'FlagBits' in bitmask_name:
bitmask_name = bitmask_name.replace('FlagBits', 'Flags')
self.bitmask_bit_map[bitmask_name] = enums_name
self.bit_bitmask_map[enums_name] = bitmask_name
self.vulkan_names.pop(enums_name, None)
self.vulkan_node_info['enum'].pop(enums_name, None)
self._init_node(enums_node, bitmask_name)
self.ctypes.add(bitmask_name)
self.ctypes.add(enums_name)
self.vulkan_names[bitmask_name] = 'bitmask'
self.vulkan_node_info['enum'].pop(bitmask_name, None)
self._init_node_info(bitmask_name, 'bitmask')
def _init_command_nodes(self, root: Node):
for commands_node in root.get_all('commands'):
if not self.is_valid_api(commands_node):
continue
for command_node in commands_node.get_all('command'):
if not self.is_valid_api(command_node):
continue
if command_node.has_attribute('alias'):
self._init_alias_node(command_node)
continue
command_name = self._parse_node_name_for_function(command_node)
if command_name is None:
continue
self._init_node(command_node, command_name)
self.ctypes.add(command_name)
self.vulkan_names[command_name] = 'command'
self._init_node_info(command_name, 'command')
self.vulkan_node_info['command'][command_name].setdefault('uses', set())
self.vulkan_node_info['command'][command_name].setdefault('return_use', None)
self.vulkan_node_info['command'][command_name].setdefault('argument_use', set())
if 'proto' in command_node.children:
proto_node = command_node.get('proto')
if 'type' in proto_node.children:
return_type = proto_node.get('type').get_text()
self.vulkan_node_info['command'][command_name]['return_use'] = return_type
for param_node in command_node.get_all('param'):
if 'type' not in param_node.children:
continue
param_type = param_node.get('type').get_text()
self.vulkan_node_info['command'][command_name]['argument_use'].add(param_type)
def _init_feature_nodes(self, root: Node):
for feature_node in root.get_all('feature'):
if not self.is_valid_api(feature_node):
continue
feature_name = feature_node.get_attribute('name')
if feature_name is None:
self.error_nodes.setdefault('missing_name', []).append(feature_node)
continue
self.feature_dependency_map.setdefault(feature_name, set())
if feature_node.get_attribute('apitype') != 'internal':
self.feature_names.add(feature_name)
self.feature_require_map.setdefault(feature_name, set())
self.feature_deprecate_map.setdefault(feature_name, set())
self.feature_remove_map.setdefault(feature_name, set())
if feature_node.has_attribute('depends'):
depends = {
x.strip()
for x in feature_node.get_attribute('depends').split(',')
}
self.feature_dependency_map[feature_name].update(depends)
self._init_inclusion_nodes(
feature_node,
feature_name,
self.feature_require_map,
self.feature_deprecate_map,
self.feature_remove_map,
self.name_feature_map
)
def _init_extension_nodes(self, root: Node):
for extensions_node in root.get_all('extensions'):
if not self.is_valid_api(extensions_node):
continue
for extension_node in extensions_node.get_all('extension'):
if not self.is_valid_api(extension_node):
continue
extension_name = extension_node.get_attribute('name')
if extension_name is None:
self.error_nodes.setdefault('missing_name', []).append(extension_node)
continue
try:
extension_number = int(extension_node.get_attribute('number'))
except ValueError:
self.error_nodes.setdefault('invalid_extension_number', []).append(extension_node)
continue
self.extension_map[extension_name] = extension_number
self.extension_require_map.setdefault(extension_name, set())
self.extension_deprecate_map.setdefault(extension_name, set())
self.extension_remove_map.setdefault(extension_name, set())
self._init_inclusion_nodes(
extension_node,
extension_name,
self.extension_require_map,
self.extension_deprecate_map,
self.extension_remove_map,
self.name_extension_map
)
def _init_inclusion_nodes(
self,
base_node: Node,
base_name: str,
inclusion_require_map: dict[str, set[str]],
inclusion_deprecate_map: dict[str, set[str]],
inclusion_remove_map: dict[str, set[str]],
name_inclusion_map: dict[str, set[str]]
):
for require_node in base_node.get_all('require'):
for enum_node in require_node.get_all('enum'):
if not self.is_valid_api(enum_node):
continue
if enum_node.has_attribute('alias'):
self._init_alias_node(enum_node)
continue
enum_name = enum_node.get_attribute('name')
if enum_name is None:
self.error_nodes.setdefault('missing_name', []).append(enum_node)
continue
group_name = None
if enum_node.has_attribute('extends'):
group_name = enum_node.get_attribute('extends')
self.group_value_map.setdefault(group_name, set()).add(enum_name)
self.value_group_map[enum_name] = group_name
inclusion_require_map[base_name].add(enum_name)
name_inclusion_map.setdefault(enum_name, set()).add(base_name)
self.vulkan_names[enum_name] = 'value'
self._init_node_info(enum_name, 'value')
self._init_node(enum_node, enum_name)
for type_node in require_node.get_all('type'):
if not self.is_valid_api(type_node):
continue
if type_node.has_attribute('alias'):
self._init_alias_node(type_node)
continue
type_name = type_node.get_attribute('name')
if type_name is None:
self.error_nodes.setdefault('missing_name', []).append(type_node)
continue
inclusion_require_map[base_name].add(type_name)
name_inclusion_map.setdefault(type_name, set()).add(base_name)
self._init_node(type_node, type_name)
for command_node in require_node.get_all('command'):
if not self.is_valid_api(command_node):
continue
if command_node.has_attribute('alias'):
self._init_alias_node(command_node)
continue
command_name = command_node.get_attribute('name')
if command_name is None:
self.error_nodes.setdefault('missing_name', []).append(command_node)
continue
inclusion_require_map[base_name].add(command_name)
name_inclusion_map.setdefault(command_name, set()).add(base_name)
self._init_node(command_node, command_name)
for inclusion_method in ['deprecate', 'remove']:
for inclusion_category in ['enum', 'type', 'command']:
for inclusion_node in base_node.get_all(inclusion_method):
if not self.is_valid_api(inclusion_node):
continue
for reference_node in inclusion_node.get_all(inclusion_category):
if not self.is_valid_api(reference_node):
continue
if reference_node.has_attribute('alias'):
self._init_alias_node(reference_node)
continue
reference_name = reference_node.get_attribute('name')
if reference_name is None:
self.error_nodes.setdefault('missing_name', []).append(inclusion_node)
continue
if inclusion_method == 'deprecate':
inclusion_deprecate_map[base_name].add(reference_name)
name_inclusion_map.setdefault(reference_name, set()).add(base_name)
elif inclusion_method == 'remove':
inclusion_remove_map[base_name].add(reference_name)
self._init_node(reference_node, reference_name)
def _resolve_alias(self, name: str) -> str:
while name in self.alias_map:
name = self.alias_map[name]
return name
def _manage_type_dependencies(self):
unsupported = {
'union': set(),
'struct': set(),
'callback': set(),
'command': set(),
}
all_unsupported = set()
# Step 1: Remove struct types that have sType members values not defined in the current API.
for name in self.vulkan_node_info['struct']:
for node in self.nodes.get(name, []):
node: Node
if 'member' not in node.children:
continue
for member_node in node.get_all('member'):
if not self.is_valid_api(member_node):
continue
if 'name' not in member_node.children or 'type' not in member_node.children:
continue
member_type = self._resolve_alias(member_node.get('type').get_text())
member_name = member_node.get('name').get_text()
if member_name != 'sType' or member_type != 'VkStructureType' or not member_node.has_attribute('values'):
continue
stype_values = {x.strip() for x in member_node.get_attribute('values').split(',')}
for stype_value in stype_values:
stype_value = self._resolve_alias(stype_value)
if stype_value not in self.group_value_map['VkStructureType']:
unsupported['struct'].add(name)
all_unsupported.add(name)
# Step 1: Remove callbacks with no user data argument.
for name in self.vulkan_node_info['callback']:
for node in self.nodes.get(name, []):
node: Node
if 'proto' not in node.children:
continue
has_user_data = False
for decl_node in node.get_all('param'):
if not self.is_valid_api(decl_node):
continue
if 'name' not in decl_node.children:
continue
param_name = decl_node.get('name').get_text()
if param_name == 'pUserData':
has_user_data = True
break
if not has_user_data:
has_binding_use = False
for depend_name in self.vulkan_node_info['callback'][name]['used_by']:
if depend_name in all_unsupported:
continue
depend_category = self.vulkan_names[depend_name]
if depend_category in ['command', 'callback']:
depend_info = self.vulkan_node_info[depend_category][depend_name]
if name in depend_info['argument_use']:
has_binding_use = True
break
elif depend_category in ['struct', 'union']:
has_binding_use = True
break
if has_binding_use:
unsupported['callback'].add(name)
all_unsupported.add(name)
# Step 2: Cascade removal of types using unsupported types.
remove_length = len(all_unsupported)
while True:
for complex_category in ['struct', 'union']:
for name in self.vulkan_node_info[complex_category]:
if name in unsupported[complex_category]:
continue
for node in self.nodes.get(name, []):
node: Node
if 'member' not in node.children:
continue
for member_node in node.get_all('member'):
if not self.is_valid_api(member_node):
continue
if 'type' not in member_node.children:
continue
member_type = self._resolve_alias(member_node.get('type').get_text())
if member_type.startswith('PFN_'):
member_type = member_type[4:]
member_type = self._resolve_alias(member_type)
if member_type in all_unsupported:
unsupported[complex_category].add(name)
all_unsupported.add(name)
for function_category in ['callback', 'command']:
for name in self.vulkan_node_info[function_category]:
if name in unsupported[function_category]:
continue
for node in self.nodes.get(name, []):
node: Node
if 'proto' not in node.children:
continue
for decl_node in node.get_all('param') + [node.get('proto')]:
if not self.is_valid_api(decl_node):
continue
type_node = decl_node.get('type')
if type_node is None:
continue
param_type = self._resolve_alias(type_node.get_text())
if param_type.startswith('PFN_'):
param_type = param_type[4:]
param_type = self._resolve_alias(param_type)
if param_type in all_unsupported:
unsupported[function_category].add(name)
all_unsupported.add(name)
new_remove_length = len(all_unsupported)
if new_remove_length == remove_length:
break
remove_length = new_remove_length
# Finally, for all unsupported types by the current API, remove them, but keep a record in self.api_unsupported_names.
for unsupported_type in unsupported:
for name in unsupported[unsupported_type]:
self.ctypes.discard(name)
self.nodes.pop(name, None)
self.vulkan_node_info[unsupported_type].pop(name, None)
self.vulkan_names.pop(name, None)
if name in self.alias_map:
target_name = self.alias_map.pop(name)
self.alias_target_map[target_name].discard(name)
if len(self.alias_target_map[target_name]) == 0:
self.alias_target_map.pop(target_name)
self.alias_map.pop(name, None)
if name in self.alias_target_map:
for alias_name in self.alias_target_map.pop(name):
self.alias_map.pop(alias_name, None)
all_unsupported.add(alias_name)
self.alias_target_map.pop(name, None)
self.api_unsupported_names.update(all_unsupported)
def _resolve_type(self, type_name: str):
type_name = self._resolve_alias(type_name)
if type_name in self.bit_bitmask_map:
return self.bit_bitmask_map[type_name]
return type_name
def _resolve_usage(self):
for complex_category in ['struct', 'union']:
for name in self.vulkan_node_info[complex_category]:
for node in self.nodes.get(name, []):
node: Node
if 'member' not in node.children:
continue
for member_node in node.get_all('member'):
if not self.is_valid_api(member_node):
continue
if 'type' not in member_node.children:
continue
member_type = self._resolve_type(member_node.get('type').get_text())
if member_type.startswith('PFN_'):
member_type = member_type[4:]
member_type = self._resolve_alias(member_type)
if member_type in self.vulkan_names:
self.vulkan_node_info[complex_category][name]['uses'].add(member_type)
self.vulkan_node_info[self.vulkan_names[member_type]][member_type]['used_by'].add(name)
elif member_type in self.vulkan_node_info['basetype']:
self.vulkan_node_info[complex_category][name]['uses'].add(member_type)
self.vulkan_node_info['basetype'][member_type]['used_by'].add(name)
for function_category in ['callback', 'command']:
for name in self.vulkan_node_info[function_category]:
for node in self.nodes.get(name, []):
node: Node
if 'proto' not in node.children:
continue
for decl_node in node.get_all('param') + [node.get('proto')]:
if not self.is_valid_api(decl_node):
continue
type_node = decl_node.get('type')
if type_node is None:
continue
param_type = self._resolve_type(type_node.get_text())
if param_type.startswith('PFN_'):
param_type = param_type[4:]
param_type = self._resolve_alias(param_type)
if param_type in self.vulkan_names:
self.vulkan_node_info[function_category][name]['uses'].add(param_type)
self.vulkan_node_info[self.vulkan_names[param_type]][param_type]['used_by'].add(name)
elif member_type in self.vulkan_node_info['basetype']:
self.vulkan_node_info[complex_category][name]['uses'].add(member_type)
self.vulkan_node_info['basetype'][member_type]['used_by'].add(name)
for name in self.vulkan_node_info['handle']:
for node in self.nodes.get(name, []):
node: Node
if 'type' not in node.children or 'name' not in node.children:
continue
if node.has_attribute('parent'):
parent_name = node.get_attribute('parent')
self.handle_parent[name] = parent_name
self.vulkan_node_info['handle'][name]['uses'].add(parent_name)
self.vulkan_node_info['handle'][parent_name]['used_by'].add(name)
handle_type = node.get('type').get_text()
if handle_type in self.vulkan_names:
self.vulkan_node_info['handle'][name]['uses'].add(handle_type)
self.vulkan_node_info[self.vulkan_names[handle_type]][handle_type]['used_by'].add(name)
elif member_type in self.vulkan_node_info['basetype']:
self.vulkan_node_info[complex_category][name]['uses'].add(member_type)
self.vulkan_node_info['basetype'][member_type]['used_by'].add(name)
def _reverse_feature_dependecies(self):
def add(target_feature: str, current_feature: str, visited: set[str]):
self.feature_dependent_map[target_feature].add(current_feature)
if current_feature in visited:
return
loop(target_feature, current_feature, visited)
def loop(target_feature: str, current_feature: str, visited: set[str]):
for dependency in self.feature_dependency_map.get(current_feature, set()):
add(target_feature, dependency, visited | {current_feature})
for feature_name in self.feature_names:
self.feature_dependent_map.setdefault(feature_name, set())
loop(feature_name, feature_name, set())
def _resolve_extends(self):
for name in self.vulkan_node_info['struct']:
for node in self.nodes.get(name, []):
node: Node
if 'member' not in node.children:
continue
if not node.has_attribute('structextends'):
continue
extend_list = {x.strip() for x in node.get_attribute('structextends').split(',')}
for extend in extend_list:
if extend in self.vulkan_node_info['struct']:
self.vulkan_node_info['struct'][name]['extends'].add(extend)
self.vulkan_node_info['struct'][extend]['extended_by'].add(name)
def _apply_ctype_aliases(self):
for name in self.alias_target_map:
self.ctypes.update(self.alias_target_map[name])
def _resolve_type_name(self, type_name: str) -> str:
type_name = self._resolve_alias(type_name)
if type_name.startswith('PFN_'):
type_name = type_name[4:]
if type_name in self.bit_bitmask_map:
type_name = self.bit_bitmask_map[type_name]
type_name = self._resolve_alias(type_name)
return type_name
def _init_members(self):
for category in ['struct', 'union']:
for name, info in self.vulkan_node_info[category].items():
members = info['members'] = OrderedDict()
for node in self.nodes.get(name, []):
node: Node
if 'member' not in node.children:
continue
info['node'] = node
for member_node in node.get_all('member'):
if not self.is_valid_api(member_node):
continue
member_info = {}
if 'name' not in member_node.children:
raise ValueError(f'Member node {member_node} of {category} {name} is missing a name')
member_info['node'] = member_node
member_name = member_node.get('name').get_text()
members[member_name] = member_info
if 'type' not in member_node.children:
member_info['class'] = 'unknown'
continue
member_type = self._resolve_type_name(member_node.get('type').get_text())
if member_type in self.vulkan_names:
member_info['class'] = self.vulkan_names[member_type]
member_info['type'] = member_type
else:
member_info['class'] = 'external'
member_info['type'] = member_type
def _init_arguments(self):
for category in ['command', 'callback']:
for name, info in self.vulkan_node_info[category].items():
arguments = info['arguments'] = OrderedDict()
for node in self.nodes.get(name, []):
node: Node
if 'proto' not in node.children:
continue
info['node'] = node
proto_node = node.get('proto')
info['return'] = return_info = {}
return_info['node'] = proto_node
if 'type' in proto_node.children:
return_type = self._resolve_type_name(proto_node.get('type').get_text())
if return_type in self.vulkan_names:
return_info['class'] = self.vulkan_names[return_type]
return_info['type'] = return_type
else:
return_info['class'] = 'ctype' if return_type in self.ctypes else 'external'
return_info['type'] = return_type
else:
return_info['class'] = 'unknown'
for argument_node in node.get_all('param'):
if not self.is_valid_api(argument_node):
continue
argument_info = {}
if 'name' not in argument_node.children:
raise ValueError(f'Argument node {argument_node} of {category} {name} is missing a name')
argument_info['node'] = argument_node
argument_name = argument_node.get('name').get_text()
arguments[argument_name] = argument_info
if 'type' not in argument_node.children:
argument_info['class'] = 'unknown'
continue
argument_type = self._resolve_type_name(argument_node.get('type').get_text())
if argument_type in self.vulkan_names:
argument_info['class'] = self.vulkan_names[argument_type]
argument_info['type'] = argument_type
else:
argument_info['class'] = 'ctype' if argument_type in self.ctypes else 'external'
argument_info['type'] = argument_type
def _init_handles(self):
for name, info in self.vulkan_node_info['handle'].items():
for node in self.nodes.get(name, []):
node: Node
if 'type' not in node.children or 'name' not in node.children or not node.has_attribute('objtypeenum'):
continue
info['parent'] = node.get_attribute('parent') if node.has_attribute('parent') else None
object_type = node.get_attribute('objtypeenum')
if not object_type.startswith('VK_OBJECT_TYPE_'):
raise ValueError(f'Handle node {node} of handle {name} has invalid object type enum {object_type}')
info['VkObjectType'] = object_type
info['VkDebugReportObjectTypeEXT'] = PREFIX_DEBUG_REPORT_OBJECT_TYPE + object_type[len(PREFIX_OBJECT_TYPE):]
info['path'] = []
info['output_of'] = set()
info['destroyed_by'] = set()
for name, info in self.vulkan_node_info['handle'].items():
path_name = name
while path_name is not None:
info['path'].append(path_name)
path_name = self.handle_parent.get(path_name, None)
def _search_for_handle_output(self, category: str, name: str, /, *path) -> set[str]:
output_handles = set()
if name in path:
return output_handles
info = self.vulkan_node_info[category][name]
for member_info in info['members'].values():
member_node = member_info['node']
member_node: Node
member_ctype = member_node.get_text_before(member_node.get('name'))
ptr_count = member_ctype.count('*')
if 'const' in member_ctype:
continue
if ptr_count > 0:
if member_info['class'] == 'handle' and member_info['type'] in self.vulkan_node_info['handle']:
output_handles.add(member_info['type'])
continue
if member_info['class'] in ('struct', 'union') and member_info['type'] in self.vulkan_node_info['struct']:
result = self._search_for_handle_output(member_info['class'], member_info['type'], *path, name)
output_handles.update(result)
return output_handles
def _init_handle_output(self):
for command_name, command_info in self.vulkan_node_info['command'].items():
for arg_info in command_info['arguments'].values():
arg_node = arg_info['node']
arg_node: Node
arg_ctype = arg_node.get_text_before(arg_node.get('name'))
ptr_count = arg_ctype.count('*')
if 'const' in arg_ctype:
continue
if ptr_count > 0:
if arg_info['class'] == 'handle' and arg_info['type'] in self.vulkan_node_info['handle']:
self.vulkan_node_info['handle'][arg_info['type']]['output_of'].add(command_name)
elif arg_info['class'] in ('struct', 'union') and arg_info['type'] in self.vulkan_node_info['struct']:
output_handles = self._search_for_handle_output(arg_info['class'], arg_info['type'])
for handle in output_handles:
self.vulkan_node_info['handle'][handle]['output_of'].add(command_name)
def _init_handle_destroy(self):
for command_name, command_info in self.vulkan_node_info['command'].items():
if not any(word in command_name for word in ['Destroy', 'Free', 'Release']):
continue
handle_params = {}
for arg_name, arg_info in command_info['arguments'].items():
if arg_info['class'] == 'handle' and arg_info['type'] in self.vulkan_node_info['handle']:
handle_params[arg_name] = arg_info['type']
target_handle_name = None
target_handle_path_length = -1
for arg_name, target_handle_name in handle_params.items():
handle_path_length = len(self.vulkan_node_info['handle'][target_handle_name]['path'])
if handle_path_length > target_handle_path_length:
target_handle_name = target_handle_name
target_handle_path_length = handle_path_length
expected_parameters = set()
additional_parameters = set()
for arg_name, arg_info in command_info['arguments'].items():
if arg_name in expected_parameters:
continue
arg_node = arg_info['node']
arg_node: Node
if arg_node.has_attribute('len') and not arg_node.has_attribute('altlen'):
length = [s.strip() for s in arg_node.get_attribute('len').split(',') if s.strip().lower() != 'null-terminated']
for length_arg in length:
length_words = [word.strip() for word in re.split(r'\b', length_arg) if word.strip()]
if len(length_words) > 0 and length_words[0] in command_info['arguments']:
expected_parameters.add(length_words[0])
continue
if arg_info['class'] == 'handle' and arg_info['type'] in self.vulkan_node_info['handle']:
expected_parameters.add(arg_name)
continue
if arg_info['class'] == 'struct' and arg_info['type'] == 'VkAllocationCallbacks':
expected_parameters.add(arg_name)
continue
additional_parameters.add(arg_name)
additional_parameters = additional_parameters - expected_parameters
if len(additional_parameters) > 0:
continue
target_handle_qualname = target_handle_name
for tag in self.tags:
if target_handle_qualname.endswith(tag):
target_handle_qualname = target_handle_qualname[:-len(tag)]
break
handle_words = re.split(r'(?<!^)(?=[A-Z])', target_handle_qualname)[1:]
handle_words = [target_handle_qualname] + handle_words
if not any(word in command_name for word in handle_words):
continue
self.vulkan_node_info['handle'][target_handle_name]['destroyed_by'].add(command_name)
for handle_name, handle_info in self.vulkan_node_info['handle'].items():
if len(handle_info['destroyed_by']) > 1:
raise ValueError(f'Handle {handle_name} is destroyed by multiple commands: {handle_info["destroyed_by"]}')
if len(handle_info['destroyed_by']) == 1:
handle_info['destroyed_by'] = next(iter(handle_info['destroyed_by']))
else:
handle_info['destroyed_by'] = None
def _init_relationship_complex(self, command_name: str, complex_name: str, relationship_type: str, /, *visited: str):
if complex_name in visited:
return
category = self.vulkan_names[complex_name]
complex_info = self.vulkan_node_info[category][complex_name]
complex_info[relationship_type].add(command_name)
if category == 'struct':
for extending_struct in complex_info['extended_by']:
self._init_relationship_complex(command_name, extending_struct, relationship_type, *visited, complex_name)
for member_info in complex_info['members'].values():
if member_info['class'] in ('struct', 'union'):
self._init_relationship_complex(command_name, member_info['type'], relationship_type, *visited, complex_name)
def _init_io(self):
for command_name, command_info in self.vulkan_node_info['command'].items():
for arg_info in command_info['arguments'].values():
if arg_info['class'] == 'struct':
arg_node = arg_info['node']
arg_node: Node
arg_type_prefix = arg_node.get_text_before(arg_node.get('type')).strip()
arg_type_suffix = arg_node.get_text_after(arg_node.get('type')).strip()
relationship_type = 'input_of' if 'const' in arg_type_prefix or arg_type_suffix.startswith('const') else 'output_of'
if arg_info['class'] in ('struct', 'union'):
self._init_relationship_complex(command_name, arg_info['type'], relationship_type)

View File

@@ -0,0 +1,257 @@
from xml.parsers import expat
from functools import cached_property
from pathlib import Path
from typing import Optional, Iterable
class Node:
class MultipleChildrenError(Exception):
pass
def __init__(self, node_name: str, node_value: str, node_type: str, attributes: dict = {}):
self.child_nodes = []
self.children = {}
self.parent_node = None
self.parent_index = None
self.parent_name_index = None
self.node_name = node_name
self.attributes = dict(attributes)
self.node_type = node_type
self.node_value = node_value
def has_attribute(self, name: str) -> bool:
return name in self.attributes
def get_attribute(self, name: str, default=None) -> str:
if self.has_attribute(name):
return self.attributes[name]
return default
def set_attribute(self, name: str, value) -> None:
if value is None:
if name in self.attributes:
del self.attributes[name]
else:
self.attributes[name] = str(value)
def get(self, name: str, default=None) -> 'Optional[Node]':
if name not in self.children:
return default
node_list = self.children[name]
if len(node_list) > 1:
raise Node.MultipleChildrenError(f'{self.file_path}: Expected 1 child with name "{name}", got {len(node_list)} children')
elif len(node_list) == 0:
return default
return node_list[0]
def get_all(self, name: str) -> 'list[Node]':
if name not in self.children:
return []
return self.children[name]
def tree_get_all(self, name: str) -> 'Iterable[Node]':
if self.node_name == name:
yield self
for child in self.child_nodes:
yield from child.tree_get_all(name)
def append_child(self, node):
if node.node_name not in self.children:
self.children[node.node_name] = []
child_index = len(self.child_nodes)
child_name_index = len(self.children[node.node_name])
node.parent_node = self
node.parent_index = child_index
node.parent_name_index = child_name_index
self.child_nodes.append(node)
self.children[node.node_name].append(node)
def get_text_nodes(self, *, skip_comments=True) -> 'Iterable[Node]':
for child in self.child_nodes:
if child.node_type == 'text':
yield child
elif child.node_type == 'element':
if skip_comments and child.node_name == 'comment':
continue
yield from child.get_text_nodes(skip_comments=skip_comments)
def get_text(self, *, skip_comments=True) -> str:
if self.node_type == 'text':
return self.node_value
elif self.node_type == 'element':
return ''.join([x.node_value for x in self.get_text_nodes(skip_comments=skip_comments)])
else:
return ''
@cached_property
def path(self) -> 'list[str]':
return (self.parent_node.path if self.parent_node is not None else []) + [self.node_name]
def get_text_nodes_before(self, node, *, skip_comments=True):
items = []
self._get_text_nodes_before(node, items, skip_comments=skip_comments)
return items
def get_text_nodes_after(self, node, *, skip_comments=True):
items = []
self._get_text_nodes_after(node, items, skip_comments=skip_comments)
return items
def get_text_before(self, node, *, skip_comments=True):
return ''.join([n.get_text() for n in self.get_text_nodes_before(node, skip_comments=skip_comments)])
def get_text_after(self, node, *, skip_comments=True):
return ''.join([n.get_text() for n in self.get_text_nodes_after(node, skip_comments=skip_comments)])
def _get_text_nodes_before(self, node, items, *, skip_comments=True):
return_value = True
for child in self.child_nodes:
if not return_value:
break
if child is node:
return False
if child.node_type == 'text':
items.append(child)
elif child.node_type == 'element':
if skip_comments and child.node_name == 'comment':
continue
return_value = child._get_text_nodes_before(node, items)
return return_value
def _get_text_nodes_after(self, node, items, *, skip_comments=True):
return_value = True
for child in reversed(self.child_nodes):
if not return_value:
break
if child is node:
return False
if child.node_type == 'text':
items.insert(0, child)
elif child.node_type == 'element':
if skip_comments and child.node_name == 'comment':
continue
return_value = child._get_text_nodes_after(node, items)
return return_value
@property
def previous_sibling(self) -> 'Optional[Node]':
if self.parent_node is None:
return None
assert isinstance(self.parent_index, int)
if self.parent_index <= 0:
return None
return self.parent_node.child_nodes[self.parent_index - 1]
@property
def next_sibling(self) -> 'Optional[Node]':
if self.parent_node is None:
return None
assert isinstance(self.parent_index, int)
try:
return self.parent_node.child_nodes[self.parent_index + 1]
except IndexError:
return None
@property
def previous_sibling_element(self) -> 'Optional[Node]':
sibling = self.previous_sibling
while sibling is not None:
if sibling.node_type == 'element':
return sibling
sibling = sibling.previous_sibling
return None
@property
def next_sibling_element(self) -> 'Optional[Node]':
sibling = self.next_sibling
while sibling is not None:
if sibling.node_type == 'element':
return sibling
sibling = sibling.next_sibling
return None
@cached_property
def root(self) -> 'Optional[Node]':
if self.parent_node is None:
return self
return self.parent_node.root
@cached_property
def file_path(self) -> str:
return f'{self.root.file}:{self.line}:{self.column}'
@cached_property
def file_url(self) -> str:
return f'{self.root.url}:{self.line}:{self.column}'
def __repr__(self):
path = '/'.join(self.path)
return f'<XMLNode {path} at {self.file_url} object at {hex(id(self))}>'
def parse_xml(document, *, is_file=False) -> Node:
root = None
parent = None
filename = document
if not is_file:
filename = f'document:{hash(document)}'
url = filename
else:
url = Path(document).absolute().as_uri()
def on_xml_root_start(name, attributes):
nonlocal root
nonlocal parent
nonlocal xml_element_start_parser
nonlocal parser
root = parent = Node(name, None, 'element', attributes)
setattr(root, 'file', filename)
setattr(root, 'url', url)
setattr(root, 'line', parser.CurrentLineNumber)
setattr(root, 'column', parser.CurrentColumnNumber)
setattr(root, 'file_offset', parser.CurrentByteIndex)
xml_element_start_parser = on_xml_non_root_start
def on_xml_non_root_start(name, attributes):
nonlocal parent
nonlocal parser
element = Node(name, None, 'element', attributes)
setattr(element, 'line', parser.CurrentLineNumber)
setattr(element, 'column', parser.CurrentColumnNumber)
setattr(element, 'file_offset', parser.CurrentByteIndex)
parent.append_child(element)
parent = element
def on_xml_element_end(name):
nonlocal parent
assert parent is not None and isinstance(parent, Node)
assert parent.node_name == name
parent = parent.parent_node
def on_xml_character_data(data):
nonlocal parent
nonlocal parser
node = Node('#text', data, 'text')
setattr(node, 'line', parser.CurrentLineNumber)
setattr(node, 'column', parser.CurrentColumnNumber)
setattr(node, 'file_offset', parser.CurrentByteIndex)
parent.append_child(node)
def on_xml_element_start(*args, **kwargs):
nonlocal xml_element_start_parser
return xml_element_start_parser(*args, **kwargs)
xml_element_start_parser = on_xml_root_start
parser = expat.ParserCreate(encoding='utf-8')
parser.StartElementHandler = on_xml_element_start
parser.EndElementHandler = on_xml_element_end
parser.CharacterDataHandler = on_xml_character_data
if is_file:
with open(document, 'rb') as file:
parser.ParseFile(file)
else:
parser.Parse(document)
return root

325
tests/test_node.py Normal file
View File

@@ -0,0 +1,325 @@
import unittest
class TestNode(unittest.TestCase):
def test_is_class(self):
from dragiyski.vulkan.registry.xml import Node
assert isinstance(Node, type)
def test_function_exists_parse_xml(self):
from dragiyski.vulkan.registry.xml import parse_xml
assert callable(parse_xml)
def test_node_simple_xml(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
xml_content = '''<root attr="value">
<child>Text</child>
<!-- A comment -->
<child2>More Text</child2>
<child3>
<text>Some Text</text>
<text>More Text</text>
<text>Even More Text</text>
</child3>
</root>'''
root = parse_xml(xml_content)
assert isinstance(root, Node)
assert root.node_name == 'root'
assert root.has_attribute('attr')
assert root.get_attribute('attr') == 'value'
assert 'child' in root.children
assert 'child2' in root.children
assert root.get('child') is root.children['child'][0]
assert root.get('child2') is root.children['child2'][0]
child_index = root.child_nodes.index(root.get('child'))
child2_index = root.child_nodes.index(root.get('child2'))
child3_index = root.child_nodes.index(root.get('child3'))
assert child_index < child2_index
assert child2_index < child3_index
for i in range(len(root.child_nodes)):
if i not in (child_index, child2_index, child3_index):
assert root.child_nodes[i].node_type == 'text'
assert root.child_nodes[i].node_name == '#text'
root.set_attribute('attr', 'foo')
assert root.get_attribute('attr') == 'foo'
assert root.has_attribute('new_attr') is False
root.set_attribute('new_attr', 'new_value')
assert root.has_attribute('new_attr') is True
assert root.get_attribute('new_attr') == 'new_value'
root.set_attribute('new_attr', None)
assert root.has_attribute('new_attr') is False
assert root.get_attribute('new_attr', 'default') == 'default'
assert root.get_attribute('new_attr', 5) == 5
assert root.get_attribute('new_attr') is None
assert root.has_attribute('not_existing') is False
root.set_attribute('not_existing', None)
assert root.has_attribute('not_existing') is False
assert root.get('not_existing', 'default') == 'default'
assert root.get('not_existing', 5) == 5
assert root.get('not_existing') is None
child1_node = root.get('child')
assert isinstance(child1_node, Node)
assert child1_node.node_name == 'child'
child2_node = root.get('child2')
assert isinstance(child2_node, Node)
assert child2_node.node_name == 'child2'
child3_node = root.get('child3')
assert isinstance(child3_node, Node)
assert child3_node.node_name == 'child3'
self.assertRaises(Node.MultipleChildrenError, lambda: child3_node.get('text'))
assert child1_node.next_sibling_element is child2_node
assert child2_node.previous_sibling_element is child1_node
assert child2_node.next_sibling_element is child3_node
assert child3_node.previous_sibling_element is child2_node
assert child1_node.previous_sibling_element is None
assert child3_node.next_sibling_element is None
assert child3_node.root == root
assert root.root == root
child3_node.children['text'].clear()
assert child3_node.get('text', 'default') == 'default'
assert child3_node.get('text', 5) == 5
assert child3_node.get('text') is None
assert root.previous_sibling is None
assert root.next_sibling is None
assert root.child_nodes[0].previous_sibling is None
assert root.child_nodes[0].next_sibling is root.child_nodes[1]
assert root.child_nodes[1].previous_sibling is root.child_nodes[0]
assert root.child_nodes[1].next_sibling is root.child_nodes[2]
assert root.child_nodes[-1].previous_sibling is root.child_nodes[-2]
assert root.child_nodes[-1].next_sibling is None
assert root.get_all('not_existing') == []
def test_node_tree_get_all(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
xml_content = '''<root>
<child>
<test>Text0</test>
</child>
<child>
<other>
<subchild>Text1</subchild>
<test>Text1</test>
</other>
</child>
<other>
<other test="test">
<subchild>
<nested>With some <test>Text2</test> before and after.</nested>
</subchild>
</other>
</other>
</root>'''
result = parse_xml(xml_content)
assert isinstance(result, Node)
i = 0
paths = [
[
'root',
'child',
'test'
],
[
'root',
'child',
'other',
'test'],
[
'root',
'other',
'other',
'subchild',
'nested',
'test'
],
]
for node in result.tree_get_all('test'):
assert node.get_text() == f'Text{i}'
assert node.path == paths[i]
i += 1
assert i == 3
def test_node_get_text(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
xml_content = '''<?xml version="1.0" encoding="UTF-8"?>
<registry>
<comment>
Copyright 2015-2025 The Khronos Group Inc.
SPDX-License-Identifier: Apache-2.0 OR MIT
</comment>
<types comment="Vulkan type definitions">
<type category="struct" name="VkMemoryDedicatedAllocateInfoTensorARM" structextends="VkMemoryAllocateInfo">
<member values="VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO_TENSOR_ARM"><type>VkStructureType</type> <name>sType</name></member>
<member><comment>const </comment><type>VkTensorARM</type> <name>tensor</name><comment>Tensor that this allocation will be bound to</comment></member>
<member optional="true">const <type>void</type>* <name>pNext</name></member>
</type>
</types>
</registry>
'''
result = parse_xml(xml_content)
assert isinstance(result, Node)
assert result.get('types').get('type').get_all('member')[0].get_text() == 'VkStructureType sType'
assert result.get('types').get('type').get_all('member')[1].get_text() == 'VkTensorARM tensor'
assert result.get('types').get('type').get_all('member')[2].get_text() == 'const void* pNext'
assert result.get('types').get('type').get_all('member')[0].get_text(skip_comments=True) == 'VkStructureType sType'
assert result.get('types').get('type').get_all('member')[1].get_text(skip_comments=True) == 'VkTensorARM tensor'
assert result.get('types').get('type').get_all('member')[2].get_text(skip_comments=True) == 'const void* pNext'
assert result.get('types').get('type').get_all('member')[0].get_text(skip_comments=False) == 'VkStructureType sType'
assert result.get('types').get('type').get_all('member')[1].get_text(skip_comments=False) == 'const VkTensorARM tensorTensor that this allocation will be bound to'
assert result.get('types').get('type').get_all('member')[2].get_text(skip_comments=False) == 'const void* pNext'
s = result.get('types').get('type').get_text()
assert result.get('types').get('type').get_text() == '''
VkStructureType sType
VkTensorARM tensor
const void* pNext
'''
text_list = [
[
'\n',
' ',
'VkStructureType',
' ',
'sType',
'\n',
' ',
],
[
'VkTensorARM',
' ',
'tensor',
],
[
'\n',
' ',
'const ',
'void',
'* ',
'pNext',
'\n',
' ',
]
]
text_list_without_comment = [
*text_list[0],
*text_list[1],
*text_list[2],
]
text_list_with_comment = [
*text_list[0],
'const ',
*text_list[1],
'Tensor that this allocation will be bound to',
*text_list[2],
]
type_node = result.get('types').get('type')
i = 0
for node in type_node.get_text_nodes():
assert node.node_type == 'text'
assert node.node_name == '#text'
assert node.node_value == text_list_without_comment[i], f'''result.get('types').get('type').child_nodes[{i}].get_text() == {text_list_without_comment[i]!r}'''
assert node.get_text() == node.node_value
i += 1
i = 0
for node in type_node.get_text_nodes(skip_comments=False):
assert node.node_type == 'text'
assert node.node_name == '#text'
assert node.node_value == text_list_with_comment[i], f'''result.get('types').get('type').child_nodes[{i}].get_text() == {text_list_with_comment[i]!r}'''
assert node.get_text() == node.node_value
i += 1
invalid_text_node_before = type_node.get_all('member')[0].get('name').child_nodes[0]
invalid_text_node_before.node_type = 'invalid'
assert invalid_text_node_before.get_text() == ''
text_list_without_comment = text_list_without_comment[0:4] + text_list_without_comment[5:]
text_list_with_comment = text_list_with_comment[0:4] + text_list_with_comment[5:]
invalid_text_node_after = type_node.get_all('member')[2].get('name')
invalid_text_node_after.node_type = 'invalid'
assert invalid_text_node_after.get_text() == ''
text_list_without_comment = text_list_without_comment[:14] + text_list_without_comment[15:]
text_list_with_comment = text_list_with_comment[:16] + text_list_with_comment[17:]
i = 0
for node in type_node.get_text_nodes():
assert node.node_type == 'text'
assert node.node_name == '#text'
assert node.node_value == text_list_without_comment[i], f'''result.get('types').get('type').child_nodes[{i}].get_text() == {text_list_without_comment[i]!r}'''
assert node.get_text() == node.node_value
i += 1
i = 0
for node in type_node.get_text_nodes(skip_comments=False):
assert node.node_type == 'text'
assert node.node_name == '#text'
assert node.node_value == text_list_with_comment[i], f'''result.get('types').get('type').child_nodes[{i}].get_text() == {text_list_with_comment[i]!r}'''
assert node.get_text() == node.node_value
i += 1
tensor_member_node = type_node.get_all('member')[1]
tensor_member_name_node = tensor_member_node.get('name')
assert tensor_member_node.get_text_before(tensor_member_name_node) == 'VkTensorARM '
assert tensor_member_node.get_text_after(tensor_member_name_node) == ''
assert tensor_member_node.get_text_before(tensor_member_name_node, skip_comments=False) == 'const VkTensorARM '
assert tensor_member_node.get_text_after(tensor_member_name_node, skip_comments=False) == 'Tensor that this allocation will be bound to'
assert [node.get_text() for node in type_node.get_text_nodes_before(tensor_member_name_node)] == text_list_without_comment[:8]
assert [node.get_text() for node in type_node.get_text_nodes_after(tensor_member_name_node)] == text_list_without_comment[9:]
def test_file_path(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
from pathlib import Path
file = Path(__file__).absolute().parent.joinpath('test_node/xml-test-1.xml')
root = parse_xml(file, is_file=True)
assert isinstance(root, Node)
assert root.line == 2
assert root.column == 0
assert root.file_path == f'{file}:{root.line}:{root.column}'
child3_node = root.get('child3')
assert isinstance(child3_node, Node)
assert child3_node.line == 6
assert child3_node.column == 4
assert child3_node.file_path == f'{file}:{child3_node.line}:{child3_node.column}'
def test_file_url(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
from pathlib import Path
file = Path(__file__).resolve().parent.joinpath('test_node/xml-test-1.xml')
root = parse_xml(file, is_file=True)
url = file.as_uri()
assert isinstance(root, Node)
assert root.line == 2
assert root.column == 0
assert root.file_url == f'{url}:{root.line}:{root.column}'
child3_node = root.get('child3')
assert isinstance(child3_node, Node)
assert child3_node.line == 6
assert child3_node.column == 4
assert child3_node.file_url == f'{url}:{child3_node.line}:{child3_node.column}'
def test_repr(self):
from dragiyski.vulkan.registry.xml import Node, parse_xml
from pathlib import Path
file = Path(__file__).resolve().parent.joinpath('test_node/xml-test-1.xml')
root = parse_xml(file, is_file=True)
url = file.as_uri()
path = '/'.join(root.path)
assert repr(root) == f'<XMLNode {path} at {root.file_url} object at {hex(id(root))}>'
child3_node = root.get('child3')
path = '/'.join(child3_node.path)
assert repr(child3_node) == f'<XMLNode {path} at {child3_node.file_url} object at {hex(id(child3_node))}>'

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<root attr="value">
<child>Text</child>
<!-- A comment -->
<child2>More Text</child2>
<child3>
<text>Some Text</text>
<text>More Text</text>
<text>Even More Text</text>
</child3>
</root>

23
tests/test_taxonomy.py Normal file
View File

@@ -0,0 +1,23 @@
import unittest
import dragiyski.vulkan.registry as registry
from collections.abc import Container
class TestTaxonomy(unittest.TestCase):
taxonomy = None
@classmethod
def setUpClass(cls):
cls.taxonomy = registry.taxonomy
def test_taxonomy_api(self):
self.assertEqual(self.taxonomy.api, 'vulkan')
def test_taxonomy_tags(self):
self.assertHasAttr(self.taxonomy, 'tags')
self.assertIsInstance(self.taxonomy.tags, Container)
self.assertIn('KHR', self.taxonomy.tags)
self.assertIn('EXT', self.taxonomy.tags)
if __name__ == '__main__':
unittest.main()