├── .coveragerc ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── tests └── test_gui.py └── tkup ├── __init__.py ├── _widgets.py ├── any_tk.py └── gui.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | tkup 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{json,md,yaml,yml}] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | - push 3 | - pull_request 4 | 5 | name: Main 6 | 7 | jobs: 8 | pre-commit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-python@v5 13 | with: 14 | python-version: '3.10' 15 | - run: | 16 | pip install poetry 17 | poetry install 18 | poetry run pre-commit run --all-files --show-diff-on-failure 19 | 20 | test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: 26 | - '3.8' 27 | - '3.9' 28 | - '3.10' 29 | - '3.11' 30 | - '3.12' 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - run: | 37 | sudo apt-get install -y xvfb 38 | pip install poetry 39 | poetry install 40 | xvfb-run -a poetry run pytest --verbose 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .cache/ 3 | .coverage 4 | .env/ 5 | .idea/ 6 | .mypy_cache/ 7 | .pytest_cache/ 8 | .tox/ 9 | .venv/ 10 | *.egg-info/ 11 | /setup.py 12 | build/ 13 | dist/ 14 | htmlcov/ 15 | pip-wheel-metadata/ 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.1.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - repo: https://github.com/Lucas-C/pre-commit-hooks 8 | rev: v1.1.6 9 | hooks: 10 | - id: forbid-tabs 11 | - repo: https://github.com/mtkennerly/pre-commit-hooks 12 | rev: v0.4.0 13 | hooks: 14 | - id: poetry-mypy 15 | - id: poetry-ruff-check 16 | - id: poetry-ruff-format 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "codezombiech.gitignore", 4 | "EditorConfig.EditorConfig", 5 | "ms-python.mypy-type-checker", 6 | "ms-python.python", 7 | "redhat.vscode-yaml", 8 | "sidneys1.gitconfig", 9 | "streetsidesoftware.code-spell-checker", 10 | "tamasfe.even-better-toml", 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv", 3 | "python.testing.pytestArgs": [ 4 | "." 5 | ], 6 | "python.testing.pytestEnabled": true, 7 | "python.testing.unittestEnabled": false, 8 | "yaml.format.enable": true, 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.2.0 (2018-04-24) 2 | 3 | * Enforced `ttk` preference programmatically in case new `ttk` versions of widgets are added. 4 | 5 | ## v1.1.0 (2018-03-04) 6 | 7 | * Added `AnyTK` union type. 8 | * Added a wrapper for the `Menu` widget. 9 | 10 | ## v1.0.0 (2018-03-04) 11 | 12 | * Initial release. 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | This project is managed using [Poetry](https://python-poetry.org/). 3 | Development requires Python 3.9+. 4 | 5 | * If you want to take advantage of the default VSCode integration, then first 6 | configure Poetry to make its virtual environment in the repository: 7 | ``` 8 | poetry config virtualenvs.in-project true 9 | ``` 10 | * After cloning the repository, activate the tooling: 11 | ``` 12 | poetry install 13 | poetry run pre-commit install 14 | ``` 15 | * Run unit tests: 16 | ``` 17 | poetry run pytest 18 | ``` 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew T. Kennerly (mtkennerly) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # tkup 3 | tkup is a thin wrapper around standard Tkinter widgets, allowing you to write 4 | code that visually reflects the widget hierarchy. It doesn't try to reinvent 5 | the wheel and just helps you use normal Tkinter in a streamlined way. 6 | 7 | Typical Tkinter code is flat, even though it represents a heavily nested 8 | structure, which makes it difficult to quickly read and understand the true 9 | organization of the GUI. You must also explicitly assign each widget's master, 10 | which is error-prone and verbose: 11 | 12 | ```python 13 | import tkinter as tk 14 | from tkinter import ttk 15 | 16 | app = tk.Tk() 17 | app.title("Demo") 18 | 19 | outer_frame = ttk.Frame(app) 20 | outer_frame.grid() 21 | hi_button = ttk.Button(outer_frame, text="hi") 22 | hi_button.grid() 23 | 24 | app.mainloop() 25 | ``` 26 | 27 | tkup solves this by using nested with-statements. There are factory functions 28 | for each kind of widget, and you can use them as context managers so that 29 | nested master widgets are automatically assigned. Post-instantiation calls, 30 | like gridding, can be accomplished two ways, most traditionally by naming the 31 | yielded value: 32 | 33 | ```python 34 | from tkup import GUI 35 | 36 | app = GUI() 37 | 38 | with app.root() as root: 39 | root.title("Demo") 40 | with app.frame() as outer_frame: 41 | outer_frame.grid() 42 | with app.button(text="hi") as hi_button: 43 | hi_button.grid() 44 | 45 | app.run() 46 | ``` 47 | 48 | However, naming every single widget just to grid it can be tiresome. 49 | Inspired by Kotlin's implicit creation of the `it` variable in lambdas, 50 | the GUI class provides an `it` method which returns the current master widget, 51 | and there is an additional convenience method called `with_it` which returns 52 | the GUI instance and its `it` method for quick assignment to variables: 53 | 54 | ```python 55 | from tkup import GUI 56 | 57 | app, it = GUI().with_it() 58 | 59 | with app.root(): 60 | it().title("Demo") 61 | with app.frame(): 62 | it().grid() 63 | with app.button(text="hi"): 64 | it().grid() 65 | 66 | app.run() 67 | ``` 68 | 69 | tkup prefers themed (ttk) widgets wherever available. If you want to use 70 | classic widgets, or if you want to use a custom subclass of `tkinter.Widget`, 71 | then you can use the GUI `widget` method and pass in the type to instantiate: 72 | 73 | ```python 74 | import tkinter as tk 75 | from tkup import GUI 76 | 77 | app = GUI() 78 | 79 | with app.root(): 80 | with app.widget(tk.Button, text="foo"): 81 | ... 82 | ``` 83 | 84 | ## Installation 85 | ``` 86 | pip install tkup 87 | ``` 88 | 89 | tkup supports Python 3.5 and higher. 90 | 91 | ## Development 92 | Please refer to [CONTRIBUTING.md](CONTRIBUTING.md). 93 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "cfgv" 5 | version = "3.4.0" 6 | description = "Validate configuration and produce human readable error messages." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 11 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 12 | ] 13 | 14 | [[package]] 15 | name = "colorama" 16 | version = "0.4.6" 17 | description = "Cross-platform colored terminal text." 18 | optional = false 19 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 20 | files = [ 21 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 22 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 23 | ] 24 | 25 | [[package]] 26 | name = "distlib" 27 | version = "0.3.8" 28 | description = "Distribution utilities" 29 | optional = false 30 | python-versions = "*" 31 | files = [ 32 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 33 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 34 | ] 35 | 36 | [[package]] 37 | name = "exceptiongroup" 38 | version = "1.2.0" 39 | description = "Backport of PEP 654 (exception groups)" 40 | optional = false 41 | python-versions = ">=3.7" 42 | files = [ 43 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 44 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 45 | ] 46 | 47 | [package.extras] 48 | test = ["pytest (>=6)"] 49 | 50 | [[package]] 51 | name = "filelock" 52 | version = "3.13.1" 53 | description = "A platform independent file lock." 54 | optional = false 55 | python-versions = ">=3.8" 56 | files = [ 57 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 58 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 59 | ] 60 | 61 | [package.extras] 62 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 63 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 64 | typing = ["typing-extensions (>=4.8)"] 65 | 66 | [[package]] 67 | name = "identify" 68 | version = "2.5.35" 69 | description = "File identification library for Python" 70 | optional = false 71 | python-versions = ">=3.8" 72 | files = [ 73 | {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, 74 | {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, 75 | ] 76 | 77 | [package.extras] 78 | license = ["ukkonen"] 79 | 80 | [[package]] 81 | name = "iniconfig" 82 | version = "2.0.0" 83 | description = "brain-dead simple config-ini parsing" 84 | optional = false 85 | python-versions = ">=3.7" 86 | files = [ 87 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 88 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 89 | ] 90 | 91 | [[package]] 92 | name = "mypy" 93 | version = "1.9.0" 94 | description = "Optional static typing for Python" 95 | optional = false 96 | python-versions = ">=3.8" 97 | files = [ 98 | {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, 99 | {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, 100 | {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, 101 | {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, 102 | {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, 103 | {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, 104 | {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, 105 | {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, 106 | {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, 107 | {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, 108 | {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, 109 | {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, 110 | {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, 111 | {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, 112 | {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, 113 | {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, 114 | {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, 115 | {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, 116 | {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, 117 | {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, 118 | {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, 119 | {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, 120 | {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, 121 | {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, 122 | {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, 123 | {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, 124 | {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, 125 | ] 126 | 127 | [package.dependencies] 128 | mypy-extensions = ">=1.0.0" 129 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 130 | typing-extensions = ">=4.1.0" 131 | 132 | [package.extras] 133 | dmypy = ["psutil (>=4.0)"] 134 | install-types = ["pip"] 135 | mypyc = ["setuptools (>=50)"] 136 | reports = ["lxml"] 137 | 138 | [[package]] 139 | name = "mypy-extensions" 140 | version = "1.0.0" 141 | description = "Type system extensions for programs checked with the mypy type checker." 142 | optional = false 143 | python-versions = ">=3.5" 144 | files = [ 145 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 146 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 147 | ] 148 | 149 | [[package]] 150 | name = "nodeenv" 151 | version = "1.8.0" 152 | description = "Node.js virtual environment builder" 153 | optional = false 154 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 155 | files = [ 156 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 157 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 158 | ] 159 | 160 | [package.dependencies] 161 | setuptools = "*" 162 | 163 | [[package]] 164 | name = "packaging" 165 | version = "24.0" 166 | description = "Core utilities for Python packages" 167 | optional = false 168 | python-versions = ">=3.7" 169 | files = [ 170 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 171 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 172 | ] 173 | 174 | [[package]] 175 | name = "platformdirs" 176 | version = "4.2.0" 177 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 178 | optional = false 179 | python-versions = ">=3.8" 180 | files = [ 181 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 182 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 183 | ] 184 | 185 | [package.extras] 186 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 187 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 188 | 189 | [[package]] 190 | name = "pluggy" 191 | version = "1.4.0" 192 | description = "plugin and hook calling mechanisms for python" 193 | optional = false 194 | python-versions = ">=3.8" 195 | files = [ 196 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 197 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 198 | ] 199 | 200 | [package.extras] 201 | dev = ["pre-commit", "tox"] 202 | testing = ["pytest", "pytest-benchmark"] 203 | 204 | [[package]] 205 | name = "pre-commit" 206 | version = "3.6.2" 207 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 208 | optional = false 209 | python-versions = ">=3.9" 210 | files = [ 211 | {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, 212 | {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, 213 | ] 214 | 215 | [package.dependencies] 216 | cfgv = ">=2.0.0" 217 | identify = ">=1.0.0" 218 | nodeenv = ">=0.11.1" 219 | pyyaml = ">=5.1" 220 | virtualenv = ">=20.10.0" 221 | 222 | [[package]] 223 | name = "pytest" 224 | version = "8.1.1" 225 | description = "pytest: simple powerful testing with Python" 226 | optional = false 227 | python-versions = ">=3.8" 228 | files = [ 229 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 230 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 231 | ] 232 | 233 | [package.dependencies] 234 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 235 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 236 | iniconfig = "*" 237 | packaging = "*" 238 | pluggy = ">=1.4,<2.0" 239 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 240 | 241 | [package.extras] 242 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 243 | 244 | [[package]] 245 | name = "pyyaml" 246 | version = "6.0.1" 247 | description = "YAML parser and emitter for Python" 248 | optional = false 249 | python-versions = ">=3.6" 250 | files = [ 251 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 252 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 253 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 254 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 255 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 256 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 257 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 258 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 259 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 260 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 261 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 262 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 263 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 264 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 265 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 266 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 267 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 268 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 269 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 270 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 271 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 272 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 273 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 274 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 275 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 276 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 277 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 278 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 279 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 280 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 281 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 282 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 283 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 284 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 285 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 286 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 287 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 288 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 289 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 290 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 291 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 292 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 293 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 294 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 295 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 296 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 297 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 298 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 299 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 300 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 301 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 302 | ] 303 | 304 | [[package]] 305 | name = "ruff" 306 | version = "0.3.3" 307 | description = "An extremely fast Python linter and code formatter, written in Rust." 308 | optional = false 309 | python-versions = ">=3.7" 310 | files = [ 311 | {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, 312 | {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, 313 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, 314 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, 315 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, 316 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, 317 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, 318 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, 319 | {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, 320 | {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, 321 | {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, 322 | {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, 323 | {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, 324 | {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, 325 | {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, 326 | {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, 327 | {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, 328 | ] 329 | 330 | [[package]] 331 | name = "setuptools" 332 | version = "69.2.0" 333 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 334 | optional = false 335 | python-versions = ">=3.8" 336 | files = [ 337 | {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, 338 | {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, 339 | ] 340 | 341 | [package.extras] 342 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 343 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 344 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 345 | 346 | [[package]] 347 | name = "tomli" 348 | version = "2.0.1" 349 | description = "A lil' TOML parser" 350 | optional = false 351 | python-versions = ">=3.7" 352 | files = [ 353 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 354 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 355 | ] 356 | 357 | [[package]] 358 | name = "typing-extensions" 359 | version = "4.10.0" 360 | description = "Backported and Experimental Type Hints for Python 3.8+" 361 | optional = false 362 | python-versions = ">=3.8" 363 | files = [ 364 | {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, 365 | {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, 366 | ] 367 | 368 | [[package]] 369 | name = "virtualenv" 370 | version = "20.25.1" 371 | description = "Virtual Python Environment builder" 372 | optional = false 373 | python-versions = ">=3.7" 374 | files = [ 375 | {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, 376 | {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, 377 | ] 378 | 379 | [package.dependencies] 380 | distlib = ">=0.3.7,<1" 381 | filelock = ">=3.12.2,<4" 382 | platformdirs = ">=3.9.1,<5" 383 | 384 | [package.extras] 385 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 386 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 387 | 388 | [metadata] 389 | lock-version = "2.0" 390 | python-versions = ">=3.5" 391 | content-hash = "7435920f637c35308cb14ded4e08a2a02df73b0a000069b6871cf955c2850973" 392 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tkup" 3 | version = "1.2.0" 4 | description = "Hierarchical Tkinter wrapper" 5 | license = "MIT" 6 | authors = ["Matthew T. Kennerly "] 7 | readme = "README.md" 8 | repository = "https://github.com/mtkennerly/tkup" 9 | keywords = ["gui", "wrapper", "tkinter"] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: End Users/Desktop", 13 | "License :: OSI Approved :: MIT License", 14 | "Natural Language :: English", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3", 17 | "Topic :: Software Development :: User Interfaces" 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = ">=3.5" 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | pytest = {version = "^8.1.1", python = ">=3.8"} 25 | mypy = {version = "^1.9.0", python = ">=3.8"} 26 | ruff = {version = "^0.3.3", python = ">=3.8"} 27 | pre-commit = {version = "^3.6.2", python = ">=3.9"} 28 | 29 | [[tool.mypy.overrides]] 30 | module = "tkup._widgets" 31 | ignore_errors = true 32 | 33 | [tool.ruff] 34 | line-length = 100 35 | lint.extend-select = ["W605", "N"] 36 | target-version = "py38" 37 | 38 | [build-system] 39 | requires = ["poetry-core>=1.0.0"] 40 | build-backend = "poetry.core.masonry.api" 41 | -------------------------------------------------------------------------------- /tests/test_gui.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | from tkup import GUI 7 | import tkup._widgets as tw 8 | 9 | 10 | @pytest.fixture 11 | def app() -> GUI: 12 | return GUI() 13 | 14 | 15 | class TestGUI: 16 | def test_it(self, app: GUI): 17 | with pytest.raises(ValueError): 18 | app.it() 19 | 20 | with app.root() as root: 21 | assert app.it() is root 22 | with app.button() as button: 23 | assert app.it() is button 24 | 25 | def test_with_it(self, app: GUI): 26 | app, it = app.with_it() 27 | 28 | with pytest.raises(ValueError): 29 | it() 30 | 31 | with app.root() as root: 32 | assert it() is root 33 | with app.button() as button: 34 | assert it() is button 35 | 36 | def test_run(self, app: GUI): 37 | mocked_root = mock.MagicMock() 38 | app._root = mocked_root 39 | app.run() 40 | mocked_root.mainloop.assert_called_once_with() 41 | 42 | def test_widget(self, app: GUI): 43 | with app.root(): 44 | with app.widget(tk.Button) as button: 45 | assert isinstance(button, tk.Button) 46 | 47 | def test_button(self, app: GUI): 48 | with app.root(): 49 | with app.button() as w: 50 | assert isinstance(w, tw.Button) 51 | 52 | def test_canvas(self, app: GUI): 53 | with app.root(): 54 | with app.canvas() as w: 55 | assert isinstance(w, tw.Canvas) 56 | 57 | def test_checkbutton(self, app: GUI): 58 | with app.root(): 59 | with app.checkbutton() as w: 60 | assert isinstance(w, tw.Checkbutton) 61 | 62 | def test_combobox(self, app: GUI): 63 | with app.root(): 64 | with app.combobox() as w: 65 | assert isinstance(w, tw.Combobox) 66 | 67 | def test_entry(self, app: GUI): 68 | with app.root(): 69 | with app.entry() as w: 70 | assert isinstance(w, tw.Entry) 71 | 72 | def test_frame(self, app: GUI): 73 | with app.root(): 74 | with app.frame() as w: 75 | assert isinstance(w, tw.Frame) 76 | 77 | def test_label(self, app: GUI): 78 | with app.root(): 79 | with app.label() as w: 80 | assert isinstance(w, tw.Label) 81 | 82 | def test_label_frame(self, app: GUI): 83 | with app.root(): 84 | with app.label_frame() as w: 85 | assert isinstance(w, tw.LabelFrame) 86 | 87 | def test_labeled_scale(self, app: GUI): 88 | with app.root(): 89 | with app.labeled_scale() as w: 90 | assert isinstance(w, tw.LabeledScale) 91 | 92 | def test_listbox(self, app: GUI): 93 | with app.root(): 94 | with app.listbox() as w: 95 | assert isinstance(w, tw.Listbox) 96 | 97 | def test_menu(self, app: GUI): 98 | with app.root(): 99 | with app.menu() as w: 100 | assert isinstance(w, tw.Menu) 101 | 102 | def test_menubutton(self, app: GUI): 103 | with app.root(): 104 | with app.menubutton() as w: 105 | assert isinstance(w, tw.Menubutton) 106 | 107 | def test_message(self, app: GUI): 108 | with app.root(): 109 | with app.message() as w: 110 | assert isinstance(w, tw.Message) 111 | 112 | def test_notebook(self, app: GUI): 113 | with app.root(): 114 | with app.notebook() as w: 115 | assert isinstance(w, tw.Notebook) 116 | 117 | def test_option_menu(self, app: GUI): 118 | with app.root(): 119 | with app.option_menu(tk.StringVar()) as w: 120 | assert isinstance(w, tw.OptionMenu) 121 | 122 | def test_paned_window(self, app: GUI): 123 | with app.root(): 124 | with app.paned_window() as w: 125 | assert isinstance(w, tw.PanedWindow) 126 | 127 | def test_progressbar(self, app: GUI): 128 | with app.root(): 129 | with app.progressbar() as w: 130 | assert isinstance(w, tw.Progressbar) 131 | 132 | def test_radiobutton(self, app: GUI): 133 | with app.root(): 134 | with app.radiobutton() as w: 135 | assert isinstance(w, tw.Radiobutton) 136 | 137 | def test_root(self, app: GUI): 138 | with app.root(): 139 | with app.root() as w: 140 | assert isinstance(w, tk.Tk) 141 | 142 | def test_scale(self, app: GUI): 143 | with app.root(): 144 | with app.scale() as w: 145 | assert isinstance(w, tw.Scale) 146 | 147 | def test_scrollbar(self, app: GUI): 148 | with app.root(): 149 | with app.scrollbar() as w: 150 | assert isinstance(w, tw.Scrollbar) 151 | 152 | def test_separator(self, app: GUI): 153 | with app.root(): 154 | with app.separator() as w: 155 | assert isinstance(w, tw.Separator) 156 | 157 | def test_sizegrip(self, app: GUI): 158 | with app.root(): 159 | with app.sizegrip() as w: 160 | assert isinstance(w, tw.Sizegrip) 161 | 162 | def test_spinbox(self, app: GUI): 163 | with app.root(): 164 | with app.spinbox() as w: 165 | assert isinstance(w, tw.Spinbox) 166 | 167 | def test_text(self, app: GUI): 168 | with app.root(): 169 | with app.text() as w: 170 | assert isinstance(w, tw.Text) 171 | 172 | def test_treeview(self, app: GUI): 173 | with app.root(): 174 | with app.treeview() as w: 175 | assert isinstance(w, tw.Treeview) 176 | -------------------------------------------------------------------------------- /tkup/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["AnyTK", "GUI"] 2 | 3 | from tkup.any_tk import AnyTK 4 | from tkup.gui import GUI 5 | -------------------------------------------------------------------------------- /tkup/_widgets.py: -------------------------------------------------------------------------------- 1 | from tkinter import * # noqa: F401, F403 2 | from tkinter.ttk import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /tkup/any_tk.py: -------------------------------------------------------------------------------- 1 | __all__ = ["AnyTK"] 2 | 3 | from typing import Union 4 | import tkinter as tk 5 | 6 | import tkup._widgets as tw 7 | 8 | #: Union of Tkinter root and all wrapped widgets for type checking. 9 | AnyTK = Union[ 10 | tw.Button, 11 | tw.Canvas, 12 | tw.Checkbutton, 13 | tw.Combobox, 14 | tw.Entry, 15 | tw.Frame, 16 | tw.Label, 17 | tw.LabelFrame, 18 | tw.LabeledScale, 19 | tw.Listbox, 20 | tw.Menu, 21 | tw.Menubutton, 22 | tw.Message, 23 | tw.Notebook, 24 | tw.OptionMenu, 25 | tw.PanedWindow, 26 | tw.Progressbar, 27 | tw.Radiobutton, 28 | tw.Scale, 29 | tw.Scrollbar, 30 | tw.Separator, 31 | tw.Sizegrip, 32 | tw.Spinbox, 33 | tw.Text, 34 | tk.Tk, 35 | tw.Treeview, 36 | tk.Widget, 37 | ] 38 | -------------------------------------------------------------------------------- /tkup/gui.py: -------------------------------------------------------------------------------- 1 | __all__ = ["GUI"] 2 | 3 | from contextlib import contextmanager 4 | from typing import Callable, Iterator, Tuple, Type, TypeVar 5 | from typing import MutableSequence # noqa: F401 6 | import tkinter as tk 7 | 8 | from tkup import AnyTK 9 | import tkup._widgets as tw 10 | 11 | T = TypeVar("T", bound=tk.Widget) 12 | 13 | 14 | class GUI: 15 | def __init__(self, *args, **kwargs) -> None: 16 | """ 17 | :param args: Passed to the Tk constructor. 18 | :param kwargs: Passed to the Tk constructor. 19 | """ 20 | 21 | self._root = tk.Tk(*args, **kwargs) 22 | self._chain = [] # type: MutableSequence[AnyTK] 23 | 24 | def it(self) -> AnyTK: 25 | """ 26 | Get the master widget for the current nesting level. 27 | 28 | :return: Current master widget. 29 | """ 30 | 31 | if not self._chain: 32 | raise ValueError("No master widget available yet.") 33 | 34 | return self._chain[-1] 35 | 36 | def with_it(self) -> Tuple["GUI", Callable[[], AnyTK]]: 37 | """ 38 | Get the class instance and its it() for quickly assigning to variables. 39 | 40 | :return: Class instance and its it() method. 41 | """ 42 | 43 | return self, self.it 44 | 45 | def run(self) -> None: 46 | """ 47 | Start the main loop. 48 | """ 49 | 50 | self._root.mainloop() 51 | 52 | @contextmanager 53 | def widget(self, kind: Type[T], *args, **kwargs) -> Iterator[T]: 54 | """ 55 | Make a widget. 56 | 57 | :param kind: Type of widget to make. It must accept its master widget 58 | as the first positional argument. 59 | :param args: Passed to constructor. 60 | :param kwargs: Passed to constructor. 61 | :return: Widget. 62 | """ 63 | 64 | w = kind(self.it(), *args, **kwargs) 65 | self._chain.append(w) 66 | yield w 67 | self._chain.pop() 68 | 69 | @contextmanager 70 | def button(self, *args, **kwargs) -> Iterator[tw.Button]: 71 | """ 72 | Make a button. 73 | 74 | :param args: Passed to constructor. 75 | :param kwargs: Passed to constructor. 76 | :return: Widget. 77 | """ 78 | 79 | with self.widget(tw.Button, *args, **kwargs) as w: 80 | yield w 81 | 82 | @contextmanager 83 | def canvas(self, *args, **kwargs) -> Iterator[tw.Canvas]: 84 | """ 85 | Make a canvas. 86 | 87 | :param args: Passed to constructor. 88 | :param kwargs: Passed to constructor. 89 | :return: Widget. 90 | """ 91 | 92 | with self.widget(tw.Canvas, *args, **kwargs) as w: 93 | yield w 94 | 95 | @contextmanager 96 | def checkbutton(self, *args, **kwargs) -> Iterator[tw.Checkbutton]: 97 | """ 98 | Make a checkbutton. 99 | 100 | :param args: Passed to constructor. 101 | :param kwargs: Passed to constructor. 102 | :return: Widget. 103 | """ 104 | 105 | with self.widget(tw.Checkbutton, *args, **kwargs) as w: 106 | yield w 107 | 108 | @contextmanager 109 | def combobox(self, *args, **kwargs) -> Iterator[tw.Combobox]: 110 | """ 111 | Make a combobox. 112 | 113 | :param args: Passed to constructor. 114 | :param kwargs: Passed to constructor. 115 | :return: Widget. 116 | """ 117 | 118 | with self.widget(tw.Combobox, *args, **kwargs) as w: 119 | yield w 120 | 121 | @contextmanager 122 | def entry(self, *args, **kwargs) -> Iterator[tw.Entry]: 123 | """ 124 | Make an entry. 125 | 126 | :param args: Passed to constructor. 127 | :param kwargs: Passed to constructor. 128 | :return: Widget. 129 | """ 130 | 131 | with self.widget(tw.Entry, *args, **kwargs) as w: 132 | yield w 133 | 134 | @contextmanager 135 | def frame(self, *args, **kwargs) -> Iterator[tw.Frame]: 136 | """ 137 | Make a frame. 138 | 139 | :param args: Passed to constructor. 140 | :param kwargs: Passed to constructor. 141 | :return: Widget. 142 | """ 143 | 144 | with self.widget(tw.Frame, *args, **kwargs) as w: 145 | yield w 146 | 147 | @contextmanager 148 | def label(self, *args, **kwargs) -> Iterator[tw.Label]: 149 | """ 150 | Make a label. 151 | 152 | :param args: Passed to constructor. 153 | :param kwargs: Passed to constructor. 154 | :return: Widget. 155 | """ 156 | 157 | with self.widget(tw.Label, *args, **kwargs) as w: 158 | yield w 159 | 160 | @contextmanager 161 | def label_frame(self, *args, **kwargs) -> Iterator[tw.LabelFrame]: 162 | """ 163 | Make a label frame. 164 | 165 | :param args: Passed to constructor. 166 | :param kwargs: Passed to constructor. 167 | :return: Widget. 168 | """ 169 | 170 | with self.widget(tw.LabelFrame, *args, **kwargs) as w: 171 | yield w 172 | 173 | @contextmanager 174 | def labeled_scale(self, *args, **kwargs) -> Iterator[tw.LabeledScale]: 175 | """ 176 | Make a labeled scale. 177 | 178 | :param args: Passed to constructor. 179 | :param kwargs: Passed to constructor. 180 | :return: Widget. 181 | """ 182 | 183 | with self.widget(tw.LabeledScale, *args, **kwargs) as w: 184 | yield w 185 | 186 | @contextmanager 187 | def listbox(self, *args, **kwargs) -> Iterator[tw.Listbox]: 188 | """ 189 | Make a listbox. 190 | 191 | :param args: Passed to constructor. 192 | :param kwargs: Passed to constructor. 193 | :return: Widget. 194 | """ 195 | 196 | with self.widget(tw.Listbox, *args, **kwargs) as w: 197 | yield w 198 | 199 | @contextmanager 200 | def menu(self, *args, **kwargs) -> Iterator[tw.Menu]: 201 | """ 202 | Make a menu. 203 | 204 | :param args: Passed to constructor. 205 | :param kwargs: Passed to constructor. 206 | :return: Widget. 207 | """ 208 | 209 | with self.widget(tw.Menu, *args, **kwargs) as w: 210 | yield w 211 | 212 | @contextmanager 213 | def menubutton(self, *args, **kwargs) -> Iterator[tw.Menubutton]: 214 | """ 215 | Make a menubutton. 216 | 217 | :param args: Passed to constructor. 218 | :param kwargs: Passed to constructor. 219 | :return: Widget. 220 | """ 221 | 222 | with self.widget(tw.Menubutton, *args, **kwargs) as w: 223 | yield w 224 | 225 | @contextmanager 226 | def message(self, *args, **kwargs) -> Iterator[tw.Message]: 227 | """ 228 | Make a message. 229 | 230 | :param args: Passed to constructor. 231 | :param kwargs: Passed to constructor. 232 | :return: Widget. 233 | """ 234 | 235 | with self.widget(tw.Message, *args, **kwargs) as w: 236 | yield w 237 | 238 | @contextmanager 239 | def notebook(self, *args, **kwargs) -> Iterator[tw.Notebook]: 240 | """ 241 | Make a notebook. 242 | 243 | :param args: Passed to constructor. 244 | :param kwargs: Passed to constructor. 245 | :return: Widget. 246 | """ 247 | 248 | with self.widget(tw.Notebook, *args, **kwargs) as w: 249 | yield w 250 | 251 | @contextmanager 252 | def option_menu(self, *args, **kwargs) -> Iterator[tw.OptionMenu]: 253 | """ 254 | Make an option menu. 255 | 256 | :param args: Passed to constructor. 257 | :param kwargs: Passed to constructor. 258 | :return: Widget. 259 | """ 260 | 261 | with self.widget(tw.OptionMenu, *args, **kwargs) as w: 262 | yield w 263 | 264 | @contextmanager 265 | def paned_window(self, *args, **kwargs) -> Iterator[tw.PanedWindow]: 266 | """ 267 | Make a paned window. 268 | 269 | :param args: Passed to constructor. 270 | :param kwargs: Passed to constructor. 271 | :return: Widget. 272 | """ 273 | 274 | with self.widget(tw.PanedWindow, *args, **kwargs) as w: 275 | yield w 276 | 277 | @contextmanager 278 | def progressbar(self, *args, **kwargs) -> Iterator[tw.Progressbar]: 279 | """ 280 | Make a progressbar. 281 | 282 | :param args: Passed to constructor. 283 | :param kwargs: Passed to constructor. 284 | :return: Widget. 285 | """ 286 | 287 | with self.widget(tw.Progressbar, *args, **kwargs) as w: 288 | yield w 289 | 290 | @contextmanager 291 | def radiobutton(self, *args, **kwargs) -> Iterator[tw.Radiobutton]: 292 | """ 293 | Make a radiobutton. 294 | 295 | :param args: Passed to constructor. 296 | :param kwargs: Passed to constructor. 297 | :return: Widget. 298 | """ 299 | 300 | with self.widget(tw.Radiobutton, *args, **kwargs) as w: 301 | yield w 302 | 303 | @contextmanager 304 | def root(self) -> Iterator[tk.Tk]: 305 | """ 306 | Yield the root instance created during class instantiation. 307 | 308 | :return: Tk root. 309 | """ 310 | 311 | self._chain.append(self._root) 312 | yield self._root 313 | self._chain.pop() 314 | 315 | @contextmanager 316 | def scale(self, *args, **kwargs) -> Iterator[tw.Scale]: 317 | """ 318 | Make a scale. 319 | 320 | :param args: Passed to constructor. 321 | :param kwargs: Passed to constructor. 322 | :return: Widget. 323 | """ 324 | 325 | with self.widget(tw.Scale, *args, **kwargs) as w: 326 | yield w 327 | 328 | @contextmanager 329 | def scrollbar(self, *args, **kwargs) -> Iterator[tw.Scrollbar]: 330 | """ 331 | Make a scrollbar. 332 | 333 | :param args: Passed to constructor. 334 | :param kwargs: Passed to constructor. 335 | :return: Widget. 336 | """ 337 | 338 | with self.widget(tw.Scrollbar, *args, **kwargs) as w: 339 | yield w 340 | 341 | @contextmanager 342 | def separator(self, *args, **kwargs) -> Iterator[tw.Separator]: 343 | """ 344 | Make a separator. 345 | 346 | :param args: Passed to constructor. 347 | :param kwargs: Passed to constructor. 348 | :return: Widget. 349 | """ 350 | 351 | with self.widget(tw.Separator, *args, **kwargs) as w: 352 | yield w 353 | 354 | @contextmanager 355 | def sizegrip(self, *args, **kwargs) -> Iterator[tw.Sizegrip]: 356 | """ 357 | Make a sizegrip. 358 | 359 | :param args: Passed to constructor. 360 | :param kwargs: Passed to constructor. 361 | :return: Widget. 362 | """ 363 | 364 | with self.widget(tw.Sizegrip, *args, **kwargs) as w: 365 | yield w 366 | 367 | @contextmanager 368 | def spinbox(self, *args, **kwargs) -> Iterator[tw.Spinbox]: 369 | """ 370 | Make a spinbox. 371 | 372 | :param args: Passed to constructor. 373 | :param kwargs: Passed to constructor. 374 | :return: Widget. 375 | """ 376 | 377 | with self.widget(tw.Spinbox, *args, **kwargs) as w: 378 | yield w 379 | 380 | @contextmanager 381 | def text(self, *args, **kwargs) -> Iterator[tw.Text]: 382 | """ 383 | Make a text. 384 | 385 | :param args: Passed to constructor. 386 | :param kwargs: Passed to constructor. 387 | :return: Widget. 388 | """ 389 | 390 | with self.widget(tw.Text, *args, **kwargs) as w: 391 | yield w 392 | 393 | @contextmanager 394 | def treeview(self, *args, **kwargs) -> Iterator[tw.Treeview]: 395 | """ 396 | Make a treeview. 397 | 398 | :param args: Passed to constructor. 399 | :param kwargs: Passed to constructor. 400 | :return: Widget. 401 | """ 402 | 403 | with self.widget(tw.Treeview, *args, **kwargs) as w: 404 | yield w 405 | --------------------------------------------------------------------------------